From 4046d82530e20586cd031d96870b1488c807809b Mon Sep 17 00:00:00 2001 From: Jude Niroshan Date: Thu, 4 Apr 2024 12:28:44 +0200 Subject: [PATCH] feat: add java spotless code formatter --- license-header | 15 + pom.xml | 35 + src/main/java/com/redhat/exhort/Api.java | 160 +- src/main/java/com/redhat/exhort/Provider.java | 76 +- .../com/redhat/exhort/api/PackageRef.java | 255 +- .../serialization/PackageRefDeserializer.java | 4 +- .../serialization/PackageURLSerializer.java | 25 +- .../PackageNotInstalledException.java | 6 +- .../java/com/redhat/exhort/image/Image.java | 576 +-- .../com/redhat/exhort/image/ImageRef.java | 256 +- .../com/redhat/exhort/image/ImageUtils.java | 780 ++-- .../com/redhat/exhort/image/Platform.java | 236 +- .../com/redhat/exhort/impl/ExhortApi.java | 1077 ++--- .../redhat/exhort/impl/RequestManager.java | 52 +- .../logging/ClientTraceIdSimpleFormatter.java | 185 +- .../redhat/exhort/logging/LoggersFactory.java | 20 +- .../exhort/providers/BaseJavaProvider.java | 333 +- .../exhort/providers/GoModulesProvider.java | 877 ++-- .../exhort/providers/GradleProvider.java | 573 +-- .../exhort/providers/JavaMavenProvider.java | 645 +-- .../providers/JavaScriptNpmProvider.java | 344 +- .../exhort/providers/PythonPipProvider.java | 406 +- .../com/redhat/exhort/sbom/CycloneDXSbom.java | 299 +- .../java/com/redhat/exhort/sbom/Sbom.java | 29 +- .../com/redhat/exhort/sbom/SbomFactory.java | 7 +- .../com/redhat/exhort/tools/Ecosystem.java | 98 +- .../com/redhat/exhort/tools/Operations.java | 379 +- .../com/redhat/exhort/tools/package-info.java | 2 +- .../exhort/utils/PythonControllerBase.java | 752 ++-- .../exhort/utils/PythonControllerRealEnv.java | 84 +- .../exhort/utils/PythonControllerTestEnv.java | 42 +- .../utils/PythonControllerVirtualEnv.java | 139 +- .../exhort/utils/StringInsensitive.java | 52 +- .../vcs/GitVersionControlSystemImpl.java | 210 +- .../java/com/redhat/exhort/vcs/TagInfo.java | 56 +- .../exhort/vcs/VersionControlSystem.java | 53 +- src/main/java/module-info.java | 53 +- .../java/com/redhat/exhort/ExhortTest.java | 98 +- .../com/redhat/exhort/image/ImageRefTest.java | 108 +- .../com/redhat/exhort/image/ImageTest.java | 901 ++-- .../redhat/exhort/image/ImageUtilsTest.java | 1650 +++---- .../com/redhat/exhort/image/PlatformTest.java | 248 +- .../com/redhat/exhort/impl/ExhortApiIT.java | 498 ++- .../redhat/exhort/impl/Exhort_Api_Test.java | 1312 +++--- .../GoModulesMainModuleVersionTest.java | 136 +- .../Golang_Modules_Provider_Test.java | 268 +- .../providers/Gradle_Provider_Test.java | 314 +- .../exhort/providers/HelperExtension.java | 75 +- .../exhort/providers/Java_Envs_Test.java | 45 +- .../providers/Java_Maven_Provider_Test.java | 281 +- .../providers/Javascript_Envs_Test.java | 67 +- .../Javascript_Npm_Provider_Test.java | 269 +- .../providers/PythonEnvironmentExtension.java | 155 +- .../providers/Python_Provider_Test.java | 281 +- .../redhat/exhort/tools/Ecosystem_Test.java | 27 +- .../redhat/exhort/tools/OperationsTest.java | 48 +- .../redhat/exhort/tools/Operations_Test.java | 65 +- .../utils/PythonControllerBaseTest.java | 3927 ++++++++--------- .../utils/PythonControllerRealEnvTest.java | 515 +-- .../utils/PythonControllerVirtualEnvTest.java | 144 +- 60 files changed, 10447 insertions(+), 10176 deletions(-) create mode 100644 license-header diff --git a/license-header b/license-header new file mode 100644 index 00000000..7fdff0ae --- /dev/null +++ b/license-header @@ -0,0 +1,15 @@ +/* + * Copyright © 2023 Red Hat, Inc. + * + * 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. + */ diff --git a/pom.xml b/pom.xml index 6d015eb8..c90495d4 100644 --- a/pom.xml +++ b/pom.xml @@ -685,6 +685,41 @@ limitations under the License.]]> + + + com.diffplug.spotless + spotless-maven-plugin + + + + src/* + + + + true + 4 + + + + + + 2.39.0 + + + + ${project.basedir}/license-header --> + + + + + + + check + + process-sources + + + diff --git a/src/main/java/com/redhat/exhort/Api.java b/src/main/java/com/redhat/exhort/Api.java index 3b6bc136..e0e332cb 100644 --- a/src/main/java/com/redhat/exhort/Api.java +++ b/src/main/java/com/redhat/exhort/Api.java @@ -15,6 +15,8 @@ */ package com.redhat.exhort; +import com.redhat.exhort.api.AnalysisReport; +import com.redhat.exhort.image.ImageRef; import java.io.IOException; import java.util.Arrays; import java.util.Map; @@ -22,93 +24,91 @@ import java.util.Set; import java.util.concurrent.CompletableFuture; -import com.redhat.exhort.api.AnalysisReport; -import com.redhat.exhort.image.ImageRef; - /** The Api interface is used for contracting API implementations. **/ public interface Api { - public static final String CYCLONEDX_MEDIA_TYPE = "application/vnd.cyclonedx+json"; + public static final String CYCLONEDX_MEDIA_TYPE = "application/vnd.cyclonedx+json"; - enum MediaType { - APPLICATION_JSON, - TEXT_HTML, - MULTIPART_MIXED; + enum MediaType { + APPLICATION_JSON, + TEXT_HTML, + MULTIPART_MIXED; - @Override - public String toString() { - return this.name().toLowerCase().replace("_", "/"); + @Override + public String toString() { + return this.name().toLowerCase().replace("_", "/"); + } } - } - /** POJO class used for aggregating multipart/mixed analysis requests. */ - class MixedReport { - final public byte[] html; - final public AnalysisReport json; - - public MixedReport(final byte[] html, final AnalysisReport json) { - this.html = html; - this.json = json; - } - public MixedReport() - { - this.html = new byte[0]; - this.json = new AnalysisReport(); - } - @Override - public boolean equals(final Object o) { - if (this == o) return true; - if (o == null || this.getClass() != o.getClass()) return false; - var that = (MixedReport) o; - return Arrays.equals(this.html, that.html) && Objects.equals(this.json, that.json); + /** POJO class used for aggregating multipart/mixed analysis requests. */ + class MixedReport { + public final byte[] html; + public final AnalysisReport json; + + public MixedReport(final byte[] html, final AnalysisReport json) { + this.html = html; + this.json = json; + } + + public MixedReport() { + this.html = new byte[0]; + this.json = new AnalysisReport(); + } + + @Override + public boolean equals(final Object o) { + if (this == o) return true; + if (o == null || this.getClass() != o.getClass()) return false; + var that = (MixedReport) o; + return Arrays.equals(this.html, that.html) && Objects.equals(this.json, that.json); + } + + @Override + public int hashCode() { + return 31 * Objects.hash(json) + Arrays.hashCode(html); + } } - @Override - public int hashCode() { - return 31 * Objects.hash(json) + Arrays.hashCode(html); - } - } - - /** - * Use for creating a stack analysis HTML report for a given manifest file. - * - * @param manifestFile the path for the manifest file - * @return a mixed reports for both HTML and JSON wrapped in a CompletableFuture - * @throws IOException when failed to load the manifest file - */ - CompletableFuture stackAnalysisMixed(String manifestFile) throws IOException; - - /** - * Use for creating a stack analysis HTML report for a given manifest file. - * - * @param manifestFile the path for the manifest file - * @return the HTML report as a String wrapped in a CompletableFuture - * @throws IOException when failed to load the manifest file - */ - CompletableFuture stackAnalysisHtml(String manifestFile) throws IOException; - - /** - * Use for creating a stack analysis deserialized Json report for a given manifest file. - * - * @param manifestFile the path for the manifest file - * @return the deserialized Json report as an AnalysisReport wrapped in a CompletableFuture - * @throws IOException when failed to load the manifest file - */ - CompletableFuture stackAnalysis(String manifestFile) throws IOException; - - /** - * Use for creating a component analysis deserialized Json report for a given type and content. - * - * @param manifestType the type of the manifest, i.e. {@code pom.xml} - * @param manifestContent a byte array of the manifest's content - * @return the deserialized Json report as an AnalysisReport wrapped in a CompletableFuture - * @throws IOException when failed to load the manifest content - */ - CompletableFuture componentAnalysis(String manifestType, byte[] manifestContent) throws IOException; - - CompletableFuture componentAnalysis(String manifestFile) throws IOException; - - CompletableFuture> imageAnalysis(Set imageRefs) throws IOException; - - CompletableFuture imageAnalysisHtml(Set imageRefs) throws IOException; + /** + * Use for creating a stack analysis HTML report for a given manifest file. + * + * @param manifestFile the path for the manifest file + * @return a mixed reports for both HTML and JSON wrapped in a CompletableFuture + * @throws IOException when failed to load the manifest file + */ + CompletableFuture stackAnalysisMixed(String manifestFile) throws IOException; + + /** + * Use for creating a stack analysis HTML report for a given manifest file. + * + * @param manifestFile the path for the manifest file + * @return the HTML report as a String wrapped in a CompletableFuture + * @throws IOException when failed to load the manifest file + */ + CompletableFuture stackAnalysisHtml(String manifestFile) throws IOException; + + /** + * Use for creating a stack analysis deserialized Json report for a given manifest file. + * + * @param manifestFile the path for the manifest file + * @return the deserialized Json report as an AnalysisReport wrapped in a CompletableFuture + * @throws IOException when failed to load the manifest file + */ + CompletableFuture stackAnalysis(String manifestFile) throws IOException; + + /** + * Use for creating a component analysis deserialized Json report for a given type and content. + * + * @param manifestType the type of the manifest, i.e. {@code pom.xml} + * @param manifestContent a byte array of the manifest's content + * @return the deserialized Json report as an AnalysisReport wrapped in a CompletableFuture + * @throws IOException when failed to load the manifest content + */ + CompletableFuture componentAnalysis(String manifestType, byte[] manifestContent) throws IOException; + + CompletableFuture componentAnalysis(String manifestFile) throws IOException; + + CompletableFuture> imageAnalysis(Set imageRefs) throws IOException; + + CompletableFuture imageAnalysisHtml(Set imageRefs) throws IOException; } diff --git a/src/main/java/com/redhat/exhort/Provider.java b/src/main/java/com/redhat/exhort/Provider.java index 1840f58e..c66ae65e 100644 --- a/src/main/java/com/redhat/exhort/Provider.java +++ b/src/main/java/com/redhat/exhort/Provider.java @@ -15,54 +15,56 @@ */ package com.redhat.exhort; -import java.io.IOException; -import java.nio.file.Path; - import com.fasterxml.jackson.databind.ObjectMapper; import com.redhat.exhort.tools.Ecosystem; +import java.io.IOException; +import java.nio.file.Path; /** * The Provider abstraction is used for contracting providers providing a {@link Content} * per manifest type for constructing backend requests. **/ public abstract class Provider { - /** - * Content is used to aggregate a content buffer and a content type. - * These will be used to construct the backend API request. - **/ - public static class Content { - public final byte[] buffer; - public final String type; - public Content(byte[] buffer, String type){ - this.buffer = buffer; - this.type = type; + /** + * Content is used to aggregate a content buffer and a content type. + * These will be used to construct the backend API request. + **/ + public static class Content { + public final byte[] buffer; + public final String type; + + public Content(byte[] buffer, String type) { + this.buffer = buffer; + this.type = type; + } } - } - /** The ecosystem of this provider, i.e. maven. */ - public final Ecosystem.Type ecosystem; - protected final ObjectMapper objectMapper = new ObjectMapper(); + /** The ecosystem of this provider, i.e. maven. */ + public final Ecosystem.Type ecosystem; + + protected final ObjectMapper objectMapper = new ObjectMapper(); + + protected Provider(Ecosystem.Type ecosystem) { + this.ecosystem = ecosystem; + } - protected Provider(Ecosystem.Type ecosystem) { - this.ecosystem = ecosystem; - } + /** + * Use for providing content for a stack analysis request. + * + * @param manifestPath the Path for the manifest file + * @return A Content record aggregating the body content and content type + * @throws IOException when failed to load the manifest file + */ + public abstract Content provideStack(Path manifestPath) throws IOException; - /** - * Use for providing content for a stack analysis request. - * - * @param manifestPath the Path for the manifest file - * @return A Content record aggregating the body content and content type - * @throws IOException when failed to load the manifest file - */ - public abstract Content provideStack(Path manifestPath) throws IOException; + /** + * Use for providing content for a component analysis request. + * + * @param manifestContent the content of the manifest file + * @return A Content record aggregating the body content and content type + * @throws IOException when failed to load the manifest content + */ + public abstract Content provideComponent(byte[] manifestContent) throws IOException; - /** - * Use for providing content for a component analysis request. - * - * @param manifestContent the content of the manifest file - * @return A Content record aggregating the body content and content type - * @throws IOException when failed to load the manifest content - */ - public abstract Content provideComponent(byte[] manifestContent) throws IOException; - public abstract Content provideComponent(Path manifestPath) throws IOException; + public abstract Content provideComponent(Path manifestPath) throws IOException; } diff --git a/src/main/java/com/redhat/exhort/api/PackageRef.java b/src/main/java/com/redhat/exhort/api/PackageRef.java index 96eeb7b0..1d15dc37 100644 --- a/src/main/java/com/redhat/exhort/api/PackageRef.java +++ b/src/main/java/com/redhat/exhort/api/PackageRef.java @@ -15,166 +15,165 @@ */ package com.redhat.exhort.api; -import java.util.Objects; - import com.fasterxml.jackson.annotation.JsonValue; import com.fasterxml.jackson.databind.annotation.JsonSerialize; import com.github.packageurl.MalformedPackageURLException; import com.github.packageurl.PackageURL; import com.redhat.exhort.api.serialization.PackageURLSerializer; +import java.util.Objects; public class PackageRef { - private static final String MAVEN_TYPE = "maven"; + private static final String MAVEN_TYPE = "maven"; - @JsonSerialize(using = PackageURLSerializer.class) - @JsonValue - private final PackageURL purl; + @JsonSerialize(using = PackageURLSerializer.class) + @JsonValue + private final PackageURL purl; - public PackageRef(String purl) { - Objects.requireNonNull(purl); - try { - this.purl = new PackageURL(purl); - } catch (MalformedPackageURLException e) { - throw new IllegalArgumentException("Unable to parse PackageURL. " + e.getMessage()); + public PackageRef(String purl) { + Objects.requireNonNull(purl); + try { + this.purl = new PackageURL(purl); + } catch (MalformedPackageURLException e) { + throw new IllegalArgumentException("Unable to parse PackageURL. " + e.getMessage()); + } } - } - - public PackageRef(PackageURL purl) { - Objects.requireNonNull(purl); - this.purl = purl; - } - - public PackageURL purl() { - return purl; - } - - public String ref() { - return purl.toString(); - } - - public String name() { - if (purl.getNamespace() == null) { - return purl.getName(); - } - return purl.getNamespace() + ":" + purl.getName(); - } - - public String version() { - return purl.getVersion(); - } - - @Override - public int hashCode() { - return purl.hashCode(); - } - - @Override - public boolean equals(Object other) { - if (other == null) { - return false; + + public PackageRef(PackageURL purl) { + Objects.requireNonNull(purl); + this.purl = purl; } - if (!(other instanceof PackageRef)) { - return false; + + public PackageURL purl() { + return purl; } - return Objects.equals(purl, ((PackageRef) other).purl()); - } - public static PackageRef parse(String gav, String pkgManager) { - var parts = gav.split(":"); - if (parts.length < 4 || parts.length > 6) { - throw new IllegalArgumentException("Unexpected GAV format. " + gav); + public String ref() { + return purl.toString(); } - if (parts.length < 6) { - return builder() - .namespace(parts[0]) - .name(parts[1]) - .version(parts[3]) - .pkgManager(pkgManager) - .build(); + + public String name() { + if (purl.getNamespace() == null) { + return purl.getName(); + } + return purl.getNamespace() + ":" + purl.getName(); } - return builder() - .namespace(parts[0]) - .name(parts[1]) - .version(parts[4]) - .pkgManager(pkgManager) - .build(); - } - - /** - * Convert the instance into URL query string. - * - * @param prefix prefix of the query string - * @return URL query string - */ - public String toUrlQueryString(String prefix) { - if (prefix == null) { - prefix = ""; + + public String version() { + return purl.getVersion(); } - return String.format("%s=%s", prefix, this.toString()); - } + @Override + public int hashCode() { + return purl.hashCode(); + } - public static Builder builder() { - return new Builder(); - } + @Override + public boolean equals(Object other) { + if (other == null) { + return false; + } + if (!(other instanceof PackageRef)) { + return false; + } + return Objects.equals(purl, ((PackageRef) other).purl()); + } - public static class Builder { + public static PackageRef parse(String gav, String pkgManager) { + var parts = gav.split(":"); + if (parts.length < 4 || parts.length > 6) { + throw new IllegalArgumentException("Unexpected GAV format. " + gav); + } + if (parts.length < 6) { + return builder() + .namespace(parts[0]) + .name(parts[1]) + .version(parts[3]) + .pkgManager(pkgManager) + .build(); + } + return builder() + .namespace(parts[0]) + .name(parts[1]) + .version(parts[4]) + .pkgManager(pkgManager) + .build(); + } - String namespace; - String name; - String version; - String pkgManager; - String purl; + /** + * Convert the instance into URL query string. + * + * @param prefix prefix of the query string + * @return URL query string + */ + public String toUrlQueryString(String prefix) { + if (prefix == null) { + prefix = ""; + } - public Builder purl(String purl) { - this.purl = purl; - return this; + return String.format("%s=%s", prefix, this.toString()); } - public Builder pkgManager(String pkgManager) { - this.pkgManager = pkgManager; - return this; + public static Builder builder() { + return new Builder(); } - public Builder version(String version) { - this.version = version; - return this; - } + public static class Builder { - public Builder name(String name) { - this.name = name; - return this; - } + String namespace; + String name; + String version; + String pkgManager; + String purl; - public Builder namespace(String namespace) { - this.namespace = namespace; - return this; - } + public Builder purl(String purl) { + this.purl = purl; + return this; + } - private Builder() {} + public Builder pkgManager(String pkgManager) { + this.pkgManager = pkgManager; + return this; + } - public PackageRef build() { - try { - if (Objects.isNull(purl)) { - Objects.requireNonNull(pkgManager); - Objects.requireNonNull(name); - Objects.requireNonNull(version); - return new PackageRef(new PackageURL(pkgManager, namespace, name, version, null, null)); + public Builder version(String version) { + this.version = version; + return this; + } + + public Builder name(String name) { + this.name = name; + return this; + } + + public Builder namespace(String namespace) { + this.namespace = namespace; + return this; + } + + private Builder() {} + + public PackageRef build() { + try { + if (Objects.isNull(purl)) { + Objects.requireNonNull(pkgManager); + Objects.requireNonNull(name); + Objects.requireNonNull(version); + return new PackageRef(new PackageURL(pkgManager, namespace, name, version, null, null)); + } + return new PackageRef(new PackageURL(purl)); + } catch (MalformedPackageURLException e) { + throw new IllegalArgumentException("Unable to parse PackageURL. " + e.getMessage()); + } } - return new PackageRef(new PackageURL(purl)); - } catch (MalformedPackageURLException e) { - throw new IllegalArgumentException("Unable to parse PackageURL. " + e.getMessage()); - } } - } - public String toGav() { - return String.format("%s:%s", name(), purl.getVersion()); - } + public String toGav() { + return String.format("%s:%s", name(), purl.getVersion()); + } - @Override - public String toString() { - return purl.toString(); - } + @Override + public String toString() { + return purl.toString(); + } } diff --git a/src/main/java/com/redhat/exhort/api/serialization/PackageRefDeserializer.java b/src/main/java/com/redhat/exhort/api/serialization/PackageRefDeserializer.java index 391ebf26..e2595732 100644 --- a/src/main/java/com/redhat/exhort/api/serialization/PackageRefDeserializer.java +++ b/src/main/java/com/redhat/exhort/api/serialization/PackageRefDeserializer.java @@ -15,14 +15,13 @@ */ package com.redhat.exhort.api.serialization; -import java.io.IOException; - import com.fasterxml.jackson.core.JacksonException; import com.fasterxml.jackson.core.JsonParser; import com.fasterxml.jackson.databind.DeserializationContext; import com.fasterxml.jackson.databind.JsonNode; import com.fasterxml.jackson.databind.deser.std.StdDeserializer; import com.redhat.exhort.api.PackageRef; +import java.io.IOException; public class PackageRefDeserializer extends StdDeserializer { @@ -43,5 +42,4 @@ public PackageRef deserialize(JsonParser p, DeserializationContext ctxt) throws } return new PackageRef(purl); } - } diff --git a/src/main/java/com/redhat/exhort/api/serialization/PackageURLSerializer.java b/src/main/java/com/redhat/exhort/api/serialization/PackageURLSerializer.java index 914a2857..ea3d9b23 100644 --- a/src/main/java/com/redhat/exhort/api/serialization/PackageURLSerializer.java +++ b/src/main/java/com/redhat/exhort/api/serialization/PackageURLSerializer.java @@ -13,29 +13,26 @@ * See the License for the specific language governing permissions and * limitations under the License. */ - package com.redhat.exhort.api.serialization; -import java.io.IOException; - import com.fasterxml.jackson.core.JsonGenerator; import com.fasterxml.jackson.databind.SerializerProvider; import com.fasterxml.jackson.databind.ser.std.StdSerializer; import com.github.packageurl.PackageURL; +import java.io.IOException; public class PackageURLSerializer extends StdSerializer { - public PackageURLSerializer() { - this(null); - } + public PackageURLSerializer() { + this(null); + } - public PackageURLSerializer(Class c) { - super(c); - } + public PackageURLSerializer(Class c) { + super(c); + } - @Override - public void serialize(PackageURL value, JsonGenerator gen, SerializerProvider provider) - throws IOException { - gen.writeString(value.toString()); - } + @Override + public void serialize(PackageURL value, JsonGenerator gen, SerializerProvider provider) throws IOException { + gen.writeString(value.toString()); + } } diff --git a/src/main/java/com/redhat/exhort/exception/PackageNotInstalledException.java b/src/main/java/com/redhat/exhort/exception/PackageNotInstalledException.java index 9e9dde79..56c006a6 100644 --- a/src/main/java/com/redhat/exhort/exception/PackageNotInstalledException.java +++ b/src/main/java/com/redhat/exhort/exception/PackageNotInstalledException.java @@ -16,7 +16,7 @@ package com.redhat.exhort.exception; public class PackageNotInstalledException extends RuntimeException { - public PackageNotInstalledException(String message) { - super(message); - } + public PackageNotInstalledException(String message) { + super(message); + } } diff --git a/src/main/java/com/redhat/exhort/image/Image.java b/src/main/java/com/redhat/exhort/image/Image.java index 1eee5044..b8c8e0de 100644 --- a/src/main/java/com/redhat/exhort/image/Image.java +++ b/src/main/java/com/redhat/exhort/image/Image.java @@ -19,7 +19,6 @@ * Contents in this file are from: * https://github.com/fabric8io/docker-maven-plugin/blob/6eeb78a9b074328ef5817c1c91392d8d8350984e/src/main/java/io/fabric8/maven/docker/util/ImageName.java */ - import java.util.ArrayList; import java.util.List; import java.util.Objects; @@ -49,310 +48,311 @@ */ public class Image { - // --------------------------------------------------------------------- - // https://github.com/docker/docker/blob/04da4041757370fb6f85510c8977c5a18ddae380/vendor/github.com/docker/distribution/reference/regexp.go#L18 - private final String nameComponentRegexp = "[a-z0-9]+(?:(?:(?:[._]|__|[-]*)[a-z0-9]+)+)?"; - // https://github.com/docker/docker/blob/04da4041757370fb6f85510c8977c5a18ddae380/vendor/github.com/docker/distribution/reference/regexp.go#L25 - private final String domainComponentRegexp = "(?:[a-zA-Z0-9]|[a-zA-Z0-9][a-zA-Z0-9-]*[a-zA-Z0-9])"; - // https://github.com/docker/docker/blob/04da4041757370fb6f85510c8977c5a18ddae380/vendor/github.com/docker/distribution/reference/regexp.go#L18 - private final Pattern NAME_COMP_REGEXP = Pattern.compile(nameComponentRegexp); - // https://github.com/docker/docker/blob/04da4041757370fb6f85510c8977c5a18ddae380/vendor/github.com/docker/distribution/reference/regexp.go#L53 - private final Pattern IMAGE_NAME_REGEXP = Pattern.compile(nameComponentRegexp + "(?:(?:/" + nameComponentRegexp + ")+)?"); - // https://github.com/docker/docker/blob/04da4041757370fb6f85510c8977c5a18ddae380/vendor/github.com/docker/distribution/reference/regexp.go#L31 - private final Pattern DOMAIN_REGEXP = Pattern.compile("^" + domainComponentRegexp + "(?:\\." + domainComponentRegexp + ")*(?::[0-9]+)?$"); - // https://github.com/docker/docker/blob/04da4041757370fb6f85510c8977c5a18ddae380/vendor/github.com/docker/distribution/reference/regexp.go#L37 - private final Pattern TAG_REGEXP = Pattern.compile("^[\\w][\\w.-]{0,127}$"); - private final Pattern DIGEST_REGEXP = Pattern.compile("^sha256:[a-z0-9]{32,}$"); - // The repository part of the full image - private String repository; - // Registry - private String registry; - // Tag name - private String tag; - // Digest - private String digest; - // User name - private String user; - - /** - * Create an image name - * - * @param fullName The fullname of the image in Docker format. - */ - public Image(String fullName) { - this(fullName, null); - } - - /** - * Create an image name with a tag. If a tag is provided (i.e. is not null) then this tag is used. - * Otherwise the tag of the provided name is used (if any). - * - * @param fullName The fullname of the image in Docker format. I - * @param givenTag tag to use. Can be null in which case the tag specified in fullName is used. - */ - public Image(String fullName, String givenTag) { - if (fullName == null) { - throw new NullPointerException("Image name must not be null"); + // --------------------------------------------------------------------- + // https://github.com/docker/docker/blob/04da4041757370fb6f85510c8977c5a18ddae380/vendor/github.com/docker/distribution/reference/regexp.go#L18 + private final String nameComponentRegexp = "[a-z0-9]+(?:(?:(?:[._]|__|[-]*)[a-z0-9]+)+)?"; + // https://github.com/docker/docker/blob/04da4041757370fb6f85510c8977c5a18ddae380/vendor/github.com/docker/distribution/reference/regexp.go#L25 + private final String domainComponentRegexp = "(?:[a-zA-Z0-9]|[a-zA-Z0-9][a-zA-Z0-9-]*[a-zA-Z0-9])"; + // https://github.com/docker/docker/blob/04da4041757370fb6f85510c8977c5a18ddae380/vendor/github.com/docker/distribution/reference/regexp.go#L18 + private final Pattern NAME_COMP_REGEXP = Pattern.compile(nameComponentRegexp); + // https://github.com/docker/docker/blob/04da4041757370fb6f85510c8977c5a18ddae380/vendor/github.com/docker/distribution/reference/regexp.go#L53 + private final Pattern IMAGE_NAME_REGEXP = + Pattern.compile(nameComponentRegexp + "(?:(?:/" + nameComponentRegexp + ")+)?"); + // https://github.com/docker/docker/blob/04da4041757370fb6f85510c8977c5a18ddae380/vendor/github.com/docker/distribution/reference/regexp.go#L31 + private final Pattern DOMAIN_REGEXP = + Pattern.compile("^" + domainComponentRegexp + "(?:\\." + domainComponentRegexp + ")*(?::[0-9]+)?$"); + // https://github.com/docker/docker/blob/04da4041757370fb6f85510c8977c5a18ddae380/vendor/github.com/docker/distribution/reference/regexp.go#L37 + private final Pattern TAG_REGEXP = Pattern.compile("^[\\w][\\w.-]{0,127}$"); + private final Pattern DIGEST_REGEXP = Pattern.compile("^sha256:[a-z0-9]{32,}$"); + // The repository part of the full image + private String repository; + // Registry + private String registry; + // Tag name + private String tag; + // Digest + private String digest; + // User name + private String user; + + /** + * Create an image name + * + * @param fullName The fullname of the image in Docker format. + */ + public Image(String fullName) { + this(fullName, null); } - // set digest to null as default - digest = null; - // check if digest is part of fullName, if so -> extract it - if (fullName.contains("@sha256")) { // Of it contains digest - String[] digestParts = fullName.split("@"); - digest = digestParts[1]; - fullName = digestParts[0]; + /** + * Create an image name with a tag. If a tag is provided (i.e. is not null) then this tag is used. + * Otherwise the tag of the provided name is used (if any). + * + * @param fullName The fullname of the image in Docker format. I + * @param givenTag tag to use. Can be null in which case the tag specified in fullName is used. + */ + public Image(String fullName, String givenTag) { + if (fullName == null) { + throw new NullPointerException("Image name must not be null"); + } + + // set digest to null as default + digest = null; + // check if digest is part of fullName, if so -> extract it + if (fullName.contains("@sha256")) { // Of it contains digest + String[] digestParts = fullName.split("@"); + digest = digestParts[1]; + fullName = digestParts[0]; + } + + // check for tag + Pattern tagPattern = Pattern.compile("^(.+?)(?::([^:/]+))?$"); + Matcher matcher = tagPattern.matcher(fullName); + if (!matcher.matches()) { + throw new IllegalArgumentException(fullName + " is not a proper image name ([registry/][repo][:port]"); + } + // extract tag if it exists + tag = givenTag != null ? givenTag : matcher.group(2); + String rest = matcher.group(1); + + // extract registry, repository, user + parseComponentsBeforeTag(rest); + + /* + * set tag to latest if tag AND digest are null + * if digest is not null but tag is -> leave it! + * -> in case of "image_name@sha256" it is not required to get resolved to "latest" + */ + if (tag == null && digest == null) { + tag = "latest"; + } + + doValidate(); } - // check for tag - Pattern tagPattern = Pattern.compile("^(.+?)(?::([^:/]+))?$"); - Matcher matcher = tagPattern.matcher(fullName); - if (!matcher.matches()) { - throw new IllegalArgumentException(fullName + " is not a proper image name ([registry/][repo][:port]"); + /** + * Check whether the given name validates against the Docker rules for names + * + * @param image image name to validate + * d@throws IllegalArgumentException if the name doesnt validate + */ + public static void validate(String image) { + // Validation will be triggered during construction + new Image(image); } - // extract tag if it exists - tag = givenTag != null ? givenTag : matcher.group(2); - String rest = matcher.group(1); - // extract registry, repository, user - parseComponentsBeforeTag(rest); + public String getRepository() { + return repository; + } - /* - * set tag to latest if tag AND digest are null - * if digest is not null but tag is -> leave it! - * -> in case of "image_name@sha256" it is not required to get resolved to "latest" - */ - if (tag == null && digest == null) { - tag = "latest"; + public String getRegistry() { + return registry; + } + + public String getTag() { + return tag; + } + + public String getDigest() { + return digest; + } + + public void setDigest(String digest) { + if (this.digest == null) { + this.digest = digest; + } + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + Image image = (Image) o; + return Objects.equals(repository, image.repository) + && Objects.equals(registry, image.registry) + && Objects.equals(tag, image.tag) + && Objects.equals(digest, image.digest) + && Objects.equals(user, image.user); + } + + @Override + public int hashCode() { + return Objects.hash(repository, registry, tag, digest, user); + } + + @Override + public String toString() { + return this.getFullName(); + } + + public boolean hasRegistry() { + return registry != null && registry.length() > 0; } - doValidate(); - } - - /** - * Check whether the given name validates agains the Docker rules for names - * - * @param image image name to validate - * d@throws IllegalArgumentException if the name doesnt validate - */ - public static void validate(String image) { - // Validation will be triggered during construction - new Image(image); - } - - public String getRepository() { - return repository; - } - - public String getRegistry() { - return registry; - } - - public String getTag() { - return tag; - } - - public String getDigest() { - return digest; - } - - public void setDigest(String digest) { - if (this.digest == null) { - this.digest = digest; + private String joinTail(String[] parts) { + StringBuilder builder = new StringBuilder(); + for (int i = 1; i < parts.length; i++) { + builder.append(parts[i]); + if (i < parts.length - 1) { + builder.append("/"); + } + } + return builder.toString(); + } + + private boolean isRegistry(String part) { + return part.contains(".") || part.contains(":"); } - } - - @Override - public boolean equals(Object o) { - if (this == o) return true; - if (o == null || getClass() != o.getClass()) return false; - Image image = (Image) o; - return Objects.equals(repository, image.repository) && - Objects.equals(registry, image.registry) && - Objects.equals(tag, image.tag) && - Objects.equals(digest, image.digest) && - Objects.equals(user, image.user); - } - - @Override - public int hashCode() { - return Objects.hash(repository, registry, tag, digest, user); - } - - @Override - public String toString() { - return this.getFullName(); - } - - public boolean hasRegistry() { - return registry != null && registry.length() > 0; - } - - private String joinTail(String[] parts) { - StringBuilder builder = new StringBuilder(); - for (int i = 1; i < parts.length; i++) { - builder.append(parts[i]); - if (i < parts.length - 1) { - builder.append("/"); - } + + /** + * Get the full name of this image, including the registry but without + * any tag (e.g. privateregistry:fabric8io/java) + * + * @return full name with the original registry + */ + public String getNameWithoutTag() { + return getNameWithoutTag(null); + } + + /** + * Get the full name of this image like {@link #getNameWithoutTag()} does, but allow + * an optional registry. This registry is used when this image does not already + * contain a registry. + * + * @param optionalRegistry optional registry to use when this image does not provide + * a registry. Can be null in which case no optional registry is used* + * @return full name with original registry (if set) or optional registry (if not null) + */ + public String getNameWithoutTag(String optionalRegistry) { + StringBuilder ret = new StringBuilder(); + if (registry != null || optionalRegistry != null) { + ret.append(registry != null ? registry : optionalRegistry).append("/"); + } + ret.append(repository); + return ret.toString(); } - return builder.toString(); - } - - private boolean isRegistry(String part) { - return part.contains(".") || part.contains(":"); - } - - /** - * Get the full name of this image, including the registry but without - * any tag (e.g. privateregistry:fabric8io/java) - * - * @return full name with the original registry - */ - public String getNameWithoutTag() { - return getNameWithoutTag(null); - } - - /** - * Get the full name of this image like {@link #getNameWithoutTag()} does, but allow - * an optional registry. This registry is used when this image does not already - * contain a registry. - * - * @param optionalRegistry optional registry to use when this image does not provide - * a registry. Can be null in which case no optional registry is used* - * @return full name with original registry (if set) or optional registry (if not null) - */ - public String getNameWithoutTag(String optionalRegistry) { - StringBuilder ret = new StringBuilder(); - if (registry != null || optionalRegistry != null) { - ret.append(registry != null ? registry : optionalRegistry).append("/"); + + // ================================================================================================ + + // Validations patterns, taken directly from the docker source --> + // https://github.com/docker/docker/blob/04da4041757370fb6f85510c8977c5a18ddae380/vendor/github.com/docker/distribution/reference/regexp.go + // https://github.com/docker/docker/blob/04da4041757370fb6f85510c8977c5a18ddae380/vendor/github.com/docker/distribution/reference/reference.go + + /** + * Get the full name of this image, including the registry and tag + * (e.g. privateregistry:fabric8io/java:7u53) + * + * @return full name with the original registry and the original tag given (if any). + */ + public String getFullName() { + return getFullName(null); } - ret.append(repository); - return ret.toString(); - } - - // ================================================================================================ - - // Validations patterns, taken directly from the docker source --> - // https://github.com/docker/docker/blob/04da4041757370fb6f85510c8977c5a18ddae380/vendor/github.com/docker/distribution/reference/regexp.go - // https://github.com/docker/docker/blob/04da4041757370fb6f85510c8977c5a18ddae380/vendor/github.com/docker/distribution/reference/reference.go - - /** - * Get the full name of this image, including the registry and tag - * (e.g. privateregistry:fabric8io/java:7u53) - * - * @return full name with the original registry and the original tag given (if any). - */ - public String getFullName() { - return getFullName(null); - } - - /** - * Get the full name of this image like {@link #getFullName(String)} does, but allow - * an optional registry. This registry is used when this image does not already - * contain a registry. If no tag was provided in the initial name, latest is used. - * - * @param optionalRegistry optional registry to use when this image does not provide - * a registry. Can be null in which case no optional registry is used* - * @return full name with original registry (if set) or optional registry (if not null). - */ - public String getFullName(String optionalRegistry) { - String fullName = getNameWithoutTag(optionalRegistry); - if (tag != null) { - fullName = fullName + ":" + tag; + + /** + * Get the full name of this image like {@link #getFullName(String)} does, but allow + * an optional registry. This registry is used when this image does not already + * contain a registry. If no tag was provided in the initial name, latest is used. + * + * @param optionalRegistry optional registry to use when this image does not provide + * a registry. Can be null in which case no optional registry is used* + * @return full name with original registry (if set) or optional registry (if not null). + */ + public String getFullName(String optionalRegistry) { + String fullName = getNameWithoutTag(optionalRegistry); + if (tag != null) { + fullName = fullName + ":" + tag; + } + if (digest != null) { + fullName = fullName + "@" + digest; + } + return fullName; } - if (digest != null) { - fullName = fullName + "@" + digest; + + // ========================================================== + + /** + * Get the user (or "project") part of the image name. This is the part after the registry and before + * the image name + * + * @return user part or null if no user is present in the name + */ + public String getUser() { + return user; } - return fullName; - } - - // ========================================================== - - /** - * Get the user (or "project") part of the image name. This is the part after the registry and before - * the image name - * - * @return user part or null if no user is present in the name - */ - public String getUser() { - return user; - } - - /** - * Get the simple name of the image, which is the repository sans the user parts. - * - * @return simple name of the image - */ - public String getSimpleName() { - String prefix = user + "/"; - return repository.startsWith(prefix) ? repository.substring(prefix.length()) : repository; - } - - public String getNameWithOptionalRepository(String optionalRepository) { - if (optionalRepository != null) { - String simpleName = getFullName(); - String[] simpleNameParts = simpleName.split("/"); - if (simpleNameParts.length > 0) { - return optionalRepository + "/" + simpleNameParts[simpleNameParts.length - 1]; - } + + /** + * Get the simple name of the image, which is the repository sans the user parts. + * + * @return simple name of the image + */ + public String getSimpleName() { + String prefix = user + "/"; + return repository.startsWith(prefix) ? repository.substring(prefix.length()) : repository; } - return getFullName(); - } - - // Validate parts and throw an IllegalArgumentException if a part is not valid - private void doValidate() { - List errors = new ArrayList<>(); - // Strip off user from repository name - String image = user != null ? repository.substring(user.length() + 1) : repository; - Object[] checks = new Object[]{ - "registry", DOMAIN_REGEXP, registry, - "image", IMAGE_NAME_REGEXP, image, - "user", NAME_COMP_REGEXP, user, - "tag", TAG_REGEXP, tag, - "digest", DIGEST_REGEXP, digest - }; - for (int i = 0; i < checks.length; i += 3) { - String value = (String) checks[i + 2]; - Pattern checkPattern = (Pattern) checks[i + 1]; - if (value != null && - !checkPattern.matcher(value).matches()) { - errors.add(String.format("%s part '%s' doesn't match allowed pattern '%s'", - checks[i], value, checkPattern.pattern())); - } + + public String getNameWithOptionalRepository(String optionalRepository) { + if (optionalRepository != null) { + String simpleName = getFullName(); + String[] simpleNameParts = simpleName.split("/"); + if (simpleNameParts.length > 0) { + return optionalRepository + "/" + simpleNameParts[simpleNameParts.length - 1]; + } + } + return getFullName(); } - if (errors.size() > 0) { - StringBuilder buf = new StringBuilder(); - buf.append(String.format("Given Docker name '%s' is invalid:\n", getFullName())); - for (String error : errors) { - buf.append(String.format(" * %s\n", error)); - } - buf.append("See http://bit.ly/docker_image_fmt for more details"); - throw new IllegalArgumentException(buf.toString()); + + // Validate parts and throw an IllegalArgumentException if a part is not valid + private void doValidate() { + List errors = new ArrayList<>(); + // Strip off user from repository name + String image = user != null ? repository.substring(user.length() + 1) : repository; + Object[] checks = new Object[] { + "registry", DOMAIN_REGEXP, registry, + "image", IMAGE_NAME_REGEXP, image, + "user", NAME_COMP_REGEXP, user, + "tag", TAG_REGEXP, tag, + "digest", DIGEST_REGEXP, digest + }; + for (int i = 0; i < checks.length; i += 3) { + String value = (String) checks[i + 2]; + Pattern checkPattern = (Pattern) checks[i + 1]; + if (value != null && !checkPattern.matcher(value).matches()) { + errors.add(String.format( + "%s part '%s' doesn't match allowed pattern '%s'", checks[i], value, checkPattern.pattern())); + } + } + if (errors.size() > 0) { + StringBuilder buf = new StringBuilder(); + buf.append(String.format("Given Docker name '%s' is invalid:\n", getFullName())); + for (String error : errors) { + buf.append(String.format(" * %s\n", error)); + } + buf.append("See http://bit.ly/docker_image_fmt for more details"); + throw new IllegalArgumentException(buf.toString()); + } } - } - - private void parseComponentsBeforeTag(String rest) { - String[] parts = rest.split("\\s*/\\s*"); - if (parts.length == 1) { - registry = null; - user = null; - repository = parts[0]; - } else if (parts.length >= 2) { - if (isRegistry(parts[0])) { - registry = parts[0]; - if (parts.length > 2) { - user = parts[1]; - repository = joinTail(parts); - } else { - user = null; - repository = parts[1]; + + private void parseComponentsBeforeTag(String rest) { + String[] parts = rest.split("\\s*/\\s*"); + if (parts.length == 1) { + registry = null; + user = null; + repository = parts[0]; + } else if (parts.length >= 2) { + if (isRegistry(parts[0])) { + registry = parts[0]; + if (parts.length > 2) { + user = parts[1]; + repository = joinTail(parts); + } else { + user = null; + repository = parts[1]; + } + } else { + registry = null; + user = parts[0]; + repository = rest; + } } - } else { - registry = null; - user = parts[0]; - repository = rest; - } } - } } diff --git a/src/main/java/com/redhat/exhort/image/ImageRef.java b/src/main/java/com/redhat/exhort/image/ImageRef.java index 92e71dec..6065a870 100644 --- a/src/main/java/com/redhat/exhort/image/ImageRef.java +++ b/src/main/java/com/redhat/exhort/image/ImageRef.java @@ -15,158 +15,156 @@ */ package com.redhat.exhort.image; +import static com.redhat.exhort.image.ImageUtils.getImageDigests; +import static com.redhat.exhort.image.ImageUtils.getImagePlatform; + import com.fasterxml.jackson.core.JsonProcessingException; import com.github.packageurl.MalformedPackageURLException; import com.github.packageurl.PackageURL; - import java.util.Map; import java.util.Objects; import java.util.TreeMap; -import static com.redhat.exhort.image.ImageUtils.getImageDigests; -import static com.redhat.exhort.image.ImageUtils.getImagePlatform; - public class ImageRef { - public static final String OCI_TYPE = "oci"; - public static final String REPOSITORY_QUALIFIER = "repository_url"; - public static final String TAG_QUALIFIER = "tag"; - public static final String ARCH_QUALIFIER = "arch"; - public static final String OS_QUALIFIER = "os"; - public static final String VARIANT_QUALIFIER = "variant"; + public static final String OCI_TYPE = "oci"; + public static final String REPOSITORY_QUALIFIER = "repository_url"; + public static final String TAG_QUALIFIER = "tag"; + public static final String ARCH_QUALIFIER = "arch"; + public static final String OS_QUALIFIER = "os"; + public static final String VARIANT_QUALIFIER = "variant"; + + private Image image; + private Platform platform; + + public ImageRef(String image, String platform) { + this.image = new Image(image); + + if (platform != null) { + this.platform = new Platform(platform); + } + + checkImageDigest(); + } + + public ImageRef(PackageURL packageURL) { + String name = null; + String version = null; + String tag = null; + String repositoryRrl = null; + String arch = null; + String os = null; + String variant = null; + + Map qualifiers = packageURL.getQualifiers(); + if (qualifiers != null && !qualifiers.isEmpty()) { + repositoryRrl = qualifiers.get(REPOSITORY_QUALIFIER); + tag = qualifiers.get(TAG_QUALIFIER); + arch = qualifiers.get(ARCH_QUALIFIER); + os = qualifiers.get(OS_QUALIFIER); + variant = qualifiers.get(VARIANT_QUALIFIER); + } + name = packageURL.getName(); + version = packageURL.getVersion(); - private Image image; - private Platform platform; + String imageName = name; + if (repositoryRrl != null) { + imageName = repositoryRrl; + } + if (tag != null) { + imageName = imageName + ":" + tag; + } + if (version != null) { + imageName = imageName + "@" + version; + } - public ImageRef(String image, String platform) { - this.image = new Image(image); + this.image = new Image(imageName); - if (platform != null) { - this.platform = new Platform(platform); + if (arch != null && os != null) { + this.platform = new Platform(os, arch, variant); + } } - checkImageDigest(); - } - - public ImageRef(PackageURL packageURL) { - String name = null; - String version = null; - String tag = null; - String repositoryRrl = null; - String arch = null; - String os = null; - String variant = null; - - Map qualifiers = packageURL.getQualifiers(); - if (qualifiers != null && !qualifiers.isEmpty()) { - repositoryRrl = qualifiers.get(REPOSITORY_QUALIFIER); - tag = qualifiers.get(TAG_QUALIFIER); - arch = qualifiers.get(ARCH_QUALIFIER); - os = qualifiers.get(OS_QUALIFIER); - variant = qualifiers.get(VARIANT_QUALIFIER); + public Image getImage() { + return image; } - name = packageURL.getName(); - version = packageURL.getVersion(); - String imageName = name; - if (repositoryRrl != null) { - imageName = repositoryRrl; + public Platform getPlatform() { + return platform; } - if (tag != null) { - imageName = imageName + ":" + tag; + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + ImageRef imageRef = (ImageRef) o; + return Objects.equals(image, imageRef.image) && Objects.equals(platform, imageRef.platform); } - if (version != null) { - imageName = imageName + "@" + version; + + @Override + public int hashCode() { + return Objects.hash(image, platform); } - this.image = new Image(imageName); + @Override + public String toString() { + return "ImageRef{" + "image='" + image + '\'' + ", platform='" + platform + '\'' + '}'; + } - if (arch != null && os != null) { - this.platform = new Platform(os, arch, variant); + void checkImageDigest() { + if (this.image.getDigest() == null) { + try { + var digests = getImageDigests(this); + if (digests.isEmpty()) { + throw new RuntimeException("Failed to get any image digest"); + } + if (digests.size() == 1 && digests.containsKey(Platform.EMPTY_PLATFORM)) { + this.image.setDigest(digests.get(Platform.EMPTY_PLATFORM)); + } else { + if (this.platform == null) { + this.platform = getImagePlatform(); + } + if (this.platform == null) { + throw new RuntimeException("Failed to get image platform for image digest"); + } + if (!digests.containsKey(this.platform)) { + throw new RuntimeException( + String.format("Failed to get image digest for platform %s", this.platform)); + } + this.image.setDigest(digests.get(this.platform)); + } + } catch (JsonProcessingException | IllegalArgumentException ex) { + throw new RuntimeException("Failed to get image digest", ex); + } + } } - } - - public Image getImage() { - return image; - } - - public Platform getPlatform() { - return platform; - } - - @Override - public boolean equals(Object o) { - if (this == o) return true; - if (o == null || getClass() != o.getClass()) return false; - ImageRef imageRef = (ImageRef) o; - return Objects.equals(image, imageRef.image) && Objects.equals(platform, imageRef.platform); - } - - @Override - public int hashCode() { - return Objects.hash(image, platform); - } - - @Override - public String toString() { - return "ImageRef{" + - "image='" + image + '\'' + - ", platform='" + platform + '\'' + - '}'; - } - - void checkImageDigest() { - if (this.image.getDigest() == null) { - try { - var digests = getImageDigests(this); - if (digests.isEmpty()) { - throw new RuntimeException("Failed to get any image digest"); + + // https://github.com/package-url/purl-spec/blob/master/PURL-TYPES.rst#oci + public PackageURL getPackageURL() throws MalformedPackageURLException { + TreeMap qualifiers = new TreeMap<>(); + var repositoryUrl = this.image.getNameWithoutTag(); + var simpleName = this.image.getSimpleName(); + if (repositoryUrl != null && !repositoryUrl.equalsIgnoreCase(simpleName)) { + qualifiers.put(REPOSITORY_QUALIFIER, repositoryUrl.toLowerCase()); } - if (digests.size() == 1 && digests.containsKey(Platform.EMPTY_PLATFORM)) { - this.image.setDigest(digests.get(Platform.EMPTY_PLATFORM)); - } else { - if (this.platform == null) { - this.platform = getImagePlatform(); - } - if (this.platform == null) { - throw new RuntimeException("Failed to get image platform for image digest"); - } - if (!digests.containsKey(this.platform)) { - throw new RuntimeException(String.format("Failed to get image digest for platform %s", this.platform)); - } - this.image.setDigest(digests.get(this.platform)); + if (this.platform != null) { + qualifiers.put(ARCH_QUALIFIER, this.platform.getArchitecture().toLowerCase()); + qualifiers.put(OS_QUALIFIER, this.platform.getOs().toLowerCase()); + if (this.platform.getVariant() != null) { + qualifiers.put(VARIANT_QUALIFIER, this.platform.getVariant().toLowerCase()); + } + } + var tag = this.image.getTag(); + if (tag != null) { + qualifiers.put(TAG_QUALIFIER, tag); } - } catch (JsonProcessingException | IllegalArgumentException ex) { - throw new RuntimeException("Failed to get image digest", ex); - } - } - } - - // https://github.com/package-url/purl-spec/blob/master/PURL-TYPES.rst#oci - public PackageURL getPackageURL() throws MalformedPackageURLException { - TreeMap qualifiers = new TreeMap<>(); - var repositoryUrl = this.image.getNameWithoutTag(); - var simpleName = this.image.getSimpleName(); - if (repositoryUrl != null && !repositoryUrl.equalsIgnoreCase(simpleName)) { - qualifiers.put(REPOSITORY_QUALIFIER, repositoryUrl.toLowerCase()); - } - if (this.platform != null) { - qualifiers.put(ARCH_QUALIFIER, this.platform.getArchitecture().toLowerCase()); - qualifiers.put(OS_QUALIFIER, this.platform.getOs().toLowerCase()); - if (this.platform.getVariant() != null) { - qualifiers.put(VARIANT_QUALIFIER, this.platform.getVariant().toLowerCase()); - } - } - var tag = this.image.getTag(); - if (tag != null) { - qualifiers.put(TAG_QUALIFIER, tag); - } - return new PackageURL(OCI_TYPE, - null, - this.image.getSimpleName().toLowerCase(), - image.getDigest().toLowerCase(), - qualifiers, - null); - } + return new PackageURL( + OCI_TYPE, + null, + this.image.getSimpleName().toLowerCase(), + image.getDigest().toLowerCase(), + qualifiers, + null); + } } diff --git a/src/main/java/com/redhat/exhort/image/ImageUtils.java b/src/main/java/com/redhat/exhort/image/ImageUtils.java index 75b5f822..97ba1e0d 100644 --- a/src/main/java/com/redhat/exhort/image/ImageUtils.java +++ b/src/main/java/com/redhat/exhort/image/ImageUtils.java @@ -13,9 +13,11 @@ * See the License for the specific language governing permissions and * limitations under the License. */ - package com.redhat.exhort.image; +import static com.redhat.exhort.image.Platform.EMPTY_PLATFORM; +import static com.redhat.exhort.impl.ExhortApi.getStringValueEnvironment; + import com.fasterxml.jackson.core.JsonProcessingException; import com.fasterxml.jackson.databind.JsonNode; import com.fasterxml.jackson.databind.ObjectMapper; @@ -24,7 +26,6 @@ import com.github.packageurl.MalformedPackageURLException; import com.redhat.exhort.logging.LoggersFactory; import com.redhat.exhort.tools.Operations; - import java.io.File; import java.io.IOException; import java.util.AbstractMap; @@ -39,426 +40,461 @@ import java.util.stream.Collectors; import java.util.stream.StreamSupport; -import static com.redhat.exhort.image.Platform.EMPTY_PLATFORM; -import static com.redhat.exhort.impl.ExhortApi.getStringValueEnvironment; - public class ImageUtils { - static final String EXHORT_SYFT_CONFIG_PATH = "EXHORT_SYFT_CONFIG_PATH"; - static final String EXHORT_SYFT_IMAGE_SOURCE = "EXHORT_SYFT_IMAGE_SOURCE"; - static final String EXHORT_IMAGE_PLATFORM = "EXHORT_IMAGE_PLATFORM"; - static final String EXHORT_IMAGE_OS = "EXHORT_IMAGE_OS"; - static final String EXHORT_IMAGE_ARCH = "EXHORT_IMAGE_ARCH"; - static final String EXHORT_IMAGE_VARIANT = "EXHORT_IMAGE_VARIANT"; - static final String EXHORT_SKOPEO_CONFIG_PATH = "EXHORT_SKOPEO_CONFIG_PATH"; - static final String EXHORT_IMAGE_SERVICE_ENDPOINT = "EXHORT_IMAGE_SERVICE_ENDPOINT"; - private static final String MEDIA_TYPE_DOCKER2_MANIFEST = "application/vnd.docker.distribution.manifest.v2+json"; - private static final String MEDIA_TYPE_DOCKER2_MANIFEST_LIST = "application/vnd.docker.distribution.manifest.list.v2+json"; - private static final String MEDIA_TYPE_OCI1_MANIFEST = "application/vnd.oci.image.manifest.v1+json"; - private static final String MEDIA_TYPE_OCI1_MANIFEST_LIST = "application/vnd.oci.image.index.v1+json"; - - private static final Logger logger = LoggersFactory.getLogger(ImageUtils.class.getName()); - - private static final ObjectMapper OBJECT_MAPPER = new ObjectMapper(); - - private static final Map archMapping = Map.ofEntries( - new AbstractMap.SimpleEntry<>("amd64", "amd64"), - new AbstractMap.SimpleEntry<>("x86_64", "amd64"), - new AbstractMap.SimpleEntry<>("armv5tl", "arm"), - new AbstractMap.SimpleEntry<>("armv5tel", "arm"), - new AbstractMap.SimpleEntry<>("armv5tejl", "arm"), - new AbstractMap.SimpleEntry<>("armv6l", "arm"), - new AbstractMap.SimpleEntry<>("armv7l", "arm"), - new AbstractMap.SimpleEntry<>("armv7ml", "arm"), - new AbstractMap.SimpleEntry<>("arm64", "arm64"), - new AbstractMap.SimpleEntry<>("aarch64", "arm64"), - new AbstractMap.SimpleEntry<>("i386", "386"), - new AbstractMap.SimpleEntry<>("i486", "386"), - new AbstractMap.SimpleEntry<>("i586", "386"), - new AbstractMap.SimpleEntry<>("i686", "386"), - new AbstractMap.SimpleEntry<>("mips64le", "mips64le"), - new AbstractMap.SimpleEntry<>("ppc64le", "ppc64le"), - new AbstractMap.SimpleEntry<>("riscv64", "riscv64"), - new AbstractMap.SimpleEntry<>("s390x", "s390x") - ); - private static final Map variantMapping = Map.ofEntries( - new AbstractMap.SimpleEntry<>("armv5tl", "v5"), - new AbstractMap.SimpleEntry<>("armv5tel", "v5"), - new AbstractMap.SimpleEntry<>("armv5tejl", "v5"), - new AbstractMap.SimpleEntry<>("armv6l", "v6"), - new AbstractMap.SimpleEntry<>("armv7l", "v7"), - new AbstractMap.SimpleEntry<>("armv7ml", "v7"), - new AbstractMap.SimpleEntry<>("arm64", "v8"), - new AbstractMap.SimpleEntry<>("aarch64", "v8") - ); - - static String updatePATHEnv(String execPath) { - String path = System.getenv("PATH"); - if (path != null) { - return String.format("PATH=%s%s%s", path, File.pathSeparator, execPath); - } else { - return String.format("PATH=%s", execPath); + static final String EXHORT_SYFT_CONFIG_PATH = "EXHORT_SYFT_CONFIG_PATH"; + static final String EXHORT_SYFT_IMAGE_SOURCE = "EXHORT_SYFT_IMAGE_SOURCE"; + static final String EXHORT_IMAGE_PLATFORM = "EXHORT_IMAGE_PLATFORM"; + static final String EXHORT_IMAGE_OS = "EXHORT_IMAGE_OS"; + static final String EXHORT_IMAGE_ARCH = "EXHORT_IMAGE_ARCH"; + static final String EXHORT_IMAGE_VARIANT = "EXHORT_IMAGE_VARIANT"; + static final String EXHORT_SKOPEO_CONFIG_PATH = "EXHORT_SKOPEO_CONFIG_PATH"; + static final String EXHORT_IMAGE_SERVICE_ENDPOINT = "EXHORT_IMAGE_SERVICE_ENDPOINT"; + private static final String MEDIA_TYPE_DOCKER2_MANIFEST = "application/vnd.docker.distribution.manifest.v2+json"; + private static final String MEDIA_TYPE_DOCKER2_MANIFEST_LIST = + "application/vnd.docker.distribution.manifest.list.v2+json"; + private static final String MEDIA_TYPE_OCI1_MANIFEST = "application/vnd.oci.image.manifest.v1+json"; + private static final String MEDIA_TYPE_OCI1_MANIFEST_LIST = "application/vnd.oci.image.index.v1+json"; + + private static final Logger logger = LoggersFactory.getLogger(ImageUtils.class.getName()); + + private static final ObjectMapper OBJECT_MAPPER = new ObjectMapper(); + + private static final Map archMapping = Map.ofEntries( + new AbstractMap.SimpleEntry<>("amd64", "amd64"), + new AbstractMap.SimpleEntry<>("x86_64", "amd64"), + new AbstractMap.SimpleEntry<>("armv5tl", "arm"), + new AbstractMap.SimpleEntry<>("armv5tel", "arm"), + new AbstractMap.SimpleEntry<>("armv5tejl", "arm"), + new AbstractMap.SimpleEntry<>("armv6l", "arm"), + new AbstractMap.SimpleEntry<>("armv7l", "arm"), + new AbstractMap.SimpleEntry<>("armv7ml", "arm"), + new AbstractMap.SimpleEntry<>("arm64", "arm64"), + new AbstractMap.SimpleEntry<>("aarch64", "arm64"), + new AbstractMap.SimpleEntry<>("i386", "386"), + new AbstractMap.SimpleEntry<>("i486", "386"), + new AbstractMap.SimpleEntry<>("i586", "386"), + new AbstractMap.SimpleEntry<>("i686", "386"), + new AbstractMap.SimpleEntry<>("mips64le", "mips64le"), + new AbstractMap.SimpleEntry<>("ppc64le", "ppc64le"), + new AbstractMap.SimpleEntry<>("riscv64", "riscv64"), + new AbstractMap.SimpleEntry<>("s390x", "s390x")); + private static final Map variantMapping = Map.ofEntries( + new AbstractMap.SimpleEntry<>("armv5tl", "v5"), + new AbstractMap.SimpleEntry<>("armv5tel", "v5"), + new AbstractMap.SimpleEntry<>("armv5tejl", "v5"), + new AbstractMap.SimpleEntry<>("armv6l", "v6"), + new AbstractMap.SimpleEntry<>("armv7l", "v7"), + new AbstractMap.SimpleEntry<>("armv7ml", "v7"), + new AbstractMap.SimpleEntry<>("arm64", "v8"), + new AbstractMap.SimpleEntry<>("aarch64", "v8")); + + static String updatePATHEnv(String execPath) { + String path = System.getenv("PATH"); + if (path != null) { + return String.format("PATH=%s%s%s", path, File.pathSeparator, execPath); + } else { + return String.format("PATH=%s", execPath); + } } - } - public static JsonNode generateImageSBOM(ImageRef imageRef) throws IOException, MalformedPackageURLException { - var output = execSyft(imageRef); + public static JsonNode generateImageSBOM(ImageRef imageRef) throws IOException, MalformedPackageURLException { + var output = execSyft(imageRef); - if (!output.getError().isEmpty() || output.getExitCode() != 0) { - throw new RuntimeException(output.getError()); - } + if (!output.getError().isEmpty() || output.getExitCode() != 0) { + throw new RuntimeException(output.getError()); + } - var node = OBJECT_MAPPER.readTree(output.getOutput()); - if (node.hasNonNull("metadata")) { - var metadataNode = node.get("metadata"); - if (metadataNode.hasNonNull("component")) { - var componentNode = metadataNode.get("component"); - if (componentNode.isObject()) { - String imagePurl = imageRef.getPackageURL().canonicalize(); - ((ObjectNode) componentNode).set("purl", new TextNode(imagePurl)); - return node; + var node = OBJECT_MAPPER.readTree(output.getOutput()); + if (node.hasNonNull("metadata")) { + var metadataNode = node.get("metadata"); + if (metadataNode.hasNonNull("component")) { + var componentNode = metadataNode.get("component"); + if (componentNode.isObject()) { + String imagePurl = imageRef.getPackageURL().canonicalize(); + ((ObjectNode) componentNode).set("purl", new TextNode(imagePurl)); + return node; + } + } } - } - } - throw new RuntimeException(String.format("The generated SBOM of the image is invalid: %s", output.getOutput())); - } - - static Operations.ProcessExecOutput execSyft(ImageRef imageRef) { - var syft = Operations.getCustomPathOrElse("syft"); - var docker = Operations.getCustomPathOrElse("docker"); - var podman = Operations.getCustomPathOrElse("podman"); - - var syftConfigPath = getStringValueEnvironment(EXHORT_SYFT_CONFIG_PATH, ""); - var imageSource = getStringValueEnvironment(EXHORT_SYFT_IMAGE_SOURCE, ""); - SyftImageSource.getImageSource(imageSource); - - var dockerPath = docker != null && docker.contains(File.separator) ? - docker.substring(0, docker.lastIndexOf(File.separator) + 1) : ""; - var podmanPath = podman != null && podman.contains(File.separator) ? - podman.substring(0, podman.lastIndexOf(File.separator) + 1) : ""; - var envs = getSyftEnvs(dockerPath, podmanPath); - - var scheme = imageRef.getImage().toString(); - - String[] cmd; - if (!imageSource.isEmpty()) { - cmd = syftConfigPath.isEmpty() ? - new String[]{syft, scheme, "--from", imageSource, "-s", "all-layers", "-o", "cyclonedx-json", "-q"} : - new String[]{syft, scheme, "--from", imageSource, "-c", syftConfigPath, "-s", "all-layers", "-o", "cyclonedx-json", "-q"}; - } else { - cmd = syftConfigPath.isEmpty() ? - new String[]{syft, scheme, "-s", "all-layers", "-o", "cyclonedx-json", "-q"} : - new String[]{syft, scheme, "-c", syftConfigPath, "-s", "all-layers", "-o", "cyclonedx-json", "-q"}; + throw new RuntimeException(String.format("The generated SBOM of the image is invalid: %s", output.getOutput())); } - return Operations.runProcessGetFullOutput(null, cmd, - envs.isEmpty() ? null : envs.toArray(new String[1])); - } - - static List getSyftEnvs(String dockerPath, String podmanPath) { - String path = null; - if (!dockerPath.isEmpty() && !podmanPath.isEmpty()) { - path = String.format("%s%s%s", dockerPath, File.pathSeparator, podmanPath); - } else if (!dockerPath.isEmpty()) { - path = dockerPath; - } else if (!podmanPath.isEmpty()) { - path = podmanPath; - } - var envPath = path != null ? updatePATHEnv(path) : null; + static Operations.ProcessExecOutput execSyft(ImageRef imageRef) { + var syft = Operations.getCustomPathOrElse("syft"); + var docker = Operations.getCustomPathOrElse("docker"); + var podman = Operations.getCustomPathOrElse("podman"); + + var syftConfigPath = getStringValueEnvironment(EXHORT_SYFT_CONFIG_PATH, ""); + var imageSource = getStringValueEnvironment(EXHORT_SYFT_IMAGE_SOURCE, ""); + SyftImageSource.getImageSource(imageSource); + + var dockerPath = docker != null && docker.contains(File.separator) + ? docker.substring(0, docker.lastIndexOf(File.separator) + 1) + : ""; + var podmanPath = podman != null && podman.contains(File.separator) + ? podman.substring(0, podman.lastIndexOf(File.separator) + 1) + : ""; + var envs = getSyftEnvs(dockerPath, podmanPath); + + var scheme = imageRef.getImage().toString(); + + String[] cmd; + if (!imageSource.isEmpty()) { + cmd = syftConfigPath.isEmpty() + ? new String[] { + syft, scheme, "--from", imageSource, "-s", "all-layers", "-o", "cyclonedx-json", "-q" + } + : new String[] { + syft, + scheme, + "--from", + imageSource, + "-c", + syftConfigPath, + "-s", + "all-layers", + "-o", + "cyclonedx-json", + "-q" + }; + } else { + cmd = syftConfigPath.isEmpty() + ? new String[] {syft, scheme, "-s", "all-layers", "-o", "cyclonedx-json", "-q"} + : new String[] {syft, scheme, "-c", syftConfigPath, "-s", "all-layers", "-o", "cyclonedx-json", "-q" + }; + } - List envs = new ArrayList<>(1); - if (envPath != null) { - envs.add(envPath); + return Operations.runProcessGetFullOutput(null, cmd, envs.isEmpty() ? null : envs.toArray(new String[1])); } - return envs; - } - public static Platform getImagePlatform() { - var platform = getStringValueEnvironment(EXHORT_IMAGE_PLATFORM, ""); - if (!platform.isEmpty()) { - return new Platform(platform); + static List getSyftEnvs(String dockerPath, String podmanPath) { + String path = null; + if (!dockerPath.isEmpty() && !podmanPath.isEmpty()) { + path = String.format("%s%s%s", dockerPath, File.pathSeparator, podmanPath); + } else if (!dockerPath.isEmpty()) { + path = dockerPath; + } else if (!podmanPath.isEmpty()) { + path = podmanPath; + } + var envPath = path != null ? updatePATHEnv(path) : null; + + List envs = new ArrayList<>(1); + if (envPath != null) { + envs.add(envPath); + } + return envs; } - var imageSource = getStringValueEnvironment(EXHORT_SYFT_IMAGE_SOURCE, ""); - SyftImageSource source = SyftImageSource.getImageSource(imageSource); + public static Platform getImagePlatform() { + var platform = getStringValueEnvironment(EXHORT_IMAGE_PLATFORM, ""); + if (!platform.isEmpty()) { + return new Platform(platform); + } + + var imageSource = getStringValueEnvironment(EXHORT_SYFT_IMAGE_SOURCE, ""); + SyftImageSource source = SyftImageSource.getImageSource(imageSource); - var os = getStringValueEnvironment(EXHORT_IMAGE_OS, ""); - if (os.isEmpty()) { - os = source.getOs(); + var os = getStringValueEnvironment(EXHORT_IMAGE_OS, ""); + if (os.isEmpty()) { + os = source.getOs(); + } + var arch = getStringValueEnvironment(EXHORT_IMAGE_ARCH, ""); + if (arch.isEmpty()) { + arch = source.getArch(); + } + if (!os.isEmpty() && !arch.isEmpty()) { + if (!Platform.isVariantRequired(os, arch)) { + return new Platform(os, arch, null); + } + + var variant = getStringValueEnvironment(EXHORT_IMAGE_VARIANT, ""); + if (variant.isEmpty()) { + variant = source.getVariant(); + } + if (!variant.isEmpty()) { + return new Platform(os, arch, variant); + } + } + + return null; } - var arch = getStringValueEnvironment(EXHORT_IMAGE_ARCH, ""); - if (arch.isEmpty()) { - arch = source.getArch(); + + static String hostInfo(String engine, String info) { + var exec = Operations.getCustomPathOrElse(engine); + var cmd = new String[] {exec, "info"}; + + var output = Operations.runProcessGetFullOutput(null, cmd, null); + if (output.getOutput().isEmpty() && (!output.getError().isEmpty() || output.getExitCode() != 0)) { + throw new RuntimeException(output.getError()); + } + + return output.getOutput() + .lines() + .filter(line -> line.stripLeading().startsWith(info + ":")) + .map(line -> line.strip().substring(info.length() + 1).strip()) + .findAny() + .orElse(""); } - if (!os.isEmpty() && !arch.isEmpty()) { - if (!Platform.isVariantRequired(os, arch)) { - return new Platform(os, arch, null); - } - - var variant = getStringValueEnvironment(EXHORT_IMAGE_VARIANT, ""); - if (variant.isEmpty()) { - variant = source.getVariant(); - } - if (!variant.isEmpty()) { - return new Platform(os, arch, variant); - } + + static String dockerGetOs() { + return hostInfo("docker", "OSType"); } - return null; - } + static String dockerGetArch() { + var arch = hostInfo("docker", "Architecture"); + arch = archMapping.get(arch); + return Objects.requireNonNullElse(arch, ""); + } - static String hostInfo(String engine, String info) { - var exec = Operations.getCustomPathOrElse(engine); - var cmd = new String[]{exec, "info"}; + static String dockerGetVariant() { + var variant = hostInfo("docker", "Architecture"); + variant = variantMapping.get(variant); + return Objects.requireNonNullElse(variant, ""); + } - var output = Operations.runProcessGetFullOutput(null, cmd, null); - if (output.getOutput().isEmpty() && (!output.getError().isEmpty() || output.getExitCode() != 0)) { - throw new RuntimeException(output.getError()); + static String podmanGetOs() { + return hostInfo("podman", "os"); } - return output.getOutput() - .lines() - .filter(line -> line.stripLeading().startsWith(info + ":")) - .map(line -> line.strip().substring(info.length() + 1).strip()) - .findAny() - .orElse(""); - } - - static String dockerGetOs() { - return hostInfo("docker", "OSType"); - } - - static String dockerGetArch() { - var arch = hostInfo("docker", "Architecture"); - arch = archMapping.get(arch); - return Objects.requireNonNullElse(arch, ""); - } - - static String dockerGetVariant() { - var variant = hostInfo("docker", "Architecture"); - variant = variantMapping.get(variant); - return Objects.requireNonNullElse(variant, ""); - } - - static String podmanGetOs() { - return hostInfo("podman", "os"); - } - - static String podmanGetArch() { - return hostInfo("podman", "arch"); - } - - static String podmanGetVariant() { - return hostInfo("podman", "variant"); - } - - static String dockerPodmanInfo(Supplier dockerSupplier, Supplier podmanSupplier) { - var info = dockerSupplier.get(); - if (info.isEmpty()) { - info = podmanSupplier.get(); + static String podmanGetArch() { + return hostInfo("podman", "arch"); } - return info; - } - public static Map getImageDigests(ImageRef imageRef) throws JsonProcessingException { - var output = execSkopeoInspect(imageRef, true); + static String podmanGetVariant() { + return hostInfo("podman", "variant"); + } - if (!output.getError().isEmpty() || output.getExitCode() != 0) { - throw new RuntimeException(output.getError()); + static String dockerPodmanInfo(Supplier dockerSupplier, Supplier podmanSupplier) { + var info = dockerSupplier.get(); + if (info.isEmpty()) { + info = podmanSupplier.get(); + } + return info; } - var node = OBJECT_MAPPER.readTree(output.getOutput()); - if (node.hasNonNull("mediaType")) { - var mediaTypeNode = node.get("mediaType"); - if (mediaTypeNode.isTextual()) { - var mediaType = mediaTypeNode.asText(); - switch (mediaType) { - case MEDIA_TYPE_OCI1_MANIFEST: - case MEDIA_TYPE_DOCKER2_MANIFEST: - return getSingleImageDigest(imageRef); - - case MEDIA_TYPE_OCI1_MANIFEST_LIST: - case MEDIA_TYPE_DOCKER2_MANIFEST_LIST: - return getMultiImageDigests(node); + public static Map getImageDigests(ImageRef imageRef) throws JsonProcessingException { + var output = execSkopeoInspect(imageRef, true); + + if (!output.getError().isEmpty() || output.getExitCode() != 0) { + throw new RuntimeException(output.getError()); } - } + + var node = OBJECT_MAPPER.readTree(output.getOutput()); + if (node.hasNonNull("mediaType")) { + var mediaTypeNode = node.get("mediaType"); + if (mediaTypeNode.isTextual()) { + var mediaType = mediaTypeNode.asText(); + switch (mediaType) { + case MEDIA_TYPE_OCI1_MANIFEST: + case MEDIA_TYPE_DOCKER2_MANIFEST: + return getSingleImageDigest(imageRef); + + case MEDIA_TYPE_OCI1_MANIFEST_LIST: + case MEDIA_TYPE_DOCKER2_MANIFEST_LIST: + return getMultiImageDigests(node); + } + } + } + + throw new RuntimeException(String.format("The image info is invalid: %s", output.getOutput())); } - throw new RuntimeException(String.format("The image info is invalid: %s", output.getOutput())); - } - - static Map getMultiImageDigests(JsonNode node) { - if (node.hasNonNull("manifests")) { - var manifestsNode = node.get("manifests"); - if (manifestsNode.isArray()) { - return StreamSupport.stream(manifestsNode.spliterator(), false) - .filter(ImageUtils::filterMediaType) - .filter(ImageUtils::filterDigest) - .filter(ImageUtils::filterPlatform) - .collect(Collectors.toMap( - manifestNode -> { - var platformNode = manifestNode.get("platform"); - var arch = platformNode.get("architecture").asText(); - var os = platformNode.get("os").asText(); - if (platformNode.hasNonNull("variant")) { - var variant = platformNode.get("variant").asText(); - return new Platform(String.format("%s/%s/%s", os, arch, variant)); - } else { - return new Platform(String.format("%s/%s", os, arch)); - } - }, - manifestNode -> manifestNode.get("digest").asText() - )); - } + static Map getMultiImageDigests(JsonNode node) { + if (node.hasNonNull("manifests")) { + var manifestsNode = node.get("manifests"); + if (manifestsNode.isArray()) { + return StreamSupport.stream(manifestsNode.spliterator(), false) + .filter(ImageUtils::filterMediaType) + .filter(ImageUtils::filterDigest) + .filter(ImageUtils::filterPlatform) + .collect(Collectors.toMap( + manifestNode -> { + var platformNode = manifestNode.get("platform"); + var arch = platformNode.get("architecture").asText(); + var os = platformNode.get("os").asText(); + if (platformNode.hasNonNull("variant")) { + var variant = + platformNode.get("variant").asText(); + return new Platform(String.format("%s/%s/%s", os, arch, variant)); + } else { + return new Platform(String.format("%s/%s", os, arch)); + } + }, + manifestNode -> manifestNode.get("digest").asText())); + } + } + return Collections.emptyMap(); } - return Collections.emptyMap(); - } - - static boolean filterMediaType(JsonNode manifestNode) { - if (manifestNode.hasNonNull("mediaType")) { - var mediaTypeNode = manifestNode.get("mediaType"); - if (mediaTypeNode.isTextual()) { - var mediaType = mediaTypeNode.asText(); - return MEDIA_TYPE_OCI1_MANIFEST.equals(mediaType) || MEDIA_TYPE_DOCKER2_MANIFEST.equals(mediaType); - } + + static boolean filterMediaType(JsonNode manifestNode) { + if (manifestNode.hasNonNull("mediaType")) { + var mediaTypeNode = manifestNode.get("mediaType"); + if (mediaTypeNode.isTextual()) { + var mediaType = mediaTypeNode.asText(); + return MEDIA_TYPE_OCI1_MANIFEST.equals(mediaType) || MEDIA_TYPE_DOCKER2_MANIFEST.equals(mediaType); + } + } + return false; } - return false; - } - static boolean filterDigest(JsonNode manifestNode) { - if (manifestNode.hasNonNull("digest")) { - var digestNode = manifestNode.get("digest"); - return digestNode.isTextual(); + static boolean filterDigest(JsonNode manifestNode) { + if (manifestNode.hasNonNull("digest")) { + var digestNode = manifestNode.get("digest"); + return digestNode.isTextual(); + } + return false; } - return false; - } - - static boolean filterPlatform(JsonNode manifestNode) { - if (manifestNode.hasNonNull("platform")) { - var platformNode = manifestNode.get("platform"); - if (platformNode.isObject()) { - if (platformNode.hasNonNull("architecture") && platformNode.hasNonNull("os")) { - var architectureNode = platformNode.get("architecture"); - var osNode = platformNode.get("os"); - if (architectureNode.isTextual() && osNode.isTextual()) { - if (platformNode.hasNonNull("variant")) { - var variantNode = platformNode.get("variant"); - if (variantNode.isTextual()) { - try { - new Platform(String.format("%s/%s/%s", osNode.asText(), architectureNode.asText(), variantNode.asText())); - } catch (IllegalArgumentException e) { - return false; + + static boolean filterPlatform(JsonNode manifestNode) { + if (manifestNode.hasNonNull("platform")) { + var platformNode = manifestNode.get("platform"); + if (platformNode.isObject()) { + if (platformNode.hasNonNull("architecture") && platformNode.hasNonNull("os")) { + var architectureNode = platformNode.get("architecture"); + var osNode = platformNode.get("os"); + if (architectureNode.isTextual() && osNode.isTextual()) { + if (platformNode.hasNonNull("variant")) { + var variantNode = platformNode.get("variant"); + if (variantNode.isTextual()) { + try { + new Platform(String.format( + "%s/%s/%s", + osNode.asText(), architectureNode.asText(), variantNode.asText())); + } catch (IllegalArgumentException e) { + return false; + } + return true; + } + } + try { + new Platform(String.format("%s/%s", osNode.asText(), architectureNode.asText())); + } catch (IllegalArgumentException e) { + return false; + } + return true; + } } - return true; - } - } - try { - new Platform(String.format("%s/%s", osNode.asText(), architectureNode.asText())); - } catch (IllegalArgumentException e) { - return false; } - return true; - } } - } + return false; } - return false; - } - static Map getSingleImageDigest(ImageRef imageRef) throws JsonProcessingException { - var output = execSkopeoInspect(imageRef, false); + static Map getSingleImageDigest(ImageRef imageRef) throws JsonProcessingException { + var output = execSkopeoInspect(imageRef, false); - if (!output.getError().isEmpty() || output.getExitCode() != 0) { - throw new RuntimeException(output.getError()); - } + if (!output.getError().isEmpty() || output.getExitCode() != 0) { + throw new RuntimeException(output.getError()); + } - var node = OBJECT_MAPPER.readTree(output.getOutput()); + var node = OBJECT_MAPPER.readTree(output.getOutput()); - if (node.hasNonNull("Digest")) { - var digestNode = node.get("Digest"); - if (digestNode.isTextual()) { - return Collections.singletonMap(EMPTY_PLATFORM, digestNode.asText()); - } - } - return Collections.emptyMap(); - } - - static Operations.ProcessExecOutput execSkopeoInspect(ImageRef imageRef, boolean raw) { - var skopeo = Operations.getCustomPathOrElse("skopeo"); - - var configPath = getStringValueEnvironment(EXHORT_SKOPEO_CONFIG_PATH, ""); - var daemonHost = getStringValueEnvironment(EXHORT_IMAGE_SERVICE_ENDPOINT, ""); - - String[] cmd; - if (daemonHost.isEmpty()) { - cmd = configPath.isEmpty() ? - new String[]{skopeo, "inspect", raw ? "--raw" : "", - String.format("docker://%s", imageRef.getImage().getFullName())} : - new String[]{skopeo, "inspect", "--authfile", configPath, raw ? "--raw" : "", - String.format("docker://%s", imageRef.getImage().getFullName())}; - } else { - cmd = configPath.isEmpty() ? - new String[]{skopeo, "inspect", "--daemon-host", daemonHost, raw ? "--raw" : "", - String.format("docker-daemon:%s", imageRef.getImage().getFullName())} : - new String[]{skopeo, "inspect", "--authfile", configPath, "--daemon-host", daemonHost, raw ? "--raw" : "", - String.format("docker-daemon:%s", imageRef.getImage().getFullName())}; + if (node.hasNonNull("Digest")) { + var digestNode = node.get("Digest"); + if (digestNode.isTextual()) { + return Collections.singletonMap(EMPTY_PLATFORM, digestNode.asText()); + } + } + return Collections.emptyMap(); } - return Operations.runProcessGetFullOutput(null, cmd, null); - } - - private enum SyftImageSource { - DEFAULT("", - () -> dockerPodmanInfo(ImageUtils::dockerGetOs, ImageUtils::podmanGetOs), - () -> dockerPodmanInfo(ImageUtils::dockerGetArch, ImageUtils::podmanGetArch), - () -> dockerPodmanInfo(ImageUtils::dockerGetVariant, ImageUtils::podmanGetVariant)), - REGISTRY("registry", - () -> dockerPodmanInfo(ImageUtils::dockerGetOs, ImageUtils::podmanGetOs), - () -> dockerPodmanInfo(ImageUtils::dockerGetArch, ImageUtils::podmanGetArch), - () -> dockerPodmanInfo(ImageUtils::dockerGetVariant, ImageUtils::podmanGetVariant)), - DOCKER("docker", - ImageUtils::dockerGetOs, - ImageUtils::dockerGetArch, - ImageUtils::dockerGetVariant), - PODMAN("podman", - ImageUtils::podmanGetOs, - ImageUtils::podmanGetArch, - ImageUtils::podmanGetVariant); - - private final String name; - private final Supplier osSupplier; - private final Supplier archSupplier; - private final Supplier variantSupplier; - - SyftImageSource(String name, - Supplier osSupplier, - Supplier archSupplier, - Supplier variantSupplier) { - this.name = name; - this.osSupplier = osSupplier; - this.archSupplier = archSupplier; - this.variantSupplier = variantSupplier; - } + static Operations.ProcessExecOutput execSkopeoInspect(ImageRef imageRef, boolean raw) { + var skopeo = Operations.getCustomPathOrElse("skopeo"); + + var configPath = getStringValueEnvironment(EXHORT_SKOPEO_CONFIG_PATH, ""); + var daemonHost = getStringValueEnvironment(EXHORT_IMAGE_SERVICE_ENDPOINT, ""); + + String[] cmd; + if (daemonHost.isEmpty()) { + cmd = configPath.isEmpty() + ? new String[] { + skopeo, + "inspect", + raw ? "--raw" : "", + String.format("docker://%s", imageRef.getImage().getFullName()) + } + : new String[] { + skopeo, + "inspect", + "--authfile", + configPath, + raw ? "--raw" : "", + String.format("docker://%s", imageRef.getImage().getFullName()) + }; + } else { + cmd = configPath.isEmpty() + ? new String[] { + skopeo, + "inspect", + "--daemon-host", + daemonHost, + raw ? "--raw" : "", + String.format("docker-daemon:%s", imageRef.getImage().getFullName()) + } + : new String[] { + skopeo, + "inspect", + "--authfile", + configPath, + "--daemon-host", + daemonHost, + raw ? "--raw" : "", + String.format("docker-daemon:%s", imageRef.getImage().getFullName()) + }; + } - static SyftImageSource getImageSource(String name) { - return EnumSet.allOf(SyftImageSource.class).stream() - .filter(s -> s.name.equals(name)) - .findAny() - .orElseThrow(() -> new IllegalArgumentException(String.format("The image source for syft is not valid: %s", name))); + return Operations.runProcessGetFullOutput(null, cmd, null); } - String getOs() { - return osSupplier.get(); - } + private enum SyftImageSource { + DEFAULT( + "", + () -> dockerPodmanInfo(ImageUtils::dockerGetOs, ImageUtils::podmanGetOs), + () -> dockerPodmanInfo(ImageUtils::dockerGetArch, ImageUtils::podmanGetArch), + () -> dockerPodmanInfo(ImageUtils::dockerGetVariant, ImageUtils::podmanGetVariant)), + REGISTRY( + "registry", + () -> dockerPodmanInfo(ImageUtils::dockerGetOs, ImageUtils::podmanGetOs), + () -> dockerPodmanInfo(ImageUtils::dockerGetArch, ImageUtils::podmanGetArch), + () -> dockerPodmanInfo(ImageUtils::dockerGetVariant, ImageUtils::podmanGetVariant)), + DOCKER("docker", ImageUtils::dockerGetOs, ImageUtils::dockerGetArch, ImageUtils::dockerGetVariant), + PODMAN("podman", ImageUtils::podmanGetOs, ImageUtils::podmanGetArch, ImageUtils::podmanGetVariant); + + private final String name; + private final Supplier osSupplier; + private final Supplier archSupplier; + private final Supplier variantSupplier; + + SyftImageSource( + String name, + Supplier osSupplier, + Supplier archSupplier, + Supplier variantSupplier) { + this.name = name; + this.osSupplier = osSupplier; + this.archSupplier = archSupplier; + this.variantSupplier = variantSupplier; + } - String getArch() { - return archSupplier.get(); - } + static SyftImageSource getImageSource(String name) { + return EnumSet.allOf(SyftImageSource.class).stream() + .filter(s -> s.name.equals(name)) + .findAny() + .orElseThrow(() -> new IllegalArgumentException( + String.format("The image source for syft is not valid: %s", name))); + } + + String getOs() { + return osSupplier.get(); + } - String getVariant() { - return variantSupplier.get(); + String getArch() { + return archSupplier.get(); + } + + String getVariant() { + return variantSupplier.get(); + } } - } } - diff --git a/src/main/java/com/redhat/exhort/image/Platform.java b/src/main/java/com/redhat/exhort/image/Platform.java index d2636a96..bd42f6db 100644 --- a/src/main/java/com/redhat/exhort/image/Platform.java +++ b/src/main/java/com/redhat/exhort/image/Platform.java @@ -20,138 +20,138 @@ public class Platform { - // $GOOS and $GOARCH - // https://github.com/docker-library/bashbrew/blob/v0.1.2/architecture/oci-platform.go#L14-L27 - private static final Set SUPPORTED_PLATFORMS = Set.of( - new Platform().os("linux").arch("amd64"), - new Platform().os("linux").arch("arm").variant("v5"), - new Platform().os("linux").arch("arm").variant("v6"), - new Platform().os("linux").arch("arm").variant("v7"), - new Platform().os("linux").arch("arm64").variant("v8"), - new Platform().os("linux").arch("386"), - new Platform().os("linux").arch("mips64le"), - new Platform().os("linux").arch("ppc64le"), - new Platform().os("linux").arch("riscv64"), - new Platform().os("linux").arch("s390x"), - - new Platform().os("windows").arch("arm64") - ); - - public static final Platform EMPTY_PLATFORM = new Platform(); - - private String os; - private String architecture; - private String variant; - - private Platform() { - } - - public Platform(String platform) { - if (platform == null) { - throw new IllegalArgumentException("Invalid platform: null"); + // $GOOS and $GOARCH + // https://github.com/docker-library/bashbrew/blob/v0.1.2/architecture/oci-platform.go#L14-L27 + private static final Set SUPPORTED_PLATFORMS = Set.of( + new Platform().os("linux").arch("amd64"), + new Platform().os("linux").arch("arm").variant("v5"), + new Platform().os("linux").arch("arm").variant("v6"), + new Platform().os("linux").arch("arm").variant("v7"), + new Platform().os("linux").arch("arm64").variant("v8"), + new Platform().os("linux").arch("386"), + new Platform().os("linux").arch("mips64le"), + new Platform().os("linux").arch("ppc64le"), + new Platform().os("linux").arch("riscv64"), + new Platform().os("linux").arch("s390x"), + new Platform().os("windows").arch("arm64")); + + public static final Platform EMPTY_PLATFORM = new Platform(); + + private String os; + private String architecture; + private String variant; + + private Platform() {} + + public Platform(String platform) { + if (platform == null) { + throw new IllegalArgumentException("Invalid platform: null"); + } + + String[] parts = platform.split("/"); + if (parts.length == 1) { + this.os = "linux"; + this.architecture = parts[0]; + } else if (parts.length == 2) { + this.os = parts[0]; + this.architecture = parts[1]; + this.variant = getVariant(this.os, this.architecture); + } else if (parts.length == 3) { + this.os = parts[0]; + this.architecture = parts[1]; + this.variant = parts[2]; + } else { + throw new IllegalArgumentException(String.format("Invalid platform: %s", platform)); + } + + if (!SUPPORTED_PLATFORMS.contains(this)) { + throw new IllegalArgumentException(String.format("Image platform is not supported: %s", platform)); + } } - String[] parts = platform.split("/"); - if (parts.length == 1) { - this.os = "linux"; - this.architecture = parts[0]; - } else if (parts.length == 2) { - this.os = parts[0]; - this.architecture = parts[1]; - this.variant = getVariant(this.os, this.architecture); - } else if (parts.length == 3) { - this.os = parts[0]; - this.architecture = parts[1]; - this.variant = parts[2]; - } else { - throw new IllegalArgumentException(String.format("Invalid platform: %s", platform)); + public Platform(String os, String arch, String variant) { + if (arch == null) { + throw new IllegalArgumentException("Invalid platform arch: null"); + } + this.architecture = arch; + + if (os == null) { + this.os = "linux"; + } else { + this.os = os; + } + + if (variant != null) { + this.variant = variant; + } else { + this.variant = getVariant(this.os, this.architecture); + } + + if (!SUPPORTED_PLATFORMS.contains(this)) { + throw new IllegalArgumentException( + String.format("Image platform is not supported: %s/%s/%s", os, arch, variant)); + } } - if (!SUPPORTED_PLATFORMS.contains(this)) { - throw new IllegalArgumentException(String.format("Image platform is not supported: %s", platform)); + static String getVariant(String os, String arch) { + if ("linux".equals(os) && "arm64".equals(arch)) { // in case variant "v8" is not specified + return "v8"; + } + return null; } - } - public Platform(String os, String arch, String variant) { - if (arch == null) { - throw new IllegalArgumentException("Invalid platform arch: null"); + public static boolean isVariantRequired(String os, String arch) { + return "linux".equals(os) && "arm".equals(arch); } - this.architecture = arch; - if (os == null) { - this.os = "linux"; - } else { - this.os = os; + private Platform os(String os) { + this.os = os; + return this; } - if (variant != null) { - this.variant = variant; - } else { - this.variant = getVariant(this.os, this.architecture); + private Platform arch(String arch) { + this.architecture = arch; + return this; } - if (!SUPPORTED_PLATFORMS.contains(this)) { - throw new IllegalArgumentException(String.format("Image platform is not supported: %s/%s/%s", os, arch, variant)); + private Platform variant(String variant) { + this.variant = variant; + return this; } - } - static String getVariant(String os, String arch) { - if ("linux".equals(os) && "arm64".equals(arch)) { // in case variant "v8" is not specified - return "v8"; + public String getOs() { + return os; } - return null; - } - - public static boolean isVariantRequired(String os, String arch) { - return "linux".equals(os) && "arm".equals(arch); - } - - private Platform os(String os) { - this.os = os; - return this; - } - - private Platform arch(String arch) { - this.architecture = arch; - return this; - } - - private Platform variant(String variant) { - this.variant = variant; - return this; - } - - public String getOs() { - return os; - } - - public String getArchitecture() { - return architecture; - } - - public String getVariant() { - return variant; - } - - @Override - public boolean equals(Object o) { - if (this == o) return true; - if (o == null || getClass() != o.getClass()) return false; - Platform platform = (Platform) o; - return Objects.equals(os, platform.os) && Objects.equals(architecture, platform.architecture) && Objects.equals(variant, platform.variant); - } - - @Override - public int hashCode() { - return Objects.hash(os, architecture, variant); - } - - @Override - public String toString() { - if (this.variant == null) { - return String.format("%s/%s", this.os, this.architecture); - } else { - return String.format("%s/%s/%s", this.os, this.architecture, this.variant); + + public String getArchitecture() { + return architecture; + } + + public String getVariant() { + return variant; + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + Platform platform = (Platform) o; + return Objects.equals(os, platform.os) + && Objects.equals(architecture, platform.architecture) + && Objects.equals(variant, platform.variant); + } + + @Override + public int hashCode() { + return Objects.hash(os, architecture, variant); + } + + @Override + public String toString() { + if (this.variant == null) { + return String.format("%s/%s", this.os, this.architecture); + } else { + return String.format("%s/%s/%s", this.os, this.architecture, this.variant); + } } - } } diff --git a/src/main/java/com/redhat/exhort/impl/ExhortApi.java b/src/main/java/com/redhat/exhort/impl/ExhortApi.java index 25f3012c..f3a0aff9 100644 --- a/src/main/java/com/redhat/exhort/impl/ExhortApi.java +++ b/src/main/java/com/redhat/exhort/impl/ExhortApi.java @@ -15,6 +15,22 @@ */ package com.redhat.exhort.impl; +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.databind.DeserializationFeature; +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.github.packageurl.MalformedPackageURLException; +import com.github.packageurl.PackageURL; +import com.redhat.exhort.Api; +import com.redhat.exhort.Provider; +import com.redhat.exhort.api.AnalysisReport; +import com.redhat.exhort.image.ImageRef; +import com.redhat.exhort.image.ImageUtils; +import com.redhat.exhort.logging.LoggersFactory; +import com.redhat.exhort.tools.Ecosystem; +import jakarta.mail.MessagingException; +import jakarta.mail.internet.MimeMultipart; +import jakarta.mail.util.ByteArrayDataSource; import java.io.IOException; import java.io.InputStream; import java.net.URI; @@ -43,541 +59,596 @@ import java.util.stream.Collectors; import java.util.stream.Stream; -import com.fasterxml.jackson.core.JsonProcessingException; -import com.fasterxml.jackson.databind.DeserializationFeature; -import com.fasterxml.jackson.databind.JsonNode; -import com.fasterxml.jackson.databind.ObjectMapper; -import com.github.packageurl.MalformedPackageURLException; -import com.github.packageurl.PackageURL; -import com.redhat.exhort.Api; -import com.redhat.exhort.Provider; -import com.redhat.exhort.api.AnalysisReport; -import com.redhat.exhort.image.ImageRef; -import com.redhat.exhort.image.ImageUtils; -import com.redhat.exhort.logging.LoggersFactory; -import com.redhat.exhort.tools.Ecosystem; - -import jakarta.mail.MessagingException; -import jakarta.mail.internet.MimeMultipart; -import jakarta.mail.util.ByteArrayDataSource; - /** * Concrete implementation of the Exhort {@link Api} Service. **/ public final class ExhortApi implements Api { -// private static final System.Logger LOG = System.getLogger(ExhortApi.class.getName()); + // private static final System.Logger LOG = System.getLogger(ExhortApi.class.getName()); + + private static final Logger LOG = LoggersFactory.getLogger(ExhortApi.class.getName()); - private static final Logger LOG = LoggersFactory.getLogger(ExhortApi.class.getName()); + public static final String DEFAULT_ENDPOINT = "https://rhda.rhcloud.com"; + public static final String DEFAULT_ENDPOINT_DEV = "https://exhort.stage.devshift.net"; + public static final String RHDA_TOKEN_HEADER = "rhda-token"; + public static final String RHDA_SOURCE_HEADER = "rhda-source"; + public static final String RHDA_OPERATION_TYPE_HEADER = "rhda-operation-type"; + public static final String EXHORT_REQUEST_ID_HEADER_NAME = "ex-request-id"; + private final String endpoint; + public String getEndpoint() { + return endpoint; + } - public static final String DEFAULT_ENDPOINT = "https://rhda.rhcloud.com"; - public static final String DEFAULT_ENDPOINT_DEV = "https://exhort.stage.devshift.net"; - public static final String RHDA_TOKEN_HEADER = "rhda-token"; - public static final String RHDA_SOURCE_HEADER = "rhda-source"; - public static final String RHDA_OPERATION_TYPE_HEADER = "rhda-operation-type"; - public static final String EXHORT_REQUEST_ID_HEADER_NAME = "ex-request-id"; + public static final void main(String[] args) throws IOException, InterruptedException, ExecutionException { + System.setProperty("EXHORT_DEV_MODE", "true"); + AnalysisReport analysisReport = new ExhortApi() + .stackAnalysisMixed("/tmp/exhort_test_10582748308498949664/pom.xml") + .get() + .json; + // ObjectMapper om = new ObjectMapper().configure(SerializationFeature.WRITE_EMPTY_JSON_ARRAYS, false); + // System.out.println(om.writerWithDefaultPrettyPrinter().writeValueAsString(analysisReport)); + // AnalysisReport analysisReport = new ExhortApi() + // byte[] analysisReport = new ExhortApi(). + // + // stackAnalysisHtml("/home/zgrinber/git/exhort-java-api/src/test/resources/tst_manifests/golang/go_mod_with_one_ignored_prefix_go/go.mod").get(); + // Path html = Files.createFile(Path.of("/","tmp", "golang0210.html")); + // Files.write(html,analysisReport); - private final String endpoint; + } - public String getEndpoint() { - return endpoint; - } + /** + * Enum for identifying token environment variables and their + * corresponding request headers. + */ + private enum TokenProvider { + SNYK, + OSS_INDEX; + + /** + * Get the expected environment variable name. + * + * @return i.e. EXHORT_SNYK_TOKEN + */ + String getVarName() { + return String.format("EXHORT_%s_TOKEN", this); + } + + String getUserVarName() { + return String.format("EXHORT_%s_USER", this); + } + + /** + * Get the expected request header name. + * + * @return i.e. ex-snyk-token + */ + String getHeaderName() { + return String.format( + "ex-%s-token", this.toString().replace("_", "-").toLowerCase()); + } - public static final void main(String[] args) throws IOException, InterruptedException, ExecutionException { - System.setProperty("EXHORT_DEV_MODE", "true"); - AnalysisReport analysisReport = new ExhortApi() - .stackAnalysisMixed("/tmp/exhort_test_10582748308498949664/pom.xml").get().json; -// ObjectMapper om = new ObjectMapper().configure(SerializationFeature.WRITE_EMPTY_JSON_ARRAYS, false); -// System.out.println(om.writerWithDefaultPrettyPrinter().writeValueAsString(analysisReport)); -// AnalysisReport analysisReport = new ExhortApi() -// byte[] analysisReport = new ExhortApi(). -// stackAnalysisHtml("/home/zgrinber/git/exhort-java-api/src/test/resources/tst_manifests/golang/go_mod_with_one_ignored_prefix_go/go.mod").get(); -// Path html = Files.createFile(Path.of("/","tmp", "golang0210.html")); -// Files.write(html,analysisReport); + String getUserHeaderName() { + return String.format("ex-%s-user", this.toString().replace("_", "-").toLowerCase()); + } + } - } + private final HttpClient client; + private final ObjectMapper mapper; - /** - * Enum for identifying token environment variables and their - * corresponding request headers. - */ - private enum TokenProvider { - SNYK, OSS_INDEX; + private LocalDateTime startTime; + private LocalDateTime providerEndTime; + private LocalDateTime endTime; + public ExhortApi() { + this(HttpClient.newHttpClient()); + } /** - * Get the expected environment variable name. + * Get the HTTP protocol Version set by client in environment variable, if not set, the default is HTTP Protocol Version 1.1 * - * @return i.e. EXHORT_SNYK_TOKEN + * @return i.e. HttpClient.Version.HTTP_1.1 */ - String getVarName() { - return String.format("EXHORT_%s_TOKEN", this); + static HttpClient.Version getHttpVersion() { + return (System.getenv("HTTP_VERSION_EXHORT_CLIENT") != null + && System.getenv("HTTP_VERSION_EXHORT_CLIENT").contains("2")) + ? HttpClient.Version.HTTP_2 + : HttpClient.Version.HTTP_1_1; + } + + ExhortApi(final HttpClient client) { + // // temp system property - as long as prod exhort url not implemented the multi-source v4 endpoint, this + // property needs to be true + // System.setProperty("EXHORT_DEV_MODE","true"); + commonHookBeginning(true); + this.client = client; + this.mapper = new ObjectMapper().disable(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES); + // Take default from config.properties in case client didn't override DEV MODE + if (System.getProperty("EXHORT_DEV_MODE") == null) { + try { + InputStream exhortConfig = this.getClass().getClassLoader().getResourceAsStream("config.properties"); + if (exhortConfig == null) { + LOG.info("config.properties not found on the class path, fallback to default DEV MODE = false"); + System.setProperty("EXHORT_DEV_MODE", "false"); + } else { + Properties properties = new Properties(); + properties.load(exhortConfig); + System.setProperty("EXHORT_DEV_MODE", (String) properties.get("EXHORT_DEV_MODE")); + } + } catch (IOException e) { + LOG.info(String.format( + "Error loading config.properties , fallback to set default property DEV MODE = false, Error message = %s", + e.getMessage())); + System.setProperty("EXHORT_DEV_MODE", "false"); + } + } + + this.endpoint = getExhortUrl(); } - String getUserVarName() { - return String.format("EXHORT_%s_USER", this); + private String commonHookBeginning(boolean startOfApi) { + if (startOfApi) { + generateClientRequestId(); + if (debugLoggingIsNeeded()) { + LOG.info("Start of exhort-java-api client"); + } + } else { + if (Objects.isNull(getClientRequestId())) { + generateClientRequestId(); + } + if (debugLoggingIsNeeded()) { + + this.startTime = LocalDateTime.now(); + + LOG.info(String.format("Starting time: %s", this.startTime)); + } + } + return getClientRequestId(); + } + + private static void generateClientRequestId() { + RequestManager.getInstance().addClientTraceIdToRequest(UUID.randomUUID().toString()); + } + + private static String getClientRequestId() { + return RequestManager.getInstance().getTraceIdOfRequest(); + } + + public String getExhortUrl() { + String endpoint; + if (getBooleanValueEnvironment("EXHORT_DEV_MODE", "false")) { + endpoint = getStringValueEnvironment("DEV_EXHORT_BACKEND_URL", DEFAULT_ENDPOINT_DEV); + + } else { + endpoint = DEFAULT_ENDPOINT; + } + if (debugLoggingIsNeeded()) { + LOG.info(String.format( + "EXHORT_DEV_MODE=%s,DEV_EXHORT_BACKEND_URL=%s, Chosen Backend URL=%s , DEFAULT_ENDPOINT_DEV=%s , DEFAULT_ENDPOINT=%s", + getBooleanValueEnvironment("EXHORT_DEV_MODE", "false"), + getStringValueEnvironment("DEV_EXHORT_BACKEND_URL", DEFAULT_ENDPOINT_DEV), + endpoint, + DEFAULT_ENDPOINT_DEV, + DEFAULT_ENDPOINT)); + } + return endpoint; + } + + public static boolean getBooleanValueEnvironment(String key, String defaultValue) { + String result = Objects.requireNonNullElse( + System.getenv(key), Objects.requireNonNullElse(System.getProperty(key), defaultValue)); + return Boolean.parseBoolean(result.trim().toLowerCase()); + } + + public static String getStringValueEnvironment(String key, String defaultValue) { + String result = Objects.requireNonNullElse( + System.getenv(key), Objects.requireNonNullElse(System.getProperty(key), defaultValue)); + return result; + } + + @Override + public CompletableFuture stackAnalysisMixed(final String manifestFile) throws IOException { + String exClientTraceId = commonHookBeginning(false); + return this.client + .sendAsync( + this.buildStackRequest(manifestFile, MediaType.MULTIPART_MIXED), + HttpResponse.BodyHandlers.ofByteArray()) + .thenApply(resp -> { + RequestManager.getInstance().addClientTraceIdToRequest(exClientTraceId); + if (debugLoggingIsNeeded()) { + logExhortRequestId(resp); + } + if (resp.statusCode() == 200) { + byte[] htmlPart = null; + AnalysisReport jsonPart = null; + var ds = new ByteArrayDataSource(resp.body(), MediaType.MULTIPART_MIXED.toString()); + try { + var mp = new MimeMultipart(ds); + for (var i = 0; i < mp.getCount(); i++) { + if (Objects.isNull(htmlPart) + && MediaType.TEXT_HTML + .toString() + .equals(mp.getBodyPart(i).getContentType())) { + htmlPart = + mp.getBodyPart(i).getInputStream().readAllBytes(); + } + if (Objects.isNull(jsonPart) + && MediaType.APPLICATION_JSON + .toString() + .equals(mp.getBodyPart(i).getContentType())) { + jsonPart = this.mapper.readValue( + mp.getBodyPart(i).getInputStream().readAllBytes(), AnalysisReport.class); + } + } + } catch (IOException | MessagingException e) { + throw new RuntimeException(e); + } + commonHookAfterExhortResponse(); + return new MixedReport(Objects.requireNonNull(htmlPart), Objects.requireNonNull(jsonPart)); + } else { + LOG.severe(String.format( + "failed to invoke stackAnalysisMixed for getting the html and json reports, Http Response Status=%s , received message from server= %s ", + resp.statusCode(), new String(resp.body()))); + return new MixedReport(); + } + }); + } + + @Override + public CompletableFuture stackAnalysisHtml(final String manifestFile) throws IOException { + String exClientTraceId = commonHookBeginning(false); + return this.client + .sendAsync( + this.buildStackRequest(manifestFile, MediaType.TEXT_HTML), + HttpResponse.BodyHandlers.ofByteArray()) + .thenApply(httpResponse -> { + RequestManager.getInstance().addClientTraceIdToRequest(exClientTraceId); + if (debugLoggingIsNeeded()) { + logExhortRequestId(httpResponse); + } + if (httpResponse.statusCode() != 200) { + LOG.severe(String.format( + "failed to invoke stackAnalysis for getting the html report, Http Response Status=%s , received message from server= %s ", + httpResponse.statusCode(), new String(httpResponse.body()))); + } + commonHookAfterExhortResponse(); + return httpResponse.body(); + }) + .exceptionally(exception -> { + LOG.severe(String.format( + "failed to invoke stackAnalysis for getting the html report, received message= %s ", + exception.getMessage())); + // LOG.log(System.Logger.Level.ERROR, "Exception Entity", exception); + commonHookAfterExhortResponse(); + return new byte[0]; + }); + } + + @Override + public CompletableFuture stackAnalysis(final String manifestFile) throws IOException { + String exClientTraceId = commonHookBeginning(false); + return this.client + .sendAsync( + this.buildStackRequest(manifestFile, MediaType.APPLICATION_JSON), + HttpResponse.BodyHandlers.ofString()) + // .thenApply(HttpResponse::body) + .thenApply( + response -> getAnalysisReportFromResponse(response, "StackAnalysis", "json", exClientTraceId)) + .exceptionally(exception -> { + LOG.severe(String.format( + "failed to invoke stackAnalysis for getting the json report, received message= %s ", + exception.getMessage())); + // LOG.log(System.Logger.Level.ERROR, "Exception Entity", exception); + return new AnalysisReport(); + }); + } + + private AnalysisReport getAnalysisReportFromResponse( + HttpResponse response, String operation, String reportName, String exClientTraceId) { + RequestManager.getInstance().addClientTraceIdToRequest(exClientTraceId); + if (debugLoggingIsNeeded()) { + logExhortRequestId(response); + } + if (response.statusCode() == 200) { + if (debugLoggingIsNeeded()) { + LOG.info(String.format( + "Response body received from exhort server : %s %s", System.lineSeparator(), response.body())); + } + commonHookAfterExhortResponse(); + try { + + return this.mapper.readValue(response.body(), AnalysisReport.class); + } catch (JsonProcessingException e) { + throw new CompletionException(e); + } + + } else { + LOG.severe(String.format( + "failed to invoke %s for getting the %s report, Http Response Status=%s , received message from server= %s ", + operation, reportName, response.statusCode(), response.body())); + return new AnalysisReport(); + } + } + + private static void logExhortRequestId(HttpResponse response) { + Optional headerExRequestId = response.headers().allValues(EXHORT_REQUEST_ID_HEADER_NAME).stream() + .findFirst(); + headerExRequestId.ifPresent(value -> LOG.info(String.format( + "Unique Identifier associated with this request ( Received from Exhort Backend ) - ex-request-id= : %s", + value))); + } + + public static boolean debugLoggingIsNeeded() { + return Boolean.parseBoolean(getStringValueEnvironment("EXHORT_DEBUG", "false")); + } + + @Override + public CompletableFuture componentAnalysis(final String manifestType, final byte[] manifestContent) + throws IOException { + String exClientTraceId = commonHookBeginning(false); + var provider = Ecosystem.getProvider(manifestType); + var uri = URI.create(String.format("%s/api/v4/analysis", this.endpoint)); + var content = provider.provideComponent(manifestContent); + commonHookAfterProviderCreatedSbomAndBeforeExhort(); + return getAnalysisReportForComponent(uri, content, exClientTraceId); + } + + private void commonHookAfterProviderCreatedSbomAndBeforeExhort() { + if (debugLoggingIsNeeded()) { + LOG.info("After Provider created sbom hook"); + this.providerEndTime = LocalDateTime.now(); + LOG.info(String.format("After Creating Sbom time: %s", this.startTime)); + LOG.info(String.format( + "Time took to create sbom file to be sent to exhort backend, in ms : %s, in seconds: %s", + this.startTime.until(this.providerEndTime, ChronoUnit.MILLIS), + (float) (this.startTime.until(this.providerEndTime, ChronoUnit.MILLIS) / 1000F))); + } + } + + private void commonHookAfterExhortResponse() { + if (debugLoggingIsNeeded()) { + this.endTime = LocalDateTime.now(); + LOG.info(String.format("After got response from exhort time: %s", this.endTime)); + LOG.info(String.format( + "Time took to get response from exhort backend, in ms: %s, in seconds: %s", + this.providerEndTime.until(this.endTime, ChronoUnit.MILLIS), + this.providerEndTime.until(this.endTime, ChronoUnit.MILLIS) / 1000F)); + LOG.info(String.format( + "Total time took for complete analysis, in ms: %s, in seconds: %s", + this.startTime.until(this.endTime, ChronoUnit.MILLIS), + this.startTime.until(this.endTime, ChronoUnit.MILLIS) / 1000F)); + } + RequestManager.getInstance().removeClientTraceIdFromRequest(); + } + + @Override + public CompletableFuture componentAnalysis(String manifestFile) throws IOException { + String exClientTraceId = commonHookBeginning(false); + var manifestPath = Paths.get(manifestFile); + var provider = Ecosystem.getProvider(manifestPath); + var uri = URI.create(String.format("%s/api/v4/analysis", this.endpoint)); + var content = provider.provideComponent(manifestPath); + commonHookAfterProviderCreatedSbomAndBeforeExhort(); + return getAnalysisReportForComponent(uri, content, exClientTraceId); + } + + private CompletableFuture getAnalysisReportForComponent( + URI uri, Provider.Content content, String exClientTraceId) { + return this.client + .sendAsync( + this.buildRequest(content, uri, MediaType.APPLICATION_JSON, "Component Analysis"), + HttpResponse.BodyHandlers.ofString()) + // .thenApply(HttpResponse::body) + .thenApply(response -> + getAnalysisReportFromResponse(response, "Component Analysis", "json", exClientTraceId)) + .exceptionally(exception -> { + LOG.severe(String.format( + "failed to invoke Component Analysis for getting the json report, received message= %s ", + exception.getMessage())); + // LOG.log(System.Logger.Level.ERROR, "Exception Entity", exception); + return new AnalysisReport(); + }); } /** - * Get the expected request header name. + * Build an HTTP request wrapper for sending to the Backend API for Stack Analysis only. * - * @return i.e. ex-snyk-token + * @param manifestFile the path for the manifest file + * @param acceptType the type of requested content + * @return a HttpRequest ready to be sent to the Backend API + * @throws IOException when failed to load the manifest file */ - String getHeaderName() { - return String.format("ex-%s-token", this.toString().replace("_", "-").toLowerCase()); - } - - String getUserHeaderName() { - return String.format("ex-%s-user", this.toString().replace("_", "-").toLowerCase()); - } - } - - private final HttpClient client; - private final ObjectMapper mapper; - - private LocalDateTime startTime; - private LocalDateTime providerEndTime; - private LocalDateTime endTime; - - public ExhortApi() { - this(HttpClient.newHttpClient()); - } - - /** - * Get the HTTP protocol Version set by client in environment variable, if not set, the default is HTTP Protocol Version 1.1 - * - * @return i.e. HttpClient.Version.HTTP_1.1 - */ - static HttpClient.Version getHttpVersion() { - return (System.getenv("HTTP_VERSION_EXHORT_CLIENT") != null && System.getenv("HTTP_VERSION_EXHORT_CLIENT").contains("2")) ? HttpClient.Version.HTTP_2 : HttpClient.Version.HTTP_1_1; - } - - ExhortApi(final HttpClient client) { -// // temp system property - as long as prod exhort url not implemented the multi-source v4 endpoint, this property needs to be true -// System.setProperty("EXHORT_DEV_MODE","true"); - commonHookBeginning(true); - this.client = client; - this.mapper = new ObjectMapper().disable(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES); - // Take default from config.properties in case client didn't override DEV MODE - if (System.getProperty("EXHORT_DEV_MODE") == null) { - try { - InputStream exhortConfig = this.getClass().getClassLoader().getResourceAsStream("config.properties"); - if (exhortConfig == null) { - LOG.info("config.properties not found on the class path, fallback to default DEV MODE = false"); - System.setProperty("EXHORT_DEV_MODE", "false"); + private HttpRequest buildStackRequest(final String manifestFile, final MediaType acceptType) throws IOException { + var manifestPath = Paths.get(manifestFile); + var provider = Ecosystem.getProvider(manifestPath); + var uri = URI.create(String.format("%s/api/v4/analysis", this.endpoint)); + var content = provider.provideStack(manifestPath); + commonHookAfterProviderCreatedSbomAndBeforeExhort(); + + return buildRequest(content, uri, acceptType, "Stack Analysis"); + } + + @Override + public CompletableFuture> imageAnalysis(final Set imageRefs) + throws IOException { + return this.performBatchAnalysis( + () -> getBatchImageSboms(imageRefs), + MediaType.APPLICATION_JSON, + HttpResponse.BodyHandlers.ofString(), + this::getBatchImageAnalysisReports, + Collections::emptyMap, + "Image Analysis"); + } + + @Override + public CompletableFuture imageAnalysisHtml(Set imageRefs) throws IOException { + return this.performBatchAnalysis( + () -> getBatchImageSboms(imageRefs), + MediaType.TEXT_HTML, + HttpResponse.BodyHandlers.ofByteArray(), + HttpResponse::body, + () -> new byte[0], + "Image Analysis"); + } + + Map getBatchImageSboms(final Set imageRefs) { + return imageRefs.parallelStream() + .map(imageRef -> { + try { + return new AbstractMap.SimpleEntry<>( + imageRef.getPackageURL().canonicalize(), ImageUtils.generateImageSBOM(imageRef)); + } catch (IOException | MalformedPackageURLException ex) { + throw new RuntimeException(ex); + } + }) + .collect(Collectors.toMap(AbstractMap.SimpleEntry::getKey, AbstractMap.SimpleEntry::getValue)); + } + + Map getBatchImageAnalysisReports(final HttpResponse httpResponse) { + if (httpResponse.statusCode() == 200) { + try { + Map reports = this.mapper.readValue(httpResponse.body(), Map.class); + return reports.entrySet().stream() + .collect(Collectors.toMap( + e -> { + try { + return new ImageRef( + new PackageURL(e.getKey().toString())); + } catch (MalformedPackageURLException ex) { + throw new RuntimeException(ex); + } + }, + e -> mapper.convertValue(e.getValue(), AnalysisReport.class))); + } catch (JsonProcessingException e) { + throw new CompletionException(e); + } } else { - Properties properties = new Properties(); - properties.load(exhortConfig); - System.setProperty("EXHORT_DEV_MODE", (String) properties.get("EXHORT_DEV_MODE")); + return Collections.emptyMap(); } - } catch (IOException e) { - LOG.info(String.format("Error loading config.properties , fallback to set default property DEV MODE = false, Error message = %s", e.getMessage())); - System.setProperty("EXHORT_DEV_MODE", "false"); - } - } - - this.endpoint = getExhortUrl(); - } - - private String commonHookBeginning(boolean startOfApi) { - if(startOfApi) { - generateClientRequestId(); - if (debugLoggingIsNeeded()) { - LOG.info("Start of exhort-java-api client"); - } - } - else { - if(Objects.isNull(getClientRequestId())) { - generateClientRequestId(); - } - if (debugLoggingIsNeeded()) { - - this.startTime = LocalDateTime.now(); - - LOG.info(String.format("Starting time: %s", this.startTime)); - } - } - return getClientRequestId(); - } - - private static void generateClientRequestId() { - RequestManager.getInstance().addClientTraceIdToRequest(UUID.randomUUID().toString()); - } - - private static String getClientRequestId() { - return RequestManager.getInstance().getTraceIdOfRequest(); - } - - public String getExhortUrl() { - String endpoint; - if (getBooleanValueEnvironment("EXHORT_DEV_MODE", "false")) { - endpoint = getStringValueEnvironment("DEV_EXHORT_BACKEND_URL", DEFAULT_ENDPOINT_DEV); - - } else { - endpoint = DEFAULT_ENDPOINT; - } - if (debugLoggingIsNeeded()) { - LOG.info(String.format("EXHORT_DEV_MODE=%s,DEV_EXHORT_BACKEND_URL=%s, Chosen Backend URL=%s , DEFAULT_ENDPOINT_DEV=%s , DEFAULT_ENDPOINT=%s", getBooleanValueEnvironment("EXHORT_DEV_MODE", "false"), getStringValueEnvironment("DEV_EXHORT_BACKEND_URL", DEFAULT_ENDPOINT_DEV), endpoint, DEFAULT_ENDPOINT_DEV, DEFAULT_ENDPOINT)); - } - return endpoint; - } - - public static boolean getBooleanValueEnvironment(String key, String defaultValue) { - String result = Objects.requireNonNullElse(System.getenv(key), Objects.requireNonNullElse(System.getProperty(key), defaultValue)); - return Boolean.parseBoolean(result.trim().toLowerCase()); - } - - public static String getStringValueEnvironment(String key, String defaultValue) { - String result = Objects.requireNonNullElse(System.getenv(key), Objects.requireNonNullElse(System.getProperty(key), defaultValue)); - return result; - } - - @Override - public CompletableFuture stackAnalysisMixed(final String manifestFile) throws IOException { - String exClientTraceId = commonHookBeginning(false); - return this.client.sendAsync(this.buildStackRequest(manifestFile, MediaType.MULTIPART_MIXED), HttpResponse.BodyHandlers.ofByteArray()).thenApply(resp -> { - RequestManager.getInstance().addClientTraceIdToRequest(exClientTraceId); - if(debugLoggingIsNeeded()) { - logExhortRequestId(resp); - } - if (resp.statusCode() == 200) { - byte[] htmlPart = null; - AnalysisReport jsonPart = null; - var ds = new ByteArrayDataSource(resp.body(), MediaType.MULTIPART_MIXED.toString()); - try { - var mp = new MimeMultipart(ds); - for (var i = 0; i < mp.getCount(); i++) { - if (Objects.isNull(htmlPart) && MediaType.TEXT_HTML.toString().equals(mp.getBodyPart(i).getContentType())) { - htmlPart = mp.getBodyPart(i).getInputStream().readAllBytes(); - } - if (Objects.isNull(jsonPart) && MediaType.APPLICATION_JSON.toString().equals(mp.getBodyPart(i).getContentType())) { - jsonPart = this.mapper.readValue(mp.getBodyPart(i).getInputStream().readAllBytes(), AnalysisReport.class); + } + + CompletableFuture performBatchAnalysis( + final Supplier> sbomsGenerator, + final MediaType mediaType, + final HttpResponse.BodyHandler responseBodyHandler, + final Function, T> responseGenerator, + final Supplier exceptionResponseGenerator, + final String analysisName) + throws IOException { + String exClientTraceId = commonHookBeginning(false); + var uri = URI.create(String.format("%s/api/v4/batch-analysis", this.endpoint)); + var sboms = sbomsGenerator.get(); + var content = new Provider.Content( + mapper.writeValueAsString(sboms).getBytes(StandardCharsets.UTF_8), Api.CYCLONEDX_MEDIA_TYPE); + commonHookAfterProviderCreatedSbomAndBeforeExhort(); + return this.client + .sendAsync(this.buildRequest(content, uri, mediaType, analysisName), responseBodyHandler) + .thenApply(response -> getBatchAnalysisReportsFromResponse( + response, responseGenerator, analysisName, "json", exClientTraceId)) + .exceptionally(exception -> { + LOG.severe(String.format( + "failed to invoke %s for getting the json report, received message= %s ", + analysisName, exception.getMessage())); + commonHookAfterExhortResponse(); + return exceptionResponseGenerator.get(); + }); + } + + T getBatchAnalysisReportsFromResponse( + final HttpResponse response, + final Function, T> responseGenerator, + final String operation, + final String reportName, + final String exClientTraceId) { + RequestManager.getInstance().addClientTraceIdToRequest(exClientTraceId); + if (debugLoggingIsNeeded()) { + logExhortRequestId(response); + } + if (response.statusCode() == 200) { + if (debugLoggingIsNeeded()) { + LOG.info(String.format( + "Response body received from exhort server : %s %s", System.lineSeparator(), response.body())); } - } - } catch (IOException | MessagingException e) { - throw new RuntimeException(e); + } else { + LOG.severe(String.format( + "failed to invoke %s for getting the %s report, Http Response Status=%s , " + + "received message from server= %s ", + operation, reportName, response.statusCode(), response.body())); } commonHookAfterExhortResponse(); - return new MixedReport(Objects.requireNonNull(htmlPart), Objects.requireNonNull(jsonPart)); - } else { - LOG.severe(String.format("failed to invoke stackAnalysisMixed for getting the html and json reports, Http Response Status=%s , received message from server= %s ", resp.statusCode(), new String(resp.body()))); - return new MixedReport(); - } - }); - } - - @Override - public CompletableFuture stackAnalysisHtml(final String manifestFile) throws IOException { - String exClientTraceId = commonHookBeginning(false); - return this.client.sendAsync(this.buildStackRequest(manifestFile, MediaType.TEXT_HTML), HttpResponse.BodyHandlers.ofByteArray()).thenApply(httpResponse -> { - RequestManager.getInstance().addClientTraceIdToRequest(exClientTraceId); - if(debugLoggingIsNeeded()) { - logExhortRequestId(httpResponse); - } - if (httpResponse.statusCode() != 200) { - LOG.severe(String.format("failed to invoke stackAnalysis for getting the html report, Http Response Status=%s , received message from server= %s ", httpResponse.statusCode(), new String(httpResponse.body()))); - } - commonHookAfterExhortResponse(); - return httpResponse.body(); - }).exceptionally(exception -> { - LOG.severe(String.format("failed to invoke stackAnalysis for getting the html report, received message= %s ", exception.getMessage())); -// LOG.log(System.Logger.Level.ERROR, "Exception Entity", exception); - commonHookAfterExhortResponse(); - return new byte[0]; - }); - } - - @Override - public CompletableFuture stackAnalysis(final String manifestFile) throws IOException { - String exClientTraceId = commonHookBeginning(false); - return this.client.sendAsync(this.buildStackRequest(manifestFile, MediaType.APPLICATION_JSON), HttpResponse.BodyHandlers.ofString()) -// .thenApply(HttpResponse::body) - .thenApply(response -> getAnalysisReportFromResponse(response, "StackAnalysis", "json",exClientTraceId)).exceptionally(exception -> { - LOG.severe(String.format("failed to invoke stackAnalysis for getting the json report, received message= %s ", exception.getMessage())); -// LOG.log(System.Logger.Level.ERROR, "Exception Entity", exception); - return new AnalysisReport(); - }); - } - - private AnalysisReport getAnalysisReportFromResponse(HttpResponse response, String operation, String reportName,String exClientTraceId) { - RequestManager.getInstance().addClientTraceIdToRequest(exClientTraceId); - if (debugLoggingIsNeeded()) { - logExhortRequestId(response); - } - if (response.statusCode() == 200) { - if (debugLoggingIsNeeded()) { - LOG.info(String.format("Response body received from exhort server : %s %s", System.lineSeparator(), response.body())); - - } - commonHookAfterExhortResponse(); - try { - - return this.mapper.readValue(response.body(), AnalysisReport.class); - } catch (JsonProcessingException e) { - throw new CompletionException(e); - } - - } else { - LOG.severe(String.format("failed to invoke %s for getting the %s report, Http Response Status=%s , received message from server= %s ", operation, reportName, response.statusCode(), response.body())); - return new AnalysisReport(); - } - - } - - private static void logExhortRequestId(HttpResponse response) { - Optional headerExRequestId = response.headers().allValues(EXHORT_REQUEST_ID_HEADER_NAME).stream().findFirst(); - headerExRequestId.ifPresent(value -> LOG.info(String.format("Unique Identifier associated with this request ( Received from Exhort Backend ) - ex-request-id= : %s", value))); - } - - public static boolean debugLoggingIsNeeded() { - return Boolean.parseBoolean(getStringValueEnvironment("EXHORT_DEBUG","false")); - } - - @Override - public CompletableFuture componentAnalysis(final String manifestType, final byte[] manifestContent) throws IOException { - String exClientTraceId = commonHookBeginning(false); - var provider = Ecosystem.getProvider(manifestType); - var uri = URI.create(String.format("%s/api/v4/analysis", this.endpoint)); - var content = provider.provideComponent(manifestContent); - commonHookAfterProviderCreatedSbomAndBeforeExhort(); - return getAnalysisReportForComponent(uri, content,exClientTraceId); - } - - private void commonHookAfterProviderCreatedSbomAndBeforeExhort() { - if(debugLoggingIsNeeded()) { - LOG.info("After Provider created sbom hook"); - this.providerEndTime = LocalDateTime.now(); - LOG.info(String.format("After Creating Sbom time: %s", this.startTime)); - LOG.info(String.format("Time took to create sbom file to be sent to exhort backend, in ms : %s, in seconds: %s",this.startTime.until(this.providerEndTime, ChronoUnit.MILLIS),(float)(this.startTime.until(this.providerEndTime, ChronoUnit.MILLIS) / 1000F))); - } - - - - } - - private void commonHookAfterExhortResponse() { - if(debugLoggingIsNeeded()) { - this.endTime = LocalDateTime.now(); - LOG.info(String.format("After got response from exhort time: %s", this.endTime)); - LOG.info(String.format("Time took to get response from exhort backend, in ms: %s, in seconds: %s",this.providerEndTime.until(this.endTime, ChronoUnit.MILLIS),this.providerEndTime.until(this.endTime, ChronoUnit.MILLIS) / 1000F)); - LOG.info(String.format("Total time took for complete analysis, in ms: %s, in seconds: %s",this.startTime.until(this.endTime, ChronoUnit.MILLIS),this.startTime.until(this.endTime, ChronoUnit.MILLIS) / 1000F)); - - } - RequestManager.getInstance().removeClientTraceIdFromRequest(); - } - @Override - public CompletableFuture componentAnalysis(String manifestFile) throws IOException { - String exClientTraceId = commonHookBeginning(false); - var manifestPath = Paths.get(manifestFile); - var provider = Ecosystem.getProvider(manifestPath); - var uri = URI.create(String.format("%s/api/v4/analysis", this.endpoint)); - var content = provider.provideComponent(manifestPath); - commonHookAfterProviderCreatedSbomAndBeforeExhort(); - return getAnalysisReportForComponent(uri, content, exClientTraceId); - } - - private CompletableFuture getAnalysisReportForComponent(URI uri, Provider.Content content, String exClientTraceId) { - return this.client.sendAsync(this.buildRequest(content, uri, MediaType.APPLICATION_JSON, "Component Analysis"), HttpResponse.BodyHandlers.ofString()) -// .thenApply(HttpResponse::body) - .thenApply(response -> getAnalysisReportFromResponse(response, "Component Analysis", "json",exClientTraceId)).exceptionally(exception -> { - LOG.severe( String.format("failed to invoke Component Analysis for getting the json report, received message= %s ", exception.getMessage())); -// LOG.log(System.Logger.Level.ERROR, "Exception Entity", exception); - return new AnalysisReport(); - }); - } - - /** - * Build an HTTP request wrapper for sending to the Backend API for Stack Analysis only. - * - * @param manifestFile the path for the manifest file - * @param acceptType the type of requested content - * @return a HttpRequest ready to be sent to the Backend API - * @throws IOException when failed to load the manifest file - */ - private HttpRequest buildStackRequest(final String manifestFile, final MediaType acceptType) throws IOException { - var manifestPath = Paths.get(manifestFile); - var provider = Ecosystem.getProvider(manifestPath); - var uri = URI.create(String.format("%s/api/v4/analysis", this.endpoint)); - var content = provider.provideStack(manifestPath); - commonHookAfterProviderCreatedSbomAndBeforeExhort(); - - return buildRequest(content, uri, acceptType, "Stack Analysis"); - } - - @Override - public CompletableFuture> imageAnalysis(final Set imageRefs) throws IOException { - return this.performBatchAnalysis( - () -> getBatchImageSboms(imageRefs), - MediaType.APPLICATION_JSON, - HttpResponse.BodyHandlers.ofString(), - this::getBatchImageAnalysisReports, - Collections::emptyMap, - "Image Analysis"); - } - - @Override - public CompletableFuture imageAnalysisHtml(Set imageRefs) throws IOException { - return this.performBatchAnalysis( - () -> getBatchImageSboms(imageRefs), - MediaType.TEXT_HTML, - HttpResponse.BodyHandlers.ofByteArray(), - HttpResponse::body, - () -> new byte[0], - "Image Analysis"); - } - - Map getBatchImageSboms(final Set imageRefs) { - return imageRefs.parallelStream().map(imageRef -> { - try { - return new AbstractMap.SimpleEntry<>( - imageRef.getPackageURL().canonicalize(), - ImageUtils.generateImageSBOM(imageRef)); - } catch (IOException | MalformedPackageURLException ex) { - throw new RuntimeException(ex); - } - }).collect(Collectors.toMap( - AbstractMap.SimpleEntry::getKey, - AbstractMap.SimpleEntry::getValue - )); - } - - Map getBatchImageAnalysisReports(final HttpResponse httpResponse) { - if (httpResponse.statusCode() == 200) { - try { - Map reports = this.mapper.readValue(httpResponse.body(), Map.class); - return reports.entrySet().stream().collect(Collectors.toMap( - e -> { - try { - return new ImageRef(new PackageURL(e.getKey().toString())); - } catch (MalformedPackageURLException ex) { - throw new RuntimeException(ex); + return responseGenerator.apply(response); + } + + /** + * Build an HTTP request for sending to the Backend API. + * + * @param content the {@link com.redhat.exhort.Provider.Content} info for the request body + * @param uri the {@link URI} for sending the request to + * @param acceptType value the Accept header in the request, indicating the required response type + * @return a HttpRequest ready to be sent to the Backend API + */ + private HttpRequest buildRequest( + final Provider.Content content, final URI uri, final MediaType acceptType, final String analysisType) { + var request = HttpRequest.newBuilder(uri) + .version(Version.HTTP_1_1) + .setHeader("Accept", acceptType.toString()) + .setHeader("Content-Type", content.type) + .POST(HttpRequest.BodyPublishers.ofString(new String(content.buffer))); + + // include tokens from environment variables of java properties as request headers + Stream.of(ExhortApi.TokenProvider.values()).forEach(p -> { + var envToken = System.getenv(p.getVarName()); + if (Objects.nonNull(envToken)) { + request.setHeader(p.getHeaderName(), envToken); + } else { + var propToken = System.getProperty(p.getVarName()); + if (Objects.nonNull(propToken)) { + request.setHeader(p.getHeaderName(), propToken); + } } - }, - e -> mapper.convertValue(e.getValue(), AnalysisReport.class) - )); - } catch (JsonProcessingException e) { - throw new CompletionException(e); - } - } else { - return Collections.emptyMap(); - } - } - - CompletableFuture performBatchAnalysis(final Supplier> sbomsGenerator, - final MediaType mediaType, - final HttpResponse.BodyHandler responseBodyHandler, - final Function, T> responseGenerator, - final Supplier exceptionResponseGenerator, - final String analysisName) throws IOException { - String exClientTraceId = commonHookBeginning(false); - var uri = URI.create(String.format("%s/api/v4/batch-analysis", this.endpoint)); - var sboms = sbomsGenerator.get(); - var content = new Provider.Content( - mapper.writeValueAsString(sboms).getBytes(StandardCharsets.UTF_8), - Api.CYCLONEDX_MEDIA_TYPE); - commonHookAfterProviderCreatedSbomAndBeforeExhort(); - return this.client - .sendAsync( - this.buildRequest(content, uri, mediaType, analysisName), responseBodyHandler) - .thenApply( - response -> getBatchAnalysisReportsFromResponse(response, responseGenerator, analysisName, - "json", exClientTraceId) - ).exceptionally(exception -> { - LOG.severe(String.format("failed to invoke %s for getting the json report, received message= %s ", - analysisName, exception.getMessage())); - commonHookAfterExhortResponse(); - return exceptionResponseGenerator.get(); - }); - } - - T getBatchAnalysisReportsFromResponse(final HttpResponse response, - final Function, T> responseGenerator, - final String operation, final String reportName, - final String exClientTraceId) { - RequestManager.getInstance().addClientTraceIdToRequest(exClientTraceId); - if (debugLoggingIsNeeded()) { - logExhortRequestId(response); - } - if (response.statusCode() == 200) { - if (debugLoggingIsNeeded()) { - LOG.info(String.format("Response body received from exhort server : %s %s", - System.lineSeparator(), response.body())); - } - } else { - LOG.severe(String.format("failed to invoke %s for getting the %s report, Http Response Status=%s , " + - "received message from server= %s ", operation, reportName, response.statusCode(), response.body())); - } - commonHookAfterExhortResponse(); - return responseGenerator.apply(response); - } - - /** - * Build an HTTP request for sending to the Backend API. - * - * @param content the {@link com.redhat.exhort.Provider.Content} info for the request body - * @param uri the {@link URI} for sending the request to - * @param acceptType value the Accept header in the request, indicating the required response type - * @return a HttpRequest ready to be sent to the Backend API - */ - private HttpRequest buildRequest(final Provider.Content content, final URI uri, final MediaType acceptType, final String analysisType) { - var request = HttpRequest.newBuilder(uri).version(Version.HTTP_1_1).setHeader("Accept", acceptType.toString()).setHeader("Content-Type", content.type).POST(HttpRequest.BodyPublishers.ofString(new String(content.buffer))); - - // include tokens from environment variables of java properties as request headers - Stream.of(ExhortApi.TokenProvider.values()).forEach(p -> { - var envToken = System.getenv(p.getVarName()); - if (Objects.nonNull(envToken)) { - request.setHeader(p.getHeaderName(), envToken); - } else { - var propToken = System.getProperty(p.getVarName()); - if (Objects.nonNull(propToken)) { - request.setHeader(p.getHeaderName(), propToken); + var envUser = System.getenv(p.getUserHeaderName()); + if (Objects.nonNull(envUser)) { + request.setHeader(p.getUserHeaderName(), envUser); + } else { + var propUser = System.getProperty(p.getUserVarName()); + if (Objects.nonNull(propUser)) { + request.setHeader(p.getUserHeaderName(), propUser); + } + } + }); + // set rhda-token + // Environment variable/property name = RHDA_TOKEN + String rhdaToken = calculateHeaderValue(RHDA_TOKEN_HEADER); + if (rhdaToken != null && Optional.of(rhdaToken).isPresent()) { + request.setHeader(RHDA_TOKEN_HEADER, rhdaToken); } - } - var envUser = System.getenv(p.getUserHeaderName()); - if (Objects.nonNull(envUser)) { - request.setHeader(p.getUserHeaderName(), envUser); - } else { - var propUser = System.getProperty(p.getUserVarName()); - if (Objects.nonNull(propUser)) { - request.setHeader(p.getUserHeaderName(), propUser); + // set rhda-source ( extension/plugin id/name) + // Environment variable/property name = RHDA_SOURCE + String rhdaSource = calculateHeaderValue(RHDA_SOURCE_HEADER); + if (rhdaSource != null && Optional.of(rhdaSource).isPresent()) { + request.setHeader(RHDA_SOURCE_HEADER, rhdaSource); } - } - - }); - //set rhda-token - // Environment variable/property name = RHDA_TOKEN - String rhdaToken = calculateHeaderValue(RHDA_TOKEN_HEADER); - if (rhdaToken != null && Optional.of(rhdaToken).isPresent()) { - request.setHeader(RHDA_TOKEN_HEADER, rhdaToken); - } - //set rhda-source ( extension/plugin id/name) - // Environment variable/property name = RHDA_SOURCE - String rhdaSource = calculateHeaderValue(RHDA_SOURCE_HEADER); - if (rhdaSource != null && Optional.of(rhdaSource).isPresent()) { - request.setHeader(RHDA_SOURCE_HEADER, rhdaSource); - } - request.setHeader(RHDA_OPERATION_TYPE_HEADER, analysisType); - - return request.build(); - } - - private String calculateHeaderValue(String headerName) { - String result; - result = calculateHeaderValueActual(headerName); - if (result == null) { - result = calculateHeaderValueActual(headerName.toUpperCase().replace("-", "_")); - } - return result; - } - - private String calculateHeaderValueActual(String headerName) { - String result = null; - result = System.getenv(headerName); - if (result == null) { - result = System.getProperty(headerName); - } - return result; - } + request.setHeader(RHDA_OPERATION_TYPE_HEADER, analysisType); + + return request.build(); + } + + private String calculateHeaderValue(String headerName) { + String result; + result = calculateHeaderValueActual(headerName); + if (result == null) { + result = calculateHeaderValueActual(headerName.toUpperCase().replace("-", "_")); + } + return result; + } + + private String calculateHeaderValueActual(String headerName) { + String result = null; + result = System.getenv(headerName); + if (result == null) { + result = System.getProperty(headerName); + } + return result; + } } diff --git a/src/main/java/com/redhat/exhort/impl/RequestManager.java b/src/main/java/com/redhat/exhort/impl/RequestManager.java index f40da1cf..ea4c4ea0 100644 --- a/src/main/java/com/redhat/exhort/impl/RequestManager.java +++ b/src/main/java/com/redhat/exhort/impl/RequestManager.java @@ -21,37 +21,35 @@ public class RequestManager { - - private static RequestManager requestManager; - private Map requests; - - public static RequestManager getInstance() - { - if(Objects.isNull(requestManager)) { - requestManager = new RequestManager(); + private static RequestManager requestManager; + private Map requests; + + public static RequestManager getInstance() { + if (Objects.isNull(requestManager)) { + requestManager = new RequestManager(); + } + return requestManager; } - return requestManager; - } - private RequestManager() - { - requests = new HashMap<>(); - } - - public synchronized void addClientTraceIdToRequest(String requestId) { - requests.put(concatenatedThreadId(), requestId); - } + private RequestManager() { + requests = new HashMap<>(); + } - public synchronized void removeClientTraceIdFromRequest() { - requests.remove(concatenatedThreadId()); - } + public synchronized void addClientTraceIdToRequest(String requestId) { + requests.put(concatenatedThreadId(), requestId); + } - public String getTraceIdOfRequest() { - return requests.get(concatenatedThreadId()); - } + public synchronized void removeClientTraceIdFromRequest() { + requests.remove(concatenatedThreadId()); + } - private static String concatenatedThreadId() { - return String.format("%s-%s",Thread.currentThread().getName(),Thread.currentThread().getId()); - } + public String getTraceIdOfRequest() { + return requests.get(concatenatedThreadId()); + } + private static String concatenatedThreadId() { + return String.format( + "%s-%s", + Thread.currentThread().getName(), Thread.currentThread().getId()); + } } diff --git a/src/main/java/com/redhat/exhort/logging/ClientTraceIdSimpleFormatter.java b/src/main/java/com/redhat/exhort/logging/ClientTraceIdSimpleFormatter.java index c42a4a91..623d54c6 100644 --- a/src/main/java/com/redhat/exhort/logging/ClientTraceIdSimpleFormatter.java +++ b/src/main/java/com/redhat/exhort/logging/ClientTraceIdSimpleFormatter.java @@ -19,12 +19,9 @@ import com.fasterxml.jackson.core.JsonProcessingException; import com.fasterxml.jackson.databind.ObjectMapper; import com.fasterxml.jackson.databind.SerializationFeature; -import com.fasterxml.jackson.databind.module.SimpleModule; import com.redhat.exhort.impl.RequestManager; - import java.io.PrintWriter; import java.io.StringWriter; -import java.time.LocalDateTime; import java.time.ZoneId; import java.time.ZonedDateTime; import java.util.HashMap; @@ -35,105 +32,105 @@ public class ClientTraceIdSimpleFormatter extends SimpleFormatter { + private final ObjectMapper objectMapper; - private final ObjectMapper objectMapper; + public ClientTraceIdSimpleFormatter() { + this.objectMapper = new ObjectMapper(); + this.objectMapper.disable(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS); + } - public ClientTraceIdSimpleFormatter() { - this.objectMapper = new ObjectMapper(); - this.objectMapper.disable(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS); - } - @Override - public String format(LogRecord record) { -// return String.format("%s, ex-client-trace-id: %s",super.format(record).trim(),RequestManager.getInstance().getTraceIdOfRequest() + System.lineSeparator()); - Map messageKeysValues = new HashMap<>(); - ObjectMapper objectMapper = new ObjectMapper(); - objectMapper.disable(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS); + @Override + public String format(LogRecord record) { + // return String.format("%s, ex-client-trace-id: + // %s",super.format(record).trim(),RequestManager.getInstance().getTraceIdOfRequest() + System.lineSeparator()); + Map messageKeysValues = new HashMap<>(); + ObjectMapper objectMapper = new ObjectMapper(); + objectMapper.disable(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS); - ZonedDateTime zdt = ZonedDateTime.ofInstant( - record.getInstant(), ZoneId.systemDefault()); - String source; - if (record.getSourceClassName() != null) { - source = record.getSourceClassName(); - if (record.getSourceMethodName() != null) { - source += " " + record.getSourceMethodName(); - } - } else { - source = record.getLoggerName(); - } - String message = formatMessage(record); - String throwable = ""; - if (record.getThrown() != null) { - StringWriter sw = new StringWriter(); - PrintWriter pw = new PrintWriter(sw); - pw.println(); - record.getThrown().printStackTrace(pw); - pw.close(); - throwable = sw.toString(); - } -// return String.format(super.format, -// zdt, -// source, -// record.getLoggerName(), -// record.getLevel().getLocalizedLevelName(), -// message, -// throwable); - messageKeysValues.put("timestamp",zdt.toString()); - messageKeysValues.put("ex-client-trace-id", RequestManager.getInstance().getTraceIdOfRequest()); - messageKeysValues.put("methodName",source); - messageKeysValues.put("loggerName",record.getLoggerName()); - messageKeysValues.put("logLevel",record.getLevel().toString()); - messageKeysValues.put("threadName",Thread.currentThread().getName()); - messageKeysValues.put("threadId",Thread.currentThread().getId()); - String jsonPartOfMessage = getJsonPartOfMessage(message); - if(isValidJson(jsonPartOfMessage) || messageContainsOutputStructure(message)) { - messageKeysValues.put("logMessage", "log Message Contains a structure , and it will follow after the log entry"); - } - else { - messageKeysValues.put("logMessage", message); - } - try { - String jsonLogRecord = objectMapper.writeValueAsString(messageKeysValues) + System.lineSeparator(); - return jsonLogRecord + suffixRequired(messageKeysValues,message); - } catch (JsonProcessingException e) { - return String.format("%s, ex-client-trace-id: %s",super.format(record).trim(),RequestManager.getInstance().getTraceIdOfRequest() + System.lineSeparator()); + ZonedDateTime zdt = ZonedDateTime.ofInstant(record.getInstant(), ZoneId.systemDefault()); + String source; + if (record.getSourceClassName() != null) { + source = record.getSourceClassName(); + if (record.getSourceMethodName() != null) { + source += " " + record.getSourceMethodName(); + } + } else { + source = record.getLoggerName(); + } + String message = formatMessage(record); + String throwable = ""; + if (record.getThrown() != null) { + StringWriter sw = new StringWriter(); + PrintWriter pw = new PrintWriter(sw); + pw.println(); + record.getThrown().printStackTrace(pw); + pw.close(); + throwable = sw.toString(); + } + // return String.format(super.format, + // zdt, + // source, + // record.getLoggerName(), + // record.getLevel().getLocalizedLevelName(), + // message, + // throwable); + messageKeysValues.put("timestamp", zdt.toString()); + messageKeysValues.put("ex-client-trace-id", RequestManager.getInstance().getTraceIdOfRequest()); + messageKeysValues.put("methodName", source); + messageKeysValues.put("loggerName", record.getLoggerName()); + messageKeysValues.put("logLevel", record.getLevel().toString()); + messageKeysValues.put("threadName", Thread.currentThread().getName()); + messageKeysValues.put("threadId", Thread.currentThread().getId()); + String jsonPartOfMessage = getJsonPartOfMessage(message); + if (isValidJson(jsonPartOfMessage) || messageContainsOutputStructure(message)) { + messageKeysValues.put( + "logMessage", "log Message Contains a structure , and it will follow after the log entry"); + } else { + messageKeysValues.put("logMessage", message); + } + try { + String jsonLogRecord = objectMapper.writeValueAsString(messageKeysValues) + System.lineSeparator(); + return jsonLogRecord + suffixRequired(messageKeysValues, message); + } catch (JsonProcessingException e) { + return String.format( + "%s, ex-client-trace-id: %s", + super.format(record).trim(), + RequestManager.getInstance().getTraceIdOfRequest() + System.lineSeparator()); + } } - } - private String suffixRequired(Map messageKeysValues, String message) { - if(((String)messageKeysValues.get("logMessage")).trim().contains("log Message Contains a structure")) { - return message.trim() + System.lineSeparator(); + private String suffixRequired(Map messageKeysValues, String message) { + if (((String) messageKeysValues.get("logMessage")).trim().contains("log Message Contains a structure")) { + return message.trim() + System.lineSeparator(); + } else { + return ""; + } } - else { - return ""; - } - } - private boolean messageContainsOutputStructure(String message) - { - String messageWithLC = message.toLowerCase(); - return messageWithLC.contains("package manager") && messageWithLC.contains("output"); - } - private boolean isValidJson(String jsonPartOfMessage) { - if (Objects.isNull(jsonPartOfMessage)) { - return false; - } - try { - objectMapper.readTree(jsonPartOfMessage); - } catch (JacksonException e) { - return false; - } - return true; - } - private String getJsonPartOfMessage(String message) { - int startOfJson = message.indexOf("{"); - int endOfJson = message.lastIndexOf("}"); - if( startOfJson > -1 && endOfJson > 0) { - return message.substring(startOfJson,endOfJson + 1); - } - else { - return null; + private boolean messageContainsOutputStructure(String message) { + String messageWithLC = message.toLowerCase(); + return messageWithLC.contains("package manager") && messageWithLC.contains("output"); } + private boolean isValidJson(String jsonPartOfMessage) { + if (Objects.isNull(jsonPartOfMessage)) { + return false; + } + try { + objectMapper.readTree(jsonPartOfMessage); + } catch (JacksonException e) { + return false; + } + return true; + } - } + private String getJsonPartOfMessage(String message) { + int startOfJson = message.indexOf("{"); + int endOfJson = message.lastIndexOf("}"); + if (startOfJson > -1 && endOfJson > 0) { + return message.substring(startOfJson, endOfJson + 1); + } else { + return null; + } + } } diff --git a/src/main/java/com/redhat/exhort/logging/LoggersFactory.java b/src/main/java/com/redhat/exhort/logging/LoggersFactory.java index 059cd2ee..e79ce515 100644 --- a/src/main/java/com/redhat/exhort/logging/LoggersFactory.java +++ b/src/main/java/com/redhat/exhort/logging/LoggersFactory.java @@ -13,22 +13,20 @@ * See the License for the specific language governing permissions and * limitations under the License. */ - package com.redhat.exhort.logging; import java.util.logging.ConsoleHandler; import java.util.logging.Logger; public class LoggersFactory { - public static Logger getLogger(String loggerName) { - Logger logger = Logger.getLogger(loggerName); - if(logger.getHandlers().length == 0 ) { - ConsoleHandler handler = new ConsoleHandler(); - handler.setFormatter(new ClientTraceIdSimpleFormatter()); - logger.addHandler(handler); + public static Logger getLogger(String loggerName) { + Logger logger = Logger.getLogger(loggerName); + if (logger.getHandlers().length == 0) { + ConsoleHandler handler = new ConsoleHandler(); + handler.setFormatter(new ClientTraceIdSimpleFormatter()); + logger.addHandler(handler); + } + logger.setUseParentHandlers(false); + return logger; } - logger.setUseParentHandlers(false); - return logger; - - } } diff --git a/src/main/java/com/redhat/exhort/providers/BaseJavaProvider.java b/src/main/java/com/redhat/exhort/providers/BaseJavaProvider.java index 6c9d42ab..f3967306 100644 --- a/src/main/java/com/redhat/exhort/providers/BaseJavaProvider.java +++ b/src/main/java/com/redhat/exhort/providers/BaseJavaProvider.java @@ -20,7 +20,6 @@ import com.redhat.exhort.Provider; import com.redhat.exhort.sbom.Sbom; import com.redhat.exhort.tools.Ecosystem; - import java.util.Arrays; import java.util.Map; import java.util.Objects; @@ -28,190 +27,188 @@ public abstract class BaseJavaProvider extends Provider { - protected BaseJavaProvider(Ecosystem.Type ecosystem) { - super(ecosystem); - } - - void parseDependencyTree(String src, int srcDepth, String [] lines, Sbom sbom) { - if(lines.length == 0) { - return; - } - if(lines.length == 1 && lines[0].trim().equals("")){ - return; + protected BaseJavaProvider(Ecosystem.Type ecosystem) { + super(ecosystem); } - int index = 0; - String target = lines[index]; - int targetDepth = getDepth(target); - while(targetDepth > srcDepth && index < lines.length ) - { - if(targetDepth == srcDepth + 1) { - PackageURL from = parseDep(src); - PackageURL to = parseDep(target); - if(dependencyIsNotTestScope(from) && dependencyIsNotTestScope(to)) { - sbom.addDependency(from, to); - } - } - else { - String[] modifiedLines = Arrays.copyOfRange(lines, index, lines.length); - parseDependencyTree(lines[index-1],getDepth(lines[index-1]),modifiedLines,sbom); - } - if(index< lines.length - 1) { - target = lines[++index]; - targetDepth = getDepth(target); - } - else - { - index++; - } - } - } - - static boolean dependencyIsNotTestScope(PackageURL artifact) { - return (Objects.nonNull(artifact.getQualifiers()) && !artifact.getQualifiers().get("scope").equals("test")) || Objects.isNull(artifact.getQualifiers()); - } - - PackageURL parseDep(String dep) { - //root package - DependencyAggregator dependencyAggregator = new DependencyAggregator(); - // in case line in dependency tree text starts with a letter ( for root artifact). - if(dep.matches("^\\w.*")) - { - dependencyAggregator = new DependencyAggregator(); - String[] parts = dep.split(":"); - dependencyAggregator.groupId = parts[0]; - dependencyAggregator.artifactId = parts[1]; - dependencyAggregator.version = parts[3]; - - return dependencyAggregator.toPurl(); + void parseDependencyTree(String src, int srcDepth, String[] lines, Sbom sbom) { + if (lines.length == 0) { + return; + } + if (lines.length == 1 && lines[0].trim().equals("")) { + return; + } + int index = 0; + String target = lines[index]; + int targetDepth = getDepth(target); + while (targetDepth > srcDepth && index < lines.length) { + if (targetDepth == srcDepth + 1) { + PackageURL from = parseDep(src); + PackageURL to = parseDep(target); + if (dependencyIsNotTestScope(from) && dependencyIsNotTestScope(to)) { + sbom.addDependency(from, to); + } + } else { + String[] modifiedLines = Arrays.copyOfRange(lines, index, lines.length); + parseDependencyTree(lines[index - 1], getDepth(lines[index - 1]), modifiedLines, sbom); + } + if (index < lines.length - 1) { + target = lines[++index]; + targetDepth = getDepth(target); + } else { + index++; + } + } } - int firstDash = dep.indexOf("-"); - String dependency = dep.substring(++firstDash).trim(); - if(dependency.startsWith("(")) - { - dependency = dependency.substring(1); - } - dependency = dependency.replace(":runtime", ":compile").replace(":provided", ":compile"); - int endIndex = Math.max(dependency.indexOf(":compile"),dependency.indexOf(":test")); - int scopeLength; - if(dependency.indexOf(":compile") > -1) { - scopeLength = ":compile".length(); - } - else { - scopeLength = ":test".length(); - } - dependency = dependency.substring(0,endIndex + scopeLength); - String[] parts = dependency.split(":"); - // contains only GAV + packaging + scope - if(parts.length == 5) - { - dependencyAggregator.groupId = parts[0]; - dependencyAggregator.artifactId= parts[1]; - dependencyAggregator.version = parts[3]; - - String conflictMessage = "omitted for conflict with"; - if (dep.contains(conflictMessage)) - { - dependencyAggregator.version = dep.substring(dep.indexOf(conflictMessage) + conflictMessage.length()).replace(")", "").trim(); - } - } - // In case there are 6 parts, there is also a classifier for artifact (version suffix) - // contains GAV + packaging + classifier + scope - else if(parts.length == 6) - { - dependencyAggregator.groupId = parts[0]; - dependencyAggregator.artifactId= parts[1]; - dependencyAggregator.version = String.format("%s-%s",parts[4],parts[3]); - String conflictMessage = "omitted for conflict with"; - if (dep.contains(conflictMessage)) - { - dependencyAggregator.version = dep.substring(dep.indexOf(conflictMessage) + conflictMessage.length()).replace(")", "").trim(); - } + static boolean dependencyIsNotTestScope(PackageURL artifact) { + return (Objects.nonNull(artifact.getQualifiers()) + && !artifact.getQualifiers().get("scope").equals("test")) + || Objects.isNull(artifact.getQualifiers()); } - else{ - throw new RuntimeException(String.format("Cannot parse dependency into PackageUrl from line = \"%s\"",dep)); - } - if(parts[parts.length - 1].matches(".*[a-z]$")) { - dependencyAggregator.scope = parts[parts.length - 1]; - } - else { - int endOfLine = Integer.min(parts[parts.length - 1].indexOf(""), parts[parts.length - 1].indexOf("-")); - dependencyAggregator.scope = parts[parts.length - 1].substring(0, endOfLine).trim(); - } - return dependencyAggregator.toPurl(); - } - int getDepth(String line) { - if(line == null || line.trim().equals("")){ - return -1; - } + PackageURL parseDep(String dep) { + // root package + DependencyAggregator dependencyAggregator = new DependencyAggregator(); + // in case line in dependency tree text starts with a letter ( for root artifact). + if (dep.matches("^\\w.*")) { + dependencyAggregator = new DependencyAggregator(); + String[] parts = dep.split(":"); + dependencyAggregator.groupId = parts[0]; + dependencyAggregator.artifactId = parts[1]; + dependencyAggregator.version = parts[3]; - if(line.matches("^\\w.*")) - { - return 0; + return dependencyAggregator.toPurl(); + } + int firstDash = dep.indexOf("-"); + String dependency = dep.substring(++firstDash).trim(); + if (dependency.startsWith("(")) { + dependency = dependency.substring(1); + } + dependency = dependency.replace(":runtime", ":compile").replace(":provided", ":compile"); + int endIndex = Math.max(dependency.indexOf(":compile"), dependency.indexOf(":test")); + int scopeLength; + if (dependency.indexOf(":compile") > -1) { + scopeLength = ":compile".length(); + } else { + scopeLength = ":test".length(); + } + dependency = dependency.substring(0, endIndex + scopeLength); + String[] parts = dependency.split(":"); + // contains only GAV + packaging + scope + if (parts.length == 5) { + dependencyAggregator.groupId = parts[0]; + dependencyAggregator.artifactId = parts[1]; + dependencyAggregator.version = parts[3]; + + String conflictMessage = "omitted for conflict with"; + if (dep.contains(conflictMessage)) { + dependencyAggregator.version = dep.substring(dep.indexOf(conflictMessage) + conflictMessage.length()) + .replace(")", "") + .trim(); + } + } + // In case there are 6 parts, there is also a classifier for artifact (version suffix) + // contains GAV + packaging + classifier + scope + else if (parts.length == 6) { + dependencyAggregator.groupId = parts[0]; + dependencyAggregator.artifactId = parts[1]; + dependencyAggregator.version = String.format("%s-%s", parts[4], parts[3]); + String conflictMessage = "omitted for conflict with"; + if (dep.contains(conflictMessage)) { + dependencyAggregator.version = dep.substring(dep.indexOf(conflictMessage) + conflictMessage.length()) + .replace(")", "") + .trim(); + } + + } else { + throw new RuntimeException( + String.format("Cannot parse dependency into PackageUrl from line = \"%s\"", dep)); + } + if (parts[parts.length - 1].matches(".*[a-z]$")) { + dependencyAggregator.scope = parts[parts.length - 1]; + } else { + int endOfLine = Integer.min(parts[parts.length - 1].indexOf(""), parts[parts.length - 1].indexOf("-")); + dependencyAggregator.scope = + parts[parts.length - 1].substring(0, endOfLine).trim(); + } + return dependencyAggregator.toPurl(); } - return ( (line.indexOf('-') -1 ) / 3) + 1; - } - - // NOTE if we want to include "scope" tags in ignore, - // add property here and a case in the start-element-switch in the getIgnored method + int getDepth(String line) { + if (line == null || line.trim().equals("")) { + return -1; + } - /** - * Aggregator class for aggregating Dependency data over stream iterations, - **/ - final static class DependencyAggregator { - String scope = "*"; - String groupId; - String artifactId; - String version; - boolean ignored = false; + if (line.matches("^\\w.*")) { + return 0; + } - /** - * Get the string representation of the dependency to use as excludes - * - * @return an exclude string for the dependency:tree plugin, ie. group-id:artifact-id:*:version - */ - @Override - public String toString() { - // NOTE if you add scope, don't forget to replace the * with its value - return String.format("%s:%s:%s:%s", groupId, artifactId, scope, version); + return ((line.indexOf('-') - 1) / 3) + 1; } - boolean isValid() { - return Objects.nonNull(groupId) && Objects.nonNull(artifactId) && Objects.nonNull(version); - } + // NOTE if we want to include "scope" tags in ignore, + // add property here and a case in the start-element-switch in the getIgnored method - boolean isTestDependency() { - return scope.trim().equals("test"); - } + /** + * Aggregator class for aggregating Dependency data over stream iterations, + **/ + static final class DependencyAggregator { + String scope = "*"; + String groupId; + String artifactId; + String version; + boolean ignored = false; + + /** + * Get the string representation of the dependency to use as excludes + * + * @return an exclude string for the dependency:tree plugin, ie. group-id:artifact-id:*:version + */ + @Override + public String toString() { + // NOTE if you add scope, don't forget to replace the * with its value + return String.format("%s:%s:%s:%s", groupId, artifactId, scope, version); + } - PackageURL toPurl() { - try { - return new PackageURL(Ecosystem.Type.MAVEN.getType(), groupId, artifactId, version, this.scope == "*" ? null : new TreeMap<>(Map.of("scope", this.scope)), null); - } catch (MalformedPackageURLException e) { - throw new IllegalArgumentException("Unable to parse PackageURL", e); - } - } + boolean isValid() { + return Objects.nonNull(groupId) && Objects.nonNull(artifactId) && Objects.nonNull(version); + } - @Override - public boolean equals(Object o) { - if (this == o) return true; - if (!(o instanceof DependencyAggregator)) return false; - var that = (DependencyAggregator) o; - // NOTE we do not compare the ignored field - // This is required for comparing pom.xml with effective_pom.xml as the latter doesn't - // contain comments indicating ignore - return Objects.equals(this.groupId, that.groupId) && - Objects.equals(this.artifactId, that.artifactId) && - Objects.equals(this.version, that.version); + boolean isTestDependency() { + return scope.trim().equals("test"); + } - } + PackageURL toPurl() { + try { + return new PackageURL( + Ecosystem.Type.MAVEN.getType(), + groupId, + artifactId, + version, + this.scope == "*" ? null : new TreeMap<>(Map.of("scope", this.scope)), + null); + } catch (MalformedPackageURLException e) { + throw new IllegalArgumentException("Unable to parse PackageURL", e); + } + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (!(o instanceof DependencyAggregator)) return false; + var that = (DependencyAggregator) o; + // NOTE we do not compare the ignored field + // This is required for comparing pom.xml with effective_pom.xml as the latter doesn't + // contain comments indicating ignore + return Objects.equals(this.groupId, that.groupId) + && Objects.equals(this.artifactId, that.artifactId) + && Objects.equals(this.version, that.version); + } - @Override - public int hashCode() { - return Objects.hash(groupId, artifactId, version); + @Override + public int hashCode() { + return Objects.hash(groupId, artifactId, version); + } } - } } diff --git a/src/main/java/com/redhat/exhort/providers/GoModulesProvider.java b/src/main/java/com/redhat/exhort/providers/GoModulesProvider.java index a7379a80..080edc4c 100644 --- a/src/main/java/com/redhat/exhort/providers/GoModulesProvider.java +++ b/src/main/java/com/redhat/exhort/providers/GoModulesProvider.java @@ -15,6 +15,9 @@ */ package com.redhat.exhort.providers; +import static com.redhat.exhort.impl.ExhortApi.debugLoggingIsNeeded; +import static com.redhat.exhort.impl.ExhortApi.getBooleanValueEnvironment; + import com.fasterxml.jackson.core.JsonProcessingException; import com.fasterxml.jackson.databind.JsonMappingException; import com.github.packageurl.MalformedPackageURLException; @@ -29,7 +32,6 @@ import com.redhat.exhort.vcs.GitVersionControlSystemImpl; import com.redhat.exhort.vcs.TagInfo; import com.redhat.exhort.vcs.VersionControlSystem; - import java.io.IOException; import java.nio.charset.StandardCharsets; import java.nio.file.Files; @@ -40,9 +42,6 @@ import java.util.regex.Pattern; import java.util.stream.Collectors; -import static com.redhat.exhort.impl.ExhortApi.debugLoggingIsNeeded; -import static com.redhat.exhort.impl.ExhortApi.getBooleanValueEnvironment; - /** * Concrete implementation of the {@link Provider} used for converting * dependency trees @@ -51,471 +50,479 @@ **/ public final class GoModulesProvider extends Provider { - private Logger log = LoggersFactory.getLogger(this.getClass().getName()); - private static final String goHostArchitectureEnvName = "GOHOSTARCH"; - private static final String goHostOperationSystemEnvName = "GOHOSTOS"; - public static final String defaultMainVersion = "v0.0.0"; - private final TreeMap goEnvironmentVariableForPurl; - private final TreeMap goEnvironmentVariablesForRef; - - public String getMainModuleVersion() { - return mainModuleVersion; - } - - private String mainModuleVersion; - - public static void main(String[] args) { - - TreeMap qualifiers = GoModulesProvider.getQualifiers(true); -// Path path = Path.of("/home/zgrinber/git/exhort-java-api/src/test/resources/tst_manifests/golang/go_mod_light_no_ignore/go.mod"); - Path path = Path.of("/tmp/xieshen/go.mod"); - Provider provider = new GoModulesProvider(); - GoModulesProvider goProvider = (GoModulesProvider) provider; -// boolean answer = goProvider.IgnoredLine(" github.com/davecgh/go-spew v1.1.1 // indirect //exhortignore"); - try { -// provider.provideStack(path); - byte[] bytes = Files.readAllBytes(path); - provider.provideComponent(bytes); - } catch (IOException e) { - throw new RuntimeException(e); + private Logger log = LoggersFactory.getLogger(this.getClass().getName()); + private static final String goHostArchitectureEnvName = "GOHOSTARCH"; + private static final String goHostOperationSystemEnvName = "GOHOSTOS"; + public static final String defaultMainVersion = "v0.0.0"; + private final TreeMap goEnvironmentVariableForPurl; + private final TreeMap goEnvironmentVariablesForRef; + + public String getMainModuleVersion() { + return mainModuleVersion; + } + + private String mainModuleVersion; + + public static void main(String[] args) { + + TreeMap qualifiers = GoModulesProvider.getQualifiers(true); + // Path path = + // Path.of("/home/zgrinber/git/exhort-java-api/src/test/resources/tst_manifests/golang/go_mod_light_no_ignore/go.mod"); + Path path = Path.of("/tmp/xieshen/go.mod"); + Provider provider = new GoModulesProvider(); + GoModulesProvider goProvider = (GoModulesProvider) provider; + // boolean answer = goProvider.IgnoredLine(" github.com/davecgh/go-spew v1.1.1 // indirect + // //exhortignore"); + try { + // provider.provideStack(path); + byte[] bytes = Files.readAllBytes(path); + provider.provideComponent(bytes); + } catch (IOException e) { + throw new RuntimeException(e); + } } - } - - public GoModulesProvider() { - super(Type.GOLANG); - this.goEnvironmentVariableForPurl=getQualifiers(true); - this.goEnvironmentVariablesForRef =getQualifiers(false); - this.mainModuleVersion= getDefaultMainModuleVersion(); - } - - - - @Override - public Content provideStack(final Path manifestPath) throws IOException { - // check for custom npm executable - Sbom sbom = getDependenciesSbom(manifestPath, true); - return new Content(sbom.getAsJsonString().getBytes(StandardCharsets.UTF_8), Api.CYCLONEDX_MEDIA_TYPE); - } - - @Override - public Content provideComponent(byte[] manifestContent) throws IOException { - // check for custom npm executable - return new Content(getDependenciesSbomCa(manifestContent).getAsJsonString().getBytes(StandardCharsets.UTF_8), - Api.CYCLONEDX_MEDIA_TYPE); - } - - @Override - public Content provideComponent(Path manifestPath) throws IOException { - throw new IllegalArgumentException("provideComponent with file system path for GoModules package manager not implemented yet"); - } - - private Sbom getDependenciesSbomCa(byte[] manifestContent) { - Sbom sbom; - try { - Path tempRepository = Files.createTempDirectory("exhort-go"); - Path path = Paths.get(tempRepository.toAbsolutePath().normalize().toString(), "go.mod"); - Files.deleteIfExists(path); - Path manifestPath = Files.createFile(path); - Files.write(manifestPath, manifestContent); - sbom = getDependenciesSbom(manifestPath, false); - - Files.delete(manifestPath); - Files.delete(tempRepository); - } catch (IOException e) { - throw new RuntimeException(e); + + public GoModulesProvider() { + super(Type.GOLANG); + this.goEnvironmentVariableForPurl = getQualifiers(true); + this.goEnvironmentVariablesForRef = getQualifiers(false); + this.mainModuleVersion = getDefaultMainModuleVersion(); } - return sbom; - } - - private PackageURL getRoot(String DependenciesGolang) { - return null; - } - - private PackageURL toPurl(String dependency, String delimiter, TreeMap qualifiers) { - try { - int lastSlashIndex = dependency.lastIndexOf("/"); - //there is no '/' char in module/package, so there is no namespace, only name - if (lastSlashIndex == -1) - { - String[] splitParts = dependency.split(delimiter); - if (splitParts.length == 2) { - return new PackageURL(Type.GOLANG.getType(), null, splitParts[0], splitParts[1], qualifiers, null); - } else { - return new PackageURL(Type.GOLANG.getType(), null, splitParts[0], this.mainModuleVersion, qualifiers, null); + + @Override + public Content provideStack(final Path manifestPath) throws IOException { + // check for custom npm executable + Sbom sbom = getDependenciesSbom(manifestPath, true); + return new Content(sbom.getAsJsonString().getBytes(StandardCharsets.UTF_8), Api.CYCLONEDX_MEDIA_TYPE); + } + + @Override + public Content provideComponent(byte[] manifestContent) throws IOException { + // check for custom npm executable + return new Content( + getDependenciesSbomCa(manifestContent).getAsJsonString().getBytes(StandardCharsets.UTF_8), + Api.CYCLONEDX_MEDIA_TYPE); + } + + @Override + public Content provideComponent(Path manifestPath) throws IOException { + throw new IllegalArgumentException( + "provideComponent with file system path for GoModules package manager not implemented yet"); + } + + private Sbom getDependenciesSbomCa(byte[] manifestContent) { + Sbom sbom; + try { + Path tempRepository = Files.createTempDirectory("exhort-go"); + Path path = Paths.get(tempRepository.toAbsolutePath().normalize().toString(), "go.mod"); + Files.deleteIfExists(path); + Path manifestPath = Files.createFile(path); + Files.write(manifestPath, manifestContent); + sbom = getDependenciesSbom(manifestPath, false); + + Files.delete(manifestPath); + Files.delete(tempRepository); + } catch (IOException e) { + throw new RuntimeException(e); } - } - String namespace = dependency.substring(0, lastSlashIndex); - String dependencyAndVersion = dependency.substring(lastSlashIndex + 1); - String[] parts = dependencyAndVersion.split(delimiter); - - if (parts.length == 2) { - return new PackageURL(Type.GOLANG.getType(), namespace, parts[0], parts[1], qualifiers, null); - // in this case, there is no version (happens with main module), thus need to take it from precalculated main module version. + return sbom; + } + + private PackageURL getRoot(String DependenciesGolang) { + return null; + } + + private PackageURL toPurl(String dependency, String delimiter, TreeMap qualifiers) { + try { + int lastSlashIndex = dependency.lastIndexOf("/"); + // there is no '/' char in module/package, so there is no namespace, only name + if (lastSlashIndex == -1) { + String[] splitParts = dependency.split(delimiter); + if (splitParts.length == 2) { + return new PackageURL(Type.GOLANG.getType(), null, splitParts[0], splitParts[1], qualifiers, null); + } else { + return new PackageURL( + Type.GOLANG.getType(), null, splitParts[0], this.mainModuleVersion, qualifiers, null); + } + } + String namespace = dependency.substring(0, lastSlashIndex); + String dependencyAndVersion = dependency.substring(lastSlashIndex + 1); + String[] parts = dependencyAndVersion.split(delimiter); + + if (parts.length == 2) { + return new PackageURL(Type.GOLANG.getType(), namespace, parts[0], parts[1], qualifiers, null); + // in this case, there is no version (happens with main module), thus need to take it from precalculated + // main module version. + } else { + return new PackageURL( + Type.GOLANG.getType(), namespace, parts[0], this.mainModuleVersion, qualifiers, null); + } + } catch (MalformedPackageURLException e) { + throw new IllegalArgumentException("Unable to parse golang module package : " + dependency, e); + } + } + + Sbom getDependenciesSbom(Path manifestPath, boolean buildTree) throws IOException { + var goModulesResult = buildGoModulesDependencies(manifestPath); + calculateMainModuleVersion(manifestPath.getParent()); + Sbom sbom; + List ignoredDeps = getIgnoredDeps(manifestPath); + boolean matchManifestVersions = getBooleanValueEnvironment("MATCH_MANIFEST_VERSIONS", "false"); + if (matchManifestVersions) { + // String rootName = getParentVertex() + String[] goModGraphLines = goModulesResult.split(System.lineSeparator()); + performManifestVersionsCheck(goModGraphLines, manifestPath); + } + if (!buildTree) { + sbom = buildSbomFromList(goModulesResult, ignoredDeps); } else { - return new PackageURL(Type.GOLANG.getType(), namespace, parts[0], this.mainModuleVersion, qualifiers, null); + sbom = buildSbomFromGraph(goModulesResult, ignoredDeps, manifestPath); } - } catch (MalformedPackageURLException e) { - throw new IllegalArgumentException("Unable to parse golang module package : " + dependency , e); + // List ignoredDeps = getIgnoredDeps(manifestPath); + // sbom.filterIgnoredDeps(ignoredDeps); + return sbom; } - } + private void performManifestVersionsCheck(String[] goModGraphLines, Path manifestPath) { + try { + String goModLines = Files.readString(manifestPath); + String[] lines = goModLines.split(System.lineSeparator()); + String root = getParentVertex(goModGraphLines[0]); + List comparisonLines = Arrays.stream(goModGraphLines) + .filter((line) -> line.startsWith(root)) + .map((line) -> getChildVertex(line)) + .collect(Collectors.toList()); + List goModDependencies = collectAllDepsFromManifest(lines, goModLines); + comparisonLines.stream().forEach((dependency) -> { + String[] parts = dependency.split("@"); + String version = parts[1]; + String depName = parts[0]; + goModDependencies.stream().forEach((dep) -> { + String[] artifactParts = dep.trim().split(" "); + String currentDepName = artifactParts[0]; + String currentVersion = artifactParts[1]; + if (currentDepName.trim().equals(depName.trim())) { + if (!currentVersion.trim().equals(version.trim())) { + throw new RuntimeException(String.format( + "Can't continue with analysis - versions mismatch for dependency name=%s, manifest version=%s, installed Version=%s, if you want to allow version mismatch for analysis between installed and requested packages, set environment variable/setting - MATCH_MANIFEST_VERSIONS=false", + depName, currentVersion, version)); + } + } + }); + }); + } catch (IOException e) { + throw new RuntimeException("Failed to open go.mod file for manifest versions check validation!"); + } + } + private List collectAllDepsFromManifest(String[] lines, String goModLines) { + List result = new ArrayList<>(); + // collect all deps that starts with require keyword + result = Arrays.stream(lines) + .filter((line) -> line.trim().startsWith("require") && !line.contains("(")) + .map((dep) -> dep.substring("require".length()).trim()) + .collect(Collectors.toList()); + + // collect all deps that are inside `require` blocks + + String currentSegmentOfGoMod = goModLines; + Map requirePosObject = decideRequireBlockIndex(currentSegmentOfGoMod); + while (requirePosObject.get("index") > -1) { + String depsInsideRequirementsBlock = currentSegmentOfGoMod + .substring(requirePosObject.get("index") + requirePosObject.get("length")) + .trim(); + int endOfBlockIndex = depsInsideRequirementsBlock.indexOf(")"); + int currentIndex = 0; + while (currentIndex < endOfBlockIndex) { + int endOfLinePosition = depsInsideRequirementsBlock.indexOf(System.lineSeparator(), currentIndex); + String dependency = depsInsideRequirementsBlock + .substring(currentIndex, endOfLinePosition) + .trim(); + result.add(dependency); + currentIndex = endOfLinePosition + 1; + } + currentSegmentOfGoMod = + currentSegmentOfGoMod.substring(endOfBlockIndex + 1).trim(); + requirePosObject = decideRequireBlockIndex(currentSegmentOfGoMod); + } + return result; + } - Sbom getDependenciesSbom(Path manifestPath, boolean buildTree) throws IOException { - var goModulesResult = buildGoModulesDependencies(manifestPath); - calculateMainModuleVersion(manifestPath.getParent()); - Sbom sbom; - List ignoredDeps = getIgnoredDeps(manifestPath); - boolean matchManifestVersions = getBooleanValueEnvironment("MATCH_MANIFEST_VERSIONS", "false"); - if(matchManifestVersions) { -// String rootName = getParentVertex() - String[] goModGraphLines = goModulesResult.split(System.lineSeparator()); - performManifestVersionsCheck(goModGraphLines,manifestPath); + private Map decideRequireBlockIndex(String currentSegmentOfGoMod) { + int index = currentSegmentOfGoMod.indexOf("require("); + int length = "require(".length(); + if (index == -1) { + index = currentSegmentOfGoMod.indexOf("require ("); + length = "require (".length(); + if (index == -1) { + index = currentSegmentOfGoMod.indexOf("require ("); + length = "require (".length(); + } + } + return Map.of("index", index, "length", length); } - if (!buildTree) { - sbom = buildSbomFromList(goModulesResult,ignoredDeps); + + public void determineMainModuleVersion(Path directory) { + this.calculateMainModuleVersion(directory); } - else - { - sbom = buildSbomFromGraph(goModulesResult,ignoredDeps,manifestPath); + + private void calculateMainModuleVersion(Path directory) { + VersionControlSystem vcs = new GitVersionControlSystemImpl(); + if (vcs.isDirectoryRepo(directory)) { + TagInfo latestTagInfo = vcs.getLatestTag(directory); + if (!latestTagInfo.getTagName().trim().equals("")) { + if (!latestTagInfo.isCurrentCommitPointedByTag()) { + String nextTagVersion = vcs.getNextTagVersion(latestTagInfo); + this.mainModuleVersion = vcs.getPseudoVersion(latestTagInfo, nextTagVersion); + } else { + this.mainModuleVersion = latestTagInfo.getTagName(); + } + } else { + if (!latestTagInfo.getCurrentCommitDigest().trim().equals("")) { + this.mainModuleVersion = vcs.getPseudoVersion(latestTagInfo, getDefaultMainModuleVersion()); + } + } + } } -// List ignoredDeps = getIgnoredDeps(manifestPath); -// sbom.filterIgnoredDeps(ignoredDeps); - return sbom; - } - - private void performManifestVersionsCheck(String[] goModGraphLines, Path manifestPath) { - try { - String goModLines = Files.readString(manifestPath); - String[] lines = goModLines.split(System.lineSeparator()); - String root = getParentVertex(goModGraphLines[0]); - List comparisonLines = Arrays.stream(goModGraphLines).filter((line) -> line.startsWith(root)).map((line) -> getChildVertex(line)).collect(Collectors.toList()); - List goModDependencies = collectAllDepsFromManifest(lines,goModLines); - comparisonLines.stream().forEach((dependency) -> - { - String[] parts = dependency.split("@"); - String version = parts[1]; - String depName = parts[0]; - goModDependencies.stream().forEach((dep) -> - { - String[] artifactParts = dep.trim().split(" "); - String currentDepName = artifactParts[0]; - String currentVersion = artifactParts[1]; - if(currentDepName.trim().equals(depName.trim())) - { - if(!currentVersion.trim().equals(version.trim())) - { - throw new RuntimeException(String.format("Can't continue with analysis - versions mismatch for dependency name=%s, manifest version=%s, installed Version=%s, if you want to allow version mismatch for analysis between installed and requested packages, set environment variable/setting - MATCH_MANIFEST_VERSIONS=false", depName, currentVersion, version)); + + private Sbom buildSbomFromGraph(String goModulesResult, List ignoredDeps, Path manifestPath) + throws IOException { + // Each entry contains a key of the module, and the list represents the module direct dependencies , so + // pairing of the key with each of the dependencies in a list is basically an edge in the graph. + Map edges = new HashMap<>(); + // iterate over go mod graph line by line and create map , with each entry to contain module as a key , and + // value of list of that module' dependencies. + String[] lines = goModulesResult.split(System.lineSeparator()); + List linesList = Arrays.asList(lines); + // System.out.print("Start time: " + LocalDateTime.now() + System.lineSeparator()); + + Integer startingIndex = 0; + Integer EndingIndex = lines.length - 1; + String[] targetLines = Arrays.copyOfRange(lines, 0, lines.length - 1); + for (String line : linesList) { + + if (!edges.containsKey(getParentVertex(line))) { + // Collect all direct dependencies of the current module into a list. + List deps = collectAllDirectDependencies(targetLines, line); + edges.put(getParentVertex(line), deps); + startingIndex += deps.size(); + // Because all the deps of the current module were collected, not need to search for next modules on + // these lines, so truncate these lines from search array to make the search more rapid and efficient. + if (startingIndex < EndingIndex) { + targetLines = Arrays.copyOfRange(lines, startingIndex, EndingIndex); + } } - } + } + // DEBUG + // System.setProperty("EXHORT_GO_MVS_LOGIC_ENABLED","true"); + boolean goMvsLogicEnabled = getBooleanValueEnvironment("EXHORT_GO_MVS_LOGIC_ENABLED", "false"); + if (goMvsLogicEnabled) { + edges = getFinalPackagesVersionsForModule(edges, manifestPath); + } + // Build Sbom + String rootPackage = getParentVertex(lines[0]); + + PackageURL root = toPurl(rootPackage, "@", this.goEnvironmentVariableForPurl); + Sbom sbom = SbomFactory.newInstance(Sbom.BelongingCondition.PURL, "sensitive"); + sbom.addRoot(root); + edges.forEach((key, value) -> { + PackageURL source = toPurl(key, "@", this.goEnvironmentVariableForPurl); + value.forEach(dep -> { + PackageURL targetPurl = toPurl((String) dep, "@", this.goEnvironmentVariableForPurl); + sbom.addDependency(source, targetPurl); + }); }); - }); - } catch (IOException e) { - throw new RuntimeException("Failed to open go.mod file for manifest versions check validation!"); - } - } - - private List collectAllDepsFromManifest(String[] lines, String goModLines) { - List result = new ArrayList<>(); - // collect all deps that starts with require keyword - result = Arrays.stream(lines).filter((line) -> line.trim().startsWith("require") && !line.contains("(")).map((dep) -> dep.substring("require".length()).trim()).collect(Collectors.toList()); - - // collect all deps that are inside `require` blocks - - String currentSegmentOfGoMod = goModLines; - Map requirePosObject = decideRequireBlockIndex(currentSegmentOfGoMod); - while(requirePosObject.get("index") > -1) - { - String depsInsideRequirementsBlock = currentSegmentOfGoMod.substring(requirePosObject.get("index") + requirePosObject.get("length")).trim(); - int endOfBlockIndex = depsInsideRequirementsBlock.indexOf(")"); - int currentIndex = 0; - while(currentIndex < endOfBlockIndex) - { - int endOfLinePosition = depsInsideRequirementsBlock.indexOf(System.lineSeparator(),currentIndex); - String dependency = depsInsideRequirementsBlock.substring(currentIndex,endOfLinePosition).trim(); - result.add(dependency); - currentIndex = endOfLinePosition + 1; - } - currentSegmentOfGoMod = currentSegmentOfGoMod.substring(endOfBlockIndex + 1).trim(); - requirePosObject = decideRequireBlockIndex(currentSegmentOfGoMod); + List ignoredDepsPurl = + ignoredDeps.stream().map(PackageURL::getCoordinates).collect(Collectors.toList()); + sbom.filterIgnoredDeps(ignoredDepsPurl); + ArrayList ignoredDepsByName = new ArrayList<>(); + ignoredDeps.forEach(purl -> { + if (sbom.checkIfPackageInsideDependsOnList(sbom.getRoot(), purl.getName())) { + ignoredDepsByName.add(purl.getName()); + } + }); + sbom.setBelongingCriteriaBinaryAlgorithm(Sbom.BelongingCondition.NAME); + sbom.filterIgnoredDeps(ignoredDepsByName); + + return sbom; } - return result; - } - - private Map decideRequireBlockIndex(String currentSegmentOfGoMod) { - int index = currentSegmentOfGoMod.indexOf("require("); - int length = "require(".length(); - if(index == -1) - { - index = currentSegmentOfGoMod.indexOf("require ("); - length = "require (".length(); - if(index == -1 ) - { - index = currentSegmentOfGoMod.indexOf("require ("); - length = "require (".length(); - } + private Map getFinalPackagesVersionsForModule(Map edges, Path manifestPath) { + Operations.runProcessGetOutput(manifestPath.getParent(), "go", "mod", "download"); + String finalVersionsForAllModules = + Operations.runProcessGetOutput(manifestPath.getParent(), "go", "list", "-m", "all"); + Map finalModulesVersions = Arrays.stream( + finalVersionsForAllModules.split(System.lineSeparator())) + .filter(string -> string.trim().split(" ").length == 2) + .collect(Collectors.toMap(t -> t.split(" ")[0], t -> t.split(" ")[1], (first, second) -> second)); + Map listWithModifiedVersions = new HashMap<>(); + edges.entrySet().stream() + .filter(string -> string.getKey().trim().split("@").length == 2) + .collect(Collectors.toList()) + .forEach((entry) -> { + String packageWithSelectedVersion = + getPackageWithFinalVersion(finalModulesVersions, entry.getKey()); + List packagesWithFinalVersions = getListOfPackagesWithFinlVersions(finalModulesVersions, entry); + listWithModifiedVersions.put(packageWithSelectedVersion, packagesWithFinalVersions); + }); + + return listWithModifiedVersions; } - return Map.of("index",index,"length",length); - - } - - public void determineMainModuleVersion(Path directory) - { - this.calculateMainModuleVersion(directory); - } - private void calculateMainModuleVersion(Path directory) { - VersionControlSystem vcs = new GitVersionControlSystemImpl(); - if(vcs.isDirectoryRepo(directory)) { - TagInfo latestTagInfo = vcs.getLatestTag(directory); - if (!latestTagInfo.getTagName().trim().equals("")) { - if(!latestTagInfo.isCurrentCommitPointedByTag()) - { - String nextTagVersion = vcs.getNextTagVersion(latestTagInfo); - this.mainModuleVersion = vcs.getPseudoVersion(latestTagInfo, nextTagVersion); - } - else - { - this.mainModuleVersion = latestTagInfo.getTagName(); - } - } - else - { - if(!latestTagInfo.getCurrentCommitDigest().trim().equals("")) { - this.mainModuleVersion = vcs.getPseudoVersion(latestTagInfo, getDefaultMainModuleVersion()); - } - } + + private List getListOfPackagesWithFinlVersions( + Map finalModulesVersions, Map.Entry entry) { + return (List) entry.getValue().stream() + .map((packageWithVersion) -> + getPackageWithFinalVersion(finalModulesVersions, (String) packageWithVersion)) + .collect(Collectors.toList()); } - } - - private Sbom buildSbomFromGraph(String goModulesResult, List ignoredDeps, Path manifestPath) throws IOException{ -// Each entry contains a key of the module, and the list represents the module direct dependencies , so pairing of the key with each of the dependencies in a list is basically an edge in the graph. - Map edges = new HashMap<>(); - // iterate over go mod graph line by line and create map , with each entry to contain module as a key , and value of list of that module' dependencies. - String[] lines = goModulesResult.split(System.lineSeparator()); - List linesList = Arrays.asList(lines); -// System.out.print("Start time: " + LocalDateTime.now() + System.lineSeparator()); - - Integer startingIndex=0; - Integer EndingIndex=lines.length - 1; - String[] targetLines = Arrays.copyOfRange(lines,0,lines.length-1); - for (String line : linesList) { - - if (!edges.containsKey(getParentVertex(line))) - { - //Collect all direct dependencies of the current module into a list. - List deps = collectAllDirectDependencies(targetLines, line); - edges.put(getParentVertex(line),deps); - startingIndex+=deps.size(); - // Because all the deps of the current module were collected, not need to search for next modules on these lines, so truncate these lines from search array to make the search more rapid and efficient. - if(startingIndex < EndingIndex) { - targetLines = Arrays.copyOfRange(lines, startingIndex, EndingIndex); - } - } + public static String getPackageWithFinalVersion( + Map finalModulesVersions, String packagePlusVersion) { + String packageName = packagePlusVersion.split("@")[0]; + String originalVersion = packagePlusVersion.split("@")[1]; + String finalVersion = finalModulesVersions.get(packageName); + if (Objects.nonNull(finalVersion)) { + return String.format("%s@%s", packageName, finalVersion); + } else { + return packagePlusVersion; + } } - //DEBUG -// System.setProperty("EXHORT_GO_MVS_LOGIC_ENABLED","true"); - boolean goMvsLogicEnabled = getBooleanValueEnvironment("EXHORT_GO_MVS_LOGIC_ENABLED", "false"); - if(goMvsLogicEnabled) { - edges = getFinalPackagesVersionsForModule(edges,manifestPath); + + private boolean dependencyNotToBeIgnored(List ignoredDeps, PackageURL checkedPurl) { + return ignoredDeps.stream() + .noneMatch(dependencyPurl -> dependencyPurl.getCoordinates().equals(checkedPurl.getCoordinates())); } -// Build Sbom - String rootPackage = getParentVertex(lines[0]); - - PackageURL root = toPurl(rootPackage, "@", this.goEnvironmentVariableForPurl); - Sbom sbom = SbomFactory.newInstance(Sbom.BelongingCondition.PURL,"sensitive"); - sbom.addRoot(root); - edges.forEach((key,value)-> { - PackageURL source = toPurl(key,"@",this.goEnvironmentVariableForPurl); - value.forEach(dep -> { - PackageURL targetPurl = toPurl((String) dep, "@", this.goEnvironmentVariableForPurl); - sbom.addDependency(source, targetPurl); - }); - - }); - List ignoredDepsPurl = ignoredDeps.stream().map(PackageURL::getCoordinates).collect(Collectors.toList()); - sbom.filterIgnoredDeps(ignoredDepsPurl); - ArrayList ignoredDepsByName = new ArrayList<>(); - ignoredDeps.forEach(purl -> - { - if(sbom.checkIfPackageInsideDependsOnList(sbom.getRoot(),purl.getName())) - { - ignoredDepsByName.add(purl.getName()); - } - }); - sbom.setBelongingCriteriaBinaryAlgorithm(Sbom.BelongingCondition.NAME); - sbom.filterIgnoredDeps(ignoredDepsByName); - - return sbom; - - } - - private Map getFinalPackagesVersionsForModule(Map edges, Path manifestPath) { - Operations.runProcessGetOutput(manifestPath.getParent(),"go","mod","download"); - String finalVersionsForAllModules = Operations.runProcessGetOutput(manifestPath.getParent(), "go", "list", "-m", "all"); - Map finalModulesVersions = Arrays.stream(finalVersionsForAllModules.split(System.lineSeparator())).filter(string -> string.trim().split(" ").length == 2).collect(Collectors.toMap(t -> t.split(" ")[0], t -> t.split(" ")[1], (first, second) -> second)); - Map listWithModifiedVersions = new HashMap<>(); - edges.entrySet().stream().filter(string -> string.getKey().trim().split("@").length == 2).collect(Collectors.toList()).forEach((entry) -> { - String packageWithSelectedVersion = getPackageWithFinalVersion(finalModulesVersions, entry.getKey()); - List packagesWithFinalVersions = getListOfPackagesWithFinlVersions(finalModulesVersions,entry); - listWithModifiedVersions.put(packageWithSelectedVersion,packagesWithFinalVersions); - }); - - - return listWithModifiedVersions; - } - - private List getListOfPackagesWithFinlVersions(Map finalModulesVersions, Map.Entry entry) { - return (List)entry.getValue().stream().map((packageWithVersion) -> getPackageWithFinalVersion(finalModulesVersions,(String)packageWithVersion)).collect(Collectors.toList()); - } - - public static String getPackageWithFinalVersion(Map finalModulesVersions, String packagePlusVersion) { - String packageName = packagePlusVersion.split("@")[0]; - String originalVersion = packagePlusVersion.split("@")[1]; - String finalVersion = finalModulesVersions.get(packageName); - if(Objects.nonNull(finalVersion)) { - return String.format("%s@%s",packageName,finalVersion); + + private static List collectAllDirectDependencies(String[] targetLines, String edge) { + return Arrays.stream(targetLines) + .filter(line2 -> getParentVertex(line2).equals(getParentVertex(edge))) + .map(GoModulesProvider::getChildVertex) + .collect(Collectors.toList()); } - else { - return packagePlusVersion; + + private static TreeMap getQualifiers(boolean includeOsAndArch) { + + if (includeOsAndArch) { + var go = Operations.getCustomPathOrElse("go"); + String goEnvironmentVariables = Operations.runProcessGetOutput(null, new String[] {go, "env"}); + String hostArch = getEnvironmentVariable(goEnvironmentVariables, goHostArchitectureEnvName); + String hostOS = getEnvironmentVariable(goEnvironmentVariables, goHostOperationSystemEnvName); + return new TreeMap(Map.of("type", "module", "goos", hostOS, "goarch", hostArch)); + } + + return new TreeMap(Map.of("type", "module")); } - } - - private boolean dependencyNotToBeIgnored(List ignoredDeps, PackageURL checkedPurl) { - return ignoredDeps.stream().noneMatch(dependencyPurl -> dependencyPurl.getCoordinates().equals(checkedPurl.getCoordinates())); - } - - private static List collectAllDirectDependencies(String[] targetLines, String edge) { - return Arrays.stream(targetLines) - .filter(line2 -> getParentVertex(line2).equals(getParentVertex(edge))) - .map(GoModulesProvider::getChildVertex) - .collect(Collectors.toList()); - } - - private static TreeMap getQualifiers(boolean includeOsAndArch) { - - if(includeOsAndArch) - { - var go = Operations.getCustomPathOrElse("go"); - String goEnvironmentVariables = Operations.runProcessGetOutput(null, new String[]{go, "env"}); - String hostArch = getEnvironmentVariable(goEnvironmentVariables, goHostArchitectureEnvName); - String hostOS = getEnvironmentVariable(goEnvironmentVariables, goHostOperationSystemEnvName); - return new TreeMap(Map.of("type", "module","goos",hostOS,"goarch",hostArch)); + + private static String getEnvironmentVariable(String goEnvironmentVariables, String envName) { + int i = goEnvironmentVariables.indexOf(String.format("%s=", envName)); + int beginIndex = i + String.format("%s=", envName).length(); + int endOfLineIndex = goEnvironmentVariables.substring(beginIndex).indexOf(System.lineSeparator()); + String envValue = goEnvironmentVariables.substring(beginIndex).substring(0, endOfLineIndex); + return envValue.replaceAll("\"", ""); } - return new TreeMap(Map.of("type", "module")); - } + private String buildGoModulesDependencies(Path manifestPath) throws JsonMappingException, JsonProcessingException { + var go = Operations.getCustomPathOrElse("go"); + String[] goModulesDeps; + goModulesDeps = new String[] {go, "mod", "graph"}; - private static String getEnvironmentVariable(String goEnvironmentVariables,String envName) { - int i = goEnvironmentVariables.indexOf(String.format("%s=",envName)); - int beginIndex = i + String.format("%s=", envName).length(); - int endOfLineIndex = goEnvironmentVariables.substring(beginIndex).indexOf(System.lineSeparator()); - String envValue = goEnvironmentVariables.substring(beginIndex).substring(0, endOfLineIndex); - return envValue.replaceAll("\"",""); + // execute the clean command + String goModulesOutput = Operations.runProcessGetOutput(manifestPath.getParent(), goModulesDeps); + if (debugLoggingIsNeeded()) { + log.info(String.format( + "Package Manager Go Mod Graph output : %s%s", System.lineSeparator(), goModulesOutput)); + } + return goModulesOutput; + } - } + private Sbom buildSbomFromList(String golangDeps, List ignoredDeps) { + String[] allModulesFlat = golangDeps.split(System.lineSeparator()); + String parentVertex = getParentVertex(allModulesFlat[0]); + PackageURL root = toPurl(parentVertex, "@", this.goEnvironmentVariableForPurl); + // Get only direct dependencies of root package/module, and that's it. + List deps = collectAllDirectDependencies(allModulesFlat, parentVertex); + + Sbom sbom = SbomFactory.newInstance(Sbom.BelongingCondition.PURL, "sensitive"); + sbom.addRoot(root); + deps.forEach(dep -> { + PackageURL targetPurl = toPurl(dep, "@", this.goEnvironmentVariableForPurl); + if (dependencyNotToBeIgnored(ignoredDeps, targetPurl)) { + sbom.addDependency(root, targetPurl); + } + }); + List ignoredDepsByName = new ArrayList<>(); + ignoredDeps.forEach(purl -> { + if (sbom.checkIfPackageInsideDependsOnList(sbom.getRoot(), purl.getName())) { + ignoredDepsByName.add(purl.getName()); + } + }); + sbom.setBelongingCriteriaBinaryAlgorithm(Sbom.BelongingCondition.NAME); + sbom.filterIgnoredDeps(ignoredDepsByName); + return sbom; + } - private String buildGoModulesDependencies(Path manifestPath) - throws JsonMappingException, JsonProcessingException { - var go = Operations.getCustomPathOrElse("go"); - String[] goModulesDeps; - goModulesDeps = new String[]{go, "mod", "graph"}; + private List getIgnoredDeps(Path manifestPath) throws IOException { - // execute the clean command - String goModulesOutput = Operations.runProcessGetOutput(manifestPath.getParent(),goModulesDeps); - if(debugLoggingIsNeeded()) { - log.info(String.format("Package Manager Go Mod Graph output : %s%s",System.lineSeparator(),goModulesOutput)); + List goModlines = Files.readAllLines(manifestPath); + List ignored = goModlines.stream() + .filter(this::IgnoredLine) + .map(this::extractPackageName) + .map(dep -> toPurl(dep, "\\s{1,3}", this.goEnvironmentVariableForPurl)) + .collect(Collectors.toList()); + return ignored; } - return goModulesOutput; - } - - private Sbom buildSbomFromList(String golangDeps, List ignoredDeps) { - String[] allModulesFlat = golangDeps.split(System.lineSeparator()); - String parentVertex = getParentVertex(allModulesFlat[0]); - PackageURL root = toPurl(parentVertex,"@",this.goEnvironmentVariableForPurl); - // Get only direct dependencies of root package/module, and that's it. - List deps = collectAllDirectDependencies(allModulesFlat, parentVertex); - - Sbom sbom = SbomFactory.newInstance(Sbom.BelongingCondition.PURL,"sensitive"); - sbom.addRoot(root); - deps.forEach(dep -> { - PackageURL targetPurl = toPurl(dep, "@", this.goEnvironmentVariableForPurl); - if(dependencyNotToBeIgnored(ignoredDeps,targetPurl)) { - sbom.addDependency(root, targetPurl); - } - }); - List ignoredDepsByName = new ArrayList<>(); - ignoredDeps.forEach(purl -> - { - if(sbom.checkIfPackageInsideDependsOnList(sbom.getRoot(),purl.getName())) - { - ignoredDepsByName.add(purl.getName()); - } - }); - sbom.setBelongingCriteriaBinaryAlgorithm(Sbom.BelongingCondition.NAME); - sbom.filterIgnoredDeps(ignoredDepsByName); - return sbom; - } - - private List getIgnoredDeps(Path manifestPath) throws IOException { - - List goModlines = Files.readAllLines(manifestPath); - List ignored = goModlines.stream() - .filter(this::IgnoredLine) - .map(this::extractPackageName) - .map(dep -> toPurl(dep,"\\s{1,3}",this.goEnvironmentVariableForPurl)) - .collect(Collectors.toList()); - return ignored; - } - - private String extractPackageName(String line) { - String trimmedRow = line.trim(); - int firstRemarkNotationOccurrence = trimmedRow.indexOf("//"); - return trimmedRow.substring(0,firstRemarkNotationOccurrence).trim(); - - } - - public boolean IgnoredLine(String line) { - boolean result = false; - if (line.contains("exhortignore")) - { - // if exhortignore is alone in a comment or is in a comment together with indirect or as a comment inside a comment ( e.g // indirect //exhort) - // then this line is to be checked if it's a comment after a package name. - if(Pattern.matches(".+//\\s*exhortignore",line) || Pattern.matches(".+//\\sindirect (//)?\\s*exhortignore",line) ) - { - String trimmedRow = line.trim(); - // filter out lines where exhortignore has no meaning - if(!trimmedRow.startsWith("module ") && !trimmedRow.startsWith("go ") && !trimmedRow.startsWith("require (") && !trimmedRow.startsWith("require(") - && !trimmedRow.startsWith("exclude ") && !trimmedRow.startsWith("replace ") && !trimmedRow.startsWith("retract ") && !trimmedRow.startsWith("use ") - && !trimmedRow.contains("=>")) - { //only for lines that after trimming starts with "require " or starting with package name followd by one space, and then a semver version. - if( trimmedRow.startsWith("require ") || Pattern.matches("^[a-z.0-9/-]+\\s{1,2}[vV][0-9]\\.[0-9](\\.[0-9]){0,2}.*",trimmedRow)) - { - result = true; - } - } - } - } - return result; - } + private String extractPackageName(String line) { + String trimmedRow = line.trim(); + int firstRemarkNotationOccurrence = trimmedRow.indexOf("//"); + return trimmedRow.substring(0, firstRemarkNotationOccurrence).trim(); + } - private static String getParentVertex(String edge) - { - String[] edgeParts = edge.trim().split(" "); - return edgeParts[0]; - } - private static String getChildVertex(String edge) - { + public boolean IgnoredLine(String line) { + boolean result = false; + if (line.contains("exhortignore")) { + // if exhortignore is alone in a comment or is in a comment together with indirect or as a comment inside a + // comment ( e.g // indirect //exhort) + // then this line is to be checked if it's a comment after a package name. + if (Pattern.matches(".+//\\s*exhortignore", line) + || Pattern.matches(".+//\\sindirect (//)?\\s*exhortignore", line)) { + String trimmedRow = line.trim(); + // filter out lines where exhortignore has no meaning + if (!trimmedRow.startsWith("module ") + && !trimmedRow.startsWith("go ") + && !trimmedRow.startsWith("require (") + && !trimmedRow.startsWith("require(") + && !trimmedRow.startsWith("exclude ") + && !trimmedRow.startsWith("replace ") + && !trimmedRow.startsWith("retract ") + && !trimmedRow.startsWith("use ") + && !trimmedRow.contains( + "=>")) { // only for lines that after trimming starts with "require " or starting with + // package name followd by one space, and then a semver version. + if (trimmedRow.startsWith("require ") + || Pattern.matches("^[a-z.0-9/-]+\\s{1,2}[vV][0-9]\\.[0-9](\\.[0-9]){0,2}.*", trimmedRow)) { + result = true; + } + } + } + } + return result; + } - String[] edgeParts = edge.trim().split(" "); - return edgeParts[1]; - } + private static String getParentVertex(String edge) { + String[] edgeParts = edge.trim().split(" "); + return edgeParts[0]; + } + + private static String getChildVertex(String edge) { - private static String getDefaultMainModuleVersion() { - return defaultMainVersion; - } + String[] edgeParts = edge.trim().split(" "); + return edgeParts[1]; + } + private static String getDefaultMainModuleVersion() { + return defaultMainVersion; + } } diff --git a/src/main/java/com/redhat/exhort/providers/GradleProvider.java b/src/main/java/com/redhat/exhort/providers/GradleProvider.java index ec42274a..e8014a36 100644 --- a/src/main/java/com/redhat/exhort/providers/GradleProvider.java +++ b/src/main/java/com/redhat/exhort/providers/GradleProvider.java @@ -15,6 +15,8 @@ */ package com.redhat.exhort.providers; +import static com.redhat.exhort.impl.ExhortApi.debugLoggingIsNeeded; + import com.github.packageurl.MalformedPackageURLException; import com.github.packageurl.PackageURL; import com.redhat.exhort.Api; @@ -24,15 +26,9 @@ import com.redhat.exhort.sbom.SbomFactory; import com.redhat.exhort.tools.Ecosystem.Type; import com.redhat.exhort.tools.Operations; -import org.tomlj.Toml; -import org.tomlj.TomlParseResult; -import org.tomlj.TomlTable; - import java.io.IOException; import java.nio.file.Files; import java.nio.file.Path; -import java.time.LocalDateTime; -import java.time.temporal.ChronoUnit; import java.util.ArrayList; import java.util.HashMap; import java.util.List; @@ -41,8 +37,9 @@ import java.util.regex.Matcher; import java.util.regex.Pattern; import java.util.stream.Collectors; - -import static com.redhat.exhort.impl.ExhortApi.debugLoggingIsNeeded; +import org.tomlj.Toml; +import org.tomlj.TomlParseResult; +import org.tomlj.TomlTable; /** * Concrete implementation of the {@link Provider} used for converting dependency trees @@ -51,311 +48,317 @@ **/ public final class GradleProvider extends BaseJavaProvider { - private Logger log = LoggersFactory.getLogger(this.getClass().getName()); - - public GradleProvider() { - super(Type.GRADLE); - } + private Logger log = LoggersFactory.getLogger(this.getClass().getName()); - @Override - public Content provideStack(final Path manifestPath) throws IOException { - Path tempFile = getDependencies(manifestPath); - if (debugLoggingIsNeeded()) { - String stackAnalysisDependencyTree = Files.readString(tempFile); - log.info(String.format("Package Manager Gradle Stack Analysis Dependency Tree Output: %s %s", System.lineSeparator(), stackAnalysisDependencyTree)); - } - Map propertiesMap = extractProperties(manifestPath); - - var sbom = buildSbomFromTextFormat(tempFile, propertiesMap, "runtimeClasspath"); - var ignored = getIgnoredDeps(manifestPath); - - return new Content(sbom.filterIgnoredDeps(ignored).getAsJsonString().getBytes(), Api.CYCLONEDX_MEDIA_TYPE); - } - - private List getIgnoredDeps(Path manifestPath) throws IOException { - List buildGradleLines = Files.readAllLines(manifestPath); - List ignored = new ArrayList<>(); - - var ignoredLines = buildGradleLines.stream() - .filter(this::isIgnoredLine) - .map(this::extractPackageName) - .collect(Collectors.toList()); - - // Process each ignored dependency - for (String dependency : ignoredLines) { - String ignoredDepInfo; - if (isNotation(dependency)) { - ignoredDepInfo = getDepFromNotation(dependency, manifestPath); - } else { - ignoredDepInfo = getDepInfo(dependency); - } - - if (ignoredDepInfo != null) { - ignored.add(ignoredDepInfo); - } + public GradleProvider() { + super(Type.GRADLE); } - return ignored; - } - - private String getDepInfo(String dependencyLine) { - // Check if the line contains "group:", "name:", and "version:" - if (dependencyLine.contains("group:") && dependencyLine.contains("name:") && dependencyLine.contains("version:")) { - Pattern pattern = Pattern.compile("(group|name|version):\\s*['\"](.*?)['\"]"); - Matcher matcher = pattern.matcher(dependencyLine); - String groupId = null, artifactId = null, version = null; - - while (matcher.find()) { - String key = matcher.group(1); - String value = matcher.group(2); - - switch (key) { - case "group": - groupId = value; - break; - case "name": - artifactId = value; - break; - case "version": - version = value; - break; + @Override + public Content provideStack(final Path manifestPath) throws IOException { + Path tempFile = getDependencies(manifestPath); + if (debugLoggingIsNeeded()) { + String stackAnalysisDependencyTree = Files.readString(tempFile); + log.info(String.format( + "Package Manager Gradle Stack Analysis Dependency Tree Output: %s %s", + System.lineSeparator(), stackAnalysisDependencyTree)); } - } - if (groupId != null && artifactId != null && version != null) { - PackageURL ignoredPackageUrl = toPurl(groupId, artifactId, version); - return ignoredPackageUrl.getCoordinates(); - } - } else { - // Regular expression pattern to capture content inside single or double quotes - Pattern pattern = Pattern.compile("['\"](.*?)['\"]"); - Matcher matcher = pattern.matcher(dependencyLine); - // Check if the matcher finds a match - if (matcher.find()) { - // Get the matched string inside single or double quotes - String dependency = matcher.group(1); - String[] dependencyParts = dependency.split(":"); - if (dependencyParts.length == 3) { - // Extract groupId, artifactId, and version - String groupId = dependencyParts[0]; - String artifactId = dependencyParts[1]; - String version = dependencyParts[2]; - - PackageURL ignoredPackageUrl = toPurl(groupId, artifactId, version); - return ignoredPackageUrl.getCoordinates(); + Map propertiesMap = extractProperties(manifestPath); + + var sbom = buildSbomFromTextFormat(tempFile, propertiesMap, "runtimeClasspath"); + var ignored = getIgnoredDeps(manifestPath); + + return new Content(sbom.filterIgnoredDeps(ignored).getAsJsonString().getBytes(), Api.CYCLONEDX_MEDIA_TYPE); + } + + private List getIgnoredDeps(Path manifestPath) throws IOException { + List buildGradleLines = Files.readAllLines(manifestPath); + List ignored = new ArrayList<>(); + + var ignoredLines = buildGradleLines.stream() + .filter(this::isIgnoredLine) + .map(this::extractPackageName) + .collect(Collectors.toList()); + + // Process each ignored dependency + for (String dependency : ignoredLines) { + String ignoredDepInfo; + if (isNotation(dependency)) { + ignoredDepInfo = getDepFromNotation(dependency, manifestPath); + } else { + ignoredDepInfo = getDepInfo(dependency); + } + + if (ignoredDepInfo != null) { + ignored.add(ignoredDepInfo); + } } - } + + return ignored; } - return null; - } - - private String getDepFromNotation(String dependency, Path manifestPath) throws IOException { - // Extract everything after "libs." - String alias = dependency.substring(dependency.indexOf("libs.") + "libs.".length()); - alias = alias.replace(".", "-"); - // Read and parse the TOML file - TomlParseResult toml = Toml.parse(getLibsVersionsTomlPath(manifestPath)); - TomlTable librariesTable = toml.getTable("libraries"); - TomlTable dependencyTable = librariesTable.getTable(alias); - if (dependencyTable != null) { - String groupId = dependencyTable.getString("module").split(":")[0]; - String artifactId = dependencyTable.getString("module").split(":")[1]; - String version = toml.getTable("versions").getString(dependencyTable.getString("version.ref")); - PackageURL ignoredPackageUrl = toPurl(groupId, artifactId, version); - return ignoredPackageUrl.getCoordinates(); + + private String getDepInfo(String dependencyLine) { + // Check if the line contains "group:", "name:", and "version:" + if (dependencyLine.contains("group:") + && dependencyLine.contains("name:") + && dependencyLine.contains("version:")) { + Pattern pattern = Pattern.compile("(group|name|version):\\s*['\"](.*?)['\"]"); + Matcher matcher = pattern.matcher(dependencyLine); + String groupId = null, artifactId = null, version = null; + + while (matcher.find()) { + String key = matcher.group(1); + String value = matcher.group(2); + + switch (key) { + case "group": + groupId = value; + break; + case "name": + artifactId = value; + break; + case "version": + version = value; + break; + } + } + if (groupId != null && artifactId != null && version != null) { + PackageURL ignoredPackageUrl = toPurl(groupId, artifactId, version); + return ignoredPackageUrl.getCoordinates(); + } + } else { + // Regular expression pattern to capture content inside single or double quotes + Pattern pattern = Pattern.compile("['\"](.*?)['\"]"); + Matcher matcher = pattern.matcher(dependencyLine); + // Check if the matcher finds a match + if (matcher.find()) { + // Get the matched string inside single or double quotes + String dependency = matcher.group(1); + String[] dependencyParts = dependency.split(":"); + if (dependencyParts.length == 3) { + // Extract groupId, artifactId, and version + String groupId = dependencyParts[0]; + String artifactId = dependencyParts[1]; + String version = dependencyParts[2]; + + PackageURL ignoredPackageUrl = toPurl(groupId, artifactId, version); + return ignoredPackageUrl.getCoordinates(); + } + } + } + return null; } - return null; + private String getDepFromNotation(String dependency, Path manifestPath) throws IOException { + // Extract everything after "libs." + String alias = dependency.substring(dependency.indexOf("libs.") + "libs.".length()); + alias = alias.replace(".", "-"); + // Read and parse the TOML file + TomlParseResult toml = Toml.parse(getLibsVersionsTomlPath(manifestPath)); + TomlTable librariesTable = toml.getTable("libraries"); + TomlTable dependencyTable = librariesTable.getTable(alias); + if (dependencyTable != null) { + String groupId = dependencyTable.getString("module").split(":")[0]; + String artifactId = dependencyTable.getString("module").split(":")[1]; + String version = toml.getTable("versions").getString(dependencyTable.getString("version.ref")); + PackageURL ignoredPackageUrl = toPurl(groupId, artifactId, version); + return ignoredPackageUrl.getCoordinates(); + } - } + return null; + } - private Path getLibsVersionsTomlPath(Path manifestPath) { - return manifestPath.getParent().resolve("gradle/libs.versions.toml"); - } + private Path getLibsVersionsTomlPath(Path manifestPath) { + return manifestPath.getParent().resolve("gradle/libs.versions.toml"); + } - public PackageURL toPurl(String groupId, String artifactId, String version) { - try { - return new PackageURL(Type.MAVEN.getType(), groupId, artifactId, version, null, null); - } catch (MalformedPackageURLException e) { - throw new IllegalArgumentException("Unable to parse PackageURL", e); + public PackageURL toPurl(String groupId, String artifactId, String version) { + try { + return new PackageURL(Type.MAVEN.getType(), groupId, artifactId, version, null, null); + } catch (MalformedPackageURLException e) { + throw new IllegalArgumentException("Unable to parse PackageURL", e); + } } - } - - public static boolean isNotation(String line) { - int colonCount = 0; - for (char c : line.toCharArray()) { - if (c == ':') { - colonCount++; - if (colonCount > 1) { - return false; // Likely full dependency with group and artifact + + public static boolean isNotation(String line) { + int colonCount = 0; + for (char c : line.toCharArray()) { + if (c == ':') { + colonCount++; + if (colonCount > 1) { + return false; // Likely full dependency with group and artifact + } + } } - } + return true; // Potentially a notation + } + + private boolean isIgnoredLine(String line) { + return line.contains("exhortignore"); } - return true; // Potentially a notation - } - - private boolean isIgnoredLine(String line) { - return line.contains("exhortignore"); - } - - private String extractPackageName(String line) { - String packageName = line.trim(); - // Extract the package name before the comment - int commentIndex = packageName.indexOf("//"); - if (commentIndex != -1) { - packageName = packageName.substring(0, commentIndex).trim(); + + private String extractPackageName(String line) { + String packageName = line.trim(); + // Extract the package name before the comment + int commentIndex = packageName.indexOf("//"); + if (commentIndex != -1) { + packageName = packageName.substring(0, commentIndex).trim(); + } + // Remove any other trailing comments or spaces + commentIndex = packageName.indexOf("/*"); + if (commentIndex != -1) { + packageName = packageName.substring(0, commentIndex).trim(); + } + return packageName; } - // Remove any other trailing comments or spaces - commentIndex = packageName.indexOf("/*"); - if (commentIndex != -1) { - packageName = packageName.substring(0, commentIndex).trim(); + + private static Path getDependencies(Path manifestPath) throws IOException { + // check for custom gradle executable + var gradle = Operations.getCustomPathOrElse("gradle"); + // create a temp file for storing the dependency tree in + var tempFile = Files.createTempFile("exhort_graph_", null); + // the command will create the dependency tree in the temp file + String gradleCommand = gradle + " dependencies"; + + String[] cmdList = gradleCommand.split("\\s+"); + String gradleOutput = + Operations.runProcessGetOutput(Path.of(manifestPath.getParent().toString()), cmdList); + Files.writeString(tempFile, gradleOutput); + + return tempFile; } - return packageName; - } - - - private static Path getDependencies(Path manifestPath) throws IOException { - // check for custom gradle executable - var gradle = Operations.getCustomPathOrElse("gradle"); - // create a temp file for storing the dependency tree in - var tempFile = Files.createTempFile("exhort_graph_", null); - // the command will create the dependency tree in the temp file - String gradleCommand = gradle + " dependencies"; - - String[] cmdList = gradleCommand.split("\\s+"); - String gradleOutput = Operations.runProcessGetOutput(Path.of(manifestPath.getParent().toString()), cmdList); - Files.writeString(tempFile, gradleOutput); - - return tempFile; - } - - private Path getProperties(Path manifestPath) throws IOException { - Path propsTempFile = Files.createTempFile("propsfile", ".txt"); - var gradle = Operations.getCustomPathOrElse("gradle"); - String propCmd = gradle + " properties"; - String[] propCmdList = propCmd.split("\\s+"); - String properties = Operations.runProcessGetOutput(Path.of(manifestPath.getParent().toString()), propCmdList); - // Create a temporary file - Files.writeString(propsTempFile, properties); - - return propsTempFile; - } - - private Sbom buildSbomFromTextFormat(Path textFormatFile, Map propertiesMap, String configName) throws IOException { - var sbom = SbomFactory.newInstance(Sbom.BelongingCondition.PURL, "sensitive"); - String root = getRoot(textFormatFile, propertiesMap); - - var rootPurl = parseDep(root); - sbom.addRoot(rootPurl); - List lines = extractLines(textFormatFile, configName); - String[] array = new String[lines.size()]; - for (int index = 0; index < array.length; index++) { - String line = lines.get(index); - line = line.replaceAll("---", "-").replaceAll(" ", " "); - line = line.replaceAll(":(.*):(.*) -> (.*)$", ":$1:$3"); - line = line.replaceAll("(.*):(.*):(.*)$", "$1:$2:jar:$3"); - line = line.replaceAll(" \\(n\\)$", ""); - line = line.replaceAll("$", ":compile"); - array[index] = line; + + private Path getProperties(Path manifestPath) throws IOException { + Path propsTempFile = Files.createTempFile("propsfile", ".txt"); + var gradle = Operations.getCustomPathOrElse("gradle"); + String propCmd = gradle + " properties"; + String[] propCmdList = propCmd.split("\\s+"); + String properties = + Operations.runProcessGetOutput(Path.of(manifestPath.getParent().toString()), propCmdList); + // Create a temporary file + Files.writeString(propsTempFile, properties); + + return propsTempFile; } - parseDependencyTree(root, 0, array, sbom); - return sbom; - } - - private String getRoot(Path textFormatFile, Map propertiesMap) throws IOException { - String group = propertiesMap.get("group"); - String version = propertiesMap.get("version"); - String rootName = extractRootProjectValue(textFormatFile); - String root = group + ':' + rootName + ':' + "jar" + ':' + version ; - return root; - } - - private String extractRootProjectValue(Path inputFilePath) throws IOException { - List lines = Files.readAllLines(inputFilePath); - for (String line : lines) { - if (line.contains("Root project")) { - Pattern pattern = Pattern.compile("Root project '(.+)'"); - Matcher matcher = pattern.matcher(line); - if (matcher.find()) { - return matcher.group(1); + + private Sbom buildSbomFromTextFormat(Path textFormatFile, Map propertiesMap, String configName) + throws IOException { + var sbom = SbomFactory.newInstance(Sbom.BelongingCondition.PURL, "sensitive"); + String root = getRoot(textFormatFile, propertiesMap); + + var rootPurl = parseDep(root); + sbom.addRoot(rootPurl); + List lines = extractLines(textFormatFile, configName); + String[] array = new String[lines.size()]; + for (int index = 0; index < array.length; index++) { + String line = lines.get(index); + line = line.replaceAll("---", "-").replaceAll(" ", " "); + line = line.replaceAll(":(.*):(.*) -> (.*)$", ":$1:$3"); + line = line.replaceAll("(.*):(.*):(.*)$", "$1:$2:jar:$3"); + line = line.replaceAll(" \\(n\\)$", ""); + line = line.replaceAll("$", ":compile"); + array[index] = line; } - } + parseDependencyTree(root, 0, array, sbom); + return sbom; + } + + private String getRoot(Path textFormatFile, Map propertiesMap) throws IOException { + String group = propertiesMap.get("group"); + String version = propertiesMap.get("version"); + String rootName = extractRootProjectValue(textFormatFile); + String root = group + ':' + rootName + ':' + "jar" + ':' + version; + return root; } - return null; - } - - private Map extractProperties(Path manifestPath) throws IOException { - Path propsTempFile = getProperties(manifestPath); - String content = Files.readString(propsTempFile); - // Define the regular expression pattern for key-value pairs - Pattern pattern = Pattern.compile("([^:]+):\\s+(.+)"); - Matcher matcher = pattern.matcher(content); - // Create a Map to store key-value pairs - Map keyValueMap = new HashMap<>(); - - // Iterate through matches and add them to the map - while (matcher.find()) { - String key = matcher.group(1).trim(); - String value = matcher.group(2).trim(); - keyValueMap.put(key, value); + + private String extractRootProjectValue(Path inputFilePath) throws IOException { + List lines = Files.readAllLines(inputFilePath); + for (String line : lines) { + if (line.contains("Root project")) { + Pattern pattern = Pattern.compile("Root project '(.+)'"); + Matcher matcher = pattern.matcher(line); + if (matcher.find()) { + return matcher.group(1); + } + } + } + return null; } - // Check if any key-value pairs were found - if (!keyValueMap.isEmpty()) { - return keyValueMap; - } else { - return null; + + private Map extractProperties(Path manifestPath) throws IOException { + Path propsTempFile = getProperties(manifestPath); + String content = Files.readString(propsTempFile); + // Define the regular expression pattern for key-value pairs + Pattern pattern = Pattern.compile("([^:]+):\\s+(.+)"); + Matcher matcher = pattern.matcher(content); + // Create a Map to store key-value pairs + Map keyValueMap = new HashMap<>(); + + // Iterate through matches and add them to the map + while (matcher.find()) { + String key = matcher.group(1).trim(); + String value = matcher.group(2).trim(); + keyValueMap.put(key, value); + } + // Check if any key-value pairs were found + if (!keyValueMap.isEmpty()) { + return keyValueMap; + } else { + return null; + } } - } - - private List extractLines(Path inputFilePath, String startMarker) throws IOException { - List lines = Files.readAllLines(inputFilePath); - List extractedLines = new ArrayList<>(); - boolean startFound = false; - - for (String line : lines) { - // If the start marker is found, set startFound to true - if (line.startsWith(startMarker)) { - startFound = true; - continue; // Skip the line containing the startMarker - } - // If startFound is true and the line is not empty, add it to the extractedLines list - if (startFound && !line.trim().isEmpty()) { - extractedLines.add(line); - } - // If an empty line is encountered, break out of the loop - if (startFound && line.trim().isEmpty()) { - break; - } + + private List extractLines(Path inputFilePath, String startMarker) throws IOException { + List lines = Files.readAllLines(inputFilePath); + List extractedLines = new ArrayList<>(); + boolean startFound = false; + + for (String line : lines) { + // If the start marker is found, set startFound to true + if (line.startsWith(startMarker)) { + startFound = true; + continue; // Skip the line containing the startMarker + } + // If startFound is true and the line is not empty, add it to the extractedLines list + if (startFound && !line.trim().isEmpty()) { + extractedLines.add(line); + } + // If an empty line is encountered, break out of the loop + if (startFound && line.trim().isEmpty()) { + break; + } + } + return extractedLines; } - return extractedLines; - } - @Override - public Content provideComponent(byte[] manifestContent) throws IOException { - throw new IllegalArgumentException("Gradle Package Manager requires the full package directory, not just the manifest content, to generate the dependency tree. Please provide the complete package directory path."); - } + @Override + public Content provideComponent(byte[] manifestContent) throws IOException { + throw new IllegalArgumentException( + "Gradle Package Manager requires the full package directory, not just the manifest content, to generate the dependency tree. Please provide the complete package directory path."); + } - @Override - public Content provideComponent(Path manifestPath) throws IOException { + @Override + public Content provideComponent(Path manifestPath) throws IOException { - Path tempFile = getDependencies(manifestPath); - Map propertiesMap = extractProperties(manifestPath); + Path tempFile = getDependencies(manifestPath); + Map propertiesMap = extractProperties(manifestPath); - String[] configurationNames = {"api", "implementation", "compile"}; + String[] configurationNames = {"api", "implementation", "compile"}; - String configName = null; - for (String configurationName : configurationNames) { - List directDependencies = extractLines(tempFile, configurationName); + String configName = null; + for (String configurationName : configurationNames) { + List directDependencies = extractLines(tempFile, configurationName); - // Check if dependencies are found for the current configuration - if (!directDependencies.isEmpty()) { - configName = configurationName; - break; - } - } + // Check if dependencies are found for the current configuration + if (!directDependencies.isEmpty()) { + configName = configurationName; + break; + } + } - var sbom = buildSbomFromTextFormat(tempFile, propertiesMap, configName); - var ignored = getIgnoredDeps(manifestPath); + var sbom = buildSbomFromTextFormat(tempFile, propertiesMap, configName); + var ignored = getIgnoredDeps(manifestPath); - return new Content(sbom.filterIgnoredDeps(ignored).getAsJsonString().getBytes(), Api.CYCLONEDX_MEDIA_TYPE); - } + return new Content(sbom.filterIgnoredDeps(ignored).getAsJsonString().getBytes(), Api.CYCLONEDX_MEDIA_TYPE); + } } diff --git a/src/main/java/com/redhat/exhort/providers/JavaMavenProvider.java b/src/main/java/com/redhat/exhort/providers/JavaMavenProvider.java index 3ec2d75a..96dbaf45 100644 --- a/src/main/java/com/redhat/exhort/providers/JavaMavenProvider.java +++ b/src/main/java/com/redhat/exhort/providers/JavaMavenProvider.java @@ -15,19 +15,7 @@ */ package com.redhat.exhort.providers; -import java.io.IOException; -import java.nio.file.Files; -import java.nio.file.Path; -import java.time.LocalDateTime; -import java.time.temporal.ChronoUnit; -import java.util.*; -import java.util.logging.Logger; -import java.util.stream.Collectors; - -import javax.xml.stream.XMLInputFactory; -import javax.xml.stream.XMLStreamConstants; -import javax.xml.stream.XMLStreamException; -import javax.xml.stream.XMLStreamReader; +import static com.redhat.exhort.impl.ExhortApi.debugLoggingIsNeeded; import com.github.packageurl.MalformedPackageURLException; import com.github.packageurl.PackageURL; @@ -40,8 +28,18 @@ import com.redhat.exhort.tools.Ecosystem; import com.redhat.exhort.tools.Ecosystem.Type; import com.redhat.exhort.tools.Operations; - -import static com.redhat.exhort.impl.ExhortApi.debugLoggingIsNeeded; +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Path; +import java.time.LocalDateTime; +import java.time.temporal.ChronoUnit; +import java.util.*; +import java.util.logging.Logger; +import java.util.stream.Collectors; +import javax.xml.stream.XMLInputFactory; +import javax.xml.stream.XMLStreamConstants; +import javax.xml.stream.XMLStreamException; +import javax.xml.stream.XMLStreamReader; /** * Concrete implementation of the {@link Provider} used for converting dependency trees @@ -50,335 +48,352 @@ **/ public final class JavaMavenProvider extends BaseJavaProvider { - private Logger log = LoggersFactory.getLogger(this.getClass().getName()); - public static void main(String[] args) throws IOException { - JavaMavenProvider javaMavenProvider = new JavaMavenProvider(); - PackageURL packageURL = javaMavenProvider.parseDep("+- org.assertj:assertj-core:jar:3.24.2:test"); - LocalDateTime start = LocalDateTime.now(); - System.out.print(start); - Content content = javaMavenProvider.provideStack(Path.of("/tmp/devfile-sample-java-springboot-basic/pom.xml")); - -// PackageURL packageURL = javaMavenProvider.parseDep("pom-with-deps-no-ignore:pom-with-dependency-not-ignored-common-paths:jar:0.0.1"); -// String report = new String(content.buffer); - System.out.println(new String(content.buffer)); - LocalDateTime end = LocalDateTime.now(); - System.out.print(end); - System.out.print("Total time elapsed = " + start.until(end, ChronoUnit.NANOS)); - - } - public JavaMavenProvider() { - super(Type.MAVEN); - } - - - - @Override - public Content provideStack(final Path manifestPath) throws IOException { - // check for custom mvn executable - var mvn = Operations.getCustomPathOrElse("mvn"); - // clean command used to clean build target - var mvnCleanCmd = new String[]{mvn, "clean", "-f", manifestPath.toString()}; - var mvnEnvs = getMvnExecEnvs(); - // execute the clean command - Operations.runProcess(mvnCleanCmd, mvnEnvs); - // create a temp file for storing the dependency tree in - var tmpFile = Files.createTempFile("exhort_dot_graph_", null); - // the tree command will build the project and create the dependency tree in the temp file - var mvnTreeCmd = new ArrayList() {{ - add(mvn); - add("org.apache.maven.plugins:maven-dependency-plugin:3.6.0:tree"); - add("-Dverbose"); - add("-DoutputType=text"); - add(String.format("-DoutputFile=%s", tmpFile.toString())); - add("-f"); - add(manifestPath.toString()); - }}; - // if we have dependencies marked as ignored, exclude them from the tree command - var ignored = getDependencies(manifestPath).stream() - .filter(d -> d.ignored) - .map(DependencyAggregator::toPurl) - .map(PackageURL::getCoordinates) - .collect(Collectors.toList()); - // execute the tree command - Operations.runProcess(mvnTreeCmd.toArray(String[]::new), mvnEnvs); - if(debugLoggingIsNeeded()) - { - String stackAnalysisDependencyTree = Files.readString(tmpFile); - log.info(String.format("Package Manager Maven Stack Analysis Dependency Tree Output: %s %s",System.lineSeparator(),stackAnalysisDependencyTree)); + private Logger log = LoggersFactory.getLogger(this.getClass().getName()); + + public static void main(String[] args) throws IOException { + JavaMavenProvider javaMavenProvider = new JavaMavenProvider(); + PackageURL packageURL = javaMavenProvider.parseDep("+- org.assertj:assertj-core:jar:3.24.2:test"); + LocalDateTime start = LocalDateTime.now(); + System.out.print(start); + Content content = javaMavenProvider.provideStack(Path.of("/tmp/devfile-sample-java-springboot-basic/pom.xml")); + + // PackageURL packageURL = + // javaMavenProvider.parseDep("pom-with-deps-no-ignore:pom-with-dependency-not-ignored-common-paths:jar:0.0.1"); + // String report = new String(content.buffer); + System.out.println(new String(content.buffer)); + LocalDateTime end = LocalDateTime.now(); + System.out.print(end); + System.out.print("Total time elapsed = " + start.until(end, ChronoUnit.NANOS)); } - var sbom = buildSbomFromTextFormat(tmpFile); - // build and return content for constructing request to the backend - return new Content(sbom.filterIgnoredDeps(ignored).getAsJsonString().getBytes(), Api.CYCLONEDX_MEDIA_TYPE); - } - - private Sbom buildSbomFromTextFormat(Path textFormatFile) throws IOException { - var sbom = SbomFactory.newInstance(Sbom.BelongingCondition.PURL,"sensitive"); - List lines = Files.readAllLines(textFormatFile); - var root = lines.get(0); - var rootPurl = parseDep(root); - sbom.addRoot(rootPurl); - lines.remove(0); - String[] array = new String[lines.size()]; - lines.toArray(array); -// createSbomIteratively(lines,sbom); - parseDependencyTree(root, 0 , array, sbom); - return sbom; - } - - private PackageURL txtPkgToPurl(String dotPkg) { - var parts = dotPkg. - replaceAll("\"", "") - .trim().split(":"); - if(parts.length >= 4) { - try { - return new PackageURL(Ecosystem.Type.MAVEN.getType(), parts[0], parts[1], parts[3], null, null); - } catch (MalformedPackageURLException e) { - throw new IllegalArgumentException("Unable to parse dot package: " + dotPkg, e); - } + public JavaMavenProvider() { + super(Type.MAVEN); } - throw new IllegalArgumentException("Invalid dot package format: " + dotPkg); - } - @Override - public Content provideComponent(byte[] manifestContent) throws IOException { - // save content in temporary file - var originPom = Files.createTempFile("exhort_orig_pom_", ".xml"); - Files.write(originPom, manifestContent); - // build effective pom command - Content content = generateSbomFromEffectivePom(originPom); - Files.delete(originPom); - return content; - } + @Override + public Content provideStack(final Path manifestPath) throws IOException { + // check for custom mvn executable + var mvn = Operations.getCustomPathOrElse("mvn"); + // clean command used to clean build target + var mvnCleanCmd = new String[] {mvn, "clean", "-f", manifestPath.toString()}; + var mvnEnvs = getMvnExecEnvs(); + // execute the clean command + Operations.runProcess(mvnCleanCmd, mvnEnvs); + // create a temp file for storing the dependency tree in + var tmpFile = Files.createTempFile("exhort_dot_graph_", null); + // the tree command will build the project and create the dependency tree in the temp file + var mvnTreeCmd = new ArrayList() { + { + add(mvn); + add("org.apache.maven.plugins:maven-dependency-plugin:3.6.0:tree"); + add("-Dverbose"); + add("-DoutputType=text"); + add(String.format("-DoutputFile=%s", tmpFile.toString())); + add("-f"); + add(manifestPath.toString()); + } + }; + // if we have dependencies marked as ignored, exclude them from the tree command + var ignored = getDependencies(manifestPath).stream() + .filter(d -> d.ignored) + .map(DependencyAggregator::toPurl) + .map(PackageURL::getCoordinates) + .collect(Collectors.toList()); + // execute the tree command + Operations.runProcess(mvnTreeCmd.toArray(String[]::new), mvnEnvs); + if (debugLoggingIsNeeded()) { + String stackAnalysisDependencyTree = Files.readString(tmpFile); + log.info(String.format( + "Package Manager Maven Stack Analysis Dependency Tree Output: %s %s", + System.lineSeparator(), stackAnalysisDependencyTree)); + } - private Content generateSbomFromEffectivePom(Path originPom) throws IOException { - // check for custom mvn executable - var mvn = Operations.getCustomPathOrElse("mvn"); - var tmpEffPom = Files.createTempFile("exhort_eff_pom_", ".xml"); - var mvnEffPomCmd = new String[]{ - mvn, - "clean", - "help:effective-pom", - String.format("-Doutput=%s", tmpEffPom.toString()), - "-f", originPom.toString() - }; - // execute the effective pom command - Operations.runProcess(mvnEffPomCmd, getMvnExecEnvs()); - if(debugLoggingIsNeeded()) - { - String CaEffectivePoM = Files.readString(tmpEffPom); - log.info(String.format("Package Manager Maven Component Analysis Effective POM Output : %s %s",System.lineSeparator(),CaEffectivePoM)); + var sbom = buildSbomFromTextFormat(tmpFile); + // build and return content for constructing request to the backend + return new Content(sbom.filterIgnoredDeps(ignored).getAsJsonString().getBytes(), Api.CYCLONEDX_MEDIA_TYPE); } - // if we have dependencies marked as ignored grab ignored dependencies from the original pom - // the effective-pom goal doesn't carry comments - List dependencies = getDependencies(originPom); - var ignored = dependencies.stream().filter(d -> d.ignored).map(DependencyAggregator::toPurl).collect(Collectors.toSet()); - var testsDeps = dependencies.stream().filter(DependencyAggregator::isTestDependency).collect(Collectors.toSet()); - var deps = getDependencies(tmpEffPom); - var sbom = SbomFactory.newInstance().addRoot(getRoot(tmpEffPom)); - deps.stream() - .filter(dep -> !testsDeps.contains(dep)) - .map(DependencyAggregator::toPurl) - .filter(dep -> ignored.stream().filter(artifact -> artifact.isCoordinatesEquals(dep)).collect(Collectors.toList()).size() == 0) - .forEach(d -> sbom.addDependency(sbom.getRoot(), d)); - // build and return content for constructing request to the backend - return new Content(sbom.getAsJsonString().getBytes(), Api.CYCLONEDX_MEDIA_TYPE); - } - - @Override - public Content provideComponent(Path manifestPath) throws IOException { - Content content = generateSbomFromEffectivePom(manifestPath); - return content; - } + private Sbom buildSbomFromTextFormat(Path textFormatFile) throws IOException { + var sbom = SbomFactory.newInstance(Sbom.BelongingCondition.PURL, "sensitive"); + List lines = Files.readAllLines(textFormatFile); + var root = lines.get(0); + var rootPurl = parseDep(root); + sbom.addRoot(rootPurl); + lines.remove(0); + String[] array = new String[lines.size()]; + lines.toArray(array); + // createSbomIteratively(lines,sbom); + parseDependencyTree(root, 0, array, sbom); + return sbom; + } - private PackageURL getRoot(final Path manifestPath) throws IOException { - XMLStreamReader reader = null; - try { - reader = XMLInputFactory.newInstance().createXMLStreamReader(Files.newInputStream(manifestPath)); - DependencyAggregator dependencyAggregator = null; - boolean isRoot = false; - while (reader.hasNext()) { - reader.next(); // get the next event - if (reader.isStartElement() && "project".equals(reader.getLocalName())) { - isRoot = true; - dependencyAggregator = new DependencyAggregator(); - continue; - } - if (!Objects.isNull(dependencyAggregator)) { - if (reader.isStartElement()) { - switch (reader.getLocalName()) { - case "groupId": // starting "groupId" tag, get next event and set to aggregator - reader.next(); - dependencyAggregator.groupId = reader.getText(); - break; - case "artifactId": // starting "artifactId" tag, get next event and set to aggregator - reader.next(); - dependencyAggregator.artifactId = reader.getText(); - break; - case "version": // starting "version" tag, get next event and set to aggregator - reader.next(); - dependencyAggregator.version = reader.getText(); - break; + private PackageURL txtPkgToPurl(String dotPkg) { + var parts = dotPkg.replaceAll("\"", "").trim().split(":"); + if (parts.length >= 4) { + try { + return new PackageURL(Ecosystem.Type.MAVEN.getType(), parts[0], parts[1], parts[3], null, null); + } catch (MalformedPackageURLException e) { + throw new IllegalArgumentException("Unable to parse dot package: " + dotPkg, e); } - } - if (isRoot && dependencyAggregator.isValid()) { - return dependencyAggregator.toPurl(); - } - } - } - } catch (XMLStreamException exc) { - throw new IOException(exc); - } finally { - if (!Objects.isNull(reader)) { - try { - reader.close(); // close stream if open - } catch (XMLStreamException e) { - // } - } + throw new IllegalArgumentException("Invalid dot package format: " + dotPkg); } - throw new IllegalStateException("Unable to retrieve Root dependency from effective pom"); - } + @Override + public Content provideComponent(byte[] manifestContent) throws IOException { + // save content in temporary file + var originPom = Files.createTempFile("exhort_orig_pom_", ".xml"); + Files.write(originPom, manifestContent); + // build effective pom command + Content content = generateSbomFromEffectivePom(originPom); + Files.delete(originPom); + return content; + } - private List getDependencies(final Path manifestPath) throws IOException { - List deps = new ArrayList<>(); - XMLStreamReader reader = null; - try { - //get a xml stream reader for the manifest file - reader = XMLInputFactory.newInstance().createXMLStreamReader(Files.newInputStream(manifestPath)); - // the following dependencyIgnore object is used to aggregate dependency data over iterations - // when a "dependency" tag starts, it will be initiated, - // when a "dependency" tag ends, it will be parsed, act upon, and reset - DependencyAggregator dependencyAggregator = null; - while (reader.hasNext()) { - reader.next(); // get the next event - if (reader.isStartElement() && "dependency".equals(reader.getLocalName())) { - // starting "dependency" tag, initiate aggregator - dependencyAggregator = new DependencyAggregator(); - continue; + private Content generateSbomFromEffectivePom(Path originPom) throws IOException { + // check for custom mvn executable + var mvn = Operations.getCustomPathOrElse("mvn"); + var tmpEffPom = Files.createTempFile("exhort_eff_pom_", ".xml"); + var mvnEffPomCmd = new String[] { + mvn, + "clean", + "help:effective-pom", + String.format("-Doutput=%s", tmpEffPom.toString()), + "-f", + originPom.toString() + }; + // execute the effective pom command + Operations.runProcess(mvnEffPomCmd, getMvnExecEnvs()); + if (debugLoggingIsNeeded()) { + String CaEffectivePoM = Files.readString(tmpEffPom); + log.info(String.format( + "Package Manager Maven Component Analysis Effective POM Output : %s %s", + System.lineSeparator(), CaEffectivePoM)); } + // if we have dependencies marked as ignored grab ignored dependencies from the original pom + // the effective-pom goal doesn't carry comments + List dependencies = getDependencies(originPom); + var ignored = dependencies.stream() + .filter(d -> d.ignored) + .map(DependencyAggregator::toPurl) + .collect(Collectors.toSet()); + var testsDeps = dependencies.stream() + .filter(DependencyAggregator::isTestDependency) + .collect(Collectors.toSet()); + var deps = getDependencies(tmpEffPom); + var sbom = SbomFactory.newInstance().addRoot(getRoot(tmpEffPom)); + deps.stream() + .filter(dep -> !testsDeps.contains(dep)) + .map(DependencyAggregator::toPurl) + .filter(dep -> ignored.stream() + .filter(artifact -> artifact.isCoordinatesEquals(dep)) + .collect(Collectors.toList()) + .size() + == 0) + .forEach(d -> sbom.addDependency(sbom.getRoot(), d)); + + // build and return content for constructing request to the backend + return new Content(sbom.getAsJsonString().getBytes(), Api.CYCLONEDX_MEDIA_TYPE); + } - // if dependency aggregator haven't been initiated, - // we're currently not iterating over a "dependency" tag - no need for further parsing - if (!Objects.isNull(dependencyAggregator)) { - // if we hit an ignore comment, mark aggregator to be ignored - if (reader.getEventType() == XMLStreamConstants.COMMENT - && "exhortignore".equals(reader.getText().strip()) - ) { - dependencyAggregator.ignored = true; - continue; - } - - if (reader.isStartElement()) { - // NOTE if we want to include "scope" tags in ignore, - // add a case here and a property in DependencyIgnore - switch (reader.getLocalName()) { - case "groupId": // starting "groupId" tag, get next event and set to aggregator - reader.next(); - dependencyAggregator.groupId = reader.getText(); - break; - case "artifactId": // starting "artifactId" tag, get next event and set to aggregator - reader.next(); - dependencyAggregator.artifactId = reader.getText(); - break; - - case "scope": - reader.next(); - dependencyAggregator.scope = reader.getText() != null ? reader.getText().trim() : "*"; - break; - case "version": // starting "version" tag, get next event and set to aggregator - reader.next(); - dependencyAggregator.version = reader.getText(); - break; - } - } + @Override + public Content provideComponent(Path manifestPath) throws IOException { + Content content = generateSbomFromEffectivePom(manifestPath); + return content; + } - if (reader.isEndElement() && "dependency".equals(reader.getLocalName())) { - // add object to list and reset dependency aggregator - deps.add(dependencyAggregator); - dependencyAggregator = null; - } - } - } - } catch (XMLStreamException exc) { - throw new IOException(exc); - } finally { - if (!Objects.isNull(reader)) { + private PackageURL getRoot(final Path manifestPath) throws IOException { + XMLStreamReader reader = null; try { - reader.close(); // close stream if open - } catch (XMLStreamException e) { - // + reader = XMLInputFactory.newInstance().createXMLStreamReader(Files.newInputStream(manifestPath)); + DependencyAggregator dependencyAggregator = null; + boolean isRoot = false; + while (reader.hasNext()) { + reader.next(); // get the next event + if (reader.isStartElement() && "project".equals(reader.getLocalName())) { + isRoot = true; + dependencyAggregator = new DependencyAggregator(); + continue; + } + if (!Objects.isNull(dependencyAggregator)) { + if (reader.isStartElement()) { + switch (reader.getLocalName()) { + case "groupId": // starting "groupId" tag, get next event and set to aggregator + reader.next(); + dependencyAggregator.groupId = reader.getText(); + break; + case "artifactId": // starting "artifactId" tag, get next event and set to aggregator + reader.next(); + dependencyAggregator.artifactId = reader.getText(); + break; + case "version": // starting "version" tag, get next event and set to aggregator + reader.next(); + dependencyAggregator.version = reader.getText(); + break; + } + } + if (isRoot && dependencyAggregator.isValid()) { + return dependencyAggregator.toPurl(); + } + } + } + } catch (XMLStreamException exc) { + throw new IOException(exc); + } finally { + if (!Objects.isNull(reader)) { + try { + reader.close(); // close stream if open + } catch (XMLStreamException e) { + // + } + } } - } - } - return deps; - } - - Map getMvnExecEnvs() { - var javaHome = ExhortApi.getStringValueEnvironment("JAVA_HOME",""); - if (javaHome != null && !javaHome.isBlank()) { - return Collections.singletonMap("JAVA_HOME", javaHome); + throw new IllegalStateException("Unable to retrieve Root dependency from effective pom"); } - return null; - } - // NOTE if we want to include "scope" tags in ignore, - // add property here and a case in the start-element-switch in the getIgnored method - /** Aggregator class for aggregating Dependency data over stream iterations, **/ - private final static class DependencyAggregator { - private String scope="*"; - private String groupId; - private String artifactId; - private String version; - boolean ignored = false; + private List getDependencies(final Path manifestPath) throws IOException { + List deps = new ArrayList<>(); + XMLStreamReader reader = null; + try { + // get a xml stream reader for the manifest file + reader = XMLInputFactory.newInstance().createXMLStreamReader(Files.newInputStream(manifestPath)); + // the following dependencyIgnore object is used to aggregate dependency data over iterations + // when a "dependency" tag starts, it will be initiated, + // when a "dependency" tag ends, it will be parsed, act upon, and reset + DependencyAggregator dependencyAggregator = null; + while (reader.hasNext()) { + reader.next(); // get the next event + if (reader.isStartElement() && "dependency".equals(reader.getLocalName())) { + // starting "dependency" tag, initiate aggregator + dependencyAggregator = new DependencyAggregator(); + continue; + } + + // if dependency aggregator haven't been initiated, + // we're currently not iterating over a "dependency" tag - no need for further parsing + if (!Objects.isNull(dependencyAggregator)) { + // if we hit an ignore comment, mark aggregator to be ignored + if (reader.getEventType() == XMLStreamConstants.COMMENT + && "exhortignore".equals(reader.getText().strip())) { + dependencyAggregator.ignored = true; + continue; + } + + if (reader.isStartElement()) { + // NOTE if we want to include "scope" tags in ignore, + // add a case here and a property in DependencyIgnore + switch (reader.getLocalName()) { + case "groupId": // starting "groupId" tag, get next event and set to aggregator + reader.next(); + dependencyAggregator.groupId = reader.getText(); + break; + case "artifactId": // starting "artifactId" tag, get next event and set to aggregator + reader.next(); + dependencyAggregator.artifactId = reader.getText(); + break; + + case "scope": + reader.next(); + dependencyAggregator.scope = reader.getText() != null + ? reader.getText().trim() + : "*"; + break; + case "version": // starting "version" tag, get next event and set to aggregator + reader.next(); + dependencyAggregator.version = reader.getText(); + break; + } + } + + if (reader.isEndElement() && "dependency".equals(reader.getLocalName())) { + // add object to list and reset dependency aggregator + deps.add(dependencyAggregator); + dependencyAggregator = null; + } + } + } + } catch (XMLStreamException exc) { + throw new IOException(exc); + } finally { + if (!Objects.isNull(reader)) { + try { + reader.close(); // close stream if open + } catch (XMLStreamException e) { + // + } + } + } - /** - * Get the string representation of the dependency to use as excludes - * @return an exclude string for the dependency:tree plugin, ie. group-id:artifact-id:*:version - */ - @Override - public String toString() { - // NOTE if you add scope, don't forget to replace the * with its value - return String.format("%s:%s:%s:%s", groupId, artifactId,scope, version); + return deps; } - public boolean isValid() { - return Objects.nonNull(groupId) && Objects.nonNull(artifactId) && Objects.nonNull(version); + Map getMvnExecEnvs() { + var javaHome = ExhortApi.getStringValueEnvironment("JAVA_HOME", ""); + if (javaHome != null && !javaHome.isBlank()) { + return Collections.singletonMap("JAVA_HOME", javaHome); + } + return null; } - public boolean isTestDependency() - { - return scope.trim().equals("test"); - } + // NOTE if we want to include "scope" tags in ignore, + // add property here and a case in the start-element-switch in the getIgnored method + /** Aggregator class for aggregating Dependency data over stream iterations, **/ + private static final class DependencyAggregator { + private String scope = "*"; + private String groupId; + private String artifactId; + private String version; + boolean ignored = false; + + /** + * Get the string representation of the dependency to use as excludes + * @return an exclude string for the dependency:tree plugin, ie. group-id:artifact-id:*:version + */ + @Override + public String toString() { + // NOTE if you add scope, don't forget to replace the * with its value + return String.format("%s:%s:%s:%s", groupId, artifactId, scope, version); + } - public PackageURL toPurl() { - try { - return new PackageURL(Type.MAVEN.getType(), groupId, artifactId, version, this.scope == "*" ? null :new TreeMap<>(Map.of("scope",this.scope)), null); - } catch (MalformedPackageURLException e) { - throw new IllegalArgumentException("Unable to parse PackageURL", e); - } - } + public boolean isValid() { + return Objects.nonNull(groupId) && Objects.nonNull(artifactId) && Objects.nonNull(version); + } - @Override - public boolean equals(Object o) { - if (this == o) return true; - if (!(o instanceof DependencyAggregator)) return false; - var that = (DependencyAggregator) o; - // NOTE we do not compare the ignored field - // This is required for comparing pom.xml with effective_pom.xml as the latter doesn't - // contain comments indicating ignore - return Objects.equals(this.groupId, that.groupId) && - Objects.equals(this.artifactId, that.artifactId) && - Objects.equals(this.version, that.version); + public boolean isTestDependency() { + return scope.trim().equals("test"); + } - } + public PackageURL toPurl() { + try { + return new PackageURL( + Type.MAVEN.getType(), + groupId, + artifactId, + version, + this.scope == "*" ? null : new TreeMap<>(Map.of("scope", this.scope)), + null); + } catch (MalformedPackageURLException e) { + throw new IllegalArgumentException("Unable to parse PackageURL", e); + } + } - @Override - public int hashCode() { - return Objects.hash(groupId, artifactId, version); + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (!(o instanceof DependencyAggregator)) return false; + var that = (DependencyAggregator) o; + // NOTE we do not compare the ignored field + // This is required for comparing pom.xml with effective_pom.xml as the latter doesn't + // contain comments indicating ignore + return Objects.equals(this.groupId, that.groupId) + && Objects.equals(this.artifactId, that.artifactId) + && Objects.equals(this.version, that.version); + } + + @Override + public int hashCode() { + return Objects.hash(groupId, artifactId, version); + } } - } } diff --git a/src/main/java/com/redhat/exhort/providers/JavaScriptNpmProvider.java b/src/main/java/com/redhat/exhort/providers/JavaScriptNpmProvider.java index f58ab34a..1427a752 100644 --- a/src/main/java/com/redhat/exhort/providers/JavaScriptNpmProvider.java +++ b/src/main/java/com/redhat/exhort/providers/JavaScriptNpmProvider.java @@ -15,17 +15,7 @@ */ package com.redhat.exhort.providers; -import java.io.File; -import java.io.IOException; -import java.nio.charset.StandardCharsets; -import java.nio.file.Files; -import java.nio.file.Path; -import java.util.ArrayList; -import java.util.Collections; -import java.util.Iterator; -import java.util.List; -import java.util.Map; -import java.util.Map.Entry; +import static com.redhat.exhort.impl.ExhortApi.debugLoggingIsNeeded; import com.fasterxml.jackson.core.JsonProcessingException; import com.fasterxml.jackson.databind.JsonMappingException; @@ -40,8 +30,17 @@ import com.redhat.exhort.tools.Ecosystem; import com.redhat.exhort.tools.Ecosystem.Type; import com.redhat.exhort.tools.Operations; - -import static com.redhat.exhort.impl.ExhortApi.debugLoggingIsNeeded; +import java.io.File; +import java.io.IOException; +import java.nio.charset.StandardCharsets; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.ArrayList; +import java.util.Collections; +import java.util.Iterator; +import java.util.List; +import java.util.Map; +import java.util.Map.Entry; /** * Concrete implementation of the {@link Provider} used for converting @@ -51,165 +50,190 @@ **/ public final class JavaScriptNpmProvider extends Provider { - private System.Logger log = System.getLogger(this.getClass().getName()); - public JavaScriptNpmProvider() { - super(Type.NPM); - } - - @Override - public Content provideStack(final Path manifestPath) throws IOException { - // check for custom npm executable - Sbom sbom = getDependencySbom(manifestPath, true, false); - return new Content(sbom.getAsJsonString().getBytes(StandardCharsets.UTF_8), Api.CYCLONEDX_MEDIA_TYPE); - } - - @Override - public Content provideComponent(byte[] manifestContent) throws IOException { - // check for custom npm executable - return new Content(getDependencyTree(manifestContent).getAsJsonString().getBytes(StandardCharsets.UTF_8), - Api.CYCLONEDX_MEDIA_TYPE); - } - - @Override - public Content provideComponent(Path manifestPath) throws IOException { - return new Content(getDependencySbom(manifestPath, false,false).getAsJsonString().getBytes(StandardCharsets.UTF_8), - Api.CYCLONEDX_MEDIA_TYPE); - } - - private Sbom getDependencyTree(byte[] manifestContent) { - Sbom sbom; - try { - Path tempDir = Files.createTempDirectory("exhort_npm"); - Path path = Files.createFile(Path.of(tempDir.toString(),"package.json")); - Files.write(path, manifestContent); - sbom = getDependencySbom(path, false, true); - Files.delete(path); - } catch (IOException e) { - throw new RuntimeException(e); - } - return sbom; - } + private System.Logger log = System.getLogger(this.getClass().getName()); - private PackageURL getRoot(JsonNode jsonDependenciesNpm) throws MalformedPackageURLException { - return toPurl(jsonDependenciesNpm.get("name").asText(), jsonDependenciesNpm.get("version").asText()); - } + public JavaScriptNpmProvider() { + super(Type.NPM); + } - private PackageURL toPurl(String name, String version) throws MalformedPackageURLException { - String[] parts = name.split("/"); - if (parts.length == 2) { - return new PackageURL(Ecosystem.Type.NPM.getType(), parts[0], parts[1], version, null, null); + @Override + public Content provideStack(final Path manifestPath) throws IOException { + // check for custom npm executable + Sbom sbom = getDependencySbom(manifestPath, true, false); + return new Content(sbom.getAsJsonString().getBytes(StandardCharsets.UTF_8), Api.CYCLONEDX_MEDIA_TYPE); } - return new PackageURL(Ecosystem.Type.NPM.getType(), null, parts[0], version, null, null); - } - - private void addDependenciesOf(Sbom sbom, PackageURL from, JsonNode dependencies) - throws MalformedPackageURLException { - Iterator> fields = dependencies.fields(); - while (fields.hasNext()) { - Entry e = fields.next(); - String name = e.getKey(); - JsonNode versionNode = e.getValue().get("version"); - if (versionNode == null) { - continue; //ignore optional dependencies - } - String version = versionNode.asText(); - PackageURL purl = toPurl(name, version); - sbom.addDependency(from, purl); - JsonNode transitiveDeps = e.getValue().findValue("dependencies"); - if (transitiveDeps != null) { - addDependenciesOf(sbom, purl, transitiveDeps); - } + + @Override + public Content provideComponent(byte[] manifestContent) throws IOException { + // check for custom npm executable + return new Content( + getDependencyTree(manifestContent).getAsJsonString().getBytes(StandardCharsets.UTF_8), + Api.CYCLONEDX_MEDIA_TYPE); } - } - - private Sbom getDependencySbom(Path manifestPath, boolean includeTransitive, boolean deletePackageLock) throws IOException { - var npmListResult = buildNpmDependencyTree(manifestPath, includeTransitive,deletePackageLock); - var sbom = buildSbom(npmListResult); - sbom.filterIgnoredDeps(getIgnoredDeps(manifestPath)); - return sbom; - } - - private JsonNode buildNpmDependencyTree(Path manifestPath, boolean includeTransitive, boolean deletePackageLock) - throws JsonMappingException, JsonProcessingException { - var npm = Operations.getCustomPathOrElse("npm"); - var npmEnvs = getNpmExecEnv(); - // clean command used to clean build target - Path packageLockJson = Path.of(manifestPath.getParent().toString(), "package-lock.json"); - var createPackageLock = new String[] { npm, "i", "--package-lock-only", "--prefix", - manifestPath.getParent().toString() }; - // execute the clean command - Operations.runProcess(createPackageLock, npmEnvs); - String[] npmAllDeps; - Path workDir=null; - if(!manifestPath.getParent().toString().trim().contains(" ")) { - - npmAllDeps = new String[]{npm, "ls", includeTransitive ? "--all" : "", "--omit=dev", "--package-lock-only", - "--json", "--prefix", manifestPath.getParent().toString()}; + + @Override + public Content provideComponent(Path manifestPath) throws IOException { + return new Content( + getDependencySbom(manifestPath, false, false).getAsJsonString().getBytes(StandardCharsets.UTF_8), + Api.CYCLONEDX_MEDIA_TYPE); } - else { - npmAllDeps = new String[]{npm, "ls", includeTransitive ? "--all" : "", "--omit=dev", "--package-lock-only", - "--json"}; - workDir = manifestPath.getParent(); + + private Sbom getDependencyTree(byte[] manifestContent) { + Sbom sbom; + try { + Path tempDir = Files.createTempDirectory("exhort_npm"); + Path path = Files.createFile(Path.of(tempDir.toString(), "package.json")); + Files.write(path, manifestContent); + sbom = getDependencySbom(path, false, true); + Files.delete(path); + } catch (IOException e) { + throw new RuntimeException(e); + } + return sbom; } - // execute the clean command - String npmOutput; - if (npmEnvs != null) { - npmOutput = Operations.runProcessGetOutput(workDir, npmAllDeps, - npmEnvs.entrySet().stream().map(e -> e.getKey() + "=" + e.getValue()).toArray(String[]::new)); - } else { - npmOutput = Operations.runProcessGetOutput(workDir, npmAllDeps); + + private PackageURL getRoot(JsonNode jsonDependenciesNpm) throws MalformedPackageURLException { + return toPurl( + jsonDependenciesNpm.get("name").asText(), + jsonDependenciesNpm.get("version").asText()); } - if(debugLoggingIsNeeded()) { - log.log(System.Logger.Level.INFO,String.format("Npm Listed Install Pacakges in Json : %s %s",System.lineSeparator(),npmOutput)); + + private PackageURL toPurl(String name, String version) throws MalformedPackageURLException { + String[] parts = name.split("/"); + if (parts.length == 2) { + return new PackageURL(Ecosystem.Type.NPM.getType(), parts[0], parts[1], version, null, null); + } + return new PackageURL(Ecosystem.Type.NPM.getType(), null, parts[0], version, null, null); } - if(!includeTransitive) { - if (deletePackageLock) { - try { - Files.delete(packageLockJson); - } catch (IOException e) { - throw new RuntimeException(e); + + private void addDependenciesOf(Sbom sbom, PackageURL from, JsonNode dependencies) + throws MalformedPackageURLException { + Iterator> fields = dependencies.fields(); + while (fields.hasNext()) { + Entry e = fields.next(); + String name = e.getKey(); + JsonNode versionNode = e.getValue().get("version"); + if (versionNode == null) { + continue; // ignore optional dependencies + } + String version = versionNode.asText(); + PackageURL purl = toPurl(name, version); + sbom.addDependency(from, purl); + JsonNode transitiveDeps = e.getValue().findValue("dependencies"); + if (transitiveDeps != null) { + addDependenciesOf(sbom, purl, transitiveDeps); + } } - } } - return objectMapper.readTree(npmOutput); - } - - private Sbom buildSbom(JsonNode npmListResult) { - Sbom sbom = SbomFactory.newInstance(); - try { - PackageURL root = getRoot(npmListResult); - sbom.addRoot(root); - JsonNode dependencies = npmListResult.get("dependencies"); - addDependenciesOf(sbom, root, dependencies); - } catch (MalformedPackageURLException e) { - throw new IllegalArgumentException("Unable to parse NPM Json", e); + + private Sbom getDependencySbom(Path manifestPath, boolean includeTransitive, boolean deletePackageLock) + throws IOException { + var npmListResult = buildNpmDependencyTree(manifestPath, includeTransitive, deletePackageLock); + var sbom = buildSbom(npmListResult); + sbom.filterIgnoredDeps(getIgnoredDeps(manifestPath)); + return sbom; } - return sbom; - } - - private List getIgnoredDeps(Path manifestPath) throws IOException { - var ignored = new ArrayList(); - var root = new ObjectMapper().readTree(Files.newInputStream(manifestPath)); - var ignoredNode = root.withArray("exhortignore"); - if (ignoredNode == null) { - return ignored; + + private JsonNode buildNpmDependencyTree(Path manifestPath, boolean includeTransitive, boolean deletePackageLock) + throws JsonMappingException, JsonProcessingException { + var npm = Operations.getCustomPathOrElse("npm"); + var npmEnvs = getNpmExecEnv(); + // clean command used to clean build target + Path packageLockJson = Path.of(manifestPath.getParent().toString(), "package-lock.json"); + var createPackageLock = new String[] { + npm, + "i", + "--package-lock-only", + "--prefix", + manifestPath.getParent().toString() + }; + // execute the clean command + Operations.runProcess(createPackageLock, npmEnvs); + String[] npmAllDeps; + Path workDir = null; + if (!manifestPath.getParent().toString().trim().contains(" ")) { + + npmAllDeps = new String[] { + npm, + "ls", + includeTransitive ? "--all" : "", + "--omit=dev", + "--package-lock-only", + "--json", + "--prefix", + manifestPath.getParent().toString() + }; + } else { + npmAllDeps = new String[] { + npm, "ls", includeTransitive ? "--all" : "", "--omit=dev", "--package-lock-only", "--json" + }; + workDir = manifestPath.getParent(); + } + // execute the clean command + String npmOutput; + if (npmEnvs != null) { + npmOutput = Operations.runProcessGetOutput( + workDir, + npmAllDeps, + npmEnvs.entrySet().stream() + .map(e -> e.getKey() + "=" + e.getValue()) + .toArray(String[]::new)); + } else { + npmOutput = Operations.runProcessGetOutput(workDir, npmAllDeps); + } + if (debugLoggingIsNeeded()) { + log.log( + System.Logger.Level.INFO, + String.format("Npm Listed Install Pacakges in Json : %s %s", System.lineSeparator(), npmOutput)); + } + if (!includeTransitive) { + if (deletePackageLock) { + try { + Files.delete(packageLockJson); + } catch (IOException e) { + throw new RuntimeException(e); + } + } + } + return objectMapper.readTree(npmOutput); } - for (JsonNode n : ignoredNode) { - ignored.add(n.asText()); + + private Sbom buildSbom(JsonNode npmListResult) { + Sbom sbom = SbomFactory.newInstance(); + try { + PackageURL root = getRoot(npmListResult); + sbom.addRoot(root); + JsonNode dependencies = npmListResult.get("dependencies"); + addDependenciesOf(sbom, root, dependencies); + } catch (MalformedPackageURLException e) { + throw new IllegalArgumentException("Unable to parse NPM Json", e); + } + return sbom; } - return ignored; - } - - Map getNpmExecEnv() { - String nodeHome = System.getProperty("NODE_HOME"); - if (nodeHome != null && !nodeHome.isBlank()) { - String path = System.getenv("PATH"); - if (path != null) { - return Collections.singletonMap("PATH", path + File.pathSeparator + nodeHome); - } else { - return Collections.singletonMap("PATH", nodeHome); - } + + private List getIgnoredDeps(Path manifestPath) throws IOException { + var ignored = new ArrayList(); + var root = new ObjectMapper().readTree(Files.newInputStream(manifestPath)); + var ignoredNode = root.withArray("exhortignore"); + if (ignoredNode == null) { + return ignored; + } + for (JsonNode n : ignoredNode) { + ignored.add(n.asText()); + } + return ignored; + } + + Map getNpmExecEnv() { + String nodeHome = System.getProperty("NODE_HOME"); + if (nodeHome != null && !nodeHome.isBlank()) { + String path = System.getenv("PATH"); + if (path != null) { + return Collections.singletonMap("PATH", path + File.pathSeparator + nodeHome); + } else { + return Collections.singletonMap("PATH", nodeHome); + } + } + return null; } - return null; - } } diff --git a/src/main/java/com/redhat/exhort/providers/PythonPipProvider.java b/src/main/java/com/redhat/exhort/providers/PythonPipProvider.java index 1e51a6c7..d286555e 100644 --- a/src/main/java/com/redhat/exhort/providers/PythonPipProvider.java +++ b/src/main/java/com/redhat/exhort/providers/PythonPipProvider.java @@ -15,6 +15,8 @@ */ package com.redhat.exhort.providers; +import static com.redhat.exhort.impl.ExhortApi.*; + import com.fasterxml.jackson.core.JsonProcessingException; import com.github.packageurl.MalformedPackageURLException; import com.github.packageurl.PackageURL; @@ -28,7 +30,6 @@ import com.redhat.exhort.utils.PythonControllerBase; import com.redhat.exhort.utils.PythonControllerRealEnv; import com.redhat.exhort.utils.PythonControllerVirtualEnv; - import java.io.IOException; import java.nio.charset.StandardCharsets; import java.nio.file.Files; @@ -38,242 +39,241 @@ import java.util.logging.Logger; import java.util.stream.Collectors; -import static com.redhat.exhort.impl.ExhortApi.*; - public final class PythonPipProvider extends Provider { - private Logger log = LoggersFactory.getLogger(this.getClass().getName()); - public void setPythonController(PythonControllerBase pythonController) { - this.pythonController = pythonController; - } + private Logger log = LoggersFactory.getLogger(this.getClass().getName()); - private PythonControllerBase pythonController; - public static void main(String[] args) { - try { - PythonPipProvider pythonPipProvider = new PythonPipProvider(); -// byte[] bytes = Files.readAllBytes(Path.of("/tmp/exhort_env/requirements.txt")); -// Content content = pythonPipProvider.provideComponent(bytes); - Content content = pythonPipProvider.provideStack(Path.of("/home/zgrinber/git/exhort-java-api/src/test/resources/tst_manifests/pip/pip_requirements_txt_ignore/requirements.txt")); - String s = new String(content.buffer); - System.out.print(s); - } catch (IOException e) { - throw new RuntimeException(e); + public void setPythonController(PythonControllerBase pythonController) { + this.pythonController = pythonController; } - } - public PythonPipProvider() { - super(Ecosystem.Type.PYTHON); - } + private PythonControllerBase pythonController; - @Override - public Content provideStack(Path manifestPath) throws IOException { - PythonControllerBase pythonController = getPythonController(); - List> dependencies = pythonController.getDependencies(manifestPath.toString(), true); - printDependenciesTree(dependencies); - Sbom sbom = SbomFactory.newInstance(Sbom.BelongingCondition.PURL,"sensitive"); - try { - sbom.addRoot(new PackageURL(Ecosystem.Type.PYTHON.getType(), "root")); - } catch (MalformedPackageURLException e) { - throw new RuntimeException(e); + public static void main(String[] args) { + try { + PythonPipProvider pythonPipProvider = new PythonPipProvider(); + // byte[] bytes = Files.readAllBytes(Path.of("/tmp/exhort_env/requirements.txt")); + // Content content = pythonPipProvider.provideComponent(bytes); + Content content = pythonPipProvider.provideStack( + Path.of( + "/home/zgrinber/git/exhort-java-api/src/test/resources/tst_manifests/pip/pip_requirements_txt_ignore/requirements.txt")); + String s = new String(content.buffer); + System.out.print(s); + } catch (IOException e) { + throw new RuntimeException(e); + } } - dependencies.stream().forEach((component) -> - { - addAllDependencies(sbom.getRoot(), component, sbom); - - }); - byte[] requirementsFile = Files.readAllBytes(manifestPath); - handleIgnoredDependencies(new String(requirementsFile), sbom); - // In python' pip requirements.txt, there is no real root element, then need to remove dummy root element that was created for creating the sbom. - sbom.removeRootComponent(); - return new Content(sbom.getAsJsonString().getBytes(StandardCharsets.UTF_8), Api.CYCLONEDX_MEDIA_TYPE); - } - - private void addAllDependencies(PackageURL source, Map component, Sbom sbom) { - - sbom.addDependency(source, toPurl((String) component.get("name"), (String) component.get("version"))); - List directDeps = (List) component.get("dependencies"); - if (directDeps != null) -// { - directDeps.stream().forEach(dep -> { - String name = (String) dep.get("name"); - String version = (String) dep.get("version"); - addAllDependencies(toPurl((String) component.get("name"), (String) component.get("version")), dep, sbom); - }); -// -// } - - } - - @Override - public Content provideComponent(byte[] manifestContent) throws IOException { - PythonControllerBase pythonController = getPythonController(); - Path tempRepository = Files.createTempDirectory("exhort-pip"); - Path path = Paths.get(tempRepository.toAbsolutePath().normalize().toString(), "requirements.txt"); - Files.deleteIfExists(path); - Path manifestPath = Files.createFile(path); - Files.write(manifestPath, manifestContent); - List> dependencies = pythonController.getDependencies(manifestPath.toString(), false); - printDependenciesTree(dependencies); - Sbom sbom = SbomFactory.newInstance(); - try { - sbom.addRoot(new PackageURL(Ecosystem.Type.PYTHON.getType(), "root")); - } catch (MalformedPackageURLException e) { - throw new RuntimeException(e); + public PythonPipProvider() { + super(Ecosystem.Type.PYTHON); } - dependencies.stream().forEach((component) -> - { - sbom.addDependency(sbom.getRoot(), toPurl((String) component.get("name"), (String) component.get("version"))); - }); - Files.delete(manifestPath); - Files.delete(tempRepository); - handleIgnoredDependencies(new String(manifestContent), sbom); - // In python' pip requirements.txt, there is no real root element, then need to remove dummy root element that was created for creating the sbom. - sbom.removeRootComponent(); - return new Content(sbom.getAsJsonString().getBytes(StandardCharsets.UTF_8), Api.CYCLONEDX_MEDIA_TYPE); - } + @Override + public Content provideStack(Path manifestPath) throws IOException { + PythonControllerBase pythonController = getPythonController(); + List> dependencies = pythonController.getDependencies(manifestPath.toString(), true); + printDependenciesTree(dependencies); + Sbom sbom = SbomFactory.newInstance(Sbom.BelongingCondition.PURL, "sensitive"); + try { + sbom.addRoot(new PackageURL(Ecosystem.Type.PYTHON.getType(), "root")); + } catch (MalformedPackageURLException e) { + throw new RuntimeException(e); + } + dependencies.stream().forEach((component) -> { + addAllDependencies(sbom.getRoot(), component, sbom); + }); + byte[] requirementsFile = Files.readAllBytes(manifestPath); + handleIgnoredDependencies(new String(requirementsFile), sbom); + // In python' pip requirements.txt, there is no real root element, then need to remove dummy root element that + // was created for creating the sbom. + sbom.removeRootComponent(); + return new Content(sbom.getAsJsonString().getBytes(StandardCharsets.UTF_8), Api.CYCLONEDX_MEDIA_TYPE); + } - private void printDependenciesTree(List> dependencies) throws JsonProcessingException { - if(debugLoggingIsNeeded()) { - String pythonControllerTree = objectMapper.writerWithDefaultPrettyPrinter().writeValueAsString(dependencies); - log.info(String.format("Python Generated Dependency Tree in Json Format: %s %s %s",System.lineSeparator(),pythonControllerTree,System.lineSeparator())); + private void addAllDependencies(PackageURL source, Map component, Sbom sbom) { - } - } + sbom.addDependency(source, toPurl((String) component.get("name"), (String) component.get("version"))); + List directDeps = (List) component.get("dependencies"); + if (directDeps != null) + // { + directDeps.stream().forEach(dep -> { + String name = (String) dep.get("name"); + String version = (String) dep.get("version"); - private void handleIgnoredDependencies(String manifestContent, Sbom sbom) { - Set ignoredDeps = getIgnoredDependencies(manifestContent); - Set ignoredDepsVersions = ignoredDeps - .stream() - .filter(dep -> !dep.getVersion().trim().equals("*")) - .map(PackageURL::getCoordinates) - .collect(Collectors.toSet()); - Set ignoredDepsNoVersions = ignoredDeps - .stream() - .filter(dep -> dep.getVersion().trim().equals("*")) - .map(PackageURL::getCoordinates) - .collect(Collectors.toSet()); + addAllDependencies( + toPurl((String) component.get("name"), (String) component.get("version")), dep, sbom); + }); + // + // } -// filter out by name only from sbom all exhortignore dependencies that their version will be resolved by pip. - sbom.setBelongingCriteriaBinaryAlgorithm(Sbom.BelongingCondition.NAME); - sbom.filterIgnoredDeps(ignoredDepsNoVersions); - boolean matchManifestVersions = getBooleanValueEnvironment("MATCH_MANIFEST_VERSIONS", "true"); - // filter out by purl from sbom all exhortignore dependencies that their version hardcoded in requirements.txt - in case all versions in manifest matching installed versions of packages in environment. - if(matchManifestVersions) - { - sbom.setBelongingCriteriaBinaryAlgorithm(Sbom.BelongingCondition.PURL); - sbom.filterIgnoredDeps(ignoredDepsVersions); } - else - { -// in case version mismatch is possible (MATCH_MANIFEST_VERSIONS=false) , need to parse the name of package from the purl, and remove the package name from sbom according to name only - Set deps = (Set) ignoredDepsVersions.stream().map(purlString -> { + + @Override + public Content provideComponent(byte[] manifestContent) throws IOException { + PythonControllerBase pythonController = getPythonController(); + Path tempRepository = Files.createTempDirectory("exhort-pip"); + Path path = Paths.get(tempRepository.toAbsolutePath().normalize().toString(), "requirements.txt"); + Files.deleteIfExists(path); + Path manifestPath = Files.createFile(path); + Files.write(manifestPath, manifestContent); + List> dependencies = pythonController.getDependencies(manifestPath.toString(), false); + printDependenciesTree(dependencies); + Sbom sbom = SbomFactory.newInstance(); try { - return new PackageURL((String) purlString).getName(); + sbom.addRoot(new PackageURL(Ecosystem.Type.PYTHON.getType(), "root")); } catch (MalformedPackageURLException e) { - throw new RuntimeException(e); + throw new RuntimeException(e); } - }).collect(Collectors.toSet()); - sbom.setBelongingCriteriaBinaryAlgorithm(Sbom.BelongingCondition.NAME); - sbom.filterIgnoredDeps(deps); + dependencies.stream().forEach((component) -> { + sbom.addDependency( + sbom.getRoot(), toPurl((String) component.get("name"), (String) component.get("version"))); + }); + Files.delete(manifestPath); + Files.delete(tempRepository); + handleIgnoredDependencies(new String(manifestContent), sbom); + // In python' pip requirements.txt, there is no real root element, then need to remove dummy root element that + // was created for creating the sbom. + sbom.removeRootComponent(); + return new Content(sbom.getAsJsonString().getBytes(StandardCharsets.UTF_8), Api.CYCLONEDX_MEDIA_TYPE); } + private void printDependenciesTree(List> dependencies) throws JsonProcessingException { + if (debugLoggingIsNeeded()) { + String pythonControllerTree = + objectMapper.writerWithDefaultPrettyPrinter().writeValueAsString(dependencies); + log.info(String.format( + "Python Generated Dependency Tree in Json Format: %s %s %s", + System.lineSeparator(), pythonControllerTree, System.lineSeparator())); + } + } + private void handleIgnoredDependencies(String manifestContent, Sbom sbom) { + Set ignoredDeps = getIgnoredDependencies(manifestContent); + Set ignoredDepsVersions = ignoredDeps.stream() + .filter(dep -> !dep.getVersion().trim().equals("*")) + .map(PackageURL::getCoordinates) + .collect(Collectors.toSet()); + Set ignoredDepsNoVersions = ignoredDeps.stream() + .filter(dep -> dep.getVersion().trim().equals("*")) + .map(PackageURL::getCoordinates) + .collect(Collectors.toSet()); + + // filter out by name only from sbom all exhortignore dependencies that their version will be resolved by pip. + sbom.setBelongingCriteriaBinaryAlgorithm(Sbom.BelongingCondition.NAME); + sbom.filterIgnoredDeps(ignoredDepsNoVersions); + boolean matchManifestVersions = getBooleanValueEnvironment("MATCH_MANIFEST_VERSIONS", "true"); + // filter out by purl from sbom all exhortignore dependencies that their version hardcoded in requirements.txt - + // in case all versions in manifest matching installed versions of packages in environment. + if (matchManifestVersions) { + sbom.setBelongingCriteriaBinaryAlgorithm(Sbom.BelongingCondition.PURL); + sbom.filterIgnoredDeps(ignoredDepsVersions); + } else { + // in case version mismatch is possible (MATCH_MANIFEST_VERSIONS=false) , need to parse the name of package + // from the purl, and remove the package name from sbom according to name only + Set deps = (Set) ignoredDepsVersions.stream() + .map(purlString -> { + try { + return new PackageURL((String) purlString).getName(); + } catch (MalformedPackageURLException e) { + throw new RuntimeException(e); + } + }) + .collect(Collectors.toSet()); + sbom.setBelongingCriteriaBinaryAlgorithm(Sbom.BelongingCondition.NAME); + sbom.filterIgnoredDeps(deps); + } + } + private Set getIgnoredDependencies(String requirementsDeps) { - } - - private Set getIgnoredDependencies(String requirementsDeps) { - - String[] requirementsLines = requirementsDeps.split(System.lineSeparator()); - Set collected = Arrays.stream(requirementsLines) - .filter(line -> line.contains("#exhortignore") || line.contains("# exhortignore")) - .map(PythonPipProvider::extractDepFull) - .map(this::splitToNameVersion) - .map(dep -> toPurl(dep[0], dep[1])) -// .map(packageURL -> packageURL.getCoordinates()) - .collect(Collectors.toSet()); + String[] requirementsLines = requirementsDeps.split(System.lineSeparator()); + Set collected = Arrays.stream(requirementsLines) + .filter(line -> line.contains("#exhortignore") || line.contains("# exhortignore")) + .map(PythonPipProvider::extractDepFull) + .map(this::splitToNameVersion) + .map(dep -> toPurl(dep[0], dep[1])) + // .map(packageURL -> packageURL.getCoordinates()) + .collect(Collectors.toSet()); - return collected; - } + return collected; + } - private String[] splitToNameVersion(String nameVersion) { - String[] result; - if (nameVersion.matches("[a-zA-Z0-9-_()]+={2}[0-9]{1,4}[.][0-9]{1,4}(([.][0-9]{1,4})|([.][a-zA-Z0-9]+)|([a-zA-Z0-9]+)|([.][a-zA-Z0-9]+[.][a-z-A-Z0-9]+))?")) { - result = nameVersion.split("=="); - } else { - String dependencyName = PythonControllerBase.getDependencyName(nameVersion); - result = new String[]{dependencyName, "*"}; + private String[] splitToNameVersion(String nameVersion) { + String[] result; + if (nameVersion.matches( + "[a-zA-Z0-9-_()]+={2}[0-9]{1,4}[.][0-9]{1,4}(([.][0-9]{1,4})|([.][a-zA-Z0-9]+)|([a-zA-Z0-9]+)|([.][a-zA-Z0-9]+[.][a-z-A-Z0-9]+))?")) { + result = nameVersion.split("=="); + } else { + String dependencyName = PythonControllerBase.getDependencyName(nameVersion); + result = new String[] {dependencyName, "*"}; + } + return result; } - return result; - } - private static String extractDepFull(String requirementLine) { - return requirementLine.substring(0, requirementLine.indexOf("#")).trim(); - } + private static String extractDepFull(String requirementLine) { + return requirementLine.substring(0, requirementLine.indexOf("#")).trim(); + } - private PackageURL toPurl(String name, String version) { + private PackageURL toPurl(String name, String version) { - try { - return new PackageURL(Ecosystem.Type.PYTHON.getType(), null, name, version, null, null); - } catch (MalformedPackageURLException e) { - throw new RuntimeException(e); + try { + return new PackageURL(Ecosystem.Type.PYTHON.getType(), null, name, version, null, null); + } catch (MalformedPackageURLException e) { + throw new RuntimeException(e); + } } - } - private PythonControllerBase getPythonController() { - String pythonPipBinaries; - String useVirtualPythonEnv; - if(!getStringValueEnvironment("EXHORT_PIP_SHOW","").trim().equals("") - && !getStringValueEnvironment("EXHORT_PIP_FREEZE","").trim().equals("")) { - pythonPipBinaries = "python;;pip"; - useVirtualPythonEnv = "false"; - } - else { - pythonPipBinaries = getPythonPipBinaries(); - useVirtualPythonEnv = Objects.requireNonNullElseGet( - System.getenv("EXHORT_PYTHON_VIRTUAL_ENV"), - () -> Objects.requireNonNullElse(System.getProperty("EXHORT_PYTHON_VIRTUAL_ENV"), "false")); - } + private PythonControllerBase getPythonController() { + String pythonPipBinaries; + String useVirtualPythonEnv; + if (!getStringValueEnvironment("EXHORT_PIP_SHOW", "").trim().equals("") + && !getStringValueEnvironment("EXHORT_PIP_FREEZE", "").trim().equals("")) { + pythonPipBinaries = "python;;pip"; + useVirtualPythonEnv = "false"; + } else { + pythonPipBinaries = getPythonPipBinaries(); + useVirtualPythonEnv = Objects.requireNonNullElseGet( + System.getenv("EXHORT_PYTHON_VIRTUAL_ENV"), + () -> Objects.requireNonNullElse(System.getProperty("EXHORT_PYTHON_VIRTUAL_ENV"), "false")); + } - String[] parts = pythonPipBinaries.split(";;"); - var python = parts[0]; - var pip = parts[1]; - useVirtualPythonEnv = Objects.requireNonNullElseGet( - System.getenv("EXHORT_PYTHON_VIRTUAL_ENV"), - () -> Objects.requireNonNullElse(System.getProperty("EXHORT_PYTHON_VIRTUAL_ENV"), "false")); - PythonControllerBase pythonController; - if(this.pythonController == null) { - if (Boolean.parseBoolean(useVirtualPythonEnv)) { - pythonController = new PythonControllerVirtualEnv(python); - } else { - pythonController = new PythonControllerRealEnv(python, pip); - } - } - else { - pythonController = this.pythonController; + String[] parts = pythonPipBinaries.split(";;"); + var python = parts[0]; + var pip = parts[1]; + useVirtualPythonEnv = Objects.requireNonNullElseGet( + System.getenv("EXHORT_PYTHON_VIRTUAL_ENV"), + () -> Objects.requireNonNullElse(System.getProperty("EXHORT_PYTHON_VIRTUAL_ENV"), "false")); + PythonControllerBase pythonController; + if (this.pythonController == null) { + if (Boolean.parseBoolean(useVirtualPythonEnv)) { + pythonController = new PythonControllerVirtualEnv(python); + } else { + pythonController = new PythonControllerRealEnv(python, pip); + } + } else { + pythonController = this.pythonController; + } + return pythonController; } - return pythonController; - } - private static String getPythonPipBinaries() { - var python = Operations.getCustomPathOrElse("python3"); - var pip = Operations.getCustomPathOrElse("pip3"); - try { - Operations.runProcess(python, "--version"); - Operations.runProcess(pip, "--version"); - } catch (Exception e) { - python = Operations.getCustomPathOrElse("python"); - pip = Operations.getCustomPathOrElse("pip"); - Operations.runProcess(python, "--version"); - Operations.runProcess(pip, "--version"); + private static String getPythonPipBinaries() { + var python = Operations.getCustomPathOrElse("python3"); + var pip = Operations.getCustomPathOrElse("pip3"); + try { + Operations.runProcess(python, "--version"); + Operations.runProcess(pip, "--version"); + } catch (Exception e) { + python = Operations.getCustomPathOrElse("python"); + pip = Operations.getCustomPathOrElse("pip"); + Operations.runProcess(python, "--version"); + Operations.runProcess(pip, "--version"); + } + return String.format("%s;;%s", python, pip); } - return String.format("%s;;%s", python, pip); - } - @Override - public Content provideComponent(Path manifestPath) throws IOException { - throw new IllegalArgumentException("provideComponent with file system path for Python pip package manager is not supported"); - } + @Override + public Content provideComponent(Path manifestPath) throws IOException { + throw new IllegalArgumentException( + "provideComponent with file system path for Python pip package manager is not supported"); + } } diff --git a/src/main/java/com/redhat/exhort/sbom/CycloneDXSbom.java b/src/main/java/com/redhat/exhort/sbom/CycloneDXSbom.java index 85e9fb9c..bf1809cc 100644 --- a/src/main/java/com/redhat/exhort/sbom/CycloneDXSbom.java +++ b/src/main/java/com/redhat/exhort/sbom/CycloneDXSbom.java @@ -15,14 +15,16 @@ */ package com.redhat.exhort.sbom; +import static com.redhat.exhort.impl.ExhortApi.debugLoggingIsNeeded; + +import com.github.packageurl.MalformedPackageURLException; +import com.github.packageurl.PackageURL; +import com.redhat.exhort.logging.LoggersFactory; import java.util.*; import java.util.function.BiPredicate; import java.util.function.Predicate; import java.util.logging.Logger; import java.util.stream.Collectors; - -import com.github.packageurl.MalformedPackageURLException; -import com.redhat.exhort.logging.LoggersFactory; import org.cyclonedx.BomGeneratorFactory; import org.cyclonedx.CycloneDxSchema.Version; import org.cyclonedx.model.Bom; @@ -31,23 +33,18 @@ import org.cyclonedx.model.Dependency; import org.cyclonedx.model.Metadata; -import com.github.packageurl.PackageURL; - -import static com.redhat.exhort.impl.ExhortApi.debugLoggingIsNeeded; - public class CycloneDXSbom implements Sbom { - private Logger log = LoggersFactory.getLogger(this.getClass().getName()); - private static final Version VERSION = Version.VERSION_14; - private String exhortIgnoreMethod; - private Bom bom; + private Logger log = LoggersFactory.getLogger(this.getClass().getName()); + private static final Version VERSION = Version.VERSION_14; + private String exhortIgnoreMethod; + private Bom bom; private PackageURL root; - private BiPredicate belongingCriteriaBinaryAlgorithm; + private BiPredicate belongingCriteriaBinaryAlgorithm; - private Predicate genericComparator(BiPredicate binaryBelongingCriteriaAlgorithm, X container) - { - return dep -> binaryBelongingCriteriaAlgorithm.test(container, dep); + private Predicate genericComparator(BiPredicate binaryBelongingCriteriaAlgorithm, X container) { + return dep -> binaryBelongingCriteriaAlgorithm.test(container, dep); } public CycloneDXSbom() { @@ -60,35 +57,32 @@ public CycloneDXSbom() { bom.setDependencies(new ArrayList<>()); belongingCriteriaBinaryAlgorithm = getBelongingConditionByName(); this.exhortIgnoreMethod = "insensitive"; - } - private static BiPredicate getBelongingConditionByName() { - return (collection, component) -> collection.contains(component.getName()); - } + private static BiPredicate getBelongingConditionByName() { + return (collection, component) -> collection.contains(component.getName()); + } - public CycloneDXSbom(BelongingCondition belongingCondition,String exhortIgnoreMethod) { - this(); - if(belongingCondition.equals(BelongingCondition.NAME)) - { - belongingCriteriaBinaryAlgorithm = getBelongingConditionByName(); - } - else if (belongingCondition.equals(BelongingCondition.PURL)){ - belongingCriteriaBinaryAlgorithm = getBelongingConditionByPurl(); - } - else - { - // fallback to belonging condition by name ( default) - this one in case the enum type will be extended and new BelongingType won't be implemented right away. - belongingCriteriaBinaryAlgorithm = getBelongingConditionByName(); - } - this.exhortIgnoreMethod = exhortIgnoreMethod; + public CycloneDXSbom(BelongingCondition belongingCondition, String exhortIgnoreMethod) { + this(); + if (belongingCondition.equals(BelongingCondition.NAME)) { + belongingCriteriaBinaryAlgorithm = getBelongingConditionByName(); + } else if (belongingCondition.equals(BelongingCondition.PURL)) { + belongingCriteriaBinaryAlgorithm = getBelongingConditionByPurl(); + } else { + // fallback to belonging condition by name ( default) - this one in case the enum type will be extended and + // new BelongingType won't be implemented right away. + belongingCriteriaBinaryAlgorithm = getBelongingConditionByName(); + } + this.exhortIgnoreMethod = exhortIgnoreMethod; } - private BiPredicate getBelongingConditionByPurl() { - return (collection, component) -> collection.contains(componentToPurl(component).getCoordinates()); - } + private BiPredicate getBelongingConditionByPurl() { + return (collection, component) -> + collection.contains(componentToPurl(component).getCoordinates()); + } - public Sbom addRoot(PackageURL rootRef) { + public Sbom addRoot(PackageURL rootRef) { this.root = rootRef; Component rootComponent = newRootComponent(rootRef); bom.getMetadata().setComponent(rootComponent); @@ -101,30 +95,30 @@ public PackageURL getRoot() { return root; } - @Override - public Sbom filterIgnoredDeps(Collection ignoredDeps) { - String exhortIgnoreMethod = Objects.requireNonNullElse(getExhortIgnoreMethod(),this.exhortIgnoreMethod ); - if(exhortIgnoreMethod.equals("insensitive")) - { - return filterIgnoredDepsInsensitive(ignoredDeps); - } - else { - return filterIgnoredDepsSensitive(ignoredDeps); + @Override + public Sbom filterIgnoredDeps(Collection ignoredDeps) { + String exhortIgnoreMethod = Objects.requireNonNullElse(getExhortIgnoreMethod(), this.exhortIgnoreMethod); + if (exhortIgnoreMethod.equals("insensitive")) { + return filterIgnoredDepsInsensitive(ignoredDeps); + } else { + return filterIgnoredDepsSensitive(ignoredDeps); + } } + private String getExhortIgnoreMethod() { + boolean result; + return System.getenv("EXHORT_IGNORE_METHOD") != null + ? System.getenv("EXHORT_IGNORE_METHOD").trim().toLowerCase() + : getExhortIgnoreProperty(); + } - } - - private String getExhortIgnoreMethod() { - boolean result; - return System.getenv("EXHORT_IGNORE_METHOD") != null ? System.getenv("EXHORT_IGNORE_METHOD").trim().toLowerCase() : getExhortIgnoreProperty(); - } - - private String getExhortIgnoreProperty() { - return System.getProperty("EXHORT_IGNORE_METHOD") != null ? System.getProperty("EXHORT_IGNORE_METHOD").trim().toLowerCase() : null ; - } + private String getExhortIgnoreProperty() { + return System.getProperty("EXHORT_IGNORE_METHOD") != null + ? System.getProperty("EXHORT_IGNORE_METHOD").trim().toLowerCase() + : null; + } - private Component newRootComponent(PackageURL ref) { + private Component newRootComponent(PackageURL ref) { Component c = new Component(); c.setBomRef(ref.getCoordinates()); c.setName(ref.getName()); @@ -146,13 +140,13 @@ private Component newComponent(PackageURL ref) { return c; } - private PackageURL componentToPurl(Component component) { - try { - return new PackageURL(component.getPurl()); - } catch (MalformedPackageURLException e) { - throw new RuntimeException(e); + private PackageURL componentToPurl(Component component) { + try { + return new PackageURL(component.getPurl()); + } catch (MalformedPackageURLException e) { + throw new RuntimeException(e); + } } - } private Dependency newDependency(PackageURL ref) { return new Dependency(ref.getCoordinates()); @@ -160,60 +154,57 @@ private Dependency newDependency(PackageURL ref) { private Sbom filterIgnoredDepsInsensitive(Collection ignoredDeps) { - List initialIgnoreRefs = bom.getComponents() - .stream() - .filter(c -> genericComparator(this.belongingCriteriaBinaryAlgorithm,ignoredDeps).test(c)) - .map(Component::getBomRef).collect(Collectors.toList()); - List refsToIgnore = createIgnoreFilter(bom.getDependencies(), - initialIgnoreRefs); - return removeIgnoredDepsFromSbom(refsToIgnore); + List initialIgnoreRefs = bom.getComponents().stream() + .filter(c -> genericComparator(this.belongingCriteriaBinaryAlgorithm, ignoredDeps) + .test(c)) + .map(Component::getBomRef) + .collect(Collectors.toList()); + List refsToIgnore = createIgnoreFilter(bom.getDependencies(), initialIgnoreRefs); + return removeIgnoredDepsFromSbom(refsToIgnore); } - private Sbom removeIgnoredDepsFromSbom(List refsToIgnore) { - bom.setComponents(bom.getComponents() - .stream() - .filter(c -> !refsToIgnore.contains(c.getBomRef())) - .collect(Collectors.toList())); - var newDeps = bom.getDependencies() - .stream() - .filter(d -> !refsToIgnore.contains(d.getRef())) - .collect(Collectors.toList()); - bom.setDependencies(newDeps); - bom.getDependencies().stream().forEach(d -> { - if (d.getDependencies() != null) { - var filteredDeps = d.getDependencies() - .stream() - .filter(td -> !refsToIgnore.contains(td.getRef())) - .collect(Collectors.toList()); - d.setDependencies(filteredDeps); - } - }); - return this; - } + private Sbom removeIgnoredDepsFromSbom(List refsToIgnore) { + bom.setComponents(bom.getComponents().stream() + .filter(c -> !refsToIgnore.contains(c.getBomRef())) + .collect(Collectors.toList())); + var newDeps = bom.getDependencies().stream() + .filter(d -> !refsToIgnore.contains(d.getRef())) + .collect(Collectors.toList()); + bom.setDependencies(newDeps); + bom.getDependencies().stream().forEach(d -> { + if (d.getDependencies() != null) { + var filteredDeps = d.getDependencies().stream() + .filter(td -> !refsToIgnore.contains(td.getRef())) + .collect(Collectors.toList()); + d.setDependencies(filteredDeps); + } + }); + return this; + } - private Sbom filterIgnoredDepsSensitive(Collection ignoredDeps) { + private Sbom filterIgnoredDepsSensitive(Collection ignoredDeps) { - List refsToIgnore = bom.getComponents() - .stream() - .filter(c -> genericComparator(this.belongingCriteriaBinaryAlgorithm,ignoredDeps).test(c)) - .map(Component::getBomRef).collect(Collectors.toList()); - return removeIgnoredDepsFromSbom(refsToIgnore); - } + List refsToIgnore = bom.getComponents().stream() + .filter(c -> genericComparator(this.belongingCriteriaBinaryAlgorithm, ignoredDeps) + .test(c)) + .map(Component::getBomRef) + .collect(Collectors.toList()); + return removeIgnoredDepsFromSbom(refsToIgnore); + } private List createIgnoreFilter(List deps, Collection toIgnore) { - List result = new ArrayList<>(toIgnore); - for (Dependency dep : deps) { - if (toIgnore.contains(dep.getRef()) && dep.getDependencies() != null) { - List collected = dep.getDependencies().stream().map(p -> p.getRef()).collect(Collectors.toList()); - result.addAll(collected); - if (dep.getDependencies().stream().filter(p -> p != null).count() > 0) { - result= createIgnoreFilter(dep.getDependencies(), result); - } - + List result = new ArrayList<>(toIgnore); + for (Dependency dep : deps) { + if (toIgnore.contains(dep.getRef()) && dep.getDependencies() != null) { + List collected = + dep.getDependencies().stream().map(p -> p.getRef()).collect(Collectors.toList()); + result.addAll(collected); + if (dep.getDependencies().stream().filter(p -> p != null).count() > 0) { + result = createIgnoreFilter(dep.getDependencies(), result); + } + } } - - } - return result; + return result; } @Override @@ -226,7 +217,8 @@ public Sbom addDependency(PackageURL sourceRef, PackageURL targetRef) { bom.addDependency(srcDep); } else { Optional existingDep = bom.getDependencies().stream() - .filter(d -> d.getRef().equals(srcComp.getBomRef())).findFirst(); + .filter(d -> d.getRef().equals(srcComp.getBomRef())) + .findFirst(); if (existingDep.isPresent()) { srcDep = existingDep.get(); } else { @@ -247,54 +239,53 @@ public Sbom addDependency(PackageURL sourceRef, PackageURL targetRef) { @Override public String getAsJsonString() { - String jsonString = BomGeneratorFactory.createJson(VERSION, bom).toJsonString(); - if(debugLoggingIsNeeded()) - { - log.info("Generated Sbom Json:" + System.lineSeparator() + jsonString); - } - return jsonString; + String jsonString = BomGeneratorFactory.createJson(VERSION, bom).toJsonString(); + if (debugLoggingIsNeeded()) { + log.info("Generated Sbom Json:" + System.lineSeparator() + jsonString); + } + return jsonString; } - @Override - public void setBelongingCriteriaBinaryAlgorithm(BelongingCondition belongingCondition) { - if(belongingCondition.equals(BelongingCondition.NAME)) - { - belongingCriteriaBinaryAlgorithm = getBelongingConditionByName(); - } - else if (belongingCondition.equals(BelongingCondition.PURL)){ - belongingCriteriaBinaryAlgorithm = getBelongingConditionByPurl(); + @Override + public void setBelongingCriteriaBinaryAlgorithm(BelongingCondition belongingCondition) { + if (belongingCondition.equals(BelongingCondition.NAME)) { + belongingCriteriaBinaryAlgorithm = getBelongingConditionByName(); + } else if (belongingCondition.equals(BelongingCondition.PURL)) { + belongingCriteriaBinaryAlgorithm = getBelongingConditionByPurl(); + } } - } + @Override + public boolean checkIfPackageInsideDependsOnList(PackageURL component, String name) { + boolean result = false; + Optional comp = this.bom.getDependencies().stream() + .filter(c -> c.getRef().equals(component.getCoordinates())) + .findFirst(); + if (comp.isPresent()) { + Dependency targetComponent = comp.get(); + List deps = targetComponent.getDependencies(); + List allDirectDeps = deps.stream() + .map(dep -> { + try { + return new PackageURL(dep.getRef()); + } catch (MalformedPackageURLException e) { + throw new RuntimeException(e); + } + }) + .collect(Collectors.toList()); - @Override - public boolean checkIfPackageInsideDependsOnList(PackageURL component, String name) { - boolean result = false; - Optional comp = this.bom.getDependencies().stream().filter(c -> c.getRef().equals(component.getCoordinates())).findFirst(); - if(comp.isPresent()) - { - Dependency targetComponent = comp.get(); - List deps = targetComponent.getDependencies(); - List allDirectDeps = deps.stream().map(dep -> { - try { - return new PackageURL(dep.getRef()); - } catch (MalformedPackageURLException e) { - throw new RuntimeException(e); + result = allDirectDeps.stream() + .filter(dep -> dep.getName().equals(name)) + .count() + > 0; } - }).collect(Collectors.toList()); - - result = allDirectDeps.stream().filter(dep -> dep.getName().equals(name)).count() > 0; - + return result; } - return result; - } - - @Override - public void removeRootComponent() - { - bom.getComponents().removeIf( (component) -> component.getBomRef().equals(this.root.getCoordinates())); - bom.getDependencies().removeIf( (dependency) -> dependency.getRef().equals(this.root.getCoordinates())); - bom.getMetadata().setComponent(null); - } + @Override + public void removeRootComponent() { + bom.getComponents().removeIf((component) -> component.getBomRef().equals(this.root.getCoordinates())); + bom.getDependencies().removeIf((dependency) -> dependency.getRef().equals(this.root.getCoordinates())); + bom.getMetadata().setComponent(null); + } } diff --git a/src/main/java/com/redhat/exhort/sbom/Sbom.java b/src/main/java/com/redhat/exhort/sbom/Sbom.java index 079d94a8..3729c8cb 100644 --- a/src/main/java/com/redhat/exhort/sbom/Sbom.java +++ b/src/main/java/com/redhat/exhort/sbom/Sbom.java @@ -15,36 +15,39 @@ */ package com.redhat.exhort.sbom; -import java.util.Collection; import com.github.packageurl.PackageURL; +import java.util.Collection; public interface Sbom { public Sbom addRoot(PackageURL root); + public PackageURL getRoot(); + public Sbom filterIgnoredDeps(Collection ignoredDeps); + public Sbom addDependency(PackageURL sourceRef, PackageURL targetRef); + public String getAsJsonString(); + public void setBelongingCriteriaBinaryAlgorithm(BelongingCondition belongingCondition); public boolean checkIfPackageInsideDependsOnList(PackageURL component, String name); void removeRootComponent(); - public enum BelongingCondition - { - NAME("name"), - PURL("purl"); + public enum BelongingCondition { + NAME("name"), + PURL("purl"); - String belongingCondition; + String belongingCondition; - BelongingCondition(String belongingCondition) { - this.belongingCondition = belongingCondition; - } + BelongingCondition(String belongingCondition) { + this.belongingCondition = belongingCondition; + } - public String getBelongingCondition() { - return belongingCondition; - } + public String getBelongingCondition() { + return belongingCondition; + } } - } diff --git a/src/main/java/com/redhat/exhort/sbom/SbomFactory.java b/src/main/java/com/redhat/exhort/sbom/SbomFactory.java index 820a6043..2157df54 100644 --- a/src/main/java/com/redhat/exhort/sbom/SbomFactory.java +++ b/src/main/java/com/redhat/exhort/sbom/SbomFactory.java @@ -13,7 +13,6 @@ * See the License for the specific language governing permissions and * limitations under the License. */ - package com.redhat.exhort.sbom; public class SbomFactory { @@ -21,8 +20,8 @@ public class SbomFactory { public static Sbom newInstance() { return new CycloneDXSbom(); } - public static Sbom newInstance(Sbom.BelongingCondition belongingCondition,String exhortIgnoreMethod) { - return new CycloneDXSbom(belongingCondition,exhortIgnoreMethod); - } + public static Sbom newInstance(Sbom.BelongingCondition belongingCondition, String exhortIgnoreMethod) { + return new CycloneDXSbom(belongingCondition, exhortIgnoreMethod); + } } diff --git a/src/main/java/com/redhat/exhort/tools/Ecosystem.java b/src/main/java/com/redhat/exhort/tools/Ecosystem.java index 782d2332..ba3b3eaa 100644 --- a/src/main/java/com/redhat/exhort/tools/Ecosystem.java +++ b/src/main/java/com/redhat/exhort/tools/Ecosystem.java @@ -15,74 +15,70 @@ */ package com.redhat.exhort.tools; -import java.nio.file.Path; - - import com.redhat.exhort.Provider; import com.redhat.exhort.providers.GoModulesProvider; +import com.redhat.exhort.providers.GradleProvider; import com.redhat.exhort.providers.JavaMavenProvider; import com.redhat.exhort.providers.JavaScriptNpmProvider; import com.redhat.exhort.providers.PythonPipProvider; -import com.redhat.exhort.providers.GradleProvider; +import java.nio.file.Path; /** Utility class used for instantiating providers. **/ public final class Ecosystem { - public enum Type { + public enum Type { + MAVEN("maven"), + NPM("npm"), + GOLANG("golang"), + PYTHON("pypi"), + GRADLE("gradle"); - MAVEN ("maven"), - NPM ("npm"), - GOLANG ("golang"), - PYTHON ("pypi"), - GRADLE ("gradle"); + String type; - String type; + public String getType() { + return type; + } - public String getType() { - return type; + Type(String type) { + this.type = type; + } } - Type(String type) { - this.type = type; + private Ecosystem() { + // constructor not required for a utility class } - } - private Ecosystem(){ - // constructor not required for a utility class - } - - /** - * Utility function for instantiating {@link Provider} implementations. - * - * @param manifestPath the manifest Path - * @return a Manifest record - */ - public static Provider getProvider(final Path manifestPath) { - return Ecosystem.getProvider(manifestPath.getFileName().toString()); - } + /** + * Utility function for instantiating {@link Provider} implementations. + * + * @param manifestPath the manifest Path + * @return a Manifest record + */ + public static Provider getProvider(final Path manifestPath) { + return Ecosystem.getProvider(manifestPath.getFileName().toString()); + } - /** - * Utility function for instantiating {@link Provider} implementations. - * - * @param manifestType the type (filename + type) of the manifest - * @return a Manifest record - */ - public static Provider getProvider(final String manifestType) { - switch (manifestType) { - case "pom.xml": - return new JavaMavenProvider(); - case "package.json": - return new JavaScriptNpmProvider(); - case "go.mod": - return new GoModulesProvider(); - case "requirements.txt": - return new PythonPipProvider(); - case "build.gradle": - return new GradleProvider(); + /** + * Utility function for instantiating {@link Provider} implementations. + * + * @param manifestType the type (filename + type) of the manifest + * @return a Manifest record + */ + public static Provider getProvider(final String manifestType) { + switch (manifestType) { + case "pom.xml": + return new JavaMavenProvider(); + case "package.json": + return new JavaScriptNpmProvider(); + case "go.mod": + return new GoModulesProvider(); + case "requirements.txt": + return new PythonPipProvider(); + case "build.gradle": + return new GradleProvider(); - default: - throw new IllegalStateException(String.format("Unknown manifest file %s", manifestType) - ); + default: + throw new IllegalStateException(String.format("Unknown manifest file %s", manifestType)); + } } - } } diff --git a/src/main/java/com/redhat/exhort/tools/Operations.java b/src/main/java/com/redhat/exhort/tools/Operations.java index bfd948da..fa82a6f0 100644 --- a/src/main/java/com/redhat/exhort/tools/Operations.java +++ b/src/main/java/com/redhat/exhort/tools/Operations.java @@ -15,6 +15,8 @@ */ package com.redhat.exhort.tools; +import static java.lang.String.join; + import java.io.BufferedReader; import java.io.IOException; import java.io.InputStream; @@ -25,230 +27,199 @@ import java.util.concurrent.TimeUnit; import java.util.stream.Collectors; -import static java.lang.String.join; - /** Utility class used for executing process on the operating system. **/ public final class Operations { - private Operations(){ - // constructor not required for a utility class - } - - /** - * Function for looking up custom executable path based on the default one provides as an - * argument. I.e. if defaultExecutable=mvn, this function will look for a custom mvn path - * set as an environment variable or a java property with the name EXHORT_MVN_PATH. If not found, - * the original mvn passed as defaultExecutable will be returned. - * Note, environment variables takes precedence on java properties. - * - * @param defaultExecutable default executable (uppercase spaces and dashes will be replaced with underscores). - * @return the custom path from the relevant environment variable or the original argument. - */ - public static String getCustomPathOrElse(String defaultExecutable) { - var target = defaultExecutable.toUpperCase() - .replaceAll(" ", "_") - .replaceAll("-", "_"); - var executableKey = String.format("EXHORT_%s_PATH", target); - return Objects.requireNonNullElseGet( - System.getenv(executableKey), - () -> Objects.requireNonNullElse(System.getProperty(executableKey) ,defaultExecutable)); - } - - /** - * Function for building a command from the command parts list and execute it as a process on - * the operating system. Will throw a RuntimeException if the command build or execution failed. - * - * @param cmdList list of command parts - */ - public static void runProcess(final String... cmdList) { - runProcess(cmdList, null); - } - - public static void runProcess(final String[] cmdList, final Map envMap) { - var processBuilder = new ProcessBuilder(); - processBuilder.command(cmdList); - if (envMap != null) { - processBuilder.environment().putAll(envMap); - } - // create a process builder or throw a runtime exception - Process process = null; - try { - process = processBuilder.start(); - } catch (final IOException e) { - throw new RuntimeException( - String.format( - "failed to build process for '%s' got %s", - join(" ", cmdList), - e.getMessage() - ) - ); + private Operations() { + // constructor not required for a utility class } - // execute the command or throw runtime exception if failed - int exitCode = 0; - try { - exitCode = process.waitFor(); - - } catch (final InterruptedException e) { - throw new RuntimeException( - String.format( - "built process for '%s' interrupted, got %s", - join(" ", cmdList), - e.getMessage() - ) - ); + /** + * Function for looking up custom executable path based on the default one provides as an + * argument. I.e. if defaultExecutable=mvn, this function will look for a custom mvn path + * set as an environment variable or a java property with the name EXHORT_MVN_PATH. If not found, + * the original mvn passed as defaultExecutable will be returned. + * Note, environment variables takes precedence on java properties. + * + * @param defaultExecutable default executable (uppercase spaces and dashes will be replaced with underscores). + * @return the custom path from the relevant environment variable or the original argument. + */ + public static String getCustomPathOrElse(String defaultExecutable) { + var target = defaultExecutable.toUpperCase().replaceAll(" ", "_").replaceAll("-", "_"); + var executableKey = String.format("EXHORT_%s_PATH", target); + return Objects.requireNonNullElseGet( + System.getenv(executableKey), + () -> Objects.requireNonNullElse(System.getProperty(executableKey), defaultExecutable)); } - // verify the command was executed successfully or throw a runtime exception - if (exitCode != 0) { - String errMsg = new BufferedReader(new InputStreamReader(process.getErrorStream())) - .lines().collect(Collectors.joining(System.lineSeparator())); - if (errMsg.isEmpty()) { - errMsg = new BufferedReader(new InputStreamReader(process.getInputStream())) - .lines().collect(Collectors.joining(System.lineSeparator())); - } - if (errMsg.isEmpty()) { - throw new RuntimeException( - String.format( - "failed to execute '%s', exit-code %d", - join(" ", cmdList), - exitCode - ) - ); - } else { - throw new RuntimeException( - String.format( - "failed to execute '%s', exit-code %d, message:%s%s%s", - join(" ", cmdList), - exitCode, - System.lineSeparator(), - errMsg, - System.lineSeparator() - ) - ); - } + + /** + * Function for building a command from the command parts list and execute it as a process on + * the operating system. Will throw a RuntimeException if the command build or execution failed. + * + * @param cmdList list of command parts + */ + public static void runProcess(final String... cmdList) { + runProcess(cmdList, null); } - } - - public static String runProcessGetOutput(Path dir, final String... cmdList) { - return runProcessGetOutput(dir, cmdList, null); - } - - public static String runProcessGetOutput(Path dir, final String[] cmdList, String[] envList) { - StringBuilder sb = new StringBuilder(); - try { - Process process; - InputStream inputStream; - if(dir == null) { - if (envList != null) { - process = Runtime.getRuntime().exec(join(" ", cmdList), envList); - } else { - process = Runtime.getRuntime().exec(join(" ", cmdList)); + + public static void runProcess(final String[] cmdList, final Map envMap) { + var processBuilder = new ProcessBuilder(); + processBuilder.command(cmdList); + if (envMap != null) { + processBuilder.environment().putAll(envMap); } - } - else - { - if (envList != null) { - process = Runtime.getRuntime().exec(join(" ", cmdList), envList, dir.toFile()); - } else { - process = Runtime.getRuntime().exec(join(" ", cmdList), null, dir.toFile()); + // create a process builder or throw a runtime exception + Process process = null; + try { + process = processBuilder.start(); + } catch (final IOException e) { + throw new RuntimeException( + String.format("failed to build process for '%s' got %s", join(" ", cmdList), e.getMessage())); } - } - - inputStream = process.getInputStream(); + // execute the command or throw runtime exception if failed + int exitCode = 0; + try { + exitCode = process.waitFor(); - BufferedReader reader = new BufferedReader( - new InputStreamReader(inputStream)); - String line; - while((line = reader.readLine()) != null) - { - sb.append(line); - if (!line.endsWith(System.lineSeparator())) - { - sb.append("\n"); + } catch (final InterruptedException e) { + throw new RuntimeException( + String.format("built process for '%s' interrupted, got %s", join(" ", cmdList), e.getMessage())); } - } - if(sb.toString().trim().equals("")) { - inputStream = process.getErrorStream(); - reader = new BufferedReader( - new InputStreamReader(inputStream)); - while ((line = reader.readLine()) != null) { - sb.append(line); - if (!line.endsWith(System.lineSeparator())) { - sb.append("\n"); - } + // verify the command was executed successfully or throw a runtime exception + if (exitCode != 0) { + String errMsg = new BufferedReader(new InputStreamReader(process.getErrorStream())) + .lines() + .collect(Collectors.joining(System.lineSeparator())); + if (errMsg.isEmpty()) { + errMsg = new BufferedReader(new InputStreamReader(process.getInputStream())) + .lines() + .collect(Collectors.joining(System.lineSeparator())); + } + if (errMsg.isEmpty()) { + throw new RuntimeException( + String.format("failed to execute '%s', exit-code %d", join(" ", cmdList), exitCode)); + } else { + throw new RuntimeException(String.format( + "failed to execute '%s', exit-code %d, message:%s%s%s", + join(" ", cmdList), exitCode, System.lineSeparator(), errMsg, System.lineSeparator())); + } } - } - } catch (IOException e) { - throw new RuntimeException(String.format("Failed to execute command '%s' ", join(" ",cmdList)),e); } - return sb.toString(); - } - - public static ProcessExecOutput runProcessGetFullOutput(Path dir, final String[] cmdList, String[] envList) { - try { - Process process; - if (dir == null) { - if (envList != null) { - process = Runtime.getRuntime().exec(join(" ", cmdList), envList); - } else { - process = Runtime.getRuntime().exec(join(" ", cmdList)); - } - } else { - if (envList != null) { - process = Runtime.getRuntime().exec(join(" ", cmdList), envList, dir.toFile()); - } else { - process = Runtime.getRuntime().exec(join(" ", cmdList), null, dir.toFile()); - } - } - - BufferedReader reader = new BufferedReader(new InputStreamReader(process.getInputStream())); - StringBuilder output = new StringBuilder(); - String line; - while ((line = reader.readLine()) != null) { - output.append(line); - if (!line.endsWith(System.lineSeparator())) { - output.append("\n"); - } - } - - reader = new BufferedReader(new InputStreamReader(process.getErrorStream())); - StringBuilder error = new StringBuilder(); - while ((line = reader.readLine()) != null) { - error.append(line); - if (!line.endsWith(System.lineSeparator())) { - error.append("\n"); - } - } - process.waitFor(30L, TimeUnit.SECONDS); - - return new ProcessExecOutput(output.toString(), error.toString(), process.exitValue()); - } catch (IOException | InterruptedException e) { - throw new RuntimeException(String.format("Failed to execute command '%s' ", join(" ",cmdList)),e); + public static String runProcessGetOutput(Path dir, final String... cmdList) { + return runProcessGetOutput(dir, cmdList, null); } - } - - public static class ProcessExecOutput { - private String output; - private String error; - private int exitCode; - public ProcessExecOutput(String output, String error, int exitCode) { - this.output = output; - this.error = error; - this.exitCode = exitCode; + public static String runProcessGetOutput(Path dir, final String[] cmdList, String[] envList) { + StringBuilder sb = new StringBuilder(); + try { + Process process; + InputStream inputStream; + if (dir == null) { + if (envList != null) { + process = Runtime.getRuntime().exec(join(" ", cmdList), envList); + } else { + process = Runtime.getRuntime().exec(join(" ", cmdList)); + } + } else { + if (envList != null) { + process = Runtime.getRuntime().exec(join(" ", cmdList), envList, dir.toFile()); + } else { + process = Runtime.getRuntime().exec(join(" ", cmdList), null, dir.toFile()); + } + } + + inputStream = process.getInputStream(); + + BufferedReader reader = new BufferedReader(new InputStreamReader(inputStream)); + String line; + while ((line = reader.readLine()) != null) { + sb.append(line); + if (!line.endsWith(System.lineSeparator())) { + sb.append("\n"); + } + } + if (sb.toString().trim().equals("")) { + inputStream = process.getErrorStream(); + reader = new BufferedReader(new InputStreamReader(inputStream)); + while ((line = reader.readLine()) != null) { + sb.append(line); + if (!line.endsWith(System.lineSeparator())) { + sb.append("\n"); + } + } + } + } catch (IOException e) { + throw new RuntimeException(String.format("Failed to execute command '%s' ", join(" ", cmdList)), e); + } + return sb.toString(); } - public String getOutput() { - return output; + public static ProcessExecOutput runProcessGetFullOutput(Path dir, final String[] cmdList, String[] envList) { + try { + Process process; + if (dir == null) { + if (envList != null) { + process = Runtime.getRuntime().exec(join(" ", cmdList), envList); + } else { + process = Runtime.getRuntime().exec(join(" ", cmdList)); + } + } else { + if (envList != null) { + process = Runtime.getRuntime().exec(join(" ", cmdList), envList, dir.toFile()); + } else { + process = Runtime.getRuntime().exec(join(" ", cmdList), null, dir.toFile()); + } + } + + BufferedReader reader = new BufferedReader(new InputStreamReader(process.getInputStream())); + StringBuilder output = new StringBuilder(); + String line; + while ((line = reader.readLine()) != null) { + output.append(line); + if (!line.endsWith(System.lineSeparator())) { + output.append("\n"); + } + } + + reader = new BufferedReader(new InputStreamReader(process.getErrorStream())); + StringBuilder error = new StringBuilder(); + while ((line = reader.readLine()) != null) { + error.append(line); + if (!line.endsWith(System.lineSeparator())) { + error.append("\n"); + } + } + + process.waitFor(30L, TimeUnit.SECONDS); + + return new ProcessExecOutput(output.toString(), error.toString(), process.exitValue()); + } catch (IOException | InterruptedException e) { + throw new RuntimeException(String.format("Failed to execute command '%s' ", join(" ", cmdList)), e); + } } - public String getError() { - return error; - } + public static class ProcessExecOutput { + private String output; + private String error; + private int exitCode; + + public ProcessExecOutput(String output, String error, int exitCode) { + this.output = output; + this.error = error; + this.exitCode = exitCode; + } + + public String getOutput() { + return output; + } - public int getExitCode() { - return exitCode; + public String getError() { + return error; + } + + public int getExitCode() { + return exitCode; + } } - } } diff --git a/src/main/java/com/redhat/exhort/tools/package-info.java b/src/main/java/com/redhat/exhort/tools/package-info.java index 5e5ec006..5166212d 100644 --- a/src/main/java/com/redhat/exhort/tools/package-info.java +++ b/src/main/java/com/redhat/exhort/tools/package-info.java @@ -1,2 +1,2 @@ /** Package hosting various utility and tools used throughout the project. **/ -package com.redhat.exhort.tools; \ No newline at end of file +package com.redhat.exhort.tools; diff --git a/src/main/java/com/redhat/exhort/utils/PythonControllerBase.java b/src/main/java/com/redhat/exhort/utils/PythonControllerBase.java index 796a905e..971fe9a0 100644 --- a/src/main/java/com/redhat/exhort/utils/PythonControllerBase.java +++ b/src/main/java/com/redhat/exhort/utils/PythonControllerBase.java @@ -15,12 +15,13 @@ */ package com.redhat.exhort.utils; +import static com.redhat.exhort.impl.ExhortApi.*; + import com.fasterxml.jackson.core.JsonProcessingException; import com.fasterxml.jackson.databind.ObjectMapper; import com.redhat.exhort.exception.PackageNotInstalledException; import com.redhat.exhort.logging.LoggersFactory; import com.redhat.exhort.tools.Operations; - import java.io.IOException; import java.nio.file.Files; import java.nio.file.Path; @@ -31,397 +32,416 @@ import java.util.stream.Collectors; import java.util.stream.Stream; -import static com.redhat.exhort.impl.ExhortApi.*; -import static java.lang.String.join; - public abstract class PythonControllerBase { - public static void main(String[] args) { - - PythonControllerBase pythonController; -// pythonController = new PythonControllerVirtualEnv("/usr/bin/python3"); - LocalDateTime start = LocalDateTime.now(); - List> dependencies; -// dependencies = pythonController.getDependencies("/tmp/requirements.txt",true); - LocalDateTime end = LocalDateTime.now(); - System.out.println("start time:" + start + "\n"); - System.out.println("end time:" + end + "\n"); - System.out.println("elapsed time: " + start.until(end, ChronoUnit.SECONDS) + "\n" ); - pythonController = new PythonControllerRealEnv("/usr/bin/python3","/usr/bin/pip3"); - start = LocalDateTime.now(); - try { - dependencies = pythonController.getDependencies("/home/zgrinber/git/exhort-java-api/src/test/resources/tst_manifests/pip/pip_requirements_txt_ignore/requirements.txt",true); - } catch (PackageNotInstalledException e) { - System.out.println(e.getMessage()); - dependencies =null; - } - end = LocalDateTime.now(); -// LocalDateTime startNaive = LocalDateTime.now(); -// List> dependenciesNaive = pythonController.getDependenciesNaive(); -// LocalDateTime endNaive = LocalDateTime.now(); - System.out.println("start time:" + start + "\n"); - System.out.println("end time:" + end + "\n"); - System.out.println("elapsed time: " + start.until(end, ChronoUnit.SECONDS) + "\n" ); -// System.out.println("naive start time:" + startNaive + "\n" ); -// System.out.println("naive end time:" + endNaive + "\n"); -// System.out.println("elapsed time: " + startNaive.until(endNaive, ChronoUnit.SECONDS)); - - ObjectMapper om = new ObjectMapper(); - try { - String json = om.writerWithDefaultPrettyPrinter().writeValueAsString(dependencies); - System.out.println(json); -// System.out.println(pythonController.counter); - - } catch (JsonProcessingException e) { - throw new RuntimeException(e); - } - } - - - private Logger log = LoggersFactory.getLogger(this.getClass().getName()); - protected Path pythonEnvironmentDir; - protected Path pipBinaryDir; - - protected String pathToPythonBin; - - protected String pipBinaryLocation; - -// public int counter =0; - - public abstract void prepareEnvironment(String pathToPythonBin); - - public abstract boolean automaticallyInstallPackageOnEnvironment(); - - public abstract boolean isRealEnv(); - - void installPackages(String pathToRequirements) - { - Operations.runProcess(pipBinaryLocation, "install", "-r", pathToRequirements); - Operations.runProcess(pipBinaryLocation, "freeze"); - - } - - public abstract boolean isVirtualEnv(); - - public abstract void cleanEnvironment(boolean deleteEnvironment); - - -// public List> getDependenciesNaive() -// { -// List> dependencies = new ArrayList<>(); -// String freeze = Operations.runProcessGetOutput(pythonEnvironmentDir, pipBinaryLocation, "freeze"); -// String[] deps = freeze.split(System.lineSeparator()); -// Arrays.stream(deps).forEach(dep -> -// { -// Map component = new HashMap<>(); -// dependencies.add(component); -// bringAllDependenciesNaive(component, getDependencyName(dep)); -// }); -// -// -// -// return dependencies; -// } -// -// private void bringAllDependenciesNaive(Map dependencies, String depName) { -// -// if(dependencies == null || depName.trim().equals("")) -// return; -// counter++; -// LocalDateTime start = LocalDateTime.now(); -// String pipShowOutput = Operations.runProcessGetOutput(pythonEnvironmentDir, pipBinaryLocation, "show", depName); -// LocalDateTime end = LocalDateTime.now(); -// System.out.println("pip show start time:" + start + "\n"); -// System.out.println("pip show end time:" + end + "\n"); -// System.out.println("pip show elapsed time: " + start.until(end, ChronoUnit.SECONDS) + "\n" ); -// String depVersion = getDependencyVersion(pipShowOutput); -// List directDeps = getDepsList(pipShowOutput); -// dependencies.put("name", depName); -// dependencies.put("version",depVersion); -// List> targetDeps = new ArrayList<>(); -// directDeps.stream().forEach(d -> { -// Map myMap = new HashMap<>(); -// targetDeps.add(myMap); -// bringAllDependenciesNaive(myMap,d); -// }); -// dependencies.put("dependencies",targetDeps); -// -// } -// public List> getDependencies() -// { -// List> dependencies = new ArrayList<>(); -// String freeze = Operations.runProcessGetOutput(pythonEnvironmentDir, pipBinaryLocation, "freeze"); -// String[] deps = freeze.split(System.lineSeparator()); -// String depNames = Arrays.stream(deps).map(this::getDependencyName).collect(Collectors.joining(" ")); -// bringAllDependencies(dependencies, depNames); -// -// -// -// -// return dependencies; -// } -// -// private void bringAllDependencies(List> dependencies, String depName) { -// -// if (dependencies == null || depName.trim().equals("")) -// return; -// counter++; -// LocalDateTime start = LocalDateTime.now(); -// String pipShowOutput = Operations.runProcessGetOutput(pythonEnvironmentDir, pipBinaryLocation, "show", depName); -// LocalDateTime end = LocalDateTime.now(); -// System.out.println("pip show start time:" + start + "\n"); -// System.out.println("pip show end time:" + end + "\n"); -// System.out.println("pip show elapsed time: " + start.until(end, ChronoUnit.MILLIS) + "\n" ); -// List allLines = Arrays.stream(pipShowOutput.split("---")).collect(Collectors.toList()); -// allLines.stream().forEach(record -> { -// String depVersion = getDependencyVersion(record); -// List directDeps = getDepsList(record); -// getDependencyNameShow(record); -// Map entry = new HashMap(); -// dependencies.add(entry); -// entry.put("name", getDependencyNameShow(record)); -// entry.put("version", depVersion); -// List> targetDeps = new ArrayList<>(); -// String depsList = directDeps.stream().map(str -> str.replace(",", "")).collect(Collectors.joining(" ")); -// bringAllDependencies(targetDeps, depsList); -// entry.put("dependencies",targetDeps); -// }); -// } - - public final List> getDependencies(String pathToRequirements, boolean includeTransitive) { - if(isVirtualEnv() || isRealEnv() ) { - prepareEnvironment(pathToPythonBin); - } - if(automaticallyInstallPackageOnEnvironment()) - { - boolean installBestEfforts = getBooleanValueEnvironment("EXHORT_PYTHON_INSTALL_BEST_EFFORTS", "false"); - // make best efforts to install the requirements.txt on the virtual environment created from the python3 passed in. - // that means that it will install the packages without referring to the versions, but will let pip choose the version - // tailored for version of the python environment( and of pip package manager) for each package. - if(installBestEfforts) - { - boolean matchManifestVersions = getBooleanValueEnvironment("MATCH_MANIFEST_VERSIONS", "true"); - if(matchManifestVersions) - { - throw new RuntimeException("Conflicting settings, EXHORT_PYTHON_INSTALL_BEST_EFFORTS=true can only work with MATCH_MANIFEST_VERSIONS=false"); + public static void main(String[] args) { + + PythonControllerBase pythonController; + // pythonController = new PythonControllerVirtualEnv("/usr/bin/python3"); + LocalDateTime start = LocalDateTime.now(); + List> dependencies; + // dependencies = pythonController.getDependencies("/tmp/requirements.txt",true); + LocalDateTime end = LocalDateTime.now(); + System.out.println("start time:" + start + "\n"); + System.out.println("end time:" + end + "\n"); + System.out.println("elapsed time: " + start.until(end, ChronoUnit.SECONDS) + "\n"); + pythonController = new PythonControllerRealEnv("/usr/bin/python3", "/usr/bin/pip3"); + start = LocalDateTime.now(); + try { + dependencies = pythonController.getDependencies( + "/home/zgrinber/git/exhort-java-api/src/test/resources/tst_manifests/pip/pip_requirements_txt_ignore/requirements.txt", + true); + } catch (PackageNotInstalledException e) { + System.out.println(e.getMessage()); + dependencies = null; } - else { - installingRequirementsOneByOne(pathToRequirements); + end = LocalDateTime.now(); + // LocalDateTime startNaive = LocalDateTime.now(); + // List> dependenciesNaive = pythonController.getDependenciesNaive(); + // LocalDateTime endNaive = LocalDateTime.now(); + System.out.println("start time:" + start + "\n"); + System.out.println("end time:" + end + "\n"); + System.out.println("elapsed time: " + start.until(end, ChronoUnit.SECONDS) + "\n"); + // System.out.println("naive start time:" + startNaive + "\n" ); + // System.out.println("naive end time:" + endNaive + "\n"); + // System.out.println("elapsed time: " + startNaive.until(endNaive, ChronoUnit.SECONDS)); + + ObjectMapper om = new ObjectMapper(); + try { + String json = om.writerWithDefaultPrettyPrinter().writeValueAsString(dependencies); + System.out.println(json); + // System.out.println(pythonController.counter); + + } catch (JsonProcessingException e) { + throw new RuntimeException(e); } - } - // - else { - installPackages(pathToRequirements); - } } - List> dependencies = getDependenciesImpl(pathToRequirements, includeTransitive); - if(isVirtualEnv()) - { - cleanEnvironment(false); + + private Logger log = LoggersFactory.getLogger(this.getClass().getName()); + protected Path pythonEnvironmentDir; + protected Path pipBinaryDir; + + protected String pathToPythonBin; + + protected String pipBinaryLocation; + + // public int counter =0; + + public abstract void prepareEnvironment(String pathToPythonBin); + + public abstract boolean automaticallyInstallPackageOnEnvironment(); + + public abstract boolean isRealEnv(); + + void installPackages(String pathToRequirements) { + Operations.runProcess(pipBinaryLocation, "install", "-r", pathToRequirements); + Operations.runProcess(pipBinaryLocation, "freeze"); } - return dependencies; - } + public abstract boolean isVirtualEnv(); + + public abstract void cleanEnvironment(boolean deleteEnvironment); + + // public List> getDependenciesNaive() + // { + // List> dependencies = new ArrayList<>(); + // String freeze = Operations.runProcessGetOutput(pythonEnvironmentDir, pipBinaryLocation, "freeze"); + // String[] deps = freeze.split(System.lineSeparator()); + // Arrays.stream(deps).forEach(dep -> + // { + // Map component = new HashMap<>(); + // dependencies.add(component); + // bringAllDependenciesNaive(component, getDependencyName(dep)); + // }); + // + // + // + // return dependencies; + // } + // + // private void bringAllDependenciesNaive(Map dependencies, String depName) { + // + // if(dependencies == null || depName.trim().equals("")) + // return; + // counter++; + // LocalDateTime start = LocalDateTime.now(); + // String pipShowOutput = Operations.runProcessGetOutput(pythonEnvironmentDir, pipBinaryLocation, "show", + // depName); + // LocalDateTime end = LocalDateTime.now(); + // System.out.println("pip show start time:" + start + "\n"); + // System.out.println("pip show end time:" + end + "\n"); + // System.out.println("pip show elapsed time: " + start.until(end, ChronoUnit.SECONDS) + "\n" ); + // String depVersion = getDependencyVersion(pipShowOutput); + // List directDeps = getDepsList(pipShowOutput); + // dependencies.put("name", depName); + // dependencies.put("version",depVersion); + // List> targetDeps = new ArrayList<>(); + // directDeps.stream().forEach(d -> { + // Map myMap = new HashMap<>(); + // targetDeps.add(myMap); + // bringAllDependenciesNaive(myMap,d); + // }); + // dependencies.put("dependencies",targetDeps); + // + // } + // public List> getDependencies() + // { + // List> dependencies = new ArrayList<>(); + // String freeze = Operations.runProcessGetOutput(pythonEnvironmentDir, pipBinaryLocation, "freeze"); + // String[] deps = freeze.split(System.lineSeparator()); + // String depNames = Arrays.stream(deps).map(this::getDependencyName).collect(Collectors.joining(" ")); + // bringAllDependencies(dependencies, depNames); + // + // + // + // + // return dependencies; + // } + // + // private void bringAllDependencies(List> dependencies, String depName) { + // + // if (dependencies == null || depName.trim().equals("")) + // return; + // counter++; + // LocalDateTime start = LocalDateTime.now(); + // String pipShowOutput = Operations.runProcessGetOutput(pythonEnvironmentDir, pipBinaryLocation, "show", + // depName); + // LocalDateTime end = LocalDateTime.now(); + // System.out.println("pip show start time:" + start + "\n"); + // System.out.println("pip show end time:" + end + "\n"); + // System.out.println("pip show elapsed time: " + start.until(end, ChronoUnit.MILLIS) + "\n" ); + // List allLines = Arrays.stream(pipShowOutput.split("---")).collect(Collectors.toList()); + // allLines.stream().forEach(record -> { + // String depVersion = getDependencyVersion(record); + // List directDeps = getDepsList(record); + // getDependencyNameShow(record); + // Map entry = new HashMap(); + // dependencies.add(entry); + // entry.put("name", getDependencyNameShow(record)); + // entry.put("version", depVersion); + // List> targetDeps = new ArrayList<>(); + // String depsList = directDeps.stream().map(str -> str.replace(",", "")).collect(Collectors.joining(" ")); + // bringAllDependencies(targetDeps, depsList); + // entry.put("dependencies",targetDeps); + // }); + // } + + public final List> getDependencies(String pathToRequirements, boolean includeTransitive) { + if (isVirtualEnv() || isRealEnv()) { + prepareEnvironment(pathToPythonBin); + } + if (automaticallyInstallPackageOnEnvironment()) { + boolean installBestEfforts = getBooleanValueEnvironment("EXHORT_PYTHON_INSTALL_BEST_EFFORTS", "false"); + // make best efforts to install the requirements.txt on the virtual environment created from the python3 + // passed in. + // that means that it will install the packages without referring to the versions, but will let pip choose + // the version + // tailored for version of the python environment( and of pip package manager) for each package. + if (installBestEfforts) { + boolean matchManifestVersions = getBooleanValueEnvironment("MATCH_MANIFEST_VERSIONS", "true"); + if (matchManifestVersions) { + throw new RuntimeException( + "Conflicting settings, EXHORT_PYTHON_INSTALL_BEST_EFFORTS=true can only work with MATCH_MANIFEST_VERSIONS=false"); + } else { + installingRequirementsOneByOne(pathToRequirements); + } + } + // + else { + installPackages(pathToRequirements); + } + } + List> dependencies = getDependenciesImpl(pathToRequirements, includeTransitive); + if (isVirtualEnv()) { + cleanEnvironment(false); + } + + return dependencies; + } - private void installingRequirementsOneByOne(String pathToRequirements) { - try { - List requirementsRows = Files.readAllLines(Path.of(pathToRequirements)); - requirementsRows.stream().filter((line) -> !line.trim().startsWith("#")).filter((line) -> !line.trim().equals("")).forEach((dependency) -> - { - String dependencyName = getDependencyName(dependency); + private void installingRequirementsOneByOne(String pathToRequirements) { try { - Operations.runProcess(this.pipBinaryLocation,"install",dependencyName); - } catch (RuntimeException e) { - throw new RuntimeException(String.format("Best efforts process - failed installing package - %s in created virtual python environment --> error message got from underlying process => %s ",dependencyName,e.getMessage())); + List requirementsRows = Files.readAllLines(Path.of(pathToRequirements)); + requirementsRows.stream() + .filter((line) -> !line.trim().startsWith("#")) + .filter((line) -> !line.trim().equals("")) + .forEach((dependency) -> { + String dependencyName = getDependencyName(dependency); + try { + Operations.runProcess(this.pipBinaryLocation, "install", dependencyName); + } catch (RuntimeException e) { + throw new RuntimeException(String.format( + "Best efforts process - failed installing package - %s in created virtual python environment --> error message got from underlying process => %s ", + dependencyName, e.getMessage())); + } + }); + + } catch (IOException e) { + throw new RuntimeException( + "Cannot continue with analysis - error opening requirements.txt file in order to install packages one by one in a best efforts manner - related error message => " + + e.getMessage()); } - }); + } + private List> getDependenciesImpl(String pathToRequirements, boolean includeTransitive) { + List> dependencies = new ArrayList<>(); + String freeze = getPipFreezeFromEnvironment(); + String freezeMessage = ""; + if (debugLoggingIsNeeded()) { + freezeMessage = String.format( + "Package Manager PIP freeze --all command result output -> %s %s", System.lineSeparator(), freeze); + log.info(freezeMessage); + } + String[] deps = freeze.split(System.lineSeparator()); + String depNames = + Arrays.stream(deps).map(PythonControllerBase::getDependencyName).collect(Collectors.joining(" ")); + String pipShowOutput = getPipShowFromEnvironment(depNames); + if (debugLoggingIsNeeded()) { + String pipShowMessage = String.format( + "Package Manager PIP show command result output -> %s %s", System.lineSeparator(), pipShowOutput); + log.info(pipShowMessage); + } + List allPipShowLines = splitPipShowLines(pipShowOutput); + boolean matchManifestVersions = getBooleanValueEnvironment("MATCH_MANIFEST_VERSIONS", "true"); + Map CachedTree = new HashMap<>(); + List linesOfRequirements; + try { - } catch (IOException e) { - throw new RuntimeException("Cannot continue with analysis - error opening requirements.txt file in order to install packages one by one in a best efforts manner - related error message => " + e.getMessage()); - } - } - - private List> getDependenciesImpl(String pathToRequirements, boolean includeTransitive) { - List> dependencies = new ArrayList<>(); - String freeze = getPipFreezeFromEnvironment(); - String freezeMessage = ""; - if(debugLoggingIsNeeded()) { - freezeMessage = String.format("Package Manager PIP freeze --all command result output -> %s %s", System.lineSeparator(), freeze); - log.info(freezeMessage); + linesOfRequirements = Files.readAllLines(Path.of(pathToRequirements)).stream() + .filter((line) -> !line.startsWith("#")) + .map(String::trim) + .collect(Collectors.toList()); + } catch (IOException e) { + throw new RuntimeException(e); + } + allPipShowLines.stream().forEach(record -> { + String dependencyNameShow = getDependencyNameShow(record); + StringInsensitive stringInsensitive = new StringInsensitive(dependencyNameShow); + CachedTree.put(stringInsensitive, record); + CachedTree.putIfAbsent(new StringInsensitive(dependencyNameShow.replace("-", "_")), record); + CachedTree.putIfAbsent(new StringInsensitive(dependencyNameShow.replace("_", "-")), record); + }); + ObjectMapper om = new ObjectMapper(); + String tree; + try { + tree = om.writerWithDefaultPrettyPrinter().writeValueAsString(CachedTree); + } catch (JsonProcessingException e) { + throw new RuntimeException(e); + } + linesOfRequirements.stream().forEach(dep -> { + if (matchManifestVersions) { + String dependencyName; + String manifestVersion; + String installedVersion = ""; + int doubleEqualSignPosition; + if (dep.contains("==")) { + doubleEqualSignPosition = dep.indexOf("=="); + manifestVersion = dep.substring(doubleEqualSignPosition + 2).trim(); + if (manifestVersion.contains("#")) { + var hashCharIndex = manifestVersion.indexOf("#"); + manifestVersion = manifestVersion.substring(0, hashCharIndex); + } + dependencyName = getDependencyName(dep); + String pipShowRecord = CachedTree.get(new StringInsensitive(dependencyName)); + if (pipShowRecord != null) { + installedVersion = getDependencyVersion(pipShowRecord); + } + if (!installedVersion.trim().equals("")) { + if (!manifestVersion.trim().equals(installedVersion.trim())) { + throw new RuntimeException(String.format( + "Can't continue with analysis - versions mismatch for dependency name=%s, manifest version=%s, installed Version=%s, if you want to allow version mismatch for analysis between installed and requested packages, set environment variable/setting - MATCH_MANIFEST_VERSIONS=false", + dependencyName, manifestVersion, installedVersion)); + } + } + } + } + List path = new ArrayList<>(); + String depName = getDependencyName(dep.toLowerCase()); + path.add(depName); + bringAllDependencies(dependencies, depName, CachedTree, includeTransitive, path); + }); + + return dependencies; } - String[] deps = freeze.split(System.lineSeparator()); - String depNames = Arrays.stream(deps).map(PythonControllerBase::getDependencyName).collect(Collectors.joining(" ")); - String pipShowOutput = getPipShowFromEnvironment(depNames); - if(debugLoggingIsNeeded()) { - String pipShowMessage = String.format("Package Manager PIP show command result output -> %s %s", System.lineSeparator(), pipShowOutput); - log.info(pipShowMessage); + + private String getPipShowFromEnvironment(String depNames) { + return getPipCommandInvokedOrDecodedFromEnvironment("EXHORT_PIP_SHOW", pipBinaryLocation, "show", depNames); + // return Operations.runProcessGetOutput(pythonEnvironmentDir, pipBinaryLocation, "show", depNames); } - List allPipShowLines = splitPipShowLines(pipShowOutput); - boolean matchManifestVersions = getBooleanValueEnvironment("MATCH_MANIFEST_VERSIONS", "true"); - Map CachedTree = new HashMap<>(); - List linesOfRequirements; - try { - - linesOfRequirements = Files.readAllLines(Path.of(pathToRequirements)).stream().filter( (line) -> !line.startsWith("#")).map(String::trim).collect(Collectors.toList()); - } catch (IOException e) { - throw new RuntimeException(e); + + String getPipFreezeFromEnvironment() { + return getPipCommandInvokedOrDecodedFromEnvironment("EXHORT_PIP_FREEZE", pipBinaryLocation, "freeze", "--all"); } - allPipShowLines.stream().forEach(record -> { - String dependencyNameShow = getDependencyNameShow(record); - StringInsensitive stringInsensitive = new StringInsensitive(dependencyNameShow); - CachedTree.put(stringInsensitive,record); - CachedTree.putIfAbsent(new StringInsensitive(dependencyNameShow.replace("-","_")),record); - CachedTree.putIfAbsent(new StringInsensitive(dependencyNameShow.replace("_","-")),record); - }); - ObjectMapper om = new ObjectMapper(); - String tree; - try { - tree = om.writerWithDefaultPrettyPrinter().writeValueAsString(CachedTree); - } catch (JsonProcessingException e) { - throw new RuntimeException(e); + + private String getPipCommandInvokedOrDecodedFromEnvironment(String EnvVar, String... cmdList) { + return getStringValueEnvironment(EnvVar, "").trim().equals("") + ? Operations.runProcessGetOutput(pythonEnvironmentDir, cmdList) + : new String(Base64.getDecoder().decode(getStringValueEnvironment(EnvVar, ""))); } - linesOfRequirements.stream().forEach( dep -> { - if(matchManifestVersions) - { - String dependencyName; - String manifestVersion; - String installedVersion = ""; - int doubleEqualSignPosition; - if(dep.contains("==")) - { - doubleEqualSignPosition = dep.indexOf("=="); - manifestVersion = dep.substring(doubleEqualSignPosition + 2).trim(); - if(manifestVersion.contains("#")) - { - var hashCharIndex = manifestVersion.indexOf("#"); - manifestVersion = manifestVersion.substring(0,hashCharIndex); - } - dependencyName = getDependencyName(dep); - String pipShowRecord = CachedTree.get(new StringInsensitive(dependencyName)); - if(pipShowRecord != null) - { - installedVersion = getDependencyVersion(pipShowRecord); - } - if(!installedVersion.trim().equals("")) { - if (!manifestVersion.trim().equals(installedVersion.trim())) { - throw new RuntimeException(String.format("Can't continue with analysis - versions mismatch for dependency name=%s, manifest version=%s, installed Version=%s, if you want to allow version mismatch for analysis between installed and requested packages, set environment variable/setting - MATCH_MANIFEST_VERSIONS=false", dependencyName, manifestVersion, installedVersion)); - } - } + + private void bringAllDependencies( + List> dependencies, + String depName, + Map cachedTree, + boolean includeTransitive, + List path) { + + if (dependencies == null || depName.trim().equals("")) return; + + String record = cachedTree.get(new StringInsensitive(depName)); + if (record == null) { + throw new PackageNotInstalledException(String.format( + "Package name=>%s is not installed on your python environment, either install it ( better to install requirements.txt altogether) or turn on environment variable EXHORT_PYTHON_VIRTUAL_ENV=true to automatically installs it on virtual environment ( will slow down the analysis)", + depName)); } - } - List path = new ArrayList<>(); - String depName = getDependencyName(dep.toLowerCase()); - path.add(depName); - bringAllDependencies(dependencies, depName,CachedTree, includeTransitive,path); - }); - - return dependencies; - } - - private String getPipShowFromEnvironment(String depNames) { - return getPipCommandInvokedOrDecodedFromEnvironment("EXHORT_PIP_SHOW",pipBinaryLocation,"show",depNames); -// return Operations.runProcessGetOutput(pythonEnvironmentDir, pipBinaryLocation, "show", depNames); - } - - String getPipFreezeFromEnvironment() { - return getPipCommandInvokedOrDecodedFromEnvironment("EXHORT_PIP_FREEZE",pipBinaryLocation,"freeze","--all"); - } - - private String getPipCommandInvokedOrDecodedFromEnvironment(String EnvVar, String... cmdList) { - return getStringValueEnvironment(EnvVar, "").trim().equals("") ? Operations.runProcessGetOutput(pythonEnvironmentDir, cmdList) : new String(Base64.getDecoder().decode(getStringValueEnvironment(EnvVar, ""))); - } - - private void bringAllDependencies(List> dependencies, String depName, Map cachedTree, boolean includeTransitive, List path) { - - if (dependencies == null || depName.trim().equals("")) - return; - - String record = cachedTree.get(new StringInsensitive(depName)); - if(record == null) - { - throw new PackageNotInstalledException(String.format("Package name=>%s is not installed on your python environment, either install it ( better to install requirements.txt altogether) or turn on environment variable EXHORT_PYTHON_VIRTUAL_ENV=true to automatically installs it on virtual environment ( will slow down the analysis)",depName)); + String depVersion = getDependencyVersion(record); + List directDeps = getDepsList(record); + getDependencyNameShow(record); + Map entry = new HashMap(); + dependencies.add(entry); + entry.put("name", getDependencyNameShow(record)); + entry.put("version", depVersion); + List> targetDeps = new ArrayList<>(); + directDeps.stream().forEach(dep -> { + if (!path.contains(dep.toLowerCase())) { + List depList = new ArrayList(); + depList.add(dep.toLowerCase()); + + if (includeTransitive) { + bringAllDependencies( + targetDeps, + dep, + cachedTree, + includeTransitive, + Stream.concat(path.stream(), depList.stream()).collect(Collectors.toList())); + } + } + Collections.sort(targetDeps, (o1, o2) -> { + String string1 = (String) (o1.get("name")); + String string2 = (String) (o2.get("name")); + return Arrays.compare(string1.toCharArray(), string2.toCharArray()); + }); + entry.put("dependencies", targetDeps); + }); } - String depVersion = getDependencyVersion(record); - List directDeps = getDepsList(record); - getDependencyNameShow(record); - Map entry = new HashMap(); - dependencies.add(entry); - entry.put("name", getDependencyNameShow(record)); - entry.put("version", depVersion); - List> targetDeps = new ArrayList<>(); - directDeps.stream().forEach( dep -> { - if(!path.contains(dep.toLowerCase())) { - List depList = new ArrayList(); - depList.add(dep.toLowerCase()); - - if (includeTransitive) { - bringAllDependencies(targetDeps, dep, cachedTree, includeTransitive, Stream.concat(path.stream(),depList.stream()).collect(Collectors.toList())); + + protected List getDepsList(String pipShowOutput) { + int requiresKeyIndex = pipShowOutput.indexOf("Requires:"); + String requiresToken = pipShowOutput.substring(requiresKeyIndex + 9); + int endOfLine = requiresToken.indexOf(System.lineSeparator()); + String listOfDeps; + if (endOfLine > -1) { + listOfDeps = requiresToken.substring(0, endOfLine).trim(); + } else { + listOfDeps = requiresToken; } - } - Collections.sort(targetDeps, (o1, o2) -> { - String string1 = (String) (o1.get("name")); - String string2 = (String) (o2.get("name")); - return Arrays.compare(string1.toCharArray(),string2.toCharArray()); - }); - entry.put("dependencies",targetDeps); - }); - - - } - - protected List getDepsList(String pipShowOutput) { - int requiresKeyIndex = pipShowOutput.indexOf("Requires:"); - String requiresToken = pipShowOutput.substring(requiresKeyIndex + 9); - int endOfLine = requiresToken.indexOf(System.lineSeparator()); - String listOfDeps; - if(endOfLine > -1) { - listOfDeps = requiresToken.substring(0, endOfLine).trim(); + return Arrays.stream(listOfDeps.split(",")) + .map(String::trim) + .filter(dep -> !dep.equals("")) + .collect(Collectors.toList()); } - else { - listOfDeps = requiresToken; + + protected String getDependencyVersion(String pipShowOutput) { + int versionKeyIndex = pipShowOutput.indexOf("Version:"); + String versionToken = pipShowOutput.substring(versionKeyIndex + 8); + int endOfLine = versionToken.indexOf(System.lineSeparator()); + return versionToken.substring(0, endOfLine).trim(); } - return Arrays.stream(listOfDeps.split(",")).map(String::trim).filter(dep -> !dep.equals("")).collect(Collectors.toList()); - } - - protected String getDependencyVersion(String pipShowOutput) { - int versionKeyIndex = pipShowOutput.indexOf("Version:"); - String versionToken = pipShowOutput.substring(versionKeyIndex + 8); - int endOfLine = versionToken.indexOf(System.lineSeparator()); - return versionToken.substring(0,endOfLine).trim(); - } - - protected String getDependencyNameShow(String pipShowOutput) { - int versionKeyIndex = pipShowOutput.indexOf("Name:"); - String versionToken = pipShowOutput.substring(versionKeyIndex + 5); - int endOfLine = versionToken.indexOf(System.lineSeparator()); - return versionToken.substring(0,endOfLine).trim(); - } - - public static String getDependencyName(String dep) { - int rightTriangleBracket = dep.indexOf(">"); - int leftTriangleBracket = dep.indexOf("<"); - int equalsSign= dep.indexOf("="); - int minimumIndex = getFirstSign(rightTriangleBracket,leftTriangleBracket,equalsSign); - String depName; - if(rightTriangleBracket == -1 && leftTriangleBracket == -1 && equalsSign == -1) - { - depName = dep; + + protected String getDependencyNameShow(String pipShowOutput) { + int versionKeyIndex = pipShowOutput.indexOf("Name:"); + String versionToken = pipShowOutput.substring(versionKeyIndex + 5); + int endOfLine = versionToken.indexOf(System.lineSeparator()); + return versionToken.substring(0, endOfLine).trim(); } - else { - depName = dep.substring(0, minimumIndex); + public static String getDependencyName(String dep) { + int rightTriangleBracket = dep.indexOf(">"); + int leftTriangleBracket = dep.indexOf("<"); + int equalsSign = dep.indexOf("="); + int minimumIndex = getFirstSign(rightTriangleBracket, leftTriangleBracket, equalsSign); + String depName; + if (rightTriangleBracket == -1 && leftTriangleBracket == -1 && equalsSign == -1) { + depName = dep; + } else { + + depName = dep.substring(0, minimumIndex); + } + return depName.trim(); } - return depName.trim(); - } - private static int getFirstSign(int rightTriangleBracket, int leftTriangleBracket, int equalsSign) { + private static int getFirstSign(int rightTriangleBracket, int leftTriangleBracket, int equalsSign) { rightTriangleBracket = rightTriangleBracket == -1 ? 999 : rightTriangleBracket; leftTriangleBracket = leftTriangleBracket == -1 ? 999 : leftTriangleBracket; equalsSign = equalsSign == -1 ? 999 : equalsSign; - return equalsSign < leftTriangleBracket && equalsSign < rightTriangleBracket ? equalsSign : (leftTriangleBracket < equalsSign && leftTriangleBracket < rightTriangleBracket ? leftTriangleBracket : rightTriangleBracket); - } + return equalsSign < leftTriangleBracket && equalsSign < rightTriangleBracket + ? equalsSign + : (leftTriangleBracket < equalsSign && leftTriangleBracket < rightTriangleBracket + ? leftTriangleBracket + : rightTriangleBracket); + } - static List splitPipShowLines(String pipShowOutput) { - return Arrays.stream(pipShowOutput.split(System.lineSeparator() + "---" + System.lineSeparator())).collect(Collectors.toList()); - } + static List splitPipShowLines(String pipShowOutput) { + return Arrays.stream(pipShowOutput.split(System.lineSeparator() + "---" + System.lineSeparator())) + .collect(Collectors.toList()); + } } diff --git a/src/main/java/com/redhat/exhort/utils/PythonControllerRealEnv.java b/src/main/java/com/redhat/exhort/utils/PythonControllerRealEnv.java index 213abee4..9cc8db44 100644 --- a/src/main/java/com/redhat/exhort/utils/PythonControllerRealEnv.java +++ b/src/main/java/com/redhat/exhort/utils/PythonControllerRealEnv.java @@ -15,62 +15,52 @@ */ package com.redhat.exhort.utils; -import com.redhat.exhort.tools.Operations; - -import java.io.IOException; import java.nio.file.FileSystems; -import java.nio.file.Files; import java.nio.file.Path; -public class PythonControllerRealEnv extends PythonControllerBase{ - public PythonControllerRealEnv(String pathToPythonBin,String pathToPip) { - Path pipPath = Path.of(pathToPip); - this.pipBinaryDir = pipPath.getParent(); - if(this.pipBinaryDir == null) - { - this.pipBinaryDir = pipPath; +public class PythonControllerRealEnv extends PythonControllerBase { + public PythonControllerRealEnv(String pathToPythonBin, String pathToPip) { + Path pipPath = Path.of(pathToPip); + this.pipBinaryDir = pipPath.getParent(); + if (this.pipBinaryDir == null) { + this.pipBinaryDir = pipPath; + } + this.pythonEnvironmentDir = Path.of(System.getProperty("user.dir")); + this.pathToPythonBin = pathToPythonBin; } - this.pythonEnvironmentDir = Path.of(System.getProperty("user.dir")); - this.pathToPythonBin = pathToPythonBin; - } - @Override - public void prepareEnvironment(String pathToPythonBin) - { - String envBinDir = pipBinaryDir.toString(); - if(envBinDir.contains(FileSystems.getDefault().getSeparator())) { - if (pathToPythonBin.contains("python3")) { - this.pipBinaryLocation = Path.of(envBinDir, "pip3").toString(); - } else { - this.pipBinaryLocation = Path.of(envBinDir, "pip").toString(); - } - } - else - { - this.pipBinaryLocation = envBinDir; + @Override + public void prepareEnvironment(String pathToPythonBin) { + String envBinDir = pipBinaryDir.toString(); + if (envBinDir.contains(FileSystems.getDefault().getSeparator())) { + if (pathToPythonBin.contains("python3")) { + this.pipBinaryLocation = Path.of(envBinDir, "pip3").toString(); + } else { + this.pipBinaryLocation = Path.of(envBinDir, "pip").toString(); + } + } else { + this.pipBinaryLocation = envBinDir; + } } - } - @Override - public boolean automaticallyInstallPackageOnEnvironment() { - return false; - } - - @Override - public boolean isRealEnv() { - return true; - } + @Override + public boolean automaticallyInstallPackageOnEnvironment() { + return false; + } - @Override - public boolean isVirtualEnv() - { - return false; - } + @Override + public boolean isRealEnv() { + return true; + } - @Override - public void cleanEnvironment(boolean cleanEnvironment) { + @Override + public boolean isVirtualEnv() { + return false; + } - //noop as this is real environment, - } + @Override + public void cleanEnvironment(boolean cleanEnvironment) { + // noop as this is real environment, + } } diff --git a/src/main/java/com/redhat/exhort/utils/PythonControllerTestEnv.java b/src/main/java/com/redhat/exhort/utils/PythonControllerTestEnv.java index b062940d..de2db76a 100644 --- a/src/main/java/com/redhat/exhort/utils/PythonControllerTestEnv.java +++ b/src/main/java/com/redhat/exhort/utils/PythonControllerTestEnv.java @@ -16,31 +16,29 @@ package com.redhat.exhort.utils; import com.redhat.exhort.tools.Operations; - import java.nio.file.Path; -public class PythonControllerTestEnv extends PythonControllerRealEnv{ -// private System.Logger log = System.getLogger("name"); - public PythonControllerTestEnv(String pathToPythonBin,String pathToPip) { - super(pathToPythonBin,pathToPip); - } +public class PythonControllerTestEnv extends PythonControllerRealEnv { + // private System.Logger log = System.getLogger("name"); + public PythonControllerTestEnv(String pathToPythonBin, String pathToPip) { + super(pathToPythonBin, pathToPip); + } - @Override - public void prepareEnvironment(String pathToPythonBin) - { - super.prepareEnvironment(pathToPythonBin); - String output = Operations.runProcessGetOutput(Path.of("."), new String[]{this.pathToPythonBin, "-m", "pip", "install", "--upgrade", "pip"}); -// log.log(System.Logger.Level.INFO,"Output from upgrading pip = " + System.lineSeparator() + output); - } + @Override + public void prepareEnvironment(String pathToPythonBin) { + super.prepareEnvironment(pathToPythonBin); + String output = Operations.runProcessGetOutput( + Path.of("."), new String[] {this.pathToPythonBin, "-m", "pip", "install", "--upgrade", "pip"}); + // log.log(System.Logger.Level.INFO,"Output from upgrading pip = " + System.lineSeparator() + output); + } - @Override - public boolean automaticallyInstallPackageOnEnvironment() { - return true; - } - @Override - public boolean isVirtualEnv() - { - return false; - } + @Override + public boolean automaticallyInstallPackageOnEnvironment() { + return true; + } + @Override + public boolean isVirtualEnv() { + return false; + } } diff --git a/src/main/java/com/redhat/exhort/utils/PythonControllerVirtualEnv.java b/src/main/java/com/redhat/exhort/utils/PythonControllerVirtualEnv.java index 15429028..f60176ef 100644 --- a/src/main/java/com/redhat/exhort/utils/PythonControllerVirtualEnv.java +++ b/src/main/java/com/redhat/exhort/utils/PythonControllerVirtualEnv.java @@ -16,93 +16,84 @@ package com.redhat.exhort.utils; import com.redhat.exhort.tools.Operations; - import java.io.IOException; import java.nio.file.FileSystems; import java.nio.file.Files; import java.nio.file.Path; import java.util.Comparator; -public class PythonControllerVirtualEnv extends PythonControllerBase{ - -// private System.Logger log = System.getLogger("name"); - public PythonControllerVirtualEnv(String pathToPythonBin) { - this.pipBinaryDir = Path.of(FileSystems.getDefault().getSeparator(), "tmp","exhort_env","bin"); - this.pythonEnvironmentDir = Path.of(FileSystems.getDefault().getSeparator(),"tmp","exhort_env"); - this.pathToPythonBin = pathToPythonBin; - } +public class PythonControllerVirtualEnv extends PythonControllerBase { - @Override - public void prepareEnvironment(String pathToPythonBin) - { - try { - if(!Files.exists(pythonEnvironmentDir)) { - Files.createDirectory(pythonEnvironmentDir); - } - } catch (IOException e) { - throw new RuntimeException(e); - } - String output = Operations.runProcessGetOutput(Path.of("."), new String[]{pathToPythonBin, "-m", "venv", pythonEnvironmentDir.toString()}); - String envBinDir = pipBinaryDir.toString(); - if(pathToPythonBin.contains("python3")) - { - this.pipBinaryLocation = Path.of(envBinDir,"pip3").toString(); + // private System.Logger log = System.getLogger("name"); + public PythonControllerVirtualEnv(String pathToPythonBin) { + this.pipBinaryDir = Path.of(FileSystems.getDefault().getSeparator(), "tmp", "exhort_env", "bin"); + this.pythonEnvironmentDir = Path.of(FileSystems.getDefault().getSeparator(), "tmp", "exhort_env"); + this.pathToPythonBin = pathToPythonBin; } - else - { - this.pipBinaryLocation = Path.of(envBinDir,"pip").toString(); - } - - } - @Override - public boolean automaticallyInstallPackageOnEnvironment() { - return true; - } + @Override + public void prepareEnvironment(String pathToPythonBin) { + try { + if (!Files.exists(pythonEnvironmentDir)) { + Files.createDirectory(pythonEnvironmentDir); + } + } catch (IOException e) { + throw new RuntimeException(e); + } + String output = Operations.runProcessGetOutput( + Path.of("."), new String[] {pathToPythonBin, "-m", "venv", pythonEnvironmentDir.toString()}); + String envBinDir = pipBinaryDir.toString(); + if (pathToPythonBin.contains("python3")) { + this.pipBinaryLocation = Path.of(envBinDir, "pip3").toString(); + } else { + this.pipBinaryLocation = Path.of(envBinDir, "pip").toString(); + } + } - @Override - public boolean isRealEnv() { - return false; - } + @Override + public boolean automaticallyInstallPackageOnEnvironment() { + return true; + } - @Override - public boolean isVirtualEnv() - { - return true; - } + @Override + public boolean isRealEnv() { + return false; + } - @Override - public void cleanEnvironment(boolean deleteEnvironment) - { - if(deleteEnvironment) - { - try { - Files - .walk(pythonEnvironmentDir) - .sorted(Comparator.reverseOrder()) - .forEach( file -> { - try { - Files.delete(file); - } catch (IOException e) { - throw new RuntimeException(e); - } - }); - } catch (IOException e) { - throw new RuntimeException(e); - } + @Override + public boolean isVirtualEnv() { + return true; } - else { - Path envRequirements = Path.of(pythonEnvironmentDir.toString(), "requirements.txt"); - try { - Files.deleteIfExists(envRequirements); - String freezeOutput = Operations.runProcessGetOutput(pythonEnvironmentDir, pipBinaryLocation, "freeze"); - Files.createFile(envRequirements); - Files.write(envRequirements,freezeOutput.getBytes()); - Operations.runProcessGetOutput(pythonEnvironmentDir, pipBinaryLocation, "uninstall","-y","-r","requirements.txt"); - } catch (IOException e) { - throw new RuntimeException(e); - } + @Override + public void cleanEnvironment(boolean deleteEnvironment) { + if (deleteEnvironment) { + try { + Files.walk(pythonEnvironmentDir) + .sorted(Comparator.reverseOrder()) + .forEach(file -> { + try { + Files.delete(file); + } catch (IOException e) { + throw new RuntimeException(e); + } + }); + } catch (IOException e) { + throw new RuntimeException(e); + } + } else { + Path envRequirements = Path.of(pythonEnvironmentDir.toString(), "requirements.txt"); + try { + Files.deleteIfExists(envRequirements); + String freezeOutput = Operations.runProcessGetOutput(pythonEnvironmentDir, pipBinaryLocation, "freeze"); + Files.createFile(envRequirements); + Files.write(envRequirements, freezeOutput.getBytes()); + Operations.runProcessGetOutput( + pythonEnvironmentDir, pipBinaryLocation, "uninstall", "-y", "-r", "requirements.txt"); + + } catch (IOException e) { + throw new RuntimeException(e); + } + } } - } } diff --git a/src/main/java/com/redhat/exhort/utils/StringInsensitive.java b/src/main/java/com/redhat/exhort/utils/StringInsensitive.java index 4eb092fb..5b7c1195 100644 --- a/src/main/java/com/redhat/exhort/utils/StringInsensitive.java +++ b/src/main/java/com/redhat/exhort/utils/StringInsensitive.java @@ -19,30 +19,30 @@ public class StringInsensitive { - public StringInsensitive(String value) { - this.value = value; - } - - @Override - public boolean equals(Object o) { - if (this == o) return true; - if (o == null || getClass() != o.getClass()) return false; - StringInsensitive that = (StringInsensitive) o; - return Objects.equals(value.toLowerCase(), that.value.toLowerCase()); - } - - @Override - public int hashCode() { - return Objects.hash(value.toLowerCase()); - } - - private String value; - - public String getValue() { - return value; - } - - public void setValue(String value) { - this.value = value; - } + public StringInsensitive(String value) { + this.value = value; + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + StringInsensitive that = (StringInsensitive) o; + return Objects.equals(value.toLowerCase(), that.value.toLowerCase()); + } + + @Override + public int hashCode() { + return Objects.hash(value.toLowerCase()); + } + + private String value; + + public String getValue() { + return value; + } + + public void setValue(String value) { + this.value = value; + } } diff --git a/src/main/java/com/redhat/exhort/vcs/GitVersionControlSystemImpl.java b/src/main/java/com/redhat/exhort/vcs/GitVersionControlSystemImpl.java index 01049ad9..eac9ed2e 100644 --- a/src/main/java/com/redhat/exhort/vcs/GitVersionControlSystemImpl.java +++ b/src/main/java/com/redhat/exhort/vcs/GitVersionControlSystemImpl.java @@ -16,7 +16,6 @@ package com.redhat.exhort.vcs; import com.redhat.exhort.tools.Operations; - import java.nio.file.Path; import java.time.LocalDateTime; import java.time.format.DateTimeFormatter; @@ -25,124 +24,117 @@ public class GitVersionControlSystemImpl implements VersionControlSystem { - private String gitBinary; - public GitVersionControlSystemImpl() - { - gitBinary = Operations.getCustomPathOrElse("git"); - } - @Override - public TagInfo getLatestTag(Path repoLocation) { - TagInfo tagInfo = new TagInfo(); - - //get current commit hash digest - String commitHash = Operations.runProcessGetOutput(repoLocation, gitBinary, "rev-parse", "HEAD").trim(); - if(Pattern.matches("^[a-f0-9]+",commitHash)) { - tagInfo.setCurrentCommitDigest(commitHash); - //get current commit timestamp. - String timeStampFromGit = Operations.runProcessGetOutput(repoLocation, gitBinary, "show", "HEAD", "--format=%cI", "--date", "local", "--quiet"); - LocalDateTime commitTimestamp = LocalDateTime.parse(timeStampFromGit.trim(), DateTimeFormatter.ISO_OFFSET_DATE_TIME); - tagInfo.setCommitTimestamp(commitTimestamp); - - - // go get last annotated tag - String resultFromInvocation = Operations.runProcessGetOutput(repoLocation, gitBinary, "describe", "--abbrev=12").trim(); - - // if there are only unannotated tag, fetch last one. - if (resultFromInvocation.contains("there were unannotated tags")) { - //fetch last unannotated tag - resultFromInvocation = Operations.runProcessGetOutput(repoLocation, gitBinary, "describe", "--tags", "--abbrev=12").trim(); - fetchLatestTag(tagInfo, resultFromInvocation); - } else { - if (resultFromInvocation.startsWith("fatal: No names found")) { - tagInfo.setCurrentCommitPointedByTag(false); - tagInfo.setTagName(""); + private String gitBinary; + + public GitVersionControlSystemImpl() { + gitBinary = Operations.getCustomPathOrElse("git"); + } + + @Override + public TagInfo getLatestTag(Path repoLocation) { + TagInfo tagInfo = new TagInfo(); + + // get current commit hash digest + String commitHash = Operations.runProcessGetOutput(repoLocation, gitBinary, "rev-parse", "HEAD") + .trim(); + if (Pattern.matches("^[a-f0-9]+", commitHash)) { + tagInfo.setCurrentCommitDigest(commitHash); + // get current commit timestamp. + String timeStampFromGit = Operations.runProcessGetOutput( + repoLocation, gitBinary, "show", "HEAD", "--format=%cI", "--date", "local", "--quiet"); + LocalDateTime commitTimestamp = + LocalDateTime.parse(timeStampFromGit.trim(), DateTimeFormatter.ISO_OFFSET_DATE_TIME); + tagInfo.setCommitTimestamp(commitTimestamp); + + // go get last annotated tag + String resultFromInvocation = Operations.runProcessGetOutput( + repoLocation, gitBinary, "describe", "--abbrev=12") + .trim(); + + // if there are only unannotated tag, fetch last one. + if (resultFromInvocation.contains("there were unannotated tags")) { + // fetch last unannotated tag + resultFromInvocation = Operations.runProcessGetOutput( + repoLocation, gitBinary, "describe", "--tags", "--abbrev=12") + .trim(); + fetchLatestTag(tagInfo, resultFromInvocation); + } else { + if (resultFromInvocation.startsWith("fatal: No names found")) { + tagInfo.setCurrentCommitPointedByTag(false); + tagInfo.setTagName(""); + } + // fetch last annotated tag + else { + fetchLatestTag(tagInfo, resultFromInvocation); + } + } } - //fetch last annotated tag - else { - fetchLatestTag(tagInfo, resultFromInvocation); + // empty git repo with no commits + else { + tagInfo.setTagName(""); + tagInfo.setCurrentCommitPointedByTag(false); + tagInfo.setCommitTimestamp( + LocalDateTime.parse(LocalDateTime.MIN.toString(), DateTimeFormatter.ISO_LOCAL_DATE_TIME)); + tagInfo.setCurrentCommitDigest(""); } - } + return tagInfo; } - // empty git repo with no commits - else - { - tagInfo.setTagName(""); - tagInfo.setCurrentCommitPointedByTag(false); - tagInfo.setCommitTimestamp(LocalDateTime.parse(LocalDateTime.MIN.toString(),DateTimeFormatter.ISO_LOCAL_DATE_TIME)); - tagInfo.setCurrentCommitDigest(""); - } - return tagInfo; - } - - - @Override - public boolean isDirectoryRepo(Path repoLocation) { - - String resultFromInvocation = Operations.runProcessGetOutput(repoLocation, gitBinary, "rev-parse", "--is-inside-work-tree"); - return resultFromInvocation.trim().equals("true"); - } - - @Override - public String getNextTagVersion(TagInfo tagInfo) { - String result=""; - //if tag version ends with a digit, then increment it by one, and append to the end -0. - if(Pattern.matches(".*[0-9]$",tagInfo.getTagName())) - { - int length = tagInfo.getTagName().toCharArray().length; - Integer lastDigit= Integer.parseInt(tagInfo.getTagName().substring(length - 1, length)); - lastDigit++; - result = String.format("%s%s-0",tagInfo.getTagName().substring(0,length-1),lastDigit.toString()); - } - else - { - //if tag version ends with some suffix starting with '.' or '-', then just append to the end -0. - if(Pattern.matches(".*-[a-zA-Z0-9]+$|.*\\.[a-zA-Z0-9]+$",tagInfo.getTagName())) - { - result = String.format("%s-0",tagInfo.getTagName()); - } - - } - return result; - } - - public String getPseudoVersion(TagInfo tagInfo, String newTagVersion) { - String stringTS = tagInfo.getCommitTimestamp().toString().replaceAll("[:-]|T",""); - String commitHash12 = tagInfo.getCurrentCommitDigest().substring(0,12); - return String.format("%s.%s-%s",newTagVersion,stringTS,commitHash12); - - } - private static void fetchLatestTag(TagInfo tagInfo, String resultFromInvocation) { - String[] parts = resultFromInvocation.split("-"); - if(parts.length > 1) - { - analyzeGitDescribeResult(tagInfo, parts); + @Override + public boolean isDirectoryRepo(Path repoLocation) { + String resultFromInvocation = + Operations.runProcessGetOutput(repoLocation, gitBinary, "rev-parse", "--is-inside-work-tree"); + return resultFromInvocation.trim().equals("true"); } - else - { - tagInfo.setCurrentCommitPointedByTag(true); - tagInfo.setTagName(parts[0]); + + @Override + public String getNextTagVersion(TagInfo tagInfo) { + String result = ""; + // if tag version ends with a digit, then increment it by one, and append to the end -0. + if (Pattern.matches(".*[0-9]$", tagInfo.getTagName())) { + int length = tagInfo.getTagName().toCharArray().length; + Integer lastDigit = Integer.parseInt(tagInfo.getTagName().substring(length - 1, length)); + lastDigit++; + result = String.format("%s%s-0", tagInfo.getTagName().substring(0, length - 1), lastDigit.toString()); + } else { + // if tag version ends with some suffix starting with '.' or '-', then just append to the end -0. + if (Pattern.matches(".*-[a-zA-Z0-9]+$|.*\\.[a-zA-Z0-9]+$", tagInfo.getTagName())) { + result = String.format("%s-0", tagInfo.getTagName()); + } + } + return result; } - } - - private static void analyzeGitDescribeResult(TagInfo tagInfo, String[] parts) { - if(Pattern.matches("g[0-9a-f]{12}", parts[parts.length-1]) - && Pattern.matches("[1-9]*", parts[parts.length-2])) - { - String[] tagNameParts = Arrays.copyOfRange(parts, 0, parts.length - 2); - tagInfo.setTagName(String.join("-" , tagNameParts)); - tagInfo.setCurrentCommitDigest(parts[parts.length-1].replace("g","")); - tagInfo.setCurrentCommitPointedByTag(false); + + public String getPseudoVersion(TagInfo tagInfo, String newTagVersion) { + String stringTS = tagInfo.getCommitTimestamp().toString().replaceAll("[:-]|T", ""); + String commitHash12 = tagInfo.getCurrentCommitDigest().substring(0, 12); + return String.format("%s.%s-%s", newTagVersion, stringTS, commitHash12); } - else - { - String[] tagNameParts = Arrays.copyOfRange(parts, 0, parts.length - 2); - tagInfo.setTagName(String.join("-" , tagNameParts)); - tagInfo.setCurrentCommitPointedByTag(true); + private static void fetchLatestTag(TagInfo tagInfo, String resultFromInvocation) { + String[] parts = resultFromInvocation.split("-"); + if (parts.length > 1) { + analyzeGitDescribeResult(tagInfo, parts); + + } else { + tagInfo.setCurrentCommitPointedByTag(true); + tagInfo.setTagName(parts[0]); + } } - } + private static void analyzeGitDescribeResult(TagInfo tagInfo, String[] parts) { + if (Pattern.matches("g[0-9a-f]{12}", parts[parts.length - 1]) + && Pattern.matches("[1-9]*", parts[parts.length - 2])) { + String[] tagNameParts = Arrays.copyOfRange(parts, 0, parts.length - 2); + tagInfo.setTagName(String.join("-", tagNameParts)); + tagInfo.setCurrentCommitDigest(parts[parts.length - 1].replace("g", "")); + tagInfo.setCurrentCommitPointedByTag(false); + } else { + String[] tagNameParts = Arrays.copyOfRange(parts, 0, parts.length - 2); + tagInfo.setTagName(String.join("-", tagNameParts)); + tagInfo.setCurrentCommitPointedByTag(true); + } + } } diff --git a/src/main/java/com/redhat/exhort/vcs/TagInfo.java b/src/main/java/com/redhat/exhort/vcs/TagInfo.java index 41ace28f..e1bb4f2e 100644 --- a/src/main/java/com/redhat/exhort/vcs/TagInfo.java +++ b/src/main/java/com/redhat/exhort/vcs/TagInfo.java @@ -19,41 +19,41 @@ public class TagInfo { - private String tagName; - private boolean currentCommitPointedByTag; - private String currentCommitDigest; + private String tagName; + private boolean currentCommitPointedByTag; + private String currentCommitDigest; - public LocalDateTime getCommitTimestamp() { - return commitTimestamp; - } + public LocalDateTime getCommitTimestamp() { + return commitTimestamp; + } - public void setCommitTimestamp(LocalDateTime commitTimestamp) { - this.commitTimestamp = commitTimestamp; - } + public void setCommitTimestamp(LocalDateTime commitTimestamp) { + this.commitTimestamp = commitTimestamp; + } - private LocalDateTime commitTimestamp; + private LocalDateTime commitTimestamp; - public String getTagName() { - return tagName; - } + public String getTagName() { + return tagName; + } - public void setTagName(String tagName) { - this.tagName = tagName; - } + public void setTagName(String tagName) { + this.tagName = tagName; + } - public boolean isCurrentCommitPointedByTag() { - return currentCommitPointedByTag; - } + public boolean isCurrentCommitPointedByTag() { + return currentCommitPointedByTag; + } - public void setCurrentCommitPointedByTag(boolean currentCommitPointedByTag) { - this.currentCommitPointedByTag = currentCommitPointedByTag; - } + public void setCurrentCommitPointedByTag(boolean currentCommitPointedByTag) { + this.currentCommitPointedByTag = currentCommitPointedByTag; + } - public String getCurrentCommitDigest() { - return currentCommitDigest; - } + public String getCurrentCommitDigest() { + return currentCommitDigest; + } - public void setCurrentCommitDigest(String currentCommitDigest) { - this.currentCommitDigest = currentCommitDigest; - } + public void setCurrentCommitDigest(String currentCommitDigest) { + this.currentCommitDigest = currentCommitDigest; + } } diff --git a/src/main/java/com/redhat/exhort/vcs/VersionControlSystem.java b/src/main/java/com/redhat/exhort/vcs/VersionControlSystem.java index 49cc3f55..03e48e3f 100644 --- a/src/main/java/com/redhat/exhort/vcs/VersionControlSystem.java +++ b/src/main/java/com/redhat/exhort/vcs/VersionControlSystem.java @@ -18,34 +18,33 @@ import java.nio.file.Path; public interface VersionControlSystem { - /** - * This method gets the latest tag in a repo, information whether current commit pointed by - * a tag or not, and a short hash digest of the current commit - * @param repoLocation - the repo directory path with inner .git directory - * @return {@link TagInfo} containing the latest tag - */ - TagInfo getLatestTag(Path repoLocation); + /** + * This method gets the latest tag in a repo, information whether current commit pointed by + * a tag or not, and a short hash digest of the current commit + * @param repoLocation - the repo directory path with inner .git directory + * @return {@link TagInfo} containing the latest tag + */ + TagInfo getLatestTag(Path repoLocation); - /** - * - * @param repoLocation - the directory path to be checked whether it's a git repo or not - * @return {boolean} - returns true if the directory is a repo. - */ - boolean isDirectoryRepo(Path repoLocation); + /** + * + * @param repoLocation - the directory path to be checked whether it's a git repo or not + * @return {boolean} - returns true if the directory is a repo. + */ + boolean isDirectoryRepo(Path repoLocation); - /** - * - * @param tagInfo - object that contains the tag info in order to calculate next version - * @return A String containing the next version - */ - String getNextTagVersion(TagInfo tagInfo); - - /** - * - * @param tagInfo - object that contains the tag info and current commit data - * @param newTagVersion - * @return - */ - String getPseudoVersion(TagInfo tagInfo , String newTagVersion); + /** + * + * @param tagInfo - object that contains the tag info in order to calculate next version + * @return A String containing the next version + */ + String getNextTagVersion(TagInfo tagInfo); + /** + * + * @param tagInfo - object that contains the tag info and current commit data + * @param newTagVersion + * @return + */ + String getPseudoVersion(TagInfo tagInfo, String newTagVersion); } diff --git a/src/main/java/module-info.java b/src/main/java/module-info.java index 1c7ffa7d..3e690a0e 100644 --- a/src/main/java/module-info.java +++ b/src/main/java/module-info.java @@ -1,30 +1,37 @@ module com.redhat.exhort { - requires java.net.http; + requires java.net.http; + requires com.fasterxml.jackson.annotation; + requires com.fasterxml.jackson.core; + requires transitive com.fasterxml.jackson.databind; + requires jakarta.annotation; + requires java.xml; + requires jakarta.mail; + requires cyclonedx.core.java; + requires transitive packageurl.java; + requires org.tomlj; - requires com.fasterxml.jackson.annotation; - requires com.fasterxml.jackson.core; - requires transitive com.fasterxml.jackson.databind; - requires jakarta.annotation; - requires java.xml; - requires jakarta.mail; - requires cyclonedx.core.java; - requires transitive packageurl.java; - requires org.tomlj; + opens com.redhat.exhort.api to + com.fasterxml.jackson.databind; + opens com.redhat.exhort.providers to + com.fasterxml.jackson.databind; - opens com.redhat.exhort.api to com.fasterxml.jackson.databind; - opens com.redhat.exhort.providers to com.fasterxml.jackson.databind; + exports com.redhat.exhort; + exports com.redhat.exhort.api; + exports com.redhat.exhort.api.serialization; + exports com.redhat.exhort.impl; + exports com.redhat.exhort.sbom; + exports com.redhat.exhort.tools; - exports com.redhat.exhort; - exports com.redhat.exhort.api; - exports com.redhat.exhort.api.serialization; - exports com.redhat.exhort.impl; - exports com.redhat.exhort.sbom; - exports com.redhat.exhort.tools; + opens com.redhat.exhort.sbom to + com.fasterxml.jackson.databind, + packageurl.java; + opens com.redhat.exhort.api.serialization to + com.fasterxml.jackson.databind; - opens com.redhat.exhort.sbom to com.fasterxml.jackson.databind, packageurl.java; - opens com.redhat.exhort.api.serialization to com.fasterxml.jackson.databind; exports com.redhat.exhort.providers; - exports com.redhat.exhort.logging; - exports com.redhat.exhort.image; - opens com.redhat.exhort.image to com.fasterxml.jackson.databind; + exports com.redhat.exhort.logging; + exports com.redhat.exhort.image; + + opens com.redhat.exhort.image to + com.fasterxml.jackson.databind; } diff --git a/src/test/java/com/redhat/exhort/ExhortTest.java b/src/test/java/com/redhat/exhort/ExhortTest.java index 74efe977..c7d08dd7 100644 --- a/src/test/java/com/redhat/exhort/ExhortTest.java +++ b/src/test/java/com/redhat/exhort/ExhortTest.java @@ -23,61 +23,61 @@ public class ExhortTest { - protected String getStringFromFile(String... list) { - byte[] bytes = new byte[0]; - try { - InputStream resourceAsStream = getResourceAsStreamDecision(this.getClass(), list); - bytes = resourceAsStream.readAllBytes(); - resourceAsStream.close(); - } catch (IOException e) { - throw new RuntimeException(e); - } - - return new String(bytes); - } + protected String getStringFromFile(String... list) { + byte[] bytes = new byte[0]; + try { + InputStream resourceAsStream = getResourceAsStreamDecision(this.getClass(), list); + bytes = resourceAsStream.readAllBytes(); + resourceAsStream.close(); + } catch (IOException e) { + throw new RuntimeException(e); + } - public static InputStream getResourceAsStreamDecision(Class theClass, String[] list) throws IOException { - InputStream resourceAsStreamFromModule = theClass.getModule().getResourceAsStream(String.join("/", list)); - if (Objects.isNull(resourceAsStreamFromModule)) { - return theClass.getClassLoader().getResourceAsStream(String.join("/", list)); + return new String(bytes); } - return resourceAsStreamFromModule; - } - protected String getFileFromResource(String fileName, String... pathList) { - Path tmpFile; - try { - var tmpDir = Files.createTempDirectory("exhort_test_"); - tmpFile = Files.createFile(tmpDir.resolve(fileName)); - try (var is = getResourceAsStreamDecision(this.getClass(), pathList)) { - if(Objects.nonNull(is)) { - Files.write(tmpFile, is.readAllBytes()); - } - else - { - InputStream resourceIs = getClass().getClassLoader().getResourceAsStream(String.join("/", pathList)); - Files.write(tmpFile, resourceIs.readAllBytes()); - resourceIs.close(); + public static InputStream getResourceAsStreamDecision(Class theClass, String[] list) + throws IOException { + InputStream resourceAsStreamFromModule = theClass.getModule().getResourceAsStream(String.join("/", list)); + if (Objects.isNull(resourceAsStreamFromModule)) { + return theClass.getClassLoader().getResourceAsStream(String.join("/", list)); } - } catch (IOException e) { - throw new RuntimeException(e); - } - } catch (IOException e) { - throw new RuntimeException(e); + return resourceAsStreamFromModule; } - return tmpFile.toString(); - } -protected String getFileFromString(String fileName, String content) { - Path tmpFile; - try { - var tmpDir = Files.createTempDirectory("exhort_test_"); - tmpFile = Files.createFile(tmpDir.resolve(fileName)); - Files.write(tmpFile, content.getBytes()); - } catch (IOException e) { - throw new RuntimeException(e); + protected String getFileFromResource(String fileName, String... pathList) { + Path tmpFile; + try { + var tmpDir = Files.createTempDirectory("exhort_test_"); + tmpFile = Files.createFile(tmpDir.resolve(fileName)); + try (var is = getResourceAsStreamDecision(this.getClass(), pathList)) { + if (Objects.nonNull(is)) { + Files.write(tmpFile, is.readAllBytes()); + } else { + InputStream resourceIs = + getClass().getClassLoader().getResourceAsStream(String.join("/", pathList)); + Files.write(tmpFile, resourceIs.readAllBytes()); + resourceIs.close(); + } + } catch (IOException e) { + throw new RuntimeException(e); + } + } catch (IOException e) { + throw new RuntimeException(e); + } + return tmpFile.toString(); } - return tmpFile.toString(); - } + protected String getFileFromString(String fileName, String content) { + Path tmpFile; + try { + var tmpDir = Files.createTempDirectory("exhort_test_"); + tmpFile = Files.createFile(tmpDir.resolve(fileName)); + Files.write(tmpFile, content.getBytes()); + + } catch (IOException e) { + throw new RuntimeException(e); + } + return tmpFile.toString(); + } } diff --git a/src/test/java/com/redhat/exhort/image/ImageRefTest.java b/src/test/java/com/redhat/exhort/image/ImageRefTest.java index af4ba6be..2ee3d06f 100644 --- a/src/test/java/com/redhat/exhort/image/ImageRefTest.java +++ b/src/test/java/com/redhat/exhort/image/ImageRefTest.java @@ -15,83 +15,83 @@ */ package com.redhat.exhort.image; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.mockito.AdditionalMatchers.aryEq; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.ArgumentMatchers.isNull; + import com.github.packageurl.MalformedPackageURLException; import com.github.packageurl.PackageURL; import com.redhat.exhort.ExhortTest; import com.redhat.exhort.tools.Operations; -import org.junit.jupiter.api.Test; -import org.mockito.MockedStatic; -import org.mockito.Mockito; - import java.io.BufferedReader; import java.io.IOException; import java.io.InputStreamReader; import java.nio.charset.StandardCharsets; import java.util.stream.Collectors; - -import static org.junit.jupiter.api.Assertions.assertEquals; -import static org.junit.jupiter.api.Assertions.assertTrue; -import static org.mockito.AdditionalMatchers.aryEq; -import static org.mockito.ArgumentMatchers.eq; -import static org.mockito.ArgumentMatchers.isNull; +import org.junit.jupiter.api.Test; +import org.mockito.MockedStatic; +import org.mockito.Mockito; class ImageRefTest extends ExhortTest { - @Test - void test_imageRef() throws MalformedPackageURLException { - var image = "test.io/test-namespace/test-repository:test-tag@sha256:333224a233db31852ac1085c6cd702016ab8aaf54cecde5c4bed5451d636adcf"; - var platform = "linux/arm/v7"; + @Test + void test_imageRef() throws MalformedPackageURLException { + var image = + "test.io/test-namespace/test-repository:test-tag@sha256:333224a233db31852ac1085c6cd702016ab8aaf54cecde5c4bed5451d636adcf"; + var platform = "linux/arm/v7"; - var imageRef = new ImageRef(image, platform); + var imageRef = new ImageRef(image, platform); - assertEquals(new Image(image), imageRef.getImage()); - assertEquals(new Platform(platform), imageRef.getPlatform()); - assertEquals("ImageRef{image='" + image + '\'' + ", platform='" + platform + '\'' + '}', imageRef.toString()); + assertEquals(new Image(image), imageRef.getImage()); + assertEquals(new Platform(platform), imageRef.getPlatform()); + assertEquals("ImageRef{image='" + image + '\'' + ", platform='" + platform + '\'' + '}', imageRef.toString()); - var purl = new PackageURL("pkg:oci/test-repository@sha256:333224a233db31852ac1085c6cd702016ab8aaf54cecde5c4bed5451d636adcf?" + - "repository_url=test.io/test-namespace/test-repository&tag=test-tag&os=linux&arch=arm&variant=v7"); - assertEquals(purl, imageRef.getPackageURL()); + var purl = new PackageURL( + "pkg:oci/test-repository@sha256:333224a233db31852ac1085c6cd702016ab8aaf54cecde5c4bed5451d636adcf?" + + "repository_url=test.io/test-namespace/test-repository&tag=test-tag&os=linux&arch=arm&variant=v7"); + assertEquals(purl, imageRef.getPackageURL()); - var imageRefPurl = new ImageRef(purl); - assertEquals(imageRef, imageRefPurl); - assertTrue(imageRef.equals(imageRefPurl)); - assertEquals(imageRef.hashCode(), imageRefPurl.hashCode()); - } + var imageRefPurl = new ImageRef(purl); + assertEquals(imageRef, imageRefPurl); + assertTrue(imageRef.equals(imageRefPurl)); + assertEquals(imageRef.hashCode(), imageRefPurl.hashCode()); + } - @Test - void test_check_image_digest() throws IOException { - try (MockedStatic mock = Mockito.mockStatic(Operations.class); - var is = getResourceAsStreamDecision(this.getClass(), new String[]{"msc", "image", "skopeo_inspect_multi_raw.json"})) { - var json = new BufferedReader(new InputStreamReader(is, StandardCharsets.UTF_8)).lines().collect(Collectors.joining("\n")); - var output = new Operations.ProcessExecOutput(json, "", 0); - var imageName = "test.io/test/test-app:test-version"; + @Test + void test_check_image_digest() throws IOException { + try (MockedStatic mock = Mockito.mockStatic(Operations.class); + var is = getResourceAsStreamDecision( + this.getClass(), new String[] {"msc", "image", "skopeo_inspect_multi_raw.json"})) { + var json = new BufferedReader(new InputStreamReader(is, StandardCharsets.UTF_8)) + .lines() + .collect(Collectors.joining("\n")); + var output = new Operations.ProcessExecOutput(json, "", 0); + var imageName = "test.io/test/test-app:test-version"; - mock.when(() -> Operations.getCustomPathOrElse(eq("skopeo"))) - .thenReturn("skopeo"); + mock.when(() -> Operations.getCustomPathOrElse(eq("skopeo"))).thenReturn("skopeo"); - mock.when(() -> Operations.runProcessGetFullOutput(isNull(), - aryEq(new String[]{"skopeo", "inspect", - "--raw", String.format("docker://%s", imageName)}), - isNull())) - .thenReturn(output); + mock.when(() -> Operations.runProcessGetFullOutput( + isNull(), + aryEq(new String[] {"skopeo", "inspect", "--raw", String.format("docker://%s", imageName)}), + isNull())) + .thenReturn(output); - mock.when(() -> Operations.getCustomPathOrElse(eq("docker"))) - .thenReturn("docker"); + mock.when(() -> Operations.getCustomPathOrElse(eq("docker"))).thenReturn("docker"); - mock.when(() -> Operations.runProcessGetFullOutput(isNull(), - aryEq(new String[]{"docker", "info"}), - isNull())) - .thenReturn(new Operations.ProcessExecOutput("OSType: linux\nArchitecture: amd64", "", 0)); + mock.when(() -> Operations.runProcessGetFullOutput( + isNull(), aryEq(new String[] {"docker", "info"}), isNull())) + .thenReturn(new Operations.ProcessExecOutput("OSType: linux\nArchitecture: amd64", "", 0)); - var imageRef = new ImageRef(imageName, null); - imageRef.checkImageDigest(); + var imageRef = new ImageRef(imageName, null); + imageRef.checkImageDigest(); - var expectedImageRef = new ImageRef(imageName + - "@sha256:06d06f15f7b641a78f2512c8817cbecaa1bf549488e273f5ac27ff1654ed33f0", "linux/amd64"); + var expectedImageRef = new ImageRef( + imageName + "@sha256:06d06f15f7b641a78f2512c8817cbecaa1bf549488e273f5ac27ff1654ed33f0", + "linux/amd64"); - assertEquals(expectedImageRef, imageRef); + assertEquals(expectedImageRef, imageRef); + } } - - - } } diff --git a/src/test/java/com/redhat/exhort/image/ImageTest.java b/src/test/java/com/redhat/exhort/image/ImageTest.java index b1e537e7..bbe5a312 100644 --- a/src/test/java/com/redhat/exhort/image/ImageTest.java +++ b/src/test/java/com/redhat/exhort/image/ImageTest.java @@ -15,486 +15,479 @@ */ package com.redhat.exhort.image; +import static org.junit.jupiter.api.Assertions.*; + +import java.util.stream.Stream; import org.junit.jupiter.api.Named; import org.junit.jupiter.api.Test; import org.junit.jupiter.params.ParameterizedTest; import org.junit.jupiter.params.provider.Arguments; import org.junit.jupiter.params.provider.MethodSource; -import java.util.stream.Stream; - -import static org.junit.jupiter.api.Assertions.*; - class ImageTest { - static Stream imageTestSources() { - return Stream.of( - Arguments.of( - Named.of("full name, host port", - "test-host:5000/test-namespace/test-repository:test-tag@sha256:333224a233db31852ac1085c6cd702016ab8aaf54cecde5c4bed5451d636adcf"), - null, - "optional-host:2500", - "sha256:b048f7d88a830ba5b7c690193644f6baf658dde41d5d1e70d9f4bc275865a9a0", - "test-host:5000", - "test-namespace/test-repository", - "test-tag", - "sha256:333224a233db31852ac1085c6cd702016ab8aaf54cecde5c4bed5451d636adcf", - "test-host:5000/test-namespace/test-repository:test-tag@sha256:333224a233db31852ac1085c6cd702016ab8aaf54cecde5c4bed5451d636adcf", - true, - "test-host:5000/test-namespace/test-repository", - "test-host:5000/test-namespace/test-repository", - "test-host:5000/test-namespace/test-repository:test-tag@sha256:333224a233db31852ac1085c6cd702016ab8aaf54cecde5c4bed5451d636adcf", - "test-host:5000/test-namespace/test-repository:test-tag@sha256:333224a233db31852ac1085c6cd702016ab8aaf54cecde5c4bed5451d636adcf", - "test-namespace", - "test-repository", - "optional-host:2500/test-repository:test-tag@sha256:333224a233db31852ac1085c6cd702016ab8aaf54cecde5c4bed5451d636adcf", - "test-host:5000/test-namespace/test-repository:test-tag@sha256:333224a233db31852ac1085c6cd702016ab8aaf54cecde5c4bed5451d636adcf" - ), - Arguments.of( - Named.of("full name, registry", - "test.io/test-namespace/test-repository:test-tag@sha256:333224a233db31852ac1085c6cd702016ab8aaf54cecde5c4bed5451d636adcf"), - null, - "optional.io", - "sha256:b048f7d88a830ba5b7c690193644f6baf658dde41d5d1e70d9f4bc275865a9a0", - "test.io", - "test-namespace/test-repository", - "test-tag", - "sha256:333224a233db31852ac1085c6cd702016ab8aaf54cecde5c4bed5451d636adcf", - "test.io/test-namespace/test-repository:test-tag@sha256:333224a233db31852ac1085c6cd702016ab8aaf54cecde5c4bed5451d636adcf", - true, - "test.io/test-namespace/test-repository", - "test.io/test-namespace/test-repository", - "test.io/test-namespace/test-repository:test-tag@sha256:333224a233db31852ac1085c6cd702016ab8aaf54cecde5c4bed5451d636adcf", - "test.io/test-namespace/test-repository:test-tag@sha256:333224a233db31852ac1085c6cd702016ab8aaf54cecde5c4bed5451d636adcf", - "test-namespace", - "test-repository", - "optional.io/test-repository:test-tag@sha256:333224a233db31852ac1085c6cd702016ab8aaf54cecde5c4bed5451d636adcf", - "test.io/test-namespace/test-repository:test-tag@sha256:333224a233db31852ac1085c6cd702016ab8aaf54cecde5c4bed5451d636adcf" - ), - Arguments.of( - Named.of("without registry", - "test-namespace/test-repository:test-tag@sha256:333224a233db31852ac1085c6cd702016ab8aaf54cecde5c4bed5451d636adcf"), - null, - "optional.io", - "sha256:b048f7d88a830ba5b7c690193644f6baf658dde41d5d1e70d9f4bc275865a9a0", - null, - "test-namespace/test-repository", - "test-tag", - "sha256:333224a233db31852ac1085c6cd702016ab8aaf54cecde5c4bed5451d636adcf", - "test-namespace/test-repository:test-tag@sha256:333224a233db31852ac1085c6cd702016ab8aaf54cecde5c4bed5451d636adcf", - false, - "test-namespace/test-repository", - "optional.io/test-namespace/test-repository", - "test-namespace/test-repository:test-tag@sha256:333224a233db31852ac1085c6cd702016ab8aaf54cecde5c4bed5451d636adcf", - "optional.io/test-namespace/test-repository:test-tag@sha256:333224a233db31852ac1085c6cd702016ab8aaf54cecde5c4bed5451d636adcf", - "test-namespace", - "test-repository", - "optional.io/test-repository:test-tag@sha256:333224a233db31852ac1085c6cd702016ab8aaf54cecde5c4bed5451d636adcf", - "test-namespace/test-repository:test-tag@sha256:333224a233db31852ac1085c6cd702016ab8aaf54cecde5c4bed5451d636adcf" - ), - Arguments.of( - Named.of("without namepsace", - "test.io/test-repository:test-tag@sha256:333224a233db31852ac1085c6cd702016ab8aaf54cecde5c4bed5451d636adcf"), - null, - "optional.io", - "sha256:b048f7d88a830ba5b7c690193644f6baf658dde41d5d1e70d9f4bc275865a9a0", - "test.io", - "test-repository", - "test-tag", - "sha256:333224a233db31852ac1085c6cd702016ab8aaf54cecde5c4bed5451d636adcf", - "test.io/test-repository:test-tag@sha256:333224a233db31852ac1085c6cd702016ab8aaf54cecde5c4bed5451d636adcf", - true, - "test.io/test-repository", - "test.io/test-repository", - "test.io/test-repository:test-tag@sha256:333224a233db31852ac1085c6cd702016ab8aaf54cecde5c4bed5451d636adcf", - "test.io/test-repository:test-tag@sha256:333224a233db31852ac1085c6cd702016ab8aaf54cecde5c4bed5451d636adcf", - null, - "test-repository", - "optional.io/test-repository:test-tag@sha256:333224a233db31852ac1085c6cd702016ab8aaf54cecde5c4bed5451d636adcf", - "test.io/test-repository:test-tag@sha256:333224a233db31852ac1085c6cd702016ab8aaf54cecde5c4bed5451d636adcf" - ), - Arguments.of( - Named.of("without registry, namespace", - "test-repository:test-tag@sha256:333224a233db31852ac1085c6cd702016ab8aaf54cecde5c4bed5451d636adcf"), - null, - "optional.io", - "sha256:b048f7d88a830ba5b7c690193644f6baf658dde41d5d1e70d9f4bc275865a9a0", - null, - "test-repository", - "test-tag", - "sha256:333224a233db31852ac1085c6cd702016ab8aaf54cecde5c4bed5451d636adcf", - "test-repository:test-tag@sha256:333224a233db31852ac1085c6cd702016ab8aaf54cecde5c4bed5451d636adcf", - false, - "test-repository", - "optional.io/test-repository", - "test-repository:test-tag@sha256:333224a233db31852ac1085c6cd702016ab8aaf54cecde5c4bed5451d636adcf", - "optional.io/test-repository:test-tag@sha256:333224a233db31852ac1085c6cd702016ab8aaf54cecde5c4bed5451d636adcf", - null, - "test-repository", - "optional.io/test-repository:test-tag@sha256:333224a233db31852ac1085c6cd702016ab8aaf54cecde5c4bed5451d636adcf", - "test-repository:test-tag@sha256:333224a233db31852ac1085c6cd702016ab8aaf54cecde5c4bed5451d636adcf" - ), - Arguments.of( - Named.of("without registry, namespace, digest", - "test-repository:test-tag"), - null, - "optional.io", - "sha256:b048f7d88a830ba5b7c690193644f6baf658dde41d5d1e70d9f4bc275865a9a0", - null, - "test-repository", - "test-tag", - null, - "test-repository:test-tag", - false, - "test-repository", - "optional.io/test-repository", - "test-repository:test-tag", - "optional.io/test-repository:test-tag", - null, - "test-repository", - "optional.io/test-repository:test-tag", - "test-repository:test-tag@sha256:b048f7d88a830ba5b7c690193644f6baf658dde41d5d1e70d9f4bc275865a9a0" - ), - Arguments.of( - Named.of("without registry, namespace, tag", - "test-repository@sha256:333224a233db31852ac1085c6cd702016ab8aaf54cecde5c4bed5451d636adcf"), - null, - "optional.io", - "sha256:b048f7d88a830ba5b7c690193644f6baf658dde41d5d1e70d9f4bc275865a9a0", - null, - "test-repository", - null, - "sha256:333224a233db31852ac1085c6cd702016ab8aaf54cecde5c4bed5451d636adcf", - "test-repository@sha256:333224a233db31852ac1085c6cd702016ab8aaf54cecde5c4bed5451d636adcf", - false, - "test-repository", - "optional.io/test-repository", - "test-repository@sha256:333224a233db31852ac1085c6cd702016ab8aaf54cecde5c4bed5451d636adcf", - "optional.io/test-repository@sha256:333224a233db31852ac1085c6cd702016ab8aaf54cecde5c4bed5451d636adcf", - null, - "test-repository", - "optional.io/test-repository@sha256:333224a233db31852ac1085c6cd702016ab8aaf54cecde5c4bed5451d636adcf", - "test-repository@sha256:333224a233db31852ac1085c6cd702016ab8aaf54cecde5c4bed5451d636adcf" - ), - Arguments.of( - Named.of("without registry, namespace, tag, digest", - "test-repository"), - null, - "optional.io", - "sha256:b048f7d88a830ba5b7c690193644f6baf658dde41d5d1e70d9f4bc275865a9a0", - null, - "test-repository", - "latest", - null, - "test-repository:latest", - false, - "test-repository", - "optional.io/test-repository", - "test-repository:latest", - "optional.io/test-repository:latest", - null, - "test-repository", - "optional.io/test-repository:latest", - "test-repository:latest@sha256:b048f7d88a830ba5b7c690193644f6baf658dde41d5d1e70d9f4bc275865a9a0" - ), - Arguments.of( - Named.of("given tag, full name, host port", - "test-host:5000/test-namespace/test-repository:test-tag@sha256:333224a233db31852ac1085c6cd702016ab8aaf54cecde5c4bed5451d636adcf"), - "alt-tag", - "optional-host:2500", - "sha256:b048f7d88a830ba5b7c690193644f6baf658dde41d5d1e70d9f4bc275865a9a0", - "test-host:5000", - "test-namespace/test-repository", - "alt-tag", - "sha256:333224a233db31852ac1085c6cd702016ab8aaf54cecde5c4bed5451d636adcf", - "test-host:5000/test-namespace/test-repository:alt-tag@sha256:333224a233db31852ac1085c6cd702016ab8aaf54cecde5c4bed5451d636adcf", - true, - "test-host:5000/test-namespace/test-repository", - "test-host:5000/test-namespace/test-repository", - "test-host:5000/test-namespace/test-repository:alt-tag@sha256:333224a233db31852ac1085c6cd702016ab8aaf54cecde5c4bed5451d636adcf", - "test-host:5000/test-namespace/test-repository:alt-tag@sha256:333224a233db31852ac1085c6cd702016ab8aaf54cecde5c4bed5451d636adcf", - "test-namespace", - "test-repository", - "optional-host:2500/test-repository:alt-tag@sha256:333224a233db31852ac1085c6cd702016ab8aaf54cecde5c4bed5451d636adcf", - "test-host:5000/test-namespace/test-repository:alt-tag@sha256:333224a233db31852ac1085c6cd702016ab8aaf54cecde5c4bed5451d636adcf" - ), - Arguments.of( - Named.of("given tag, full name, registry", - "test.io/test-namespace/test-repository:test-tag@sha256:333224a233db31852ac1085c6cd702016ab8aaf54cecde5c4bed5451d636adcf"), - "alt-tag", - "optional.io", - "sha256:b048f7d88a830ba5b7c690193644f6baf658dde41d5d1e70d9f4bc275865a9a0", - "test.io", - "test-namespace/test-repository", - "alt-tag", - "sha256:333224a233db31852ac1085c6cd702016ab8aaf54cecde5c4bed5451d636adcf", - "test.io/test-namespace/test-repository:alt-tag@sha256:333224a233db31852ac1085c6cd702016ab8aaf54cecde5c4bed5451d636adcf", - true, - "test.io/test-namespace/test-repository", - "test.io/test-namespace/test-repository", - "test.io/test-namespace/test-repository:alt-tag@sha256:333224a233db31852ac1085c6cd702016ab8aaf54cecde5c4bed5451d636adcf", - "test.io/test-namespace/test-repository:alt-tag@sha256:333224a233db31852ac1085c6cd702016ab8aaf54cecde5c4bed5451d636adcf", - "test-namespace", - "test-repository", - "optional.io/test-repository:alt-tag@sha256:333224a233db31852ac1085c6cd702016ab8aaf54cecde5c4bed5451d636adcf", - "test.io/test-namespace/test-repository:alt-tag@sha256:333224a233db31852ac1085c6cd702016ab8aaf54cecde5c4bed5451d636adcf" - ), - Arguments.of( - Named.of("given tag, without registry", - "test-namespace/test-repository:test-tag@sha256:333224a233db31852ac1085c6cd702016ab8aaf54cecde5c4bed5451d636adcf"), - "alt-tag", - "optional.io", - "sha256:b048f7d88a830ba5b7c690193644f6baf658dde41d5d1e70d9f4bc275865a9a0", - null, - "test-namespace/test-repository", - "alt-tag", - "sha256:333224a233db31852ac1085c6cd702016ab8aaf54cecde5c4bed5451d636adcf", - "test-namespace/test-repository:alt-tag@sha256:333224a233db31852ac1085c6cd702016ab8aaf54cecde5c4bed5451d636adcf", - false, - "test-namespace/test-repository", - "optional.io/test-namespace/test-repository", - "test-namespace/test-repository:alt-tag@sha256:333224a233db31852ac1085c6cd702016ab8aaf54cecde5c4bed5451d636adcf", - "optional.io/test-namespace/test-repository:alt-tag@sha256:333224a233db31852ac1085c6cd702016ab8aaf54cecde5c4bed5451d636adcf", - "test-namespace", - "test-repository", - "optional.io/test-repository:alt-tag@sha256:333224a233db31852ac1085c6cd702016ab8aaf54cecde5c4bed5451d636adcf", - "test-namespace/test-repository:alt-tag@sha256:333224a233db31852ac1085c6cd702016ab8aaf54cecde5c4bed5451d636adcf" - ), - Arguments.of( - Named.of("given tag, without namepsace", - "test.io/test-repository:test-tag@sha256:333224a233db31852ac1085c6cd702016ab8aaf54cecde5c4bed5451d636adcf"), - "alt-tag", - "optional.io", - "sha256:b048f7d88a830ba5b7c690193644f6baf658dde41d5d1e70d9f4bc275865a9a0", - "test.io", - "test-repository", - "alt-tag", - "sha256:333224a233db31852ac1085c6cd702016ab8aaf54cecde5c4bed5451d636adcf", - "test.io/test-repository:alt-tag@sha256:333224a233db31852ac1085c6cd702016ab8aaf54cecde5c4bed5451d636adcf", - true, - "test.io/test-repository", - "test.io/test-repository", - "test.io/test-repository:alt-tag@sha256:333224a233db31852ac1085c6cd702016ab8aaf54cecde5c4bed5451d636adcf", - "test.io/test-repository:alt-tag@sha256:333224a233db31852ac1085c6cd702016ab8aaf54cecde5c4bed5451d636adcf", - null, - "test-repository", - "optional.io/test-repository:alt-tag@sha256:333224a233db31852ac1085c6cd702016ab8aaf54cecde5c4bed5451d636adcf", - "test.io/test-repository:alt-tag@sha256:333224a233db31852ac1085c6cd702016ab8aaf54cecde5c4bed5451d636adcf" - ), - Arguments.of( - Named.of("given tag, without registry, namespace", - "test-repository:test-tag@sha256:333224a233db31852ac1085c6cd702016ab8aaf54cecde5c4bed5451d636adcf"), - "alt-tag", - "optional.io", - "sha256:b048f7d88a830ba5b7c690193644f6baf658dde41d5d1e70d9f4bc275865a9a0", - null, - "test-repository", - "alt-tag", - "sha256:333224a233db31852ac1085c6cd702016ab8aaf54cecde5c4bed5451d636adcf", - "test-repository:alt-tag@sha256:333224a233db31852ac1085c6cd702016ab8aaf54cecde5c4bed5451d636adcf", - false, - "test-repository", - "optional.io/test-repository", - "test-repository:alt-tag@sha256:333224a233db31852ac1085c6cd702016ab8aaf54cecde5c4bed5451d636adcf", - "optional.io/test-repository:alt-tag@sha256:333224a233db31852ac1085c6cd702016ab8aaf54cecde5c4bed5451d636adcf", - null, - "test-repository", - "optional.io/test-repository:alt-tag@sha256:333224a233db31852ac1085c6cd702016ab8aaf54cecde5c4bed5451d636adcf", - "test-repository:alt-tag@sha256:333224a233db31852ac1085c6cd702016ab8aaf54cecde5c4bed5451d636adcf" - ), - Arguments.of( - Named.of("given tag, without registry, namespace, digest", - "test-repository:test-tag"), - "alt-tag", - "optional.io", - "sha256:b048f7d88a830ba5b7c690193644f6baf658dde41d5d1e70d9f4bc275865a9a0", - null, - "test-repository", - "alt-tag", - null, - "test-repository:alt-tag", - false, - "test-repository", - "optional.io/test-repository", - "test-repository:alt-tag", - "optional.io/test-repository:alt-tag", - null, - "test-repository", - "optional.io/test-repository:alt-tag", - "test-repository:alt-tag@sha256:b048f7d88a830ba5b7c690193644f6baf658dde41d5d1e70d9f4bc275865a9a0" - ), - Arguments.of( - Named.of("given tag, without registry, namespace, tag", - "test-repository@sha256:333224a233db31852ac1085c6cd702016ab8aaf54cecde5c4bed5451d636adcf"), - "alt-tag", - "optional.io", - "sha256:b048f7d88a830ba5b7c690193644f6baf658dde41d5d1e70d9f4bc275865a9a0", - null, - "test-repository", - "alt-tag", - "sha256:333224a233db31852ac1085c6cd702016ab8aaf54cecde5c4bed5451d636adcf", - "test-repository:alt-tag@sha256:333224a233db31852ac1085c6cd702016ab8aaf54cecde5c4bed5451d636adcf", - false, - "test-repository", - "optional.io/test-repository", - "test-repository:alt-tag@sha256:333224a233db31852ac1085c6cd702016ab8aaf54cecde5c4bed5451d636adcf", - "optional.io/test-repository:alt-tag@sha256:333224a233db31852ac1085c6cd702016ab8aaf54cecde5c4bed5451d636adcf", - null, - "test-repository", - "optional.io/test-repository:alt-tag@sha256:333224a233db31852ac1085c6cd702016ab8aaf54cecde5c4bed5451d636adcf", - "test-repository:alt-tag@sha256:333224a233db31852ac1085c6cd702016ab8aaf54cecde5c4bed5451d636adcf" - ), - Arguments.of( - Named.of("given tag, without registry, namespace, tag, digest", - "test-repository"), - "alt-tag", - "optional.io", - "sha256:b048f7d88a830ba5b7c690193644f6baf658dde41d5d1e70d9f4bc275865a9a0", - null, - "test-repository", - "alt-tag", - null, - "test-repository:alt-tag", - false, - "test-repository", - "optional.io/test-repository", - "test-repository:alt-tag", - "optional.io/test-repository:alt-tag", - null, - "test-repository", - "optional.io/test-repository:alt-tag", - "test-repository:alt-tag@sha256:b048f7d88a830ba5b7c690193644f6baf658dde41d5d1e70d9f4bc275865a9a0" - ) - ); - } + static Stream imageTestSources() { + return Stream.of( + Arguments.of( + Named.of( + "full name, host port", + "test-host:5000/test-namespace/test-repository:test-tag@sha256:333224a233db31852ac1085c6cd702016ab8aaf54cecde5c4bed5451d636adcf"), + null, + "optional-host:2500", + "sha256:b048f7d88a830ba5b7c690193644f6baf658dde41d5d1e70d9f4bc275865a9a0", + "test-host:5000", + "test-namespace/test-repository", + "test-tag", + "sha256:333224a233db31852ac1085c6cd702016ab8aaf54cecde5c4bed5451d636adcf", + "test-host:5000/test-namespace/test-repository:test-tag@sha256:333224a233db31852ac1085c6cd702016ab8aaf54cecde5c4bed5451d636adcf", + true, + "test-host:5000/test-namespace/test-repository", + "test-host:5000/test-namespace/test-repository", + "test-host:5000/test-namespace/test-repository:test-tag@sha256:333224a233db31852ac1085c6cd702016ab8aaf54cecde5c4bed5451d636adcf", + "test-host:5000/test-namespace/test-repository:test-tag@sha256:333224a233db31852ac1085c6cd702016ab8aaf54cecde5c4bed5451d636adcf", + "test-namespace", + "test-repository", + "optional-host:2500/test-repository:test-tag@sha256:333224a233db31852ac1085c6cd702016ab8aaf54cecde5c4bed5451d636adcf", + "test-host:5000/test-namespace/test-repository:test-tag@sha256:333224a233db31852ac1085c6cd702016ab8aaf54cecde5c4bed5451d636adcf"), + Arguments.of( + Named.of( + "full name, registry", + "test.io/test-namespace/test-repository:test-tag@sha256:333224a233db31852ac1085c6cd702016ab8aaf54cecde5c4bed5451d636adcf"), + null, + "optional.io", + "sha256:b048f7d88a830ba5b7c690193644f6baf658dde41d5d1e70d9f4bc275865a9a0", + "test.io", + "test-namespace/test-repository", + "test-tag", + "sha256:333224a233db31852ac1085c6cd702016ab8aaf54cecde5c4bed5451d636adcf", + "test.io/test-namespace/test-repository:test-tag@sha256:333224a233db31852ac1085c6cd702016ab8aaf54cecde5c4bed5451d636adcf", + true, + "test.io/test-namespace/test-repository", + "test.io/test-namespace/test-repository", + "test.io/test-namespace/test-repository:test-tag@sha256:333224a233db31852ac1085c6cd702016ab8aaf54cecde5c4bed5451d636adcf", + "test.io/test-namespace/test-repository:test-tag@sha256:333224a233db31852ac1085c6cd702016ab8aaf54cecde5c4bed5451d636adcf", + "test-namespace", + "test-repository", + "optional.io/test-repository:test-tag@sha256:333224a233db31852ac1085c6cd702016ab8aaf54cecde5c4bed5451d636adcf", + "test.io/test-namespace/test-repository:test-tag@sha256:333224a233db31852ac1085c6cd702016ab8aaf54cecde5c4bed5451d636adcf"), + Arguments.of( + Named.of( + "without registry", + "test-namespace/test-repository:test-tag@sha256:333224a233db31852ac1085c6cd702016ab8aaf54cecde5c4bed5451d636adcf"), + null, + "optional.io", + "sha256:b048f7d88a830ba5b7c690193644f6baf658dde41d5d1e70d9f4bc275865a9a0", + null, + "test-namespace/test-repository", + "test-tag", + "sha256:333224a233db31852ac1085c6cd702016ab8aaf54cecde5c4bed5451d636adcf", + "test-namespace/test-repository:test-tag@sha256:333224a233db31852ac1085c6cd702016ab8aaf54cecde5c4bed5451d636adcf", + false, + "test-namespace/test-repository", + "optional.io/test-namespace/test-repository", + "test-namespace/test-repository:test-tag@sha256:333224a233db31852ac1085c6cd702016ab8aaf54cecde5c4bed5451d636adcf", + "optional.io/test-namespace/test-repository:test-tag@sha256:333224a233db31852ac1085c6cd702016ab8aaf54cecde5c4bed5451d636adcf", + "test-namespace", + "test-repository", + "optional.io/test-repository:test-tag@sha256:333224a233db31852ac1085c6cd702016ab8aaf54cecde5c4bed5451d636adcf", + "test-namespace/test-repository:test-tag@sha256:333224a233db31852ac1085c6cd702016ab8aaf54cecde5c4bed5451d636adcf"), + Arguments.of( + Named.of( + "without namepsace", + "test.io/test-repository:test-tag@sha256:333224a233db31852ac1085c6cd702016ab8aaf54cecde5c4bed5451d636adcf"), + null, + "optional.io", + "sha256:b048f7d88a830ba5b7c690193644f6baf658dde41d5d1e70d9f4bc275865a9a0", + "test.io", + "test-repository", + "test-tag", + "sha256:333224a233db31852ac1085c6cd702016ab8aaf54cecde5c4bed5451d636adcf", + "test.io/test-repository:test-tag@sha256:333224a233db31852ac1085c6cd702016ab8aaf54cecde5c4bed5451d636adcf", + true, + "test.io/test-repository", + "test.io/test-repository", + "test.io/test-repository:test-tag@sha256:333224a233db31852ac1085c6cd702016ab8aaf54cecde5c4bed5451d636adcf", + "test.io/test-repository:test-tag@sha256:333224a233db31852ac1085c6cd702016ab8aaf54cecde5c4bed5451d636adcf", + null, + "test-repository", + "optional.io/test-repository:test-tag@sha256:333224a233db31852ac1085c6cd702016ab8aaf54cecde5c4bed5451d636adcf", + "test.io/test-repository:test-tag@sha256:333224a233db31852ac1085c6cd702016ab8aaf54cecde5c4bed5451d636adcf"), + Arguments.of( + Named.of( + "without registry, namespace", + "test-repository:test-tag@sha256:333224a233db31852ac1085c6cd702016ab8aaf54cecde5c4bed5451d636adcf"), + null, + "optional.io", + "sha256:b048f7d88a830ba5b7c690193644f6baf658dde41d5d1e70d9f4bc275865a9a0", + null, + "test-repository", + "test-tag", + "sha256:333224a233db31852ac1085c6cd702016ab8aaf54cecde5c4bed5451d636adcf", + "test-repository:test-tag@sha256:333224a233db31852ac1085c6cd702016ab8aaf54cecde5c4bed5451d636adcf", + false, + "test-repository", + "optional.io/test-repository", + "test-repository:test-tag@sha256:333224a233db31852ac1085c6cd702016ab8aaf54cecde5c4bed5451d636adcf", + "optional.io/test-repository:test-tag@sha256:333224a233db31852ac1085c6cd702016ab8aaf54cecde5c4bed5451d636adcf", + null, + "test-repository", + "optional.io/test-repository:test-tag@sha256:333224a233db31852ac1085c6cd702016ab8aaf54cecde5c4bed5451d636adcf", + "test-repository:test-tag@sha256:333224a233db31852ac1085c6cd702016ab8aaf54cecde5c4bed5451d636adcf"), + Arguments.of( + Named.of("without registry, namespace, digest", "test-repository:test-tag"), + null, + "optional.io", + "sha256:b048f7d88a830ba5b7c690193644f6baf658dde41d5d1e70d9f4bc275865a9a0", + null, + "test-repository", + "test-tag", + null, + "test-repository:test-tag", + false, + "test-repository", + "optional.io/test-repository", + "test-repository:test-tag", + "optional.io/test-repository:test-tag", + null, + "test-repository", + "optional.io/test-repository:test-tag", + "test-repository:test-tag@sha256:b048f7d88a830ba5b7c690193644f6baf658dde41d5d1e70d9f4bc275865a9a0"), + Arguments.of( + Named.of( + "without registry, namespace, tag", + "test-repository@sha256:333224a233db31852ac1085c6cd702016ab8aaf54cecde5c4bed5451d636adcf"), + null, + "optional.io", + "sha256:b048f7d88a830ba5b7c690193644f6baf658dde41d5d1e70d9f4bc275865a9a0", + null, + "test-repository", + null, + "sha256:333224a233db31852ac1085c6cd702016ab8aaf54cecde5c4bed5451d636adcf", + "test-repository@sha256:333224a233db31852ac1085c6cd702016ab8aaf54cecde5c4bed5451d636adcf", + false, + "test-repository", + "optional.io/test-repository", + "test-repository@sha256:333224a233db31852ac1085c6cd702016ab8aaf54cecde5c4bed5451d636adcf", + "optional.io/test-repository@sha256:333224a233db31852ac1085c6cd702016ab8aaf54cecde5c4bed5451d636adcf", + null, + "test-repository", + "optional.io/test-repository@sha256:333224a233db31852ac1085c6cd702016ab8aaf54cecde5c4bed5451d636adcf", + "test-repository@sha256:333224a233db31852ac1085c6cd702016ab8aaf54cecde5c4bed5451d636adcf"), + Arguments.of( + Named.of("without registry, namespace, tag, digest", "test-repository"), + null, + "optional.io", + "sha256:b048f7d88a830ba5b7c690193644f6baf658dde41d5d1e70d9f4bc275865a9a0", + null, + "test-repository", + "latest", + null, + "test-repository:latest", + false, + "test-repository", + "optional.io/test-repository", + "test-repository:latest", + "optional.io/test-repository:latest", + null, + "test-repository", + "optional.io/test-repository:latest", + "test-repository:latest@sha256:b048f7d88a830ba5b7c690193644f6baf658dde41d5d1e70d9f4bc275865a9a0"), + Arguments.of( + Named.of( + "given tag, full name, host port", + "test-host:5000/test-namespace/test-repository:test-tag@sha256:333224a233db31852ac1085c6cd702016ab8aaf54cecde5c4bed5451d636adcf"), + "alt-tag", + "optional-host:2500", + "sha256:b048f7d88a830ba5b7c690193644f6baf658dde41d5d1e70d9f4bc275865a9a0", + "test-host:5000", + "test-namespace/test-repository", + "alt-tag", + "sha256:333224a233db31852ac1085c6cd702016ab8aaf54cecde5c4bed5451d636adcf", + "test-host:5000/test-namespace/test-repository:alt-tag@sha256:333224a233db31852ac1085c6cd702016ab8aaf54cecde5c4bed5451d636adcf", + true, + "test-host:5000/test-namespace/test-repository", + "test-host:5000/test-namespace/test-repository", + "test-host:5000/test-namespace/test-repository:alt-tag@sha256:333224a233db31852ac1085c6cd702016ab8aaf54cecde5c4bed5451d636adcf", + "test-host:5000/test-namespace/test-repository:alt-tag@sha256:333224a233db31852ac1085c6cd702016ab8aaf54cecde5c4bed5451d636adcf", + "test-namespace", + "test-repository", + "optional-host:2500/test-repository:alt-tag@sha256:333224a233db31852ac1085c6cd702016ab8aaf54cecde5c4bed5451d636adcf", + "test-host:5000/test-namespace/test-repository:alt-tag@sha256:333224a233db31852ac1085c6cd702016ab8aaf54cecde5c4bed5451d636adcf"), + Arguments.of( + Named.of( + "given tag, full name, registry", + "test.io/test-namespace/test-repository:test-tag@sha256:333224a233db31852ac1085c6cd702016ab8aaf54cecde5c4bed5451d636adcf"), + "alt-tag", + "optional.io", + "sha256:b048f7d88a830ba5b7c690193644f6baf658dde41d5d1e70d9f4bc275865a9a0", + "test.io", + "test-namespace/test-repository", + "alt-tag", + "sha256:333224a233db31852ac1085c6cd702016ab8aaf54cecde5c4bed5451d636adcf", + "test.io/test-namespace/test-repository:alt-tag@sha256:333224a233db31852ac1085c6cd702016ab8aaf54cecde5c4bed5451d636adcf", + true, + "test.io/test-namespace/test-repository", + "test.io/test-namespace/test-repository", + "test.io/test-namespace/test-repository:alt-tag@sha256:333224a233db31852ac1085c6cd702016ab8aaf54cecde5c4bed5451d636adcf", + "test.io/test-namespace/test-repository:alt-tag@sha256:333224a233db31852ac1085c6cd702016ab8aaf54cecde5c4bed5451d636adcf", + "test-namespace", + "test-repository", + "optional.io/test-repository:alt-tag@sha256:333224a233db31852ac1085c6cd702016ab8aaf54cecde5c4bed5451d636adcf", + "test.io/test-namespace/test-repository:alt-tag@sha256:333224a233db31852ac1085c6cd702016ab8aaf54cecde5c4bed5451d636adcf"), + Arguments.of( + Named.of( + "given tag, without registry", + "test-namespace/test-repository:test-tag@sha256:333224a233db31852ac1085c6cd702016ab8aaf54cecde5c4bed5451d636adcf"), + "alt-tag", + "optional.io", + "sha256:b048f7d88a830ba5b7c690193644f6baf658dde41d5d1e70d9f4bc275865a9a0", + null, + "test-namespace/test-repository", + "alt-tag", + "sha256:333224a233db31852ac1085c6cd702016ab8aaf54cecde5c4bed5451d636adcf", + "test-namespace/test-repository:alt-tag@sha256:333224a233db31852ac1085c6cd702016ab8aaf54cecde5c4bed5451d636adcf", + false, + "test-namespace/test-repository", + "optional.io/test-namespace/test-repository", + "test-namespace/test-repository:alt-tag@sha256:333224a233db31852ac1085c6cd702016ab8aaf54cecde5c4bed5451d636adcf", + "optional.io/test-namespace/test-repository:alt-tag@sha256:333224a233db31852ac1085c6cd702016ab8aaf54cecde5c4bed5451d636adcf", + "test-namespace", + "test-repository", + "optional.io/test-repository:alt-tag@sha256:333224a233db31852ac1085c6cd702016ab8aaf54cecde5c4bed5451d636adcf", + "test-namespace/test-repository:alt-tag@sha256:333224a233db31852ac1085c6cd702016ab8aaf54cecde5c4bed5451d636adcf"), + Arguments.of( + Named.of( + "given tag, without namepsace", + "test.io/test-repository:test-tag@sha256:333224a233db31852ac1085c6cd702016ab8aaf54cecde5c4bed5451d636adcf"), + "alt-tag", + "optional.io", + "sha256:b048f7d88a830ba5b7c690193644f6baf658dde41d5d1e70d9f4bc275865a9a0", + "test.io", + "test-repository", + "alt-tag", + "sha256:333224a233db31852ac1085c6cd702016ab8aaf54cecde5c4bed5451d636adcf", + "test.io/test-repository:alt-tag@sha256:333224a233db31852ac1085c6cd702016ab8aaf54cecde5c4bed5451d636adcf", + true, + "test.io/test-repository", + "test.io/test-repository", + "test.io/test-repository:alt-tag@sha256:333224a233db31852ac1085c6cd702016ab8aaf54cecde5c4bed5451d636adcf", + "test.io/test-repository:alt-tag@sha256:333224a233db31852ac1085c6cd702016ab8aaf54cecde5c4bed5451d636adcf", + null, + "test-repository", + "optional.io/test-repository:alt-tag@sha256:333224a233db31852ac1085c6cd702016ab8aaf54cecde5c4bed5451d636adcf", + "test.io/test-repository:alt-tag@sha256:333224a233db31852ac1085c6cd702016ab8aaf54cecde5c4bed5451d636adcf"), + Arguments.of( + Named.of( + "given tag, without registry, namespace", + "test-repository:test-tag@sha256:333224a233db31852ac1085c6cd702016ab8aaf54cecde5c4bed5451d636adcf"), + "alt-tag", + "optional.io", + "sha256:b048f7d88a830ba5b7c690193644f6baf658dde41d5d1e70d9f4bc275865a9a0", + null, + "test-repository", + "alt-tag", + "sha256:333224a233db31852ac1085c6cd702016ab8aaf54cecde5c4bed5451d636adcf", + "test-repository:alt-tag@sha256:333224a233db31852ac1085c6cd702016ab8aaf54cecde5c4bed5451d636adcf", + false, + "test-repository", + "optional.io/test-repository", + "test-repository:alt-tag@sha256:333224a233db31852ac1085c6cd702016ab8aaf54cecde5c4bed5451d636adcf", + "optional.io/test-repository:alt-tag@sha256:333224a233db31852ac1085c6cd702016ab8aaf54cecde5c4bed5451d636adcf", + null, + "test-repository", + "optional.io/test-repository:alt-tag@sha256:333224a233db31852ac1085c6cd702016ab8aaf54cecde5c4bed5451d636adcf", + "test-repository:alt-tag@sha256:333224a233db31852ac1085c6cd702016ab8aaf54cecde5c4bed5451d636adcf"), + Arguments.of( + Named.of("given tag, without registry, namespace, digest", "test-repository:test-tag"), + "alt-tag", + "optional.io", + "sha256:b048f7d88a830ba5b7c690193644f6baf658dde41d5d1e70d9f4bc275865a9a0", + null, + "test-repository", + "alt-tag", + null, + "test-repository:alt-tag", + false, + "test-repository", + "optional.io/test-repository", + "test-repository:alt-tag", + "optional.io/test-repository:alt-tag", + null, + "test-repository", + "optional.io/test-repository:alt-tag", + "test-repository:alt-tag@sha256:b048f7d88a830ba5b7c690193644f6baf658dde41d5d1e70d9f4bc275865a9a0"), + Arguments.of( + Named.of( + "given tag, without registry, namespace, tag", + "test-repository@sha256:333224a233db31852ac1085c6cd702016ab8aaf54cecde5c4bed5451d636adcf"), + "alt-tag", + "optional.io", + "sha256:b048f7d88a830ba5b7c690193644f6baf658dde41d5d1e70d9f4bc275865a9a0", + null, + "test-repository", + "alt-tag", + "sha256:333224a233db31852ac1085c6cd702016ab8aaf54cecde5c4bed5451d636adcf", + "test-repository:alt-tag@sha256:333224a233db31852ac1085c6cd702016ab8aaf54cecde5c4bed5451d636adcf", + false, + "test-repository", + "optional.io/test-repository", + "test-repository:alt-tag@sha256:333224a233db31852ac1085c6cd702016ab8aaf54cecde5c4bed5451d636adcf", + "optional.io/test-repository:alt-tag@sha256:333224a233db31852ac1085c6cd702016ab8aaf54cecde5c4bed5451d636adcf", + null, + "test-repository", + "optional.io/test-repository:alt-tag@sha256:333224a233db31852ac1085c6cd702016ab8aaf54cecde5c4bed5451d636adcf", + "test-repository:alt-tag@sha256:333224a233db31852ac1085c6cd702016ab8aaf54cecde5c4bed5451d636adcf"), + Arguments.of( + Named.of("given tag, without registry, namespace, tag, digest", "test-repository"), + "alt-tag", + "optional.io", + "sha256:b048f7d88a830ba5b7c690193644f6baf658dde41d5d1e70d9f4bc275865a9a0", + null, + "test-repository", + "alt-tag", + null, + "test-repository:alt-tag", + false, + "test-repository", + "optional.io/test-repository", + "test-repository:alt-tag", + "optional.io/test-repository:alt-tag", + null, + "test-repository", + "optional.io/test-repository:alt-tag", + "test-repository:alt-tag@sha256:b048f7d88a830ba5b7c690193644f6baf658dde41d5d1e70d9f4bc275865a9a0")); + } - @ParameterizedTest(name = "{0}") - @MethodSource("imageTestSources") - void test_image_no_tag(String fullName, - String givenTag, - String optionalRegistry, - String optionalDigest, - String expectedRegistry, - String expectedRepository, - String expectedTag, - String expectedDigest, - String expectedString, - boolean expectedHasRegistry, - String expectedNameWithoutTag, - String expectedNameWithoutTagOptionalRegistry, - String expectedFullName, - String expectedFullNameOptionalRegistry, - String expectedUser, - String expectedSimpleName, - String expectedNameWithOptionalRepository, - String expectedFullNameOptionalDigest) { + @ParameterizedTest(name = "{0}") + @MethodSource("imageTestSources") + void test_image_no_tag( + String fullName, + String givenTag, + String optionalRegistry, + String optionalDigest, + String expectedRegistry, + String expectedRepository, + String expectedTag, + String expectedDigest, + String expectedString, + boolean expectedHasRegistry, + String expectedNameWithoutTag, + String expectedNameWithoutTagOptionalRegistry, + String expectedFullName, + String expectedFullNameOptionalRegistry, + String expectedUser, + String expectedSimpleName, + String expectedNameWithOptionalRepository, + String expectedFullNameOptionalDigest) { - var image = givenTag == null ? new Image(fullName) : new Image(fullName, givenTag); + var image = givenTag == null ? new Image(fullName) : new Image(fullName, givenTag); - assertEquals(expectedRegistry, image.getRegistry()); - assertEquals(expectedRepository, image.getRepository()); - assertEquals(expectedTag, image.getTag()); - assertEquals(expectedDigest, image.getDigest()); - assertEquals(expectedString, image.toString()); - assertEquals(expectedHasRegistry, image.hasRegistry()); - assertEquals(expectedNameWithoutTag, image.getNameWithoutTag()); - assertEquals(expectedNameWithoutTagOptionalRegistry, image.getNameWithoutTag(optionalRegistry)); - assertEquals(expectedFullName, image.getFullName()); - assertEquals(expectedFullNameOptionalRegistry, image.getFullName(optionalRegistry)); - assertEquals(expectedUser, image.getUser()); - assertEquals(expectedSimpleName, image.getSimpleName()); - assertEquals(expectedNameWithOptionalRepository, image.getNameWithOptionalRepository(optionalRegistry)); - assertEquals(expectedFullName, image.getNameWithOptionalRepository(null)); + assertEquals(expectedRegistry, image.getRegistry()); + assertEquals(expectedRepository, image.getRepository()); + assertEquals(expectedTag, image.getTag()); + assertEquals(expectedDigest, image.getDigest()); + assertEquals(expectedString, image.toString()); + assertEquals(expectedHasRegistry, image.hasRegistry()); + assertEquals(expectedNameWithoutTag, image.getNameWithoutTag()); + assertEquals(expectedNameWithoutTagOptionalRegistry, image.getNameWithoutTag(optionalRegistry)); + assertEquals(expectedFullName, image.getFullName()); + assertEquals(expectedFullNameOptionalRegistry, image.getFullName(optionalRegistry)); + assertEquals(expectedUser, image.getUser()); + assertEquals(expectedSimpleName, image.getSimpleName()); + assertEquals(expectedNameWithOptionalRepository, image.getNameWithOptionalRepository(optionalRegistry)); + assertEquals(expectedFullName, image.getNameWithOptionalRepository(null)); - image.setDigest(optionalDigest); - assertEquals(expectedFullNameOptionalDigest, image.getFullName()); - } + image.setDigest(optionalDigest); + assertEquals(expectedFullNameOptionalDigest, image.getFullName()); + } - @Test - void test_equals() { - var image1 = new Image("test-image"); - var image2 = new Image("test-image:latest"); - var image3 = new Image("test-image:old"); + @Test + void test_equals() { + var image1 = new Image("test-image"); + var image2 = new Image("test-image:latest"); + var image3 = new Image("test-image:old"); - assertTrue(image1.equals(image2)); - assertFalse(image2.equals(image3)); - } + assertTrue(image1.equals(image2)); + assertFalse(image2.equals(image3)); + } - @Test - void test_hashCode() { - var image1 = new Image("test-image"); - var image2 = new Image("test-image:latest"); - var image3 = new Image("test-image:old"); + @Test + void test_hashCode() { + var image1 = new Image("test-image"); + var image2 = new Image("test-image:latest"); + var image3 = new Image("test-image:old"); - assertEquals(image1.hashCode(), image2.hashCode()); - assertNotEquals(image2.hashCode(), image3.hashCode()); - } + assertEquals(image1.hashCode(), image2.hashCode()); + assertNotEquals(image2.hashCode(), image3.hashCode()); + } - @Test - void test_image_null() { - var expectedMessage = "Image name must not be null"; + @Test + void test_image_null() { + var expectedMessage = "Image name must not be null"; - var exception1 = assertThrows(NullPointerException.class, () -> { - new Image(null); - }); - assertEquals(expectedMessage, exception1.getMessage()); + var exception1 = assertThrows(NullPointerException.class, () -> { + new Image(null); + }); + assertEquals(expectedMessage, exception1.getMessage()); - var exception2 = assertThrows(NullPointerException.class, () -> { - new Image(null, "test"); - }); - assertEquals(expectedMessage, exception2.getMessage()); + var exception2 = assertThrows(NullPointerException.class, () -> { + new Image(null, "test"); + }); + assertEquals(expectedMessage, exception2.getMessage()); - var exception3 = assertThrows(NullPointerException.class, () -> { - Image.validate(null); - }); - assertEquals(expectedMessage, exception3.getMessage()); - } + var exception3 = assertThrows(NullPointerException.class, () -> { + Image.validate(null); + }); + assertEquals(expectedMessage, exception3.getMessage()); + } - @Test - void test_image_invalid() { - var imageName = ""; - var expectedMessage = imageName + " is not a proper image name ([registry/][repo][:port]"; + @Test + void test_image_invalid() { + var imageName = ""; + var expectedMessage = imageName + " is not a proper image name ([registry/][repo][:port]"; - var exception1 = assertThrows(IllegalArgumentException.class, () -> { - new Image(imageName); - }); - assertEquals(expectedMessage, exception1.getMessage()); + var exception1 = assertThrows(IllegalArgumentException.class, () -> { + new Image(imageName); + }); + assertEquals(expectedMessage, exception1.getMessage()); - var exception2 = assertThrows(IllegalArgumentException.class, () -> { - new Image(imageName, "test"); - }); - assertEquals(expectedMessage, exception2.getMessage()); + var exception2 = assertThrows(IllegalArgumentException.class, () -> { + new Image(imageName, "test"); + }); + assertEquals(expectedMessage, exception2.getMessage()); - var exception3 = assertThrows(IllegalArgumentException.class, () -> { - Image.validate(imageName); - }); - assertEquals(expectedMessage, exception3.getMessage()); - } + var exception3 = assertThrows(IllegalArgumentException.class, () -> { + Image.validate(imageName); + }); + assertEquals(expectedMessage, exception3.getMessage()); + } - @Test - void test_all_invalid() { - var imageName = "%&^.%*/*(*&(/&(&(&:&^*&@sha256:333224A233DB31852AC1085C6CD702016AB8AAF54CECDE5C4BED5451D636ADCF"; - var expectedMessage = "Given Docker name '%&^.%*/*(*&(/&(&(&:&^*&@sha256:333224A233DB31852AC1085C6CD702016AB8AAF54CECDE5C4BED5451D636ADCF' is invalid:\n" + - " * registry part '%&^.%*' doesn't match allowed pattern '^(?:[a-zA-Z0-9]|[a-zA-Z0-9][a-zA-Z0-9-]*[a-zA-Z0-9])(?:\\.(?:[a-zA-Z0-9]|[a-zA-Z0-9][a-zA-Z0-9-]*[a-zA-Z0-9]))*(?::[0-9]+)?$'\n" + - " * image part '&(&(&' doesn't match allowed pattern '[a-z0-9]+(?:(?:(?:[._]|__|[-]*)[a-z0-9]+)+)?(?:(?:/[a-z0-9]+(?:(?:(?:[._]|__|[-]*)[a-z0-9]+)+)?)+)?'\n" + - " * user part '*(*&(' doesn't match allowed pattern '[a-z0-9]+(?:(?:(?:[._]|__|[-]*)[a-z0-9]+)+)?'\n" + - " * tag part '&^*&' doesn't match allowed pattern '^[\\w][\\w.-]{0,127}$'\n" + - " * digest part 'sha256:333224A233DB31852AC1085C6CD702016AB8AAF54CECDE5C4BED5451D636ADCF' doesn't match allowed pattern '^sha256:[a-z0-9]{32,}$'\n" + - "See http://bit.ly/docker_image_fmt for more details"; + @Test + void test_all_invalid() { + var imageName = + "%&^.%*/*(*&(/&(&(&:&^*&@sha256:333224A233DB31852AC1085C6CD702016AB8AAF54CECDE5C4BED5451D636ADCF"; + var expectedMessage = + "Given Docker name '%&^.%*/*(*&(/&(&(&:&^*&@sha256:333224A233DB31852AC1085C6CD702016AB8AAF54CECDE5C4BED5451D636ADCF' is invalid:\n" + + " * registry part '%&^.%*' doesn't match allowed pattern '^(?:[a-zA-Z0-9]|[a-zA-Z0-9][a-zA-Z0-9-]*[a-zA-Z0-9])(?:\\.(?:[a-zA-Z0-9]|[a-zA-Z0-9][a-zA-Z0-9-]*[a-zA-Z0-9]))*(?::[0-9]+)?$'\n" + + " * image part '&(&(&' doesn't match allowed pattern '[a-z0-9]+(?:(?:(?:[._]|__|[-]*)[a-z0-9]+)+)?(?:(?:/[a-z0-9]+(?:(?:(?:[._]|__|[-]*)[a-z0-9]+)+)?)+)?'\n" + + " * user part '*(*&(' doesn't match allowed pattern '[a-z0-9]+(?:(?:(?:[._]|__|[-]*)[a-z0-9]+)+)?'\n" + + " * tag part '&^*&' doesn't match allowed pattern '^[\\w][\\w.-]{0,127}$'\n" + + " * digest part 'sha256:333224A233DB31852AC1085C6CD702016AB8AAF54CECDE5C4BED5451D636ADCF' doesn't match allowed pattern '^sha256:[a-z0-9]{32,}$'\n" + + "See http://bit.ly/docker_image_fmt for more details"; - var exception1 = assertThrows(IllegalArgumentException.class, () -> { - new Image(imageName); - }); - assertEquals(expectedMessage, exception1.getMessage()); + var exception1 = assertThrows(IllegalArgumentException.class, () -> { + new Image(imageName); + }); + assertEquals(expectedMessage, exception1.getMessage()); - var exception2 = assertThrows(IllegalArgumentException.class, () -> { - new Image(imageName, "&^*&"); - }); - assertEquals(expectedMessage, exception2.getMessage()); + var exception2 = assertThrows(IllegalArgumentException.class, () -> { + new Image(imageName, "&^*&"); + }); + assertEquals(expectedMessage, exception2.getMessage()); - var exception3 = assertThrows(IllegalArgumentException.class, () -> { - Image.validate(imageName); - }); - assertEquals(expectedMessage, exception3.getMessage()); - } + var exception3 = assertThrows(IllegalArgumentException.class, () -> { + Image.validate(imageName); + }); + assertEquals(expectedMessage, exception3.getMessage()); + } } diff --git a/src/test/java/com/redhat/exhort/image/ImageUtilsTest.java b/src/test/java/com/redhat/exhort/image/ImageUtilsTest.java index c2bf5a0c..2788d0df 100644 --- a/src/test/java/com/redhat/exhort/image/ImageUtilsTest.java +++ b/src/test/java/com/redhat/exhort/image/ImageUtilsTest.java @@ -15,6 +15,23 @@ */ package com.redhat.exhort.image; +import static com.redhat.exhort.image.ImageUtils.EXHORT_IMAGE_ARCH; +import static com.redhat.exhort.image.ImageUtils.EXHORT_IMAGE_OS; +import static com.redhat.exhort.image.ImageUtils.EXHORT_IMAGE_PLATFORM; +import static com.redhat.exhort.image.ImageUtils.EXHORT_IMAGE_SERVICE_ENDPOINT; +import static com.redhat.exhort.image.ImageUtils.EXHORT_IMAGE_VARIANT; +import static com.redhat.exhort.image.ImageUtils.EXHORT_SKOPEO_CONFIG_PATH; +import static com.redhat.exhort.image.ImageUtils.EXHORT_SYFT_CONFIG_PATH; +import static com.redhat.exhort.image.ImageUtils.EXHORT_SYFT_IMAGE_SOURCE; +import static org.assertj.core.api.Assertions.assertThat; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNull; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.mockito.AdditionalMatchers.aryEq; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.ArgumentMatchers.isNull; + import com.fasterxml.jackson.core.JsonProcessingException; import com.fasterxml.jackson.databind.ObjectMapper; import com.fasterxml.jackson.databind.node.ObjectNode; @@ -22,16 +39,6 @@ import com.github.packageurl.MalformedPackageURLException; import com.redhat.exhort.ExhortTest; import com.redhat.exhort.tools.Operations; -import org.junit.jupiter.api.Named; -import org.junit.jupiter.api.Test; -import org.junit.jupiter.params.ParameterizedTest; -import org.junit.jupiter.params.provider.Arguments; -import org.junit.jupiter.params.provider.MethodSource; -import org.junitpioneer.jupiter.ClearEnvironmentVariable; -import org.junitpioneer.jupiter.SetEnvironmentVariable; -import org.mockito.MockedStatic; -import org.mockito.Mockito; - import java.io.BufferedReader; import java.io.File; import java.io.IOException; @@ -43,854 +50,907 @@ import java.util.Map; import java.util.stream.Collectors; import java.util.stream.Stream; - -import static com.redhat.exhort.image.ImageUtils.EXHORT_IMAGE_ARCH; -import static com.redhat.exhort.image.ImageUtils.EXHORT_IMAGE_OS; -import static com.redhat.exhort.image.ImageUtils.EXHORT_IMAGE_PLATFORM; -import static com.redhat.exhort.image.ImageUtils.EXHORT_IMAGE_SERVICE_ENDPOINT; -import static com.redhat.exhort.image.ImageUtils.EXHORT_IMAGE_VARIANT; -import static com.redhat.exhort.image.ImageUtils.EXHORT_SKOPEO_CONFIG_PATH; -import static com.redhat.exhort.image.ImageUtils.EXHORT_SYFT_CONFIG_PATH; -import static com.redhat.exhort.image.ImageUtils.EXHORT_SYFT_IMAGE_SOURCE; -import static org.assertj.core.api.Assertions.assertThat; -import static org.junit.jupiter.api.Assertions.assertEquals; -import static org.junit.jupiter.api.Assertions.assertNull; -import static org.junit.jupiter.api.Assertions.assertThrows; -import static org.junit.jupiter.api.Assertions.assertTrue; -import static org.mockito.AdditionalMatchers.aryEq; -import static org.mockito.ArgumentMatchers.eq; -import static org.mockito.ArgumentMatchers.isNull; +import org.junit.jupiter.api.Named; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.Arguments; +import org.junit.jupiter.params.provider.MethodSource; +import org.junitpioneer.jupiter.ClearEnvironmentVariable; +import org.junitpioneer.jupiter.SetEnvironmentVariable; +import org.mockito.MockedStatic; +import org.mockito.Mockito; class ImageUtilsTest extends ExhortTest { - static final String mockImageName = "test.io/test/test-app:test-version@sha256:1fafb0905264413501df60d90a92ca32df8a2011cbfb4876ddff5ceb20c8f165"; - static final String mockImagePlatform = "linux/amd64"; - static final ImageRef mockImageRef = new ImageRef(mockImageName, mockImagePlatform); - static final String mockSyftPath = "test-path/syft"; - static final String mockSyftConfig = "test-path/syft-config"; - static final String mockSyftSource = "registry"; - static final String mockSkopeoPath = "test-path/skopeo"; - static final String mockSkopeoConfig = "test-path/skopeo-config"; - static final String mockSkopeoDaemon = "test-path/daemon-host"; - static final String mockDockerPath = "test-path/docker"; - static final String mockPodmanPath = "test-path/podman"; - static final String mockPath = "test-path"; - static final String mockOs = "linux"; - static final String mockArch = "arm"; - static final String mockVariant = "v7"; - - static Stream dockerArchSources() { - return Stream.of( - Arguments.of( - Named.of("amd64", "amd64"), "amd64" - ), - Arguments.of( - Named.of("x86_64", "x86_64"), "amd64" - ), - Arguments.of( - Named.of("armv5tl", "armv5tl"), "arm" - ), - Arguments.of( - Named.of("armv5tel", "armv5tel"), "arm" - ), - Arguments.of( - Named.of("armv5tejl", "armv5tejl"), "arm" - ), - Arguments.of( - Named.of("armv6l", "armv6l"), "arm" - ), - Arguments.of( - Named.of("armv7l", "armv7l"), "arm" - ), - Arguments.of( - Named.of("armv7ml", "armv7ml"), "arm" - ), - Arguments.of( - Named.of("arm64", "arm64"), "arm64" - ), - Arguments.of( - Named.of("aarch64", "aarch64"), "arm64" - ), - Arguments.of( - Named.of("i386", "i386"), "386" - ), - Arguments.of( - Named.of("i486", "i486"), "386" - ), - Arguments.of( - Named.of("i586", "i586"), "386" - ), - Arguments.of( - Named.of("i686", "i686"), "386" - ), - Arguments.of( - Named.of("mips64le", "mips64le"), "mips64le" - ), - Arguments.of( - Named.of("ppc64le", "ppc64le"), "ppc64le" - ), - Arguments.of( - Named.of("riscv64", "riscv64"), "riscv64" - ), - Arguments.of( - Named.of("s390x", "s390x"), "s390x" - ), - Arguments.of( - Named.of("empty", ""), "" - ) - ); - } - - static Stream dockerVariantSources() { - return Stream.of( - Arguments.of( - Named.of("armv5tl", "armv5tl"), "v5" - ), - Arguments.of( - Named.of("armv5tel", "armv5tel"), "v5" - ), - Arguments.of( - Named.of("armv5tejl", "armv5tejl"), "v5" - ), - Arguments.of( - Named.of("armv6l", "armv6l"), "v6" - ), - Arguments.of( - Named.of("armv7l", "armv7l"), "v7" - ), - Arguments.of( - Named.of("armv7ml", "armv7ml"), "v7" - ), - Arguments.of( - Named.of("arm64", "arm64"), "v8" - ), - Arguments.of( - Named.of("aarch64", "aarch64"), "v8" - ), - Arguments.of( - Named.of("empty", ""), "" - ) - ); - } - - @Test - @ClearEnvironmentVariable(key = "PATH") - @ClearEnvironmentVariable(key = "EXHORT_SYFT_PATH") - @ClearEnvironmentVariable(key = EXHORT_SYFT_CONFIG_PATH) - @ClearEnvironmentVariable(key = "EXHORT_DOCKER_PATH") - @ClearEnvironmentVariable(key = "EXHORT_PODMAN_PATH") - @ClearEnvironmentVariable(key = EXHORT_SYFT_IMAGE_SOURCE) - void test_generate_image_sbom() throws IOException, MalformedPackageURLException { - try (MockedStatic mock = Mockito.mockStatic(Operations.class); - var is = getResourceAsStreamDecision(this.getClass(), new String[]{"msc", "image", "image_sbom.json"})) { - var json = new BufferedReader(new InputStreamReader(is, StandardCharsets.UTF_8)).lines().collect(Collectors.joining("\n")); - var output = new Operations.ProcessExecOutput(json, "", 0); - - mock.when(() -> Operations.getCustomPathOrElse(eq("syft"))) - .thenReturn("syft"); - - mock.when(() -> Operations.runProcessGetFullOutput(isNull(), - aryEq(new String[]{"syft", mockImageRef.getImage().getFullName(), - "-s", "all-layers", "-o", "cyclonedx-json", "-q"}), - isNull())) - .thenReturn(output); - - var sbom = ImageUtils.generateImageSBOM(mockImageRef); - - var mapper = new ObjectMapper(); - var node = mapper.readTree(json); - ((ObjectNode) node.get("metadata").get("component")).set("purl", new TextNode(mockImageRef.getPackageURL().canonicalize())); - - assertEquals(node, sbom); - } - } - - @Test - @ClearEnvironmentVariable(key = "EXHORT_SKOPEO_PATH") - @ClearEnvironmentVariable(key = EXHORT_SKOPEO_CONFIG_PATH) - @ClearEnvironmentVariable(key = EXHORT_IMAGE_SERVICE_ENDPOINT) - void test_get_image_digests_single() throws IOException { - try (MockedStatic mock = Mockito.mockStatic(Operations.class); - var isRaw = getResourceAsStreamDecision(this.getClass(), new String[]{"msc", "image", "skopeo_inspect_single_raw.json"}); - var is = getResourceAsStreamDecision(this.getClass(), new String[]{"msc", "image", "skopeo_inspect_single.json"})) { - var jsonRaw = new BufferedReader(new InputStreamReader(isRaw, StandardCharsets.UTF_8)).lines().collect(Collectors.joining("\n")); - var outputRaw = new Operations.ProcessExecOutput(jsonRaw, "", 0); - - mock.when(() -> Operations.getCustomPathOrElse(eq("skopeo"))) - .thenReturn("skopeo"); - - mock.when(() -> Operations.runProcessGetFullOutput(isNull(), - aryEq(new String[]{"skopeo", "inspect", - "--raw", String.format("docker://%s", mockImageRef.getImage().getFullName())}), - isNull())) - .thenReturn(outputRaw); - - var json = new BufferedReader(new InputStreamReader(is, StandardCharsets.UTF_8)).lines().collect(Collectors.joining("\n")); - var output = new Operations.ProcessExecOutput(json, "", 0); - - mock.when(() -> Operations.runProcessGetFullOutput(isNull(), - aryEq(new String[]{"skopeo", "inspect", - "", String.format("docker://%s", mockImageRef.getImage().getFullName())}), - isNull())) - .thenReturn(output); - - var digests = ImageUtils.getImageDigests(mockImageRef); - - var expectedDigests = Collections.singletonMap(Platform.EMPTY_PLATFORM, "sha256:9aa20fd4e4842854ec1c081d2dae77c686601a8640018d68782f36c60eb1a19e"); - - assertEquals(expectedDigests, digests); - } - } - - @Test - @ClearEnvironmentVariable(key = "EXHORT_SKOPEO_PATH") - @ClearEnvironmentVariable(key = EXHORT_SKOPEO_CONFIG_PATH) - @ClearEnvironmentVariable(key = EXHORT_IMAGE_SERVICE_ENDPOINT) - void test_get_image_digests_multiple() throws IOException { - try (MockedStatic mock = Mockito.mockStatic(Operations.class); - var is = getResourceAsStreamDecision(this.getClass(), new String[]{"msc", "image", "skopeo_inspect_multi_raw.json"})) { - var json = new BufferedReader(new InputStreamReader(is, StandardCharsets.UTF_8)).lines().collect(Collectors.joining("\n")); - var output = new Operations.ProcessExecOutput(json, "", 0); - - mock.when(() -> Operations.getCustomPathOrElse(eq("skopeo"))) - .thenReturn("skopeo"); - - mock.when(() -> Operations.runProcessGetFullOutput(isNull(), - aryEq(new String[]{"skopeo", "inspect", - "--raw", String.format("docker://%s", mockImageRef.getImage().getFullName())}), - isNull())) - .thenReturn(output); - - var digests = ImageUtils.getImageDigests(mockImageRef); - - var expectedDigests = new HashMap<>(); - expectedDigests.put(new Platform("linux", "amd64", null), "sha256:06d06f15f7b641a78f2512c8817cbecaa1bf549488e273f5ac27ff1654ed33f0"); - expectedDigests.put(new Platform("linux", "arm64", null), "sha256:199d5daca3dba0a7deaf0086331917dee256089e94272bef5613517d0007f6f5"); - expectedDigests.put(new Platform("linux", "ppc64le", null), "sha256:1bba662cff053201db85aa55caf3273216a6b0e1766409ee133cf78df9b59314"); - expectedDigests.put(new Platform("linux", "s390x", null), "sha256:b39f9f6998e1693e29b7bd002bc32255fd4f69610e950523b647e61d2bb1dd66"); - - assertEquals(expectedDigests, digests); - } - } - - @Test - @SetEnvironmentVariable(key = EXHORT_IMAGE_PLATFORM, value = mockImagePlatform) - @SetEnvironmentVariable(key = EXHORT_SYFT_IMAGE_SOURCE, value = mockSyftSource) - @SetEnvironmentVariable(key = EXHORT_IMAGE_OS, value = mockOs) - @SetEnvironmentVariable(key = EXHORT_IMAGE_ARCH, value = mockArch) - @SetEnvironmentVariable(key = EXHORT_IMAGE_VARIANT, value = mockVariant) - void test_get_image_platform() { - var platform = ImageUtils.getImagePlatform(); - assertEquals(new Platform(mockImagePlatform), platform); - } - - @Test - @ClearEnvironmentVariable(key = EXHORT_IMAGE_PLATFORM) - @SetEnvironmentVariable(key = EXHORT_SYFT_IMAGE_SOURCE, value = mockSyftSource) - @SetEnvironmentVariable(key = EXHORT_IMAGE_OS, value = mockOs) - @SetEnvironmentVariable(key = EXHORT_IMAGE_ARCH, value = mockArch) - @SetEnvironmentVariable(key = EXHORT_IMAGE_VARIANT, value = mockVariant) - void test_get_image_platform_no_default() { - var platform = ImageUtils.getImagePlatform(); - assertEquals(new Platform(mockOs, mockArch, mockVariant), platform); - } - - @Test - @ClearEnvironmentVariable(key = EXHORT_IMAGE_PLATFORM) - @SetEnvironmentVariable(key = EXHORT_SYFT_IMAGE_SOURCE, value = "podman") - @SetEnvironmentVariable(key = EXHORT_IMAGE_OS, value = mockOs) - @SetEnvironmentVariable(key = EXHORT_IMAGE_ARCH, value = mockArch) - @ClearEnvironmentVariable(key = EXHORT_IMAGE_VARIANT) - void test_get_image_platform_no_default_no_variant() { - try (MockedStatic mock = Mockito.mockStatic(Operations.class)) { - mock.when(() -> Operations.getCustomPathOrElse(eq("podman"))) - .thenReturn("podman"); - - mock.when(() -> Operations.runProcessGetFullOutput(isNull(), - aryEq(new String[]{"podman", "info"}), - isNull())) - .thenReturn(new Operations.ProcessExecOutput("", "", 0)); - - var platform = ImageUtils.getImagePlatform(); - assertNull(platform); - } - } - - @Test - @ClearEnvironmentVariable(key = EXHORT_IMAGE_PLATFORM) - @SetEnvironmentVariable(key = EXHORT_SYFT_IMAGE_SOURCE, value = "podman") - @ClearEnvironmentVariable(key = EXHORT_IMAGE_OS) - @ClearEnvironmentVariable(key = EXHORT_IMAGE_ARCH) - @ClearEnvironmentVariable(key = EXHORT_IMAGE_VARIANT) - @ClearEnvironmentVariable(key = "PATH") - void test_get_image_platform_no_defaults() { - try (MockedStatic mock = Mockito.mockStatic(Operations.class)) { - mock.when(() -> Operations.getCustomPathOrElse(eq("podman"))) - .thenReturn("podman"); - - mock.when(() -> Operations.runProcessGetFullOutput(isNull(), - aryEq(new String[]{"podman", "info"}), - isNull())) - .thenReturn(new Operations.ProcessExecOutput("os: linux\narch: arm64\nvariant=v8", "", 0)); - - var platform = ImageUtils.getImagePlatform(); - assertEquals(new Platform("linux", "arm64", "v8"), platform); - } - } - - @Test - @ClearEnvironmentVariable(key = "PATH") - @SetEnvironmentVariable(key = "EXHORT_SYFT_PATH", value = mockSyftPath) - @SetEnvironmentVariable(key = EXHORT_SYFT_CONFIG_PATH, value = mockSyftConfig) - @SetEnvironmentVariable(key = "EXHORT_DOCKER_PATH", value = mockDockerPath) - @SetEnvironmentVariable(key = "EXHORT_PODMAN_PATH", value = mockPodmanPath) - @SetEnvironmentVariable(key = EXHORT_SYFT_IMAGE_SOURCE, value = mockSyftSource) - void test_exec_syft() { - try (MockedStatic mock = Mockito.mockStatic(Operations.class)) { - var output = new Operations.ProcessExecOutput("test-output", "test-error", 0); - - mock.when(() -> Operations.getCustomPathOrElse(eq("syft"))) - .thenReturn(mockSyftPath); - - mock.when(() -> Operations.getCustomPathOrElse(eq("docker"))) - .thenReturn(mockDockerPath); - - mock.when(() -> Operations.getCustomPathOrElse(eq("podman"))) - .thenReturn(mockPodmanPath); - - mock.when(() -> Operations.runProcessGetFullOutput(isNull(), - aryEq(new String[]{mockSyftPath, mockImageRef.getImage().getFullName(), - "--from", mockSyftSource, "-c", mockSyftConfig, "-s", "all-layers", - "-o", "cyclonedx-json", "-q"}), - eq(new String[]{"PATH=" + "test-path/" + File.pathSeparator + "test-path/"}))) - .thenReturn(output); - - assertThat(ImageUtils.execSyft(mockImageRef)).isEqualTo(output); - } - } - - @Test - @ClearEnvironmentVariable(key = "PATH") - @ClearEnvironmentVariable(key = "EXHORT_SYFT_PATH") - @ClearEnvironmentVariable(key = EXHORT_SYFT_CONFIG_PATH) - @ClearEnvironmentVariable(key = "EXHORT_DOCKER_PATH") - @ClearEnvironmentVariable(key = "EXHORT_PODMAN_PATH") - @ClearEnvironmentVariable(key = EXHORT_SYFT_IMAGE_SOURCE) - void test_exec_syft_no_config_no_source() { - try (MockedStatic mock = Mockito.mockStatic(Operations.class)) { - var output = new Operations.ProcessExecOutput("test-output", "test-error", 0); - - mock.when(() -> Operations.getCustomPathOrElse(eq("docker"))) - .thenReturn("docker"); - - mock.when(() -> Operations.getCustomPathOrElse(eq("podman"))) - .thenReturn("podman"); - - mock.when(() -> Operations.getCustomPathOrElse(eq("syft"))) - .thenReturn("syft"); - - mock.when(() -> Operations.runProcessGetFullOutput(isNull(), - aryEq(new String[]{"syft", mockImageRef.getImage().getFullName(), - "-s", "all-layers", "-o", "cyclonedx-json", "-q"}), - isNull())) - .thenReturn(output); - - assertThat(ImageUtils.execSyft(mockImageRef)).isEqualTo(output); - } - } - - @Test - @ClearEnvironmentVariable(key = "PATH") - void test_get_syft_envs() { - var envs1 = ImageUtils.getSyftEnvs("", ""); - assertTrue(envs1.isEmpty()); - - var envs2 = ImageUtils.getSyftEnvs("test-docker-path", ""); - var expected_envs2 = new ArrayList<>(); - expected_envs2.add("PATH=test-docker-path"); - assertEquals(expected_envs2, envs2); - - var envs3 = ImageUtils.getSyftEnvs("", "test-podman-path"); - var expected_envs3 = new ArrayList<>(); - expected_envs3.add("PATH=test-podman-path"); - assertEquals(expected_envs3, envs3); - - var envs4 = ImageUtils.getSyftEnvs("test-docker-path", "test-podman-path"); - var expected_envs4 = new ArrayList<>(); - expected_envs4.add("PATH=test-docker-path" + File.pathSeparator + "test-podman-path"); - assertEquals(expected_envs4, envs4); - } - - @Test - @SetEnvironmentVariable(key = "PATH", value = mockPath) - void test_update_PATH_env() { - var path = ImageUtils.updatePATHEnv("test-exec-path"); - assertEquals("PATH=test-path" + File.pathSeparator + "test-exec-path", path); - } - - @Test - @ClearEnvironmentVariable(key = "PATH") - void test_update_PATH_env_no_PATH() { - var path = ImageUtils.updatePATHEnv("test-exec-path"); - assertEquals("PATH=test-exec-path", path); - } - - @Test - @SetEnvironmentVariable(key = "EXHORT_DOCKER_PATH", value = mockDockerPath) - @SetEnvironmentVariable(key = "PATH", value = mockPath) - void test_host_info_docker() { - try (MockedStatic mock = Mockito.mockStatic(Operations.class)) { - var output = new Operations.ProcessExecOutput("info0: test\n info: test-output", "", 0); - - mock.when(() -> Operations.getCustomPathOrElse(eq("docker"))) - .thenReturn(mockDockerPath); - - mock.when(() -> Operations.runProcessGetFullOutput(isNull(), - aryEq(new String[]{mockDockerPath, "info"}), - isNull())) - .thenReturn(output); - - assertThat(ImageUtils.hostInfo("docker", "info")).isEqualTo("test-output"); - } - } - - @Test - @ClearEnvironmentVariable(key = "EXHORT_DOCKER_PATH") - void test_host_info_no_docker_path() { - try (MockedStatic mock = Mockito.mockStatic(Operations.class)) { - var output = new Operations.ProcessExecOutput("", "test-error", 0); - - mock.when(() -> Operations.getCustomPathOrElse(eq("docker"))) - .thenReturn("docker"); - - mock.when(() -> Operations.runProcessGetFullOutput(isNull(), - aryEq(new String[]{"docker", "info"}), - isNull())) - .thenReturn(output); - - var exception = assertThrows(RuntimeException.class, () -> { - ImageUtils.hostInfo("docker", "info"); - }); - assertEquals("test-error", exception.getMessage()); - } - } - - @Test - @SetEnvironmentVariable(key = "EXHORT_DOCKER_PATH", value = mockDockerPath) - @SetEnvironmentVariable(key = "PATH", value = mockPath) - void test_docker_get_os() { - try (MockedStatic mock = Mockito.mockStatic(Operations.class)) { - var output = new Operations.ProcessExecOutput("OSType: test-output", "", 0); - - mock.when(() -> Operations.getCustomPathOrElse(eq("docker"))) - .thenReturn(mockDockerPath); - - mock.when(() -> Operations.runProcessGetFullOutput(isNull(), - aryEq(new String[]{mockDockerPath, "info"}), - isNull())) - .thenReturn(output); - - assertThat(ImageUtils.dockerGetOs()).isEqualTo("test-output"); - } - } - - @ParameterizedTest(name = "{0}") - @SetEnvironmentVariable(key = "EXHORT_DOCKER_PATH", value = mockDockerPath) - @SetEnvironmentVariable(key = "PATH", value = mockPath) - @MethodSource("dockerArchSources") - void test_docker_get_arch(String sysArch, String arch) { - try (MockedStatic mock = Mockito.mockStatic(Operations.class)) { - var output = new Operations.ProcessExecOutput("Architecture:" + sysArch, "", 0); - - mock.when(() -> Operations.getCustomPathOrElse(eq("docker"))) - .thenReturn(mockDockerPath); - - mock.when(() -> Operations.runProcessGetFullOutput(isNull(), - aryEq(new String[]{mockDockerPath, "info"}), - isNull())) - .thenReturn(output); + static final String mockImageName = + "test.io/test/test-app:test-version@sha256:1fafb0905264413501df60d90a92ca32df8a2011cbfb4876ddff5ceb20c8f165"; + static final String mockImagePlatform = "linux/amd64"; + static final ImageRef mockImageRef = new ImageRef(mockImageName, mockImagePlatform); + static final String mockSyftPath = "test-path/syft"; + static final String mockSyftConfig = "test-path/syft-config"; + static final String mockSyftSource = "registry"; + static final String mockSkopeoPath = "test-path/skopeo"; + static final String mockSkopeoConfig = "test-path/skopeo-config"; + static final String mockSkopeoDaemon = "test-path/daemon-host"; + static final String mockDockerPath = "test-path/docker"; + static final String mockPodmanPath = "test-path/podman"; + static final String mockPath = "test-path"; + static final String mockOs = "linux"; + static final String mockArch = "arm"; + static final String mockVariant = "v7"; + + static Stream dockerArchSources() { + return Stream.of( + Arguments.of(Named.of("amd64", "amd64"), "amd64"), + Arguments.of(Named.of("x86_64", "x86_64"), "amd64"), + Arguments.of(Named.of("armv5tl", "armv5tl"), "arm"), + Arguments.of(Named.of("armv5tel", "armv5tel"), "arm"), + Arguments.of(Named.of("armv5tejl", "armv5tejl"), "arm"), + Arguments.of(Named.of("armv6l", "armv6l"), "arm"), + Arguments.of(Named.of("armv7l", "armv7l"), "arm"), + Arguments.of(Named.of("armv7ml", "armv7ml"), "arm"), + Arguments.of(Named.of("arm64", "arm64"), "arm64"), + Arguments.of(Named.of("aarch64", "aarch64"), "arm64"), + Arguments.of(Named.of("i386", "i386"), "386"), + Arguments.of(Named.of("i486", "i486"), "386"), + Arguments.of(Named.of("i586", "i586"), "386"), + Arguments.of(Named.of("i686", "i686"), "386"), + Arguments.of(Named.of("mips64le", "mips64le"), "mips64le"), + Arguments.of(Named.of("ppc64le", "ppc64le"), "ppc64le"), + Arguments.of(Named.of("riscv64", "riscv64"), "riscv64"), + Arguments.of(Named.of("s390x", "s390x"), "s390x"), + Arguments.of(Named.of("empty", ""), "")); + } + + static Stream dockerVariantSources() { + return Stream.of( + Arguments.of(Named.of("armv5tl", "armv5tl"), "v5"), + Arguments.of(Named.of("armv5tel", "armv5tel"), "v5"), + Arguments.of(Named.of("armv5tejl", "armv5tejl"), "v5"), + Arguments.of(Named.of("armv6l", "armv6l"), "v6"), + Arguments.of(Named.of("armv7l", "armv7l"), "v7"), + Arguments.of(Named.of("armv7ml", "armv7ml"), "v7"), + Arguments.of(Named.of("arm64", "arm64"), "v8"), + Arguments.of(Named.of("aarch64", "aarch64"), "v8"), + Arguments.of(Named.of("empty", ""), "")); + } + + @Test + @ClearEnvironmentVariable(key = "PATH") + @ClearEnvironmentVariable(key = "EXHORT_SYFT_PATH") + @ClearEnvironmentVariable(key = EXHORT_SYFT_CONFIG_PATH) + @ClearEnvironmentVariable(key = "EXHORT_DOCKER_PATH") + @ClearEnvironmentVariable(key = "EXHORT_PODMAN_PATH") + @ClearEnvironmentVariable(key = EXHORT_SYFT_IMAGE_SOURCE) + void test_generate_image_sbom() throws IOException, MalformedPackageURLException { + try (MockedStatic mock = Mockito.mockStatic(Operations.class); + var is = getResourceAsStreamDecision( + this.getClass(), new String[] {"msc", "image", "image_sbom.json"})) { + var json = new BufferedReader(new InputStreamReader(is, StandardCharsets.UTF_8)) + .lines() + .collect(Collectors.joining("\n")); + var output = new Operations.ProcessExecOutput(json, "", 0); + + mock.when(() -> Operations.getCustomPathOrElse(eq("syft"))).thenReturn("syft"); + + mock.when(() -> Operations.runProcessGetFullOutput( + isNull(), + aryEq(new String[] { + "syft", + mockImageRef.getImage().getFullName(), + "-s", + "all-layers", + "-o", + "cyclonedx-json", + "-q" + }), + isNull())) + .thenReturn(output); + + var sbom = ImageUtils.generateImageSBOM(mockImageRef); + + var mapper = new ObjectMapper(); + var node = mapper.readTree(json); + ((ObjectNode) node.get("metadata").get("component")) + .set("purl", new TextNode(mockImageRef.getPackageURL().canonicalize())); + + assertEquals(node, sbom); + } + } + + @Test + @ClearEnvironmentVariable(key = "EXHORT_SKOPEO_PATH") + @ClearEnvironmentVariable(key = EXHORT_SKOPEO_CONFIG_PATH) + @ClearEnvironmentVariable(key = EXHORT_IMAGE_SERVICE_ENDPOINT) + void test_get_image_digests_single() throws IOException { + try (MockedStatic mock = Mockito.mockStatic(Operations.class); + var isRaw = getResourceAsStreamDecision( + this.getClass(), new String[] {"msc", "image", "skopeo_inspect_single_raw.json"}); + var is = getResourceAsStreamDecision( + this.getClass(), new String[] {"msc", "image", "skopeo_inspect_single.json"})) { + var jsonRaw = new BufferedReader(new InputStreamReader(isRaw, StandardCharsets.UTF_8)) + .lines() + .collect(Collectors.joining("\n")); + var outputRaw = new Operations.ProcessExecOutput(jsonRaw, "", 0); + + mock.when(() -> Operations.getCustomPathOrElse(eq("skopeo"))).thenReturn("skopeo"); + + mock.when(() -> Operations.runProcessGetFullOutput( + isNull(), + aryEq(new String[] { + "skopeo", + "inspect", + "--raw", + String.format( + "docker://%s", mockImageRef.getImage().getFullName()) + }), + isNull())) + .thenReturn(outputRaw); + + var json = new BufferedReader(new InputStreamReader(is, StandardCharsets.UTF_8)) + .lines() + .collect(Collectors.joining("\n")); + var output = new Operations.ProcessExecOutput(json, "", 0); + + mock.when(() -> Operations.runProcessGetFullOutput( + isNull(), + aryEq(new String[] { + "skopeo", + "inspect", + "", + String.format( + "docker://%s", mockImageRef.getImage().getFullName()) + }), + isNull())) + .thenReturn(output); + + var digests = ImageUtils.getImageDigests(mockImageRef); + + var expectedDigests = Collections.singletonMap( + Platform.EMPTY_PLATFORM, "sha256:9aa20fd4e4842854ec1c081d2dae77c686601a8640018d68782f36c60eb1a19e"); + + assertEquals(expectedDigests, digests); + } + } + + @Test + @ClearEnvironmentVariable(key = "EXHORT_SKOPEO_PATH") + @ClearEnvironmentVariable(key = EXHORT_SKOPEO_CONFIG_PATH) + @ClearEnvironmentVariable(key = EXHORT_IMAGE_SERVICE_ENDPOINT) + void test_get_image_digests_multiple() throws IOException { + try (MockedStatic mock = Mockito.mockStatic(Operations.class); + var is = getResourceAsStreamDecision( + this.getClass(), new String[] {"msc", "image", "skopeo_inspect_multi_raw.json"})) { + var json = new BufferedReader(new InputStreamReader(is, StandardCharsets.UTF_8)) + .lines() + .collect(Collectors.joining("\n")); + var output = new Operations.ProcessExecOutput(json, "", 0); + + mock.when(() -> Operations.getCustomPathOrElse(eq("skopeo"))).thenReturn("skopeo"); + + mock.when(() -> Operations.runProcessGetFullOutput( + isNull(), + aryEq(new String[] { + "skopeo", + "inspect", + "--raw", + String.format( + "docker://%s", mockImageRef.getImage().getFullName()) + }), + isNull())) + .thenReturn(output); + + var digests = ImageUtils.getImageDigests(mockImageRef); + + var expectedDigests = new HashMap<>(); + expectedDigests.put( + new Platform("linux", "amd64", null), + "sha256:06d06f15f7b641a78f2512c8817cbecaa1bf549488e273f5ac27ff1654ed33f0"); + expectedDigests.put( + new Platform("linux", "arm64", null), + "sha256:199d5daca3dba0a7deaf0086331917dee256089e94272bef5613517d0007f6f5"); + expectedDigests.put( + new Platform("linux", "ppc64le", null), + "sha256:1bba662cff053201db85aa55caf3273216a6b0e1766409ee133cf78df9b59314"); + expectedDigests.put( + new Platform("linux", "s390x", null), + "sha256:b39f9f6998e1693e29b7bd002bc32255fd4f69610e950523b647e61d2bb1dd66"); + + assertEquals(expectedDigests, digests); + } + } + + @Test + @SetEnvironmentVariable(key = EXHORT_IMAGE_PLATFORM, value = mockImagePlatform) + @SetEnvironmentVariable(key = EXHORT_SYFT_IMAGE_SOURCE, value = mockSyftSource) + @SetEnvironmentVariable(key = EXHORT_IMAGE_OS, value = mockOs) + @SetEnvironmentVariable(key = EXHORT_IMAGE_ARCH, value = mockArch) + @SetEnvironmentVariable(key = EXHORT_IMAGE_VARIANT, value = mockVariant) + void test_get_image_platform() { + var platform = ImageUtils.getImagePlatform(); + assertEquals(new Platform(mockImagePlatform), platform); + } + + @Test + @ClearEnvironmentVariable(key = EXHORT_IMAGE_PLATFORM) + @SetEnvironmentVariable(key = EXHORT_SYFT_IMAGE_SOURCE, value = mockSyftSource) + @SetEnvironmentVariable(key = EXHORT_IMAGE_OS, value = mockOs) + @SetEnvironmentVariable(key = EXHORT_IMAGE_ARCH, value = mockArch) + @SetEnvironmentVariable(key = EXHORT_IMAGE_VARIANT, value = mockVariant) + void test_get_image_platform_no_default() { + var platform = ImageUtils.getImagePlatform(); + assertEquals(new Platform(mockOs, mockArch, mockVariant), platform); + } + + @Test + @ClearEnvironmentVariable(key = EXHORT_IMAGE_PLATFORM) + @SetEnvironmentVariable(key = EXHORT_SYFT_IMAGE_SOURCE, value = "podman") + @SetEnvironmentVariable(key = EXHORT_IMAGE_OS, value = mockOs) + @SetEnvironmentVariable(key = EXHORT_IMAGE_ARCH, value = mockArch) + @ClearEnvironmentVariable(key = EXHORT_IMAGE_VARIANT) + void test_get_image_platform_no_default_no_variant() { + try (MockedStatic mock = Mockito.mockStatic(Operations.class)) { + mock.when(() -> Operations.getCustomPathOrElse(eq("podman"))).thenReturn("podman"); + + mock.when(() -> Operations.runProcessGetFullOutput( + isNull(), aryEq(new String[] {"podman", "info"}), isNull())) + .thenReturn(new Operations.ProcessExecOutput("", "", 0)); + + var platform = ImageUtils.getImagePlatform(); + assertNull(platform); + } + } + + @Test + @ClearEnvironmentVariable(key = EXHORT_IMAGE_PLATFORM) + @SetEnvironmentVariable(key = EXHORT_SYFT_IMAGE_SOURCE, value = "podman") + @ClearEnvironmentVariable(key = EXHORT_IMAGE_OS) + @ClearEnvironmentVariable(key = EXHORT_IMAGE_ARCH) + @ClearEnvironmentVariable(key = EXHORT_IMAGE_VARIANT) + @ClearEnvironmentVariable(key = "PATH") + void test_get_image_platform_no_defaults() { + try (MockedStatic mock = Mockito.mockStatic(Operations.class)) { + mock.when(() -> Operations.getCustomPathOrElse(eq("podman"))).thenReturn("podman"); + + mock.when(() -> Operations.runProcessGetFullOutput( + isNull(), aryEq(new String[] {"podman", "info"}), isNull())) + .thenReturn(new Operations.ProcessExecOutput("os: linux\narch: arm64\nvariant=v8", "", 0)); + + var platform = ImageUtils.getImagePlatform(); + assertEquals(new Platform("linux", "arm64", "v8"), platform); + } + } + + @Test + @ClearEnvironmentVariable(key = "PATH") + @SetEnvironmentVariable(key = "EXHORT_SYFT_PATH", value = mockSyftPath) + @SetEnvironmentVariable(key = EXHORT_SYFT_CONFIG_PATH, value = mockSyftConfig) + @SetEnvironmentVariable(key = "EXHORT_DOCKER_PATH", value = mockDockerPath) + @SetEnvironmentVariable(key = "EXHORT_PODMAN_PATH", value = mockPodmanPath) + @SetEnvironmentVariable(key = EXHORT_SYFT_IMAGE_SOURCE, value = mockSyftSource) + void test_exec_syft() { + try (MockedStatic mock = Mockito.mockStatic(Operations.class)) { + var output = new Operations.ProcessExecOutput("test-output", "test-error", 0); + + mock.when(() -> Operations.getCustomPathOrElse(eq("syft"))).thenReturn(mockSyftPath); + + mock.when(() -> Operations.getCustomPathOrElse(eq("docker"))).thenReturn(mockDockerPath); + + mock.when(() -> Operations.getCustomPathOrElse(eq("podman"))).thenReturn(mockPodmanPath); + + mock.when(() -> Operations.runProcessGetFullOutput( + isNull(), + aryEq(new String[] { + mockSyftPath, + mockImageRef.getImage().getFullName(), + "--from", + mockSyftSource, + "-c", + mockSyftConfig, + "-s", + "all-layers", + "-o", + "cyclonedx-json", + "-q" + }), + eq(new String[] {"PATH=" + "test-path/" + File.pathSeparator + "test-path/"}))) + .thenReturn(output); + + assertThat(ImageUtils.execSyft(mockImageRef)).isEqualTo(output); + } + } + + @Test + @ClearEnvironmentVariable(key = "PATH") + @ClearEnvironmentVariable(key = "EXHORT_SYFT_PATH") + @ClearEnvironmentVariable(key = EXHORT_SYFT_CONFIG_PATH) + @ClearEnvironmentVariable(key = "EXHORT_DOCKER_PATH") + @ClearEnvironmentVariable(key = "EXHORT_PODMAN_PATH") + @ClearEnvironmentVariable(key = EXHORT_SYFT_IMAGE_SOURCE) + void test_exec_syft_no_config_no_source() { + try (MockedStatic mock = Mockito.mockStatic(Operations.class)) { + var output = new Operations.ProcessExecOutput("test-output", "test-error", 0); + + mock.when(() -> Operations.getCustomPathOrElse(eq("docker"))).thenReturn("docker"); + + mock.when(() -> Operations.getCustomPathOrElse(eq("podman"))).thenReturn("podman"); + + mock.when(() -> Operations.getCustomPathOrElse(eq("syft"))).thenReturn("syft"); + + mock.when(() -> Operations.runProcessGetFullOutput( + isNull(), + aryEq(new String[] { + "syft", + mockImageRef.getImage().getFullName(), + "-s", + "all-layers", + "-o", + "cyclonedx-json", + "-q" + }), + isNull())) + .thenReturn(output); + + assertThat(ImageUtils.execSyft(mockImageRef)).isEqualTo(output); + } + } + + @Test + @ClearEnvironmentVariable(key = "PATH") + void test_get_syft_envs() { + var envs1 = ImageUtils.getSyftEnvs("", ""); + assertTrue(envs1.isEmpty()); + + var envs2 = ImageUtils.getSyftEnvs("test-docker-path", ""); + var expected_envs2 = new ArrayList<>(); + expected_envs2.add("PATH=test-docker-path"); + assertEquals(expected_envs2, envs2); + + var envs3 = ImageUtils.getSyftEnvs("", "test-podman-path"); + var expected_envs3 = new ArrayList<>(); + expected_envs3.add("PATH=test-podman-path"); + assertEquals(expected_envs3, envs3); + + var envs4 = ImageUtils.getSyftEnvs("test-docker-path", "test-podman-path"); + var expected_envs4 = new ArrayList<>(); + expected_envs4.add("PATH=test-docker-path" + File.pathSeparator + "test-podman-path"); + assertEquals(expected_envs4, envs4); + } - assertThat(ImageUtils.dockerGetArch()).isEqualTo(arch); - } - } - - @ParameterizedTest(name = "{0}") - @SetEnvironmentVariable(key = "EXHORT_DOCKER_PATH", value = mockDockerPath) - @SetEnvironmentVariable(key = "PATH", value = mockPath) - @MethodSource("dockerVariantSources") - void test_docker_get_variant(String sysArch, String variant) { - try (MockedStatic mock = Mockito.mockStatic(Operations.class)) { - var output = new Operations.ProcessExecOutput("Architecture:" + sysArch, "", 0); - - mock.when(() -> Operations.getCustomPathOrElse(eq("docker"))) - .thenReturn(mockDockerPath); - - mock.when(() -> Operations.runProcessGetFullOutput(isNull(), - aryEq(new String[]{mockDockerPath, "info"}), - isNull())) - .thenReturn(output); + @Test + @SetEnvironmentVariable(key = "PATH", value = mockPath) + void test_update_PATH_env() { + var path = ImageUtils.updatePATHEnv("test-exec-path"); + assertEquals("PATH=test-path" + File.pathSeparator + "test-exec-path", path); + } - assertThat(ImageUtils.dockerGetVariant()).isEqualTo(variant); + @Test + @ClearEnvironmentVariable(key = "PATH") + void test_update_PATH_env_no_PATH() { + var path = ImageUtils.updatePATHEnv("test-exec-path"); + assertEquals("PATH=test-exec-path", path); } - } - @Test - @SetEnvironmentVariable(key = "EXHORT_PODMAN_PATH", value = mockPodmanPath) - @SetEnvironmentVariable(key = "PATH", value = mockPath) - void test_host_info_podman() { - try (MockedStatic mock = Mockito.mockStatic(Operations.class)) { - var output = new Operations.ProcessExecOutput("info: test-output\nabcdesss", "", 0); + @Test + @SetEnvironmentVariable(key = "EXHORT_DOCKER_PATH", value = mockDockerPath) + @SetEnvironmentVariable(key = "PATH", value = mockPath) + void test_host_info_docker() { + try (MockedStatic mock = Mockito.mockStatic(Operations.class)) { + var output = new Operations.ProcessExecOutput("info0: test\n info: test-output", "", 0); - mock.when(() -> Operations.getCustomPathOrElse(eq("podman"))) - .thenReturn(mockPodmanPath); + mock.when(() -> Operations.getCustomPathOrElse(eq("docker"))).thenReturn(mockDockerPath); - mock.when(() -> Operations.runProcessGetFullOutput(isNull(), - aryEq(new String[]{mockPodmanPath, "info"}), - isNull())) - .thenReturn(output); + mock.when(() -> Operations.runProcessGetFullOutput( + isNull(), aryEq(new String[] {mockDockerPath, "info"}), isNull())) + .thenReturn(output); - assertThat(ImageUtils.hostInfo("podman", "info")).isEqualTo("test-output"); + assertThat(ImageUtils.hostInfo("docker", "info")).isEqualTo("test-output"); + } } - } - @Test - @SetEnvironmentVariable(key = "EXHORT_PODMAN_PATH", value = mockPodmanPath) - @SetEnvironmentVariable(key = "PATH", value = mockPath) - void test_podman_get_os() { - try (MockedStatic mock = Mockito.mockStatic(Operations.class)) { - var output = new Operations.ProcessExecOutput("os: test-output", "", 0); + @Test + @ClearEnvironmentVariable(key = "EXHORT_DOCKER_PATH") + void test_host_info_no_docker_path() { + try (MockedStatic mock = Mockito.mockStatic(Operations.class)) { + var output = new Operations.ProcessExecOutput("", "test-error", 0); + + mock.when(() -> Operations.getCustomPathOrElse(eq("docker"))).thenReturn("docker"); - mock.when(() -> Operations.getCustomPathOrElse(eq("podman"))) - .thenReturn(mockPodmanPath); + mock.when(() -> Operations.runProcessGetFullOutput( + isNull(), aryEq(new String[] {"docker", "info"}), isNull())) + .thenReturn(output); - mock.when(() -> Operations.runProcessGetFullOutput(isNull(), - aryEq(new String[]{mockPodmanPath, "info"}), - isNull())) - .thenReturn(output); + var exception = assertThrows(RuntimeException.class, () -> { + ImageUtils.hostInfo("docker", "info"); + }); + assertEquals("test-error", exception.getMessage()); + } + } - assertThat(ImageUtils.podmanGetOs()).isEqualTo("test-output"); - } - } + @Test + @SetEnvironmentVariable(key = "EXHORT_DOCKER_PATH", value = mockDockerPath) + @SetEnvironmentVariable(key = "PATH", value = mockPath) + void test_docker_get_os() { + try (MockedStatic mock = Mockito.mockStatic(Operations.class)) { + var output = new Operations.ProcessExecOutput("OSType: test-output", "", 0); - @Test - @SetEnvironmentVariable(key = "EXHORT_PODMAN_PATH", value = mockPodmanPath) - @SetEnvironmentVariable(key = "PATH", value = mockPath) - void test_podman_get_arch() { - try (MockedStatic mock = Mockito.mockStatic(Operations.class)) { - var output = new Operations.ProcessExecOutput("arch: test-output", "", 0); - - mock.when(() -> Operations.getCustomPathOrElse(eq("podman"))) - .thenReturn(mockPodmanPath); + mock.when(() -> Operations.getCustomPathOrElse(eq("docker"))).thenReturn(mockDockerPath); - mock.when(() -> Operations.runProcessGetFullOutput(isNull(), - aryEq(new String[]{mockPodmanPath, "info"}), - isNull())) - .thenReturn(output); + mock.when(() -> Operations.runProcessGetFullOutput( + isNull(), aryEq(new String[] {mockDockerPath, "info"}), isNull())) + .thenReturn(output); - assertThat(ImageUtils.podmanGetArch()).isEqualTo("test-output"); + assertThat(ImageUtils.dockerGetOs()).isEqualTo("test-output"); + } } - } - - @Test - @SetEnvironmentVariable(key = "EXHORT_PODMAN_PATH", value = mockPodmanPath) - @SetEnvironmentVariable(key = "PATH", value = mockPath) - void test_podman_get_variant() { - try (MockedStatic mock = Mockito.mockStatic(Operations.class)) { - var output = new Operations.ProcessExecOutput("variant: test-output", "", 0); - - mock.when(() -> Operations.getCustomPathOrElse(eq("podman"))) - .thenReturn(mockPodmanPath); - mock.when(() -> Operations.runProcessGetFullOutput(isNull(), - aryEq(new String[]{mockPodmanPath, "info"}), - isNull())) - .thenReturn(output); + @ParameterizedTest(name = "{0}") + @SetEnvironmentVariable(key = "EXHORT_DOCKER_PATH", value = mockDockerPath) + @SetEnvironmentVariable(key = "PATH", value = mockPath) + @MethodSource("dockerArchSources") + void test_docker_get_arch(String sysArch, String arch) { + try (MockedStatic mock = Mockito.mockStatic(Operations.class)) { + var output = new Operations.ProcessExecOutput("Architecture:" + sysArch, "", 0); + + mock.when(() -> Operations.getCustomPathOrElse(eq("docker"))).thenReturn(mockDockerPath); - assertThat(ImageUtils.podmanGetVariant()).isEqualTo("test-output"); + mock.when(() -> Operations.runProcessGetFullOutput( + isNull(), aryEq(new String[] {mockDockerPath, "info"}), isNull())) + .thenReturn(output); + + assertThat(ImageUtils.dockerGetArch()).isEqualTo(arch); + } } - } - @Test - void test_docker_podman_info() { - var info = ImageUtils.dockerPodmanInfo(() -> "docker", () -> "podman"); - assertEquals("docker", info); - - info = ImageUtils.dockerPodmanInfo(() -> "", () -> "podman"); - assertEquals("podman", info); - } - - @Test - @SetEnvironmentVariable(key = "EXHORT_SKOPEO_PATH", value = mockSkopeoPath) - @SetEnvironmentVariable(key = EXHORT_SKOPEO_CONFIG_PATH, value = mockSkopeoConfig) - @SetEnvironmentVariable(key = EXHORT_IMAGE_SERVICE_ENDPOINT, value = mockSkopeoDaemon) - void test_exec_skopeo_inspect_raw() { - try (MockedStatic mock = Mockito.mockStatic(Operations.class)) { - var output = new Operations.ProcessExecOutput("test-output", "test-error", 0); + @ParameterizedTest(name = "{0}") + @SetEnvironmentVariable(key = "EXHORT_DOCKER_PATH", value = mockDockerPath) + @SetEnvironmentVariable(key = "PATH", value = mockPath) + @MethodSource("dockerVariantSources") + void test_docker_get_variant(String sysArch, String variant) { + try (MockedStatic mock = Mockito.mockStatic(Operations.class)) { + var output = new Operations.ProcessExecOutput("Architecture:" + sysArch, "", 0); + + mock.when(() -> Operations.getCustomPathOrElse(eq("docker"))).thenReturn(mockDockerPath); - mock.when(() -> Operations.getCustomPathOrElse(eq("skopeo"))) - .thenReturn(mockSkopeoPath); + mock.when(() -> Operations.runProcessGetFullOutput( + isNull(), aryEq(new String[] {mockDockerPath, "info"}), isNull())) + .thenReturn(output); - mock.when(() -> Operations.runProcessGetFullOutput(isNull(), - aryEq(new String[]{mockSkopeoPath, "inspect", "--authfile", mockSkopeoConfig, - "--daemon-host", mockSkopeoDaemon, "--raw", - String.format("docker-daemon:%s", mockImageRef.getImage().getFullName())}), - isNull())) - .thenReturn(output); - - assertThat(ImageUtils.execSkopeoInspect(mockImageRef, true)).isEqualTo(output); - } - } - - @Test - @SetEnvironmentVariable(key = "EXHORT_SKOPEO_PATH", value = mockSkopeoPath) - @ClearEnvironmentVariable(key = EXHORT_SKOPEO_CONFIG_PATH) - @SetEnvironmentVariable(key = EXHORT_IMAGE_SERVICE_ENDPOINT, value = mockSkopeoDaemon) - void test_exec_skopeo_inspect_raw_no_config() { - try (MockedStatic mock = Mockito.mockStatic(Operations.class)) { - var output = new Operations.ProcessExecOutput("test-output", "test-error", 0); + assertThat(ImageUtils.dockerGetVariant()).isEqualTo(variant); + } + } - mock.when(() -> Operations.getCustomPathOrElse(eq("skopeo"))) - .thenReturn(mockSkopeoPath); - - mock.when(() -> Operations.runProcessGetFullOutput(isNull(), - aryEq(new String[]{mockSkopeoPath, "inspect", - "--daemon-host", mockSkopeoDaemon, "--raw", - String.format("docker-daemon:%s", mockImageRef.getImage().getFullName())}), - isNull())) - .thenReturn(output); - - assertThat(ImageUtils.execSkopeoInspect(mockImageRef, true)).isEqualTo(output); - } - } - - @Test - @ClearEnvironmentVariable(key = "EXHORT_SKOPEO_PATH") - @SetEnvironmentVariable(key = EXHORT_SKOPEO_CONFIG_PATH, value = mockSkopeoConfig) - @ClearEnvironmentVariable(key = EXHORT_IMAGE_SERVICE_ENDPOINT) - void test_exec_skopeo_inspect_raw_no_daemon() { - try (MockedStatic mock = Mockito.mockStatic(Operations.class)) { - var output = new Operations.ProcessExecOutput("test-output", "test-error", 0); - - mock.when(() -> Operations.getCustomPathOrElse(eq("skopeo"))) - .thenReturn("skopeo"); - - mock.when(() -> Operations.runProcessGetFullOutput(isNull(), - aryEq(new String[]{"skopeo", "inspect", "--authfile", mockSkopeoConfig, - "--raw", String.format("docker://%s", mockImageRef.getImage().getFullName())}), - isNull())) - .thenReturn(output); - - assertThat(ImageUtils.execSkopeoInspect(mockImageRef, true)).isEqualTo(output); - } - } - - @Test - @ClearEnvironmentVariable(key = "EXHORT_SKOPEO_PATH") - @ClearEnvironmentVariable(key = EXHORT_SKOPEO_CONFIG_PATH) - @ClearEnvironmentVariable(key = EXHORT_IMAGE_SERVICE_ENDPOINT) - void test_exec_skopeo_inspect_raw_no_config_no_daemon() { - try (MockedStatic mock = Mockito.mockStatic(Operations.class)) { - var output = new Operations.ProcessExecOutput("test-output", "test-error", 0); - - mock.when(() -> Operations.getCustomPathOrElse(eq("skopeo"))) - .thenReturn("skopeo"); - - mock.when(() -> Operations.runProcessGetFullOutput(isNull(), - aryEq(new String[]{"skopeo", "inspect", - "--raw", String.format("docker://%s", mockImageRef.getImage().getFullName())}), - isNull())) - .thenReturn(output); - - assertThat(ImageUtils.execSkopeoInspect(mockImageRef, true)).isEqualTo(output); - } - } - - @Test - @SetEnvironmentVariable(key = "EXHORT_SKOPEO_PATH", value = mockSkopeoPath) - @SetEnvironmentVariable(key = EXHORT_SKOPEO_CONFIG_PATH, value = mockSkopeoConfig) - @SetEnvironmentVariable(key = EXHORT_IMAGE_SERVICE_ENDPOINT, value = mockSkopeoDaemon) - void test_exec_skopeo_inspect() { - try (MockedStatic mock = Mockito.mockStatic(Operations.class)) { - var output = new Operations.ProcessExecOutput("test-output", "test-error", 0); - - mock.when(() -> Operations.getCustomPathOrElse(eq("skopeo"))) - .thenReturn(mockSkopeoPath); - - mock.when(() -> Operations.runProcessGetFullOutput(isNull(), - aryEq(new String[]{mockSkopeoPath, "inspect", "--authfile", mockSkopeoConfig, - "--daemon-host", mockSkopeoDaemon, "", - String.format("docker-daemon:%s", mockImageRef.getImage().getFullName())}), - isNull())) - .thenReturn(output); - - assertThat(ImageUtils.execSkopeoInspect(mockImageRef, false)).isEqualTo(output); - } - } - - @Test - @SetEnvironmentVariable(key = "EXHORT_SKOPEO_PATH", value = mockSkopeoPath) - @ClearEnvironmentVariable(key = EXHORT_SKOPEO_CONFIG_PATH) - @SetEnvironmentVariable(key = EXHORT_IMAGE_SERVICE_ENDPOINT, value = mockSkopeoDaemon) - void test_exec_skopeo_inspect_no_config() { - try (MockedStatic mock = Mockito.mockStatic(Operations.class)) { - var output = new Operations.ProcessExecOutput("test-output", "test-error", 0); - - mock.when(() -> Operations.getCustomPathOrElse(eq("skopeo"))) - .thenReturn(mockSkopeoPath); + @Test + @SetEnvironmentVariable(key = "EXHORT_PODMAN_PATH", value = mockPodmanPath) + @SetEnvironmentVariable(key = "PATH", value = mockPath) + void test_host_info_podman() { + try (MockedStatic mock = Mockito.mockStatic(Operations.class)) { + var output = new Operations.ProcessExecOutput("info: test-output\nabcdesss", "", 0); - mock.when(() -> Operations.runProcessGetFullOutput(isNull(), - aryEq(new String[]{mockSkopeoPath, "inspect", - "--daemon-host", mockSkopeoDaemon, "", - String.format("docker-daemon:%s", mockImageRef.getImage().getFullName())}), - isNull())) - .thenReturn(output); - - assertThat(ImageUtils.execSkopeoInspect(mockImageRef, false)).isEqualTo(output); + mock.when(() -> Operations.getCustomPathOrElse(eq("podman"))).thenReturn(mockPodmanPath); + + mock.when(() -> Operations.runProcessGetFullOutput( + isNull(), aryEq(new String[] {mockPodmanPath, "info"}), isNull())) + .thenReturn(output); + + assertThat(ImageUtils.hostInfo("podman", "info")).isEqualTo("test-output"); + } } - } - @Test - @ClearEnvironmentVariable(key = "EXHORT_SKOPEO_PATH") - @SetEnvironmentVariable(key = EXHORT_SKOPEO_CONFIG_PATH, value = mockSkopeoConfig) - @ClearEnvironmentVariable(key = EXHORT_IMAGE_SERVICE_ENDPOINT) - void test_exec_skopeo_inspect_no_daemon() { - try (MockedStatic mock = Mockito.mockStatic(Operations.class)) { - var output = new Operations.ProcessExecOutput("test-output", "test-error", 0); + @Test + @SetEnvironmentVariable(key = "EXHORT_PODMAN_PATH", value = mockPodmanPath) + @SetEnvironmentVariable(key = "PATH", value = mockPath) + void test_podman_get_os() { + try (MockedStatic mock = Mockito.mockStatic(Operations.class)) { + var output = new Operations.ProcessExecOutput("os: test-output", "", 0); + + mock.when(() -> Operations.getCustomPathOrElse(eq("podman"))).thenReturn(mockPodmanPath); - mock.when(() -> Operations.getCustomPathOrElse(eq("skopeo"))) - .thenReturn("skopeo"); + mock.when(() -> Operations.runProcessGetFullOutput( + isNull(), aryEq(new String[] {mockPodmanPath, "info"}), isNull())) + .thenReturn(output); - mock.when(() -> Operations.runProcessGetFullOutput(isNull(), - aryEq(new String[]{"skopeo", "inspect", "--authfile", mockSkopeoConfig, - "", String.format("docker://%s", mockImageRef.getImage().getFullName())}), - isNull())) - .thenReturn(output); - - assertThat(ImageUtils.execSkopeoInspect(mockImageRef, false)).isEqualTo(output); + assertThat(ImageUtils.podmanGetOs()).isEqualTo("test-output"); + } } - } - @Test - @ClearEnvironmentVariable(key = "EXHORT_SKOPEO_PATH") - @ClearEnvironmentVariable(key = EXHORT_SKOPEO_CONFIG_PATH) - @ClearEnvironmentVariable(key = EXHORT_IMAGE_SERVICE_ENDPOINT) - void test_exec_skopeo_inspect_no_config_no_daemon() { - try (MockedStatic mock = Mockito.mockStatic(Operations.class)) { - var output = new Operations.ProcessExecOutput("test-output", "test-error", 0); + @Test + @SetEnvironmentVariable(key = "EXHORT_PODMAN_PATH", value = mockPodmanPath) + @SetEnvironmentVariable(key = "PATH", value = mockPath) + void test_podman_get_arch() { + try (MockedStatic mock = Mockito.mockStatic(Operations.class)) { + var output = new Operations.ProcessExecOutput("arch: test-output", "", 0); - mock.when(() -> Operations.getCustomPathOrElse(eq("skopeo"))) - .thenReturn("skopeo"); + mock.when(() -> Operations.getCustomPathOrElse(eq("podman"))).thenReturn(mockPodmanPath); - mock.when(() -> Operations.runProcessGetFullOutput(isNull(), - aryEq(new String[]{"skopeo", "inspect", - "", String.format("docker://%s", mockImageRef.getImage().getFullName())}), - isNull())) - .thenReturn(output); + mock.when(() -> Operations.runProcessGetFullOutput( + isNull(), aryEq(new String[] {mockPodmanPath, "info"}), isNull())) + .thenReturn(output); - assertThat(ImageUtils.execSkopeoInspect(mockImageRef, false)).isEqualTo(output); + assertThat(ImageUtils.podmanGetArch()).isEqualTo("test-output"); + } } - } - @Test - void test_get_multi_image_digests() throws IOException { - try (var is = getResourceAsStreamDecision(this.getClass(), new String[]{"msc", "image", "skopeo_inspect_multi_raw.json"})) { - var mapper = new ObjectMapper(); - var node = mapper.readTree(is); + @Test + @SetEnvironmentVariable(key = "EXHORT_PODMAN_PATH", value = mockPodmanPath) + @SetEnvironmentVariable(key = "PATH", value = mockPath) + void test_podman_get_variant() { + try (MockedStatic mock = Mockito.mockStatic(Operations.class)) { + var output = new Operations.ProcessExecOutput("variant: test-output", "", 0); - var digests = ImageUtils.getMultiImageDigests(node); - Map expectedDigests = new HashMap<>(); - expectedDigests.put(new Platform("linux", "amd64", null), "sha256:06d06f15f7b641a78f2512c8817cbecaa1bf549488e273f5ac27ff1654ed33f0"); - expectedDigests.put(new Platform("linux", "arm64", null), "sha256:199d5daca3dba0a7deaf0086331917dee256089e94272bef5613517d0007f6f5"); - expectedDigests.put(new Platform("linux", "ppc64le", null), "sha256:1bba662cff053201db85aa55caf3273216a6b0e1766409ee133cf78df9b59314"); - expectedDigests.put(new Platform("linux", "s390x", null), "sha256:b39f9f6998e1693e29b7bd002bc32255fd4f69610e950523b647e61d2bb1dd66"); + mock.when(() -> Operations.getCustomPathOrElse(eq("podman"))).thenReturn(mockPodmanPath); - assertEquals(expectedDigests, digests); + mock.when(() -> Operations.runProcessGetFullOutput( + isNull(), aryEq(new String[] {mockPodmanPath, "info"}), isNull())) + .thenReturn(output); + + assertThat(ImageUtils.podmanGetVariant()).isEqualTo("test-output"); + } } - } - @Test - void test_get_multi_image_digests_empty() { - var node = new TextNode("root"); + @Test + void test_docker_podman_info() { + var info = ImageUtils.dockerPodmanInfo(() -> "docker", () -> "podman"); + assertEquals("docker", info); - var digests = ImageUtils.getMultiImageDigests(node); - Map expectedDigests = Collections.emptyMap(); + info = ImageUtils.dockerPodmanInfo(() -> "", () -> "podman"); + assertEquals("podman", info); + } - assertEquals(expectedDigests, digests); - } + @Test + @SetEnvironmentVariable(key = "EXHORT_SKOPEO_PATH", value = mockSkopeoPath) + @SetEnvironmentVariable(key = EXHORT_SKOPEO_CONFIG_PATH, value = mockSkopeoConfig) + @SetEnvironmentVariable(key = EXHORT_IMAGE_SERVICE_ENDPOINT, value = mockSkopeoDaemon) + void test_exec_skopeo_inspect_raw() { + try (MockedStatic mock = Mockito.mockStatic(Operations.class)) { + var output = new Operations.ProcessExecOutput("test-output", "test-error", 0); + + mock.when(() -> Operations.getCustomPathOrElse(eq("skopeo"))).thenReturn(mockSkopeoPath); + + mock.when(() -> Operations.runProcessGetFullOutput( + isNull(), + aryEq(new String[] { + mockSkopeoPath, + "inspect", + "--authfile", + mockSkopeoConfig, + "--daemon-host", + mockSkopeoDaemon, + "--raw", + String.format( + "docker-daemon:%s", + mockImageRef.getImage().getFullName()) + }), + isNull())) + .thenReturn(output); + + assertThat(ImageUtils.execSkopeoInspect(mockImageRef, true)).isEqualTo(output); + } + } - @Test - void test_filter_mediaType() throws IOException { - try (var is = getResourceAsStreamDecision(this.getClass(), new String[]{"msc", "image", "skopeo_inspect_multi_raw.json"})) { - var mapper = new ObjectMapper(); - var node = mapper.readTree(is); + @Test + @SetEnvironmentVariable(key = "EXHORT_SKOPEO_PATH", value = mockSkopeoPath) + @ClearEnvironmentVariable(key = EXHORT_SKOPEO_CONFIG_PATH) + @SetEnvironmentVariable(key = EXHORT_IMAGE_SERVICE_ENDPOINT, value = mockSkopeoDaemon) + void test_exec_skopeo_inspect_raw_no_config() { + try (MockedStatic mock = Mockito.mockStatic(Operations.class)) { + var output = new Operations.ProcessExecOutput("test-output", "test-error", 0); + + mock.when(() -> Operations.getCustomPathOrElse(eq("skopeo"))).thenReturn(mockSkopeoPath); + + mock.when(() -> Operations.runProcessGetFullOutput( + isNull(), + aryEq(new String[] { + mockSkopeoPath, + "inspect", + "--daemon-host", + mockSkopeoDaemon, + "--raw", + String.format( + "docker-daemon:%s", + mockImageRef.getImage().getFullName()) + }), + isNull())) + .thenReturn(output); + + assertThat(ImageUtils.execSkopeoInspect(mockImageRef, true)).isEqualTo(output); + } + } + + @Test + @ClearEnvironmentVariable(key = "EXHORT_SKOPEO_PATH") + @SetEnvironmentVariable(key = EXHORT_SKOPEO_CONFIG_PATH, value = mockSkopeoConfig) + @ClearEnvironmentVariable(key = EXHORT_IMAGE_SERVICE_ENDPOINT) + void test_exec_skopeo_inspect_raw_no_daemon() { + try (MockedStatic mock = Mockito.mockStatic(Operations.class)) { + var output = new Operations.ProcessExecOutput("test-output", "test-error", 0); + + mock.when(() -> Operations.getCustomPathOrElse(eq("skopeo"))).thenReturn("skopeo"); + + mock.when(() -> Operations.runProcessGetFullOutput( + isNull(), + aryEq(new String[] { + "skopeo", + "inspect", + "--authfile", + mockSkopeoConfig, + "--raw", + String.format( + "docker://%s", mockImageRef.getImage().getFullName()) + }), + isNull())) + .thenReturn(output); + + assertThat(ImageUtils.execSkopeoInspect(mockImageRef, true)).isEqualTo(output); + } + } - assertTrue(ImageUtils.filterMediaType(node.get("manifests").get(0))); + @Test + @ClearEnvironmentVariable(key = "EXHORT_SKOPEO_PATH") + @ClearEnvironmentVariable(key = EXHORT_SKOPEO_CONFIG_PATH) + @ClearEnvironmentVariable(key = EXHORT_IMAGE_SERVICE_ENDPOINT) + void test_exec_skopeo_inspect_raw_no_config_no_daemon() { + try (MockedStatic mock = Mockito.mockStatic(Operations.class)) { + var output = new Operations.ProcessExecOutput("test-output", "test-error", 0); + + mock.when(() -> Operations.getCustomPathOrElse(eq("skopeo"))).thenReturn("skopeo"); + + mock.when(() -> Operations.runProcessGetFullOutput( + isNull(), + aryEq(new String[] { + "skopeo", + "inspect", + "--raw", + String.format( + "docker://%s", mockImageRef.getImage().getFullName()) + }), + isNull())) + .thenReturn(output); + + assertThat(ImageUtils.execSkopeoInspect(mockImageRef, true)).isEqualTo(output); + } } - } - @Test - void test_filter_digest() throws IOException { - try (var is = getResourceAsStreamDecision(this.getClass(), new String[]{"msc", "image", "skopeo_inspect_multi_raw.json"})) { - var mapper = new ObjectMapper(); - var node = mapper.readTree(is); + @Test + @SetEnvironmentVariable(key = "EXHORT_SKOPEO_PATH", value = mockSkopeoPath) + @SetEnvironmentVariable(key = EXHORT_SKOPEO_CONFIG_PATH, value = mockSkopeoConfig) + @SetEnvironmentVariable(key = EXHORT_IMAGE_SERVICE_ENDPOINT, value = mockSkopeoDaemon) + void test_exec_skopeo_inspect() { + try (MockedStatic mock = Mockito.mockStatic(Operations.class)) { + var output = new Operations.ProcessExecOutput("test-output", "test-error", 0); + + mock.when(() -> Operations.getCustomPathOrElse(eq("skopeo"))).thenReturn(mockSkopeoPath); + + mock.when(() -> Operations.runProcessGetFullOutput( + isNull(), + aryEq(new String[] { + mockSkopeoPath, + "inspect", + "--authfile", + mockSkopeoConfig, + "--daemon-host", + mockSkopeoDaemon, + "", + String.format( + "docker-daemon:%s", + mockImageRef.getImage().getFullName()) + }), + isNull())) + .thenReturn(output); + + assertThat(ImageUtils.execSkopeoInspect(mockImageRef, false)).isEqualTo(output); + } + } + + @Test + @SetEnvironmentVariable(key = "EXHORT_SKOPEO_PATH", value = mockSkopeoPath) + @ClearEnvironmentVariable(key = EXHORT_SKOPEO_CONFIG_PATH) + @SetEnvironmentVariable(key = EXHORT_IMAGE_SERVICE_ENDPOINT, value = mockSkopeoDaemon) + void test_exec_skopeo_inspect_no_config() { + try (MockedStatic mock = Mockito.mockStatic(Operations.class)) { + var output = new Operations.ProcessExecOutput("test-output", "test-error", 0); + + mock.when(() -> Operations.getCustomPathOrElse(eq("skopeo"))).thenReturn(mockSkopeoPath); + + mock.when(() -> Operations.runProcessGetFullOutput( + isNull(), + aryEq(new String[] { + mockSkopeoPath, + "inspect", + "--daemon-host", + mockSkopeoDaemon, + "", + String.format( + "docker-daemon:%s", + mockImageRef.getImage().getFullName()) + }), + isNull())) + .thenReturn(output); + + assertThat(ImageUtils.execSkopeoInspect(mockImageRef, false)).isEqualTo(output); + } + } - assertTrue(ImageUtils.filterDigest(node.get("manifests").get(0))); + @Test + @ClearEnvironmentVariable(key = "EXHORT_SKOPEO_PATH") + @SetEnvironmentVariable(key = EXHORT_SKOPEO_CONFIG_PATH, value = mockSkopeoConfig) + @ClearEnvironmentVariable(key = EXHORT_IMAGE_SERVICE_ENDPOINT) + void test_exec_skopeo_inspect_no_daemon() { + try (MockedStatic mock = Mockito.mockStatic(Operations.class)) { + var output = new Operations.ProcessExecOutput("test-output", "test-error", 0); + + mock.when(() -> Operations.getCustomPathOrElse(eq("skopeo"))).thenReturn("skopeo"); + + mock.when(() -> Operations.runProcessGetFullOutput( + isNull(), + aryEq(new String[] { + "skopeo", + "inspect", + "--authfile", + mockSkopeoConfig, + "", + String.format( + "docker://%s", mockImageRef.getImage().getFullName()) + }), + isNull())) + .thenReturn(output); + + assertThat(ImageUtils.execSkopeoInspect(mockImageRef, false)).isEqualTo(output); + } } - } - @Test - void test_filter_platform() throws IOException { - try (var is = getResourceAsStreamDecision(this.getClass(), new String[]{"msc", "image", "skopeo_inspect_multi_raw.json"})) { - var mapper = new ObjectMapper(); - var node = mapper.readTree(is); + @Test + @ClearEnvironmentVariable(key = "EXHORT_SKOPEO_PATH") + @ClearEnvironmentVariable(key = EXHORT_SKOPEO_CONFIG_PATH) + @ClearEnvironmentVariable(key = EXHORT_IMAGE_SERVICE_ENDPOINT) + void test_exec_skopeo_inspect_no_config_no_daemon() { + try (MockedStatic mock = Mockito.mockStatic(Operations.class)) { + var output = new Operations.ProcessExecOutput("test-output", "test-error", 0); + + mock.when(() -> Operations.getCustomPathOrElse(eq("skopeo"))).thenReturn("skopeo"); + + mock.when(() -> Operations.runProcessGetFullOutput( + isNull(), + aryEq(new String[] { + "skopeo", + "inspect", + "", + String.format( + "docker://%s", mockImageRef.getImage().getFullName()) + }), + isNull())) + .thenReturn(output); + + assertThat(ImageUtils.execSkopeoInspect(mockImageRef, false)).isEqualTo(output); + } + } - assertTrue(ImageUtils.filterPlatform(node.get("manifests").get(0))); + @Test + void test_get_multi_image_digests() throws IOException { + try (var is = getResourceAsStreamDecision( + this.getClass(), new String[] {"msc", "image", "skopeo_inspect_multi_raw.json"})) { + var mapper = new ObjectMapper(); + var node = mapper.readTree(is); + + var digests = ImageUtils.getMultiImageDigests(node); + Map expectedDigests = new HashMap<>(); + expectedDigests.put( + new Platform("linux", "amd64", null), + "sha256:06d06f15f7b641a78f2512c8817cbecaa1bf549488e273f5ac27ff1654ed33f0"); + expectedDigests.put( + new Platform("linux", "arm64", null), + "sha256:199d5daca3dba0a7deaf0086331917dee256089e94272bef5613517d0007f6f5"); + expectedDigests.put( + new Platform("linux", "ppc64le", null), + "sha256:1bba662cff053201db85aa55caf3273216a6b0e1766409ee133cf78df9b59314"); + expectedDigests.put( + new Platform("linux", "s390x", null), + "sha256:b39f9f6998e1693e29b7bd002bc32255fd4f69610e950523b647e61d2bb1dd66"); + + assertEquals(expectedDigests, digests); + } } - } - @Test - void test_get_single_image_digest() throws IOException { - try (MockedStatic mock = Mockito.mockStatic(Operations.class); - var is = getResourceAsStreamDecision(this.getClass(), new String[]{"msc", "image", "skopeo_inspect_single.json"})) { - var json = new BufferedReader(new InputStreamReader(is, StandardCharsets.UTF_8)).lines().collect(Collectors.joining("\n")); - var output = new Operations.ProcessExecOutput(json, "", 0); + @Test + void test_get_multi_image_digests_empty() { + var node = new TextNode("root"); - mock.when(() -> Operations.getCustomPathOrElse(eq("skopeo"))) - .thenReturn("skopeo"); + var digests = ImageUtils.getMultiImageDigests(node); + Map expectedDigests = Collections.emptyMap(); - mock.when(() -> Operations.runProcessGetFullOutput(isNull(), - aryEq(new String[]{"skopeo", "inspect", - "", String.format("docker://%s", mockImageRef.getImage().getFullName())}), - isNull())) - .thenReturn(output); + assertEquals(expectedDigests, digests); + } - var digests = ImageUtils.getSingleImageDigest(mockImageRef); - Map expectedDigests = Collections.singletonMap(Platform.EMPTY_PLATFORM, "sha256:9aa20fd4e4842854ec1c081d2dae77c686601a8640018d68782f36c60eb1a19e"); + @Test + void test_filter_mediaType() throws IOException { + try (var is = getResourceAsStreamDecision( + this.getClass(), new String[] {"msc", "image", "skopeo_inspect_multi_raw.json"})) { + var mapper = new ObjectMapper(); + var node = mapper.readTree(is); - assertEquals(expectedDigests, digests); + assertTrue(ImageUtils.filterMediaType(node.get("manifests").get(0))); + } } - } - @Test - void test_get_single_image_digest_empty() throws JsonProcessingException { - try (MockedStatic mock = Mockito.mockStatic(Operations.class)) { - var mapper = new ObjectMapper(); - var node = new TextNode("root"); - var output = new Operations.ProcessExecOutput(mapper.writeValueAsString(node), "", 0); + @Test + void test_filter_digest() throws IOException { + try (var is = getResourceAsStreamDecision( + this.getClass(), new String[] {"msc", "image", "skopeo_inspect_multi_raw.json"})) { + var mapper = new ObjectMapper(); + var node = mapper.readTree(is); + + assertTrue(ImageUtils.filterDigest(node.get("manifests").get(0))); + } + } - mock.when(() -> Operations.getCustomPathOrElse(eq("skopeo"))) - .thenReturn("skopeo"); + @Test + void test_filter_platform() throws IOException { + try (var is = getResourceAsStreamDecision( + this.getClass(), new String[] {"msc", "image", "skopeo_inspect_multi_raw.json"})) { + var mapper = new ObjectMapper(); + var node = mapper.readTree(is); - mock.when(() -> Operations.runProcessGetFullOutput(isNull(), - aryEq(new String[]{"skopeo", "inspect", - "", String.format("docker://%s", mockImageRef.getImage().getFullName())}), - isNull())) - .thenReturn(output); + assertTrue(ImageUtils.filterPlatform(node.get("manifests").get(0))); + } + } - var digests = ImageUtils.getSingleImageDigest(mockImageRef); - Map expectedDigests = Collections.emptyMap(); + @Test + void test_get_single_image_digest() throws IOException { + try (MockedStatic mock = Mockito.mockStatic(Operations.class); + var is = getResourceAsStreamDecision( + this.getClass(), new String[] {"msc", "image", "skopeo_inspect_single.json"})) { + var json = new BufferedReader(new InputStreamReader(is, StandardCharsets.UTF_8)) + .lines() + .collect(Collectors.joining("\n")); + var output = new Operations.ProcessExecOutput(json, "", 0); + + mock.when(() -> Operations.getCustomPathOrElse(eq("skopeo"))).thenReturn("skopeo"); + + mock.when(() -> Operations.runProcessGetFullOutput( + isNull(), + aryEq(new String[] { + "skopeo", + "inspect", + "", + String.format( + "docker://%s", mockImageRef.getImage().getFullName()) + }), + isNull())) + .thenReturn(output); + + var digests = ImageUtils.getSingleImageDigest(mockImageRef); + Map expectedDigests = Collections.singletonMap( + Platform.EMPTY_PLATFORM, "sha256:9aa20fd4e4842854ec1c081d2dae77c686601a8640018d68782f36c60eb1a19e"); + + assertEquals(expectedDigests, digests); + } + } - assertEquals(expectedDigests, digests); + @Test + void test_get_single_image_digest_empty() throws JsonProcessingException { + try (MockedStatic mock = Mockito.mockStatic(Operations.class)) { + var mapper = new ObjectMapper(); + var node = new TextNode("root"); + var output = new Operations.ProcessExecOutput(mapper.writeValueAsString(node), "", 0); + + mock.when(() -> Operations.getCustomPathOrElse(eq("skopeo"))).thenReturn("skopeo"); + + mock.when(() -> Operations.runProcessGetFullOutput( + isNull(), + aryEq(new String[] { + "skopeo", + "inspect", + "", + String.format( + "docker://%s", mockImageRef.getImage().getFullName()) + }), + isNull())) + .thenReturn(output); + + var digests = ImageUtils.getSingleImageDigest(mockImageRef); + Map expectedDigests = Collections.emptyMap(); + + assertEquals(expectedDigests, digests); + } } - } } diff --git a/src/test/java/com/redhat/exhort/image/PlatformTest.java b/src/test/java/com/redhat/exhort/image/PlatformTest.java index e21667f9..fbce17e4 100644 --- a/src/test/java/com/redhat/exhort/image/PlatformTest.java +++ b/src/test/java/com/redhat/exhort/image/PlatformTest.java @@ -15,199 +15,91 @@ */ package com.redhat.exhort.image; +import static org.junit.jupiter.api.Assertions.*; + +import java.util.stream.Stream; import org.junit.jupiter.api.Named; import org.junit.jupiter.api.Test; import org.junit.jupiter.params.ParameterizedTest; import org.junit.jupiter.params.provider.Arguments; import org.junit.jupiter.params.provider.MethodSource; -import java.util.stream.Stream; - -import static org.junit.jupiter.api.Assertions.*; - class PlatformTest { - static Stream PlatformSources() { - return Stream.of( - Arguments.of( - Named.of( - "amd64", "amd64" - ), - "linux", - "amd64", - null, - "linux/amd64", - false - ), - Arguments.of( - Named.of( - "linux/amd64", "linux/amd64" - ), - "linux", - "amd64", - null, - "linux/amd64", - false - ), - Arguments.of( - Named.of( - "linux/arm/v5", "linux/arm/v5" - ), - "linux", - "arm", - "v5", - "linux/arm/v5", - true - ), - Arguments.of( - Named.of( - "linux/arm/v6", "linux/arm/v6" - ), - "linux", - "arm", - "v6", - "linux/arm/v6", - true - ), - Arguments.of( - Named.of( - "linux/arm/v7", "linux/arm/v7" - ), - "linux", - "arm", - "v7", - "linux/arm/v7", - true - ), - Arguments.of( - Named.of( - "linux/arm64", "linux/arm64" - ), - "linux", - "arm64", - "v8", - "linux/arm64/v8", - false - ), - Arguments.of( - Named.of( - "linux/arm64/v8", "linux/arm64/v8" - ), - "linux", - "arm64", - "v8", - "linux/arm64/v8", - false - ), - Arguments.of( - Named.of( - "linux/386", "linux/386" - ), - "linux", - "386", - null, - "linux/386", - false - ), - Arguments.of( - Named.of( - "linux/mips64le", "linux/mips64le" - ), - "linux", - "mips64le", - null, - "linux/mips64le", - false - ), - Arguments.of( - Named.of( - "linux/ppc64le", "linux/ppc64le" - ), - "linux", - "ppc64le", - null, - "linux/ppc64le", - false - ), - Arguments.of( - Named.of( - "linux/riscv64", "linux/riscv64" - ), - "linux", - "riscv64", - null, - "linux/riscv64", - false - ), - Arguments.of( - Named.of( - "linux/s390x", "linux/s390x" - ), - "linux", - "s390x", - null, - "linux/s390x", - false - ), - Arguments.of( - Named.of( - "windows/arm64", "windows/arm64" - ), - "windows", - "arm64", - null, - "windows/arm64", - false - ) - ); - } + static Stream PlatformSources() { + return Stream.of( + Arguments.of(Named.of("amd64", "amd64"), "linux", "amd64", null, "linux/amd64", false), + Arguments.of(Named.of("linux/amd64", "linux/amd64"), "linux", "amd64", null, "linux/amd64", false), + Arguments.of(Named.of("linux/arm/v5", "linux/arm/v5"), "linux", "arm", "v5", "linux/arm/v5", true), + Arguments.of(Named.of("linux/arm/v6", "linux/arm/v6"), "linux", "arm", "v6", "linux/arm/v6", true), + Arguments.of(Named.of("linux/arm/v7", "linux/arm/v7"), "linux", "arm", "v7", "linux/arm/v7", true), + Arguments.of(Named.of("linux/arm64", "linux/arm64"), "linux", "arm64", "v8", "linux/arm64/v8", false), + Arguments.of( + Named.of("linux/arm64/v8", "linux/arm64/v8"), "linux", "arm64", "v8", "linux/arm64/v8", false), + Arguments.of(Named.of("linux/386", "linux/386"), "linux", "386", null, "linux/386", false), + Arguments.of( + Named.of("linux/mips64le", "linux/mips64le"), + "linux", + "mips64le", + null, + "linux/mips64le", + false), + Arguments.of( + Named.of("linux/ppc64le", "linux/ppc64le"), "linux", "ppc64le", null, "linux/ppc64le", false), + Arguments.of( + Named.of("linux/riscv64", "linux/riscv64"), "linux", "riscv64", null, "linux/riscv64", false), + Arguments.of(Named.of("linux/s390x", "linux/s390x"), "linux", "s390x", null, "linux/s390x", false), + Arguments.of( + Named.of("windows/arm64", "windows/arm64"), "windows", "arm64", null, "windows/arm64", false)); + } - @ParameterizedTest(name = "{0}") - @MethodSource("PlatformSources") - void test_platform(String platform, String os, String arch, String variant, String platformStr, boolean variantRequired) { - var p = new Platform(platform); + @ParameterizedTest(name = "{0}") + @MethodSource("PlatformSources") + void test_platform( + String platform, String os, String arch, String variant, String platformStr, boolean variantRequired) { + var p = new Platform(platform); - assertEquals(os, p.getOs()); - assertEquals(arch, p.getArchitecture()); - assertEquals(variant, p.getVariant()); - assertEquals(platformStr, p.toString()); - assertEquals(variantRequired, Platform.isVariantRequired(p.getOs(), p.getArchitecture())); + assertEquals(os, p.getOs()); + assertEquals(arch, p.getArchitecture()); + assertEquals(variant, p.getVariant()); + assertEquals(platformStr, p.toString()); + assertEquals(variantRequired, Platform.isVariantRequired(p.getOs(), p.getArchitecture())); - var pf = new Platform(os, arch, variant); - assertTrue(p.equals(pf)); - assertEquals(p.hashCode(), pf.hashCode()); - } + var pf = new Platform(os, arch, variant); + assertTrue(p.equals(pf)); + assertEquals(p.hashCode(), pf.hashCode()); + } - @Test - void test_platform_invalid() { - var exception1 = assertThrows(IllegalArgumentException.class, () -> { - new Platform(null); - }); - assertEquals("Invalid platform: null", exception1.getMessage()); + @Test + void test_platform_invalid() { + var exception1 = assertThrows(IllegalArgumentException.class, () -> { + new Platform(null); + }); + assertEquals("Invalid platform: null", exception1.getMessage()); - var exception2 = assertThrows(IllegalArgumentException.class, () -> { - new Platform("linux/arm/v8/a"); - }); - assertEquals("Invalid platform: linux/arm/v8/a", exception2.getMessage()); + var exception2 = assertThrows(IllegalArgumentException.class, () -> { + new Platform("linux/arm/v8/a"); + }); + assertEquals("Invalid platform: linux/arm/v8/a", exception2.getMessage()); - var exception3 = assertThrows(IllegalArgumentException.class, () -> { - new Platform("linux/abc"); - }); - assertEquals("Image platform is not supported: linux/abc", exception3.getMessage()); + var exception3 = assertThrows(IllegalArgumentException.class, () -> { + new Platform("linux/abc"); + }); + assertEquals("Image platform is not supported: linux/abc", exception3.getMessage()); - var exception4 = assertThrows(IllegalArgumentException.class, () -> { - new Platform("", null, ""); - }); - assertEquals("Invalid platform arch: null", exception4.getMessage()); + var exception4 = assertThrows(IllegalArgumentException.class, () -> { + new Platform("", null, ""); + }); + assertEquals("Invalid platform arch: null", exception4.getMessage()); - var exception5 = assertThrows(IllegalArgumentException.class, () -> { - new Platform("linux", "arm", "v8"); - }); - assertEquals("Image platform is not supported: linux/arm/v8", exception5.getMessage()); + var exception5 = assertThrows(IllegalArgumentException.class, () -> { + new Platform("linux", "arm", "v8"); + }); + assertEquals("Image platform is not supported: linux/arm/v8", exception5.getMessage()); - var exception6 = assertThrows(IllegalArgumentException.class, () -> { - new Platform(null, "arm", null); - }); - assertEquals("Image platform is not supported: null/arm/null", exception6.getMessage()); - } + var exception6 = assertThrows(IllegalArgumentException.class, () -> { + new Platform(null, "arm", null); + }); + assertEquals("Image platform is not supported: null/arm/null", exception6.getMessage()); + } } diff --git a/src/test/java/com/redhat/exhort/impl/ExhortApiIT.java b/src/test/java/com/redhat/exhort/impl/ExhortApiIT.java index 149dd225..5b9ad56e 100644 --- a/src/test/java/com/redhat/exhort/impl/ExhortApiIT.java +++ b/src/test/java/com/redhat/exhort/impl/ExhortApiIT.java @@ -15,6 +15,13 @@ */ package com.redhat.exhort.impl; +import static org.junit.jupiter.api.Assertions.*; +import static org.mockito.AdditionalMatchers.aryEq; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.ArgumentMatchers.isNull; +import static org.mockito.Mockito.mockStatic; + import com.fasterxml.jackson.core.JsonProcessingException; import com.fasterxml.jackson.databind.JsonNode; import com.fasterxml.jackson.databind.ObjectMapper; @@ -26,18 +33,6 @@ import com.redhat.exhort.providers.HelperExtension; import com.redhat.exhort.tools.Ecosystem; import com.redhat.exhort.tools.Operations; -import org.junit.jupiter.api.AfterAll; -import org.junit.jupiter.api.BeforeAll; -import org.junit.jupiter.api.Tag; -import org.junit.jupiter.api.Test; -import org.junit.jupiter.api.extension.ExtendWith; -import org.junit.jupiter.params.ParameterizedTest; -import org.junit.jupiter.params.provider.EnumSource; -import org.mockito.MockedStatic; -import org.mockito.Mockito; -import org.mockito.invocation.InvocationOnMock; -import org.mockito.junit.jupiter.MockitoExtension; - import java.io.BufferedReader; import java.io.IOException; import java.io.InputStreamReader; @@ -54,248 +49,295 @@ import java.util.concurrent.ExecutionException; import java.util.function.Function; import java.util.stream.Collectors; - -import static org.junit.jupiter.api.Assertions.*; -import static org.mockito.AdditionalMatchers.aryEq; -import static org.mockito.ArgumentMatchers.any; -import static org.mockito.ArgumentMatchers.eq; -import static org.mockito.ArgumentMatchers.isNull; -import static org.mockito.Mockito.mockStatic; +import org.junit.jupiter.api.AfterAll; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.Tag; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.EnumSource; +import org.mockito.MockedStatic; +import org.mockito.Mockito; +import org.mockito.invocation.InvocationOnMock; +import org.mockito.junit.jupiter.MockitoExtension; @Tag("IntegrationTest") @ExtendWith(HelperExtension.class) @ExtendWith(MockitoExtension.class) class ExhortApiIT extends ExhortTest { - private static Api api; - private static Map ecoSystemsManifestNames; - - private MockedStatic mockedOperations; - @BeforeAll - static void beforeAll() { - api = new ExhortApi(); - System.setProperty("RHDA_SOURCE","exhort-java-api-it"); - System.setProperty("EXHORT_DEV_MODE","false"); - ecoSystemsManifestNames = Map.of("golang", "go.mod","maven","pom.xml","npm","package.json","pypi","requirements.txt", "gradle", "build.gradle"); - - } - - @Tag("IntegrationTest") - @AfterAll - static void afterAll() { - System.clearProperty("RHDA_SOURCE"); - System.clearProperty("EXHORT_DEV_MODE"); - api = null; - } - @Tag("IntegrationTest") - @ParameterizedTest - @EnumSource(value = Ecosystem.Type.class, names = { "GOLANG", "MAVEN", "NPM", "PYTHON", "GRADLE" }) - void Integration_Test_End_To_End_Stack_Analysis(Ecosystem.Type packageManager) throws IOException, ExecutionException, InterruptedException { - String manifestFileName = ecoSystemsManifestNames.get(packageManager.getType()); - String pathToManifest = getFileFromResource(manifestFileName, "tst_manifests", "it", packageManager.getType(), manifestFileName); - preparePythonEnvironment(packageManager); - // Github action runner with all maven and java versions seems to enter infinite loop in integration tests of MAVEN when runnig dependency maven plugin to produce verbose text dependenct tree format. - // locally it's not recreated with same versions - mockMavenDependencyTree(packageManager); - AnalysisReport analysisReportResult = api.stackAnalysis(pathToManifest).get(); - handleJsonResponse(analysisReportResult,true); - releaseStaticMock(packageManager); - } - - private void releaseStaticMock(Ecosystem.Type packageManager) { - if(packageManager.equals(Ecosystem.Type.MAVEN)) { - this.mockedOperations.close(); + private static Api api; + private static Map ecoSystemsManifestNames; + + private MockedStatic mockedOperations; + + @BeforeAll + static void beforeAll() { + api = new ExhortApi(); + System.setProperty("RHDA_SOURCE", "exhort-java-api-it"); + System.setProperty("EXHORT_DEV_MODE", "false"); + ecoSystemsManifestNames = Map.of( + "golang", + "go.mod", + "maven", + "pom.xml", + "npm", + "package.json", + "pypi", + "requirements.txt", + "gradle", + "build.gradle"); } - } - - - @Tag("IntegrationTest") - @ParameterizedTest - @EnumSource(value = Ecosystem.Type.class, names = { "GOLANG", "MAVEN", "NPM", "PYTHON", "GRADLE" }) - void Integration_Test_End_To_End_Stack_Analysis_Mixed(Ecosystem.Type packageManager) throws IOException, ExecutionException, InterruptedException { - String manifestFileName = ecoSystemsManifestNames.get(packageManager.getType()); - String pathToManifest = getFileFromResource(manifestFileName, "tst_manifests", "it", packageManager.getType(), manifestFileName); - preparePythonEnvironment(packageManager); - // Github action runner with all maven and java versions seems to enter infinite loop in integration tests of MAVEN when runnig dependency maven plugin to produce verbose text dependenct tree format. - // locally it's not recreated with same versions - mockMavenDependencyTree(packageManager); - AnalysisReport analysisReportJson = api.stackAnalysisMixed(pathToManifest).get().json; - String analysisReportHtml = new String(api.stackAnalysisMixed(pathToManifest).get().html); - handleJsonResponse(analysisReportJson,true); - handleHtmlResponse(analysisReportHtml); - releaseStaticMock(packageManager); - } - - @Tag("IntegrationTest") - @ParameterizedTest - @EnumSource(value = Ecosystem.Type.class, names = { "GOLANG", "MAVEN", "NPM", "PYTHON", "GRADLE" }) - void Integration_Test_End_To_End_Stack_Analysis_Html(Ecosystem.Type packageManager) throws IOException, ExecutionException, InterruptedException { - String manifestFileName = ecoSystemsManifestNames.get(packageManager.getType()); - String pathToManifest = getFileFromResource(manifestFileName, "tst_manifests", "it", packageManager.getType(), manifestFileName); - preparePythonEnvironment(packageManager); - // Github action runner with all maven and java versions seems to enter infinite loop in integration tests of MAVEN when running dependency maven plugin to produce verbose text dependenct tree format. - // locally it's not recreated with same versions - mockMavenDependencyTree(packageManager); - String analysisReportHtml = new String(api.stackAnalysisHtml(pathToManifest).get()); - releaseStaticMock(packageManager); - handleHtmlResponse(analysisReportHtml); - } - - - @Tag("IntegrationTest") - @ParameterizedTest - @EnumSource(value = Ecosystem.Type.class, names = { "GOLANG", "MAVEN", "NPM", "PYTHON" }) - void Integration_Test_End_To_End_Component_Analysis(Ecosystem.Type packageManager) throws IOException, ExecutionException, InterruptedException { - String manifestFileName = ecoSystemsManifestNames.get(packageManager.getType()); - byte[] manifestContent = getStringFromFile("tst_manifests", "it", packageManager.getType(), manifestFileName).getBytes(); - preparePythonEnvironment(packageManager); - AnalysisReport analysisReportResult = api.componentAnalysis(manifestFileName,manifestContent).get(); - handleJsonResponse(analysisReportResult,false); - } - - @Tag("IntegrationTest") - @Test - void Integration_Test_End_To_End_Image_Analysis() throws IOException { - var result = testImageAnalysis(i -> { - try { - return api.imageAnalysis(i).get(); - } catch (InterruptedException | ExecutionException | IOException e) { - throw new RuntimeException(e); - } - }); - - assertEquals(1, result.size()); - handleJsonResponse(new ArrayList<>(result.values()).get(0), false); - } - @Tag("IntegrationTest") - @Test - void Integration_Test_End_To_End_Image_Analysis_Html() throws IOException { - var result = testImageAnalysis(i -> { - try { - return api.imageAnalysisHtml(i).get(); - } catch (InterruptedException | ExecutionException | IOException e) { - throw new RuntimeException(e); - } - }); - - handleHtmlResponseForImage(new String(result)); - } - - private static T testImageAnalysis(Function, T> imageAnalysisFunction) throws IOException { - try (MockedStatic mock = Mockito.mockStatic(Operations.class); - var sbomIS = getResourceAsStreamDecision(ExhortApiIT.class, new String[]{"msc", "image", "image_sbom.json"})) { - - var imageRef = new ImageRef("test.io/test/test-app:test-version@sha256:1fafb0905264413501df60d90a92ca32df8a2011cbfb4876ddff5ceb20c8f165", "linux/amd64"); - - var jsonSbom = new BufferedReader(new InputStreamReader(sbomIS, StandardCharsets.UTF_8)).lines().collect(Collectors.joining("\n")); - var output = new Operations.ProcessExecOutput(jsonSbom, "", 0); + @Tag("IntegrationTest") + @AfterAll + static void afterAll() { + System.clearProperty("RHDA_SOURCE"); + System.clearProperty("EXHORT_DEV_MODE"); + api = null; + } - mock.when(() -> Operations.getCustomPathOrElse(eq("syft"))) - .thenReturn("syft"); + @Tag("IntegrationTest") + @ParameterizedTest + @EnumSource( + value = Ecosystem.Type.class, + names = {"GOLANG", "MAVEN", "NPM", "PYTHON", "GRADLE"}) + void Integration_Test_End_To_End_Stack_Analysis(Ecosystem.Type packageManager) + throws IOException, ExecutionException, InterruptedException { + String manifestFileName = ecoSystemsManifestNames.get(packageManager.getType()); + String pathToManifest = getFileFromResource( + manifestFileName, "tst_manifests", "it", packageManager.getType(), manifestFileName); + preparePythonEnvironment(packageManager); + // Github action runner with all maven and java versions seems to enter infinite loop in integration tests of + // MAVEN when runnig dependency maven plugin to produce verbose text dependenct tree format. + // locally it's not recreated with same versions + mockMavenDependencyTree(packageManager); + AnalysisReport analysisReportResult = api.stackAnalysis(pathToManifest).get(); + handleJsonResponse(analysisReportResult, true); + releaseStaticMock(packageManager); + } - mock.when(() -> Operations.runProcessGetFullOutput(isNull(), - aryEq(new String[]{"syft", imageRef.getImage().getFullName(), - "-s", "all-layers", "-o", "cyclonedx-json", "-q"}), - isNull())) - .thenReturn(output); + private void releaseStaticMock(Ecosystem.Type packageManager) { + if (packageManager.equals(Ecosystem.Type.MAVEN)) { + this.mockedOperations.close(); + } + } - return imageAnalysisFunction.apply(Set.of(imageRef)); + @Tag("IntegrationTest") + @ParameterizedTest + @EnumSource( + value = Ecosystem.Type.class, + names = {"GOLANG", "MAVEN", "NPM", "PYTHON", "GRADLE"}) + void Integration_Test_End_To_End_Stack_Analysis_Mixed(Ecosystem.Type packageManager) + throws IOException, ExecutionException, InterruptedException { + String manifestFileName = ecoSystemsManifestNames.get(packageManager.getType()); + String pathToManifest = getFileFromResource( + manifestFileName, "tst_manifests", "it", packageManager.getType(), manifestFileName); + preparePythonEnvironment(packageManager); + // Github action runner with all maven and java versions seems to enter infinite loop in integration tests of + // MAVEN when runnig dependency maven plugin to produce verbose text dependenct tree format. + // locally it's not recreated with same versions + mockMavenDependencyTree(packageManager); + AnalysisReport analysisReportJson = + api.stackAnalysisMixed(pathToManifest).get().json; + String analysisReportHtml = + new String(api.stackAnalysisMixed(pathToManifest).get().html); + handleJsonResponse(analysisReportJson, true); + handleHtmlResponse(analysisReportHtml); + releaseStaticMock(packageManager); } - } - private static void preparePythonEnvironment(Ecosystem.Type packageManager) { - if(packageManager.equals(Ecosystem.Type.PYTHON)) { - System.setProperty("EXHORT_PYTHON_VIRTUAL_ENV","true"); - System.setProperty("EXHORT_PYTHON_INSTALL_BEST_EFFORTS","true"); - System.setProperty("MATCH_MANIFEST_VERSIONS","false"); + @Tag("IntegrationTest") + @ParameterizedTest + @EnumSource( + value = Ecosystem.Type.class, + names = {"GOLANG", "MAVEN", "NPM", "PYTHON", "GRADLE"}) + void Integration_Test_End_To_End_Stack_Analysis_Html(Ecosystem.Type packageManager) + throws IOException, ExecutionException, InterruptedException { + String manifestFileName = ecoSystemsManifestNames.get(packageManager.getType()); + String pathToManifest = getFileFromResource( + manifestFileName, "tst_manifests", "it", packageManager.getType(), manifestFileName); + preparePythonEnvironment(packageManager); + // Github action runner with all maven and java versions seems to enter infinite loop in integration tests of + // MAVEN when running dependency maven plugin to produce verbose text dependenct tree format. + // locally it's not recreated with same versions + mockMavenDependencyTree(packageManager); + String analysisReportHtml = + new String(api.stackAnalysisHtml(pathToManifest).get()); + releaseStaticMock(packageManager); + handleHtmlResponse(analysisReportHtml); } - else { - System.clearProperty("EXHORT_PYTHON_VIRTUAL_ENV"); - System.clearProperty("EXHORT_PYTHON_INSTALL_BEST_EFFORTS"); - System.clearProperty("MATCH_MANIFEST_VERSIONS"); + + @Tag("IntegrationTest") + @ParameterizedTest + @EnumSource( + value = Ecosystem.Type.class, + names = {"GOLANG", "MAVEN", "NPM", "PYTHON"}) + void Integration_Test_End_To_End_Component_Analysis(Ecosystem.Type packageManager) + throws IOException, ExecutionException, InterruptedException { + String manifestFileName = ecoSystemsManifestNames.get(packageManager.getType()); + byte[] manifestContent = getStringFromFile("tst_manifests", "it", packageManager.getType(), manifestFileName) + .getBytes(); + preparePythonEnvironment(packageManager); + AnalysisReport analysisReportResult = + api.componentAnalysis(manifestFileName, manifestContent).get(); + handleJsonResponse(analysisReportResult, false); } - } - private static void handleJsonResponse(AnalysisReport analysisReportResult, boolean positiveNumberOfTransitives) { - analysisReportResult.getProviders().entrySet().stream().forEach(provider -> { assertTrue(provider.getValue().getStatus().getOk()); - assertTrue(provider.getValue().getStatus().getCode() == HttpURLConnection.HTTP_OK); - }); - analysisReportResult.getProviders().entrySet().stream() - .map(Map.Entry::getValue) - .map(ProviderReport::getSources) - .map(Map::entrySet) - .flatMap(Collection::stream) - .map(Map.Entry::getValue) - .forEach( source -> assertTrue(source.getSummary().getTotal() > 0 )); + @Tag("IntegrationTest") + @Test + void Integration_Test_End_To_End_Image_Analysis() throws IOException { + var result = testImageAnalysis(i -> { + try { + return api.imageAnalysis(i).get(); + } catch (InterruptedException | ExecutionException | IOException e) { + throw new RuntimeException(e); + } + }); + + assertEquals(1, result.size()); + handleJsonResponse(new ArrayList<>(result.values()).get(0), false); + } - if(positiveNumberOfTransitives) { - assertTrue(analysisReportResult.getScanned().getTransitive() > 0); + @Tag("IntegrationTest") + @Test + void Integration_Test_End_To_End_Image_Analysis_Html() throws IOException { + var result = testImageAnalysis(i -> { + try { + return api.imageAnalysisHtml(i).get(); + } catch (InterruptedException | ExecutionException | IOException e) { + throw new RuntimeException(e); + } + }); + + handleHtmlResponseForImage(new String(result)); } - else { - assertEquals(0,analysisReportResult.getScanned().getTransitive()); + + private static T testImageAnalysis(Function, T> imageAnalysisFunction) throws IOException { + try (MockedStatic mock = Mockito.mockStatic(Operations.class); + var sbomIS = getResourceAsStreamDecision( + ExhortApiIT.class, new String[] {"msc", "image", "image_sbom.json"})) { + + var imageRef = new ImageRef( + "test.io/test/test-app:test-version@sha256:1fafb0905264413501df60d90a92ca32df8a2011cbfb4876ddff5ceb20c8f165", + "linux/amd64"); + + var jsonSbom = new BufferedReader(new InputStreamReader(sbomIS, StandardCharsets.UTF_8)) + .lines() + .collect(Collectors.joining("\n")); + var output = new Operations.ProcessExecOutput(jsonSbom, "", 0); + + mock.when(() -> Operations.getCustomPathOrElse(eq("syft"))).thenReturn("syft"); + + mock.when(() -> Operations.runProcessGetFullOutput( + isNull(), + aryEq(new String[] { + "syft", + imageRef.getImage().getFullName(), + "-s", + "all-layers", + "-o", + "cyclonedx-json", + "-q" + }), + isNull())) + .thenReturn(output); + + return imageAnalysisFunction.apply(Set.of(imageRef)); + } } - } - private void handleHtmlResponse(String analysisReportHtml) throws JsonProcessingException { - ObjectMapper om = new ObjectMapper(); - assertTrue(analysisReportHtml.contains("svg") && analysisReportHtml.contains("html")); - int jsonStart = analysisReportHtml.indexOf("\"report\":"); - int jsonEnd = analysisReportHtml.indexOf("}}}}}"); - if(jsonEnd == -1) { - jsonEnd = analysisReportHtml.indexOf("}}}}"); + private static void preparePythonEnvironment(Ecosystem.Type packageManager) { + if (packageManager.equals(Ecosystem.Type.PYTHON)) { + System.setProperty("EXHORT_PYTHON_VIRTUAL_ENV", "true"); + System.setProperty("EXHORT_PYTHON_INSTALL_BEST_EFFORTS", "true"); + System.setProperty("MATCH_MANIFEST_VERSIONS", "false"); + } else { + System.clearProperty("EXHORT_PYTHON_VIRTUAL_ENV"); + System.clearProperty("EXHORT_PYTHON_INSTALL_BEST_EFFORTS"); + System.clearProperty("MATCH_MANIFEST_VERSIONS"); + } } - String embeddedJson = analysisReportHtml.substring(jsonStart + 9 ,jsonEnd + 5); - JsonNode jsonInHtml = om.readTree(embeddedJson); - JsonNode scannedNode = jsonInHtml.get("scanned"); - assertTrue(scannedNode.get("total").asInt(0) > 0); - assertTrue(scannedNode.get("transitive").asInt(0) > 0); - JsonNode status = jsonInHtml.get("providers").get("osv-nvd").get("status"); - assertTrue(status.get("code").asInt(0) == 200); - assertTrue(status.get("ok").asBoolean(false)); - } + private static void handleJsonResponse(AnalysisReport analysisReportResult, boolean positiveNumberOfTransitives) { + analysisReportResult.getProviders().entrySet().stream().forEach(provider -> { + assertTrue(provider.getValue().getStatus().getOk()); + assertTrue(provider.getValue().getStatus().getCode() == HttpURLConnection.HTTP_OK); + }); + analysisReportResult.getProviders().entrySet().stream() + .map(Map.Entry::getValue) + .map(ProviderReport::getSources) + .map(Map::entrySet) + .flatMap(Collection::stream) + .map(Map.Entry::getValue) + .forEach(source -> assertTrue(source.getSummary().getTotal() > 0)); + + if (positiveNumberOfTransitives) { + assertTrue(analysisReportResult.getScanned().getTransitive() > 0); + } else { + assertEquals(0, analysisReportResult.getScanned().getTransitive()); + } + } - private void handleHtmlResponseForImage(String analysisReportHtml) throws JsonProcessingException { - ObjectMapper om = new ObjectMapper(); - assertTrue(analysisReportHtml.contains("svg") && analysisReportHtml.contains("html")); - int jsonStart = analysisReportHtml.indexOf("\"report\":"); - int jsonEnd = analysisReportHtml.indexOf("}}}}}}"); - String embeddedJson = analysisReportHtml.substring(jsonStart + 9 ,jsonEnd + 6); - JsonNode jsonInHtml = om.readTree(embeddedJson); - JsonNode scannedNode = jsonInHtml.findValue("scanned"); - assertTrue(scannedNode.get("total").asInt(0) > 0); - assertTrue(scannedNode.get("transitive").asInt(0) >= 0); - JsonNode status = jsonInHtml.findValue("providers").get("osv-nvd").get("status"); - assertTrue(status.get("code").asInt(0) == 200); - assertTrue(status.get("ok").asBoolean(false)); - } + private void handleHtmlResponse(String analysisReportHtml) throws JsonProcessingException { + ObjectMapper om = new ObjectMapper(); + assertTrue(analysisReportHtml.contains("svg") && analysisReportHtml.contains("html")); + int jsonStart = analysisReportHtml.indexOf("\"report\":"); + int jsonEnd = analysisReportHtml.indexOf("}}}}}"); + if (jsonEnd == -1) { + jsonEnd = analysisReportHtml.indexOf("}}}}"); + } + String embeddedJson = analysisReportHtml.substring(jsonStart + 9, jsonEnd + 5); + JsonNode jsonInHtml = om.readTree(embeddedJson); + JsonNode scannedNode = jsonInHtml.get("scanned"); + assertTrue(scannedNode.get("total").asInt(0) > 0); + assertTrue(scannedNode.get("transitive").asInt(0) > 0); + JsonNode status = jsonInHtml.get("providers").get("osv-nvd").get("status"); + assertTrue(status.get("code").asInt(0) == 200); + assertTrue(status.get("ok").asBoolean(false)); + } - private void mockMavenDependencyTree(Ecosystem.Type packageManager) throws IOException { - if(packageManager.equals(Ecosystem.Type.MAVEN)) { - mockedOperations = mockStatic(Operations.class); - String depTree; - try (var is = getResourceAsStreamDecision(getClass(), new String [] { "tst_manifests", "it","maven", "depTree.txt"})) { - depTree = new String(is.readAllBytes()); - } - mockedOperations.when(() -> Operations.runProcess(any(),any())).thenAnswer(invocationOnMock -> { return getOutputFileAndOverwriteItWithMock(depTree, invocationOnMock, "-DoutputFile");}); + private void handleHtmlResponseForImage(String analysisReportHtml) throws JsonProcessingException { + ObjectMapper om = new ObjectMapper(); + assertTrue(analysisReportHtml.contains("svg") && analysisReportHtml.contains("html")); + int jsonStart = analysisReportHtml.indexOf("\"report\":"); + int jsonEnd = analysisReportHtml.indexOf("}}}}}}"); + String embeddedJson = analysisReportHtml.substring(jsonStart + 9, jsonEnd + 6); + JsonNode jsonInHtml = om.readTree(embeddedJson); + JsonNode scannedNode = jsonInHtml.findValue("scanned"); + assertTrue(scannedNode.get("total").asInt(0) > 0); + assertTrue(scannedNode.get("transitive").asInt(0) >= 0); + JsonNode status = jsonInHtml.findValue("providers").get("osv-nvd").get("status"); + assertTrue(status.get("code").asInt(0) == 200); + assertTrue(status.get("ok").asBoolean(false)); } - } - public static String getOutputFileAndOverwriteItWithMock(String outputFileContent, InvocationOnMock invocationOnMock, String parameterPrefix) throws IOException { - String[] rawArguments = (String[]) invocationOnMock.getRawArguments()[0]; - Optional outputFileArg = Arrays.stream(rawArguments).filter(arg -> arg!= null && arg.startsWith(parameterPrefix)).findFirst(); - String outputFilePath=null; - if(outputFileArg.isPresent()) - { - String outputFile = outputFileArg.get(); - outputFilePath = outputFile.substring(outputFile.indexOf("=") + 1); - Files.writeString(Path.of(outputFilePath), outputFileContent); + private void mockMavenDependencyTree(Ecosystem.Type packageManager) throws IOException { + if (packageManager.equals(Ecosystem.Type.MAVEN)) { + mockedOperations = mockStatic(Operations.class); + String depTree; + try (var is = getResourceAsStreamDecision( + getClass(), new String[] {"tst_manifests", "it", "maven", "depTree.txt"})) { + depTree = new String(is.readAllBytes()); + } + mockedOperations.when(() -> Operations.runProcess(any(), any())).thenAnswer(invocationOnMock -> { + return getOutputFileAndOverwriteItWithMock(depTree, invocationOnMock, "-DoutputFile"); + }); + } } - return outputFilePath; - } + public static String getOutputFileAndOverwriteItWithMock( + String outputFileContent, InvocationOnMock invocationOnMock, String parameterPrefix) throws IOException { + String[] rawArguments = (String[]) invocationOnMock.getRawArguments()[0]; + Optional outputFileArg = Arrays.stream(rawArguments) + .filter(arg -> arg != null && arg.startsWith(parameterPrefix)) + .findFirst(); + String outputFilePath = null; + if (outputFileArg.isPresent()) { + String outputFile = outputFileArg.get(); + outputFilePath = outputFile.substring(outputFile.indexOf("=") + 1); + Files.writeString(Path.of(outputFilePath), outputFileContent); + } + return outputFilePath; + } } - - - diff --git a/src/test/java/com/redhat/exhort/impl/Exhort_Api_Test.java b/src/test/java/com/redhat/exhort/impl/Exhort_Api_Test.java index 79c3059c..d844c0a0 100644 --- a/src/test/java/com/redhat/exhort/impl/Exhort_Api_Test.java +++ b/src/test/java/com/redhat/exhort/impl/Exhort_Api_Test.java @@ -30,6 +30,20 @@ import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; +import com.fasterxml.jackson.databind.DeserializationFeature; +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.databind.node.ObjectNode; +import com.fasterxml.jackson.databind.node.TextNode; +import com.github.packageurl.MalformedPackageURLException; +import com.github.packageurl.PackageURL; +import com.redhat.exhort.Api; +import com.redhat.exhort.ExhortTest; +import com.redhat.exhort.Provider; +import com.redhat.exhort.api.AnalysisReport; +import com.redhat.exhort.image.ImageRef; +import com.redhat.exhort.tools.Ecosystem; +import com.redhat.exhort.tools.Operations; import java.io.BufferedReader; import java.io.IOException; import java.io.InputStreamReader; @@ -46,7 +60,6 @@ import java.util.function.Function; import java.util.function.Supplier; import java.util.stream.Collectors; - import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; @@ -59,653 +72,742 @@ import org.mockito.Mockito; import org.mockito.junit.jupiter.MockitoExtension; -import com.fasterxml.jackson.databind.DeserializationFeature; -import com.fasterxml.jackson.databind.JsonNode; -import com.fasterxml.jackson.databind.ObjectMapper; -import com.fasterxml.jackson.databind.node.ObjectNode; -import com.fasterxml.jackson.databind.node.TextNode; -import com.github.packageurl.MalformedPackageURLException; -import com.github.packageurl.PackageURL; -import com.redhat.exhort.Api; -import com.redhat.exhort.ExhortTest; -import com.redhat.exhort.Provider; -import com.redhat.exhort.api.AnalysisReport; -import com.redhat.exhort.image.ImageRef; -import com.redhat.exhort.tools.Ecosystem; -import com.redhat.exhort.tools.Operations; - @ExtendWith(MockitoExtension.class) -@ClearEnvironmentVariable(key="EXHORT_SNYK_TOKEN") -@ClearEnvironmentVariable(key="EXHORT_DEV_MODE") -@ClearEnvironmentVariable(key="DEV_EXHORT_BACKEND_URL") -@ClearEnvironmentVariable(key="RHDA_TOKEN") -@ClearEnvironmentVariable(key="RHDA_SOURCE") +@ClearEnvironmentVariable(key = "EXHORT_SNYK_TOKEN") +@ClearEnvironmentVariable(key = "EXHORT_DEV_MODE") +@ClearEnvironmentVariable(key = "DEV_EXHORT_BACKEND_URL") +@ClearEnvironmentVariable(key = "RHDA_TOKEN") +@ClearEnvironmentVariable(key = "RHDA_SOURCE") @SuppressWarnings("unchecked") class Exhort_Api_Test extends ExhortTest { - @Mock - Provider mockProvider; - @Mock - HttpClient mockHttpClient; - @InjectMocks - ExhortApi exhortApiSut; - - @AfterEach - void cleanup() { - System.clearProperty("EXHORT_SNYK_TOKEN"); - } - - @Test - @SetEnvironmentVariable(key="EXHORT_SNYK_TOKEN", value="snyk-token-from-env-var") - @SetEnvironmentVariable(key="RHDA_TOKEN", value="rhda-token-from-env-var") - @SetEnvironmentVariable(key="RHDA_SOURCE", value="rhda-source-from-env-var") - void stackAnalysisHtml_with_pom_xml_should_return_html_report_from_the_backend() - throws IOException, ExecutionException, InterruptedException { - // create a temporary pom.xml file - var tmpFile = Files.createTempFile("exhort_test_pom_", ".xml"); - try (var is = getResourceAsStreamDecision(this.getClass(), new String [] {"tst_manifests","maven","empty","pom.xml"})) { - Files.write(tmpFile, is.readAllBytes()); - } - - // stub the mocked provider with a fake content object - given(mockProvider.provideStack(tmpFile)) - .willReturn(new Provider.Content("fake-body-content".getBytes(), "fake-content-type")); - - // create an argument matcher to make sure we mock the response to for right request - ArgumentMatcher matchesRequest = r -> - r.headers().firstValue("Content-Type").get().equals("fake-content-type") && - r.headers().firstValue("Accept").get().equals("text/html") && - // snyk token is set using the environment variable (annotation) - r.headers().firstValue("ex-snyk-token").get().equals("snyk-token-from-env-var") && - r.headers().firstValue("rhda-token").get().equals("rhda-token-from-env-var") && - r.headers().firstValue("rhda-source").get().equals("rhda-source-from-env-var") && - r.headers().firstValue("rhda-operation-type").get().equals("Stack Analysis") && - - r.method().equals("POST"); - - // load dummy html and set as the expected analysis - byte[] expectedHtml; - try (var is = getResourceAsStreamDecision(this.getClass(), new String [] {"dummy_responses","maven","analysis-report.html"})) { - expectedHtml = is.readAllBytes(); - } - - // mock and http response object and stub it to return a fake body - var mockHttpResponse = mock(HttpResponse.class); - given(mockHttpResponse.body()).willReturn(expectedHtml); - given(mockHttpResponse.statusCode()).willReturn(200); + @Mock + Provider mockProvider; - // mock static getProvider utility function - try(var ecosystemTool = mockStatic(Ecosystem.class)) { - // stub static getProvider utility function to return our mock provider - ecosystemTool.when(() -> Ecosystem.getProvider(tmpFile)).thenReturn(mockProvider); + @Mock + HttpClient mockHttpClient; - // stub the http client to return our mocked response when request matches our arg matcher - given(mockHttpClient.sendAsync(argThat(matchesRequest), any())) - .willReturn(CompletableFuture.completedFuture(mockHttpResponse)); + @InjectMocks + ExhortApi exhortApiSut; - // when invoking the api for a html stack analysis report - var htmlTxt = exhortApiSut.stackAnalysisHtml(tmpFile.toString()); - // verify we got the correct html response - then(htmlTxt.get()).isEqualTo(expectedHtml); - } - // cleanup - Files.deleteIfExists(tmpFile); - } - - @Test -// System.setProperty("RHDA_TOKEN", "rhda-token-from-property"); -// System.setProperty("RHDA_SOURCE", "rhda-source-from-property"); - @SetEnvironmentVariable(key="EXHORT_SNYK_TOKEN", value="snyk-token-from-env-var") - @SetEnvironmentVariable(key="RHDA_TOKEN", value="rhda-token-from-env-var") - @SetEnvironmentVariable(key="RHDA_SOURCE", value="rhda-source-from-env-var") - void stackAnalysis_with_pom_xml_should_return_json_object_from_the_backend() - throws IOException, ExecutionException, InterruptedException { - // create a temporary pom.xml file - var tmpFile = Files.createTempFile("exhort_test_pom_", ".xml"); - try (var is = getResourceAsStreamDecision(this.getClass(), new String [] {"tst_manifests","maven","empty","pom.xml"})) { - Files.write(tmpFile, is.readAllBytes()); + @AfterEach + void cleanup() { + System.clearProperty("EXHORT_SNYK_TOKEN"); } - // stub the mocked provider with a fake content object - given(mockProvider.provideStack(tmpFile)) - .willReturn(new Provider.Content("fake-body-content".getBytes(), "fake-content-type")); - - // we expect this to be ignored because tokens from env vars takes precedence - System.setProperty("EXHORT_SNYK_TOKEN", "snyk-token-from-property"); - - // create an argument matcher to make sure we mock the response for the right request - ArgumentMatcher matchesRequest = r -> - r.headers().firstValue("Content-Type").get().equals("fake-content-type") && - r.headers().firstValue("Accept").get().equals("application/json") && - // snyk token is set using the environment variable (annotation) - ignored the one set in properties - r.headers().firstValue("ex-snyk-token").get().equals("snyk-token-from-env-var") && - r.headers().firstValue("rhda-token").get().equals("rhda-token-from-env-var") && - r.headers().firstValue("rhda-source").get().equals("rhda-source-from-env-var") && - r.headers().firstValue("rhda-operation-type").get().equals("Stack Analysis") && - r.method().equals("POST"); - - // load dummy json and set as the expected analysis - var mapper = new ObjectMapper().disable(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES); - AnalysisReport expectedAnalysis; - try (var is = getResourceAsStreamDecision(this.getClass(), new String [] {"dummy_responses","maven","analysis-report.json"})) { - expectedAnalysis = mapper.readValue(is, AnalysisReport.class); + @Test + @SetEnvironmentVariable(key = "EXHORT_SNYK_TOKEN", value = "snyk-token-from-env-var") + @SetEnvironmentVariable(key = "RHDA_TOKEN", value = "rhda-token-from-env-var") + @SetEnvironmentVariable(key = "RHDA_SOURCE", value = "rhda-source-from-env-var") + void stackAnalysisHtml_with_pom_xml_should_return_html_report_from_the_backend() + throws IOException, ExecutionException, InterruptedException { + // create a temporary pom.xml file + var tmpFile = Files.createTempFile("exhort_test_pom_", ".xml"); + try (var is = getResourceAsStreamDecision( + this.getClass(), new String[] {"tst_manifests", "maven", "empty", "pom.xml"})) { + Files.write(tmpFile, is.readAllBytes()); + } + + // stub the mocked provider with a fake content object + given(mockProvider.provideStack(tmpFile)) + .willReturn(new Provider.Content("fake-body-content".getBytes(), "fake-content-type")); + + // create an argument matcher to make sure we mock the response to for right request + ArgumentMatcher matchesRequest = + r -> r.headers().firstValue("Content-Type").get().equals("fake-content-type") + && r.headers().firstValue("Accept").get().equals("text/html") + && + // snyk token is set using the environment variable (annotation) + r.headers().firstValue("ex-snyk-token").get().equals("snyk-token-from-env-var") + && r.headers().firstValue("rhda-token").get().equals("rhda-token-from-env-var") + && r.headers().firstValue("rhda-source").get().equals("rhda-source-from-env-var") + && r.headers().firstValue("rhda-operation-type").get().equals("Stack Analysis") + && r.method().equals("POST"); + + // load dummy html and set as the expected analysis + byte[] expectedHtml; + try (var is = getResourceAsStreamDecision( + this.getClass(), new String[] {"dummy_responses", "maven", "analysis-report.html"})) { + expectedHtml = is.readAllBytes(); + } + + // mock and http response object and stub it to return a fake body + var mockHttpResponse = mock(HttpResponse.class); + given(mockHttpResponse.body()).willReturn(expectedHtml); + given(mockHttpResponse.statusCode()).willReturn(200); + + // mock static getProvider utility function + try (var ecosystemTool = mockStatic(Ecosystem.class)) { + // stub static getProvider utility function to return our mock provider + ecosystemTool.when(() -> Ecosystem.getProvider(tmpFile)).thenReturn(mockProvider); + + // stub the http client to return our mocked response when request matches our arg matcher + given(mockHttpClient.sendAsync(argThat(matchesRequest), any())) + .willReturn(CompletableFuture.completedFuture(mockHttpResponse)); + + // when invoking the api for a html stack analysis report + var htmlTxt = exhortApiSut.stackAnalysisHtml(tmpFile.toString()); + // verify we got the correct html response + then(htmlTxt.get()).isEqualTo(expectedHtml); + } + // cleanup + Files.deleteIfExists(tmpFile); } - // mock and http response object and stub it to return the expected analysis - var mockHttpResponse = mock(HttpResponse.class); - given(mockHttpResponse.body()).willReturn(mapper.writeValueAsString(expectedAnalysis)); - given(mockHttpResponse.statusCode()).willReturn(200); - - // mock static getProvider utility function - try(var ecosystemTool = mockStatic(Ecosystem.class)) { - // stub static getProvider utility function to return our mock provider - ecosystemTool.when(() -> Ecosystem.getProvider(tmpFile)).thenReturn(mockProvider); - - // stub the http client to return our mocked response when request matches our arg matcher - given(mockHttpClient.sendAsync(argThat(matchesRequest), any())) - .willReturn(CompletableFuture.completedFuture(mockHttpResponse)); - - // when invoking the api for a json stack analysis report - var responseAnalysis = exhortApiSut.stackAnalysis(tmpFile.toString()); - // verify we got the correct analysis report - then(responseAnalysis.get()).isEqualTo(expectedAnalysis); - } - // cleanup - Files.deleteIfExists(tmpFile); - } - - @Test - void componentAnalysis_with_pom_xml_should_return_json_object_from_the_backend() - throws IOException, ExecutionException, InterruptedException { - // load pom.xml - byte[] targetPom; - try (var is = getResourceAsStreamDecision(this.getClass(), new String [] {"tst_manifests","maven","empty","pom.xml"})) { - targetPom = is.readAllBytes(); + @Test + // System.setProperty("RHDA_TOKEN", "rhda-token-from-property"); + // System.setProperty("RHDA_SOURCE", "rhda-source-from-property"); + @SetEnvironmentVariable(key = "EXHORT_SNYK_TOKEN", value = "snyk-token-from-env-var") + @SetEnvironmentVariable(key = "RHDA_TOKEN", value = "rhda-token-from-env-var") + @SetEnvironmentVariable(key = "RHDA_SOURCE", value = "rhda-source-from-env-var") + void stackAnalysis_with_pom_xml_should_return_json_object_from_the_backend() + throws IOException, ExecutionException, InterruptedException { + // create a temporary pom.xml file + var tmpFile = Files.createTempFile("exhort_test_pom_", ".xml"); + try (var is = getResourceAsStreamDecision( + this.getClass(), new String[] {"tst_manifests", "maven", "empty", "pom.xml"})) { + Files.write(tmpFile, is.readAllBytes()); + } + + // stub the mocked provider with a fake content object + given(mockProvider.provideStack(tmpFile)) + .willReturn(new Provider.Content("fake-body-content".getBytes(), "fake-content-type")); + + // we expect this to be ignored because tokens from env vars takes precedence + System.setProperty("EXHORT_SNYK_TOKEN", "snyk-token-from-property"); + + // create an argument matcher to make sure we mock the response for the right request + ArgumentMatcher matchesRequest = + r -> r.headers().firstValue("Content-Type").get().equals("fake-content-type") + && r.headers().firstValue("Accept").get().equals("application/json") + && + // snyk token is set using the environment variable (annotation) - ignored the one set in + // properties + r.headers().firstValue("ex-snyk-token").get().equals("snyk-token-from-env-var") + && r.headers().firstValue("rhda-token").get().equals("rhda-token-from-env-var") + && r.headers().firstValue("rhda-source").get().equals("rhda-source-from-env-var") + && r.headers().firstValue("rhda-operation-type").get().equals("Stack Analysis") + && r.method().equals("POST"); + + // load dummy json and set as the expected analysis + var mapper = new ObjectMapper().disable(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES); + AnalysisReport expectedAnalysis; + try (var is = getResourceAsStreamDecision( + this.getClass(), new String[] {"dummy_responses", "maven", "analysis-report.json"})) { + expectedAnalysis = mapper.readValue(is, AnalysisReport.class); + } + + // mock and http response object and stub it to return the expected analysis + var mockHttpResponse = mock(HttpResponse.class); + given(mockHttpResponse.body()).willReturn(mapper.writeValueAsString(expectedAnalysis)); + given(mockHttpResponse.statusCode()).willReturn(200); + + // mock static getProvider utility function + try (var ecosystemTool = mockStatic(Ecosystem.class)) { + // stub static getProvider utility function to return our mock provider + ecosystemTool.when(() -> Ecosystem.getProvider(tmpFile)).thenReturn(mockProvider); + + // stub the http client to return our mocked response when request matches our arg matcher + given(mockHttpClient.sendAsync(argThat(matchesRequest), any())) + .willReturn(CompletableFuture.completedFuture(mockHttpResponse)); + + // when invoking the api for a json stack analysis report + var responseAnalysis = exhortApiSut.stackAnalysis(tmpFile.toString()); + // verify we got the correct analysis report + then(responseAnalysis.get()).isEqualTo(expectedAnalysis); + } + // cleanup + Files.deleteIfExists(tmpFile); } - // stub the mocked provider with a fake content object - given(mockProvider.provideComponent(targetPom)) - .willReturn(new Provider.Content("fake-body-content".getBytes(), "fake-content-type")); - - // we expect this to picked up because no env var to take precedence - System.setProperty("EXHORT_SNYK_TOKEN", "snyk-token-from-property"); - System.setProperty("RHDA_TOKEN", "rhda-token-from-property"); - System.setProperty("RHDA_SOURCE", "rhda-source-from-property"); - - // create an argument matcher to make sure we mock the response for the right request - ArgumentMatcher matchesRequest = r -> - r.headers().firstValue("Content-Type").get().equals("fake-content-type") && - r.headers().firstValue("Accept").get().equals("application/json") && - // snyk token is set using properties which is picked up because no env var specified - r.headers().firstValue("ex-snyk-token").get().equals("snyk-token-from-property") && - r.headers().firstValue("rhda-token").get().equals("rhda-token-from-property") && - r.headers().firstValue("rhda-source").get().equals("rhda-source-from-property") && - r.headers().firstValue("rhda-operation-type").get().equals("Component Analysis") && - r.method().equals("POST"); - - // load dummy json and set as the expected analysis - var mapper = new ObjectMapper().disable(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES); - AnalysisReport expectedReport; - try (var is = getResourceAsStreamDecision(this.getClass(), new String [] {"dummy_responses","maven","analysis-report.json"})) { - expectedReport = mapper.readValue(is, AnalysisReport.class); + @Test + void componentAnalysis_with_pom_xml_should_return_json_object_from_the_backend() + throws IOException, ExecutionException, InterruptedException { + // load pom.xml + byte[] targetPom; + try (var is = getResourceAsStreamDecision( + this.getClass(), new String[] {"tst_manifests", "maven", "empty", "pom.xml"})) { + targetPom = is.readAllBytes(); + } + + // stub the mocked provider with a fake content object + given(mockProvider.provideComponent(targetPom)) + .willReturn(new Provider.Content("fake-body-content".getBytes(), "fake-content-type")); + + // we expect this to picked up because no env var to take precedence + System.setProperty("EXHORT_SNYK_TOKEN", "snyk-token-from-property"); + System.setProperty("RHDA_TOKEN", "rhda-token-from-property"); + System.setProperty("RHDA_SOURCE", "rhda-source-from-property"); + + // create an argument matcher to make sure we mock the response for the right request + ArgumentMatcher matchesRequest = + r -> r.headers().firstValue("Content-Type").get().equals("fake-content-type") + && r.headers().firstValue("Accept").get().equals("application/json") + && + // snyk token is set using properties which is picked up because no env var specified + r.headers().firstValue("ex-snyk-token").get().equals("snyk-token-from-property") + && r.headers().firstValue("rhda-token").get().equals("rhda-token-from-property") + && r.headers().firstValue("rhda-source").get().equals("rhda-source-from-property") + && r.headers().firstValue("rhda-operation-type").get().equals("Component Analysis") + && r.method().equals("POST"); + + // load dummy json and set as the expected analysis + var mapper = new ObjectMapper().disable(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES); + AnalysisReport expectedReport; + try (var is = getResourceAsStreamDecision( + this.getClass(), new String[] {"dummy_responses", "maven", "analysis-report.json"})) { + expectedReport = mapper.readValue(is, AnalysisReport.class); + } + + // mock and http response object and stub it to return the expected analysis + var mockHttpResponse = mock(HttpResponse.class); + given(mockHttpResponse.body()).willReturn(mapper.writeValueAsString(expectedReport)); + given(mockHttpResponse.statusCode()).willReturn(200); + + // mock static getProvider utility function + try (var ecosystemTool = mockStatic(Ecosystem.class)) { + // stub static getProvider utility function to return our mock provider + ecosystemTool.when(() -> Ecosystem.getProvider("pom.xml")).thenReturn(mockProvider); + + // stub the http client to return our mocked response when request matches our arg matcher + given(mockHttpClient.sendAsync(argThat(matchesRequest), any())) + .willReturn(CompletableFuture.completedFuture(mockHttpResponse)); + + // when invoking the api for a json stack analysis report + var responseAnalysis = exhortApiSut.componentAnalysis("pom.xml", targetPom); + // verify we got the correct analysis report + then(responseAnalysis.get()).isEqualTo(expectedReport); + } } - // mock and http response object and stub it to return the expected analysis - var mockHttpResponse = mock(HttpResponse.class); - given(mockHttpResponse.body()).willReturn(mapper.writeValueAsString(expectedReport)); - given(mockHttpResponse.statusCode()).willReturn(200); - - // mock static getProvider utility function - try (var ecosystemTool = mockStatic(Ecosystem.class)) { - // stub static getProvider utility function to return our mock provider - ecosystemTool.when(() -> Ecosystem.getProvider("pom.xml")).thenReturn(mockProvider); - - // stub the http client to return our mocked response when request matches our arg matcher - given(mockHttpClient.sendAsync(argThat(matchesRequest), any())) - .willReturn(CompletableFuture.completedFuture(mockHttpResponse)); - - // when invoking the api for a json stack analysis report - var responseAnalysis = exhortApiSut.componentAnalysis("pom.xml", targetPom); - // verify we got the correct analysis report - then(responseAnalysis.get()).isEqualTo(expectedReport); - } - } - - @Test - void stackAnalysisMixed_with_pom_xml_should_return_both_html_text_and_json_object_from_the_backend() - throws IOException, ExecutionException, InterruptedException { - // load dummy json and set as the expected analysis - var mapper = new ObjectMapper().disable(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES); - AnalysisReport expectedJson; - try (var is = getResourceAsStreamDecision(this.getClass(), new String [] {"dummy_responses","maven","analysis-report.json"})) { - expectedJson = mapper.readValue(is, AnalysisReport.class); + @Test + void stackAnalysisMixed_with_pom_xml_should_return_both_html_text_and_json_object_from_the_backend() + throws IOException, ExecutionException, InterruptedException { + // load dummy json and set as the expected analysis + var mapper = new ObjectMapper().disable(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES); + AnalysisReport expectedJson; + try (var is = getResourceAsStreamDecision( + this.getClass(), new String[] {"dummy_responses", "maven", "analysis-report.json"})) { + expectedJson = mapper.readValue(is, AnalysisReport.class); + } + + // load dummy html and set as the expected analysis + byte[] expectedHtml; + try (var is = getResourceAsStreamDecision( + this.getClass(), new String[] {"dummy_responses", "maven", "analysis-report.html"})) { + expectedHtml = is.readAllBytes(); + } + + // create a temporary pom.xml file + var tmpFile = Files.createTempFile("exhort_test_pom_", ".xml"); + try (var is = getResourceAsStreamDecision( + this.getClass(), new String[] {"tst_manifests", "maven", "empty", "pom.xml"})) { + Files.write(tmpFile, is.readAllBytes()); + } + + // stub the mocked provider with a fake content object + given(mockProvider.provideStack(tmpFile)) + .willReturn(new Provider.Content("fake-body-content".getBytes(), "fake-content-type")); + + // create an argument matcher to make sure we mock the response for the right request + ArgumentMatcher matchesRequest = + r -> r.headers().firstValue("Content-Type").get().equals("fake-content-type") + && r.headers().firstValue("Accept").get().equals("multipart/mixed") + && r.method().equals("POST"); + + // load dummy mixed and set as the expected analysis + byte[] mixedResponse; + try (var is = getResourceAsStreamDecision( + this.getClass(), new String[] {"dummy_responses", "maven", "analysis-report.mixed"})) { + mixedResponse = is.readAllBytes(); + } + + // mock and http response object and stub it to return the expected analysis + var mockHttpResponse = mock(HttpResponse.class); + given(mockHttpResponse.body()).willReturn(mixedResponse); + given(mockHttpResponse.statusCode()).willReturn(200); + + // mock static getProvider utility function + try (var ecosystemTool = mockStatic(Ecosystem.class)) { + // stub static getProvider utility function to return our mock provider + ecosystemTool.when(() -> Ecosystem.getProvider(tmpFile)).thenReturn(mockProvider); + + // stub the http client to return our mocked response when request matches our arg matcher + given(mockHttpClient.sendAsync(argThat(matchesRequest), any())) + .willReturn(CompletableFuture.completedFuture(mockHttpResponse)); + + // when invoking the api for a json stack analysis mixed report + var responseAnalysis = + exhortApiSut.stackAnalysisMixed(tmpFile.toString()).get(); + // verify we got the correct mixed report + then(new String(responseAnalysis.html).trim()).isEqualTo(new String(expectedHtml).trim()); + then(responseAnalysis.json).isEqualTo(expectedJson); + } + // cleanup + Files.deleteIfExists(tmpFile); } - // load dummy html and set as the expected analysis - byte[] expectedHtml; - try (var is = getResourceAsStreamDecision(this.getClass(), new String [] {"dummy_responses","maven","analysis-report.html"})) { - expectedHtml = is.readAllBytes(); + @Test + void componentAnalysis_with_pom_xml_as_path_should_return_json_object_from_the_backend() + throws IOException, ExecutionException, InterruptedException { + // load pom.xml + var tmpFile = Files.createTempFile("exhort_test_pom_", ".xml"); + try (var is = getResourceAsStreamDecision( + this.getClass(), new String[] {"tst_manifests", "maven", "empty", "pom.xml"})) { + Files.write(tmpFile, is.readAllBytes()); + } + + // stub the mocked provider with a fake content object + given(mockProvider.provideComponent(tmpFile)) + .willReturn(new Provider.Content("fake-body-content".getBytes(), "fake-content-type")); + + // we expect this to picked up because no env var to take precedence + System.setProperty("EXHORT_SNYK_TOKEN", "snyk-token-from-property"); + + // create an argument matcher to make sure we mock the response for the right request + ArgumentMatcher matchesRequest = + r -> r.headers().firstValue("Content-Type").get().equals("fake-content-type") + && r.headers().firstValue("Accept").get().equals("application/json") + && + // snyk token is set using properties which is picked up because no env var specified + r.headers().firstValue("ex-snyk-token").get().equals("snyk-token-from-property") + && r.method().equals("POST"); + + // load dummy json and set as the expected analysis + var mapper = new ObjectMapper().disable(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES); + AnalysisReport expectedReport; + try (var is = getResourceAsStreamDecision( + this.getClass(), new String[] {"dummy_responses", "maven", "analysis-report.json"})) { + expectedReport = mapper.readValue(is, AnalysisReport.class); + } + + // mock and http response object and stub it to return the expected analysis + var mockHttpResponse = mock(HttpResponse.class); + given(mockHttpResponse.body()).willReturn(mapper.writeValueAsString(expectedReport)); + given(mockHttpResponse.statusCode()).willReturn(200); + + // mock static getProvider utility function + try (var ecosystemTool = mockStatic(Ecosystem.class)) { + // stub static getProvider utility function to return our mock provider + ecosystemTool.when(() -> Ecosystem.getProvider(tmpFile)).thenReturn(mockProvider); + + // stub the http client to return our mocked response when request matches our arg matcher + given(mockHttpClient.sendAsync(argThat(matchesRequest), any())) + .willReturn(CompletableFuture.completedFuture(mockHttpResponse)); + + // when invoking the api for a json stack analysis report + var responseAnalysis = exhortApiSut.componentAnalysis(tmpFile.toString()); + // verify we got the correct analysis report + then(responseAnalysis.get()).isEqualTo(expectedReport); + // cleanup + Files.deleteIfExists(tmpFile); + } } - // create a temporary pom.xml file - var tmpFile = Files.createTempFile("exhort_test_pom_", ".xml"); - try (var is = getResourceAsStreamDecision(this.getClass(), new String [] {"tst_manifests","maven","empty","pom.xml"})) { - Files.write(tmpFile, is.readAllBytes()); + @AfterEach + void afterEach() { + System.clearProperty("EXHORT_DEV_MODE"); + System.clearProperty("DEV_EXHORT_BACKEND_URL"); + System.clearProperty("RHDA_TOKEN"); + System.clearProperty("RHDA_SOURCE"); } - // stub the mocked provider with a fake content object - given(mockProvider.provideStack(tmpFile)) - .willReturn(new Provider.Content("fake-body-content".getBytes(), "fake-content-type")); - - // create an argument matcher to make sure we mock the response for the right request - ArgumentMatcher matchesRequest = r -> - r.headers().firstValue("Content-Type").get().equals("fake-content-type") && - r.headers().firstValue("Accept").get().equals("multipart/mixed") && - r.method().equals("POST"); - - // load dummy mixed and set as the expected analysis - byte[] mixedResponse; - try (var is = getResourceAsStreamDecision(this.getClass(), new String [] {"dummy_responses","maven","analysis-report.mixed"})) { - mixedResponse = is.readAllBytes(); + @Test + @SetEnvironmentVariable(key = "EXHORT_DEV_MODE", value = "true") + @ClearEnvironmentVariable(key = "DEV_EXHORT_BACKEND_URL") + void check_Exhort_Url_When_DEV_Mode_true_Both() { + System.setProperty("EXHORT_DEV_MODE", "true"); + ExhortApi exhortApi = new ExhortApi(); + then(exhortApi.getEndpoint()).isEqualTo(ExhortApi.DEFAULT_ENDPOINT_DEV); + then(exhortApi.getEndpoint()).isNotEqualTo(ExhortApi.DEFAULT_ENDPOINT); } - // mock and http response object and stub it to return the expected analysis - var mockHttpResponse = mock(HttpResponse.class); - given(mockHttpResponse.body()).willReturn(mixedResponse); - given(mockHttpResponse.statusCode()).willReturn(200); - - // mock static getProvider utility function - try(var ecosystemTool = mockStatic(Ecosystem.class)) { - // stub static getProvider utility function to return our mock provider - ecosystemTool.when(() -> Ecosystem.getProvider(tmpFile)).thenReturn(mockProvider); - - // stub the http client to return our mocked response when request matches our arg matcher - given(mockHttpClient.sendAsync(argThat(matchesRequest), any())) - .willReturn(CompletableFuture.completedFuture(mockHttpResponse)); - - // when invoking the api for a json stack analysis mixed report - var responseAnalysis = exhortApiSut.stackAnalysisMixed(tmpFile.toString()).get(); - // verify we got the correct mixed report - then(new String(responseAnalysis.html).trim()).isEqualTo(new String(expectedHtml).trim()); - then(responseAnalysis.json).isEqualTo(expectedJson); - } - // cleanup - Files.deleteIfExists(tmpFile); - } - - @Test - void componentAnalysis_with_pom_xml_as_path_should_return_json_object_from_the_backend() - throws IOException, ExecutionException, InterruptedException { - // load pom.xml - var tmpFile = Files.createTempFile("exhort_test_pom_", ".xml"); - try (var is = getResourceAsStreamDecision(this.getClass(), new String [] {"tst_manifests","maven","empty","pom.xml"})) { - Files.write(tmpFile, is.readAllBytes()); + @Test + @SetEnvironmentVariable(key = "EXHORT_DEV_MODE", value = "true") + @ClearEnvironmentVariable(key = "DEV_EXHORT_BACKEND_URL") + void check_Exhort_Url_When_env_DEV_Mode_true_property_DEV_Mode_false() { + System.setProperty("EXHORT_DEV_MODE", "false"); + ExhortApi exhortApi = new ExhortApi(); + then(exhortApi.getEndpoint()).isEqualTo(ExhortApi.DEFAULT_ENDPOINT_DEV); + then(exhortApi.getEndpoint()).isNotEqualTo(ExhortApi.DEFAULT_ENDPOINT); } - // stub the mocked provider with a fake content object - given(mockProvider.provideComponent(tmpFile)) - .willReturn(new Provider.Content("fake-body-content".getBytes(), "fake-content-type")); - - // we expect this to picked up because no env var to take precedence - System.setProperty("EXHORT_SNYK_TOKEN", "snyk-token-from-property"); - - // create an argument matcher to make sure we mock the response for the right request - ArgumentMatcher matchesRequest = r -> - r.headers().firstValue("Content-Type").get().equals("fake-content-type") && - r.headers().firstValue("Accept").get().equals("application/json") && - // snyk token is set using properties which is picked up because no env var specified - r.headers().firstValue("ex-snyk-token").get().equals("snyk-token-from-property") && - r.method().equals("POST"); - - // load dummy json and set as the expected analysis - var mapper = new ObjectMapper().disable(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES); - AnalysisReport expectedReport; - try (var is = getResourceAsStreamDecision(this.getClass(), new String [] {"dummy_responses","maven","analysis-report.json"})) { - expectedReport = mapper.readValue(is, AnalysisReport.class); + @Test + @SetEnvironmentVariable(key = "EXHORT_DEV_MODE", value = "true") + @ClearEnvironmentVariable(key = "DEV_EXHORT_BACKEND_URL") + void check_Exhort_Url_When_env_DEV_Mode_true_And_DEV_Exhort_Url_Set_Then_Default_DEV_Exhort_URL_Not_Selected() { + String dummyUrl = "http://dummy-url"; + System.setProperty("DEV_EXHORT_BACKEND_URL", dummyUrl); + ExhortApi exhortApi = new ExhortApi(); + then(exhortApi.getEndpoint()).isEqualTo(dummyUrl); + then(exhortApi.getEndpoint()).isNotEqualTo(ExhortApi.DEFAULT_ENDPOINT_DEV); } - // mock and http response object and stub it to return the expected analysis - var mockHttpResponse = mock(HttpResponse.class); - given(mockHttpResponse.body()).willReturn(mapper.writeValueAsString(expectedReport)); - given(mockHttpResponse.statusCode()).willReturn(200); - - // mock static getProvider utility function - try (var ecosystemTool = mockStatic(Ecosystem.class)) { - // stub static getProvider utility function to return our mock provider - ecosystemTool.when(() -> Ecosystem.getProvider(tmpFile)).thenReturn(mockProvider); - - // stub the http client to return our mocked response when request matches our arg matcher - given(mockHttpClient.sendAsync(argThat(matchesRequest), any())) - .willReturn(CompletableFuture.completedFuture(mockHttpResponse)); - - // when invoking the api for a json stack analysis report - var responseAnalysis = exhortApiSut.componentAnalysis(tmpFile.toString()); - // verify we got the correct analysis report - then(responseAnalysis.get()).isEqualTo(expectedReport); - //cleanup - Files.deleteIfExists(tmpFile); - } - } - - - @AfterEach - void afterEach() { - System.clearProperty("EXHORT_DEV_MODE"); - System.clearProperty("DEV_EXHORT_BACKEND_URL"); - System.clearProperty("RHDA_TOKEN"); - System.clearProperty("RHDA_SOURCE"); - - } - - @Test - @SetEnvironmentVariable(key="EXHORT_DEV_MODE", value="true") - @ClearEnvironmentVariable(key="DEV_EXHORT_BACKEND_URL") - void check_Exhort_Url_When_DEV_Mode_true_Both() { - System.setProperty("EXHORT_DEV_MODE","true"); - ExhortApi exhortApi = new ExhortApi(); - then(exhortApi.getEndpoint()).isEqualTo(ExhortApi.DEFAULT_ENDPOINT_DEV); - then(exhortApi.getEndpoint()).isNotEqualTo(ExhortApi.DEFAULT_ENDPOINT); - } -@Test - @SetEnvironmentVariable(key="EXHORT_DEV_MODE", value="true") - @ClearEnvironmentVariable(key="DEV_EXHORT_BACKEND_URL") - void check_Exhort_Url_When_env_DEV_Mode_true_property_DEV_Mode_false() { - System.setProperty("EXHORT_DEV_MODE","false"); - ExhortApi exhortApi = new ExhortApi(); - then(exhortApi.getEndpoint()).isEqualTo(ExhortApi.DEFAULT_ENDPOINT_DEV); - then(exhortApi.getEndpoint()).isNotEqualTo(ExhortApi.DEFAULT_ENDPOINT); - } - -@Test - @SetEnvironmentVariable(key="EXHORT_DEV_MODE", value="true") - @ClearEnvironmentVariable(key="DEV_EXHORT_BACKEND_URL") - void check_Exhort_Url_When_env_DEV_Mode_true_And_DEV_Exhort_Url_Set_Then_Default_DEV_Exhort_URL_Not_Selected() { - String dummyUrl = "http://dummy-url"; - System.setProperty("DEV_EXHORT_BACKEND_URL", dummyUrl); - ExhortApi exhortApi = new ExhortApi(); - then(exhortApi.getEndpoint()).isEqualTo(dummyUrl); - then(exhortApi.getEndpoint()).isNotEqualTo(ExhortApi.DEFAULT_ENDPOINT_DEV); - } - -@Test - @SetEnvironmentVariable(key="EXHORT_DEV_MODE", value="false") - @ClearEnvironmentVariable(key="DEV_EXHORT_BACKEND_URL") -void check_Exhort_Url_When_env_DEV_Mode_false_And_DEV_Exhort_Url_Set_Then_Default_DEV_Exhort_URL_Not_Selected() { - System.setProperty("EXHORT_DEV_MODE", "false"); - ExhortApi exhortApi = new ExhortApi(); - then(exhortApi.getEndpoint()).isEqualTo(ExhortApi.DEFAULT_ENDPOINT); - then(exhortApi.getEndpoint()).isNotEqualTo(ExhortApi.DEFAULT_ENDPOINT_DEV); - } - - - @Test - @SetEnvironmentVariable(key="EXHORT_DEV_MODE", value= "false") - void check_Exhort_Url_When_env_DEV_Mode_false_And_Property_Dev_Mode_true_Default_Exhort_URL_Selected() { - System.setProperty("EXHORT_DEV_MODE", "true"); - ExhortApi exhortApi = new ExhortApi(); - then(exhortApi.getEndpoint()).isEqualTo(ExhortApi.DEFAULT_ENDPOINT); - then(exhortApi.getEndpoint()).isNotEqualTo(ExhortApi.DEFAULT_ENDPOINT_DEV); - } - - @Test - @SetEnvironmentVariable(key="EXHORT_DEV_MODE", value="false") - @SetEnvironmentVariable(key="DEV_EXHORT_BACKEND_URL", value="http://dummy-route") - void check_Exhort_Url_When_env_DEV_Mode_false_And_DEV_Exhort_Url_Set_Then_Default_Exhort_URL_Selected_Anyway() { - System.setProperty("EXHORT_DEV_MODE", "true"); - System.setProperty("DEV_EXHORT_BACKEND_URL","http://dummy-route2"); - ExhortApi exhortApi = new ExhortApi(); - then(exhortApi.getEndpoint()).isEqualTo(ExhortApi.DEFAULT_ENDPOINT); - then(exhortApi.getEndpoint()).isNotEqualTo(System.getenv("DEV_EXHORT_BACKEND_URL")); - then(exhortApi.getEndpoint()).isNotEqualTo(System.getProperty("DEV_EXHORT_BACKEND_URL")); - - } - @Test - void check_Exhort_Url_When_env_DEV_Mode_not_set_And_Property_Exhort_Dev_Mode_false_Then_Default_Exhort_URL_Selected() { - System.setProperty("EXHORT_DEV_MODE", "false"); - ExhortApi exhortApi = new ExhortApi(); - then(exhortApi.getEndpoint()).isEqualTo(ExhortApi.DEFAULT_ENDPOINT); - then(exhortApi.getEndpoint()).isNotEqualTo(ExhortApi.DEFAULT_ENDPOINT_DEV); - } - @Test - void check_Exhort_Url_When_env_DEV_Mode_not_set_And_Property_Exhort_Dev_Mode_true_Then_Default_DEV_Exhort_URL_Selected() { - System.setProperty("EXHORT_DEV_MODE", "true"); - ExhortApi exhortApi = new ExhortApi(); - then(exhortApi.getEndpoint()).isNotEqualTo(ExhortApi.DEFAULT_ENDPOINT); - then(exhortApi.getEndpoint()).isEqualTo(ExhortApi.DEFAULT_ENDPOINT_DEV); - } - @Test - @SetEnvironmentVariable(key="DEV_EXHORT_BACKEND_URL", value="http://dummy-route") - void check_Exhort_Url_When_env_DEV_Mode_not_set_And_Property_Exhort_Dev_Mode_true_and_Env_DEV_Exhort_Backend_Url_Set_Then_DEV_ENV_Exhort_URL_Selected() { - System.setProperty("EXHORT_DEV_MODE", "true"); - System.setProperty("DEV_EXHORT_BACKEND_URL", "http://dummy-route2"); - ExhortApi exhortApi = new ExhortApi(); - then(exhortApi.getEndpoint()).isNotEqualTo(ExhortApi.DEFAULT_ENDPOINT); - then(exhortApi.getEndpoint()).isNotEqualTo(ExhortApi.DEFAULT_ENDPOINT_DEV); - then(exhortApi.getEndpoint()).isNotEqualTo("http://dummy-route2"); - then(exhortApi.getEndpoint()).isEqualTo("http://dummy-route"); - } - - @Test - void check_Exhort_Url_When_Nothing_Set_Then_Default_Exhort_URL_Selected() { - ExhortApi exhortApi = new ExhortApi(); - then(exhortApi.getEndpoint()).isEqualTo(ExhortApi.DEFAULT_ENDPOINT); - - } - - @Test - @SetEnvironmentVariable(key = "EXHORT_SNYK_TOKEN", value = "snyk-token-from-env-var") - @SetEnvironmentVariable(key = "RHDA_TOKEN", value = "rhda-token-from-env-var") - @SetEnvironmentVariable(key = "RHDA_SOURCE", value = "rhda-source-from-env-var") - void test_image_analysis() throws IOException, ExecutionException, InterruptedException, MalformedPackageURLException { - try (MockedStatic mock = Mockito.mockStatic(Operations.class); - var sbomIS = getResourceAsStreamDecision(this.getClass(), new String[]{"msc", "image", "image_sbom.json"}); - var reportIS = getResourceAsStreamDecision(this.getClass(), new String[]{"msc", "image", "image_reports.json"})) { - - var imageRef = new ImageRef("test.io/test/test-app:test-version@sha256:1fafb0905264413501df60d90a92ca32df8a2011cbfb4876ddff5ceb20c8f165", "linux/amd64"); - - var jsonSbom = new BufferedReader(new InputStreamReader(sbomIS, StandardCharsets.UTF_8)).lines().collect(Collectors.joining("\n")); - var output = new Operations.ProcessExecOutput(jsonSbom, "", 0); - - mock.when(() -> Operations.getCustomPathOrElse(eq("syft"))) - .thenReturn("syft"); - - mock.when(() -> Operations.runProcessGetFullOutput(isNull(), - aryEq(new String[]{"syft", imageRef.getImage().getFullName(), - "-s", "all-layers", "-o", "cyclonedx-json", "-q"}), - isNull())) - .thenReturn(output); - - var jsonReport = new BufferedReader(new InputStreamReader(reportIS, StandardCharsets.UTF_8)).lines().collect(Collectors.joining("\n")); - - var httpResponse = mock(HttpResponse.class); - when(httpResponse.statusCode()).thenReturn(200); - when(httpResponse.body()).thenReturn(jsonReport); - - ArgumentMatcher matchesRequest = r -> - r.uri().equals(URI.create(String.format("%s/api/v4/batch-analysis", exhortApiSut.getEndpoint()))) && - r.headers().firstValue("Content-Type").get().equals(Api.CYCLONEDX_MEDIA_TYPE) && - r.headers().firstValue("Accept").get().equals(Api.MediaType.APPLICATION_JSON.toString()) && - r.headers().firstValue("ex-snyk-token").get().equals("snyk-token-from-env-var") && - r.headers().firstValue("rhda-token").get().equals("rhda-token-from-env-var") && - r.headers().firstValue("rhda-source").get().equals("rhda-source-from-env-var") && - r.headers().firstValue("rhda-operation-type").get().equals("Image Analysis") && - r.method().equals("POST"); - - when(mockHttpClient.sendAsync(argThat(matchesRequest), any())) - .thenReturn(CompletableFuture.completedFuture(httpResponse)); - - var result = exhortApiSut.imageAnalysis(Set.of(imageRef)); - var reports = result.get(); - assertEquals(2, reports.size()); - assertTrue(reports.containsKey(new ImageRef(new PackageURL("pkg:oci/ubi@sha256:f5983f7c7878cc9b26a3962be7756e3c810e9831b0b9f9613e6f6b445f884e74?repository_url=registry.access.redhat.com/ubi9/ubi&tag=9.3-1552&arch=amd64")))); - assertTrue(reports.containsKey(new ImageRef(new PackageURL("pkg:oci/default-app@sha256:333224a233db31852ac1085c6cd702016ab8aaf54cecde5c4bed5451d636adcf?repository_url=quay.io/default-app&tag=0.0.1")))); - assertNotNull(reports.get(new ImageRef(new PackageURL("pkg:oci/ubi@sha256:f5983f7c7878cc9b26a3962be7756e3c810e9831b0b9f9613e6f6b445f884e74?repository_url=registry.access.redhat.com/ubi9/ubi&tag=9.3-1552&arch=amd64r")))); - assertNotNull(reports.get(new ImageRef(new PackageURL("pkg:oci/default-app@sha256:333224a233db31852ac1085c6cd702016ab8aaf54cecde5c4bed5451d636adcf?repository_url=quay.io/default-app&tag=0.0.1")))); + @Test + @SetEnvironmentVariable(key = "EXHORT_DEV_MODE", value = "false") + @ClearEnvironmentVariable(key = "DEV_EXHORT_BACKEND_URL") + void check_Exhort_Url_When_env_DEV_Mode_false_And_DEV_Exhort_Url_Set_Then_Default_DEV_Exhort_URL_Not_Selected() { + System.setProperty("EXHORT_DEV_MODE", "false"); + ExhortApi exhortApi = new ExhortApi(); + then(exhortApi.getEndpoint()).isEqualTo(ExhortApi.DEFAULT_ENDPOINT); + then(exhortApi.getEndpoint()).isNotEqualTo(ExhortApi.DEFAULT_ENDPOINT_DEV); } - } - - @Test - @SetEnvironmentVariable(key = "EXHORT_SNYK_TOKEN", value = "snyk-token-from-env-var") - @SetEnvironmentVariable(key = "RHDA_TOKEN", value = "rhda-token-from-env-var") - @SetEnvironmentVariable(key = "RHDA_SOURCE", value = "rhda-source-from-env-var") - void imageAnalysisHtml() throws IOException, ExecutionException, InterruptedException, MalformedPackageURLException { - try (MockedStatic mock = Mockito.mockStatic(Operations.class); - var sbomIS = getResourceAsStreamDecision(this.getClass(), new String[]{"msc", "image", "image_sbom.json"}); - var reportIS = getResourceAsStreamDecision(this.getClass(), new String[]{"msc", "image", "image_reports.json"})) { - - var imageRef = new ImageRef("test.io/test/test-app:test-version@sha256:1fafb0905264413501df60d90a92ca32df8a2011cbfb4876ddff5ceb20c8f165", "linux/amd64"); - - var jsonSbom = new BufferedReader(new InputStreamReader(sbomIS, StandardCharsets.UTF_8)).lines().collect(Collectors.joining("\n")); - var output = new Operations.ProcessExecOutput(jsonSbom, "", 0); - - mock.when(() -> Operations.getCustomPathOrElse(eq("syft"))) - .thenReturn("syft"); - - mock.when(() -> Operations.runProcessGetFullOutput(isNull(), - aryEq(new String[]{"syft", imageRef.getImage().getFullName(), - "-s", "all-layers", "-o", "cyclonedx-json", "-q"}), - isNull())) - .thenReturn(output); - - var jsonReport = new BufferedReader(new InputStreamReader(reportIS, StandardCharsets.UTF_8)).lines().collect(Collectors.joining("\n")); - - var httpResponse = mock(HttpResponse.class); - when(httpResponse.statusCode()).thenReturn(200); - when(httpResponse.body()).thenReturn(jsonReport); - - ArgumentMatcher matchesRequest = r -> - r.uri().equals(URI.create(String.format("%s/api/v4/batch-analysis", exhortApiSut.getEndpoint()))) && - r.headers().firstValue("Content-Type").get().equals(Api.CYCLONEDX_MEDIA_TYPE) && - r.headers().firstValue("Accept").get().equals(Api.MediaType.TEXT_HTML.toString()) && - r.headers().firstValue("ex-snyk-token").get().equals("snyk-token-from-env-var") && - r.headers().firstValue("rhda-token").get().equals("rhda-token-from-env-var") && - r.headers().firstValue("rhda-source").get().equals("rhda-source-from-env-var") && - r.headers().firstValue("rhda-operation-type").get().equals("Image Analysis") && - r.method().equals("POST"); - - when(mockHttpClient.sendAsync(argThat(matchesRequest), any())) - .thenReturn(CompletableFuture.completedFuture(httpResponse)); - - var result = exhortApiSut.imageAnalysisHtml(Set.of(imageRef)); - assertEquals(jsonReport, result.get()); + + @Test + @SetEnvironmentVariable(key = "EXHORT_DEV_MODE", value = "false") + void check_Exhort_Url_When_env_DEV_Mode_false_And_Property_Dev_Mode_true_Default_Exhort_URL_Selected() { + System.setProperty("EXHORT_DEV_MODE", "true"); + ExhortApi exhortApi = new ExhortApi(); + then(exhortApi.getEndpoint()).isEqualTo(ExhortApi.DEFAULT_ENDPOINT); + then(exhortApi.getEndpoint()).isNotEqualTo(ExhortApi.DEFAULT_ENDPOINT_DEV); } - } - - @Test - @SetEnvironmentVariable(key = "EXHORT_SNYK_TOKEN", value = "snyk-token-from-env-var") - @SetEnvironmentVariable(key = "RHDA_TOKEN", value = "rhda-token-from-env-var") - @SetEnvironmentVariable(key = "RHDA_SOURCE", value = "rhda-source-from-env-var") - void test_perform_batch_analysis() throws IOException, MalformedPackageURLException, ExecutionException, InterruptedException { - try (var is = getResourceAsStreamDecision(this.getClass(), new String[]{"msc", "image", "image_sbom.json"})) { - var sbomsGenerator = mock(Supplier.class); - var responseBodyHandler = mock(HttpResponse.BodyHandler.class); - var responseGenerator = mock(Function.class); - var exceptionResponseGenerator = mock(Supplier.class); - - ArgumentMatcher matchesRequest = r -> - r.uri().equals(URI.create(String.format("%s/api/v4/batch-analysis", exhortApiSut.getEndpoint()))) && - r.headers().firstValue("Content-Type").get().equals(Api.CYCLONEDX_MEDIA_TYPE) && - r.headers().firstValue("Accept").get().equals(Api.MediaType.APPLICATION_JSON.toString()) && - r.headers().firstValue("ex-snyk-token").get().equals("snyk-token-from-env-var") && - r.headers().firstValue("rhda-token").get().equals("rhda-token-from-env-var") && - r.headers().firstValue("rhda-source").get().equals("rhda-source-from-env-var") && - r.headers().firstValue("rhda-operation-type").get().equals("Image Analysis") && - r.method().equals("POST"); - - var imageRef = new ImageRef("test.io/test/test-app:test-version@sha256:1fafb0905264413501df60d90a92ca32df8a2011cbfb4876ddff5ceb20c8f165", "linux/amd64"); - var sboms = new HashMap(); - sboms.put(imageRef.getPackageURL().canonicalize(), new ObjectMapper().readTree(is)); - - var httpResponse = mock(HttpResponse.class); - when(httpResponse.statusCode()).thenReturn(200); - - when(mockHttpClient.sendAsync(argThat(matchesRequest), any())) - .thenReturn(CompletableFuture.completedFuture(httpResponse)); - - when(sbomsGenerator.get()).thenReturn(sboms); - - var expectedResult = "test-result"; - when(responseGenerator.apply(eq(httpResponse))).thenReturn(expectedResult); - - var result = exhortApiSut.performBatchAnalysis( - sbomsGenerator, - Api.MediaType.APPLICATION_JSON, - responseBodyHandler, - responseGenerator, - exceptionResponseGenerator, - "Image Analysis"); - - assertEquals(expectedResult, result.get()); + + @Test + @SetEnvironmentVariable(key = "EXHORT_DEV_MODE", value = "false") + @SetEnvironmentVariable(key = "DEV_EXHORT_BACKEND_URL", value = "http://dummy-route") + void check_Exhort_Url_When_env_DEV_Mode_false_And_DEV_Exhort_Url_Set_Then_Default_Exhort_URL_Selected_Anyway() { + System.setProperty("EXHORT_DEV_MODE", "true"); + System.setProperty("DEV_EXHORT_BACKEND_URL", "http://dummy-route2"); + ExhortApi exhortApi = new ExhortApi(); + then(exhortApi.getEndpoint()).isEqualTo(ExhortApi.DEFAULT_ENDPOINT); + then(exhortApi.getEndpoint()).isNotEqualTo(System.getenv("DEV_EXHORT_BACKEND_URL")); + then(exhortApi.getEndpoint()).isNotEqualTo(System.getProperty("DEV_EXHORT_BACKEND_URL")); } - } - @Test - void test_get_batch_image_sboms() throws IOException, MalformedPackageURLException { - try (MockedStatic mock = Mockito.mockStatic(Operations.class); - var is = getResourceAsStreamDecision(this.getClass(), new String[]{"msc", "image", "image_sbom.json"})) { - var imageRef = new ImageRef("test.io/test/test-app:test-version@sha256:1fafb0905264413501df60d90a92ca32df8a2011cbfb4876ddff5ceb20c8f165", "linux/amd64"); + @Test + void + check_Exhort_Url_When_env_DEV_Mode_not_set_And_Property_Exhort_Dev_Mode_false_Then_Default_Exhort_URL_Selected() { + System.setProperty("EXHORT_DEV_MODE", "false"); + ExhortApi exhortApi = new ExhortApi(); + then(exhortApi.getEndpoint()).isEqualTo(ExhortApi.DEFAULT_ENDPOINT); + then(exhortApi.getEndpoint()).isNotEqualTo(ExhortApi.DEFAULT_ENDPOINT_DEV); + } - var json = new BufferedReader(new InputStreamReader(is, StandardCharsets.UTF_8)).lines().collect(Collectors.joining("\n")); - var output = new Operations.ProcessExecOutput(json, "", 0); + @Test + void + check_Exhort_Url_When_env_DEV_Mode_not_set_And_Property_Exhort_Dev_Mode_true_Then_Default_DEV_Exhort_URL_Selected() { + System.setProperty("EXHORT_DEV_MODE", "true"); + ExhortApi exhortApi = new ExhortApi(); + then(exhortApi.getEndpoint()).isNotEqualTo(ExhortApi.DEFAULT_ENDPOINT); + then(exhortApi.getEndpoint()).isEqualTo(ExhortApi.DEFAULT_ENDPOINT_DEV); + } - mock.when(() -> Operations.getCustomPathOrElse(eq("syft"))) - .thenReturn("syft"); + @Test + @SetEnvironmentVariable(key = "DEV_EXHORT_BACKEND_URL", value = "http://dummy-route") + void + check_Exhort_Url_When_env_DEV_Mode_not_set_And_Property_Exhort_Dev_Mode_true_and_Env_DEV_Exhort_Backend_Url_Set_Then_DEV_ENV_Exhort_URL_Selected() { + System.setProperty("EXHORT_DEV_MODE", "true"); + System.setProperty("DEV_EXHORT_BACKEND_URL", "http://dummy-route2"); + ExhortApi exhortApi = new ExhortApi(); + then(exhortApi.getEndpoint()).isNotEqualTo(ExhortApi.DEFAULT_ENDPOINT); + then(exhortApi.getEndpoint()).isNotEqualTo(ExhortApi.DEFAULT_ENDPOINT_DEV); + then(exhortApi.getEndpoint()).isNotEqualTo("http://dummy-route2"); + then(exhortApi.getEndpoint()).isEqualTo("http://dummy-route"); + } - mock.when(() -> Operations.runProcessGetFullOutput(isNull(), - aryEq(new String[]{"syft", imageRef.getImage().getFullName(), - "-s", "all-layers", "-o", "cyclonedx-json", "-q"}), - isNull())) - .thenReturn(output); + @Test + void check_Exhort_Url_When_Nothing_Set_Then_Default_Exhort_URL_Selected() { + ExhortApi exhortApi = new ExhortApi(); + then(exhortApi.getEndpoint()).isEqualTo(ExhortApi.DEFAULT_ENDPOINT); + } - var sboms = exhortApiSut.getBatchImageSboms(Set.of(imageRef)); + @Test + @SetEnvironmentVariable(key = "EXHORT_SNYK_TOKEN", value = "snyk-token-from-env-var") + @SetEnvironmentVariable(key = "RHDA_TOKEN", value = "rhda-token-from-env-var") + @SetEnvironmentVariable(key = "RHDA_SOURCE", value = "rhda-source-from-env-var") + void test_image_analysis() + throws IOException, ExecutionException, InterruptedException, MalformedPackageURLException { + try (MockedStatic mock = Mockito.mockStatic(Operations.class); + var sbomIS = + getResourceAsStreamDecision(this.getClass(), new String[] {"msc", "image", "image_sbom.json"}); + var reportIS = getResourceAsStreamDecision( + this.getClass(), new String[] {"msc", "image", "image_reports.json"})) { + + var imageRef = new ImageRef( + "test.io/test/test-app:test-version@sha256:1fafb0905264413501df60d90a92ca32df8a2011cbfb4876ddff5ceb20c8f165", + "linux/amd64"); + + var jsonSbom = new BufferedReader(new InputStreamReader(sbomIS, StandardCharsets.UTF_8)) + .lines() + .collect(Collectors.joining("\n")); + var output = new Operations.ProcessExecOutput(jsonSbom, "", 0); + + mock.when(() -> Operations.getCustomPathOrElse(eq("syft"))).thenReturn("syft"); + + mock.when(() -> Operations.runProcessGetFullOutput( + isNull(), + aryEq(new String[] { + "syft", + imageRef.getImage().getFullName(), + "-s", + "all-layers", + "-o", + "cyclonedx-json", + "-q" + }), + isNull())) + .thenReturn(output); + + var jsonReport = new BufferedReader(new InputStreamReader(reportIS, StandardCharsets.UTF_8)) + .lines() + .collect(Collectors.joining("\n")); + + var httpResponse = mock(HttpResponse.class); + when(httpResponse.statusCode()).thenReturn(200); + when(httpResponse.body()).thenReturn(jsonReport); + + ArgumentMatcher matchesRequest = r -> r.uri() + .equals(URI.create(String.format("%s/api/v4/batch-analysis", exhortApiSut.getEndpoint()))) + && r.headers().firstValue("Content-Type").get().equals(Api.CYCLONEDX_MEDIA_TYPE) + && r.headers().firstValue("Accept").get().equals(Api.MediaType.APPLICATION_JSON.toString()) + && r.headers().firstValue("ex-snyk-token").get().equals("snyk-token-from-env-var") + && r.headers().firstValue("rhda-token").get().equals("rhda-token-from-env-var") + && r.headers().firstValue("rhda-source").get().equals("rhda-source-from-env-var") + && r.headers().firstValue("rhda-operation-type").get().equals("Image Analysis") + && r.method().equals("POST"); + + when(mockHttpClient.sendAsync(argThat(matchesRequest), any())) + .thenReturn(CompletableFuture.completedFuture(httpResponse)); + + var result = exhortApiSut.imageAnalysis(Set.of(imageRef)); + var reports = result.get(); + assertEquals(2, reports.size()); + assertTrue( + reports.containsKey( + new ImageRef( + new PackageURL( + "pkg:oci/ubi@sha256:f5983f7c7878cc9b26a3962be7756e3c810e9831b0b9f9613e6f6b445f884e74?repository_url=registry.access.redhat.com/ubi9/ubi&tag=9.3-1552&arch=amd64")))); + assertTrue( + reports.containsKey( + new ImageRef( + new PackageURL( + "pkg:oci/default-app@sha256:333224a233db31852ac1085c6cd702016ab8aaf54cecde5c4bed5451d636adcf?repository_url=quay.io/default-app&tag=0.0.1")))); + assertNotNull( + reports.get( + new ImageRef( + new PackageURL( + "pkg:oci/ubi@sha256:f5983f7c7878cc9b26a3962be7756e3c810e9831b0b9f9613e6f6b445f884e74?repository_url=registry.access.redhat.com/ubi9/ubi&tag=9.3-1552&arch=amd64r")))); + assertNotNull( + reports.get( + new ImageRef( + new PackageURL( + "pkg:oci/default-app@sha256:333224a233db31852ac1085c6cd702016ab8aaf54cecde5c4bed5451d636adcf?repository_url=quay.io/default-app&tag=0.0.1")))); + } + } - var mapper = new ObjectMapper(); - var node = mapper.readTree(json); - ((ObjectNode) node.get("metadata").get("component")).set("purl", new TextNode(imageRef.getPackageURL().canonicalize())); + @Test + @SetEnvironmentVariable(key = "EXHORT_SNYK_TOKEN", value = "snyk-token-from-env-var") + @SetEnvironmentVariable(key = "RHDA_TOKEN", value = "rhda-token-from-env-var") + @SetEnvironmentVariable(key = "RHDA_SOURCE", value = "rhda-source-from-env-var") + void imageAnalysisHtml() + throws IOException, ExecutionException, InterruptedException, MalformedPackageURLException { + try (MockedStatic mock = Mockito.mockStatic(Operations.class); + var sbomIS = + getResourceAsStreamDecision(this.getClass(), new String[] {"msc", "image", "image_sbom.json"}); + var reportIS = getResourceAsStreamDecision( + this.getClass(), new String[] {"msc", "image", "image_reports.json"})) { + + var imageRef = new ImageRef( + "test.io/test/test-app:test-version@sha256:1fafb0905264413501df60d90a92ca32df8a2011cbfb4876ddff5ceb20c8f165", + "linux/amd64"); + + var jsonSbom = new BufferedReader(new InputStreamReader(sbomIS, StandardCharsets.UTF_8)) + .lines() + .collect(Collectors.joining("\n")); + var output = new Operations.ProcessExecOutput(jsonSbom, "", 0); + + mock.when(() -> Operations.getCustomPathOrElse(eq("syft"))).thenReturn("syft"); + + mock.when(() -> Operations.runProcessGetFullOutput( + isNull(), + aryEq(new String[] { + "syft", + imageRef.getImage().getFullName(), + "-s", + "all-layers", + "-o", + "cyclonedx-json", + "-q" + }), + isNull())) + .thenReturn(output); + + var jsonReport = new BufferedReader(new InputStreamReader(reportIS, StandardCharsets.UTF_8)) + .lines() + .collect(Collectors.joining("\n")); + + var httpResponse = mock(HttpResponse.class); + when(httpResponse.statusCode()).thenReturn(200); + when(httpResponse.body()).thenReturn(jsonReport); + + ArgumentMatcher matchesRequest = r -> r.uri() + .equals(URI.create(String.format("%s/api/v4/batch-analysis", exhortApiSut.getEndpoint()))) + && r.headers().firstValue("Content-Type").get().equals(Api.CYCLONEDX_MEDIA_TYPE) + && r.headers().firstValue("Accept").get().equals(Api.MediaType.TEXT_HTML.toString()) + && r.headers().firstValue("ex-snyk-token").get().equals("snyk-token-from-env-var") + && r.headers().firstValue("rhda-token").get().equals("rhda-token-from-env-var") + && r.headers().firstValue("rhda-source").get().equals("rhda-source-from-env-var") + && r.headers().firstValue("rhda-operation-type").get().equals("Image Analysis") + && r.method().equals("POST"); + + when(mockHttpClient.sendAsync(argThat(matchesRequest), any())) + .thenReturn(CompletableFuture.completedFuture(httpResponse)); + + var result = exhortApiSut.imageAnalysisHtml(Set.of(imageRef)); + assertEquals(jsonReport, result.get()); + } + } - var map = new HashMap(); - map.put(imageRef.getPackageURL().canonicalize(), node); + @Test + @SetEnvironmentVariable(key = "EXHORT_SNYK_TOKEN", value = "snyk-token-from-env-var") + @SetEnvironmentVariable(key = "RHDA_TOKEN", value = "rhda-token-from-env-var") + @SetEnvironmentVariable(key = "RHDA_SOURCE", value = "rhda-source-from-env-var") + void test_perform_batch_analysis() + throws IOException, MalformedPackageURLException, ExecutionException, InterruptedException { + try (var is = getResourceAsStreamDecision(this.getClass(), new String[] {"msc", "image", "image_sbom.json"})) { + var sbomsGenerator = mock(Supplier.class); + var responseBodyHandler = mock(HttpResponse.BodyHandler.class); + var responseGenerator = mock(Function.class); + var exceptionResponseGenerator = mock(Supplier.class); + + ArgumentMatcher matchesRequest = r -> r.uri() + .equals(URI.create(String.format("%s/api/v4/batch-analysis", exhortApiSut.getEndpoint()))) + && r.headers().firstValue("Content-Type").get().equals(Api.CYCLONEDX_MEDIA_TYPE) + && r.headers().firstValue("Accept").get().equals(Api.MediaType.APPLICATION_JSON.toString()) + && r.headers().firstValue("ex-snyk-token").get().equals("snyk-token-from-env-var") + && r.headers().firstValue("rhda-token").get().equals("rhda-token-from-env-var") + && r.headers().firstValue("rhda-source").get().equals("rhda-source-from-env-var") + && r.headers().firstValue("rhda-operation-type").get().equals("Image Analysis") + && r.method().equals("POST"); + + var imageRef = new ImageRef( + "test.io/test/test-app:test-version@sha256:1fafb0905264413501df60d90a92ca32df8a2011cbfb4876ddff5ceb20c8f165", + "linux/amd64"); + var sboms = new HashMap(); + sboms.put(imageRef.getPackageURL().canonicalize(), new ObjectMapper().readTree(is)); + + var httpResponse = mock(HttpResponse.class); + when(httpResponse.statusCode()).thenReturn(200); + + when(mockHttpClient.sendAsync(argThat(matchesRequest), any())) + .thenReturn(CompletableFuture.completedFuture(httpResponse)); + + when(sbomsGenerator.get()).thenReturn(sboms); + + var expectedResult = "test-result"; + when(responseGenerator.apply(eq(httpResponse))).thenReturn(expectedResult); + + var result = exhortApiSut.performBatchAnalysis( + sbomsGenerator, + Api.MediaType.APPLICATION_JSON, + responseBodyHandler, + responseGenerator, + exceptionResponseGenerator, + "Image Analysis"); + + assertEquals(expectedResult, result.get()); + } + } - assertEquals(map, sboms); + @Test + void test_get_batch_image_sboms() throws IOException, MalformedPackageURLException { + try (MockedStatic mock = Mockito.mockStatic(Operations.class); + var is = getResourceAsStreamDecision( + this.getClass(), new String[] {"msc", "image", "image_sbom.json"})) { + var imageRef = new ImageRef( + "test.io/test/test-app:test-version@sha256:1fafb0905264413501df60d90a92ca32df8a2011cbfb4876ddff5ceb20c8f165", + "linux/amd64"); + + var json = new BufferedReader(new InputStreamReader(is, StandardCharsets.UTF_8)) + .lines() + .collect(Collectors.joining("\n")); + var output = new Operations.ProcessExecOutput(json, "", 0); + + mock.when(() -> Operations.getCustomPathOrElse(eq("syft"))).thenReturn("syft"); + + mock.when(() -> Operations.runProcessGetFullOutput( + isNull(), + aryEq(new String[] { + "syft", + imageRef.getImage().getFullName(), + "-s", + "all-layers", + "-o", + "cyclonedx-json", + "-q" + }), + isNull())) + .thenReturn(output); + + var sboms = exhortApiSut.getBatchImageSboms(Set.of(imageRef)); + + var mapper = new ObjectMapper(); + var node = mapper.readTree(json); + ((ObjectNode) node.get("metadata").get("component")) + .set("purl", new TextNode(imageRef.getPackageURL().canonicalize())); + + var map = new HashMap(); + map.put(imageRef.getPackageURL().canonicalize(), node); + + assertEquals(map, sboms); + } } - } - - @Test - void test_get_batch_image_analysis_reports() throws IOException, MalformedPackageURLException { - try (var is = getResourceAsStreamDecision(this.getClass(), new String[]{"msc", "image", "image_reports.json"})) { - var json = new BufferedReader(new InputStreamReader(is, StandardCharsets.UTF_8)).lines().collect(Collectors.joining("\n")); - - var httpResponse = mock(HttpResponse.class); - when(httpResponse.statusCode()).thenReturn(200); - when(httpResponse.body()).thenReturn(json); - - var reports = exhortApiSut.getBatchImageAnalysisReports(httpResponse); - assertEquals(2, reports.size()); - assertTrue(reports.containsKey(new ImageRef(new PackageURL("pkg:oci/ubi@sha256:f5983f7c7878cc9b26a3962be7756e3c810e9831b0b9f9613e6f6b445f884e74?repository_url=registry.access.redhat.com/ubi9/ubi&tag=9.3-1552&arch=amd64")))); - assertTrue(reports.containsKey(new ImageRef(new PackageURL("pkg:oci/default-app@sha256:333224a233db31852ac1085c6cd702016ab8aaf54cecde5c4bed5451d636adcf?repository_url=quay.io/default-app&tag=0.0.1")))); - assertNotNull(reports.get(new ImageRef(new PackageURL("pkg:oci/ubi@sha256:f5983f7c7878cc9b26a3962be7756e3c810e9831b0b9f9613e6f6b445f884e74?repository_url=registry.access.redhat.com/ubi9/ubi&tag=9.3-1552&arch=amd64r")))); - assertNotNull(reports.get(new ImageRef(new PackageURL("pkg:oci/default-app@sha256:333224a233db31852ac1085c6cd702016ab8aaf54cecde5c4bed5451d636adcf?repository_url=quay.io/default-app&tag=0.0.1")))); + + @Test + void test_get_batch_image_analysis_reports() throws IOException, MalformedPackageURLException { + try (var is = + getResourceAsStreamDecision(this.getClass(), new String[] {"msc", "image", "image_reports.json"})) { + var json = new BufferedReader(new InputStreamReader(is, StandardCharsets.UTF_8)) + .lines() + .collect(Collectors.joining("\n")); + + var httpResponse = mock(HttpResponse.class); + when(httpResponse.statusCode()).thenReturn(200); + when(httpResponse.body()).thenReturn(json); + + var reports = exhortApiSut.getBatchImageAnalysisReports(httpResponse); + assertEquals(2, reports.size()); + assertTrue( + reports.containsKey( + new ImageRef( + new PackageURL( + "pkg:oci/ubi@sha256:f5983f7c7878cc9b26a3962be7756e3c810e9831b0b9f9613e6f6b445f884e74?repository_url=registry.access.redhat.com/ubi9/ubi&tag=9.3-1552&arch=amd64")))); + assertTrue( + reports.containsKey( + new ImageRef( + new PackageURL( + "pkg:oci/default-app@sha256:333224a233db31852ac1085c6cd702016ab8aaf54cecde5c4bed5451d636adcf?repository_url=quay.io/default-app&tag=0.0.1")))); + assertNotNull( + reports.get( + new ImageRef( + new PackageURL( + "pkg:oci/ubi@sha256:f5983f7c7878cc9b26a3962be7756e3c810e9831b0b9f9613e6f6b445f884e74?repository_url=registry.access.redhat.com/ubi9/ubi&tag=9.3-1552&arch=amd64r")))); + assertNotNull( + reports.get( + new ImageRef( + new PackageURL( + "pkg:oci/default-app@sha256:333224a233db31852ac1085c6cd702016ab8aaf54cecde5c4bed5451d636adcf?repository_url=quay.io/default-app&tag=0.0.1")))); + } } - } - @Test - void test_get_batch_image_analysis_reports_error_response() { - var httpResponse = mock(HttpResponse.class); - when(httpResponse.statusCode()).thenReturn(400); + @Test + void test_get_batch_image_analysis_reports_error_response() { + var httpResponse = mock(HttpResponse.class); + when(httpResponse.statusCode()).thenReturn(400); - var reports = exhortApiSut.getBatchImageAnalysisReports(httpResponse); - assertTrue(reports.isEmpty()); - } + var reports = exhortApiSut.getBatchImageAnalysisReports(httpResponse); + assertTrue(reports.isEmpty()); + } - @Test - void test_get_batch_analysis_reports_from_response() { - var httpResponse = mock(HttpResponse.class); - when(httpResponse.statusCode()).thenReturn(200); + @Test + void test_get_batch_analysis_reports_from_response() { + var httpResponse = mock(HttpResponse.class); + when(httpResponse.statusCode()).thenReturn(200); - var responseGenerator = mock(Function.class); + var responseGenerator = mock(Function.class); - exhortApiSut.getBatchAnalysisReportsFromResponse(httpResponse, responseGenerator, - "test-operation", "testReport", "testTraceId"); + exhortApiSut.getBatchAnalysisReportsFromResponse( + httpResponse, responseGenerator, "test-operation", "testReport", "testTraceId"); - verify(responseGenerator).apply(eq(httpResponse)); - } + verify(responseGenerator).apply(eq(httpResponse)); + } } diff --git a/src/test/java/com/redhat/exhort/providers/GoModulesMainModuleVersionTest.java b/src/test/java/com/redhat/exhort/providers/GoModulesMainModuleVersionTest.java index eeccea8a..8ab9bbee 100644 --- a/src/test/java/com/redhat/exhort/providers/GoModulesMainModuleVersionTest.java +++ b/src/test/java/com/redhat/exhort/providers/GoModulesMainModuleVersionTest.java @@ -15,105 +15,89 @@ */ package com.redhat.exhort.providers; -import com.redhat.exhort.tools.Operations; -import org.apache.commons.io.FileSystemUtils; -import org.junit.jupiter.api.*; +import static org.junit.jupiter.api.Assertions.*; +import com.redhat.exhort.tools.Operations; import java.io.IOException; import java.nio.file.Files; import java.nio.file.Path; import java.util.regex.Pattern; - import org.apache.commons.io.FileUtils; - -import static org.junit.jupiter.api.Assertions.*; +import org.junit.jupiter.api.*; @Tag("gitTest") class GoModulesMainModuleVersionTest { - private Path noGitRepo; - private Path testGitRepo; - private GoModulesProvider goModulesProvider; - - - @BeforeEach - void setUp() { - try { - this.goModulesProvider = new GoModulesProvider(); - this.testGitRepo = Files.createTempDirectory("exhort_tmp"); - Operations.runProcessGetOutput(this.testGitRepo,"git" , "init"); - Operations.runProcessGetOutput(this.testGitRepo,"git" , "config","user.email","tester@exhort-java-api.com"); - Operations.runProcessGetOutput(this.testGitRepo,"git" , "config","user.name","exhort-java-api-tester"); - this.noGitRepo = Files.createTempDirectory("exhort_tmp"); - } catch (IOException e) { - throw new RuntimeException(e); + private Path noGitRepo; + private Path testGitRepo; + private GoModulesProvider goModulesProvider; + + @BeforeEach + void setUp() { + try { + this.goModulesProvider = new GoModulesProvider(); + this.testGitRepo = Files.createTempDirectory("exhort_tmp"); + Operations.runProcessGetOutput(this.testGitRepo, "git", "init"); + Operations.runProcessGetOutput( + this.testGitRepo, "git", "config", "user.email", "tester@exhort-java-api.com"); + Operations.runProcessGetOutput(this.testGitRepo, "git", "config", "user.name", "exhort-java-api-tester"); + this.noGitRepo = Files.createTempDirectory("exhort_tmp"); + } catch (IOException e) { + throw new RuntimeException(e); + } } - } - @AfterEach - void tearDown() { - try { - FileUtils.deleteDirectory(this.testGitRepo.toFile()); - FileUtils.deleteDirectory(this.noGitRepo.toFile()); + @AfterEach + void tearDown() { + try { + FileUtils.deleteDirectory(this.testGitRepo.toFile()); + FileUtils.deleteDirectory(this.noGitRepo.toFile()); - } catch (IOException e) { - throw new RuntimeException(e); + } catch (IOException e) { + throw new RuntimeException(e); + } } - } - - - - - @Test - void determine_Main_Module_Version_NoRepo() { - goModulesProvider.determineMainModuleVersion(noGitRepo); - assertEquals(goModulesProvider.defaultMainVersion,goModulesProvider.getMainModuleVersion()); - } - - @Test - void determine_Main_Module_Version_GitRepo() { - goModulesProvider.determineMainModuleVersion(testGitRepo); - assertEquals(goModulesProvider.defaultMainVersion, goModulesProvider.getMainModuleVersion()); - } - - @Test - void determine_Main_Module_Version_GitRepo_commit_is_tag() { - - Operations.runProcessGetOutput(this.testGitRepo, "git", "commit", "-m \"sample\"", "--allow-empty"); - Operations.runProcessGetOutput(this.testGitRepo, "git", "tag", "v1.0.0"); - - goModulesProvider.determineMainModuleVersion(testGitRepo); - assertEquals("v1.0.0", goModulesProvider.getMainModuleVersion()); - - } - - @Test - void determine_Main_Module_Version_GitRepo_commit_is_annotated_tag() { - - Operations.runProcessGetOutput(this.testGitRepo, "git", "commit", "-m \"sample\"", "--allow-empty"); - Operations.runProcessGetOutput(this.testGitRepo, "git", "tag", "-a", "-m" ,"annotatedTag", "v1.0.0a"); - - goModulesProvider.determineMainModuleVersion(testGitRepo); - assertEquals("v1.0.0a", goModulesProvider.getMainModuleVersion()); - - } + @Test + void determine_Main_Module_Version_NoRepo() { + goModulesProvider.determineMainModuleVersion(noGitRepo); + assertEquals(goModulesProvider.defaultMainVersion, goModulesProvider.getMainModuleVersion()); + } + @Test + void determine_Main_Module_Version_GitRepo() { + goModulesProvider.determineMainModuleVersion(testGitRepo); + assertEquals(goModulesProvider.defaultMainVersion, goModulesProvider.getMainModuleVersion()); + } - @Test - void determine_Main_Module_Version_GitRepo_commit_is_after_tag() { + @Test + void determine_Main_Module_Version_GitRepo_commit_is_tag() { - Operations.runProcessGetOutput(this.testGitRepo, "git", "commit", "-m \"sample\"", "--allow-empty"); - Operations.runProcessGetOutput(this.testGitRepo, "git", "tag", "v1.0.0"); - Operations.runProcessGetOutput(this.testGitRepo, "git", "commit", "-m \"sample2\"", "--allow-empty"); + Operations.runProcessGetOutput(this.testGitRepo, "git", "commit", "-m \"sample\"", "--allow-empty"); + Operations.runProcessGetOutput(this.testGitRepo, "git", "tag", "v1.0.0"); - goModulesProvider.determineMainModuleVersion(testGitRepo); - assertTrue(Pattern.matches("v1.0.1-0.[0-9]{14}-[a-f0-9]{12}",goModulesProvider.getMainModuleVersion())); + goModulesProvider.determineMainModuleVersion(testGitRepo); + assertEquals("v1.0.0", goModulesProvider.getMainModuleVersion()); + } - } + @Test + void determine_Main_Module_Version_GitRepo_commit_is_annotated_tag() { + Operations.runProcessGetOutput(this.testGitRepo, "git", "commit", "-m \"sample\"", "--allow-empty"); + Operations.runProcessGetOutput(this.testGitRepo, "git", "tag", "-a", "-m", "annotatedTag", "v1.0.0a"); + goModulesProvider.determineMainModuleVersion(testGitRepo); + assertEquals("v1.0.0a", goModulesProvider.getMainModuleVersion()); + } + @Test + void determine_Main_Module_Version_GitRepo_commit_is_after_tag() { + Operations.runProcessGetOutput(this.testGitRepo, "git", "commit", "-m \"sample\"", "--allow-empty"); + Operations.runProcessGetOutput(this.testGitRepo, "git", "tag", "v1.0.0"); + Operations.runProcessGetOutput(this.testGitRepo, "git", "commit", "-m \"sample2\"", "--allow-empty"); + goModulesProvider.determineMainModuleVersion(testGitRepo); + assertTrue(Pattern.matches("v1.0.1-0.[0-9]{14}-[a-f0-9]{12}", goModulesProvider.getMainModuleVersion())); + } } diff --git a/src/test/java/com/redhat/exhort/providers/Golang_Modules_Provider_Test.java b/src/test/java/com/redhat/exhort/providers/Golang_Modules_Provider_Test.java index 542fc01e..71090fd9 100644 --- a/src/test/java/com/redhat/exhort/providers/Golang_Modules_Provider_Test.java +++ b/src/test/java/com/redhat/exhort/providers/Golang_Modules_Provider_Test.java @@ -15,151 +15,167 @@ */ package com.redhat.exhort.providers; +import static org.assertj.core.api.Assertions.*; +import static org.junit.jupiter.api.Assertions.*; + import com.fasterxml.jackson.databind.ObjectMapper; import com.redhat.exhort.Api; import com.redhat.exhort.ExhortTest; -import org.junit.jupiter.api.Test; -import org.junit.jupiter.api.extension.ExtendWith; -import org.junit.jupiter.params.ParameterizedTest; -import org.junit.jupiter.params.provider.MethodSource; -import org.junit.jupiter.params.provider.ValueSource; - import java.io.IOException; import java.nio.file.Files; import java.nio.file.Path; import java.util.Arrays; import java.util.stream.Stream; - -import static org.assertj.core.api.Assertions.*; -import static org.junit.jupiter.api.Assertions.*; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.MethodSource; +import org.junit.jupiter.params.provider.ValueSource; @ExtendWith(HelperExtension.class) class Golang_Modules_Provider_Test extends ExhortTest { - // test folder are located at src/test/resources/tst_manifests/npm - // each folder should contain: - // - package.json: the target manifest for testing - // - expected_sbom.json: the SBOM expected to be provided - static Stream testFolders() { - return Stream.of( - "go_mod_light_no_ignore", - "go_mod_no_ignore", - "go_mod_with_ignore", - "go_mod_with_all_ignore", - "go_mod_with_one_ignored_prefix_go", - "go_mod_no_path" - ); - } - - @ParameterizedTest - @MethodSource("testFolders") - void test_the_provideStack(String testFolder) throws IOException, InterruptedException { - // create temp file hosting our sut package.json - var tmpGoModulesDir = Files.createTempDirectory("exhort_test_"); - var tmpGolangFile = Files.createFile(tmpGoModulesDir.resolve("go.mod")); - try (var is = getResourceAsStreamDecision(this.getClass(), new String [] { "tst_manifests", "golang", testFolder, "go.mod"})) { - Files.write(tmpGolangFile, is.readAllBytes()); - } - // load expected SBOM - String expectedSbom; - try (var is = getResourceAsStreamDecision(this.getClass(), new String [] { "tst_manifests", "golang", testFolder, "expected_sbom_stack_analysis.json"})) { - expectedSbom = new String(is.readAllBytes()); - } - // when providing stack content for our pom - var content = new GoModulesProvider().provideStack(tmpGolangFile); - // cleanup - Files.deleteIfExists(tmpGolangFile); - // verify expected SBOM is returned - assertThat(content.type).isEqualTo(Api.CYCLONEDX_MEDIA_TYPE); - assertThat(dropIgnored(new String(content.buffer))) - .isEqualTo(dropIgnored(expectedSbom)); - } - - @ParameterizedTest - @MethodSource("testFolders") - void test_the_provideComponent(String testFolder) throws IOException, InterruptedException { - // load the pom target pom file - byte[] targetPom; - try (var is = getResourceAsStreamDecision(this.getClass(), new String [] { "tst_manifests", "golang", testFolder, "go.mod"})) { - targetPom = is.readAllBytes(); + // test folder are located at src/test/resources/tst_manifests/npm + // each folder should contain: + // - package.json: the target manifest for testing + // - expected_sbom.json: the SBOM expected to be provided + static Stream testFolders() { + return Stream.of( + "go_mod_light_no_ignore", + "go_mod_no_ignore", + "go_mod_with_ignore", + "go_mod_with_all_ignore", + "go_mod_with_one_ignored_prefix_go", + "go_mod_no_path"); } - // load expected SBOM - String expectedSbom = ""; - try (var is = getResourceAsStreamDecision(this.getClass(), new String [] { "tst_manifests", "golang", testFolder, "expected_sbom_component_analysis.json"})) { - expectedSbom = new String(is.readAllBytes()); - } - // when providing component content for our pom - var content = new GoModulesProvider().provideComponent(targetPom); - // verify expected SBOM is returned - assertThat(content.type).isEqualTo(Api.CYCLONEDX_MEDIA_TYPE); - assertThat(dropIgnored(new String(content.buffer))) - .isEqualTo(dropIgnored(expectedSbom)); - - - } - - - @Test - void Test_The_ProvideComponent_Path_Should_Throw_Exception() { - - GoModulesProvider goModulesProvider = new GoModulesProvider(); - assertThatIllegalArgumentException().isThrownBy(() -> { - goModulesProvider.provideComponent(Path.of(".")); - }).withMessage("provideComponent with file system path for GoModules package manager not implemented yet"); - - - } - - @ParameterizedTest - @ValueSource(booleans = { true,false }) - void Test_Golang_Modules_with_Match_Manifest_Version(boolean MatchManifestVersionsEnabled) { - String goModPath = getFileFromResource("go.mod", "msc", "golang", "go.mod"); - GoModulesProvider goModulesProvider = new GoModulesProvider(); - - if(MatchManifestVersionsEnabled) - { - System.setProperty("MATCH_MANIFEST_VERSIONS", "true"); - RuntimeException runtimeException = assertThrows(RuntimeException.class, () -> goModulesProvider.getDependenciesSbom(Path.of(goModPath), true), "Expected getDependenciesSbom/2 to throw RuntimeException, due to version mismatch, but it didn't."); - assertTrue(runtimeException.getMessage().contains("Can't continue with analysis - versions mismatch for dependency name=github.com/google/uuid, manifest version=v1.1.0, installed Version=v1.1.1")); - System.clearProperty("MATCH_MANIFEST_VERSIONS"); - } - else - { - String sbomString = assertDoesNotThrow(() -> goModulesProvider.getDependenciesSbom(Path.of(goModPath), false).getAsJsonString()); - String actualSbomWithTSStripped = dropIgnoredKeepFormat(sbomString); - assertEquals(getStringFromFile("msc","golang","expected_sbom_ca.json").trim(), actualSbomWithTSStripped); - System.out.println(sbomString); - } - } - - @Test - void Test_Golang_MvS_Logic_Enabled() throws IOException { - ObjectMapper om = new ObjectMapper(); - System.setProperty("EXHORT_GO_MVS_LOGIC_ENABLED", "true"); - String goModPath = getFileFromResource("go.mod", "msc", "golang","mvs_logic", "go.mod"); - GoModulesProvider goModulesProvider = new GoModulesProvider(); - String resultSbom = dropIgnoredKeepFormat(goModulesProvider.getDependenciesSbom(Path.of(goModPath),true).getAsJsonString()); - String expectedSbom = getStringFromFile("msc", "golang", "mvs_logic", "expected_sbom_stack_analysis.json").trim(); + @ParameterizedTest + @MethodSource("testFolders") + void test_the_provideStack(String testFolder) throws IOException, InterruptedException { + // create temp file hosting our sut package.json + var tmpGoModulesDir = Files.createTempDirectory("exhort_test_"); + var tmpGolangFile = Files.createFile(tmpGoModulesDir.resolve("go.mod")); + try (var is = getResourceAsStreamDecision( + this.getClass(), new String[] {"tst_manifests", "golang", testFolder, "go.mod"})) { + Files.write(tmpGolangFile, is.readAllBytes()); + } + // load expected SBOM + String expectedSbom; + try (var is = getResourceAsStreamDecision( + this.getClass(), + new String[] {"tst_manifests", "golang", testFolder, "expected_sbom_stack_analysis.json"})) { + expectedSbom = new String(is.readAllBytes()); + } + // when providing stack content for our pom + var content = new GoModulesProvider().provideStack(tmpGolangFile); + // cleanup + Files.deleteIfExists(tmpGolangFile); + // verify expected SBOM is returned + assertThat(content.type).isEqualTo(Api.CYCLONEDX_MEDIA_TYPE); + assertThat(dropIgnored(new String(content.buffer))).isEqualTo(dropIgnored(expectedSbom)); + } - assertEquals(expectedSbom,resultSbom); + @ParameterizedTest + @MethodSource("testFolders") + void test_the_provideComponent(String testFolder) throws IOException, InterruptedException { + // load the pom target pom file + byte[] targetPom; + try (var is = getResourceAsStreamDecision( + this.getClass(), new String[] {"tst_manifests", "golang", testFolder, "go.mod"})) { + targetPom = is.readAllBytes(); + } + // load expected SBOM + String expectedSbom = ""; + try (var is = getResourceAsStreamDecision( + this.getClass(), + new String[] {"tst_manifests", "golang", testFolder, "expected_sbom_component_analysis.json"})) { + expectedSbom = new String(is.readAllBytes()); + } + // when providing component content for our pom + var content = new GoModulesProvider().provideComponent(targetPom); + // verify expected SBOM is returned + assertThat(content.type).isEqualTo(Api.CYCLONEDX_MEDIA_TYPE); + assertThat(dropIgnored(new String(content.buffer))).isEqualTo(dropIgnored(expectedSbom)); + } - // check that only one version of package golang/go.opencensus.io is in sbom for EXHORT_GO_MVS_LOGIC_ENABLED=true - assertTrue(Arrays.stream(resultSbom.split(System.lineSeparator())).filter(str -> str.contains("\"ref\" : \"pkg:golang/go.opencensus.io@")).count() == 1); + @Test + void Test_The_ProvideComponent_Path_Should_Throw_Exception() { - System.clearProperty("EXHORT_GO_MVS_LOGIC_ENABLED"); + GoModulesProvider goModulesProvider = new GoModulesProvider(); + assertThatIllegalArgumentException() + .isThrownBy(() -> { + goModulesProvider.provideComponent(Path.of(".")); + }) + .withMessage( + "provideComponent with file system path for GoModules package manager not implemented yet"); + } - resultSbom = dropIgnoredKeepFormat(goModulesProvider.getDependenciesSbom(Path.of(goModPath),true).getAsJsonString()); - // check that there is more than one version of package golang/go.opencensus.io in sbom for EXHORT_GO_MVS_LOGIC_ENABLED=false - assertTrue(Arrays.stream(resultSbom.split(System.lineSeparator())).filter(str -> str.contains("\"ref\" : \"pkg:golang/go.opencensus.io@")).count() > 1); + @ParameterizedTest + @ValueSource(booleans = {true, false}) + void Test_Golang_Modules_with_Match_Manifest_Version(boolean MatchManifestVersionsEnabled) { + String goModPath = getFileFromResource("go.mod", "msc", "golang", "go.mod"); + GoModulesProvider goModulesProvider = new GoModulesProvider(); + + if (MatchManifestVersionsEnabled) { + System.setProperty("MATCH_MANIFEST_VERSIONS", "true"); + RuntimeException runtimeException = assertThrows( + RuntimeException.class, + () -> goModulesProvider.getDependenciesSbom(Path.of(goModPath), true), + "Expected getDependenciesSbom/2 to throw RuntimeException, due to version mismatch, but it didn't."); + assertTrue( + runtimeException + .getMessage() + .contains( + "Can't continue with analysis - versions mismatch for dependency name=github.com/google/uuid, manifest version=v1.1.0, installed Version=v1.1.1")); + System.clearProperty("MATCH_MANIFEST_VERSIONS"); + } else { + String sbomString = assertDoesNotThrow(() -> goModulesProvider + .getDependenciesSbom(Path.of(goModPath), false) + .getAsJsonString()); + String actualSbomWithTSStripped = dropIgnoredKeepFormat(sbomString); + assertEquals( + getStringFromFile("msc", "golang", "expected_sbom_ca.json").trim(), actualSbomWithTSStripped); + + System.out.println(sbomString); + } + } - } + @Test + void Test_Golang_MvS_Logic_Enabled() throws IOException { + ObjectMapper om = new ObjectMapper(); + System.setProperty("EXHORT_GO_MVS_LOGIC_ENABLED", "true"); + String goModPath = getFileFromResource("go.mod", "msc", "golang", "mvs_logic", "go.mod"); + GoModulesProvider goModulesProvider = new GoModulesProvider(); + String resultSbom = dropIgnoredKeepFormat( + goModulesProvider.getDependenciesSbom(Path.of(goModPath), true).getAsJsonString()); + String expectedSbom = getStringFromFile("msc", "golang", "mvs_logic", "expected_sbom_stack_analysis.json") + .trim(); + + assertEquals(expectedSbom, resultSbom); + + // check that only one version of package golang/go.opencensus.io is in sbom for + // EXHORT_GO_MVS_LOGIC_ENABLED=true + assertTrue(Arrays.stream(resultSbom.split(System.lineSeparator())) + .filter(str -> str.contains("\"ref\" : \"pkg:golang/go.opencensus.io@")) + .count() + == 1); + + System.clearProperty("EXHORT_GO_MVS_LOGIC_ENABLED"); + + resultSbom = dropIgnoredKeepFormat( + goModulesProvider.getDependenciesSbom(Path.of(goModPath), true).getAsJsonString()); + // check that there is more than one version of package golang/go.opencensus.io in sbom for + // EXHORT_GO_MVS_LOGIC_ENABLED=false + assertTrue(Arrays.stream(resultSbom.split(System.lineSeparator())) + .filter(str -> str.contains("\"ref\" : \"pkg:golang/go.opencensus.io@")) + .count() + > 1); + } + private String dropIgnored(String s) { + return s.replaceAll("\\s+", "").replaceAll("\"timestamp\":\"[a-zA-Z0-9\\-\\:]+\",", ""); + } - private String dropIgnored(String s) { - return s.replaceAll("\\s+","").replaceAll("\"timestamp\":\"[a-zA-Z0-9\\-\\:]+\",", ""); - } private String dropIgnoredKeepFormat(String s) { - return s.replaceAll("\"timestamp\" : \"[a-zA-Z0-9\\-\\:]+\",\n ", ""); - } - + return s.replaceAll("\"timestamp\" : \"[a-zA-Z0-9\\-\\:]+\",\n ", ""); + } } diff --git a/src/test/java/com/redhat/exhort/providers/Gradle_Provider_Test.java b/src/test/java/com/redhat/exhort/providers/Gradle_Provider_Test.java index 595d33ed..36d2f6d7 100644 --- a/src/test/java/com/redhat/exhort/providers/Gradle_Provider_Test.java +++ b/src/test/java/com/redhat/exhort/providers/Gradle_Provider_Test.java @@ -15,169 +15,201 @@ */ package com.redhat.exhort.providers; +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.argThat; +import static org.mockito.Mockito.mockStatic; + import com.redhat.exhort.Api; import com.redhat.exhort.ExhortTest; import com.redhat.exhort.tools.Operations; +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.stream.Stream; import org.junit.jupiter.api.extension.ExtendWith; import org.junit.jupiter.params.ParameterizedTest; import org.junit.jupiter.params.provider.MethodSource; import org.mockito.ArgumentMatcher; import org.mockito.MockedStatic; -import org.mockito.Mockito; -import org.mockito.invocation.InvocationOnMock; import org.mockito.junit.jupiter.MockitoExtension; -import java.io.IOException; -import java.nio.file.Files; -import java.nio.file.Path; -import java.util.Arrays; -import java.util.Optional; -import java.util.stream.Stream; - -import static org.assertj.core.api.Assertions.assertThat; -import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException; -import static org.mockito.ArgumentMatchers.any; -import static org.mockito.ArgumentMatchers.argThat; -import static org.mockito.Mockito.mockStatic; - @ExtendWith(HelperExtension.class) @ExtendWith(MockitoExtension.class) class Gradle_Provider_Test extends ExhortTest { -// private static System.Logger log = System.getLogger("Gradle_Provider_Test"); - // test folder are located at src/test/resources/tst_manifests - // each folder should contain: - // - build.gradle: the target manifest for testing - // - expected_sbom.json: the SBOM expected to be provided - static Stream testFolders() { - return Stream.of( - "deps_with_ignore_full_specification", - "deps_with_ignore_named_params", - "deps_with_ignore_notations", - "deps_with_no_ignore_common_paths" - ); - } - - @ParameterizedTest - @MethodSource("testFolders") - void test_the_provideStack(String testFolder) throws IOException, InterruptedException { - // create temp file hosting our sut build.gradle - var tmpGradleDir = Files.createTempDirectory("exhort_test_"); - var tmpGradleFile = Files.createFile(tmpGradleDir.resolve("build.gradle")); -// log.log(System.Logger.Level.INFO,"the test folder is : " + testFolder); - try (var is = getClass().getClassLoader().getResourceAsStream(String.join("/","tst_manifests", "gradle", testFolder, "build.gradle"))) { - Files.write(tmpGradleFile, is.readAllBytes()); - } - var settingsFile = Files.createFile(tmpGradleDir.resolve("settings.gradle")); - try (var is = getClass().getClassLoader().getResourceAsStream(String.join("/","tst_manifests", "gradle", testFolder, "settings.gradle"))) { - Files.write(settingsFile, is.readAllBytes()); - } - var subGradleDir = Files.createDirectories(tmpGradleDir.resolve("gradle")); - var libsVersionFile = Files.createFile(subGradleDir.resolve("libs.versions.toml")); - try (var is = getClass().getClassLoader().getResourceAsStream(String.join("/","tst_manifests", "gradle", testFolder, "gradle", "libs.versions.toml"))) { - Files.write(libsVersionFile, is.readAllBytes()); - } - // load expected SBOM - String expectedSbom; - try (var is = getClass().getClassLoader().getResourceAsStream(String.join("/","tst_manifests", "gradle", testFolder, "expected_stack_sbom.json"))) { - expectedSbom = new String(is.readAllBytes()); - } - String depTree; - try (var is = getClass().getClassLoader().getResourceAsStream(String.join("/","tst_manifests", "gradle", testFolder, "depTree.txt"))) { - depTree = new String(is.readAllBytes()); - } - String gradleProperties; - try (var is = getClass().getClassLoader().getResourceAsStream(String.join("/","tst_manifests", "gradle", testFolder, "gradle.properties"))) { - gradleProperties = new String(is.readAllBytes()); + // private static System.Logger log = System.getLogger("Gradle_Provider_Test"); + // test folder are located at src/test/resources/tst_manifests + // each folder should contain: + // - build.gradle: the target manifest for testing + // - expected_sbom.json: the SBOM expected to be provided + static Stream testFolders() { + return Stream.of( + "deps_with_ignore_full_specification", + "deps_with_ignore_named_params", + "deps_with_ignore_notations", + "deps_with_no_ignore_common_paths"); } - MockedStatic mockedOperations = mockStatic(Operations.class); - ArgumentMatcher gradle = string -> string.equals("gradle"); - ArgumentMatcher dependencies = string -> string.equals("dependencies"); - ArgumentMatcher properties = string -> string.equals("properties"); - mockedOperations.when(() -> Operations.getCustomPathOrElse("gradle")).thenReturn("gradle"); - mockedOperations.when(() -> Operations.runProcessGetOutput(any(Path.class), argThat(gradle),argThat(dependencies))).thenReturn(depTree); - mockedOperations.when(() -> Operations.runProcessGetOutput(any(Path.class), argThat(gradle), argThat(properties))).thenReturn(gradleProperties); + @ParameterizedTest + @MethodSource("testFolders") + void test_the_provideStack(String testFolder) throws IOException, InterruptedException { + // create temp file hosting our sut build.gradle + var tmpGradleDir = Files.createTempDirectory("exhort_test_"); + var tmpGradleFile = Files.createFile(tmpGradleDir.resolve("build.gradle")); + // log.log(System.Logger.Level.INFO,"the test folder is : " + testFolder); + try (var is = getClass() + .getClassLoader() + .getResourceAsStream(String.join("/", "tst_manifests", "gradle", testFolder, "build.gradle"))) { + Files.write(tmpGradleFile, is.readAllBytes()); + } + var settingsFile = Files.createFile(tmpGradleDir.resolve("settings.gradle")); + try (var is = getClass() + .getClassLoader() + .getResourceAsStream(String.join("/", "tst_manifests", "gradle", testFolder, "settings.gradle"))) { + Files.write(settingsFile, is.readAllBytes()); + } + var subGradleDir = Files.createDirectories(tmpGradleDir.resolve("gradle")); + var libsVersionFile = Files.createFile(subGradleDir.resolve("libs.versions.toml")); + try (var is = getClass() + .getClassLoader() + .getResourceAsStream( + String.join("/", "tst_manifests", "gradle", testFolder, "gradle", "libs.versions.toml"))) { + Files.write(libsVersionFile, is.readAllBytes()); + } + // load expected SBOM + String expectedSbom; + try (var is = getClass() + .getClassLoader() + .getResourceAsStream( + String.join("/", "tst_manifests", "gradle", testFolder, "expected_stack_sbom.json"))) { + expectedSbom = new String(is.readAllBytes()); + } + String depTree; + try (var is = getClass() + .getClassLoader() + .getResourceAsStream(String.join("/", "tst_manifests", "gradle", testFolder, "depTree.txt"))) { + depTree = new String(is.readAllBytes()); + } + String gradleProperties; + try (var is = getClass() + .getClassLoader() + .getResourceAsStream(String.join("/", "tst_manifests", "gradle", testFolder, "gradle.properties"))) { + gradleProperties = new String(is.readAllBytes()); + } - // when providing stack content for our pom - var content = new GradleProvider().provideStack(tmpGradleFile); - // cleanup - Files.deleteIfExists(tmpGradleFile); - // verify expected SBOM is returned - mockedOperations.close(); - assertThat(content.type).isEqualTo(Api.CYCLONEDX_MEDIA_TYPE); - assertThat(dropIgnored(new String(content.buffer))) - .isEqualTo(dropIgnored(expectedSbom)); + MockedStatic mockedOperations = mockStatic(Operations.class); + ArgumentMatcher gradle = string -> string.equals("gradle"); + ArgumentMatcher dependencies = string -> string.equals("dependencies"); + ArgumentMatcher properties = string -> string.equals("properties"); + mockedOperations.when(() -> Operations.getCustomPathOrElse("gradle")).thenReturn("gradle"); + mockedOperations + .when(() -> Operations.runProcessGetOutput(any(Path.class), argThat(gradle), argThat(dependencies))) + .thenReturn(depTree); + mockedOperations + .when(() -> Operations.runProcessGetOutput(any(Path.class), argThat(gradle), argThat(properties))) + .thenReturn(gradleProperties); - } - - @ParameterizedTest - @MethodSource("testFolders") - void test_the_provideComponent(String testFolder) throws IOException, InterruptedException { - // load the pom target pom file - byte[] targetGradleBuild; - try (var is = getClass().getClassLoader().getResourceAsStream(String.join("/","tst_manifests", "gradle", testFolder, "build.gradle"))) { - targetGradleBuild = is.readAllBytes(); + // when providing stack content for our pom + var content = new GradleProvider().provideStack(tmpGradleFile); + // cleanup + Files.deleteIfExists(tmpGradleFile); + // verify expected SBOM is returned + mockedOperations.close(); + assertThat(content.type).isEqualTo(Api.CYCLONEDX_MEDIA_TYPE); + assertThat(dropIgnored(new String(content.buffer))).isEqualTo(dropIgnored(expectedSbom)); } - GradleProvider gradleProvider = new GradleProvider(); - assertThatIllegalArgumentException().isThrownBy(() -> { - gradleProvider.provideComponent(targetGradleBuild); - }).withMessage("Gradle Package Manager requires the full package directory, not just the manifest content, to generate the dependency tree. Please provide the complete package directory path."); + @ParameterizedTest + @MethodSource("testFolders") + void test_the_provideComponent(String testFolder) throws IOException, InterruptedException { + // load the pom target pom file + byte[] targetGradleBuild; + try (var is = getClass() + .getClassLoader() + .getResourceAsStream(String.join("/", "tst_manifests", "gradle", testFolder, "build.gradle"))) { + targetGradleBuild = is.readAllBytes(); + } - } - @ParameterizedTest - @MethodSource("testFolders") - void test_the_provideComponent_With_Path(String testFolder) throws IOException, InterruptedException { - // create temp file hosting our sut build.gradle - var tmpGradleDir = Files.createTempDirectory("exhort_test_"); - var tmpGradleFile = Files.createFile(tmpGradleDir.resolve("build.gradle")); -// log.log(System.Logger.Level.INFO,"the test folder is : " + testFolder); - try (var is = getClass().getClassLoader().getResourceAsStream(String.join("/","tst_manifests", "gradle", testFolder, "build.gradle"))) { - Files.write(tmpGradleFile, is.readAllBytes()); - } - var settingsFile = Files.createFile(tmpGradleDir.resolve("settings.gradle")); - try (var is = getClass().getClassLoader().getResourceAsStream(String.join("/","tst_manifests", "gradle", testFolder, "settings.gradle"))) { - Files.write(settingsFile, is.readAllBytes()); - } - var subGradleDir = Files.createDirectories(tmpGradleDir.resolve("gradle")); - var libsVersionFile = Files.createFile(subGradleDir.resolve("libs.versions.toml")); - try (var is = getClass().getClassLoader().getResourceAsStream(String.join("/","tst_manifests", "gradle", testFolder, "gradle", "libs.versions.toml"))) { - Files.write(libsVersionFile, is.readAllBytes()); - } - // load expected SBOM - String expectedSbom; - try (var is = getClass().getClassLoader().getResourceAsStream(String.join("/","tst_manifests", "gradle", testFolder, "expected_component_sbom.json"))) { - expectedSbom = new String(is.readAllBytes()); - } - String depTree; - try (var is = getClass().getClassLoader().getResourceAsStream(String.join("/","tst_manifests", "gradle", testFolder, "depTree.txt"))) { - depTree = new String(is.readAllBytes()); - } - String gradleProperties; - try (var is = getClass().getClassLoader().getResourceAsStream(String.join("/","tst_manifests", "gradle", testFolder, "gradle.properties"))) { - gradleProperties = new String(is.readAllBytes()); + GradleProvider gradleProvider = new GradleProvider(); + assertThatIllegalArgumentException() + .isThrownBy(() -> { + gradleProvider.provideComponent(targetGradleBuild); + }) + .withMessage( + "Gradle Package Manager requires the full package directory, not just the manifest content, to generate the dependency tree. Please provide the complete package directory path."); } - MockedStatic mockedOperations = mockStatic(Operations.class); - ArgumentMatcher gradle = string -> string.equals("gradle"); - ArgumentMatcher dependencies = string -> string.equals("dependencies"); - ArgumentMatcher properties = string -> string.equals("properties"); - mockedOperations.when(() -> Operations.getCustomPathOrElse("gradle")).thenReturn("gradle"); - mockedOperations.when(() -> Operations.runProcessGetOutput(any(Path.class), argThat(gradle),argThat(dependencies))).thenReturn(depTree); - mockedOperations.when(() -> Operations.runProcessGetOutput(any(Path.class), argThat(gradle), argThat(properties))).thenReturn(gradleProperties); + @ParameterizedTest + @MethodSource("testFolders") + void test_the_provideComponent_With_Path(String testFolder) throws IOException, InterruptedException { + // create temp file hosting our sut build.gradle + var tmpGradleDir = Files.createTempDirectory("exhort_test_"); + var tmpGradleFile = Files.createFile(tmpGradleDir.resolve("build.gradle")); + // log.log(System.Logger.Level.INFO,"the test folder is : " + testFolder); + try (var is = getClass() + .getClassLoader() + .getResourceAsStream(String.join("/", "tst_manifests", "gradle", testFolder, "build.gradle"))) { + Files.write(tmpGradleFile, is.readAllBytes()); + } + var settingsFile = Files.createFile(tmpGradleDir.resolve("settings.gradle")); + try (var is = getClass() + .getClassLoader() + .getResourceAsStream(String.join("/", "tst_manifests", "gradle", testFolder, "settings.gradle"))) { + Files.write(settingsFile, is.readAllBytes()); + } + var subGradleDir = Files.createDirectories(tmpGradleDir.resolve("gradle")); + var libsVersionFile = Files.createFile(subGradleDir.resolve("libs.versions.toml")); + try (var is = getClass() + .getClassLoader() + .getResourceAsStream( + String.join("/", "tst_manifests", "gradle", testFolder, "gradle", "libs.versions.toml"))) { + Files.write(libsVersionFile, is.readAllBytes()); + } + // load expected SBOM + String expectedSbom; + try (var is = getClass() + .getClassLoader() + .getResourceAsStream( + String.join("/", "tst_manifests", "gradle", testFolder, "expected_component_sbom.json"))) { + expectedSbom = new String(is.readAllBytes()); + } + String depTree; + try (var is = getClass() + .getClassLoader() + .getResourceAsStream(String.join("/", "tst_manifests", "gradle", testFolder, "depTree.txt"))) { + depTree = new String(is.readAllBytes()); + } + String gradleProperties; + try (var is = getClass() + .getClassLoader() + .getResourceAsStream(String.join("/", "tst_manifests", "gradle", testFolder, "gradle.properties"))) { + gradleProperties = new String(is.readAllBytes()); + } + + MockedStatic mockedOperations = mockStatic(Operations.class); + ArgumentMatcher gradle = string -> string.equals("gradle"); + ArgumentMatcher dependencies = string -> string.equals("dependencies"); + ArgumentMatcher properties = string -> string.equals("properties"); + mockedOperations.when(() -> Operations.getCustomPathOrElse("gradle")).thenReturn("gradle"); + mockedOperations + .when(() -> Operations.runProcessGetOutput(any(Path.class), argThat(gradle), argThat(dependencies))) + .thenReturn(depTree); + mockedOperations + .when(() -> Operations.runProcessGetOutput(any(Path.class), argThat(gradle), argThat(properties))) + .thenReturn(gradleProperties); - // when providing component content for our pom - var content = new GradleProvider().provideComponent(tmpGradleFile); - // cleanup - Files.deleteIfExists(tmpGradleFile); - // verify expected SBOM is returned - mockedOperations.close(); - assertThat(content.type).isEqualTo(Api.CYCLONEDX_MEDIA_TYPE); - assertThat(dropIgnored(new String(content.buffer))) - .isEqualTo(dropIgnored(expectedSbom)); - } + // when providing component content for our pom + var content = new GradleProvider().provideComponent(tmpGradleFile); + // cleanup + Files.deleteIfExists(tmpGradleFile); + // verify expected SBOM is returned + mockedOperations.close(); + assertThat(content.type).isEqualTo(Api.CYCLONEDX_MEDIA_TYPE); + assertThat(dropIgnored(new String(content.buffer))).isEqualTo(dropIgnored(expectedSbom)); + } - private String dropIgnored(String s) { - return s.replaceAll("\\s+","").replaceAll("\"timestamp\":\"[a-zA-Z0-9\\-\\:]+\",", ""); - } + private String dropIgnored(String s) { + return s.replaceAll("\\s+", "").replaceAll("\"timestamp\":\"[a-zA-Z0-9\\-\\:]+\",", ""); + } } diff --git a/src/test/java/com/redhat/exhort/providers/HelperExtension.java b/src/test/java/com/redhat/exhort/providers/HelperExtension.java index a33de94d..578fb8d7 100644 --- a/src/test/java/com/redhat/exhort/providers/HelperExtension.java +++ b/src/test/java/com/redhat/exhort/providers/HelperExtension.java @@ -15,46 +15,45 @@ */ package com.redhat.exhort.providers; -import com.redhat.exhort.tools.Operations; -import com.redhat.exhort.utils.PythonControllerBase; -import com.redhat.exhort.utils.PythonControllerTestEnv; -import org.junit.jupiter.api.extension.*; - -import java.io.IOException; -import java.nio.file.Files; import java.util.List; - +import org.junit.jupiter.api.extension.*; public class HelperExtension implements BeforeAllCallback, AfterAllCallback, BeforeEachCallback, AfterEachCallback { - private System.Logger log = System.getLogger(this.getClass().getName()); - -// public PythonEnvironmentExtension(List requirementsFiles) { -// this.requirementsFiles = requirementsFiles; -// } - - private List requirementsFiles; - @Override - public void afterAll(ExtensionContext extensionContext) throws Exception { - log.log(System.Logger.Level.INFO,"Finished all tests!!"); - - } - - @Override - public void afterEach(ExtensionContext extensionContext) throws Exception { - log.log(System.Logger.Level.INFO,String.format("Finished Test Method: %s_%s", extensionContext.getRequiredTestMethod().getName(), extensionContext.getDisplayName())); - } - - @Override - public void beforeAll(ExtensionContext extensionContext) throws Exception { - - log.log(System.Logger.Level.INFO,"Before all tests"); - } - - @Override - public void beforeEach(ExtensionContext extensionContext) throws Exception { - log.log(System.Logger.Level.INFO,String.format("Started Test Method: %s_%s", extensionContext.getRequiredTestMethod().getName(), extensionContext.getDisplayName())); - } - - + private System.Logger log = System.getLogger(this.getClass().getName()); + + // public PythonEnvironmentExtension(List requirementsFiles) { + // this.requirementsFiles = requirementsFiles; + // } + + private List requirementsFiles; + + @Override + public void afterAll(ExtensionContext extensionContext) throws Exception { + log.log(System.Logger.Level.INFO, "Finished all tests!!"); + } + + @Override + public void afterEach(ExtensionContext extensionContext) throws Exception { + log.log( + System.Logger.Level.INFO, + String.format( + "Finished Test Method: %s_%s", + extensionContext.getRequiredTestMethod().getName(), extensionContext.getDisplayName())); + } + + @Override + public void beforeAll(ExtensionContext extensionContext) throws Exception { + + log.log(System.Logger.Level.INFO, "Before all tests"); + } + + @Override + public void beforeEach(ExtensionContext extensionContext) throws Exception { + log.log( + System.Logger.Level.INFO, + String.format( + "Started Test Method: %s_%s", + extensionContext.getRequiredTestMethod().getName(), extensionContext.getDisplayName())); + } } diff --git a/src/test/java/com/redhat/exhort/providers/Java_Envs_Test.java b/src/test/java/com/redhat/exhort/providers/Java_Envs_Test.java index 1fb06664..de3c7f22 100644 --- a/src/test/java/com/redhat/exhort/providers/Java_Envs_Test.java +++ b/src/test/java/com/redhat/exhort/providers/Java_Envs_Test.java @@ -15,35 +15,34 @@ */ package com.redhat.exhort.providers; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNull; + +import java.util.Collections; import org.junit.jupiter.api.Test; import org.junitpioneer.jupiter.ClearEnvironmentVariable; import org.junitpioneer.jupiter.SetEnvironmentVariable; -import java.util.Collections; - -import static org.junit.jupiter.api.Assertions.assertEquals; -import static org.junit.jupiter.api.Assertions.assertNull; - public class Java_Envs_Test { - @Test - @SetEnvironmentVariable(key = "JAVA_HOME", value = "test-java-home") - void test_java_get_envs() { - var envs = new JavaMavenProvider().getMvnExecEnvs(); - assertEquals(Collections.singletonMap("JAVA_HOME", "test-java-home"), envs); - } + @Test + @SetEnvironmentVariable(key = "JAVA_HOME", value = "test-java-home") + void test_java_get_envs() { + var envs = new JavaMavenProvider().getMvnExecEnvs(); + assertEquals(Collections.singletonMap("JAVA_HOME", "test-java-home"), envs); + } - @Test - @SetEnvironmentVariable(key = "JAVA_HOME", value = "") - void test_java_get_envs_empty_java_home() { - var envs = new JavaMavenProvider().getMvnExecEnvs(); - assertNull(envs); - } + @Test + @SetEnvironmentVariable(key = "JAVA_HOME", value = "") + void test_java_get_envs_empty_java_home() { + var envs = new JavaMavenProvider().getMvnExecEnvs(); + assertNull(envs); + } - @Test - @ClearEnvironmentVariable(key = "JAVA_HOME") - void test_java_get_envs_no_java_home() { - var envs = new JavaMavenProvider().getMvnExecEnvs(); - assertNull(envs); - } + @Test + @ClearEnvironmentVariable(key = "JAVA_HOME") + void test_java_get_envs_no_java_home() { + var envs = new JavaMavenProvider().getMvnExecEnvs(); + assertNull(envs); + } } diff --git a/src/test/java/com/redhat/exhort/providers/Java_Maven_Provider_Test.java b/src/test/java/com/redhat/exhort/providers/Java_Maven_Provider_Test.java index 2fbde9b0..6b20b650 100644 --- a/src/test/java/com/redhat/exhort/providers/Java_Maven_Provider_Test.java +++ b/src/test/java/com/redhat/exhort/providers/Java_Maven_Provider_Test.java @@ -16,25 +16,21 @@ package com.redhat.exhort.providers; import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.mockStatic; +import com.redhat.exhort.Api; +import com.redhat.exhort.ExhortTest; +import com.redhat.exhort.tools.Operations; import java.io.IOException; import java.nio.file.Files; import java.nio.file.Path; import java.util.Arrays; import java.util.Optional; import java.util.stream.Stream; - -import com.redhat.exhort.ExhortTest; -import com.redhat.exhort.tools.Operations; import org.junit.jupiter.api.extension.ExtendWith; import org.junit.jupiter.params.ParameterizedTest; import org.junit.jupiter.params.provider.MethodSource; - -import static org.mockito.ArgumentMatchers.any; -import static org.mockito.Mockito.mockStatic; - - -import com.redhat.exhort.Api; import org.mockito.MockedStatic; import org.mockito.invocation.InvocationOnMock; import org.mockito.junit.jupiter.MockitoExtension; @@ -43,147 +39,148 @@ @ExtendWith(MockitoExtension.class) public class Java_Maven_Provider_Test extends ExhortTest { - -// private static System.Logger log = System.getLogger("Java_Maven_Provider_Test"); - // test folder are located at src/test/resources/tst_manifests - // each folder should contain: - // - pom.xml: the target manifest for testing - // - expected_sbom.json: the SBOM expected to be provided - static Stream testFolders() { - return Stream.of( - "pom_deps_with_no_ignore_provided_scope", - "deps_no_trivial_with_ignore", - "deps_with_ignore_on_artifact", - "deps_with_ignore_on_dependency", - "deps_with_ignore_on_group", - "deps_with_ignore_on_version", - "deps_with_ignore_on_wrong", - "deps_with_no_ignore", - "pom_deps_with_no_ignore_common_paths" - - - ); - } - - @ParameterizedTest - @MethodSource("testFolders") - void test_the_provideStack(String testFolder) throws IOException, InterruptedException { - // create temp file hosting our sut pom.xml - var tmpPomFile = Files.createTempFile("exhort_test_", ".xml"); -// log.log(System.Logger.Level.INFO,"the test folder is : " + testFolder); - try (var is = getResourceAsStreamDecision(getClass(), new String []{ "tst_manifests", "maven", testFolder, "pom.xml"})) { - Files.write(tmpPomFile, is.readAllBytes()); - } - // load expected SBOM - String expectedSbom; - try (var is = getResourceAsStreamDecision(getClass(), new String [] { "tst_manifests", "maven", testFolder, "expected_stack_sbom.json"})) { - expectedSbom = new String(is.readAllBytes()); - } - String depTree; - try (var is = getResourceAsStreamDecision(getClass(), new String [] { "tst_manifests", "maven", testFolder, "depTree.txt"})) { - depTree = new String(is.readAllBytes()); + // private static System.Logger log = System.getLogger("Java_Maven_Provider_Test"); + // test folder are located at src/test/resources/tst_manifests + // each folder should contain: + // - pom.xml: the target manifest for testing + // - expected_sbom.json: the SBOM expected to be provided + static Stream testFolders() { + return Stream.of( + "pom_deps_with_no_ignore_provided_scope", + "deps_no_trivial_with_ignore", + "deps_with_ignore_on_artifact", + "deps_with_ignore_on_dependency", + "deps_with_ignore_on_group", + "deps_with_ignore_on_version", + "deps_with_ignore_on_wrong", + "deps_with_no_ignore", + "pom_deps_with_no_ignore_common_paths"); } - MockedStatic mockedOperations = mockStatic(Operations.class); - mockedOperations.when(() -> Operations.runProcess(any(),any())).thenAnswer(invocationOnMock -> { - return getOutputFileAndOverwriteItWithMock(depTree, invocationOnMock,"-DoutputFile"); - }); - - - // when providing stack content for our pom - var content = new JavaMavenProvider().provideStack(tmpPomFile); - // cleanup - Files.deleteIfExists(tmpPomFile); - // verify expected SBOM is returned - mockedOperations.close(); - assertThat(content.type).isEqualTo(Api.CYCLONEDX_MEDIA_TYPE); - assertThat(dropIgnored(new String(content.buffer))) - .isEqualTo(dropIgnored(expectedSbom)); - - } - - public static String getOutputFileAndOverwriteItWithMock(String outputFileContent, InvocationOnMock invocationOnMock,String parameterPrefix) throws IOException { - String[] rawArguments = (String[]) invocationOnMock.getRawArguments()[0]; - Optional outputFileArg = Arrays.stream(rawArguments).filter(arg -> arg!= null && arg.startsWith(parameterPrefix)).findFirst(); - String outputFilePath=null; - if(outputFileArg.isPresent()) - { - String outputFile = outputFileArg.get(); - outputFilePath = outputFile.substring(outputFile.indexOf("=") + 1); - Files.writeString(Path.of(outputFilePath), outputFileContent); - } - return outputFilePath; - } - - @ParameterizedTest - @MethodSource("testFolders") - void test_the_provideComponent(String testFolder) throws IOException, InterruptedException { - // load the pom target pom file - byte[] targetPom; - try (var is = getResourceAsStreamDecision(getClass(), new String [] { "tst_manifests", "maven", testFolder, "pom.xml"})) { - targetPom = is.readAllBytes(); - } - // load expected SBOM - String expectedSbom = ""; - try (var is = getResourceAsStreamDecision(getClass(), new String [] { "tst_manifests", "maven", testFolder, "expected_component_sbom.json"})) { - expectedSbom = new String(is.readAllBytes()); + @ParameterizedTest + @MethodSource("testFolders") + void test_the_provideStack(String testFolder) throws IOException, InterruptedException { + // create temp file hosting our sut pom.xml + var tmpPomFile = Files.createTempFile("exhort_test_", ".xml"); + // log.log(System.Logger.Level.INFO,"the test folder is : " + testFolder); + try (var is = getResourceAsStreamDecision( + getClass(), new String[] {"tst_manifests", "maven", testFolder, "pom.xml"})) { + Files.write(tmpPomFile, is.readAllBytes()); + } + // load expected SBOM + String expectedSbom; + try (var is = getResourceAsStreamDecision( + getClass(), new String[] {"tst_manifests", "maven", testFolder, "expected_stack_sbom.json"})) { + expectedSbom = new String(is.readAllBytes()); + } + String depTree; + try (var is = getResourceAsStreamDecision( + getClass(), new String[] {"tst_manifests", "maven", testFolder, "depTree.txt"})) { + depTree = new String(is.readAllBytes()); + } + + MockedStatic mockedOperations = mockStatic(Operations.class); + mockedOperations.when(() -> Operations.runProcess(any(), any())).thenAnswer(invocationOnMock -> { + return getOutputFileAndOverwriteItWithMock(depTree, invocationOnMock, "-DoutputFile"); + }); + + // when providing stack content for our pom + var content = new JavaMavenProvider().provideStack(tmpPomFile); + // cleanup + Files.deleteIfExists(tmpPomFile); + // verify expected SBOM is returned + mockedOperations.close(); + assertThat(content.type).isEqualTo(Api.CYCLONEDX_MEDIA_TYPE); + assertThat(dropIgnored(new String(content.buffer))).isEqualTo(dropIgnored(expectedSbom)); } - String effectivePom; - try (var is = getResourceAsStreamDecision(getClass(), new String [] { "tst_manifests", "maven", testFolder, "effectivePom.xml"})) { - effectivePom = new String(is.readAllBytes()); + public static String getOutputFileAndOverwriteItWithMock( + String outputFileContent, InvocationOnMock invocationOnMock, String parameterPrefix) throws IOException { + String[] rawArguments = (String[]) invocationOnMock.getRawArguments()[0]; + Optional outputFileArg = Arrays.stream(rawArguments) + .filter(arg -> arg != null && arg.startsWith(parameterPrefix)) + .findFirst(); + String outputFilePath = null; + if (outputFileArg.isPresent()) { + String outputFile = outputFileArg.get(); + outputFilePath = outputFile.substring(outputFile.indexOf("=") + 1); + Files.writeString(Path.of(outputFilePath), outputFileContent); + } + return outputFilePath; } - MockedStatic mockedOperations = mockStatic(Operations.class); - mockedOperations.when(() -> Operations.runProcess(any(),any())).thenAnswer(invocationOnMock -> { - return getOutputFileAndOverwriteItWithMock(effectivePom, invocationOnMock,"-Doutput"); - }); - - // when providing component content for our pom - var content = new JavaMavenProvider().provideComponent(targetPom); - mockedOperations.close(); - // verify expected SBOM is returned - assertThat(content.type).isEqualTo(Api.CYCLONEDX_MEDIA_TYPE); - assertThat(dropIgnored(new String(content.buffer))) - .isEqualTo(dropIgnored(expectedSbom)); - - } - @ParameterizedTest - @MethodSource("testFolders") - void test_the_provideComponent_With_Path(String testFolder) throws IOException, InterruptedException { - // load the pom target pom file - // create temp file hosting our sut pom.xml - var tmpPomFile = Files.createTempFile("exhort_test_", ".xml"); - try (var is = getResourceAsStreamDecision(getClass(),new String [] { "tst_manifests", "maven", testFolder, "pom.xml"})) { - Files.write(tmpPomFile, is.readAllBytes()); - } - // load expected SBOM - String expectedSbom = ""; - try (var is = getResourceAsStreamDecision(getClass(), new String [] { "tst_manifests", "maven", testFolder, "expected_component_sbom.json"})) { - expectedSbom = new String(is.readAllBytes()); + @ParameterizedTest + @MethodSource("testFolders") + void test_the_provideComponent(String testFolder) throws IOException, InterruptedException { + // load the pom target pom file + byte[] targetPom; + try (var is = getResourceAsStreamDecision( + getClass(), new String[] {"tst_manifests", "maven", testFolder, "pom.xml"})) { + targetPom = is.readAllBytes(); + } + // load expected SBOM + String expectedSbom = ""; + try (var is = getResourceAsStreamDecision( + getClass(), new String[] {"tst_manifests", "maven", testFolder, "expected_component_sbom.json"})) { + expectedSbom = new String(is.readAllBytes()); + } + + String effectivePom; + try (var is = getResourceAsStreamDecision( + getClass(), new String[] {"tst_manifests", "maven", testFolder, "effectivePom.xml"})) { + effectivePom = new String(is.readAllBytes()); + } + + MockedStatic mockedOperations = mockStatic(Operations.class); + mockedOperations.when(() -> Operations.runProcess(any(), any())).thenAnswer(invocationOnMock -> { + return getOutputFileAndOverwriteItWithMock(effectivePom, invocationOnMock, "-Doutput"); + }); + + // when providing component content for our pom + var content = new JavaMavenProvider().provideComponent(targetPom); + mockedOperations.close(); + // verify expected SBOM is returned + assertThat(content.type).isEqualTo(Api.CYCLONEDX_MEDIA_TYPE); + assertThat(dropIgnored(new String(content.buffer))).isEqualTo(dropIgnored(expectedSbom)); } - String effectivePom; - try (var is = getResourceAsStreamDecision(getClass(), new String [] { "tst_manifests", "maven", testFolder, "effectivePom.xml"})) { - effectivePom = new String(is.readAllBytes()); + @ParameterizedTest + @MethodSource("testFolders") + void test_the_provideComponent_With_Path(String testFolder) throws IOException, InterruptedException { + // load the pom target pom file + // create temp file hosting our sut pom.xml + var tmpPomFile = Files.createTempFile("exhort_test_", ".xml"); + try (var is = getResourceAsStreamDecision( + getClass(), new String[] {"tst_manifests", "maven", testFolder, "pom.xml"})) { + Files.write(tmpPomFile, is.readAllBytes()); + } + // load expected SBOM + String expectedSbom = ""; + try (var is = getResourceAsStreamDecision( + getClass(), new String[] {"tst_manifests", "maven", testFolder, "expected_component_sbom.json"})) { + expectedSbom = new String(is.readAllBytes()); + } + + String effectivePom; + try (var is = getResourceAsStreamDecision( + getClass(), new String[] {"tst_manifests", "maven", testFolder, "effectivePom.xml"})) { + effectivePom = new String(is.readAllBytes()); + } + + MockedStatic mockedOperations = mockStatic(Operations.class); + mockedOperations.when(() -> Operations.runProcess(any(), any())).thenAnswer(invocationOnMock -> { + return getOutputFileAndOverwriteItWithMock(effectivePom, invocationOnMock, "-Doutput"); + }); + + // when providing component content for our pom + var content = new JavaMavenProvider().provideComponent(tmpPomFile); + // verify expected SBOM is returned + mockedOperations.close(); + assertThat(content.type).isEqualTo(Api.CYCLONEDX_MEDIA_TYPE); + assertThat(dropIgnored(new String(content.buffer))).isEqualTo(dropIgnored(expectedSbom)); } - MockedStatic mockedOperations = mockStatic(Operations.class); - mockedOperations.when(() -> Operations.runProcess(any(),any())).thenAnswer(invocationOnMock -> { - return getOutputFileAndOverwriteItWithMock(effectivePom, invocationOnMock,"-Doutput"); - }); - - // when providing component content for our pom - var content = new JavaMavenProvider().provideComponent(tmpPomFile); - // verify expected SBOM is returned - mockedOperations.close(); - assertThat(content.type).isEqualTo(Api.CYCLONEDX_MEDIA_TYPE); - assertThat(dropIgnored(new String(content.buffer))) - .isEqualTo(dropIgnored(expectedSbom)); - - } - - private String dropIgnored(String s) { - return s.replaceAll("\\s+","").replaceAll("\"timestamp\":\"[a-zA-Z0-9\\-\\:]+\",", ""); - } + private String dropIgnored(String s) { + return s.replaceAll("\\s+", "").replaceAll("\"timestamp\":\"[a-zA-Z0-9\\-\\:]+\",", ""); + } } diff --git a/src/test/java/com/redhat/exhort/providers/Javascript_Envs_Test.java b/src/test/java/com/redhat/exhort/providers/Javascript_Envs_Test.java index 8c35032a..8d3645ca 100644 --- a/src/test/java/com/redhat/exhort/providers/Javascript_Envs_Test.java +++ b/src/test/java/com/redhat/exhort/providers/Javascript_Envs_Test.java @@ -15,48 +15,47 @@ */ package com.redhat.exhort.providers; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNull; + +import java.io.File; +import java.util.Collections; import org.junit.jupiter.api.Test; import org.junitpioneer.jupiter.ClearEnvironmentVariable; import org.junitpioneer.jupiter.ClearSystemProperty; import org.junitpioneer.jupiter.SetEnvironmentVariable; import org.junitpioneer.jupiter.SetSystemProperty; -import java.io.File; -import java.util.Collections; - -import static org.junit.jupiter.api.Assertions.assertEquals; -import static org.junit.jupiter.api.Assertions.assertNull; - public class Javascript_Envs_Test { - @Test - @SetSystemProperty(key = "NODE_HOME", value = "test-node-home") - @SetEnvironmentVariable(key = "PATH", value = "test-path") - void test_javascript_get_envs() { - var envs = new JavaScriptNpmProvider().getNpmExecEnv(); - assertEquals(Collections.singletonMap("PATH", "test-path" + File.pathSeparator + "test-node-home"), envs); - } + @Test + @SetSystemProperty(key = "NODE_HOME", value = "test-node-home") + @SetEnvironmentVariable(key = "PATH", value = "test-path") + void test_javascript_get_envs() { + var envs = new JavaScriptNpmProvider().getNpmExecEnv(); + assertEquals(Collections.singletonMap("PATH", "test-path" + File.pathSeparator + "test-node-home"), envs); + } - @Test - @SetSystemProperty(key = "NODE_HOME", value = "test-node-home") - @ClearEnvironmentVariable(key = "PATH") - void test_javascript_get_envs_no_path() { - var envs = new JavaScriptNpmProvider().getNpmExecEnv(); - assertEquals(Collections.singletonMap("PATH", "test-node-home"), envs); - } + @Test + @SetSystemProperty(key = "NODE_HOME", value = "test-node-home") + @ClearEnvironmentVariable(key = "PATH") + void test_javascript_get_envs_no_path() { + var envs = new JavaScriptNpmProvider().getNpmExecEnv(); + assertEquals(Collections.singletonMap("PATH", "test-node-home"), envs); + } - @Test - @SetSystemProperty(key = "NODE_HOME", value = "") - @SetEnvironmentVariable(key = "PATH", value = "test-path") - void test_javascript_get_envs_empty_java_home() { - var envs = new JavaScriptNpmProvider().getNpmExecEnv(); - assertNull(envs); - } + @Test + @SetSystemProperty(key = "NODE_HOME", value = "") + @SetEnvironmentVariable(key = "PATH", value = "test-path") + void test_javascript_get_envs_empty_java_home() { + var envs = new JavaScriptNpmProvider().getNpmExecEnv(); + assertNull(envs); + } - @Test - @ClearSystemProperty(key = "NODE_HOME") - @SetEnvironmentVariable(key = "PATH", value = "test-path") - void test_javascript_get_envs_no_java_home() { - var envs = new JavaScriptNpmProvider().getNpmExecEnv(); - assertNull(envs); - } + @Test + @ClearSystemProperty(key = "NODE_HOME") + @SetEnvironmentVariable(key = "PATH", value = "test-path") + void test_javascript_get_envs_no_java_home() { + var envs = new JavaScriptNpmProvider().getNpmExecEnv(); + assertNull(envs); + } } diff --git a/src/test/java/com/redhat/exhort/providers/Javascript_Npm_Provider_Test.java b/src/test/java/com/redhat/exhort/providers/Javascript_Npm_Provider_Test.java index f573aa2f..a5a8b4bc 100644 --- a/src/test/java/com/redhat/exhort/providers/Javascript_Npm_Provider_Test.java +++ b/src/test/java/com/redhat/exhort/providers/Javascript_Npm_Provider_Test.java @@ -19,156 +19,163 @@ import static org.mockito.ArgumentMatchers.*; import static org.mockito.Mockito.*; +import com.redhat.exhort.Api; +import com.redhat.exhort.ExhortTest; +import com.redhat.exhort.tools.Operations; import java.io.IOException; import java.nio.file.Files; import java.nio.file.Path; import java.util.stream.Stream; - -import com.redhat.exhort.ExhortTest; -import com.redhat.exhort.tools.Operations; import org.junit.jupiter.api.extension.ExtendWith; import org.junit.jupiter.params.ParameterizedTest; import org.junit.jupiter.params.provider.MethodSource; - -import com.redhat.exhort.Api; import org.mockito.*; @ExtendWith(HelperExtension.class) class Javascript_Npm_Provider_Test extends ExhortTest { - // test folder are located at src/test/resources/tst_manifests/npm - // each folder should contain: - // - package.json: the target manifest for testing - // - expected_sbom.json: the SBOM expected to be provided - static Stream testFolders() { - return Stream.of( - "deps_with_ignore", - "deps_with_no_ignore" - ); - } - - + // test folder are located at src/test/resources/tst_manifests/npm + // each folder should contain: + // - package.json: the target manifest for testing + // - expected_sbom.json: the SBOM expected to be provided + static Stream testFolders() { + return Stream.of("deps_with_ignore", "deps_with_no_ignore"); + } + @ParameterizedTest + @MethodSource("testFolders") + void test_the_provideStack(String testFolder) throws IOException, InterruptedException { + // create temp file hosting our sut package.json + var tmpNpmFolder = Files.createTempDirectory("exhort_test_"); + var tmpNpmFile = Files.createFile(tmpNpmFolder.resolve("package.json")); + var tmpLockFile = Files.createFile(tmpNpmFolder.resolve("package-lock.json")); + try (var is = getResourceAsStreamDecision( + this.getClass(), new String[] {"tst_manifests", "npm", testFolder, "package.json"})) { + Files.write(tmpNpmFile, is.readAllBytes()); + } - @ParameterizedTest - @MethodSource("testFolders") - void test_the_provideStack(String testFolder) throws IOException, InterruptedException { - // create temp file hosting our sut package.json - var tmpNpmFolder = Files.createTempDirectory("exhort_test_"); - var tmpNpmFile = Files.createFile(tmpNpmFolder.resolve("package.json")); - var tmpLockFile = Files.createFile(tmpNpmFolder.resolve("package-lock.json")); - try (var is = getResourceAsStreamDecision(this.getClass(), new String [] { "tst_manifests", "npm", testFolder, "package.json"})) { - Files.write(tmpNpmFile, is.readAllBytes()); + try (var is = getResourceAsStreamDecision( + this.getClass(), new String[] {"tst_manifests", "npm", testFolder, "package-lock.json"})) { + Files.write(tmpLockFile, is.readAllBytes()); + } + // load expected SBOM + String expectedSbom; + try (var is = getResourceAsStreamDecision( + this.getClass(), new String[] {"tst_manifests", "npm", testFolder, "expected_stack_sbom.json"})) { + expectedSbom = new String(is.readAllBytes()); + } + String npmListingStack; + try (var is = getResourceAsStreamDecision( + this.getClass(), new String[] {"tst_manifests", "npm", testFolder, "npm-ls-stack.json"})) { + npmListingStack = new String(is.readAllBytes()); + } + MockedStatic mockedOperations = mockStatic(Operations.class); + // Operations.runProcess(contains("npm i"),any()) + ArgumentMatcher matchPath = path -> path == null; + mockedOperations + .when(() -> Operations.runProcessGetOutput(argThat(matchPath), any(String[].class))) + .thenReturn(npmListingStack); + // when providing stack content for our pom + var content = new JavaScriptNpmProvider().provideStack(tmpNpmFile); + // cleanup + Files.deleteIfExists(tmpNpmFile); + Files.deleteIfExists(tmpLockFile); + Files.deleteIfExists(tmpNpmFolder); + mockedOperations.close(); + // verify expected SBOM is returned + assertThat(content.type).isEqualTo(Api.CYCLONEDX_MEDIA_TYPE); + assertThat(dropIgnored(new String(content.buffer))).isEqualTo(dropIgnored(expectedSbom)); } - try (var is = getResourceAsStreamDecision(this.getClass(), new String [] { "tst_manifests", "npm", testFolder, "package-lock.json"})) { - Files.write(tmpLockFile, is.readAllBytes()); - } - // load expected SBOM - String expectedSbom; - try (var is = getResourceAsStreamDecision(this.getClass(), new String [] { "tst_manifests", "npm", testFolder, "expected_stack_sbom.json"})) { - expectedSbom = new String(is.readAllBytes()); - } - String npmListingStack; - try (var is = getResourceAsStreamDecision(this.getClass(), new String [] { "tst_manifests", "npm", testFolder, "npm-ls-stack.json"})) { - npmListingStack = new String(is.readAllBytes()); - } - MockedStatic mockedOperations = mockStatic(Operations.class); - //Operations.runProcess(contains("npm i"),any()) - ArgumentMatcher matchPath = path -> path == null; - mockedOperations.when(() -> Operations.runProcessGetOutput(argThat(matchPath),any(String[].class))).thenReturn(npmListingStack); - // when providing stack content for our pom - var content = new JavaScriptNpmProvider().provideStack(tmpNpmFile); - // cleanup - Files.deleteIfExists(tmpNpmFile); - Files.deleteIfExists(tmpLockFile); - Files.deleteIfExists(tmpNpmFolder); - mockedOperations.close(); - // verify expected SBOM is returned - assertThat(content.type).isEqualTo(Api.CYCLONEDX_MEDIA_TYPE); - assertThat(dropIgnored(new String(content.buffer))) - .isEqualTo(dropIgnored(expectedSbom)); - } + @ParameterizedTest + @MethodSource("testFolders") + void test_the_provideComponent(String testFolder) throws IOException, InterruptedException { + // load the pom target pom file + byte[] targetPom; + try (var is = getResourceAsStreamDecision( + this.getClass(), new String[] {"tst_manifests", "npm", testFolder, "package.json"})) { + targetPom = is.readAllBytes(); + } + // load expected SBOM + String expectedSbom = ""; + try (var is = getResourceAsStreamDecision( + this.getClass(), new String[] {"tst_manifests", "npm", testFolder, "expected_component_sbom.json"})) { + expectedSbom = new String(is.readAllBytes()); + } + String npmListingComponent; + try (var is = getResourceAsStreamDecision( + this.getClass(), new String[] {"tst_manifests", "npm", testFolder, "npm-ls-component.json"})) { + npmListingComponent = new String(is.readAllBytes()); + } - @ParameterizedTest - @MethodSource("testFolders") - void test_the_provideComponent(String testFolder) throws IOException, InterruptedException { - // load the pom target pom file - byte[] targetPom; - try (var is = getResourceAsStreamDecision(this.getClass(), new String [] { "tst_manifests", "npm", testFolder, "package.json"})) { - targetPom = is.readAllBytes(); - } - // load expected SBOM - String expectedSbom = ""; - try (var is = getResourceAsStreamDecision(this.getClass(), new String [] { "tst_manifests", "npm", testFolder, "expected_component_sbom.json"})) { - expectedSbom = new String(is.readAllBytes()); - } - String npmListingComponent; - try (var is = getResourceAsStreamDecision(this.getClass(), new String [] { "tst_manifests", "npm", testFolder, "npm-ls-component.json"})) { - npmListingComponent = new String(is.readAllBytes()); - } + // MockedStatic javaFiles = mockStatic(Files.class); + // Operations.runProcess(contains("npm i"),any()) + // mockedOperations.when(() -> + // Operations.runProcessGetOutput(eq(null),any())).thenReturn(npmListingComponent); + MockedStatic mockedOperations = mockStatic(Operations.class); + mockedOperations.when(() -> Operations.runProcess(any(), any())).thenAnswer((invocationOnMock) -> { + String[] commandParts = (String[]) invocationOnMock.getRawArguments()[0]; + int lastElementIsDir = commandParts.length - 1; + String packageLockJson = commandParts[lastElementIsDir] + "/package-lock.json"; + Files.createFile(Path.of(packageLockJson)); + return packageLockJson; + }); + ArgumentMatcher matchPath = path -> path == null; -// MockedStatic javaFiles = mockStatic(Files.class); - //Operations.runProcess(contains("npm i"),any()) -// mockedOperations.when(() -> Operations.runProcessGetOutput(eq(null),any())).thenReturn(npmListingComponent); - MockedStatic mockedOperations = mockStatic(Operations.class); - mockedOperations.when(() -> Operations.runProcess(any(),any())).thenAnswer((invocationOnMock) -> { - String[] commandParts = (String [])invocationOnMock.getRawArguments()[0]; - int lastElementIsDir = commandParts.length - 1; - String packageLockJson = commandParts[lastElementIsDir] + "/package-lock.json"; - Files.createFile(Path.of(packageLockJson)); - return packageLockJson ; - }); - ArgumentMatcher matchPath = path -> path == null; + mockedOperations + .when(() -> Operations.runProcessGetOutput(argThat(matchPath), any(String[].class))) + .thenReturn(npmListingComponent); + // when providing component content for our pom + var content = new JavaScriptNpmProvider().provideComponent(targetPom); + mockedOperations.close(); + // javaFiles.close(); + // verify expected SBOM is returned + assertThat(content.type).isEqualTo(Api.CYCLONEDX_MEDIA_TYPE); + assertThat(dropIgnored(new String(content.buffer))).isEqualTo(dropIgnored(expectedSbom)); + } - mockedOperations.when(() -> Operations.runProcessGetOutput(argThat(matchPath),any(String[].class))).thenReturn(npmListingComponent); - // when providing component content for our pom - var content = new JavaScriptNpmProvider().provideComponent(targetPom); - mockedOperations.close(); -// javaFiles.close(); - // verify expected SBOM is returned - assertThat(content.type).isEqualTo(Api.CYCLONEDX_MEDIA_TYPE); - assertThat(dropIgnored(new String(content.buffer))) - .isEqualTo(dropIgnored(expectedSbom)); - } -@ParameterizedTest - @MethodSource("testFolders") - void test_the_provideComponent_with_Path(String testFolder) throws Exception { - // load the pom target pom file + @ParameterizedTest + @MethodSource("testFolders") + void test_the_provideComponent_with_Path(String testFolder) throws Exception { + // load the pom target pom file - // create temp file hosting our sut package.json - var tmpNpmFolder = Files.createTempDirectory("exhort_test_"); - var tmpNpmFile = Files.createFile(tmpNpmFolder.resolve("package.json")); - try (var is = getResourceAsStreamDecision(this.getClass(), new String [] { "tst_manifests", "npm", testFolder, "package.json"})) { - Files.write(tmpNpmFile, is.readAllBytes()); - } - String expectedSbom = ""; - try (var is = getResourceAsStreamDecision(this.getClass(), new String [] { "tst_manifests", "npm", testFolder, "expected_component_sbom.json"})) { - expectedSbom = new String(is.readAllBytes()); + // create temp file hosting our sut package.json + var tmpNpmFolder = Files.createTempDirectory("exhort_test_"); + var tmpNpmFile = Files.createFile(tmpNpmFolder.resolve("package.json")); + try (var is = getResourceAsStreamDecision( + this.getClass(), new String[] {"tst_manifests", "npm", testFolder, "package.json"})) { + Files.write(tmpNpmFile, is.readAllBytes()); + } + String expectedSbom = ""; + try (var is = getResourceAsStreamDecision( + this.getClass(), new String[] {"tst_manifests", "npm", testFolder, "expected_component_sbom.json"})) { + expectedSbom = new String(is.readAllBytes()); + } + String npmListingComponent; + try (var is = getResourceAsStreamDecision( + this.getClass(), new String[] {"tst_manifests", "npm", testFolder, "npm-ls-component.json"})) { + npmListingComponent = new String(is.readAllBytes()); + } + ArgumentMatcher matchPath = path -> path == null; + MockedStatic mockedOperations = mockStatic(Operations.class); + mockedOperations.when(() -> Operations.runProcess(any(), any())).thenAnswer((invocationOnMock) -> { + String[] commandParts = (String[]) invocationOnMock.getRawArguments()[0]; + int lastElementIsDir = commandParts.length - 1; + String packageLockJson = commandParts[lastElementIsDir] + "/package-lock.json"; + Files.createFile(Path.of(packageLockJson)); + return packageLockJson; + }); + mockedOperations + .when(() -> Operations.runProcessGetOutput(argThat(matchPath), any(String[].class))) + .thenReturn(npmListingComponent); + // when providing component content for our pom + var content = new JavaScriptNpmProvider().provideComponent(tmpNpmFile); + mockedOperations.close(); + // verify expected SBOM is returned + assertThat(content.type).isEqualTo(Api.CYCLONEDX_MEDIA_TYPE); + assertThat(dropIgnored(new String(content.buffer))).isEqualTo(dropIgnored(expectedSbom)); } - String npmListingComponent; - try (var is = getResourceAsStreamDecision(this.getClass(), new String [] { "tst_manifests", "npm", testFolder, "npm-ls-component.json"})) { - npmListingComponent = new String(is.readAllBytes()); - } - ArgumentMatcher matchPath = path -> path == null; - MockedStatic mockedOperations = mockStatic(Operations.class); - mockedOperations.when(() -> Operations.runProcess(any(),any())).thenAnswer((invocationOnMock) -> { - String[] commandParts = (String [])invocationOnMock.getRawArguments()[0]; - int lastElementIsDir = commandParts.length - 1; - String packageLockJson = commandParts[lastElementIsDir] + "/package-lock.json"; - Files.createFile(Path.of(packageLockJson)); - return packageLockJson ; - }); - mockedOperations.when(() -> Operations.runProcessGetOutput(argThat(matchPath),any(String[].class))).thenReturn(npmListingComponent); - // when providing component content for our pom - var content = new JavaScriptNpmProvider().provideComponent(tmpNpmFile); - mockedOperations.close(); - // verify expected SBOM is returned - assertThat(content.type).isEqualTo(Api.CYCLONEDX_MEDIA_TYPE); - assertThat(dropIgnored(new String(content.buffer))) - .isEqualTo(dropIgnored(expectedSbom)); - } - private String dropIgnored(String s) { - return s.replaceAll("\\s+","").replaceAll("\"timestamp\":\"[a-zA-Z0-9\\-\\:]+\"", ""); - } + private String dropIgnored(String s) { + return s.replaceAll("\\s+", "").replaceAll("\"timestamp\":\"[a-zA-Z0-9\\-\\:]+\"", ""); + } } diff --git a/src/test/java/com/redhat/exhort/providers/PythonEnvironmentExtension.java b/src/test/java/com/redhat/exhort/providers/PythonEnvironmentExtension.java index 34a7fe35..25ad5557 100644 --- a/src/test/java/com/redhat/exhort/providers/PythonEnvironmentExtension.java +++ b/src/test/java/com/redhat/exhort/providers/PythonEnvironmentExtension.java @@ -17,81 +17,88 @@ import com.redhat.exhort.tools.Operations; import com.redhat.exhort.utils.PythonControllerBase; -import com.redhat.exhort.utils.PythonControllerVirtualEnv; import com.redhat.exhort.utils.PythonControllerTestEnv; -import org.junit.jupiter.api.extension.*; - -import java.io.IOException; -import java.nio.file.Files; import java.util.List; +import org.junit.jupiter.api.extension.*; - -public class PythonEnvironmentExtension implements BeforeAllCallback, AfterAllCallback, BeforeEachCallback, AfterEachCallback, ParameterResolver, BeforeTestExecutionCallback { - - - private PythonControllerBase pythonController = new PythonControllerTestEnv(Operations.getCustomPathOrElse("python3"),Operations.getCustomPathOrElse("pip3")); - private System.Logger log = System.getLogger(this.getClass().getName()); - -// public PythonEnvironmentExtension(List requirementsFiles) { -// this.requirementsFiles = requirementsFiles; -// } - - private List requirementsFiles; - @Override - public void afterAll(ExtensionContext extensionContext) throws Exception { - log.log(System.Logger.Level.INFO,"Finished all python tests and about to clean environment"); - pythonController.cleanEnvironment(true); - } - - @Override - public void afterEach(ExtensionContext extensionContext) throws Exception { - log.log(System.Logger.Level.INFO,String.format("Finished Test Method: %s", extensionContext.getRequiredTestMethod())); - } - - @Override - public void beforeAll(ExtensionContext extensionContext) throws Exception { - log.log(System.Logger.Level.INFO,"Preparing python environment for tests"); - String python3 = Operations.getCustomPathOrElse("python3"); - String pip3 = Operations.getCustomPathOrElse("pip3"); - this.pythonController = new PythonControllerTestEnv(python3,pip3); - log.log(System.Logger.Level.INFO,"Finished Preparing environment for testing"); -// var tmpPythonModuleDir = Files.createTempDirectory("exhort_test_"); -// var tmpPythonFile = Files.createFile(tmpPythonModuleDir.resolve("requirements.txt")); -// Python_Provider_Test.testFolders().forEach( test -> { -// try (var is = getClass().getClassLoader().getResourceAsStream(String.join("/","tst_manifests", "pip", test, "requirements.txt"))) { -// Files.write(tmpPythonFile, is.readAllBytes()); -// pythonController.installPackage(tmpPythonFile.toAbsolutePath().toString()); -// -// } catch (IOException e) { -// throw new RuntimeException(e); -// } -// }); -// log.log(System.Logger.Level.INFO,"Finished Installing all requirements.txt files"); -// Files.deleteIfExists(tmpPythonFile); -// Files.deleteIfExists(tmpPythonModuleDir); - - - } - - @Override - public void beforeEach(ExtensionContext extensionContext) throws Exception { - log.log(System.Logger.Level.INFO,String.format("About to Start Test Method: %s", extensionContext.getRequiredTestMethod())); - } - - @Override - public boolean supportsParameter(ParameterContext parameterContext, ExtensionContext extensionContext) throws ParameterResolutionException { - return parameterContext.getParameter().getType() - .equals(PythonControllerBase.class) || parameterContext.getParameter().getType() - .equals(PythonControllerTestEnv.class); - } - - @Override - public Object resolveParameter(ParameterContext parameterContext, ExtensionContext extensionContext) throws ParameterResolutionException { - return this.pythonController; - } - - @Override - public void beforeTestExecution(ExtensionContext extensionContext) throws Exception { -// Method requiredTestMethod = extensionContext.getRequiredTestInstances(); - } +public class PythonEnvironmentExtension + implements BeforeAllCallback, + AfterAllCallback, + BeforeEachCallback, + AfterEachCallback, + ParameterResolver, + BeforeTestExecutionCallback { + + private PythonControllerBase pythonController = new PythonControllerTestEnv( + Operations.getCustomPathOrElse("python3"), Operations.getCustomPathOrElse("pip3")); + private System.Logger log = System.getLogger(this.getClass().getName()); + + // public PythonEnvironmentExtension(List requirementsFiles) { + // this.requirementsFiles = requirementsFiles; + // } + + private List requirementsFiles; + + @Override + public void afterAll(ExtensionContext extensionContext) throws Exception { + log.log(System.Logger.Level.INFO, "Finished all python tests and about to clean environment"); + pythonController.cleanEnvironment(true); + } + + @Override + public void afterEach(ExtensionContext extensionContext) throws Exception { + log.log( + System.Logger.Level.INFO, + String.format("Finished Test Method: %s", extensionContext.getRequiredTestMethod())); + } + + @Override + public void beforeAll(ExtensionContext extensionContext) throws Exception { + log.log(System.Logger.Level.INFO, "Preparing python environment for tests"); + String python3 = Operations.getCustomPathOrElse("python3"); + String pip3 = Operations.getCustomPathOrElse("pip3"); + this.pythonController = new PythonControllerTestEnv(python3, pip3); + log.log(System.Logger.Level.INFO, "Finished Preparing environment for testing"); + // var tmpPythonModuleDir = Files.createTempDirectory("exhort_test_"); + // var tmpPythonFile = Files.createFile(tmpPythonModuleDir.resolve("requirements.txt")); + // Python_Provider_Test.testFolders().forEach( test -> { + // try (var is = getClass().getClassLoader().getResourceAsStream(String.join("/","tst_manifests", "pip", + // test, "requirements.txt"))) { + // Files.write(tmpPythonFile, is.readAllBytes()); + // pythonController.installPackage(tmpPythonFile.toAbsolutePath().toString()); + // + // } catch (IOException e) { + // throw new RuntimeException(e); + // } + // }); + // log.log(System.Logger.Level.INFO,"Finished Installing all requirements.txt files"); + // Files.deleteIfExists(tmpPythonFile); + // Files.deleteIfExists(tmpPythonModuleDir); + + } + + @Override + public void beforeEach(ExtensionContext extensionContext) throws Exception { + log.log( + System.Logger.Level.INFO, + String.format("About to Start Test Method: %s", extensionContext.getRequiredTestMethod())); + } + + @Override + public boolean supportsParameter(ParameterContext parameterContext, ExtensionContext extensionContext) + throws ParameterResolutionException { + return parameterContext.getParameter().getType().equals(PythonControllerBase.class) + || parameterContext.getParameter().getType().equals(PythonControllerTestEnv.class); + } + + @Override + public Object resolveParameter(ParameterContext parameterContext, ExtensionContext extensionContext) + throws ParameterResolutionException { + return this.pythonController; + } + + @Override + public void beforeTestExecution(ExtensionContext extensionContext) throws Exception { + // Method requiredTestMethod = extensionContext.getRequiredTestInstances(); + } } diff --git a/src/test/java/com/redhat/exhort/providers/Python_Provider_Test.java b/src/test/java/com/redhat/exhort/providers/Python_Provider_Test.java index 1e3070aa..0f919234 100644 --- a/src/test/java/com/redhat/exhort/providers/Python_Provider_Test.java +++ b/src/test/java/com/redhat/exhort/providers/Python_Provider_Test.java @@ -15,171 +15,166 @@ */ package com.redhat.exhort.providers; +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException; + import com.redhat.exhort.Api; import com.redhat.exhort.ExhortTest; import com.redhat.exhort.utils.PythonControllerBase; -import org.junit.jupiter.api.Test; -import org.junit.jupiter.api.condition.EnabledIfEnvironmentVariable; -import org.junit.jupiter.api.extension.ExtendWith; -import org.junit.jupiter.params.ParameterizedTest; -import org.junit.jupiter.params.provider.MethodSource; - import java.io.IOException; import java.nio.file.Files; import java.nio.file.Path; import java.util.Base64; import java.util.stream.Stream; - -import static org.assertj.core.api.Assertions.assertThat; -import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.condition.EnabledIfEnvironmentVariable; +import org.junit.jupiter.api.extension.ExtendWith; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.MethodSource; @ExtendWith(PythonEnvironmentExtension.class) class Python_Provider_Test extends ExhortTest { - static Stream testFolders() { - return Stream.of( -"pip_requirements_txt_no_ignore", - "pip_requirements_txt_ignore" - - ); - } - -// @RegisterExtension -// private PythonEnvironmentExtension pythonEnvironmentExtension = new PythonEnvironmentExtension(); + static Stream testFolders() { + return Stream.of("pip_requirements_txt_no_ignore", "pip_requirements_txt_ignore"); + } - public Python_Provider_Test(PythonControllerBase pythonController) { - this.pythonController = pythonController; - this.pythonPipProvider = new PythonPipProvider(); - this.pythonPipProvider.setPythonController(pythonController); - } + // @RegisterExtension + // private PythonEnvironmentExtension pythonEnvironmentExtension = new PythonEnvironmentExtension(); - private PythonControllerBase pythonController; - private PythonPipProvider pythonPipProvider; - @EnabledIfEnvironmentVariable(named = "RUN_PYTHON_BIN",matches = "true") - @ParameterizedTest - @MethodSource("testFolders") - void test_the_provideStack(String testFolder) throws IOException, InterruptedException { - // create temp file hosting our sut package.json - var tmpPythonModuleDir = Files.createTempDirectory("exhort_test_"); - var tmpPythonFile = Files.createFile(tmpPythonModuleDir.resolve("requirements.txt")); - try (var is = getResourceAsStreamDecision(this.getClass(), new String [] { "tst_manifests", "pip", testFolder, "requirements.txt"})) { - Files.write(tmpPythonFile, is.readAllBytes()); - } - // load expected SBOM - String expectedSbom; - try (var is = getResourceAsStreamDecision(this.getClass(), new String [] { "tst_manifests", "pip", testFolder, "expected_stack_sbom.json"})) { - expectedSbom = new String(is.readAllBytes()); + public Python_Provider_Test(PythonControllerBase pythonController) { + this.pythonController = pythonController; + this.pythonPipProvider = new PythonPipProvider(); + this.pythonPipProvider.setPythonController(pythonController); } - // when providing stack content for our pom - var content = this.pythonPipProvider.provideStack(tmpPythonFile); - // cleanup - Files.deleteIfExists(tmpPythonFile); - Files.deleteIfExists(tmpPythonModuleDir); - // verify expected SBOM is returned - assertThat(content.type).isEqualTo(Api.CYCLONEDX_MEDIA_TYPE); - assertThat(dropIgnored(new String(content.buffer))) - .isEqualTo(dropIgnored(expectedSbom)); - } - @EnabledIfEnvironmentVariable(named = "RUN_PYTHON_BIN",matches = "true") - @ParameterizedTest - @MethodSource("testFolders") - void test_the_provideComponent(String testFolder) throws IOException, InterruptedException { - // load the pom target pom file - byte[] targetRequirementsTxt; - try (var is = getResourceAsStreamDecision(this.getClass(), new String [] { "tst_manifests", "pip", testFolder, "requirements.txt"})) { - targetRequirementsTxt = is.readAllBytes(); + private PythonControllerBase pythonController; + private PythonPipProvider pythonPipProvider; + + @EnabledIfEnvironmentVariable(named = "RUN_PYTHON_BIN", matches = "true") + @ParameterizedTest + @MethodSource("testFolders") + void test_the_provideStack(String testFolder) throws IOException, InterruptedException { + // create temp file hosting our sut package.json + var tmpPythonModuleDir = Files.createTempDirectory("exhort_test_"); + var tmpPythonFile = Files.createFile(tmpPythonModuleDir.resolve("requirements.txt")); + try (var is = getResourceAsStreamDecision( + this.getClass(), new String[] {"tst_manifests", "pip", testFolder, "requirements.txt"})) { + Files.write(tmpPythonFile, is.readAllBytes()); + } + // load expected SBOM + String expectedSbom; + try (var is = getResourceAsStreamDecision( + this.getClass(), new String[] {"tst_manifests", "pip", testFolder, "expected_stack_sbom.json"})) { + expectedSbom = new String(is.readAllBytes()); + } + // when providing stack content for our pom + var content = this.pythonPipProvider.provideStack(tmpPythonFile); + // cleanup + Files.deleteIfExists(tmpPythonFile); + Files.deleteIfExists(tmpPythonModuleDir); + // verify expected SBOM is returned + assertThat(content.type).isEqualTo(Api.CYCLONEDX_MEDIA_TYPE); + assertThat(dropIgnored(new String(content.buffer))).isEqualTo(dropIgnored(expectedSbom)); } - // load expected SBOM - String expectedSbom = ""; - try (var is = getResourceAsStreamDecision(this.getClass(), new String [] { "tst_manifests", "pip", testFolder, "expected_component_sbom.json"})) { - expectedSbom = new String(is.readAllBytes()); - } - // when providing component content for our pom - var content = this.pythonPipProvider.provideComponent(targetRequirementsTxt); - // verify expected SBOM is returned - assertThat(content.type).isEqualTo(Api.CYCLONEDX_MEDIA_TYPE); - assertThat(dropIgnored(new String(content.buffer))) - .isEqualTo(dropIgnored(expectedSbom)); - - } - - - @ParameterizedTest - @MethodSource("testFolders") - void test_the_provideStack_with_properties(String testFolder) throws IOException, InterruptedException { - // create temp file hosting our sut package.json - var tmpPythonModuleDir = Files.createTempDirectory("exhort_test_"); - var tmpPythonFile = Files.createFile(tmpPythonModuleDir.resolve("requirements.txt")); - try (var is = getResourceAsStreamDecision(this.getClass(), new String [] { "tst_manifests", "pip", testFolder, "requirements.txt"})) { - Files.write(tmpPythonFile, is.readAllBytes()); - } - // load expected SBOM - String expectedSbom; - try (var is = getResourceAsStreamDecision(this.getClass(), new String [] { "tst_manifests", "pip", testFolder, "expected_stack_sbom.json"})) { - expectedSbom = new String(is.readAllBytes()); + @EnabledIfEnvironmentVariable(named = "RUN_PYTHON_BIN", matches = "true") + @ParameterizedTest + @MethodSource("testFolders") + void test_the_provideComponent(String testFolder) throws IOException, InterruptedException { + // load the pom target pom file + byte[] targetRequirementsTxt; + try (var is = getResourceAsStreamDecision( + this.getClass(), new String[] {"tst_manifests", "pip", testFolder, "requirements.txt"})) { + targetRequirementsTxt = is.readAllBytes(); + } + // load expected SBOM + String expectedSbom = ""; + try (var is = getResourceAsStreamDecision( + this.getClass(), new String[] {"tst_manifests", "pip", testFolder, "expected_component_sbom.json"})) { + expectedSbom = new String(is.readAllBytes()); + } + // when providing component content for our pom + var content = this.pythonPipProvider.provideComponent(targetRequirementsTxt); + // verify expected SBOM is returned + assertThat(content.type).isEqualTo(Api.CYCLONEDX_MEDIA_TYPE); + assertThat(dropIgnored(new String(content.buffer))).isEqualTo(dropIgnored(expectedSbom)); } - // when providing stack content for our pom - var content = this.pythonPipProvider.provideStack(tmpPythonFile); - String pipShowContent = this.getStringFromFile("tst_manifests", "pip", "pip-show.txt"); - String pipFreezeContent = this.getStringFromFile("tst_manifests", "pip", "pip-freeze-all.txt"); - String base64PipShow = new String(Base64.getEncoder().encode(pipShowContent.getBytes())); - String base64PipFreeze = new String(Base64.getEncoder().encode(pipFreezeContent.getBytes())); - System.setProperty("EXHORT_PIP_SHOW",base64PipShow); - System.setProperty("EXHORT_PIP_FREEZE",base64PipFreeze); - // cleanup - Files.deleteIfExists(tmpPythonFile); - Files.deleteIfExists(tmpPythonModuleDir); - System.clearProperty("EXHORT_PIP_SHOW"); - System.clearProperty("EXHORT_PIP_FREEZE"); - // verify expected SBOM is returned - assertThat(content.type).isEqualTo(Api.CYCLONEDX_MEDIA_TYPE); - assertThat(dropIgnored(new String(content.buffer))) - .isEqualTo(dropIgnored(expectedSbom)); - } - @ParameterizedTest - @MethodSource("testFolders") - void test_the_provideComponent_with_properties(String testFolder) throws IOException, InterruptedException { - // load the pom target pom file - byte[] targetRequirementsTxt; - try (var is = getResourceAsStreamDecision(this.getClass(), new String [] { "tst_manifests", "pip", testFolder, "requirements.txt"})) { - targetRequirementsTxt = is.readAllBytes(); + @ParameterizedTest + @MethodSource("testFolders") + void test_the_provideStack_with_properties(String testFolder) throws IOException, InterruptedException { + // create temp file hosting our sut package.json + var tmpPythonModuleDir = Files.createTempDirectory("exhort_test_"); + var tmpPythonFile = Files.createFile(tmpPythonModuleDir.resolve("requirements.txt")); + try (var is = getResourceAsStreamDecision( + this.getClass(), new String[] {"tst_manifests", "pip", testFolder, "requirements.txt"})) { + Files.write(tmpPythonFile, is.readAllBytes()); + } + // load expected SBOM + String expectedSbom; + try (var is = getResourceAsStreamDecision( + this.getClass(), new String[] {"tst_manifests", "pip", testFolder, "expected_stack_sbom.json"})) { + expectedSbom = new String(is.readAllBytes()); + } + // when providing stack content for our pom + var content = this.pythonPipProvider.provideStack(tmpPythonFile); + String pipShowContent = this.getStringFromFile("tst_manifests", "pip", "pip-show.txt"); + String pipFreezeContent = this.getStringFromFile("tst_manifests", "pip", "pip-freeze-all.txt"); + String base64PipShow = new String(Base64.getEncoder().encode(pipShowContent.getBytes())); + String base64PipFreeze = new String(Base64.getEncoder().encode(pipFreezeContent.getBytes())); + System.setProperty("EXHORT_PIP_SHOW", base64PipShow); + System.setProperty("EXHORT_PIP_FREEZE", base64PipFreeze); + // cleanup + Files.deleteIfExists(tmpPythonFile); + Files.deleteIfExists(tmpPythonModuleDir); + System.clearProperty("EXHORT_PIP_SHOW"); + System.clearProperty("EXHORT_PIP_FREEZE"); + // verify expected SBOM is returned + assertThat(content.type).isEqualTo(Api.CYCLONEDX_MEDIA_TYPE); + assertThat(dropIgnored(new String(content.buffer))).isEqualTo(dropIgnored(expectedSbom)); } - // load expected SBOM - String expectedSbom = ""; - try (var is = getResourceAsStreamDecision(this.getClass(), new String [] {"tst_manifests", "pip", testFolder, "expected_component_sbom.json"})) { - expectedSbom = new String(is.readAllBytes()); - } - String pipShowContent = this.getStringFromFile("tst_manifests", "pip", "pip-show.txt"); - String pipFreezeContent = this.getStringFromFile("tst_manifests", "pip", "pip-freeze-all.txt"); - String base64PipShow = new String(Base64.getEncoder().encode(pipShowContent.getBytes())); - String base64PipFreeze = new String(Base64.getEncoder().encode(pipFreezeContent.getBytes())); - System.setProperty("EXHORT_PIP_SHOW",base64PipShow); - System.setProperty("EXHORT_PIP_FREEZE",base64PipFreeze); - // when providing component content for our pom - var content = this.pythonPipProvider.provideComponent(targetRequirementsTxt); - // verify expected SBOM is returned - assertThat(content.type).isEqualTo(Api.CYCLONEDX_MEDIA_TYPE); - assertThat(dropIgnored(new String(content.buffer))) - .isEqualTo(dropIgnored(expectedSbom)); - System.clearProperty("EXHORT_PIP_SHOW"); - System.clearProperty("EXHORT_PIP_FREEZE"); - - } - - - @Test - void Test_The_ProvideComponent_Path_Should_Throw_Exception() { - assertThatIllegalArgumentException().isThrownBy(() -> { - this.pythonPipProvider.provideComponent(Path.of(".")); - }).withMessage("provideComponent with file system path for Python pip package manager is not supported"); + @ParameterizedTest + @MethodSource("testFolders") + void test_the_provideComponent_with_properties(String testFolder) throws IOException, InterruptedException { + // load the pom target pom file + byte[] targetRequirementsTxt; + try (var is = getResourceAsStreamDecision( + this.getClass(), new String[] {"tst_manifests", "pip", testFolder, "requirements.txt"})) { + targetRequirementsTxt = is.readAllBytes(); + } + // load expected SBOM + String expectedSbom = ""; + try (var is = getResourceAsStreamDecision( + this.getClass(), new String[] {"tst_manifests", "pip", testFolder, "expected_component_sbom.json"})) { + expectedSbom = new String(is.readAllBytes()); + } + String pipShowContent = this.getStringFromFile("tst_manifests", "pip", "pip-show.txt"); + String pipFreezeContent = this.getStringFromFile("tst_manifests", "pip", "pip-freeze-all.txt"); + String base64PipShow = new String(Base64.getEncoder().encode(pipShowContent.getBytes())); + String base64PipFreeze = new String(Base64.getEncoder().encode(pipFreezeContent.getBytes())); + System.setProperty("EXHORT_PIP_SHOW", base64PipShow); + System.setProperty("EXHORT_PIP_FREEZE", base64PipFreeze); + // when providing component content for our pom + var content = this.pythonPipProvider.provideComponent(targetRequirementsTxt); + // verify expected SBOM is returned + assertThat(content.type).isEqualTo(Api.CYCLONEDX_MEDIA_TYPE); + assertThat(dropIgnored(new String(content.buffer))).isEqualTo(dropIgnored(expectedSbom)); + System.clearProperty("EXHORT_PIP_SHOW"); + System.clearProperty("EXHORT_PIP_FREEZE"); + } - } + @Test + void Test_The_ProvideComponent_Path_Should_Throw_Exception() { + assertThatIllegalArgumentException() + .isThrownBy(() -> { + this.pythonPipProvider.provideComponent(Path.of(".")); + }) + .withMessage("provideComponent with file system path for Python pip package manager is not supported"); + } - private String dropIgnored(String s) { - return s.replaceAll("\\s+","").replaceAll("\"timestamp\":\"[a-zA-Z0-9\\-\\:]+\"", ""); - } + private String dropIgnored(String s) { + return s.replaceAll("\\s+", "").replaceAll("\"timestamp\":\"[a-zA-Z0-9\\-\\:]+\"", ""); + } } diff --git a/src/test/java/com/redhat/exhort/tools/Ecosystem_Test.java b/src/test/java/com/redhat/exhort/tools/Ecosystem_Test.java index 545ebee3..ddda0d03 100644 --- a/src/test/java/com/redhat/exhort/tools/Ecosystem_Test.java +++ b/src/test/java/com/redhat/exhort/tools/Ecosystem_Test.java @@ -18,24 +18,21 @@ import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThatExceptionOfType; +import com.redhat.exhort.providers.JavaMavenProvider; import java.nio.file.Paths; - import org.junit.jupiter.api.Test; -import com.redhat.exhort.providers.JavaMavenProvider; - class Ecosystem_Test { - @Test - void get_a_provider_for_an_unknown_package_file_should_throw_an_exception() { - var manifestPath = Paths.get("/not/a/supported/mani.fest"); - assertThatExceptionOfType(IllegalStateException.class).isThrownBy(() -> Ecosystem.getProvider(manifestPath)); - } - - @Test - void get_a_provider_for_a_pom_xml_file_should_return_java_maven_manifest() { - var manifestPath = Paths.get("/supported/manifest/pom.xml"); - assertThat(Ecosystem.getProvider(manifestPath)).isInstanceOf(JavaMavenProvider.class); - } - + @Test + void get_a_provider_for_an_unknown_package_file_should_throw_an_exception() { + var manifestPath = Paths.get("/not/a/supported/mani.fest"); + assertThatExceptionOfType(IllegalStateException.class).isThrownBy(() -> Ecosystem.getProvider(manifestPath)); + } + + @Test + void get_a_provider_for_a_pom_xml_file_should_return_java_maven_manifest() { + var manifestPath = Paths.get("/supported/manifest/pom.xml"); + assertThat(Ecosystem.getProvider(manifestPath)).isInstanceOf(JavaMavenProvider.class); + } } diff --git a/src/test/java/com/redhat/exhort/tools/OperationsTest.java b/src/test/java/com/redhat/exhort/tools/OperationsTest.java index 9428b296..568ff357 100644 --- a/src/test/java/com/redhat/exhort/tools/OperationsTest.java +++ b/src/test/java/com/redhat/exhort/tools/OperationsTest.java @@ -15,32 +15,34 @@ */ package com.redhat.exhort.tools; -import org.junit.jupiter.api.Test; - -import java.nio.file.Path; - import static org.assertj.core.api.Assertions.assertThatNoException; import static org.assertj.core.api.Assertions.assertThatRuntimeException; -class OperationsTest { - - @Test - void when_running_process_for_existing_command_should_not_throw_exception() { - assertThatNoException().isThrownBy(() -> Operations.runProcess("ls", ".")); - } - - @Test - void when_running_process_for_non_existing_command_should_throw_runtime_exception() { - assertThatRuntimeException().isThrownBy(() -> Operations.runProcess("unknown", "--command")); - } +import java.nio.file.Path; +import org.junit.jupiter.api.Test; - @Test - void when_running_process_get_full_output_for_existing_command_should_not_throw_exception() { - assertThatNoException().isThrownBy(() -> Operations.runProcessGetFullOutput(null, new String[]{"ls", "."}, null)); - } +class OperationsTest { - @Test - void when_running_process_get_full_output_for_non_existing_command_should_throw_runtime_exception() { - assertThatRuntimeException().isThrownBy(() -> Operations.runProcessGetFullOutput(Path.of("."), new String[]{"unknown", "--command"}, new String[]{"PATH=123"})); - } + @Test + void when_running_process_for_existing_command_should_not_throw_exception() { + assertThatNoException().isThrownBy(() -> Operations.runProcess("ls", ".")); + } + + @Test + void when_running_process_for_non_existing_command_should_throw_runtime_exception() { + assertThatRuntimeException().isThrownBy(() -> Operations.runProcess("unknown", "--command")); + } + + @Test + void when_running_process_get_full_output_for_existing_command_should_not_throw_exception() { + assertThatNoException() + .isThrownBy(() -> Operations.runProcessGetFullOutput(null, new String[] {"ls", "."}, null)); + } + + @Test + void when_running_process_get_full_output_for_non_existing_command_should_throw_runtime_exception() { + assertThatRuntimeException() + .isThrownBy(() -> Operations.runProcessGetFullOutput( + Path.of("."), new String[] {"unknown", "--command"}, new String[] {"PATH=123"})); + } } diff --git a/src/test/java/com/redhat/exhort/tools/Operations_Test.java b/src/test/java/com/redhat/exhort/tools/Operations_Test.java index 2c10837b..e10fcc6f 100644 --- a/src/test/java/com/redhat/exhort/tools/Operations_Test.java +++ b/src/test/java/com/redhat/exhort/tools/Operations_Test.java @@ -21,49 +21,48 @@ import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.Nested; -import org.junit.jupiter.api.Tag; import org.junit.jupiter.api.Test; import org.junitpioneer.jupiter.ClearEnvironmentVariable; import org.junitpioneer.jupiter.SetEnvironmentVariable; class Operations_Test { - @Nested - class Test_runProcess { - @Test - void when_running_process_for_existing_command_should_not_throw_exception() { - assertThatNoException().isThrownBy(() -> Operations.runProcess("ls", ".")); - } + @Nested + class Test_runProcess { + @Test + void when_running_process_for_existing_command_should_not_throw_exception() { + assertThatNoException().isThrownBy(() -> Operations.runProcess("ls", ".")); + } - @Test - void when_running_process_for_non_existing_command_should_throw_runtime_exception() { - assertThatRuntimeException().isThrownBy(() -> Operations.runProcess("unknown", "--command")); + @Test + void when_running_process_for_non_existing_command_should_throw_runtime_exception() { + assertThatRuntimeException().isThrownBy(() -> Operations.runProcess("unknown", "--command")); + } } - } - @Nested - @ClearEnvironmentVariable(key="EXHORT_MADE_UP_CMD_PATH") - class Test_getCustomPathOrElse { - @AfterEach - void cleanup() { - System.clearProperty("EXHORT_MADE_UP_CMD_PATH"); - } + @Nested + @ClearEnvironmentVariable(key = "EXHORT_MADE_UP_CMD_PATH") + class Test_getCustomPathOrElse { + @AfterEach + void cleanup() { + System.clearProperty("EXHORT_MADE_UP_CMD_PATH"); + } - @Test - @SetEnvironmentVariable(key="EXHORT_MADE_UP_CMD_PATH", value="/path/to/env/made_up_cmd") - void when_custom_path_exists_in_env_vars_and_properties_should_return_from_env_vars() { - System.setProperty("EXHORT_MADE_UP_CMD_PATH", "/path/to/property/made_up_cmd"); - assertThat(Operations.getCustomPathOrElse("made-up cmd")).isEqualTo("/path/to/env/made_up_cmd"); - } + @Test + @SetEnvironmentVariable(key = "EXHORT_MADE_UP_CMD_PATH", value = "/path/to/env/made_up_cmd") + void when_custom_path_exists_in_env_vars_and_properties_should_return_from_env_vars() { + System.setProperty("EXHORT_MADE_UP_CMD_PATH", "/path/to/property/made_up_cmd"); + assertThat(Operations.getCustomPathOrElse("made-up cmd")).isEqualTo("/path/to/env/made_up_cmd"); + } - @Test - void when_custom_path_not_in_env_var_but_exists_in_properties_should_return_from_properties() { - System.setProperty("EXHORT_MADE_UP_CMD_PATH", "/path/to/property/made_up_cmd"); - assertThat(Operations.getCustomPathOrElse("made-up_cmd")).isEqualTo("/path/to/property/made_up_cmd"); - } + @Test + void when_custom_path_not_in_env_var_but_exists_in_properties_should_return_from_properties() { + System.setProperty("EXHORT_MADE_UP_CMD_PATH", "/path/to/property/made_up_cmd"); + assertThat(Operations.getCustomPathOrElse("made-up_cmd")).isEqualTo("/path/to/property/made_up_cmd"); + } - @Test - void when_no_custom_path_in_env_var_or_properties_should_return_the_default_executable() { - assertThat(Operations.getCustomPathOrElse("madeupcmd")).isEqualTo("madeupcmd"); + @Test + void when_no_custom_path_in_env_var_or_properties_should_return_the_default_executable() { + assertThat(Operations.getCustomPathOrElse("madeupcmd")).isEqualTo("madeupcmd"); + } } - } } diff --git a/src/test/java/com/redhat/exhort/utils/PythonControllerBaseTest.java b/src/test/java/com/redhat/exhort/utils/PythonControllerBaseTest.java index efe03ddf..ad05957f 100644 --- a/src/test/java/com/redhat/exhort/utils/PythonControllerBaseTest.java +++ b/src/test/java/com/redhat/exhort/utils/PythonControllerBaseTest.java @@ -13,1992 +13,1981 @@ * See the License for the specific language governing permissions and * limitations under the License. */ - package com.redhat.exhort.utils; -import com.redhat.exhort.ExhortTest; -import org.junit.jupiter.api.Test; -import org.mockito.ArgumentMatcher; +import static org.junit.jupiter.api.Assertions.assertEquals; +import com.redhat.exhort.ExhortTest; import java.util.Arrays; import java.util.LinkedList; import java.util.List; - -import static org.junit.jupiter.api.Assertions.assertEquals; +import org.junit.jupiter.api.Test; +import org.mockito.ArgumentMatcher; class PythonControllerBaseTest extends ExhortTest { - static ArgumentMatcher matchCommandPipFreeze = new ArgumentMatcher() { - @Override - public boolean matches(String[] command) { - return Arrays.stream(command).anyMatch(word -> word.contains("freeze")); - } - // in var args, must override type default method' void.class in argumentMatcher interface in order to let custom ArgumentMatcher work correctly. - @Override - public Class type() - { - return String[].class; - } + static ArgumentMatcher matchCommandPipFreeze = new ArgumentMatcher() { + @Override + public boolean matches(String[] command) { + return Arrays.stream(command).anyMatch(word -> word.contains("freeze")); + } + // in var args, must override type default method' void.class in argumentMatcher interface in order to let + // custom ArgumentMatcher work correctly. + @Override + public Class type() { + return String[].class; + } + }; - }; + static ArgumentMatcher matchCommandPipShow = new ArgumentMatcher() { + @Override + public boolean matches(String[] command) { + return Arrays.stream(command).anyMatch(word -> word.contains("show")); + } - static ArgumentMatcher matchCommandPipShow = new ArgumentMatcher() { - @Override - public boolean matches(String[] command) { - return Arrays.stream(command).anyMatch(word -> word.contains("show")); - } + @Override + public Class type() { + return String[].class; + } + }; - @Override - public Class type() - { - return String[].class; + @Test + void when_spliting_pip_show_dep_with_license() { + List results = PythonControllerBase.splitPipShowLines(PIP_SHOW_LINES); + assertEquals(EXPECTED_PIP_SHOW_RESULTS, results); } - }; - @Test - void when_spliting_pip_show_dep_with_license() { - List results = PythonControllerBase.splitPipShowLines(PIP_SHOW_LINES); - assertEquals(EXPECTED_PIP_SHOW_RESULTS, results); - } - - + private static final String PIP_SHOW_LINES; - private static final String PIP_SHOW_LINES; - - static { - - PIP_SHOW_LINES = "Name: altgraph\n" + - "Version: 0.17.2\n" + - "Summary: Python graph (network) package\n" + - "Home-page: https://altgraph.readthedocs.io\n" + - "Author: Ronald Oussoren\n" + - "Author-email: ronaldoussoren@mac.com\n" + - "License: MIT\n" + - "Location: /Library/Developer/CommandLineTools/Library/Frameworks/Python3.framework/Versions/3.9/lib/python3.9/site-packages\n" + - "Requires: \n" + - "Required-by: macholib\n" + - "---\n" + - "Name: scipy\n" + - "Version: 1.11.3\n" + - "Summary: Fundamental algorithms for scientific computing in Python\n" + - "Home-page: https://scipy.org/\n" + - "Author: \n" + - "Author-email: \n" + - "License: Copyright (c) 2001-2002 Enthought, Inc. 2003-2023, SciPy Developers.\n" + - " All rights reserved.\n" + - " \n" + - " Redistribution and use in source and binary forms, with or without\n" + - " modification, are permitted provided that the following conditions\n" + - " are met:\n" + - " \n" + - " 1. Redistributions of source code must retain the above copyright\n" + - " notice, this list of conditions and the following disclaimer.\n" + - " \n" + - " 2. Redistributions in binary form must reproduce the above\n" + - " copyright notice, this list of conditions and the following\n" + - " disclaimer in the documentation and/or other materials provided\n" + - " with the distribution.\n" + - " \n" + - " 3. Neither the name of the copyright holder nor the names of its\n" + - " contributors may be used to endorse or promote products derived\n" + - " from this software without specific prior written permission.\n" + - " \n" + - " THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS\n" + - " \"AS IS\" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT\n" + - " LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR\n" + - " A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT\n" + - " OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,\n" + - " SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT\n" + - " LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,\n" + - " DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY\n" + - " THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT\n" + - " (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE\n" + - " OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.\n" + - " \n" + - " ----\n" + - " \n" + - " This binary distribution of SciPy also bundles the following software:\n" + - " \n" + - " \n" + - " Name: OpenBLAS\n" + - " Files: scipy/.dylibs/libopenblas*.so\n" + - " Description: bundled as a dynamically linked library\n" + - " Availability: https://github.com/OpenMathLib/OpenBLAS/\n" + - " License: BSD-3-Clause-Attribution\n" + - " Copyright (c) 2011-2014, The OpenBLAS Project\n" + - " All rights reserved.\n" + - " \n" + - " Redistribution and use in source and binary forms, with or without\n" + - " modification, are permitted provided that the following conditions are\n" + - " met:\n" + - " \n" + - " 1. Redistributions of source code must retain the above copyright\n" + - " notice, this list of conditions and the following disclaimer.\n" + - " \n" + - " 2. Redistributions in binary form must reproduce the above copyright\n" + - " notice, this list of conditions and the following disclaimer in\n" + - " the documentation and/or other materials provided with the\n" + - " distribution.\n" + - " 3. Neither the name of the OpenBLAS project nor the names of\n" + - " its contributors may be used to endorse or promote products\n" + - " derived from this software without specific prior written\n" + - " permission.\n" + - " \n" + - " THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS \"AS IS\"\n" + - " AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE\n" + - " IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE\n" + - " ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE\n" + - " LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL\n" + - " DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR\n" + - " SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER\n" + - " CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,\n" + - " OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE\n" + - " USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.\n" + - " \n" + - " \n" + - " Name: LAPACK\n" + - " Files: scipy/.dylibs/libopenblas*.so\n" + - " Description: bundled in OpenBLAS\n" + - " Availability: https://github.com/OpenMathLib/OpenBLAS/\n" + - " License: BSD-3-Clause-Attribution\n" + - " Copyright (c) 1992-2013 The University of Tennessee and The University\n" + - " of Tennessee Research Foundation. All rights\n" + - " reserved.\n" + - " Copyright (c) 2000-2013 The University of California Berkeley. All\n" + - " rights reserved.\n" + - " Copyright (c) 2006-2013 The University of Colorado Denver. All rights\n" + - " reserved.\n" + - " \n" + - " $COPYRIGHT$\n" + - " \n" + - " Additional copyrights may follow\n" + - " \n" + - " $HEADER$\n" + - " \n" + - " Redistribution and use in source and binary forms, with or without\n" + - " modification, are permitted provided that the following conditions are\n" + - " met:\n" + - " \n" + - " - Redistributions of source code must retain the above copyright\n" + - " notice, this list of conditions and the following disclaimer.\n" + - " \n" + - " - Redistributions in binary form must reproduce the above copyright\n" + - " notice, this list of conditions and the following disclaimer listed\n" + - " in this license in the documentation and/or other materials\n" + - " provided with the distribution.\n" + - " \n" + - " - Neither the name of the copyright holders nor the names of its\n" + - " contributors may be used to endorse or promote products derived from\n" + - " this software without specific prior written permission.\n" + - " \n" + - " The copyright holders provide no reassurances that the source code\n" + - " provided does not infringe any patent, copyright, or any other\n" + - " intellectual property rights of third parties. The copyright holders\n" + - " disclaim any liability to any recipient for claims brought against\n" + - " recipient by any third party for infringement of that parties\n" + - " intellectual property rights.\n" + - " \n" + - " THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS\n" + - " \"AS IS\" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT\n" + - " LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR\n" + - " A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT\n" + - " OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,\n" + - " SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT\n" + - " LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,\n" + - " DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY\n" + - " THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT\n" + - " (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE\n" + - " OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.\n" + - " \n" + - " \n" + - " Name: GCC runtime library\n" + - " Files: scipy/.dylibs/libgfortran*, scipy/.dylibs/libgcc*\n" + - " Description: dynamically linked to files compiled with gcc\n" + - " Availability: https://gcc.gnu.org/git/?p=gcc.git;a=tree;f=libgfortran\n" + - " License: GPL-3.0-with-GCC-exception\n" + - " Copyright (C) 2002-2017 Free Software Foundation, Inc.\n" + - " \n" + - " Libgfortran is free software; you can redistribute it and/or modify\n" + - " it under the terms of the GNU General Public License as published by\n" + - " the Free Software Foundation; either version 3, or (at your option)\n" + - " any later version.\n" + - " \n" + - " Libgfortran is distributed in the hope that it will be useful,\n" + - " but WITHOUT ANY WARRANTY; without even the implied warranty of\n" + - " MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the\n" + - " GNU General Public License for more details.\n" + - " \n" + - " Under Section 7 of GPL version 3, you are granted additional\n" + - " permissions described in the GCC Runtime Library Exception, version\n" + - " 3.1, as published by the Free Software Foundation.\n" + - " \n" + - " You should have received a copy of the GNU General Public License and\n" + - " a copy of the GCC Runtime Library Exception along with this program;\n" + - " see the files COPYING3 and COPYING.RUNTIME respectively. If not, see\n" + - " .\n" + - " \n" + - " ----\n" + - " \n" + - " Full text of license texts referred to above follows (that they are\n" + - " listed below does not necessarily imply the conditions apply to the\n" + - " present binary release):\n" + - " \n" + - " ----\n" + - " \n" + - " GCC RUNTIME LIBRARY EXCEPTION\n" + - " \n" + - " Version 3.1, 31 March 2009\n" + - " \n" + - " Copyright (C) 2009 Free Software Foundation, Inc. \n" + - " \n" + - " Everyone is permitted to copy and distribute verbatim copies of this\n" + - " license document, but changing it is not allowed.\n" + - " \n" + - " This GCC Runtime Library Exception (\"Exception\") is an additional\n" + - " permission under section 7 of the GNU General Public License, version\n" + - " 3 (\"GPLv3\"). It applies to a given file (the \"Runtime Library\") that\n" + - " bears a notice placed by the copyright holder of the file stating that\n" + - " the file is governed by GPLv3 along with this Exception.\n" + - " \n" + - " When you use GCC to compile a program, GCC may combine portions of\n" + - " certain GCC header files and runtime libraries with the compiled\n" + - " program. The purpose of this Exception is to allow compilation of\n" + - " non-GPL (including proprietary) programs to use, in this way, the\n" + - " header files and runtime libraries covered by this Exception.\n" + - " \n" + - " 0. Definitions.\n" + - " \n" + - " A file is an \"Independent Module\" if it either requires the Runtime\n" + - " Library for execution after a Compilation Process, or makes use of an\n" + - " interface provided by the Runtime Library, but is not otherwise based\n" + - " on the Runtime Library.\n" + - " \n" + - " \"GCC\" means a version of the GNU Compiler Collection, with or without\n" + - " modifications, governed by version 3 (or a specified later version) of\n" + - " the GNU General Public License (GPL) with the option of using any\n" + - " subsequent versions published by the FSF.\n" + - " \n" + - " \"GPL-compatible Software\" is software whose conditions of propagation,\n" + - " modification and use would permit combination with GCC in accord with\n" + - " the license of GCC.\n" + - " \n" + - " \"Target Code\" refers to output from any compiler for a real or virtual\n" + - " target processor architecture, in executable form or suitable for\n" + - " input to an assembler, loader, linker and/or execution\n" + - " phase. Notwithstanding that, Target Code does not include data in any\n" + - " format that is used as a compiler intermediate representation, or used\n" + - " for producing a compiler intermediate representation.\n" + - " \n" + - " The \"Compilation Process\" transforms code entirely represented in\n" + - " non-intermediate languages designed for human-written code, and/or in\n" + - " Java Virtual Machine byte code, into Target Code. Thus, for example,\n" + - " use of source code generators and preprocessors need not be considered\n" + - " part of the Compilation Process, since the Compilation Process can be\n" + - " understood as starting with the output of the generators or\n" + - " preprocessors.\n" + - " \n" + - " A Compilation Process is \"Eligible\" if it is done using GCC, alone or\n" + - " with other GPL-compatible software, or if it is done without using any\n" + - " work based on GCC. For example, using non-GPL-compatible Software to\n" + - " optimize any GCC intermediate representations would not qualify as an\n" + - " Eligible Compilation Process.\n" + - " \n" + - " 1. Grant of Additional Permission.\n" + - " \n" + - " You have permission to propagate a work of Target Code formed by\n" + - " combining the Runtime Library with Independent Modules, even if such\n" + - " propagation would otherwise violate the terms of GPLv3, provided that\n" + - " all Target Code was generated by Eligible Compilation Processes. You\n" + - " may then convey such a combination under terms of your choice,\n" + - " consistent with the licensing of the Independent Modules.\n" + - " \n" + - " 2. No Weakening of GCC Copyleft.\n" + - " \n" + - " The availability of this Exception does not imply any general\n" + - " presumption that third-party software is unaffected by the copyleft\n" + - " requirements of the license of GCC.\n" + - " \n" + - " ----\n" + - " \n" + - " GNU GENERAL PUBLIC LICENSE\n" + - " Version 3, 29 June 2007\n" + - " \n" + - " Copyright (C) 2007 Free Software Foundation, Inc. \n" + - " Everyone is permitted to copy and distribute verbatim copies\n" + - " of this license document, but changing it is not allowed.\n" + - " \n" + - " Preamble\n" + - " \n" + - " The GNU General Public License is a free, copyleft license for\n" + - " software and other kinds of works.\n" + - " \n" + - " The licenses for most software and other practical works are designed\n" + - " to take away your freedom to share and change the works. By contrast,\n" + - " the GNU General Public License is intended to guarantee your freedom to\n" + - " share and change all versions of a program--to make sure it remains free\n" + - " software for all its users. We, the Free Software Foundation, use the\n" + - " GNU General Public License for most of our software; it applies also to\n" + - " any other work released this way by its authors. You can apply it to\n" + - " your programs, too.\n" + - " \n" + - " When we speak of free software, we are referring to freedom, not\n" + - " price. Our General Public Licenses are designed to make sure that you\n" + - " have the freedom to distribute copies of free software (and charge for\n" + - " them if you wish), that you receive source code or can get it if you\n" + - " want it, that you can change the software or use pieces of it in new\n" + - " free programs, and that you know you can do these things.\n" + - " \n" + - " To protect your rights, we need to prevent others from denying you\n" + - " these rights or asking you to surrender the rights. Therefore, you have\n" + - " certain responsibilities if you distribute copies of the software, or if\n" + - " you modify it: responsibilities to respect the freedom of others.\n" + - " \n" + - " For example, if you distribute copies of such a program, whether\n" + - " gratis or for a fee, you must pass on to the recipients the same\n" + - " freedoms that you received. You must make sure that they, too, receive\n" + - " or can get the source code. And you must show them these terms so they\n" + - " know their rights.\n" + - " \n" + - " Developers that use the GNU GPL protect your rights with two steps:\n" + - " (1) assert copyright on the software, and (2) offer you this License\n" + - " giving you legal permission to copy, distribute and/or modify it.\n" + - " \n" + - " For the developers' and authors' protection, the GPL clearly explains\n" + - " that there is no warranty for this free software. For both users' and\n" + - " authors' sake, the GPL requires that modified versions be marked as\n" + - " changed, so that their problems will not be attributed erroneously to\n" + - " authors of previous versions.\n" + - " \n" + - " Some devices are designed to deny users access to install or run\n" + - " modified versions of the software inside them, although the manufacturer\n" + - " can do so. This is fundamentally incompatible with the aim of\n" + - " protecting users' freedom to change the software. The systematic\n" + - " pattern of such abuse occurs in the area of products for individuals to\n" + - " use, which is precisely where it is most unacceptable. Therefore, we\n" + - " have designed this version of the GPL to prohibit the practice for those\n" + - " products. If such problems arise substantially in other domains, we\n" + - " stand ready to extend this provision to those domains in future versions\n" + - " of the GPL, as needed to protect the freedom of users.\n" + - " \n" + - " Finally, every program is threatened constantly by software patents.\n" + - " States should not allow patents to restrict development and use of\n" + - " software on general-purpose computers, but in those that do, we wish to\n" + - " avoid the special danger that patents applied to a free program could\n" + - " make it effectively proprietary. To prevent this, the GPL assures that\n" + - " patents cannot be used to render the program non-free.\n" + - " \n" + - " The precise terms and conditions for copying, distribution and\n" + - " modification follow.\n" + - " \n" + - " TERMS AND CONDITIONS\n" + - " \n" + - " 0. Definitions.\n" + - " \n" + - " \"This License\" refers to version 3 of the GNU General Public License.\n" + - " \n" + - " \"Copyright\" also means copyright-like laws that apply to other kinds of\n" + - " works, such as semiconductor masks.\n" + - " \n" + - " \"The Program\" refers to any copyrightable work licensed under this\n" + - " License. Each licensee is addressed as \"you\". \"Licensees\" and\n" + - " \"recipients\" may be individuals or organizations.\n" + - " \n" + - " To \"modify\" a work means to copy from or adapt all or part of the work\n" + - " in a fashion requiring copyright permission, other than the making of an\n" + - " exact copy. The resulting work is called a \"modified version\" of the\n" + - " earlier work or a work \"based on\" the earlier work.\n" + - " \n" + - " A \"covered work\" means either the unmodified Program or a work based\n" + - " on the Program.\n" + - " \n" + - " To \"propagate\" a work means to do anything with it that, without\n" + - " permission, would make you directly or secondarily liable for\n" + - " infringement under applicable copyright law, except executing it on a\n" + - " computer or modifying a private copy. Propagation includes copying,\n" + - " distribution (with or without modification), making available to the\n" + - " public, and in some countries other activities as well.\n" + - " \n" + - " To \"convey\" a work means any kind of propagation that enables other\n" + - " parties to make or receive copies. Mere interaction with a user through\n" + - " a computer network, with no transfer of a copy, is not conveying.\n" + - " \n" + - " An interactive user interface displays \"Appropriate Legal Notices\"\n" + - " to the extent that it includes a convenient and prominently visible\n" + - " feature that (1) displays an appropriate copyright notice, and (2)\n" + - " tells the user that there is no warranty for the work (except to the\n" + - " extent that warranties are provided), that licensees may convey the\n" + - " work under this License, and how to view a copy of this License. If\n" + - " the interface presents a list of user commands or options, such as a\n" + - " menu, a prominent item in the list meets this criterion.\n" + - " \n" + - " 1. Source Code.\n" + - " \n" + - " The \"source code\" for a work means the preferred form of the work\n" + - " for making modifications to it. \"Object code\" means any non-source\n" + - " form of a work.\n" + - " \n" + - " A \"Standard Interface\" means an interface that either is an official\n" + - " standard defined by a recognized standards body, or, in the case of\n" + - " interfaces specified for a particular programming language, one that\n" + - " is widely used among developers working in that language.\n" + - " \n" + - " The \"System Libraries\" of an executable work include anything, other\n" + - " than the work as a whole, that (a) is included in the normal form of\n" + - " packaging a Major Component, but which is not part of that Major\n" + - " Component, and (b) serves only to enable use of the work with that\n" + - " Major Component, or to implement a Standard Interface for which an\n" + - " implementation is available to the public in source code form. A\n" + - " \"Major Component\", in this context, means a major essential component\n" + - " (kernel, window system, and so on) of the specific operating system\n" + - " (if any) on which the executable work runs, or a compiler used to\n" + - " produce the work, or an object code interpreter used to run it.\n" + - " \n" + - " The \"Corresponding Source\" for a work in object code form means all\n" + - " the source code needed to generate, install, and (for an executable\n" + - " work) run the object code and to modify the work, including scripts to\n" + - " control those activities. However, it does not include the work's\n" + - " System Libraries, or general-purpose tools or generally available free\n" + - " programs which are used unmodified in performing those activities but\n" + - " which are not part of the work. For example, Corresponding Source\n" + - " includes interface definition files associated with source files for\n" + - " the work, and the source code for shared libraries and dynamically\n" + - " linked subprograms that the work is specifically designed to require,\n" + - " such as by intimate data communication or control flow between those\n" + - " subprograms and other parts of the work.\n" + - " \n" + - " The Corresponding Source need not include anything that users\n" + - " can regenerate automatically from other parts of the Corresponding\n" + - " Source.\n" + - " \n" + - " The Corresponding Source for a work in source code form is that\n" + - " same work.\n" + - " \n" + - " 2. Basic Permissions.\n" + - " \n" + - " All rights granted under this License are granted for the term of\n" + - " copyright on the Program, and are irrevocable provided the stated\n" + - " conditions are met. This License explicitly affirms your unlimited\n" + - " permission to run the unmodified Program. The output from running a\n" + - " covered work is covered by this License only if the output, given its\n" + - " content, constitutes a covered work. This License acknowledges your\n" + - " rights of fair use or other equivalent, as provided by copyright law.\n" + - " \n" + - " You may make, run and propagate covered works that you do not\n" + - " convey, without conditions so long as your license otherwise remains\n" + - " in force. You may convey covered works to others for the sole purpose\n" + - " of having them make modifications exclusively for you, or provide you\n" + - " with facilities for running those works, provided that you comply with\n" + - " the terms of this License in conveying all material for which you do\n" + - " not control copyright. Those thus making or running the covered works\n" + - " for you must do so exclusively on your behalf, under your direction\n" + - " and control, on terms that prohibit them from making any copies of\n" + - " your copyrighted material outside their relationship with you.\n" + - " \n" + - " Conveying under any other circumstances is permitted solely under\n" + - " the conditions stated below. Sublicensing is not allowed; section 10\n" + - " makes it unnecessary.\n" + - " \n" + - " 3. Protecting Users' Legal Rights From Anti-Circumvention Law.\n" + - " \n" + - " No covered work shall be deemed part of an effective technological\n" + - " measure under any applicable law fulfilling obligations under article\n" + - " 11 of the WIPO copyright treaty adopted on 20 December 1996, or\n" + - " similar laws prohibiting or restricting circumvention of such\n" + - " measures.\n" + - " \n" + - " When you convey a covered work, you waive any legal power to forbid\n" + - " circumvention of technological measures to the extent such circumvention\n" + - " is effected by exercising rights under this License with respect to\n" + - " the covered work, and you disclaim any intention to limit operation or\n" + - " modification of the work as a means of enforcing, against the work's\n" + - " users, your or third parties' legal rights to forbid circumvention of\n" + - " technological measures.\n" + - " \n" + - " 4. Conveying Verbatim Copies.\n" + - " \n" + - " You may convey verbatim copies of the Program's source code as you\n" + - " receive it, in any medium, provided that you conspicuously and\n" + - " appropriately publish on each copy an appropriate copyright notice;\n" + - " keep intact all notices stating that this License and any\n" + - " non-permissive terms added in accord with section 7 apply to the code;\n" + - " keep intact all notices of the absence of any warranty; and give all\n" + - " recipients a copy of this License along with the Program.\n" + - " \n" + - " You may charge any price or no price for each copy that you convey,\n" + - " and you may offer support or warranty protection for a fee.\n" + - " \n" + - " 5. Conveying Modified Source Versions.\n" + - " \n" + - " You may convey a work based on the Program, or the modifications to\n" + - " produce it from the Program, in the form of source code under the\n" + - " terms of section 4, provided that you also meet all of these conditions:\n" + - " \n" + - " a) The work must carry prominent notices stating that you modified\n" + - " it, and giving a relevant date.\n" + - " \n" + - " b) The work must carry prominent notices stating that it is\n" + - " released under this License and any conditions added under section\n" + - " 7. This requirement modifies the requirement in section 4 to\n" + - " \"keep intact all notices\".\n" + - " \n" + - " c) You must license the entire work, as a whole, under this\n" + - " License to anyone who comes into possession of a copy. This\n" + - " License will therefore apply, along with any applicable section 7\n" + - " additional terms, to the whole of the work, and all its parts,\n" + - " regardless of how they are packaged. This License gives no\n" + - " permission to license the work in any other way, but it does not\n" + - " invalidate such permission if you have separately received it.\n" + - " \n" + - " d) If the work has interactive user interfaces, each must display\n" + - " Appropriate Legal Notices; however, if the Program has interactive\n" + - " interfaces that do not display Appropriate Legal Notices, your\n" + - " work need not make them do so.\n" + - " \n" + - " A compilation of a covered work with other separate and independent\n" + - " works, which are not by their nature extensions of the covered work,\n" + - " and which are not combined with it such as to form a larger program,\n" + - " in or on a volume of a storage or distribution medium, is called an\n" + - " \"aggregate\" if the compilation and its resulting copyright are not\n" + - " used to limit the access or legal rights of the compilation's users\n" + - " beyond what the individual works permit. Inclusion of a covered work\n" + - " in an aggregate does not cause this License to apply to the other\n" + - " parts of the aggregate.\n" + - " \n" + - " 6. Conveying Non-Source Forms.\n" + - " \n" + - " You may convey a covered work in object code form under the terms\n" + - " of sections 4 and 5, provided that you also convey the\n" + - " machine-readable Corresponding Source under the terms of this License,\n" + - " in one of these ways:\n" + - " \n" + - " a) Convey the object code in, or embodied in, a physical product\n" + - " (including a physical distribution medium), accompanied by the\n" + - " Corresponding Source fixed on a durable physical medium\n" + - " customarily used for software interchange.\n" + - " \n" + - " b) Convey the object code in, or embodied in, a physical product\n" + - " (including a physical distribution medium), accompanied by a\n" + - " written offer, valid for at least three years and valid for as\n" + - " long as you offer spare parts or customer support for that product\n" + - " model, to give anyone who possesses the object code either (1) a\n" + - " copy of the Corresponding Source for all the software in the\n" + - " product that is covered by this License, on a durable physical\n" + - " medium customarily used for software interchange, for a price no\n" + - " more than your reasonable cost of physically performing this\n" + - " conveying of source, or (2) access to copy the\n" + - " Corresponding Source from a network server at no charge.\n" + - " \n" + - " c) Convey individual copies of the object code with a copy of the\n" + - " written offer to provide the Corresponding Source. This\n" + - " alternative is allowed only occasionally and noncommercially, and\n" + - " only if you received the object code with such an offer, in accord\n" + - " with subsection 6b.\n" + - " \n" + - " d) Convey the object code by offering access from a designated\n" + - " place (gratis or for a charge), and offer equivalent access to the\n" + - " Corresponding Source in the same way through the same place at no\n" + - " further charge. You need not require recipients to copy the\n" + - " Corresponding Source along with the object code. If the place to\n" + - " copy the object code is a network server, the Corresponding Source\n" + - " may be on a different server (operated by you or a third party)\n" + - " that supports equivalent copying facilities, provided you maintain\n" + - " clear directions next to the object code saying where to find the\n" + - " Corresponding Source. Regardless of what server hosts the\n" + - " Corresponding Source, you remain obligated to ensure that it is\n" + - " available for as long as needed to satisfy these requirements.\n" + - " \n" + - " e) Convey the object code using peer-to-peer transmission, provided\n" + - " you inform other peers where the object code and Corresponding\n" + - " Source of the work are being offered to the general public at no\n" + - " charge under subsection 6d.\n" + - " \n" + - " A separable portion of the object code, whose source code is excluded\n" + - " from the Corresponding Source as a System Library, need not be\n" + - " included in conveying the object code work.\n" + - " \n" + - " A \"User Product\" is either (1) a \"consumer product\", which means any\n" + - " tangible personal property which is normally used for personal, family,\n" + - " or household purposes, or (2) anything designed or sold for incorporation\n" + - " into a dwelling. In determining whether a product is a consumer product,\n" + - " doubtful cases shall be resolved in favor of coverage. For a particular\n" + - " product received by a particular user, \"normally used\" refers to a\n" + - " typical or common use of that class of product, regardless of the status\n" + - " of the particular user or of the way in which the particular user\n" + - " actually uses, or expects or is expected to use, the product. A product\n" + - " is a consumer product regardless of whether the product has substantial\n" + - " commercial, industrial or non-consumer uses, unless such uses represent\n" + - " the only significant mode of use of the product.\n" + - " \n" + - " \"Installation Information\" for a User Product means any methods,\n" + - " procedures, authorization keys, or other information required to install\n" + - " and execute modified versions of a covered work in that User Product from\n" + - " a modified version of its Corresponding Source. The information must\n" + - " suffice to ensure that the continued functioning of the modified object\n" + - " code is in no case prevented or interfered with solely because\n" + - " modification has been made.\n" + - " \n" + - " If you convey an object code work under this section in, or with, or\n" + - " specifically for use in, a User Product, and the conveying occurs as\n" + - " part of a transaction in which the right of possession and use of the\n" + - " User Product is transferred to the recipient in perpetuity or for a\n" + - " fixed term (regardless of how the transaction is characterized), the\n" + - " Corresponding Source conveyed under this section must be accompanied\n" + - " by the Installation Information. But this requirement does not apply\n" + - " if neither you nor any third party retains the ability to install\n" + - " modified object code on the User Product (for example, the work has\n" + - " been installed in ROM).\n" + - " \n" + - " The requirement to provide Installation Information does not include a\n" + - " requirement to continue to provide support service, warranty, or updates\n" + - " for a work that has been modified or installed by the recipient, or for\n" + - " the User Product in which it has been modified or installed. Access to a\n" + - " network may be denied when the modification itself materially and\n" + - " adversely affects the operation of the network or violates the rules and\n" + - " protocols for communication across the network.\n" + - " \n" + - " Corresponding Source conveyed, and Installation Information provided,\n" + - " in accord with this section must be in a format that is publicly\n" + - " documented (and with an implementation available to the public in\n" + - " source code form), and must require no special password or key for\n" + - " unpacking, reading or copying.\n" + - " \n" + - " 7. Additional Terms.\n" + - " \n" + - " \"Additional permissions\" are terms that supplement the terms of this\n" + - " License by making exceptions from one or more of its conditions.\n" + - " Additional permissions that are applicable to the entire Program shall\n" + - " be treated as though they were included in this License, to the extent\n" + - " that they are valid under applicable law. If additional permissions\n" + - " apply only to part of the Program, that part may be used separately\n" + - " under those permissions, but the entire Program remains governed by\n" + - " this License without regard to the additional permissions.\n" + - " \n" + - " When you convey a copy of a covered work, you may at your option\n" + - " remove any additional permissions from that copy, or from any part of\n" + - " it. (Additional permissions may be written to require their own\n" + - " removal in certain cases when you modify the work.) You may place\n" + - " additional permissions on material, added by you to a covered work,\n" + - " for which you have or can give appropriate copyright permission.\n" + - " \n" + - " Notwithstanding any other provision of this License, for material you\n" + - " add to a covered work, you may (if authorized by the copyright holders of\n" + - " that material) supplement the terms of this License with terms:\n" + - " \n" + - " a) Disclaiming warranty or limiting liability differently from the\n" + - " terms of sections 15 and 16 of this License; or\n" + - " \n" + - " b) Requiring preservation of specified reasonable legal notices or\n" + - " author attributions in that material or in the Appropriate Legal\n" + - " Notices displayed by works containing it; or\n" + - " \n" + - " c) Prohibiting misrepresentation of the origin of that material, or\n" + - " requiring that modified versions of such material be marked in\n" + - " reasonable ways as different from the original version; or\n" + - " \n" + - " d) Limiting the use for publicity purposes of names of licensors or\n" + - " authors of the material; or\n" + - " \n" + - " e) Declining to grant rights under trademark law for use of some\n" + - " trade names, trademarks, or service marks; or\n" + - " \n" + - " f) Requiring indemnification of licensors and authors of that\n" + - " material by anyone who conveys the material (or modified versions of\n" + - " it) with contractual assumptions of liability to the recipient, for\n" + - " any liability that these contractual assumptions directly impose on\n" + - " those licensors and authors.\n" + - " \n" + - " All other non-permissive additional terms are considered \"further\n" + - " restrictions\" within the meaning of section 10. If the Program as you\n" + - " received it, or any part of it, contains a notice stating that it is\n" + - " governed by this License along with a term that is a further\n" + - " restriction, you may remove that term. If a license document contains\n" + - " a further restriction but permits relicensing or conveying under this\n" + - " License, you may add to a covered work material governed by the terms\n" + - " of that license document, provided that the further restriction does\n" + - " not survive such relicensing or conveying.\n" + - " \n" + - " If you add terms to a covered work in accord with this section, you\n" + - " must place, in the relevant source files, a statement of the\n" + - " additional terms that apply to those files, or a notice indicating\n" + - " where to find the applicable terms.\n" + - " \n" + - " Additional terms, permissive or non-permissive, may be stated in the\n" + - " form of a separately written license, or stated as exceptions;\n" + - " the above requirements apply either way.\n" + - " \n" + - " 8. Termination.\n" + - " \n" + - " You may not propagate or modify a covered work except as expressly\n" + - " provided under this License. Any attempt otherwise to propagate or\n" + - " modify it is void, and will automatically terminate your rights under\n" + - " this License (including any patent licenses granted under the third\n" + - " paragraph of section 11).\n" + - " \n" + - " However, if you cease all violation of this License, then your\n" + - " license from a particular copyright holder is reinstated (a)\n" + - " provisionally, unless and until the copyright holder explicitly and\n" + - " finally terminates your license, and (b) permanently, if the copyright\n" + - " holder fails to notify you of the violation by some reasonable means\n" + - " prior to 60 days after the cessation.\n" + - " \n" + - " Moreover, your license from a particular copyright holder is\n" + - " reinstated permanently if the copyright holder notifies you of the\n" + - " violation by some reasonable means, this is the first time you have\n" + - " received notice of violation of this License (for any work) from that\n" + - " copyright holder, and you cure the violation prior to 30 days after\n" + - " your receipt of the notice.\n" + - " \n" + - " Termination of your rights under this section does not terminate the\n" + - " licenses of parties who have received copies or rights from you under\n" + - " this License. If your rights have been terminated and not permanently\n" + - " reinstated, you do not qualify to receive new licenses for the same\n" + - " material under section 10.\n" + - " \n" + - " 9. Acceptance Not Required for Having Copies.\n" + - " \n" + - " You are not required to accept this License in order to receive or\n" + - " run a copy of the Program. Ancillary propagation of a covered work\n" + - " occurring solely as a consequence of using peer-to-peer transmission\n" + - " to receive a copy likewise does not require acceptance. However,\n" + - " nothing other than this License grants you permission to propagate or\n" + - " modify any covered work. These actions infringe copyright if you do\n" + - " not accept this License. Therefore, by modifying or propagating a\n" + - " covered work, you indicate your acceptance of this License to do so.\n" + - " \n" + - " 10. Automatic Licensing of Downstream Recipients.\n" + - " \n" + - " Each time you convey a covered work, the recipient automatically\n" + - " receives a license from the original licensors, to run, modify and\n" + - " propagate that work, subject to this License. You are not responsible\n" + - " for enforcing compliance by third parties with this License.\n" + - " \n" + - " An \"entity transaction\" is a transaction transferring control of an\n" + - " organization, or substantially all assets of one, or subdividing an\n" + - " organization, or merging organizations. If propagation of a covered\n" + - " work results from an entity transaction, each party to that\n" + - " transaction who receives a copy of the work also receives whatever\n" + - " licenses to the work the party's predecessor in interest had or could\n" + - " give under the previous paragraph, plus a right to possession of the\n" + - " Corresponding Source of the work from the predecessor in interest, if\n" + - " the predecessor has it or can get it with reasonable efforts.\n" + - " \n" + - " You may not impose any further restrictions on the exercise of the\n" + - " rights granted or affirmed under this License. For example, you may\n" + - " not impose a license fee, royalty, or other charge for exercise of\n" + - " rights granted under this License, and you may not initiate litigation\n" + - " (including a cross-claim or counterclaim in a lawsuit) alleging that\n" + - " any patent claim is infringed by making, using, selling, offering for\n" + - " sale, or importing the Program or any portion of it.\n" + - " \n" + - " 11. Patents.\n" + - " \n" + - " A \"contributor\" is a copyright holder who authorizes use under this\n" + - " License of the Program or a work on which the Program is based. The\n" + - " work thus licensed is called the contributor's \"contributor version\".\n" + - " \n" + - " A contributor's \"essential patent claims\" are all patent claims\n" + - " owned or controlled by the contributor, whether already acquired or\n" + - " hereafter acquired, that would be infringed by some manner, permitted\n" + - " by this License, of making, using, or selling its contributor version,\n" + - " but do not include claims that would be infringed only as a\n" + - " consequence of further modification of the contributor version. For\n" + - " purposes of this definition, \"control\" includes the right to grant\n" + - " patent sublicenses in a manner consistent with the requirements of\n" + - " this License.\n" + - " \n" + - " Each contributor grants you a non-exclusive, worldwide, royalty-free\n" + - " patent license under the contributor's essential patent claims, to\n" + - " make, use, sell, offer for sale, import and otherwise run, modify and\n" + - " propagate the contents of its contributor version.\n" + - " \n" + - " In the following three paragraphs, a \"patent license\" is any express\n" + - " agreement or commitment, however denominated, not to enforce a patent\n" + - " (such as an express permission to practice a patent or covenant not to\n" + - " sue for patent infringement). To \"grant\" such a patent license to a\n" + - " party means to make such an agreement or commitment not to enforce a\n" + - " patent against the party.\n" + - " \n" + - " If you convey a covered work, knowingly relying on a patent license,\n" + - " and the Corresponding Source of the work is not available for anyone\n" + - " to copy, free of charge and under the terms of this License, through a\n" + - " publicly available network server or other readily accessible means,\n" + - " then you must either (1) cause the Corresponding Source to be so\n" + - " available, or (2) arrange to deprive yourself of the benefit of the\n" + - " patent license for this particular work, or (3) arrange, in a manner\n" + - " consistent with the requirements of this License, to extend the patent\n" + - " license to downstream recipients. \"Knowingly relying\" means you have\n" + - " actual knowledge that, but for the patent license, your conveying the\n" + - " covered work in a country, or your recipient's use of the covered work\n" + - " in a country, would infringe one or more identifiable patents in that\n" + - " country that you have reason to believe are valid.\n" + - " \n" + - " If, pursuant to or in connection with a single transaction or\n" + - " arrangement, you convey, or propagate by procuring conveyance of, a\n" + - " covered work, and grant a patent license to some of the parties\n" + - " receiving the covered work authorizing them to use, propagate, modify\n" + - " or convey a specific copy of the covered work, then the patent license\n" + - " you grant is automatically extended to all recipients of the covered\n" + - " work and works based on it.\n" + - " \n" + - " A patent license is \"discriminatory\" if it does not include within\n" + - " the scope of its coverage, prohibits the exercise of, or is\n" + - " conditioned on the non-exercise of one or more of the rights that are\n" + - " specifically granted under this License. You may not convey a covered\n" + - " work if you are a party to an arrangement with a third party that is\n" + - " in the business of distributing software, under which you make payment\n" + - " to the third party based on the extent of your activity of conveying\n" + - " the work, and under which the third party grants, to any of the\n" + - " parties who would receive the covered work from you, a discriminatory\n" + - " patent license (a) in connection with copies of the covered work\n" + - " conveyed by you (or copies made from those copies), or (b) primarily\n" + - " for and in connection with specific products or compilations that\n" + - " contain the covered work, unless you entered into that arrangement,\n" + - " or that patent license was granted, prior to 28 March 2007.\n" + - " \n" + - " Nothing in this License shall be construed as excluding or limiting\n" + - " any implied license or other defenses to infringement that may\n" + - " otherwise be available to you under applicable patent law.\n" + - " \n" + - " 12. No Surrender of Others' Freedom.\n" + - " \n" + - " If conditions are imposed on you (whether by court order, agreement or\n" + - " otherwise) that contradict the conditions of this License, they do not\n" + - " excuse you from the conditions of this License. If you cannot convey a\n" + - " covered work so as to satisfy simultaneously your obligations under this\n" + - " License and any other pertinent obligations, then as a consequence you may\n" + - " not convey it at all. For example, if you agree to terms that obligate you\n" + - " to collect a royalty for further conveying from those to whom you convey\n" + - " the Program, the only way you could satisfy both those terms and this\n" + - " License would be to refrain entirely from conveying the Program.\n" + - " \n" + - " 13. Use with the GNU Affero General Public License.\n" + - " \n" + - " Notwithstanding any other provision of this License, you have\n" + - " permission to link or combine any covered work with a work licensed\n" + - " under version 3 of the GNU Affero General Public License into a single\n" + - " combined work, and to convey the resulting work. The terms of this\n" + - " License will continue to apply to the part which is the covered work,\n" + - " but the special requirements of the GNU Affero General Public License,\n" + - " section 13, concerning interaction through a network will apply to the\n" + - " combination as such.\n" + - " \n" + - " 14. Revised Versions of this License.\n" + - " \n" + - " The Free Software Foundation may publish revised and/or new versions of\n" + - " the GNU General Public License from time to time. Such new versions will\n" + - " be similar in spirit to the present version, but may differ in detail to\n" + - " address new problems or concerns.\n" + - " \n" + - " Each version is given a distinguishing version number. If the\n" + - " Program specifies that a certain numbered version of the GNU General\n" + - " Public License \"or any later version\" applies to it, you have the\n" + - " option of following the terms and conditions either of that numbered\n" + - " version or of any later version published by the Free Software\n" + - " Foundation. If the Program does not specify a version number of the\n" + - " GNU General Public License, you may choose any version ever published\n" + - " by the Free Software Foundation.\n" + - " \n" + - " If the Program specifies that a proxy can decide which future\n" + - " versions of the GNU General Public License can be used, that proxy's\n" + - " public statement of acceptance of a version permanently authorizes you\n" + - " to choose that version for the Program.\n" + - " \n" + - " Later license versions may give you additional or different\n" + - " permissions. However, no additional obligations are imposed on any\n" + - " author or copyright holder as a result of your choosing to follow a\n" + - " later version.\n" + - " \n" + - " 15. Disclaimer of Warranty.\n" + - " \n" + - " THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY\n" + - " APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT\n" + - " HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM \"AS IS\" WITHOUT WARRANTY\n" + - " OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO,\n" + - " THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR\n" + - " PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM\n" + - " IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF\n" + - " ALL NECESSARY SERVICING, REPAIR OR CORRECTION.\n" + - " \n" + - " 16. Limitation of Liability.\n" + - " \n" + - " IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING\n" + - " WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS\n" + - " THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY\n" + - " GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE\n" + - " USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF\n" + - " DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD\n" + - " PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS),\n" + - " EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF\n" + - " SUCH DAMAGES.\n" + - " \n" + - " 17. Interpretation of Sections 15 and 16.\n" + - " \n" + - " If the disclaimer of warranty and limitation of liability provided\n" + - " above cannot be given local legal effect according to their terms,\n" + - " reviewing courts shall apply local law that most closely approximates\n" + - " an absolute waiver of all civil liability in connection with the\n" + - " Program, unless a warranty or assumption of liability accompanies a\n" + - " copy of the Program in return for a fee.\n" + - " \n" + - " END OF TERMS AND CONDITIONS\n" + - " \n" + - " How to Apply These Terms to Your New Programs\n" + - " \n" + - " If you develop a new program, and you want it to be of the greatest\n" + - " possible use to the public, the best way to achieve this is to make it\n" + - " free software which everyone can redistribute and change under these terms.\n" + - " \n" + - " To do so, attach the following notices to the program. It is safest\n" + - " to attach them to the start of each source file to most effectively\n" + - " state the exclusion of warranty; and each file should have at least\n" + - " the \"copyright\" line and a pointer to where the full notice is found.\n" + - " \n" + - " \n" + - " Copyright (C) \n" + - " \n" + - " This program is free software: you can redistribute it and/or modify\n" + - " it under the terms of the GNU General Public License as published by\n" + - " the Free Software Foundation, either version 3 of the License, or\n" + - " (at your option) any later version.\n" + - " \n" + - " This program is distributed in the hope that it will be useful,\n" + - " but WITHOUT ANY WARRANTY; without even the implied warranty of\n" + - " MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the\n" + - " GNU General Public License for more details.\n" + - " \n" + - " You should have received a copy of the GNU General Public License\n" + - " along with this program. If not, see .\n" + - " \n" + - " Also add information on how to contact you by electronic and paper mail.\n" + - " \n" + - " If the program does terminal interaction, make it output a short\n" + - " notice like this when it starts in an interactive mode:\n" + - " \n" + - " Copyright (C) \n" + - " This program comes with ABSOLUTELY NO WARRANTY; for details type `show w'.\n" + - " This is free software, and you are welcome to redistribute it\n" + - " under certain conditions; type `show c' for details.\n" + - " \n" + - " The hypothetical commands `show w' and `show c' should show the appropriate\n" + - " parts of the General Public License. Of course, your program's commands\n" + - " might be different; for a GUI interface, you would use an \"about box\".\n" + - " \n" + - " You should also get your employer (if you work as a programmer) or school,\n" + - " if any, to sign a \"copyright disclaimer\" for the program, if necessary.\n" + - " For more information on this, and how to apply and follow the GNU GPL, see\n" + - " .\n" + - " \n" + - " The GNU General Public License does not permit incorporating your program\n" + - " into proprietary programs. If your program is a subroutine library, you\n" + - " may consider it more useful to permit linking proprietary applications with\n" + - " the library. If this is what you want to do, use the GNU Lesser General\n" + - " Public License instead of this License. But first, please read\n" + - " .\n" + - " \n" + - " \n" + - " Name: libquadmath\n" + - " Files: scipy/.dylibs/libquadmath*.so\n" + - " Description: dynamically linked to files compiled with gcc\n" + - " Availability: https://gcc.gnu.org/git/?p=gcc.git;a=tree;f=libquadmath\n" + - " License: LGPL-2.1-or-later\n" + - " \n" + - " GCC Quad-Precision Math Library\n" + - " Copyright (C) 2010-2019 Free Software Foundation, Inc.\n" + - " Written by Francois-Xavier Coudert \n" + - " \n" + - " This file is part of the libquadmath library.\n" + - " Libquadmath is free software; you can redistribute it and/or\n" + - " modify it under the terms of the GNU Library General Public\n" + - " License as published by the Free Software Foundation; either\n" + - " version 2.1 of the License, or (at your option) any later version.\n" + - " \n" + - " Libquadmath is distributed in the hope that it will be useful,\n" + - " but WITHOUT ANY WARRANTY; without even the implied warranty of\n" + - " MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU\n" + - " Lesser General Public License for more details.\n" + - " https://www.gnu.org/licenses/old-licenses/lgpl-2.1.html\n" + - "Location: /Users/abc/Library/Python/3.9/lib/python/site-packages\n" + - "Requires: numpy\n" + - "Required-by: gensim\n" + - "---\n" + - "Name: six\n" + - "Version: 1.16.0\n" + - "Summary: Python 2 and 3 compatibility utilities\n" + - "Home-page: https://github.com/benjaminp/six\n" + - "Author: Benjamin Peterson\n" + - "Author-email: benjamin@python.org\n" + - "License: MIT\n" + - "Location: /Users/abc/Library/Python/3.9/lib/python/site-packages\n" + - "Requires: \n" + - "Required-by: cycler, gensim, gTTS, python-dateutil, tweepy\n"; - } + static { + PIP_SHOW_LINES = "Name: altgraph\n" + "Version: 0.17.2\n" + + "Summary: Python graph (network) package\n" + + "Home-page: https://altgraph.readthedocs.io\n" + + "Author: Ronald Oussoren\n" + + "Author-email: ronaldoussoren@mac.com\n" + + "License: MIT\n" + + "Location: /Library/Developer/CommandLineTools/Library/Frameworks/Python3.framework/Versions/3.9/lib/python3.9/site-packages\n" + + "Requires: \n" + + "Required-by: macholib\n" + + "---\n" + + "Name: scipy\n" + + "Version: 1.11.3\n" + + "Summary: Fundamental algorithms for scientific computing in Python\n" + + "Home-page: https://scipy.org/\n" + + "Author: \n" + + "Author-email: \n" + + "License: Copyright (c) 2001-2002 Enthought, Inc. 2003-2023, SciPy Developers.\n" + + " All rights reserved.\n" + + " \n" + + " Redistribution and use in source and binary forms, with or without\n" + + " modification, are permitted provided that the following conditions\n" + + " are met:\n" + + " \n" + + " 1. Redistributions of source code must retain the above copyright\n" + + " notice, this list of conditions and the following disclaimer.\n" + + " \n" + + " 2. Redistributions in binary form must reproduce the above\n" + + " copyright notice, this list of conditions and the following\n" + + " disclaimer in the documentation and/or other materials provided\n" + + " with the distribution.\n" + + " \n" + + " 3. Neither the name of the copyright holder nor the names of its\n" + + " contributors may be used to endorse or promote products derived\n" + + " from this software without specific prior written permission.\n" + + " \n" + + " THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS\n" + + " \"AS IS\" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT\n" + + " LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR\n" + + " A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT\n" + + " OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,\n" + + " SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT\n" + + " LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,\n" + + " DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY\n" + + " THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT\n" + + " (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE\n" + + " OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.\n" + + " \n" + + " ----\n" + + " \n" + + " This binary distribution of SciPy also bundles the following software:\n" + + " \n" + + " \n" + + " Name: OpenBLAS\n" + + " Files: scipy/.dylibs/libopenblas*.so\n" + + " Description: bundled as a dynamically linked library\n" + + " Availability: https://github.com/OpenMathLib/OpenBLAS/\n" + + " License: BSD-3-Clause-Attribution\n" + + " Copyright (c) 2011-2014, The OpenBLAS Project\n" + + " All rights reserved.\n" + + " \n" + + " Redistribution and use in source and binary forms, with or without\n" + + " modification, are permitted provided that the following conditions are\n" + + " met:\n" + + " \n" + + " 1. Redistributions of source code must retain the above copyright\n" + + " notice, this list of conditions and the following disclaimer.\n" + + " \n" + + " 2. Redistributions in binary form must reproduce the above copyright\n" + + " notice, this list of conditions and the following disclaimer in\n" + + " the documentation and/or other materials provided with the\n" + + " distribution.\n" + + " 3. Neither the name of the OpenBLAS project nor the names of\n" + + " its contributors may be used to endorse or promote products\n" + + " derived from this software without specific prior written\n" + + " permission.\n" + + " \n" + + " THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS \"AS IS\"\n" + + " AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE\n" + + " IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE\n" + + " ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE\n" + + " LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL\n" + + " DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR\n" + + " SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER\n" + + " CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,\n" + + " OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE\n" + + " USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.\n" + + " \n" + + " \n" + + " Name: LAPACK\n" + + " Files: scipy/.dylibs/libopenblas*.so\n" + + " Description: bundled in OpenBLAS\n" + + " Availability: https://github.com/OpenMathLib/OpenBLAS/\n" + + " License: BSD-3-Clause-Attribution\n" + + " Copyright (c) 1992-2013 The University of Tennessee and The University\n" + + " of Tennessee Research Foundation. All rights\n" + + " reserved.\n" + + " Copyright (c) 2000-2013 The University of California Berkeley. All\n" + + " rights reserved.\n" + + " Copyright (c) 2006-2013 The University of Colorado Denver. All rights\n" + + " reserved.\n" + + " \n" + + " $COPYRIGHT$\n" + + " \n" + + " Additional copyrights may follow\n" + + " \n" + + " $HEADER$\n" + + " \n" + + " Redistribution and use in source and binary forms, with or without\n" + + " modification, are permitted provided that the following conditions are\n" + + " met:\n" + + " \n" + + " - Redistributions of source code must retain the above copyright\n" + + " notice, this list of conditions and the following disclaimer.\n" + + " \n" + + " - Redistributions in binary form must reproduce the above copyright\n" + + " notice, this list of conditions and the following disclaimer listed\n" + + " in this license in the documentation and/or other materials\n" + + " provided with the distribution.\n" + + " \n" + + " - Neither the name of the copyright holders nor the names of its\n" + + " contributors may be used to endorse or promote products derived from\n" + + " this software without specific prior written permission.\n" + + " \n" + + " The copyright holders provide no reassurances that the source code\n" + + " provided does not infringe any patent, copyright, or any other\n" + + " intellectual property rights of third parties. The copyright holders\n" + + " disclaim any liability to any recipient for claims brought against\n" + + " recipient by any third party for infringement of that parties\n" + + " intellectual property rights.\n" + + " \n" + + " THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS\n" + + " \"AS IS\" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT\n" + + " LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR\n" + + " A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT\n" + + " OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,\n" + + " SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT\n" + + " LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,\n" + + " DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY\n" + + " THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT\n" + + " (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE\n" + + " OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.\n" + + " \n" + + " \n" + + " Name: GCC runtime library\n" + + " Files: scipy/.dylibs/libgfortran*, scipy/.dylibs/libgcc*\n" + + " Description: dynamically linked to files compiled with gcc\n" + + " Availability: https://gcc.gnu.org/git/?p=gcc.git;a=tree;f=libgfortran\n" + + " License: GPL-3.0-with-GCC-exception\n" + + " Copyright (C) 2002-2017 Free Software Foundation, Inc.\n" + + " \n" + + " Libgfortran is free software; you can redistribute it and/or modify\n" + + " it under the terms of the GNU General Public License as published by\n" + + " the Free Software Foundation; either version 3, or (at your option)\n" + + " any later version.\n" + + " \n" + + " Libgfortran is distributed in the hope that it will be useful,\n" + + " but WITHOUT ANY WARRANTY; without even the implied warranty of\n" + + " MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the\n" + + " GNU General Public License for more details.\n" + + " \n" + + " Under Section 7 of GPL version 3, you are granted additional\n" + + " permissions described in the GCC Runtime Library Exception, version\n" + + " 3.1, as published by the Free Software Foundation.\n" + + " \n" + + " You should have received a copy of the GNU General Public License and\n" + + " a copy of the GCC Runtime Library Exception along with this program;\n" + + " see the files COPYING3 and COPYING.RUNTIME respectively. If not, see\n" + + " .\n" + + " \n" + + " ----\n" + + " \n" + + " Full text of license texts referred to above follows (that they are\n" + + " listed below does not necessarily imply the conditions apply to the\n" + + " present binary release):\n" + + " \n" + + " ----\n" + + " \n" + + " GCC RUNTIME LIBRARY EXCEPTION\n" + + " \n" + + " Version 3.1, 31 March 2009\n" + + " \n" + + " Copyright (C) 2009 Free Software Foundation, Inc. \n" + + " \n" + + " Everyone is permitted to copy and distribute verbatim copies of this\n" + + " license document, but changing it is not allowed.\n" + + " \n" + + " This GCC Runtime Library Exception (\"Exception\") is an additional\n" + + " permission under section 7 of the GNU General Public License, version\n" + + " 3 (\"GPLv3\"). It applies to a given file (the \"Runtime Library\") that\n" + + " bears a notice placed by the copyright holder of the file stating that\n" + + " the file is governed by GPLv3 along with this Exception.\n" + + " \n" + + " When you use GCC to compile a program, GCC may combine portions of\n" + + " certain GCC header files and runtime libraries with the compiled\n" + + " program. The purpose of this Exception is to allow compilation of\n" + + " non-GPL (including proprietary) programs to use, in this way, the\n" + + " header files and runtime libraries covered by this Exception.\n" + + " \n" + + " 0. Definitions.\n" + + " \n" + + " A file is an \"Independent Module\" if it either requires the Runtime\n" + + " Library for execution after a Compilation Process, or makes use of an\n" + + " interface provided by the Runtime Library, but is not otherwise based\n" + + " on the Runtime Library.\n" + + " \n" + + " \"GCC\" means a version of the GNU Compiler Collection, with or without\n" + + " modifications, governed by version 3 (or a specified later version) of\n" + + " the GNU General Public License (GPL) with the option of using any\n" + + " subsequent versions published by the FSF.\n" + + " \n" + + " \"GPL-compatible Software\" is software whose conditions of propagation,\n" + + " modification and use would permit combination with GCC in accord with\n" + + " the license of GCC.\n" + + " \n" + + " \"Target Code\" refers to output from any compiler for a real or virtual\n" + + " target processor architecture, in executable form or suitable for\n" + + " input to an assembler, loader, linker and/or execution\n" + + " phase. Notwithstanding that, Target Code does not include data in any\n" + + " format that is used as a compiler intermediate representation, or used\n" + + " for producing a compiler intermediate representation.\n" + + " \n" + + " The \"Compilation Process\" transforms code entirely represented in\n" + + " non-intermediate languages designed for human-written code, and/or in\n" + + " Java Virtual Machine byte code, into Target Code. Thus, for example,\n" + + " use of source code generators and preprocessors need not be considered\n" + + " part of the Compilation Process, since the Compilation Process can be\n" + + " understood as starting with the output of the generators or\n" + + " preprocessors.\n" + + " \n" + + " A Compilation Process is \"Eligible\" if it is done using GCC, alone or\n" + + " with other GPL-compatible software, or if it is done without using any\n" + + " work based on GCC. For example, using non-GPL-compatible Software to\n" + + " optimize any GCC intermediate representations would not qualify as an\n" + + " Eligible Compilation Process.\n" + + " \n" + + " 1. Grant of Additional Permission.\n" + + " \n" + + " You have permission to propagate a work of Target Code formed by\n" + + " combining the Runtime Library with Independent Modules, even if such\n" + + " propagation would otherwise violate the terms of GPLv3, provided that\n" + + " all Target Code was generated by Eligible Compilation Processes. You\n" + + " may then convey such a combination under terms of your choice,\n" + + " consistent with the licensing of the Independent Modules.\n" + + " \n" + + " 2. No Weakening of GCC Copyleft.\n" + + " \n" + + " The availability of this Exception does not imply any general\n" + + " presumption that third-party software is unaffected by the copyleft\n" + + " requirements of the license of GCC.\n" + + " \n" + + " ----\n" + + " \n" + + " GNU GENERAL PUBLIC LICENSE\n" + + " Version 3, 29 June 2007\n" + + " \n" + + " Copyright (C) 2007 Free Software Foundation, Inc. \n" + + " Everyone is permitted to copy and distribute verbatim copies\n" + + " of this license document, but changing it is not allowed.\n" + + " \n" + + " Preamble\n" + + " \n" + + " The GNU General Public License is a free, copyleft license for\n" + + " software and other kinds of works.\n" + + " \n" + + " The licenses for most software and other practical works are designed\n" + + " to take away your freedom to share and change the works. By contrast,\n" + + " the GNU General Public License is intended to guarantee your freedom to\n" + + " share and change all versions of a program--to make sure it remains free\n" + + " software for all its users. We, the Free Software Foundation, use the\n" + + " GNU General Public License for most of our software; it applies also to\n" + + " any other work released this way by its authors. You can apply it to\n" + + " your programs, too.\n" + + " \n" + + " When we speak of free software, we are referring to freedom, not\n" + + " price. Our General Public Licenses are designed to make sure that you\n" + + " have the freedom to distribute copies of free software (and charge for\n" + + " them if you wish), that you receive source code or can get it if you\n" + + " want it, that you can change the software or use pieces of it in new\n" + + " free programs, and that you know you can do these things.\n" + + " \n" + + " To protect your rights, we need to prevent others from denying you\n" + + " these rights or asking you to surrender the rights. Therefore, you have\n" + + " certain responsibilities if you distribute copies of the software, or if\n" + + " you modify it: responsibilities to respect the freedom of others.\n" + + " \n" + + " For example, if you distribute copies of such a program, whether\n" + + " gratis or for a fee, you must pass on to the recipients the same\n" + + " freedoms that you received. You must make sure that they, too, receive\n" + + " or can get the source code. And you must show them these terms so they\n" + + " know their rights.\n" + + " \n" + + " Developers that use the GNU GPL protect your rights with two steps:\n" + + " (1) assert copyright on the software, and (2) offer you this License\n" + + " giving you legal permission to copy, distribute and/or modify it.\n" + + " \n" + + " For the developers' and authors' protection, the GPL clearly explains\n" + + " that there is no warranty for this free software. For both users' and\n" + + " authors' sake, the GPL requires that modified versions be marked as\n" + + " changed, so that their problems will not be attributed erroneously to\n" + + " authors of previous versions.\n" + + " \n" + + " Some devices are designed to deny users access to install or run\n" + + " modified versions of the software inside them, although the manufacturer\n" + + " can do so. This is fundamentally incompatible with the aim of\n" + + " protecting users' freedom to change the software. The systematic\n" + + " pattern of such abuse occurs in the area of products for individuals to\n" + + " use, which is precisely where it is most unacceptable. Therefore, we\n" + + " have designed this version of the GPL to prohibit the practice for those\n" + + " products. If such problems arise substantially in other domains, we\n" + + " stand ready to extend this provision to those domains in future versions\n" + + " of the GPL, as needed to protect the freedom of users.\n" + + " \n" + + " Finally, every program is threatened constantly by software patents.\n" + + " States should not allow patents to restrict development and use of\n" + + " software on general-purpose computers, but in those that do, we wish to\n" + + " avoid the special danger that patents applied to a free program could\n" + + " make it effectively proprietary. To prevent this, the GPL assures that\n" + + " patents cannot be used to render the program non-free.\n" + + " \n" + + " The precise terms and conditions for copying, distribution and\n" + + " modification follow.\n" + + " \n" + + " TERMS AND CONDITIONS\n" + + " \n" + + " 0. Definitions.\n" + + " \n" + + " \"This License\" refers to version 3 of the GNU General Public License.\n" + + " \n" + + " \"Copyright\" also means copyright-like laws that apply to other kinds of\n" + + " works, such as semiconductor masks.\n" + + " \n" + + " \"The Program\" refers to any copyrightable work licensed under this\n" + + " License. Each licensee is addressed as \"you\". \"Licensees\" and\n" + + " \"recipients\" may be individuals or organizations.\n" + + " \n" + + " To \"modify\" a work means to copy from or adapt all or part of the work\n" + + " in a fashion requiring copyright permission, other than the making of an\n" + + " exact copy. The resulting work is called a \"modified version\" of the\n" + + " earlier work or a work \"based on\" the earlier work.\n" + + " \n" + + " A \"covered work\" means either the unmodified Program or a work based\n" + + " on the Program.\n" + + " \n" + + " To \"propagate\" a work means to do anything with it that, without\n" + + " permission, would make you directly or secondarily liable for\n" + + " infringement under applicable copyright law, except executing it on a\n" + + " computer or modifying a private copy. Propagation includes copying,\n" + + " distribution (with or without modification), making available to the\n" + + " public, and in some countries other activities as well.\n" + + " \n" + + " To \"convey\" a work means any kind of propagation that enables other\n" + + " parties to make or receive copies. Mere interaction with a user through\n" + + " a computer network, with no transfer of a copy, is not conveying.\n" + + " \n" + + " An interactive user interface displays \"Appropriate Legal Notices\"\n" + + " to the extent that it includes a convenient and prominently visible\n" + + " feature that (1) displays an appropriate copyright notice, and (2)\n" + + " tells the user that there is no warranty for the work (except to the\n" + + " extent that warranties are provided), that licensees may convey the\n" + + " work under this License, and how to view a copy of this License. If\n" + + " the interface presents a list of user commands or options, such as a\n" + + " menu, a prominent item in the list meets this criterion.\n" + + " \n" + + " 1. Source Code.\n" + + " \n" + + " The \"source code\" for a work means the preferred form of the work\n" + + " for making modifications to it. \"Object code\" means any non-source\n" + + " form of a work.\n" + + " \n" + + " A \"Standard Interface\" means an interface that either is an official\n" + + " standard defined by a recognized standards body, or, in the case of\n" + + " interfaces specified for a particular programming language, one that\n" + + " is widely used among developers working in that language.\n" + + " \n" + + " The \"System Libraries\" of an executable work include anything, other\n" + + " than the work as a whole, that (a) is included in the normal form of\n" + + " packaging a Major Component, but which is not part of that Major\n" + + " Component, and (b) serves only to enable use of the work with that\n" + + " Major Component, or to implement a Standard Interface for which an\n" + + " implementation is available to the public in source code form. A\n" + + " \"Major Component\", in this context, means a major essential component\n" + + " (kernel, window system, and so on) of the specific operating system\n" + + " (if any) on which the executable work runs, or a compiler used to\n" + + " produce the work, or an object code interpreter used to run it.\n" + + " \n" + + " The \"Corresponding Source\" for a work in object code form means all\n" + + " the source code needed to generate, install, and (for an executable\n" + + " work) run the object code and to modify the work, including scripts to\n" + + " control those activities. However, it does not include the work's\n" + + " System Libraries, or general-purpose tools or generally available free\n" + + " programs which are used unmodified in performing those activities but\n" + + " which are not part of the work. For example, Corresponding Source\n" + + " includes interface definition files associated with source files for\n" + + " the work, and the source code for shared libraries and dynamically\n" + + " linked subprograms that the work is specifically designed to require,\n" + + " such as by intimate data communication or control flow between those\n" + + " subprograms and other parts of the work.\n" + + " \n" + + " The Corresponding Source need not include anything that users\n" + + " can regenerate automatically from other parts of the Corresponding\n" + + " Source.\n" + + " \n" + + " The Corresponding Source for a work in source code form is that\n" + + " same work.\n" + + " \n" + + " 2. Basic Permissions.\n" + + " \n" + + " All rights granted under this License are granted for the term of\n" + + " copyright on the Program, and are irrevocable provided the stated\n" + + " conditions are met. This License explicitly affirms your unlimited\n" + + " permission to run the unmodified Program. The output from running a\n" + + " covered work is covered by this License only if the output, given its\n" + + " content, constitutes a covered work. This License acknowledges your\n" + + " rights of fair use or other equivalent, as provided by copyright law.\n" + + " \n" + + " You may make, run and propagate covered works that you do not\n" + + " convey, without conditions so long as your license otherwise remains\n" + + " in force. You may convey covered works to others for the sole purpose\n" + + " of having them make modifications exclusively for you, or provide you\n" + + " with facilities for running those works, provided that you comply with\n" + + " the terms of this License in conveying all material for which you do\n" + + " not control copyright. Those thus making or running the covered works\n" + + " for you must do so exclusively on your behalf, under your direction\n" + + " and control, on terms that prohibit them from making any copies of\n" + + " your copyrighted material outside their relationship with you.\n" + + " \n" + + " Conveying under any other circumstances is permitted solely under\n" + + " the conditions stated below. Sublicensing is not allowed; section 10\n" + + " makes it unnecessary.\n" + + " \n" + + " 3. Protecting Users' Legal Rights From Anti-Circumvention Law.\n" + + " \n" + + " No covered work shall be deemed part of an effective technological\n" + + " measure under any applicable law fulfilling obligations under article\n" + + " 11 of the WIPO copyright treaty adopted on 20 December 1996, or\n" + + " similar laws prohibiting or restricting circumvention of such\n" + + " measures.\n" + + " \n" + + " When you convey a covered work, you waive any legal power to forbid\n" + + " circumvention of technological measures to the extent such circumvention\n" + + " is effected by exercising rights under this License with respect to\n" + + " the covered work, and you disclaim any intention to limit operation or\n" + + " modification of the work as a means of enforcing, against the work's\n" + + " users, your or third parties' legal rights to forbid circumvention of\n" + + " technological measures.\n" + + " \n" + + " 4. Conveying Verbatim Copies.\n" + + " \n" + + " You may convey verbatim copies of the Program's source code as you\n" + + " receive it, in any medium, provided that you conspicuously and\n" + + " appropriately publish on each copy an appropriate copyright notice;\n" + + " keep intact all notices stating that this License and any\n" + + " non-permissive terms added in accord with section 7 apply to the code;\n" + + " keep intact all notices of the absence of any warranty; and give all\n" + + " recipients a copy of this License along with the Program.\n" + + " \n" + + " You may charge any price or no price for each copy that you convey,\n" + + " and you may offer support or warranty protection for a fee.\n" + + " \n" + + " 5. Conveying Modified Source Versions.\n" + + " \n" + + " You may convey a work based on the Program, or the modifications to\n" + + " produce it from the Program, in the form of source code under the\n" + + " terms of section 4, provided that you also meet all of these conditions:\n" + + " \n" + + " a) The work must carry prominent notices stating that you modified\n" + + " it, and giving a relevant date.\n" + + " \n" + + " b) The work must carry prominent notices stating that it is\n" + + " released under this License and any conditions added under section\n" + + " 7. This requirement modifies the requirement in section 4 to\n" + + " \"keep intact all notices\".\n" + + " \n" + + " c) You must license the entire work, as a whole, under this\n" + + " License to anyone who comes into possession of a copy. This\n" + + " License will therefore apply, along with any applicable section 7\n" + + " additional terms, to the whole of the work, and all its parts,\n" + + " regardless of how they are packaged. This License gives no\n" + + " permission to license the work in any other way, but it does not\n" + + " invalidate such permission if you have separately received it.\n" + + " \n" + + " d) If the work has interactive user interfaces, each must display\n" + + " Appropriate Legal Notices; however, if the Program has interactive\n" + + " interfaces that do not display Appropriate Legal Notices, your\n" + + " work need not make them do so.\n" + + " \n" + + " A compilation of a covered work with other separate and independent\n" + + " works, which are not by their nature extensions of the covered work,\n" + + " and which are not combined with it such as to form a larger program,\n" + + " in or on a volume of a storage or distribution medium, is called an\n" + + " \"aggregate\" if the compilation and its resulting copyright are not\n" + + " used to limit the access or legal rights of the compilation's users\n" + + " beyond what the individual works permit. Inclusion of a covered work\n" + + " in an aggregate does not cause this License to apply to the other\n" + + " parts of the aggregate.\n" + + " \n" + + " 6. Conveying Non-Source Forms.\n" + + " \n" + + " You may convey a covered work in object code form under the terms\n" + + " of sections 4 and 5, provided that you also convey the\n" + + " machine-readable Corresponding Source under the terms of this License,\n" + + " in one of these ways:\n" + + " \n" + + " a) Convey the object code in, or embodied in, a physical product\n" + + " (including a physical distribution medium), accompanied by the\n" + + " Corresponding Source fixed on a durable physical medium\n" + + " customarily used for software interchange.\n" + + " \n" + + " b) Convey the object code in, or embodied in, a physical product\n" + + " (including a physical distribution medium), accompanied by a\n" + + " written offer, valid for at least three years and valid for as\n" + + " long as you offer spare parts or customer support for that product\n" + + " model, to give anyone who possesses the object code either (1) a\n" + + " copy of the Corresponding Source for all the software in the\n" + + " product that is covered by this License, on a durable physical\n" + + " medium customarily used for software interchange, for a price no\n" + + " more than your reasonable cost of physically performing this\n" + + " conveying of source, or (2) access to copy the\n" + + " Corresponding Source from a network server at no charge.\n" + + " \n" + + " c) Convey individual copies of the object code with a copy of the\n" + + " written offer to provide the Corresponding Source. This\n" + + " alternative is allowed only occasionally and noncommercially, and\n" + + " only if you received the object code with such an offer, in accord\n" + + " with subsection 6b.\n" + + " \n" + + " d) Convey the object code by offering access from a designated\n" + + " place (gratis or for a charge), and offer equivalent access to the\n" + + " Corresponding Source in the same way through the same place at no\n" + + " further charge. You need not require recipients to copy the\n" + + " Corresponding Source along with the object code. If the place to\n" + + " copy the object code is a network server, the Corresponding Source\n" + + " may be on a different server (operated by you or a third party)\n" + + " that supports equivalent copying facilities, provided you maintain\n" + + " clear directions next to the object code saying where to find the\n" + + " Corresponding Source. Regardless of what server hosts the\n" + + " Corresponding Source, you remain obligated to ensure that it is\n" + + " available for as long as needed to satisfy these requirements.\n" + + " \n" + + " e) Convey the object code using peer-to-peer transmission, provided\n" + + " you inform other peers where the object code and Corresponding\n" + + " Source of the work are being offered to the general public at no\n" + + " charge under subsection 6d.\n" + + " \n" + + " A separable portion of the object code, whose source code is excluded\n" + + " from the Corresponding Source as a System Library, need not be\n" + + " included in conveying the object code work.\n" + + " \n" + + " A \"User Product\" is either (1) a \"consumer product\", which means any\n" + + " tangible personal property which is normally used for personal, family,\n" + + " or household purposes, or (2) anything designed or sold for incorporation\n" + + " into a dwelling. In determining whether a product is a consumer product,\n" + + " doubtful cases shall be resolved in favor of coverage. For a particular\n" + + " product received by a particular user, \"normally used\" refers to a\n" + + " typical or common use of that class of product, regardless of the status\n" + + " of the particular user or of the way in which the particular user\n" + + " actually uses, or expects or is expected to use, the product. A product\n" + + " is a consumer product regardless of whether the product has substantial\n" + + " commercial, industrial or non-consumer uses, unless such uses represent\n" + + " the only significant mode of use of the product.\n" + + " \n" + + " \"Installation Information\" for a User Product means any methods,\n" + + " procedures, authorization keys, or other information required to install\n" + + " and execute modified versions of a covered work in that User Product from\n" + + " a modified version of its Corresponding Source. The information must\n" + + " suffice to ensure that the continued functioning of the modified object\n" + + " code is in no case prevented or interfered with solely because\n" + + " modification has been made.\n" + + " \n" + + " If you convey an object code work under this section in, or with, or\n" + + " specifically for use in, a User Product, and the conveying occurs as\n" + + " part of a transaction in which the right of possession and use of the\n" + + " User Product is transferred to the recipient in perpetuity or for a\n" + + " fixed term (regardless of how the transaction is characterized), the\n" + + " Corresponding Source conveyed under this section must be accompanied\n" + + " by the Installation Information. But this requirement does not apply\n" + + " if neither you nor any third party retains the ability to install\n" + + " modified object code on the User Product (for example, the work has\n" + + " been installed in ROM).\n" + + " \n" + + " The requirement to provide Installation Information does not include a\n" + + " requirement to continue to provide support service, warranty, or updates\n" + + " for a work that has been modified or installed by the recipient, or for\n" + + " the User Product in which it has been modified or installed. Access to a\n" + + " network may be denied when the modification itself materially and\n" + + " adversely affects the operation of the network or violates the rules and\n" + + " protocols for communication across the network.\n" + + " \n" + + " Corresponding Source conveyed, and Installation Information provided,\n" + + " in accord with this section must be in a format that is publicly\n" + + " documented (and with an implementation available to the public in\n" + + " source code form), and must require no special password or key for\n" + + " unpacking, reading or copying.\n" + + " \n" + + " 7. Additional Terms.\n" + + " \n" + + " \"Additional permissions\" are terms that supplement the terms of this\n" + + " License by making exceptions from one or more of its conditions.\n" + + " Additional permissions that are applicable to the entire Program shall\n" + + " be treated as though they were included in this License, to the extent\n" + + " that they are valid under applicable law. If additional permissions\n" + + " apply only to part of the Program, that part may be used separately\n" + + " under those permissions, but the entire Program remains governed by\n" + + " this License without regard to the additional permissions.\n" + + " \n" + + " When you convey a copy of a covered work, you may at your option\n" + + " remove any additional permissions from that copy, or from any part of\n" + + " it. (Additional permissions may be written to require their own\n" + + " removal in certain cases when you modify the work.) You may place\n" + + " additional permissions on material, added by you to a covered work,\n" + + " for which you have or can give appropriate copyright permission.\n" + + " \n" + + " Notwithstanding any other provision of this License, for material you\n" + + " add to a covered work, you may (if authorized by the copyright holders of\n" + + " that material) supplement the terms of this License with terms:\n" + + " \n" + + " a) Disclaiming warranty or limiting liability differently from the\n" + + " terms of sections 15 and 16 of this License; or\n" + + " \n" + + " b) Requiring preservation of specified reasonable legal notices or\n" + + " author attributions in that material or in the Appropriate Legal\n" + + " Notices displayed by works containing it; or\n" + + " \n" + + " c) Prohibiting misrepresentation of the origin of that material, or\n" + + " requiring that modified versions of such material be marked in\n" + + " reasonable ways as different from the original version; or\n" + + " \n" + + " d) Limiting the use for publicity purposes of names of licensors or\n" + + " authors of the material; or\n" + + " \n" + + " e) Declining to grant rights under trademark law for use of some\n" + + " trade names, trademarks, or service marks; or\n" + + " \n" + + " f) Requiring indemnification of licensors and authors of that\n" + + " material by anyone who conveys the material (or modified versions of\n" + + " it) with contractual assumptions of liability to the recipient, for\n" + + " any liability that these contractual assumptions directly impose on\n" + + " those licensors and authors.\n" + + " \n" + + " All other non-permissive additional terms are considered \"further\n" + + " restrictions\" within the meaning of section 10. If the Program as you\n" + + " received it, or any part of it, contains a notice stating that it is\n" + + " governed by this License along with a term that is a further\n" + + " restriction, you may remove that term. If a license document contains\n" + + " a further restriction but permits relicensing or conveying under this\n" + + " License, you may add to a covered work material governed by the terms\n" + + " of that license document, provided that the further restriction does\n" + + " not survive such relicensing or conveying.\n" + + " \n" + + " If you add terms to a covered work in accord with this section, you\n" + + " must place, in the relevant source files, a statement of the\n" + + " additional terms that apply to those files, or a notice indicating\n" + + " where to find the applicable terms.\n" + + " \n" + + " Additional terms, permissive or non-permissive, may be stated in the\n" + + " form of a separately written license, or stated as exceptions;\n" + + " the above requirements apply either way.\n" + + " \n" + + " 8. Termination.\n" + + " \n" + + " You may not propagate or modify a covered work except as expressly\n" + + " provided under this License. Any attempt otherwise to propagate or\n" + + " modify it is void, and will automatically terminate your rights under\n" + + " this License (including any patent licenses granted under the third\n" + + " paragraph of section 11).\n" + + " \n" + + " However, if you cease all violation of this License, then your\n" + + " license from a particular copyright holder is reinstated (a)\n" + + " provisionally, unless and until the copyright holder explicitly and\n" + + " finally terminates your license, and (b) permanently, if the copyright\n" + + " holder fails to notify you of the violation by some reasonable means\n" + + " prior to 60 days after the cessation.\n" + + " \n" + + " Moreover, your license from a particular copyright holder is\n" + + " reinstated permanently if the copyright holder notifies you of the\n" + + " violation by some reasonable means, this is the first time you have\n" + + " received notice of violation of this License (for any work) from that\n" + + " copyright holder, and you cure the violation prior to 30 days after\n" + + " your receipt of the notice.\n" + + " \n" + + " Termination of your rights under this section does not terminate the\n" + + " licenses of parties who have received copies or rights from you under\n" + + " this License. If your rights have been terminated and not permanently\n" + + " reinstated, you do not qualify to receive new licenses for the same\n" + + " material under section 10.\n" + + " \n" + + " 9. Acceptance Not Required for Having Copies.\n" + + " \n" + + " You are not required to accept this License in order to receive or\n" + + " run a copy of the Program. Ancillary propagation of a covered work\n" + + " occurring solely as a consequence of using peer-to-peer transmission\n" + + " to receive a copy likewise does not require acceptance. However,\n" + + " nothing other than this License grants you permission to propagate or\n" + + " modify any covered work. These actions infringe copyright if you do\n" + + " not accept this License. Therefore, by modifying or propagating a\n" + + " covered work, you indicate your acceptance of this License to do so.\n" + + " \n" + + " 10. Automatic Licensing of Downstream Recipients.\n" + + " \n" + + " Each time you convey a covered work, the recipient automatically\n" + + " receives a license from the original licensors, to run, modify and\n" + + " propagate that work, subject to this License. You are not responsible\n" + + " for enforcing compliance by third parties with this License.\n" + + " \n" + + " An \"entity transaction\" is a transaction transferring control of an\n" + + " organization, or substantially all assets of one, or subdividing an\n" + + " organization, or merging organizations. If propagation of a covered\n" + + " work results from an entity transaction, each party to that\n" + + " transaction who receives a copy of the work also receives whatever\n" + + " licenses to the work the party's predecessor in interest had or could\n" + + " give under the previous paragraph, plus a right to possession of the\n" + + " Corresponding Source of the work from the predecessor in interest, if\n" + + " the predecessor has it or can get it with reasonable efforts.\n" + + " \n" + + " You may not impose any further restrictions on the exercise of the\n" + + " rights granted or affirmed under this License. For example, you may\n" + + " not impose a license fee, royalty, or other charge for exercise of\n" + + " rights granted under this License, and you may not initiate litigation\n" + + " (including a cross-claim or counterclaim in a lawsuit) alleging that\n" + + " any patent claim is infringed by making, using, selling, offering for\n" + + " sale, or importing the Program or any portion of it.\n" + + " \n" + + " 11. Patents.\n" + + " \n" + + " A \"contributor\" is a copyright holder who authorizes use under this\n" + + " License of the Program or a work on which the Program is based. The\n" + + " work thus licensed is called the contributor's \"contributor version\".\n" + + " \n" + + " A contributor's \"essential patent claims\" are all patent claims\n" + + " owned or controlled by the contributor, whether already acquired or\n" + + " hereafter acquired, that would be infringed by some manner, permitted\n" + + " by this License, of making, using, or selling its contributor version,\n" + + " but do not include claims that would be infringed only as a\n" + + " consequence of further modification of the contributor version. For\n" + + " purposes of this definition, \"control\" includes the right to grant\n" + + " patent sublicenses in a manner consistent with the requirements of\n" + + " this License.\n" + + " \n" + + " Each contributor grants you a non-exclusive, worldwide, royalty-free\n" + + " patent license under the contributor's essential patent claims, to\n" + + " make, use, sell, offer for sale, import and otherwise run, modify and\n" + + " propagate the contents of its contributor version.\n" + + " \n" + + " In the following three paragraphs, a \"patent license\" is any express\n" + + " agreement or commitment, however denominated, not to enforce a patent\n" + + " (such as an express permission to practice a patent or covenant not to\n" + + " sue for patent infringement). To \"grant\" such a patent license to a\n" + + " party means to make such an agreement or commitment not to enforce a\n" + + " patent against the party.\n" + + " \n" + + " If you convey a covered work, knowingly relying on a patent license,\n" + + " and the Corresponding Source of the work is not available for anyone\n" + + " to copy, free of charge and under the terms of this License, through a\n" + + " publicly available network server or other readily accessible means,\n" + + " then you must either (1) cause the Corresponding Source to be so\n" + + " available, or (2) arrange to deprive yourself of the benefit of the\n" + + " patent license for this particular work, or (3) arrange, in a manner\n" + + " consistent with the requirements of this License, to extend the patent\n" + + " license to downstream recipients. \"Knowingly relying\" means you have\n" + + " actual knowledge that, but for the patent license, your conveying the\n" + + " covered work in a country, or your recipient's use of the covered work\n" + + " in a country, would infringe one or more identifiable patents in that\n" + + " country that you have reason to believe are valid.\n" + + " \n" + + " If, pursuant to or in connection with a single transaction or\n" + + " arrangement, you convey, or propagate by procuring conveyance of, a\n" + + " covered work, and grant a patent license to some of the parties\n" + + " receiving the covered work authorizing them to use, propagate, modify\n" + + " or convey a specific copy of the covered work, then the patent license\n" + + " you grant is automatically extended to all recipients of the covered\n" + + " work and works based on it.\n" + + " \n" + + " A patent license is \"discriminatory\" if it does not include within\n" + + " the scope of its coverage, prohibits the exercise of, or is\n" + + " conditioned on the non-exercise of one or more of the rights that are\n" + + " specifically granted under this License. You may not convey a covered\n" + + " work if you are a party to an arrangement with a third party that is\n" + + " in the business of distributing software, under which you make payment\n" + + " to the third party based on the extent of your activity of conveying\n" + + " the work, and under which the third party grants, to any of the\n" + + " parties who would receive the covered work from you, a discriminatory\n" + + " patent license (a) in connection with copies of the covered work\n" + + " conveyed by you (or copies made from those copies), or (b) primarily\n" + + " for and in connection with specific products or compilations that\n" + + " contain the covered work, unless you entered into that arrangement,\n" + + " or that patent license was granted, prior to 28 March 2007.\n" + + " \n" + + " Nothing in this License shall be construed as excluding or limiting\n" + + " any implied license or other defenses to infringement that may\n" + + " otherwise be available to you under applicable patent law.\n" + + " \n" + + " 12. No Surrender of Others' Freedom.\n" + + " \n" + + " If conditions are imposed on you (whether by court order, agreement or\n" + + " otherwise) that contradict the conditions of this License, they do not\n" + + " excuse you from the conditions of this License. If you cannot convey a\n" + + " covered work so as to satisfy simultaneously your obligations under this\n" + + " License and any other pertinent obligations, then as a consequence you may\n" + + " not convey it at all. For example, if you agree to terms that obligate you\n" + + " to collect a royalty for further conveying from those to whom you convey\n" + + " the Program, the only way you could satisfy both those terms and this\n" + + " License would be to refrain entirely from conveying the Program.\n" + + " \n" + + " 13. Use with the GNU Affero General Public License.\n" + + " \n" + + " Notwithstanding any other provision of this License, you have\n" + + " permission to link or combine any covered work with a work licensed\n" + + " under version 3 of the GNU Affero General Public License into a single\n" + + " combined work, and to convey the resulting work. The terms of this\n" + + " License will continue to apply to the part which is the covered work,\n" + + " but the special requirements of the GNU Affero General Public License,\n" + + " section 13, concerning interaction through a network will apply to the\n" + + " combination as such.\n" + + " \n" + + " 14. Revised Versions of this License.\n" + + " \n" + + " The Free Software Foundation may publish revised and/or new versions of\n" + + " the GNU General Public License from time to time. Such new versions will\n" + + " be similar in spirit to the present version, but may differ in detail to\n" + + " address new problems or concerns.\n" + + " \n" + + " Each version is given a distinguishing version number. If the\n" + + " Program specifies that a certain numbered version of the GNU General\n" + + " Public License \"or any later version\" applies to it, you have the\n" + + " option of following the terms and conditions either of that numbered\n" + + " version or of any later version published by the Free Software\n" + + " Foundation. If the Program does not specify a version number of the\n" + + " GNU General Public License, you may choose any version ever published\n" + + " by the Free Software Foundation.\n" + + " \n" + + " If the Program specifies that a proxy can decide which future\n" + + " versions of the GNU General Public License can be used, that proxy's\n" + + " public statement of acceptance of a version permanently authorizes you\n" + + " to choose that version for the Program.\n" + + " \n" + + " Later license versions may give you additional or different\n" + + " permissions. However, no additional obligations are imposed on any\n" + + " author or copyright holder as a result of your choosing to follow a\n" + + " later version.\n" + + " \n" + + " 15. Disclaimer of Warranty.\n" + + " \n" + + " THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY\n" + + " APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT\n" + + " HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM \"AS IS\" WITHOUT WARRANTY\n" + + " OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO,\n" + + " THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR\n" + + " PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM\n" + + " IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF\n" + + " ALL NECESSARY SERVICING, REPAIR OR CORRECTION.\n" + + " \n" + + " 16. Limitation of Liability.\n" + + " \n" + + " IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING\n" + + " WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS\n" + + " THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY\n" + + " GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE\n" + + " USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF\n" + + " DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD\n" + + " PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS),\n" + + " EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF\n" + + " SUCH DAMAGES.\n" + + " \n" + + " 17. Interpretation of Sections 15 and 16.\n" + + " \n" + + " If the disclaimer of warranty and limitation of liability provided\n" + + " above cannot be given local legal effect according to their terms,\n" + + " reviewing courts shall apply local law that most closely approximates\n" + + " an absolute waiver of all civil liability in connection with the\n" + + " Program, unless a warranty or assumption of liability accompanies a\n" + + " copy of the Program in return for a fee.\n" + + " \n" + + " END OF TERMS AND CONDITIONS\n" + + " \n" + + " How to Apply These Terms to Your New Programs\n" + + " \n" + + " If you develop a new program, and you want it to be of the greatest\n" + + " possible use to the public, the best way to achieve this is to make it\n" + + " free software which everyone can redistribute and change under these terms.\n" + + " \n" + + " To do so, attach the following notices to the program. It is safest\n" + + " to attach them to the start of each source file to most effectively\n" + + " state the exclusion of warranty; and each file should have at least\n" + + " the \"copyright\" line and a pointer to where the full notice is found.\n" + + " \n" + + " \n" + + " Copyright (C) \n" + + " \n" + + " This program is free software: you can redistribute it and/or modify\n" + + " it under the terms of the GNU General Public License as published by\n" + + " the Free Software Foundation, either version 3 of the License, or\n" + + " (at your option) any later version.\n" + + " \n" + + " This program is distributed in the hope that it will be useful,\n" + + " but WITHOUT ANY WARRANTY; without even the implied warranty of\n" + + " MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the\n" + + " GNU General Public License for more details.\n" + + " \n" + + " You should have received a copy of the GNU General Public License\n" + + " along with this program. If not, see .\n" + + " \n" + + " Also add information on how to contact you by electronic and paper mail.\n" + + " \n" + + " If the program does terminal interaction, make it output a short\n" + + " notice like this when it starts in an interactive mode:\n" + + " \n" + + " Copyright (C) \n" + + " This program comes with ABSOLUTELY NO WARRANTY; for details type `show w'.\n" + + " This is free software, and you are welcome to redistribute it\n" + + " under certain conditions; type `show c' for details.\n" + + " \n" + + " The hypothetical commands `show w' and `show c' should show the appropriate\n" + + " parts of the General Public License. Of course, your program's commands\n" + + " might be different; for a GUI interface, you would use an \"about box\".\n" + + " \n" + + " You should also get your employer (if you work as a programmer) or school,\n" + + " if any, to sign a \"copyright disclaimer\" for the program, if necessary.\n" + + " For more information on this, and how to apply and follow the GNU GPL, see\n" + + " .\n" + + " \n" + + " The GNU General Public License does not permit incorporating your program\n" + + " into proprietary programs. If your program is a subroutine library, you\n" + + " may consider it more useful to permit linking proprietary applications with\n" + + " the library. If this is what you want to do, use the GNU Lesser General\n" + + " Public License instead of this License. But first, please read\n" + + " .\n" + + " \n" + + " \n" + + " Name: libquadmath\n" + + " Files: scipy/.dylibs/libquadmath*.so\n" + + " Description: dynamically linked to files compiled with gcc\n" + + " Availability: https://gcc.gnu.org/git/?p=gcc.git;a=tree;f=libquadmath\n" + + " License: LGPL-2.1-or-later\n" + + " \n" + + " GCC Quad-Precision Math Library\n" + + " Copyright (C) 2010-2019 Free Software Foundation, Inc.\n" + + " Written by Francois-Xavier Coudert \n" + + " \n" + + " This file is part of the libquadmath library.\n" + + " Libquadmath is free software; you can redistribute it and/or\n" + + " modify it under the terms of the GNU Library General Public\n" + + " License as published by the Free Software Foundation; either\n" + + " version 2.1 of the License, or (at your option) any later version.\n" + + " \n" + + " Libquadmath is distributed in the hope that it will be useful,\n" + + " but WITHOUT ANY WARRANTY; without even the implied warranty of\n" + + " MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU\n" + + " Lesser General Public License for more details.\n" + + " https://www.gnu.org/licenses/old-licenses/lgpl-2.1.html\n" + + "Location: /Users/abc/Library/Python/3.9/lib/python/site-packages\n" + + "Requires: numpy\n" + + "Required-by: gensim\n" + + "---\n" + + "Name: six\n" + + "Version: 1.16.0\n" + + "Summary: Python 2 and 3 compatibility utilities\n" + + "Home-page: https://github.com/benjaminp/six\n" + + "Author: Benjamin Peterson\n" + + "Author-email: benjamin@python.org\n" + + "License: MIT\n" + + "Location: /Users/abc/Library/Python/3.9/lib/python/site-packages\n" + + "Requires: \n" + + "Required-by: cycler, gensim, gTTS, python-dateutil, tweepy\n"; + } - private static final List EXPECTED_PIP_SHOW_RESULTS = new LinkedList<>(); + private static final List EXPECTED_PIP_SHOW_RESULTS = new LinkedList<>(); - static { - EXPECTED_PIP_SHOW_RESULTS.add("Name: altgraph\n" + - "Version: 0.17.2\n" + - "Summary: Python graph (network) package\n" + - "Home-page: https://altgraph.readthedocs.io\n" + - "Author: Ronald Oussoren\n" + - "Author-email: ronaldoussoren@mac.com\n" + - "License: MIT\n" + - "Location: /Library/Developer/CommandLineTools/Library/Frameworks/Python3.framework/Versions/3.9/lib/python3.9/site-packages\n" + - "Requires: \n" + - "Required-by: macholib"); + static { + EXPECTED_PIP_SHOW_RESULTS.add("Name: altgraph\n" + "Version: 0.17.2\n" + + "Summary: Python graph (network) package\n" + + "Home-page: https://altgraph.readthedocs.io\n" + + "Author: Ronald Oussoren\n" + + "Author-email: ronaldoussoren@mac.com\n" + + "License: MIT\n" + + "Location: /Library/Developer/CommandLineTools/Library/Frameworks/Python3.framework/Versions/3.9/lib/python3.9/site-packages\n" + + "Requires: \n" + + "Required-by: macholib"); - EXPECTED_PIP_SHOW_RESULTS.add("Name: scipy\n" + - "Version: 1.11.3\n" + - "Summary: Fundamental algorithms for scientific computing in Python\n" + - "Home-page: https://scipy.org/\n" + - "Author: \n" + - "Author-email: \n" + - "License: Copyright (c) 2001-2002 Enthought, Inc. 2003-2023, SciPy Developers.\n" + - " All rights reserved.\n" + - " \n" + - " Redistribution and use in source and binary forms, with or without\n" + - " modification, are permitted provided that the following conditions\n" + - " are met:\n" + - " \n" + - " 1. Redistributions of source code must retain the above copyright\n" + - " notice, this list of conditions and the following disclaimer.\n" + - " \n" + - " 2. Redistributions in binary form must reproduce the above\n" + - " copyright notice, this list of conditions and the following\n" + - " disclaimer in the documentation and/or other materials provided\n" + - " with the distribution.\n" + - " \n" + - " 3. Neither the name of the copyright holder nor the names of its\n" + - " contributors may be used to endorse or promote products derived\n" + - " from this software without specific prior written permission.\n" + - " \n" + - " THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS\n" + - " \"AS IS\" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT\n" + - " LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR\n" + - " A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT\n" + - " OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,\n" + - " SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT\n" + - " LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,\n" + - " DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY\n" + - " THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT\n" + - " (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE\n" + - " OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.\n" + - " \n" + - " ----\n" + - " \n" + - " This binary distribution of SciPy also bundles the following software:\n" + - " \n" + - " \n" + - " Name: OpenBLAS\n" + - " Files: scipy/.dylibs/libopenblas*.so\n" + - " Description: bundled as a dynamically linked library\n" + - " Availability: https://github.com/OpenMathLib/OpenBLAS/\n" + - " License: BSD-3-Clause-Attribution\n" + - " Copyright (c) 2011-2014, The OpenBLAS Project\n" + - " All rights reserved.\n" + - " \n" + - " Redistribution and use in source and binary forms, with or without\n" + - " modification, are permitted provided that the following conditions are\n" + - " met:\n" + - " \n" + - " 1. Redistributions of source code must retain the above copyright\n" + - " notice, this list of conditions and the following disclaimer.\n" + - " \n" + - " 2. Redistributions in binary form must reproduce the above copyright\n" + - " notice, this list of conditions and the following disclaimer in\n" + - " the documentation and/or other materials provided with the\n" + - " distribution.\n" + - " 3. Neither the name of the OpenBLAS project nor the names of\n" + - " its contributors may be used to endorse or promote products\n" + - " derived from this software without specific prior written\n" + - " permission.\n" + - " \n" + - " THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS \"AS IS\"\n" + - " AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE\n" + - " IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE\n" + - " ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE\n" + - " LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL\n" + - " DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR\n" + - " SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER\n" + - " CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,\n" + - " OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE\n" + - " USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.\n" + - " \n" + - " \n" + - " Name: LAPACK\n" + - " Files: scipy/.dylibs/libopenblas*.so\n" + - " Description: bundled in OpenBLAS\n" + - " Availability: https://github.com/OpenMathLib/OpenBLAS/\n" + - " License: BSD-3-Clause-Attribution\n" + - " Copyright (c) 1992-2013 The University of Tennessee and The University\n" + - " of Tennessee Research Foundation. All rights\n" + - " reserved.\n" + - " Copyright (c) 2000-2013 The University of California Berkeley. All\n" + - " rights reserved.\n" + - " Copyright (c) 2006-2013 The University of Colorado Denver. All rights\n" + - " reserved.\n" + - " \n" + - " $COPYRIGHT$\n" + - " \n" + - " Additional copyrights may follow\n" + - " \n" + - " $HEADER$\n" + - " \n" + - " Redistribution and use in source and binary forms, with or without\n" + - " modification, are permitted provided that the following conditions are\n" + - " met:\n" + - " \n" + - " - Redistributions of source code must retain the above copyright\n" + - " notice, this list of conditions and the following disclaimer.\n" + - " \n" + - " - Redistributions in binary form must reproduce the above copyright\n" + - " notice, this list of conditions and the following disclaimer listed\n" + - " in this license in the documentation and/or other materials\n" + - " provided with the distribution.\n" + - " \n" + - " - Neither the name of the copyright holders nor the names of its\n" + - " contributors may be used to endorse or promote products derived from\n" + - " this software without specific prior written permission.\n" + - " \n" + - " The copyright holders provide no reassurances that the source code\n" + - " provided does not infringe any patent, copyright, or any other\n" + - " intellectual property rights of third parties. The copyright holders\n" + - " disclaim any liability to any recipient for claims brought against\n" + - " recipient by any third party for infringement of that parties\n" + - " intellectual property rights.\n" + - " \n" + - " THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS\n" + - " \"AS IS\" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT\n" + - " LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR\n" + - " A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT\n" + - " OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,\n" + - " SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT\n" + - " LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,\n" + - " DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY\n" + - " THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT\n" + - " (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE\n" + - " OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.\n" + - " \n" + - " \n" + - " Name: GCC runtime library\n" + - " Files: scipy/.dylibs/libgfortran*, scipy/.dylibs/libgcc*\n" + - " Description: dynamically linked to files compiled with gcc\n" + - " Availability: https://gcc.gnu.org/git/?p=gcc.git;a=tree;f=libgfortran\n" + - " License: GPL-3.0-with-GCC-exception\n" + - " Copyright (C) 2002-2017 Free Software Foundation, Inc.\n" + - " \n" + - " Libgfortran is free software; you can redistribute it and/or modify\n" + - " it under the terms of the GNU General Public License as published by\n" + - " the Free Software Foundation; either version 3, or (at your option)\n" + - " any later version.\n" + - " \n" + - " Libgfortran is distributed in the hope that it will be useful,\n" + - " but WITHOUT ANY WARRANTY; without even the implied warranty of\n" + - " MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the\n" + - " GNU General Public License for more details.\n" + - " \n" + - " Under Section 7 of GPL version 3, you are granted additional\n" + - " permissions described in the GCC Runtime Library Exception, version\n" + - " 3.1, as published by the Free Software Foundation.\n" + - " \n" + - " You should have received a copy of the GNU General Public License and\n" + - " a copy of the GCC Runtime Library Exception along with this program;\n" + - " see the files COPYING3 and COPYING.RUNTIME respectively. If not, see\n" + - " .\n" + - " \n" + - " ----\n" + - " \n" + - " Full text of license texts referred to above follows (that they are\n" + - " listed below does not necessarily imply the conditions apply to the\n" + - " present binary release):\n" + - " \n" + - " ----\n" + - " \n" + - " GCC RUNTIME LIBRARY EXCEPTION\n" + - " \n" + - " Version 3.1, 31 March 2009\n" + - " \n" + - " Copyright (C) 2009 Free Software Foundation, Inc. \n" + - " \n" + - " Everyone is permitted to copy and distribute verbatim copies of this\n" + - " license document, but changing it is not allowed.\n" + - " \n" + - " This GCC Runtime Library Exception (\"Exception\") is an additional\n" + - " permission under section 7 of the GNU General Public License, version\n" + - " 3 (\"GPLv3\"). It applies to a given file (the \"Runtime Library\") that\n" + - " bears a notice placed by the copyright holder of the file stating that\n" + - " the file is governed by GPLv3 along with this Exception.\n" + - " \n" + - " When you use GCC to compile a program, GCC may combine portions of\n" + - " certain GCC header files and runtime libraries with the compiled\n" + - " program. The purpose of this Exception is to allow compilation of\n" + - " non-GPL (including proprietary) programs to use, in this way, the\n" + - " header files and runtime libraries covered by this Exception.\n" + - " \n" + - " 0. Definitions.\n" + - " \n" + - " A file is an \"Independent Module\" if it either requires the Runtime\n" + - " Library for execution after a Compilation Process, or makes use of an\n" + - " interface provided by the Runtime Library, but is not otherwise based\n" + - " on the Runtime Library.\n" + - " \n" + - " \"GCC\" means a version of the GNU Compiler Collection, with or without\n" + - " modifications, governed by version 3 (or a specified later version) of\n" + - " the GNU General Public License (GPL) with the option of using any\n" + - " subsequent versions published by the FSF.\n" + - " \n" + - " \"GPL-compatible Software\" is software whose conditions of propagation,\n" + - " modification and use would permit combination with GCC in accord with\n" + - " the license of GCC.\n" + - " \n" + - " \"Target Code\" refers to output from any compiler for a real or virtual\n" + - " target processor architecture, in executable form or suitable for\n" + - " input to an assembler, loader, linker and/or execution\n" + - " phase. Notwithstanding that, Target Code does not include data in any\n" + - " format that is used as a compiler intermediate representation, or used\n" + - " for producing a compiler intermediate representation.\n" + - " \n" + - " The \"Compilation Process\" transforms code entirely represented in\n" + - " non-intermediate languages designed for human-written code, and/or in\n" + - " Java Virtual Machine byte code, into Target Code. Thus, for example,\n" + - " use of source code generators and preprocessors need not be considered\n" + - " part of the Compilation Process, since the Compilation Process can be\n" + - " understood as starting with the output of the generators or\n" + - " preprocessors.\n" + - " \n" + - " A Compilation Process is \"Eligible\" if it is done using GCC, alone or\n" + - " with other GPL-compatible software, or if it is done without using any\n" + - " work based on GCC. For example, using non-GPL-compatible Software to\n" + - " optimize any GCC intermediate representations would not qualify as an\n" + - " Eligible Compilation Process.\n" + - " \n" + - " 1. Grant of Additional Permission.\n" + - " \n" + - " You have permission to propagate a work of Target Code formed by\n" + - " combining the Runtime Library with Independent Modules, even if such\n" + - " propagation would otherwise violate the terms of GPLv3, provided that\n" + - " all Target Code was generated by Eligible Compilation Processes. You\n" + - " may then convey such a combination under terms of your choice,\n" + - " consistent with the licensing of the Independent Modules.\n" + - " \n" + - " 2. No Weakening of GCC Copyleft.\n" + - " \n" + - " The availability of this Exception does not imply any general\n" + - " presumption that third-party software is unaffected by the copyleft\n" + - " requirements of the license of GCC.\n" + - " \n" + - " ----\n" + - " \n" + - " GNU GENERAL PUBLIC LICENSE\n" + - " Version 3, 29 June 2007\n" + - " \n" + - " Copyright (C) 2007 Free Software Foundation, Inc. \n" + - " Everyone is permitted to copy and distribute verbatim copies\n" + - " of this license document, but changing it is not allowed.\n" + - " \n" + - " Preamble\n" + - " \n" + - " The GNU General Public License is a free, copyleft license for\n" + - " software and other kinds of works.\n" + - " \n" + - " The licenses for most software and other practical works are designed\n" + - " to take away your freedom to share and change the works. By contrast,\n" + - " the GNU General Public License is intended to guarantee your freedom to\n" + - " share and change all versions of a program--to make sure it remains free\n" + - " software for all its users. We, the Free Software Foundation, use the\n" + - " GNU General Public License for most of our software; it applies also to\n" + - " any other work released this way by its authors. You can apply it to\n" + - " your programs, too.\n" + - " \n" + - " When we speak of free software, we are referring to freedom, not\n" + - " price. Our General Public Licenses are designed to make sure that you\n" + - " have the freedom to distribute copies of free software (and charge for\n" + - " them if you wish), that you receive source code or can get it if you\n" + - " want it, that you can change the software or use pieces of it in new\n" + - " free programs, and that you know you can do these things.\n" + - " \n" + - " To protect your rights, we need to prevent others from denying you\n" + - " these rights or asking you to surrender the rights. Therefore, you have\n" + - " certain responsibilities if you distribute copies of the software, or if\n" + - " you modify it: responsibilities to respect the freedom of others.\n" + - " \n" + - " For example, if you distribute copies of such a program, whether\n" + - " gratis or for a fee, you must pass on to the recipients the same\n" + - " freedoms that you received. You must make sure that they, too, receive\n" + - " or can get the source code. And you must show them these terms so they\n" + - " know their rights.\n" + - " \n" + - " Developers that use the GNU GPL protect your rights with two steps:\n" + - " (1) assert copyright on the software, and (2) offer you this License\n" + - " giving you legal permission to copy, distribute and/or modify it.\n" + - " \n" + - " For the developers' and authors' protection, the GPL clearly explains\n" + - " that there is no warranty for this free software. For both users' and\n" + - " authors' sake, the GPL requires that modified versions be marked as\n" + - " changed, so that their problems will not be attributed erroneously to\n" + - " authors of previous versions.\n" + - " \n" + - " Some devices are designed to deny users access to install or run\n" + - " modified versions of the software inside them, although the manufacturer\n" + - " can do so. This is fundamentally incompatible with the aim of\n" + - " protecting users' freedom to change the software. The systematic\n" + - " pattern of such abuse occurs in the area of products for individuals to\n" + - " use, which is precisely where it is most unacceptable. Therefore, we\n" + - " have designed this version of the GPL to prohibit the practice for those\n" + - " products. If such problems arise substantially in other domains, we\n" + - " stand ready to extend this provision to those domains in future versions\n" + - " of the GPL, as needed to protect the freedom of users.\n" + - " \n" + - " Finally, every program is threatened constantly by software patents.\n" + - " States should not allow patents to restrict development and use of\n" + - " software on general-purpose computers, but in those that do, we wish to\n" + - " avoid the special danger that patents applied to a free program could\n" + - " make it effectively proprietary. To prevent this, the GPL assures that\n" + - " patents cannot be used to render the program non-free.\n" + - " \n" + - " The precise terms and conditions for copying, distribution and\n" + - " modification follow.\n" + - " \n" + - " TERMS AND CONDITIONS\n" + - " \n" + - " 0. Definitions.\n" + - " \n" + - " \"This License\" refers to version 3 of the GNU General Public License.\n" + - " \n" + - " \"Copyright\" also means copyright-like laws that apply to other kinds of\n" + - " works, such as semiconductor masks.\n" + - " \n" + - " \"The Program\" refers to any copyrightable work licensed under this\n" + - " License. Each licensee is addressed as \"you\". \"Licensees\" and\n" + - " \"recipients\" may be individuals or organizations.\n" + - " \n" + - " To \"modify\" a work means to copy from or adapt all or part of the work\n" + - " in a fashion requiring copyright permission, other than the making of an\n" + - " exact copy. The resulting work is called a \"modified version\" of the\n" + - " earlier work or a work \"based on\" the earlier work.\n" + - " \n" + - " A \"covered work\" means either the unmodified Program or a work based\n" + - " on the Program.\n" + - " \n" + - " To \"propagate\" a work means to do anything with it that, without\n" + - " permission, would make you directly or secondarily liable for\n" + - " infringement under applicable copyright law, except executing it on a\n" + - " computer or modifying a private copy. Propagation includes copying,\n" + - " distribution (with or without modification), making available to the\n" + - " public, and in some countries other activities as well.\n" + - " \n" + - " To \"convey\" a work means any kind of propagation that enables other\n" + - " parties to make or receive copies. Mere interaction with a user through\n" + - " a computer network, with no transfer of a copy, is not conveying.\n" + - " \n" + - " An interactive user interface displays \"Appropriate Legal Notices\"\n" + - " to the extent that it includes a convenient and prominently visible\n" + - " feature that (1) displays an appropriate copyright notice, and (2)\n" + - " tells the user that there is no warranty for the work (except to the\n" + - " extent that warranties are provided), that licensees may convey the\n" + - " work under this License, and how to view a copy of this License. If\n" + - " the interface presents a list of user commands or options, such as a\n" + - " menu, a prominent item in the list meets this criterion.\n" + - " \n" + - " 1. Source Code.\n" + - " \n" + - " The \"source code\" for a work means the preferred form of the work\n" + - " for making modifications to it. \"Object code\" means any non-source\n" + - " form of a work.\n" + - " \n" + - " A \"Standard Interface\" means an interface that either is an official\n" + - " standard defined by a recognized standards body, or, in the case of\n" + - " interfaces specified for a particular programming language, one that\n" + - " is widely used among developers working in that language.\n" + - " \n" + - " The \"System Libraries\" of an executable work include anything, other\n" + - " than the work as a whole, that (a) is included in the normal form of\n" + - " packaging a Major Component, but which is not part of that Major\n" + - " Component, and (b) serves only to enable use of the work with that\n" + - " Major Component, or to implement a Standard Interface for which an\n" + - " implementation is available to the public in source code form. A\n" + - " \"Major Component\", in this context, means a major essential component\n" + - " (kernel, window system, and so on) of the specific operating system\n" + - " (if any) on which the executable work runs, or a compiler used to\n" + - " produce the work, or an object code interpreter used to run it.\n" + - " \n" + - " The \"Corresponding Source\" for a work in object code form means all\n" + - " the source code needed to generate, install, and (for an executable\n" + - " work) run the object code and to modify the work, including scripts to\n" + - " control those activities. However, it does not include the work's\n" + - " System Libraries, or general-purpose tools or generally available free\n" + - " programs which are used unmodified in performing those activities but\n" + - " which are not part of the work. For example, Corresponding Source\n" + - " includes interface definition files associated with source files for\n" + - " the work, and the source code for shared libraries and dynamically\n" + - " linked subprograms that the work is specifically designed to require,\n" + - " such as by intimate data communication or control flow between those\n" + - " subprograms and other parts of the work.\n" + - " \n" + - " The Corresponding Source need not include anything that users\n" + - " can regenerate automatically from other parts of the Corresponding\n" + - " Source.\n" + - " \n" + - " The Corresponding Source for a work in source code form is that\n" + - " same work.\n" + - " \n" + - " 2. Basic Permissions.\n" + - " \n" + - " All rights granted under this License are granted for the term of\n" + - " copyright on the Program, and are irrevocable provided the stated\n" + - " conditions are met. This License explicitly affirms your unlimited\n" + - " permission to run the unmodified Program. The output from running a\n" + - " covered work is covered by this License only if the output, given its\n" + - " content, constitutes a covered work. This License acknowledges your\n" + - " rights of fair use or other equivalent, as provided by copyright law.\n" + - " \n" + - " You may make, run and propagate covered works that you do not\n" + - " convey, without conditions so long as your license otherwise remains\n" + - " in force. You may convey covered works to others for the sole purpose\n" + - " of having them make modifications exclusively for you, or provide you\n" + - " with facilities for running those works, provided that you comply with\n" + - " the terms of this License in conveying all material for which you do\n" + - " not control copyright. Those thus making or running the covered works\n" + - " for you must do so exclusively on your behalf, under your direction\n" + - " and control, on terms that prohibit them from making any copies of\n" + - " your copyrighted material outside their relationship with you.\n" + - " \n" + - " Conveying under any other circumstances is permitted solely under\n" + - " the conditions stated below. Sublicensing is not allowed; section 10\n" + - " makes it unnecessary.\n" + - " \n" + - " 3. Protecting Users' Legal Rights From Anti-Circumvention Law.\n" + - " \n" + - " No covered work shall be deemed part of an effective technological\n" + - " measure under any applicable law fulfilling obligations under article\n" + - " 11 of the WIPO copyright treaty adopted on 20 December 1996, or\n" + - " similar laws prohibiting or restricting circumvention of such\n" + - " measures.\n" + - " \n" + - " When you convey a covered work, you waive any legal power to forbid\n" + - " circumvention of technological measures to the extent such circumvention\n" + - " is effected by exercising rights under this License with respect to\n" + - " the covered work, and you disclaim any intention to limit operation or\n" + - " modification of the work as a means of enforcing, against the work's\n" + - " users, your or third parties' legal rights to forbid circumvention of\n" + - " technological measures.\n" + - " \n" + - " 4. Conveying Verbatim Copies.\n" + - " \n" + - " You may convey verbatim copies of the Program's source code as you\n" + - " receive it, in any medium, provided that you conspicuously and\n" + - " appropriately publish on each copy an appropriate copyright notice;\n" + - " keep intact all notices stating that this License and any\n" + - " non-permissive terms added in accord with section 7 apply to the code;\n" + - " keep intact all notices of the absence of any warranty; and give all\n" + - " recipients a copy of this License along with the Program.\n" + - " \n" + - " You may charge any price or no price for each copy that you convey,\n" + - " and you may offer support or warranty protection for a fee.\n" + - " \n" + - " 5. Conveying Modified Source Versions.\n" + - " \n" + - " You may convey a work based on the Program, or the modifications to\n" + - " produce it from the Program, in the form of source code under the\n" + - " terms of section 4, provided that you also meet all of these conditions:\n" + - " \n" + - " a) The work must carry prominent notices stating that you modified\n" + - " it, and giving a relevant date.\n" + - " \n" + - " b) The work must carry prominent notices stating that it is\n" + - " released under this License and any conditions added under section\n" + - " 7. This requirement modifies the requirement in section 4 to\n" + - " \"keep intact all notices\".\n" + - " \n" + - " c) You must license the entire work, as a whole, under this\n" + - " License to anyone who comes into possession of a copy. This\n" + - " License will therefore apply, along with any applicable section 7\n" + - " additional terms, to the whole of the work, and all its parts,\n" + - " regardless of how they are packaged. This License gives no\n" + - " permission to license the work in any other way, but it does not\n" + - " invalidate such permission if you have separately received it.\n" + - " \n" + - " d) If the work has interactive user interfaces, each must display\n" + - " Appropriate Legal Notices; however, if the Program has interactive\n" + - " interfaces that do not display Appropriate Legal Notices, your\n" + - " work need not make them do so.\n" + - " \n" + - " A compilation of a covered work with other separate and independent\n" + - " works, which are not by their nature extensions of the covered work,\n" + - " and which are not combined with it such as to form a larger program,\n" + - " in or on a volume of a storage or distribution medium, is called an\n" + - " \"aggregate\" if the compilation and its resulting copyright are not\n" + - " used to limit the access or legal rights of the compilation's users\n" + - " beyond what the individual works permit. Inclusion of a covered work\n" + - " in an aggregate does not cause this License to apply to the other\n" + - " parts of the aggregate.\n" + - " \n" + - " 6. Conveying Non-Source Forms.\n" + - " \n" + - " You may convey a covered work in object code form under the terms\n" + - " of sections 4 and 5, provided that you also convey the\n" + - " machine-readable Corresponding Source under the terms of this License,\n" + - " in one of these ways:\n" + - " \n" + - " a) Convey the object code in, or embodied in, a physical product\n" + - " (including a physical distribution medium), accompanied by the\n" + - " Corresponding Source fixed on a durable physical medium\n" + - " customarily used for software interchange.\n" + - " \n" + - " b) Convey the object code in, or embodied in, a physical product\n" + - " (including a physical distribution medium), accompanied by a\n" + - " written offer, valid for at least three years and valid for as\n" + - " long as you offer spare parts or customer support for that product\n" + - " model, to give anyone who possesses the object code either (1) a\n" + - " copy of the Corresponding Source for all the software in the\n" + - " product that is covered by this License, on a durable physical\n" + - " medium customarily used for software interchange, for a price no\n" + - " more than your reasonable cost of physically performing this\n" + - " conveying of source, or (2) access to copy the\n" + - " Corresponding Source from a network server at no charge.\n" + - " \n" + - " c) Convey individual copies of the object code with a copy of the\n" + - " written offer to provide the Corresponding Source. This\n" + - " alternative is allowed only occasionally and noncommercially, and\n" + - " only if you received the object code with such an offer, in accord\n" + - " with subsection 6b.\n" + - " \n" + - " d) Convey the object code by offering access from a designated\n" + - " place (gratis or for a charge), and offer equivalent access to the\n" + - " Corresponding Source in the same way through the same place at no\n" + - " further charge. You need not require recipients to copy the\n" + - " Corresponding Source along with the object code. If the place to\n" + - " copy the object code is a network server, the Corresponding Source\n" + - " may be on a different server (operated by you or a third party)\n" + - " that supports equivalent copying facilities, provided you maintain\n" + - " clear directions next to the object code saying where to find the\n" + - " Corresponding Source. Regardless of what server hosts the\n" + - " Corresponding Source, you remain obligated to ensure that it is\n" + - " available for as long as needed to satisfy these requirements.\n" + - " \n" + - " e) Convey the object code using peer-to-peer transmission, provided\n" + - " you inform other peers where the object code and Corresponding\n" + - " Source of the work are being offered to the general public at no\n" + - " charge under subsection 6d.\n" + - " \n" + - " A separable portion of the object code, whose source code is excluded\n" + - " from the Corresponding Source as a System Library, need not be\n" + - " included in conveying the object code work.\n" + - " \n" + - " A \"User Product\" is either (1) a \"consumer product\", which means any\n" + - " tangible personal property which is normally used for personal, family,\n" + - " or household purposes, or (2) anything designed or sold for incorporation\n" + - " into a dwelling. In determining whether a product is a consumer product,\n" + - " doubtful cases shall be resolved in favor of coverage. For a particular\n" + - " product received by a particular user, \"normally used\" refers to a\n" + - " typical or common use of that class of product, regardless of the status\n" + - " of the particular user or of the way in which the particular user\n" + - " actually uses, or expects or is expected to use, the product. A product\n" + - " is a consumer product regardless of whether the product has substantial\n" + - " commercial, industrial or non-consumer uses, unless such uses represent\n" + - " the only significant mode of use of the product.\n" + - " \n" + - " \"Installation Information\" for a User Product means any methods,\n" + - " procedures, authorization keys, or other information required to install\n" + - " and execute modified versions of a covered work in that User Product from\n" + - " a modified version of its Corresponding Source. The information must\n" + - " suffice to ensure that the continued functioning of the modified object\n" + - " code is in no case prevented or interfered with solely because\n" + - " modification has been made.\n" + - " \n" + - " If you convey an object code work under this section in, or with, or\n" + - " specifically for use in, a User Product, and the conveying occurs as\n" + - " part of a transaction in which the right of possession and use of the\n" + - " User Product is transferred to the recipient in perpetuity or for a\n" + - " fixed term (regardless of how the transaction is characterized), the\n" + - " Corresponding Source conveyed under this section must be accompanied\n" + - " by the Installation Information. But this requirement does not apply\n" + - " if neither you nor any third party retains the ability to install\n" + - " modified object code on the User Product (for example, the work has\n" + - " been installed in ROM).\n" + - " \n" + - " The requirement to provide Installation Information does not include a\n" + - " requirement to continue to provide support service, warranty, or updates\n" + - " for a work that has been modified or installed by the recipient, or for\n" + - " the User Product in which it has been modified or installed. Access to a\n" + - " network may be denied when the modification itself materially and\n" + - " adversely affects the operation of the network or violates the rules and\n" + - " protocols for communication across the network.\n" + - " \n" + - " Corresponding Source conveyed, and Installation Information provided,\n" + - " in accord with this section must be in a format that is publicly\n" + - " documented (and with an implementation available to the public in\n" + - " source code form), and must require no special password or key for\n" + - " unpacking, reading or copying.\n" + - " \n" + - " 7. Additional Terms.\n" + - " \n" + - " \"Additional permissions\" are terms that supplement the terms of this\n" + - " License by making exceptions from one or more of its conditions.\n" + - " Additional permissions that are applicable to the entire Program shall\n" + - " be treated as though they were included in this License, to the extent\n" + - " that they are valid under applicable law. If additional permissions\n" + - " apply only to part of the Program, that part may be used separately\n" + - " under those permissions, but the entire Program remains governed by\n" + - " this License without regard to the additional permissions.\n" + - " \n" + - " When you convey a copy of a covered work, you may at your option\n" + - " remove any additional permissions from that copy, or from any part of\n" + - " it. (Additional permissions may be written to require their own\n" + - " removal in certain cases when you modify the work.) You may place\n" + - " additional permissions on material, added by you to a covered work,\n" + - " for which you have or can give appropriate copyright permission.\n" + - " \n" + - " Notwithstanding any other provision of this License, for material you\n" + - " add to a covered work, you may (if authorized by the copyright holders of\n" + - " that material) supplement the terms of this License with terms:\n" + - " \n" + - " a) Disclaiming warranty or limiting liability differently from the\n" + - " terms of sections 15 and 16 of this License; or\n" + - " \n" + - " b) Requiring preservation of specified reasonable legal notices or\n" + - " author attributions in that material or in the Appropriate Legal\n" + - " Notices displayed by works containing it; or\n" + - " \n" + - " c) Prohibiting misrepresentation of the origin of that material, or\n" + - " requiring that modified versions of such material be marked in\n" + - " reasonable ways as different from the original version; or\n" + - " \n" + - " d) Limiting the use for publicity purposes of names of licensors or\n" + - " authors of the material; or\n" + - " \n" + - " e) Declining to grant rights under trademark law for use of some\n" + - " trade names, trademarks, or service marks; or\n" + - " \n" + - " f) Requiring indemnification of licensors and authors of that\n" + - " material by anyone who conveys the material (or modified versions of\n" + - " it) with contractual assumptions of liability to the recipient, for\n" + - " any liability that these contractual assumptions directly impose on\n" + - " those licensors and authors.\n" + - " \n" + - " All other non-permissive additional terms are considered \"further\n" + - " restrictions\" within the meaning of section 10. If the Program as you\n" + - " received it, or any part of it, contains a notice stating that it is\n" + - " governed by this License along with a term that is a further\n" + - " restriction, you may remove that term. If a license document contains\n" + - " a further restriction but permits relicensing or conveying under this\n" + - " License, you may add to a covered work material governed by the terms\n" + - " of that license document, provided that the further restriction does\n" + - " not survive such relicensing or conveying.\n" + - " \n" + - " If you add terms to a covered work in accord with this section, you\n" + - " must place, in the relevant source files, a statement of the\n" + - " additional terms that apply to those files, or a notice indicating\n" + - " where to find the applicable terms.\n" + - " \n" + - " Additional terms, permissive or non-permissive, may be stated in the\n" + - " form of a separately written license, or stated as exceptions;\n" + - " the above requirements apply either way.\n" + - " \n" + - " 8. Termination.\n" + - " \n" + - " You may not propagate or modify a covered work except as expressly\n" + - " provided under this License. Any attempt otherwise to propagate or\n" + - " modify it is void, and will automatically terminate your rights under\n" + - " this License (including any patent licenses granted under the third\n" + - " paragraph of section 11).\n" + - " \n" + - " However, if you cease all violation of this License, then your\n" + - " license from a particular copyright holder is reinstated (a)\n" + - " provisionally, unless and until the copyright holder explicitly and\n" + - " finally terminates your license, and (b) permanently, if the copyright\n" + - " holder fails to notify you of the violation by some reasonable means\n" + - " prior to 60 days after the cessation.\n" + - " \n" + - " Moreover, your license from a particular copyright holder is\n" + - " reinstated permanently if the copyright holder notifies you of the\n" + - " violation by some reasonable means, this is the first time you have\n" + - " received notice of violation of this License (for any work) from that\n" + - " copyright holder, and you cure the violation prior to 30 days after\n" + - " your receipt of the notice.\n" + - " \n" + - " Termination of your rights under this section does not terminate the\n" + - " licenses of parties who have received copies or rights from you under\n" + - " this License. If your rights have been terminated and not permanently\n" + - " reinstated, you do not qualify to receive new licenses for the same\n" + - " material under section 10.\n" + - " \n" + - " 9. Acceptance Not Required for Having Copies.\n" + - " \n" + - " You are not required to accept this License in order to receive or\n" + - " run a copy of the Program. Ancillary propagation of a covered work\n" + - " occurring solely as a consequence of using peer-to-peer transmission\n" + - " to receive a copy likewise does not require acceptance. However,\n" + - " nothing other than this License grants you permission to propagate or\n" + - " modify any covered work. These actions infringe copyright if you do\n" + - " not accept this License. Therefore, by modifying or propagating a\n" + - " covered work, you indicate your acceptance of this License to do so.\n" + - " \n" + - " 10. Automatic Licensing of Downstream Recipients.\n" + - " \n" + - " Each time you convey a covered work, the recipient automatically\n" + - " receives a license from the original licensors, to run, modify and\n" + - " propagate that work, subject to this License. You are not responsible\n" + - " for enforcing compliance by third parties with this License.\n" + - " \n" + - " An \"entity transaction\" is a transaction transferring control of an\n" + - " organization, or substantially all assets of one, or subdividing an\n" + - " organization, or merging organizations. If propagation of a covered\n" + - " work results from an entity transaction, each party to that\n" + - " transaction who receives a copy of the work also receives whatever\n" + - " licenses to the work the party's predecessor in interest had or could\n" + - " give under the previous paragraph, plus a right to possession of the\n" + - " Corresponding Source of the work from the predecessor in interest, if\n" + - " the predecessor has it or can get it with reasonable efforts.\n" + - " \n" + - " You may not impose any further restrictions on the exercise of the\n" + - " rights granted or affirmed under this License. For example, you may\n" + - " not impose a license fee, royalty, or other charge for exercise of\n" + - " rights granted under this License, and you may not initiate litigation\n" + - " (including a cross-claim or counterclaim in a lawsuit) alleging that\n" + - " any patent claim is infringed by making, using, selling, offering for\n" + - " sale, or importing the Program or any portion of it.\n" + - " \n" + - " 11. Patents.\n" + - " \n" + - " A \"contributor\" is a copyright holder who authorizes use under this\n" + - " License of the Program or a work on which the Program is based. The\n" + - " work thus licensed is called the contributor's \"contributor version\".\n" + - " \n" + - " A contributor's \"essential patent claims\" are all patent claims\n" + - " owned or controlled by the contributor, whether already acquired or\n" + - " hereafter acquired, that would be infringed by some manner, permitted\n" + - " by this License, of making, using, or selling its contributor version,\n" + - " but do not include claims that would be infringed only as a\n" + - " consequence of further modification of the contributor version. For\n" + - " purposes of this definition, \"control\" includes the right to grant\n" + - " patent sublicenses in a manner consistent with the requirements of\n" + - " this License.\n" + - " \n" + - " Each contributor grants you a non-exclusive, worldwide, royalty-free\n" + - " patent license under the contributor's essential patent claims, to\n" + - " make, use, sell, offer for sale, import and otherwise run, modify and\n" + - " propagate the contents of its contributor version.\n" + - " \n" + - " In the following three paragraphs, a \"patent license\" is any express\n" + - " agreement or commitment, however denominated, not to enforce a patent\n" + - " (such as an express permission to practice a patent or covenant not to\n" + - " sue for patent infringement). To \"grant\" such a patent license to a\n" + - " party means to make such an agreement or commitment not to enforce a\n" + - " patent against the party.\n" + - " \n" + - " If you convey a covered work, knowingly relying on a patent license,\n" + - " and the Corresponding Source of the work is not available for anyone\n" + - " to copy, free of charge and under the terms of this License, through a\n" + - " publicly available network server or other readily accessible means,\n" + - " then you must either (1) cause the Corresponding Source to be so\n" + - " available, or (2) arrange to deprive yourself of the benefit of the\n" + - " patent license for this particular work, or (3) arrange, in a manner\n" + - " consistent with the requirements of this License, to extend the patent\n" + - " license to downstream recipients. \"Knowingly relying\" means you have\n" + - " actual knowledge that, but for the patent license, your conveying the\n" + - " covered work in a country, or your recipient's use of the covered work\n" + - " in a country, would infringe one or more identifiable patents in that\n" + - " country that you have reason to believe are valid.\n" + - " \n" + - " If, pursuant to or in connection with a single transaction or\n" + - " arrangement, you convey, or propagate by procuring conveyance of, a\n" + - " covered work, and grant a patent license to some of the parties\n" + - " receiving the covered work authorizing them to use, propagate, modify\n" + - " or convey a specific copy of the covered work, then the patent license\n" + - " you grant is automatically extended to all recipients of the covered\n" + - " work and works based on it.\n" + - " \n" + - " A patent license is \"discriminatory\" if it does not include within\n" + - " the scope of its coverage, prohibits the exercise of, or is\n" + - " conditioned on the non-exercise of one or more of the rights that are\n" + - " specifically granted under this License. You may not convey a covered\n" + - " work if you are a party to an arrangement with a third party that is\n" + - " in the business of distributing software, under which you make payment\n" + - " to the third party based on the extent of your activity of conveying\n" + - " the work, and under which the third party grants, to any of the\n" + - " parties who would receive the covered work from you, a discriminatory\n" + - " patent license (a) in connection with copies of the covered work\n" + - " conveyed by you (or copies made from those copies), or (b) primarily\n" + - " for and in connection with specific products or compilations that\n" + - " contain the covered work, unless you entered into that arrangement,\n" + - " or that patent license was granted, prior to 28 March 2007.\n" + - " \n" + - " Nothing in this License shall be construed as excluding or limiting\n" + - " any implied license or other defenses to infringement that may\n" + - " otherwise be available to you under applicable patent law.\n" + - " \n" + - " 12. No Surrender of Others' Freedom.\n" + - " \n" + - " If conditions are imposed on you (whether by court order, agreement or\n" + - " otherwise) that contradict the conditions of this License, they do not\n" + - " excuse you from the conditions of this License. If you cannot convey a\n" + - " covered work so as to satisfy simultaneously your obligations under this\n" + - " License and any other pertinent obligations, then as a consequence you may\n" + - " not convey it at all. For example, if you agree to terms that obligate you\n" + - " to collect a royalty for further conveying from those to whom you convey\n" + - " the Program, the only way you could satisfy both those terms and this\n" + - " License would be to refrain entirely from conveying the Program.\n" + - " \n" + - " 13. Use with the GNU Affero General Public License.\n" + - " \n" + - " Notwithstanding any other provision of this License, you have\n" + - " permission to link or combine any covered work with a work licensed\n" + - " under version 3 of the GNU Affero General Public License into a single\n" + - " combined work, and to convey the resulting work. The terms of this\n" + - " License will continue to apply to the part which is the covered work,\n" + - " but the special requirements of the GNU Affero General Public License,\n" + - " section 13, concerning interaction through a network will apply to the\n" + - " combination as such.\n" + - " \n" + - " 14. Revised Versions of this License.\n" + - " \n" + - " The Free Software Foundation may publish revised and/or new versions of\n" + - " the GNU General Public License from time to time. Such new versions will\n" + - " be similar in spirit to the present version, but may differ in detail to\n" + - " address new problems or concerns.\n" + - " \n" + - " Each version is given a distinguishing version number. If the\n" + - " Program specifies that a certain numbered version of the GNU General\n" + - " Public License \"or any later version\" applies to it, you have the\n" + - " option of following the terms and conditions either of that numbered\n" + - " version or of any later version published by the Free Software\n" + - " Foundation. If the Program does not specify a version number of the\n" + - " GNU General Public License, you may choose any version ever published\n" + - " by the Free Software Foundation.\n" + - " \n" + - " If the Program specifies that a proxy can decide which future\n" + - " versions of the GNU General Public License can be used, that proxy's\n" + - " public statement of acceptance of a version permanently authorizes you\n" + - " to choose that version for the Program.\n" + - " \n" + - " Later license versions may give you additional or different\n" + - " permissions. However, no additional obligations are imposed on any\n" + - " author or copyright holder as a result of your choosing to follow a\n" + - " later version.\n" + - " \n" + - " 15. Disclaimer of Warranty.\n" + - " \n" + - " THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY\n" + - " APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT\n" + - " HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM \"AS IS\" WITHOUT WARRANTY\n" + - " OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO,\n" + - " THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR\n" + - " PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM\n" + - " IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF\n" + - " ALL NECESSARY SERVICING, REPAIR OR CORRECTION.\n" + - " \n" + - " 16. Limitation of Liability.\n" + - " \n" + - " IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING\n" + - " WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS\n" + - " THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY\n" + - " GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE\n" + - " USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF\n" + - " DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD\n" + - " PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS),\n" + - " EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF\n" + - " SUCH DAMAGES.\n" + - " \n" + - " 17. Interpretation of Sections 15 and 16.\n" + - " \n" + - " If the disclaimer of warranty and limitation of liability provided\n" + - " above cannot be given local legal effect according to their terms,\n" + - " reviewing courts shall apply local law that most closely approximates\n" + - " an absolute waiver of all civil liability in connection with the\n" + - " Program, unless a warranty or assumption of liability accompanies a\n" + - " copy of the Program in return for a fee.\n" + - " \n" + - " END OF TERMS AND CONDITIONS\n" + - " \n" + - " How to Apply These Terms to Your New Programs\n" + - " \n" + - " If you develop a new program, and you want it to be of the greatest\n" + - " possible use to the public, the best way to achieve this is to make it\n" + - " free software which everyone can redistribute and change under these terms.\n" + - " \n" + - " To do so, attach the following notices to the program. It is safest\n" + - " to attach them to the start of each source file to most effectively\n" + - " state the exclusion of warranty; and each file should have at least\n" + - " the \"copyright\" line and a pointer to where the full notice is found.\n" + - " \n" + - " \n" + - " Copyright (C) \n" + - " \n" + - " This program is free software: you can redistribute it and/or modify\n" + - " it under the terms of the GNU General Public License as published by\n" + - " the Free Software Foundation, either version 3 of the License, or\n" + - " (at your option) any later version.\n" + - " \n" + - " This program is distributed in the hope that it will be useful,\n" + - " but WITHOUT ANY WARRANTY; without even the implied warranty of\n" + - " MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the\n" + - " GNU General Public License for more details.\n" + - " \n" + - " You should have received a copy of the GNU General Public License\n" + - " along with this program. If not, see .\n" + - " \n" + - " Also add information on how to contact you by electronic and paper mail.\n" + - " \n" + - " If the program does terminal interaction, make it output a short\n" + - " notice like this when it starts in an interactive mode:\n" + - " \n" + - " Copyright (C) \n" + - " This program comes with ABSOLUTELY NO WARRANTY; for details type `show w'.\n" + - " This is free software, and you are welcome to redistribute it\n" + - " under certain conditions; type `show c' for details.\n" + - " \n" + - " The hypothetical commands `show w' and `show c' should show the appropriate\n" + - " parts of the General Public License. Of course, your program's commands\n" + - " might be different; for a GUI interface, you would use an \"about box\".\n" + - " \n" + - " You should also get your employer (if you work as a programmer) or school,\n" + - " if any, to sign a \"copyright disclaimer\" for the program, if necessary.\n" + - " For more information on this, and how to apply and follow the GNU GPL, see\n" + - " .\n" + - " \n" + - " The GNU General Public License does not permit incorporating your program\n" + - " into proprietary programs. If your program is a subroutine library, you\n" + - " may consider it more useful to permit linking proprietary applications with\n" + - " the library. If this is what you want to do, use the GNU Lesser General\n" + - " Public License instead of this License. But first, please read\n" + - " .\n" + - " \n" + - " \n" + - " Name: libquadmath\n" + - " Files: scipy/.dylibs/libquadmath*.so\n" + - " Description: dynamically linked to files compiled with gcc\n" + - " Availability: https://gcc.gnu.org/git/?p=gcc.git;a=tree;f=libquadmath\n" + - " License: LGPL-2.1-or-later\n" + - " \n" + - " GCC Quad-Precision Math Library\n" + - " Copyright (C) 2010-2019 Free Software Foundation, Inc.\n" + - " Written by Francois-Xavier Coudert \n" + - " \n" + - " This file is part of the libquadmath library.\n" + - " Libquadmath is free software; you can redistribute it and/or\n" + - " modify it under the terms of the GNU Library General Public\n" + - " License as published by the Free Software Foundation; either\n" + - " version 2.1 of the License, or (at your option) any later version.\n" + - " \n" + - " Libquadmath is distributed in the hope that it will be useful,\n" + - " but WITHOUT ANY WARRANTY; without even the implied warranty of\n" + - " MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU\n" + - " Lesser General Public License for more details.\n" + - " https://www.gnu.org/licenses/old-licenses/lgpl-2.1.html\n" + - "Location: /Users/abc/Library/Python/3.9/lib/python/site-packages\n" + - "Requires: numpy\n" + - "Required-by: gensim"); + EXPECTED_PIP_SHOW_RESULTS.add("Name: scipy\n" + "Version: 1.11.3\n" + + "Summary: Fundamental algorithms for scientific computing in Python\n" + + "Home-page: https://scipy.org/\n" + + "Author: \n" + + "Author-email: \n" + + "License: Copyright (c) 2001-2002 Enthought, Inc. 2003-2023, SciPy Developers.\n" + + " All rights reserved.\n" + + " \n" + + " Redistribution and use in source and binary forms, with or without\n" + + " modification, are permitted provided that the following conditions\n" + + " are met:\n" + + " \n" + + " 1. Redistributions of source code must retain the above copyright\n" + + " notice, this list of conditions and the following disclaimer.\n" + + " \n" + + " 2. Redistributions in binary form must reproduce the above\n" + + " copyright notice, this list of conditions and the following\n" + + " disclaimer in the documentation and/or other materials provided\n" + + " with the distribution.\n" + + " \n" + + " 3. Neither the name of the copyright holder nor the names of its\n" + + " contributors may be used to endorse or promote products derived\n" + + " from this software without specific prior written permission.\n" + + " \n" + + " THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS\n" + + " \"AS IS\" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT\n" + + " LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR\n" + + " A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT\n" + + " OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,\n" + + " SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT\n" + + " LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,\n" + + " DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY\n" + + " THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT\n" + + " (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE\n" + + " OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.\n" + + " \n" + + " ----\n" + + " \n" + + " This binary distribution of SciPy also bundles the following software:\n" + + " \n" + + " \n" + + " Name: OpenBLAS\n" + + " Files: scipy/.dylibs/libopenblas*.so\n" + + " Description: bundled as a dynamically linked library\n" + + " Availability: https://github.com/OpenMathLib/OpenBLAS/\n" + + " License: BSD-3-Clause-Attribution\n" + + " Copyright (c) 2011-2014, The OpenBLAS Project\n" + + " All rights reserved.\n" + + " \n" + + " Redistribution and use in source and binary forms, with or without\n" + + " modification, are permitted provided that the following conditions are\n" + + " met:\n" + + " \n" + + " 1. Redistributions of source code must retain the above copyright\n" + + " notice, this list of conditions and the following disclaimer.\n" + + " \n" + + " 2. Redistributions in binary form must reproduce the above copyright\n" + + " notice, this list of conditions and the following disclaimer in\n" + + " the documentation and/or other materials provided with the\n" + + " distribution.\n" + + " 3. Neither the name of the OpenBLAS project nor the names of\n" + + " its contributors may be used to endorse or promote products\n" + + " derived from this software without specific prior written\n" + + " permission.\n" + + " \n" + + " THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS \"AS IS\"\n" + + " AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE\n" + + " IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE\n" + + " ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE\n" + + " LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL\n" + + " DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR\n" + + " SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER\n" + + " CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,\n" + + " OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE\n" + + " USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.\n" + + " \n" + + " \n" + + " Name: LAPACK\n" + + " Files: scipy/.dylibs/libopenblas*.so\n" + + " Description: bundled in OpenBLAS\n" + + " Availability: https://github.com/OpenMathLib/OpenBLAS/\n" + + " License: BSD-3-Clause-Attribution\n" + + " Copyright (c) 1992-2013 The University of Tennessee and The University\n" + + " of Tennessee Research Foundation. All rights\n" + + " reserved.\n" + + " Copyright (c) 2000-2013 The University of California Berkeley. All\n" + + " rights reserved.\n" + + " Copyright (c) 2006-2013 The University of Colorado Denver. All rights\n" + + " reserved.\n" + + " \n" + + " $COPYRIGHT$\n" + + " \n" + + " Additional copyrights may follow\n" + + " \n" + + " $HEADER$\n" + + " \n" + + " Redistribution and use in source and binary forms, with or without\n" + + " modification, are permitted provided that the following conditions are\n" + + " met:\n" + + " \n" + + " - Redistributions of source code must retain the above copyright\n" + + " notice, this list of conditions and the following disclaimer.\n" + + " \n" + + " - Redistributions in binary form must reproduce the above copyright\n" + + " notice, this list of conditions and the following disclaimer listed\n" + + " in this license in the documentation and/or other materials\n" + + " provided with the distribution.\n" + + " \n" + + " - Neither the name of the copyright holders nor the names of its\n" + + " contributors may be used to endorse or promote products derived from\n" + + " this software without specific prior written permission.\n" + + " \n" + + " The copyright holders provide no reassurances that the source code\n" + + " provided does not infringe any patent, copyright, or any other\n" + + " intellectual property rights of third parties. The copyright holders\n" + + " disclaim any liability to any recipient for claims brought against\n" + + " recipient by any third party for infringement of that parties\n" + + " intellectual property rights.\n" + + " \n" + + " THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS\n" + + " \"AS IS\" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT\n" + + " LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR\n" + + " A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT\n" + + " OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,\n" + + " SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT\n" + + " LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,\n" + + " DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY\n" + + " THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT\n" + + " (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE\n" + + " OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.\n" + + " \n" + + " \n" + + " Name: GCC runtime library\n" + + " Files: scipy/.dylibs/libgfortran*, scipy/.dylibs/libgcc*\n" + + " Description: dynamically linked to files compiled with gcc\n" + + " Availability: https://gcc.gnu.org/git/?p=gcc.git;a=tree;f=libgfortran\n" + + " License: GPL-3.0-with-GCC-exception\n" + + " Copyright (C) 2002-2017 Free Software Foundation, Inc.\n" + + " \n" + + " Libgfortran is free software; you can redistribute it and/or modify\n" + + " it under the terms of the GNU General Public License as published by\n" + + " the Free Software Foundation; either version 3, or (at your option)\n" + + " any later version.\n" + + " \n" + + " Libgfortran is distributed in the hope that it will be useful,\n" + + " but WITHOUT ANY WARRANTY; without even the implied warranty of\n" + + " MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the\n" + + " GNU General Public License for more details.\n" + + " \n" + + " Under Section 7 of GPL version 3, you are granted additional\n" + + " permissions described in the GCC Runtime Library Exception, version\n" + + " 3.1, as published by the Free Software Foundation.\n" + + " \n" + + " You should have received a copy of the GNU General Public License and\n" + + " a copy of the GCC Runtime Library Exception along with this program;\n" + + " see the files COPYING3 and COPYING.RUNTIME respectively. If not, see\n" + + " .\n" + + " \n" + + " ----\n" + + " \n" + + " Full text of license texts referred to above follows (that they are\n" + + " listed below does not necessarily imply the conditions apply to the\n" + + " present binary release):\n" + + " \n" + + " ----\n" + + " \n" + + " GCC RUNTIME LIBRARY EXCEPTION\n" + + " \n" + + " Version 3.1, 31 March 2009\n" + + " \n" + + " Copyright (C) 2009 Free Software Foundation, Inc. \n" + + " \n" + + " Everyone is permitted to copy and distribute verbatim copies of this\n" + + " license document, but changing it is not allowed.\n" + + " \n" + + " This GCC Runtime Library Exception (\"Exception\") is an additional\n" + + " permission under section 7 of the GNU General Public License, version\n" + + " 3 (\"GPLv3\"). It applies to a given file (the \"Runtime Library\") that\n" + + " bears a notice placed by the copyright holder of the file stating that\n" + + " the file is governed by GPLv3 along with this Exception.\n" + + " \n" + + " When you use GCC to compile a program, GCC may combine portions of\n" + + " certain GCC header files and runtime libraries with the compiled\n" + + " program. The purpose of this Exception is to allow compilation of\n" + + " non-GPL (including proprietary) programs to use, in this way, the\n" + + " header files and runtime libraries covered by this Exception.\n" + + " \n" + + " 0. Definitions.\n" + + " \n" + + " A file is an \"Independent Module\" if it either requires the Runtime\n" + + " Library for execution after a Compilation Process, or makes use of an\n" + + " interface provided by the Runtime Library, but is not otherwise based\n" + + " on the Runtime Library.\n" + + " \n" + + " \"GCC\" means a version of the GNU Compiler Collection, with or without\n" + + " modifications, governed by version 3 (or a specified later version) of\n" + + " the GNU General Public License (GPL) with the option of using any\n" + + " subsequent versions published by the FSF.\n" + + " \n" + + " \"GPL-compatible Software\" is software whose conditions of propagation,\n" + + " modification and use would permit combination with GCC in accord with\n" + + " the license of GCC.\n" + + " \n" + + " \"Target Code\" refers to output from any compiler for a real or virtual\n" + + " target processor architecture, in executable form or suitable for\n" + + " input to an assembler, loader, linker and/or execution\n" + + " phase. Notwithstanding that, Target Code does not include data in any\n" + + " format that is used as a compiler intermediate representation, or used\n" + + " for producing a compiler intermediate representation.\n" + + " \n" + + " The \"Compilation Process\" transforms code entirely represented in\n" + + " non-intermediate languages designed for human-written code, and/or in\n" + + " Java Virtual Machine byte code, into Target Code. Thus, for example,\n" + + " use of source code generators and preprocessors need not be considered\n" + + " part of the Compilation Process, since the Compilation Process can be\n" + + " understood as starting with the output of the generators or\n" + + " preprocessors.\n" + + " \n" + + " A Compilation Process is \"Eligible\" if it is done using GCC, alone or\n" + + " with other GPL-compatible software, or if it is done without using any\n" + + " work based on GCC. For example, using non-GPL-compatible Software to\n" + + " optimize any GCC intermediate representations would not qualify as an\n" + + " Eligible Compilation Process.\n" + + " \n" + + " 1. Grant of Additional Permission.\n" + + " \n" + + " You have permission to propagate a work of Target Code formed by\n" + + " combining the Runtime Library with Independent Modules, even if such\n" + + " propagation would otherwise violate the terms of GPLv3, provided that\n" + + " all Target Code was generated by Eligible Compilation Processes. You\n" + + " may then convey such a combination under terms of your choice,\n" + + " consistent with the licensing of the Independent Modules.\n" + + " \n" + + " 2. No Weakening of GCC Copyleft.\n" + + " \n" + + " The availability of this Exception does not imply any general\n" + + " presumption that third-party software is unaffected by the copyleft\n" + + " requirements of the license of GCC.\n" + + " \n" + + " ----\n" + + " \n" + + " GNU GENERAL PUBLIC LICENSE\n" + + " Version 3, 29 June 2007\n" + + " \n" + + " Copyright (C) 2007 Free Software Foundation, Inc. \n" + + " Everyone is permitted to copy and distribute verbatim copies\n" + + " of this license document, but changing it is not allowed.\n" + + " \n" + + " Preamble\n" + + " \n" + + " The GNU General Public License is a free, copyleft license for\n" + + " software and other kinds of works.\n" + + " \n" + + " The licenses for most software and other practical works are designed\n" + + " to take away your freedom to share and change the works. By contrast,\n" + + " the GNU General Public License is intended to guarantee your freedom to\n" + + " share and change all versions of a program--to make sure it remains free\n" + + " software for all its users. We, the Free Software Foundation, use the\n" + + " GNU General Public License for most of our software; it applies also to\n" + + " any other work released this way by its authors. You can apply it to\n" + + " your programs, too.\n" + + " \n" + + " When we speak of free software, we are referring to freedom, not\n" + + " price. Our General Public Licenses are designed to make sure that you\n" + + " have the freedom to distribute copies of free software (and charge for\n" + + " them if you wish), that you receive source code or can get it if you\n" + + " want it, that you can change the software or use pieces of it in new\n" + + " free programs, and that you know you can do these things.\n" + + " \n" + + " To protect your rights, we need to prevent others from denying you\n" + + " these rights or asking you to surrender the rights. Therefore, you have\n" + + " certain responsibilities if you distribute copies of the software, or if\n" + + " you modify it: responsibilities to respect the freedom of others.\n" + + " \n" + + " For example, if you distribute copies of such a program, whether\n" + + " gratis or for a fee, you must pass on to the recipients the same\n" + + " freedoms that you received. You must make sure that they, too, receive\n" + + " or can get the source code. And you must show them these terms so they\n" + + " know their rights.\n" + + " \n" + + " Developers that use the GNU GPL protect your rights with two steps:\n" + + " (1) assert copyright on the software, and (2) offer you this License\n" + + " giving you legal permission to copy, distribute and/or modify it.\n" + + " \n" + + " For the developers' and authors' protection, the GPL clearly explains\n" + + " that there is no warranty for this free software. For both users' and\n" + + " authors' sake, the GPL requires that modified versions be marked as\n" + + " changed, so that their problems will not be attributed erroneously to\n" + + " authors of previous versions.\n" + + " \n" + + " Some devices are designed to deny users access to install or run\n" + + " modified versions of the software inside them, although the manufacturer\n" + + " can do so. This is fundamentally incompatible with the aim of\n" + + " protecting users' freedom to change the software. The systematic\n" + + " pattern of such abuse occurs in the area of products for individuals to\n" + + " use, which is precisely where it is most unacceptable. Therefore, we\n" + + " have designed this version of the GPL to prohibit the practice for those\n" + + " products. If such problems arise substantially in other domains, we\n" + + " stand ready to extend this provision to those domains in future versions\n" + + " of the GPL, as needed to protect the freedom of users.\n" + + " \n" + + " Finally, every program is threatened constantly by software patents.\n" + + " States should not allow patents to restrict development and use of\n" + + " software on general-purpose computers, but in those that do, we wish to\n" + + " avoid the special danger that patents applied to a free program could\n" + + " make it effectively proprietary. To prevent this, the GPL assures that\n" + + " patents cannot be used to render the program non-free.\n" + + " \n" + + " The precise terms and conditions for copying, distribution and\n" + + " modification follow.\n" + + " \n" + + " TERMS AND CONDITIONS\n" + + " \n" + + " 0. Definitions.\n" + + " \n" + + " \"This License\" refers to version 3 of the GNU General Public License.\n" + + " \n" + + " \"Copyright\" also means copyright-like laws that apply to other kinds of\n" + + " works, such as semiconductor masks.\n" + + " \n" + + " \"The Program\" refers to any copyrightable work licensed under this\n" + + " License. Each licensee is addressed as \"you\". \"Licensees\" and\n" + + " \"recipients\" may be individuals or organizations.\n" + + " \n" + + " To \"modify\" a work means to copy from or adapt all or part of the work\n" + + " in a fashion requiring copyright permission, other than the making of an\n" + + " exact copy. The resulting work is called a \"modified version\" of the\n" + + " earlier work or a work \"based on\" the earlier work.\n" + + " \n" + + " A \"covered work\" means either the unmodified Program or a work based\n" + + " on the Program.\n" + + " \n" + + " To \"propagate\" a work means to do anything with it that, without\n" + + " permission, would make you directly or secondarily liable for\n" + + " infringement under applicable copyright law, except executing it on a\n" + + " computer or modifying a private copy. Propagation includes copying,\n" + + " distribution (with or without modification), making available to the\n" + + " public, and in some countries other activities as well.\n" + + " \n" + + " To \"convey\" a work means any kind of propagation that enables other\n" + + " parties to make or receive copies. Mere interaction with a user through\n" + + " a computer network, with no transfer of a copy, is not conveying.\n" + + " \n" + + " An interactive user interface displays \"Appropriate Legal Notices\"\n" + + " to the extent that it includes a convenient and prominently visible\n" + + " feature that (1) displays an appropriate copyright notice, and (2)\n" + + " tells the user that there is no warranty for the work (except to the\n" + + " extent that warranties are provided), that licensees may convey the\n" + + " work under this License, and how to view a copy of this License. If\n" + + " the interface presents a list of user commands or options, such as a\n" + + " menu, a prominent item in the list meets this criterion.\n" + + " \n" + + " 1. Source Code.\n" + + " \n" + + " The \"source code\" for a work means the preferred form of the work\n" + + " for making modifications to it. \"Object code\" means any non-source\n" + + " form of a work.\n" + + " \n" + + " A \"Standard Interface\" means an interface that either is an official\n" + + " standard defined by a recognized standards body, or, in the case of\n" + + " interfaces specified for a particular programming language, one that\n" + + " is widely used among developers working in that language.\n" + + " \n" + + " The \"System Libraries\" of an executable work include anything, other\n" + + " than the work as a whole, that (a) is included in the normal form of\n" + + " packaging a Major Component, but which is not part of that Major\n" + + " Component, and (b) serves only to enable use of the work with that\n" + + " Major Component, or to implement a Standard Interface for which an\n" + + " implementation is available to the public in source code form. A\n" + + " \"Major Component\", in this context, means a major essential component\n" + + " (kernel, window system, and so on) of the specific operating system\n" + + " (if any) on which the executable work runs, or a compiler used to\n" + + " produce the work, or an object code interpreter used to run it.\n" + + " \n" + + " The \"Corresponding Source\" for a work in object code form means all\n" + + " the source code needed to generate, install, and (for an executable\n" + + " work) run the object code and to modify the work, including scripts to\n" + + " control those activities. However, it does not include the work's\n" + + " System Libraries, or general-purpose tools or generally available free\n" + + " programs which are used unmodified in performing those activities but\n" + + " which are not part of the work. For example, Corresponding Source\n" + + " includes interface definition files associated with source files for\n" + + " the work, and the source code for shared libraries and dynamically\n" + + " linked subprograms that the work is specifically designed to require,\n" + + " such as by intimate data communication or control flow between those\n" + + " subprograms and other parts of the work.\n" + + " \n" + + " The Corresponding Source need not include anything that users\n" + + " can regenerate automatically from other parts of the Corresponding\n" + + " Source.\n" + + " \n" + + " The Corresponding Source for a work in source code form is that\n" + + " same work.\n" + + " \n" + + " 2. Basic Permissions.\n" + + " \n" + + " All rights granted under this License are granted for the term of\n" + + " copyright on the Program, and are irrevocable provided the stated\n" + + " conditions are met. This License explicitly affirms your unlimited\n" + + " permission to run the unmodified Program. The output from running a\n" + + " covered work is covered by this License only if the output, given its\n" + + " content, constitutes a covered work. This License acknowledges your\n" + + " rights of fair use or other equivalent, as provided by copyright law.\n" + + " \n" + + " You may make, run and propagate covered works that you do not\n" + + " convey, without conditions so long as your license otherwise remains\n" + + " in force. You may convey covered works to others for the sole purpose\n" + + " of having them make modifications exclusively for you, or provide you\n" + + " with facilities for running those works, provided that you comply with\n" + + " the terms of this License in conveying all material for which you do\n" + + " not control copyright. Those thus making or running the covered works\n" + + " for you must do so exclusively on your behalf, under your direction\n" + + " and control, on terms that prohibit them from making any copies of\n" + + " your copyrighted material outside their relationship with you.\n" + + " \n" + + " Conveying under any other circumstances is permitted solely under\n" + + " the conditions stated below. Sublicensing is not allowed; section 10\n" + + " makes it unnecessary.\n" + + " \n" + + " 3. Protecting Users' Legal Rights From Anti-Circumvention Law.\n" + + " \n" + + " No covered work shall be deemed part of an effective technological\n" + + " measure under any applicable law fulfilling obligations under article\n" + + " 11 of the WIPO copyright treaty adopted on 20 December 1996, or\n" + + " similar laws prohibiting or restricting circumvention of such\n" + + " measures.\n" + + " \n" + + " When you convey a covered work, you waive any legal power to forbid\n" + + " circumvention of technological measures to the extent such circumvention\n" + + " is effected by exercising rights under this License with respect to\n" + + " the covered work, and you disclaim any intention to limit operation or\n" + + " modification of the work as a means of enforcing, against the work's\n" + + " users, your or third parties' legal rights to forbid circumvention of\n" + + " technological measures.\n" + + " \n" + + " 4. Conveying Verbatim Copies.\n" + + " \n" + + " You may convey verbatim copies of the Program's source code as you\n" + + " receive it, in any medium, provided that you conspicuously and\n" + + " appropriately publish on each copy an appropriate copyright notice;\n" + + " keep intact all notices stating that this License and any\n" + + " non-permissive terms added in accord with section 7 apply to the code;\n" + + " keep intact all notices of the absence of any warranty; and give all\n" + + " recipients a copy of this License along with the Program.\n" + + " \n" + + " You may charge any price or no price for each copy that you convey,\n" + + " and you may offer support or warranty protection for a fee.\n" + + " \n" + + " 5. Conveying Modified Source Versions.\n" + + " \n" + + " You may convey a work based on the Program, or the modifications to\n" + + " produce it from the Program, in the form of source code under the\n" + + " terms of section 4, provided that you also meet all of these conditions:\n" + + " \n" + + " a) The work must carry prominent notices stating that you modified\n" + + " it, and giving a relevant date.\n" + + " \n" + + " b) The work must carry prominent notices stating that it is\n" + + " released under this License and any conditions added under section\n" + + " 7. This requirement modifies the requirement in section 4 to\n" + + " \"keep intact all notices\".\n" + + " \n" + + " c) You must license the entire work, as a whole, under this\n" + + " License to anyone who comes into possession of a copy. This\n" + + " License will therefore apply, along with any applicable section 7\n" + + " additional terms, to the whole of the work, and all its parts,\n" + + " regardless of how they are packaged. This License gives no\n" + + " permission to license the work in any other way, but it does not\n" + + " invalidate such permission if you have separately received it.\n" + + " \n" + + " d) If the work has interactive user interfaces, each must display\n" + + " Appropriate Legal Notices; however, if the Program has interactive\n" + + " interfaces that do not display Appropriate Legal Notices, your\n" + + " work need not make them do so.\n" + + " \n" + + " A compilation of a covered work with other separate and independent\n" + + " works, which are not by their nature extensions of the covered work,\n" + + " and which are not combined with it such as to form a larger program,\n" + + " in or on a volume of a storage or distribution medium, is called an\n" + + " \"aggregate\" if the compilation and its resulting copyright are not\n" + + " used to limit the access or legal rights of the compilation's users\n" + + " beyond what the individual works permit. Inclusion of a covered work\n" + + " in an aggregate does not cause this License to apply to the other\n" + + " parts of the aggregate.\n" + + " \n" + + " 6. Conveying Non-Source Forms.\n" + + " \n" + + " You may convey a covered work in object code form under the terms\n" + + " of sections 4 and 5, provided that you also convey the\n" + + " machine-readable Corresponding Source under the terms of this License,\n" + + " in one of these ways:\n" + + " \n" + + " a) Convey the object code in, or embodied in, a physical product\n" + + " (including a physical distribution medium), accompanied by the\n" + + " Corresponding Source fixed on a durable physical medium\n" + + " customarily used for software interchange.\n" + + " \n" + + " b) Convey the object code in, or embodied in, a physical product\n" + + " (including a physical distribution medium), accompanied by a\n" + + " written offer, valid for at least three years and valid for as\n" + + " long as you offer spare parts or customer support for that product\n" + + " model, to give anyone who possesses the object code either (1) a\n" + + " copy of the Corresponding Source for all the software in the\n" + + " product that is covered by this License, on a durable physical\n" + + " medium customarily used for software interchange, for a price no\n" + + " more than your reasonable cost of physically performing this\n" + + " conveying of source, or (2) access to copy the\n" + + " Corresponding Source from a network server at no charge.\n" + + " \n" + + " c) Convey individual copies of the object code with a copy of the\n" + + " written offer to provide the Corresponding Source. This\n" + + " alternative is allowed only occasionally and noncommercially, and\n" + + " only if you received the object code with such an offer, in accord\n" + + " with subsection 6b.\n" + + " \n" + + " d) Convey the object code by offering access from a designated\n" + + " place (gratis or for a charge), and offer equivalent access to the\n" + + " Corresponding Source in the same way through the same place at no\n" + + " further charge. You need not require recipients to copy the\n" + + " Corresponding Source along with the object code. If the place to\n" + + " copy the object code is a network server, the Corresponding Source\n" + + " may be on a different server (operated by you or a third party)\n" + + " that supports equivalent copying facilities, provided you maintain\n" + + " clear directions next to the object code saying where to find the\n" + + " Corresponding Source. Regardless of what server hosts the\n" + + " Corresponding Source, you remain obligated to ensure that it is\n" + + " available for as long as needed to satisfy these requirements.\n" + + " \n" + + " e) Convey the object code using peer-to-peer transmission, provided\n" + + " you inform other peers where the object code and Corresponding\n" + + " Source of the work are being offered to the general public at no\n" + + " charge under subsection 6d.\n" + + " \n" + + " A separable portion of the object code, whose source code is excluded\n" + + " from the Corresponding Source as a System Library, need not be\n" + + " included in conveying the object code work.\n" + + " \n" + + " A \"User Product\" is either (1) a \"consumer product\", which means any\n" + + " tangible personal property which is normally used for personal, family,\n" + + " or household purposes, or (2) anything designed or sold for incorporation\n" + + " into a dwelling. In determining whether a product is a consumer product,\n" + + " doubtful cases shall be resolved in favor of coverage. For a particular\n" + + " product received by a particular user, \"normally used\" refers to a\n" + + " typical or common use of that class of product, regardless of the status\n" + + " of the particular user or of the way in which the particular user\n" + + " actually uses, or expects or is expected to use, the product. A product\n" + + " is a consumer product regardless of whether the product has substantial\n" + + " commercial, industrial or non-consumer uses, unless such uses represent\n" + + " the only significant mode of use of the product.\n" + + " \n" + + " \"Installation Information\" for a User Product means any methods,\n" + + " procedures, authorization keys, or other information required to install\n" + + " and execute modified versions of a covered work in that User Product from\n" + + " a modified version of its Corresponding Source. The information must\n" + + " suffice to ensure that the continued functioning of the modified object\n" + + " code is in no case prevented or interfered with solely because\n" + + " modification has been made.\n" + + " \n" + + " If you convey an object code work under this section in, or with, or\n" + + " specifically for use in, a User Product, and the conveying occurs as\n" + + " part of a transaction in which the right of possession and use of the\n" + + " User Product is transferred to the recipient in perpetuity or for a\n" + + " fixed term (regardless of how the transaction is characterized), the\n" + + " Corresponding Source conveyed under this section must be accompanied\n" + + " by the Installation Information. But this requirement does not apply\n" + + " if neither you nor any third party retains the ability to install\n" + + " modified object code on the User Product (for example, the work has\n" + + " been installed in ROM).\n" + + " \n" + + " The requirement to provide Installation Information does not include a\n" + + " requirement to continue to provide support service, warranty, or updates\n" + + " for a work that has been modified or installed by the recipient, or for\n" + + " the User Product in which it has been modified or installed. Access to a\n" + + " network may be denied when the modification itself materially and\n" + + " adversely affects the operation of the network or violates the rules and\n" + + " protocols for communication across the network.\n" + + " \n" + + " Corresponding Source conveyed, and Installation Information provided,\n" + + " in accord with this section must be in a format that is publicly\n" + + " documented (and with an implementation available to the public in\n" + + " source code form), and must require no special password or key for\n" + + " unpacking, reading or copying.\n" + + " \n" + + " 7. Additional Terms.\n" + + " \n" + + " \"Additional permissions\" are terms that supplement the terms of this\n" + + " License by making exceptions from one or more of its conditions.\n" + + " Additional permissions that are applicable to the entire Program shall\n" + + " be treated as though they were included in this License, to the extent\n" + + " that they are valid under applicable law. If additional permissions\n" + + " apply only to part of the Program, that part may be used separately\n" + + " under those permissions, but the entire Program remains governed by\n" + + " this License without regard to the additional permissions.\n" + + " \n" + + " When you convey a copy of a covered work, you may at your option\n" + + " remove any additional permissions from that copy, or from any part of\n" + + " it. (Additional permissions may be written to require their own\n" + + " removal in certain cases when you modify the work.) You may place\n" + + " additional permissions on material, added by you to a covered work,\n" + + " for which you have or can give appropriate copyright permission.\n" + + " \n" + + " Notwithstanding any other provision of this License, for material you\n" + + " add to a covered work, you may (if authorized by the copyright holders of\n" + + " that material) supplement the terms of this License with terms:\n" + + " \n" + + " a) Disclaiming warranty or limiting liability differently from the\n" + + " terms of sections 15 and 16 of this License; or\n" + + " \n" + + " b) Requiring preservation of specified reasonable legal notices or\n" + + " author attributions in that material or in the Appropriate Legal\n" + + " Notices displayed by works containing it; or\n" + + " \n" + + " c) Prohibiting misrepresentation of the origin of that material, or\n" + + " requiring that modified versions of such material be marked in\n" + + " reasonable ways as different from the original version; or\n" + + " \n" + + " d) Limiting the use for publicity purposes of names of licensors or\n" + + " authors of the material; or\n" + + " \n" + + " e) Declining to grant rights under trademark law for use of some\n" + + " trade names, trademarks, or service marks; or\n" + + " \n" + + " f) Requiring indemnification of licensors and authors of that\n" + + " material by anyone who conveys the material (or modified versions of\n" + + " it) with contractual assumptions of liability to the recipient, for\n" + + " any liability that these contractual assumptions directly impose on\n" + + " those licensors and authors.\n" + + " \n" + + " All other non-permissive additional terms are considered \"further\n" + + " restrictions\" within the meaning of section 10. If the Program as you\n" + + " received it, or any part of it, contains a notice stating that it is\n" + + " governed by this License along with a term that is a further\n" + + " restriction, you may remove that term. If a license document contains\n" + + " a further restriction but permits relicensing or conveying under this\n" + + " License, you may add to a covered work material governed by the terms\n" + + " of that license document, provided that the further restriction does\n" + + " not survive such relicensing or conveying.\n" + + " \n" + + " If you add terms to a covered work in accord with this section, you\n" + + " must place, in the relevant source files, a statement of the\n" + + " additional terms that apply to those files, or a notice indicating\n" + + " where to find the applicable terms.\n" + + " \n" + + " Additional terms, permissive or non-permissive, may be stated in the\n" + + " form of a separately written license, or stated as exceptions;\n" + + " the above requirements apply either way.\n" + + " \n" + + " 8. Termination.\n" + + " \n" + + " You may not propagate or modify a covered work except as expressly\n" + + " provided under this License. Any attempt otherwise to propagate or\n" + + " modify it is void, and will automatically terminate your rights under\n" + + " this License (including any patent licenses granted under the third\n" + + " paragraph of section 11).\n" + + " \n" + + " However, if you cease all violation of this License, then your\n" + + " license from a particular copyright holder is reinstated (a)\n" + + " provisionally, unless and until the copyright holder explicitly and\n" + + " finally terminates your license, and (b) permanently, if the copyright\n" + + " holder fails to notify you of the violation by some reasonable means\n" + + " prior to 60 days after the cessation.\n" + + " \n" + + " Moreover, your license from a particular copyright holder is\n" + + " reinstated permanently if the copyright holder notifies you of the\n" + + " violation by some reasonable means, this is the first time you have\n" + + " received notice of violation of this License (for any work) from that\n" + + " copyright holder, and you cure the violation prior to 30 days after\n" + + " your receipt of the notice.\n" + + " \n" + + " Termination of your rights under this section does not terminate the\n" + + " licenses of parties who have received copies or rights from you under\n" + + " this License. If your rights have been terminated and not permanently\n" + + " reinstated, you do not qualify to receive new licenses for the same\n" + + " material under section 10.\n" + + " \n" + + " 9. Acceptance Not Required for Having Copies.\n" + + " \n" + + " You are not required to accept this License in order to receive or\n" + + " run a copy of the Program. Ancillary propagation of a covered work\n" + + " occurring solely as a consequence of using peer-to-peer transmission\n" + + " to receive a copy likewise does not require acceptance. However,\n" + + " nothing other than this License grants you permission to propagate or\n" + + " modify any covered work. These actions infringe copyright if you do\n" + + " not accept this License. Therefore, by modifying or propagating a\n" + + " covered work, you indicate your acceptance of this License to do so.\n" + + " \n" + + " 10. Automatic Licensing of Downstream Recipients.\n" + + " \n" + + " Each time you convey a covered work, the recipient automatically\n" + + " receives a license from the original licensors, to run, modify and\n" + + " propagate that work, subject to this License. You are not responsible\n" + + " for enforcing compliance by third parties with this License.\n" + + " \n" + + " An \"entity transaction\" is a transaction transferring control of an\n" + + " organization, or substantially all assets of one, or subdividing an\n" + + " organization, or merging organizations. If propagation of a covered\n" + + " work results from an entity transaction, each party to that\n" + + " transaction who receives a copy of the work also receives whatever\n" + + " licenses to the work the party's predecessor in interest had or could\n" + + " give under the previous paragraph, plus a right to possession of the\n" + + " Corresponding Source of the work from the predecessor in interest, if\n" + + " the predecessor has it or can get it with reasonable efforts.\n" + + " \n" + + " You may not impose any further restrictions on the exercise of the\n" + + " rights granted or affirmed under this License. For example, you may\n" + + " not impose a license fee, royalty, or other charge for exercise of\n" + + " rights granted under this License, and you may not initiate litigation\n" + + " (including a cross-claim or counterclaim in a lawsuit) alleging that\n" + + " any patent claim is infringed by making, using, selling, offering for\n" + + " sale, or importing the Program or any portion of it.\n" + + " \n" + + " 11. Patents.\n" + + " \n" + + " A \"contributor\" is a copyright holder who authorizes use under this\n" + + " License of the Program or a work on which the Program is based. The\n" + + " work thus licensed is called the contributor's \"contributor version\".\n" + + " \n" + + " A contributor's \"essential patent claims\" are all patent claims\n" + + " owned or controlled by the contributor, whether already acquired or\n" + + " hereafter acquired, that would be infringed by some manner, permitted\n" + + " by this License, of making, using, or selling its contributor version,\n" + + " but do not include claims that would be infringed only as a\n" + + " consequence of further modification of the contributor version. For\n" + + " purposes of this definition, \"control\" includes the right to grant\n" + + " patent sublicenses in a manner consistent with the requirements of\n" + + " this License.\n" + + " \n" + + " Each contributor grants you a non-exclusive, worldwide, royalty-free\n" + + " patent license under the contributor's essential patent claims, to\n" + + " make, use, sell, offer for sale, import and otherwise run, modify and\n" + + " propagate the contents of its contributor version.\n" + + " \n" + + " In the following three paragraphs, a \"patent license\" is any express\n" + + " agreement or commitment, however denominated, not to enforce a patent\n" + + " (such as an express permission to practice a patent or covenant not to\n" + + " sue for patent infringement). To \"grant\" such a patent license to a\n" + + " party means to make such an agreement or commitment not to enforce a\n" + + " patent against the party.\n" + + " \n" + + " If you convey a covered work, knowingly relying on a patent license,\n" + + " and the Corresponding Source of the work is not available for anyone\n" + + " to copy, free of charge and under the terms of this License, through a\n" + + " publicly available network server or other readily accessible means,\n" + + " then you must either (1) cause the Corresponding Source to be so\n" + + " available, or (2) arrange to deprive yourself of the benefit of the\n" + + " patent license for this particular work, or (3) arrange, in a manner\n" + + " consistent with the requirements of this License, to extend the patent\n" + + " license to downstream recipients. \"Knowingly relying\" means you have\n" + + " actual knowledge that, but for the patent license, your conveying the\n" + + " covered work in a country, or your recipient's use of the covered work\n" + + " in a country, would infringe one or more identifiable patents in that\n" + + " country that you have reason to believe are valid.\n" + + " \n" + + " If, pursuant to or in connection with a single transaction or\n" + + " arrangement, you convey, or propagate by procuring conveyance of, a\n" + + " covered work, and grant a patent license to some of the parties\n" + + " receiving the covered work authorizing them to use, propagate, modify\n" + + " or convey a specific copy of the covered work, then the patent license\n" + + " you grant is automatically extended to all recipients of the covered\n" + + " work and works based on it.\n" + + " \n" + + " A patent license is \"discriminatory\" if it does not include within\n" + + " the scope of its coverage, prohibits the exercise of, or is\n" + + " conditioned on the non-exercise of one or more of the rights that are\n" + + " specifically granted under this License. You may not convey a covered\n" + + " work if you are a party to an arrangement with a third party that is\n" + + " in the business of distributing software, under which you make payment\n" + + " to the third party based on the extent of your activity of conveying\n" + + " the work, and under which the third party grants, to any of the\n" + + " parties who would receive the covered work from you, a discriminatory\n" + + " patent license (a) in connection with copies of the covered work\n" + + " conveyed by you (or copies made from those copies), or (b) primarily\n" + + " for and in connection with specific products or compilations that\n" + + " contain the covered work, unless you entered into that arrangement,\n" + + " or that patent license was granted, prior to 28 March 2007.\n" + + " \n" + + " Nothing in this License shall be construed as excluding or limiting\n" + + " any implied license or other defenses to infringement that may\n" + + " otherwise be available to you under applicable patent law.\n" + + " \n" + + " 12. No Surrender of Others' Freedom.\n" + + " \n" + + " If conditions are imposed on you (whether by court order, agreement or\n" + + " otherwise) that contradict the conditions of this License, they do not\n" + + " excuse you from the conditions of this License. If you cannot convey a\n" + + " covered work so as to satisfy simultaneously your obligations under this\n" + + " License and any other pertinent obligations, then as a consequence you may\n" + + " not convey it at all. For example, if you agree to terms that obligate you\n" + + " to collect a royalty for further conveying from those to whom you convey\n" + + " the Program, the only way you could satisfy both those terms and this\n" + + " License would be to refrain entirely from conveying the Program.\n" + + " \n" + + " 13. Use with the GNU Affero General Public License.\n" + + " \n" + + " Notwithstanding any other provision of this License, you have\n" + + " permission to link or combine any covered work with a work licensed\n" + + " under version 3 of the GNU Affero General Public License into a single\n" + + " combined work, and to convey the resulting work. The terms of this\n" + + " License will continue to apply to the part which is the covered work,\n" + + " but the special requirements of the GNU Affero General Public License,\n" + + " section 13, concerning interaction through a network will apply to the\n" + + " combination as such.\n" + + " \n" + + " 14. Revised Versions of this License.\n" + + " \n" + + " The Free Software Foundation may publish revised and/or new versions of\n" + + " the GNU General Public License from time to time. Such new versions will\n" + + " be similar in spirit to the present version, but may differ in detail to\n" + + " address new problems or concerns.\n" + + " \n" + + " Each version is given a distinguishing version number. If the\n" + + " Program specifies that a certain numbered version of the GNU General\n" + + " Public License \"or any later version\" applies to it, you have the\n" + + " option of following the terms and conditions either of that numbered\n" + + " version or of any later version published by the Free Software\n" + + " Foundation. If the Program does not specify a version number of the\n" + + " GNU General Public License, you may choose any version ever published\n" + + " by the Free Software Foundation.\n" + + " \n" + + " If the Program specifies that a proxy can decide which future\n" + + " versions of the GNU General Public License can be used, that proxy's\n" + + " public statement of acceptance of a version permanently authorizes you\n" + + " to choose that version for the Program.\n" + + " \n" + + " Later license versions may give you additional or different\n" + + " permissions. However, no additional obligations are imposed on any\n" + + " author or copyright holder as a result of your choosing to follow a\n" + + " later version.\n" + + " \n" + + " 15. Disclaimer of Warranty.\n" + + " \n" + + " THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY\n" + + " APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT\n" + + " HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM \"AS IS\" WITHOUT WARRANTY\n" + + " OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO,\n" + + " THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR\n" + + " PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM\n" + + " IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF\n" + + " ALL NECESSARY SERVICING, REPAIR OR CORRECTION.\n" + + " \n" + + " 16. Limitation of Liability.\n" + + " \n" + + " IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING\n" + + " WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS\n" + + " THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY\n" + + " GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE\n" + + " USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF\n" + + " DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD\n" + + " PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS),\n" + + " EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF\n" + + " SUCH DAMAGES.\n" + + " \n" + + " 17. Interpretation of Sections 15 and 16.\n" + + " \n" + + " If the disclaimer of warranty and limitation of liability provided\n" + + " above cannot be given local legal effect according to their terms,\n" + + " reviewing courts shall apply local law that most closely approximates\n" + + " an absolute waiver of all civil liability in connection with the\n" + + " Program, unless a warranty or assumption of liability accompanies a\n" + + " copy of the Program in return for a fee.\n" + + " \n" + + " END OF TERMS AND CONDITIONS\n" + + " \n" + + " How to Apply These Terms to Your New Programs\n" + + " \n" + + " If you develop a new program, and you want it to be of the greatest\n" + + " possible use to the public, the best way to achieve this is to make it\n" + + " free software which everyone can redistribute and change under these terms.\n" + + " \n" + + " To do so, attach the following notices to the program. It is safest\n" + + " to attach them to the start of each source file to most effectively\n" + + " state the exclusion of warranty; and each file should have at least\n" + + " the \"copyright\" line and a pointer to where the full notice is found.\n" + + " \n" + + " \n" + + " Copyright (C) \n" + + " \n" + + " This program is free software: you can redistribute it and/or modify\n" + + " it under the terms of the GNU General Public License as published by\n" + + " the Free Software Foundation, either version 3 of the License, or\n" + + " (at your option) any later version.\n" + + " \n" + + " This program is distributed in the hope that it will be useful,\n" + + " but WITHOUT ANY WARRANTY; without even the implied warranty of\n" + + " MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the\n" + + " GNU General Public License for more details.\n" + + " \n" + + " You should have received a copy of the GNU General Public License\n" + + " along with this program. If not, see .\n" + + " \n" + + " Also add information on how to contact you by electronic and paper mail.\n" + + " \n" + + " If the program does terminal interaction, make it output a short\n" + + " notice like this when it starts in an interactive mode:\n" + + " \n" + + " Copyright (C) \n" + + " This program comes with ABSOLUTELY NO WARRANTY; for details type `show w'.\n" + + " This is free software, and you are welcome to redistribute it\n" + + " under certain conditions; type `show c' for details.\n" + + " \n" + + " The hypothetical commands `show w' and `show c' should show the appropriate\n" + + " parts of the General Public License. Of course, your program's commands\n" + + " might be different; for a GUI interface, you would use an \"about box\".\n" + + " \n" + + " You should also get your employer (if you work as a programmer) or school,\n" + + " if any, to sign a \"copyright disclaimer\" for the program, if necessary.\n" + + " For more information on this, and how to apply and follow the GNU GPL, see\n" + + " .\n" + + " \n" + + " The GNU General Public License does not permit incorporating your program\n" + + " into proprietary programs. If your program is a subroutine library, you\n" + + " may consider it more useful to permit linking proprietary applications with\n" + + " the library. If this is what you want to do, use the GNU Lesser General\n" + + " Public License instead of this License. But first, please read\n" + + " .\n" + + " \n" + + " \n" + + " Name: libquadmath\n" + + " Files: scipy/.dylibs/libquadmath*.so\n" + + " Description: dynamically linked to files compiled with gcc\n" + + " Availability: https://gcc.gnu.org/git/?p=gcc.git;a=tree;f=libquadmath\n" + + " License: LGPL-2.1-or-later\n" + + " \n" + + " GCC Quad-Precision Math Library\n" + + " Copyright (C) 2010-2019 Free Software Foundation, Inc.\n" + + " Written by Francois-Xavier Coudert \n" + + " \n" + + " This file is part of the libquadmath library.\n" + + " Libquadmath is free software; you can redistribute it and/or\n" + + " modify it under the terms of the GNU Library General Public\n" + + " License as published by the Free Software Foundation; either\n" + + " version 2.1 of the License, or (at your option) any later version.\n" + + " \n" + + " Libquadmath is distributed in the hope that it will be useful,\n" + + " but WITHOUT ANY WARRANTY; without even the implied warranty of\n" + + " MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU\n" + + " Lesser General Public License for more details.\n" + + " https://www.gnu.org/licenses/old-licenses/lgpl-2.1.html\n" + + "Location: /Users/abc/Library/Python/3.9/lib/python/site-packages\n" + + "Requires: numpy\n" + + "Required-by: gensim"); - EXPECTED_PIP_SHOW_RESULTS.add("Name: six\n" + - "Version: 1.16.0\n" + - "Summary: Python 2 and 3 compatibility utilities\n" + - "Home-page: https://github.com/benjaminp/six\n" + - "Author: Benjamin Peterson\n" + - "Author-email: benjamin@python.org\n" + - "License: MIT\n" + - "Location: /Users/abc/Library/Python/3.9/lib/python/site-packages\n" + - "Requires: \n" + - "Required-by: cycler, gensim, gTTS, python-dateutil, tweepy\n"); - } + EXPECTED_PIP_SHOW_RESULTS.add("Name: six\n" + "Version: 1.16.0\n" + + "Summary: Python 2 and 3 compatibility utilities\n" + + "Home-page: https://github.com/benjaminp/six\n" + + "Author: Benjamin Peterson\n" + + "Author-email: benjamin@python.org\n" + + "License: MIT\n" + + "Location: /Users/abc/Library/Python/3.9/lib/python/site-packages\n" + + "Requires: \n" + + "Required-by: cycler, gensim, gTTS, python-dateutil, tweepy\n"); + } } diff --git a/src/test/java/com/redhat/exhort/utils/PythonControllerRealEnvTest.java b/src/test/java/com/redhat/exhort/utils/PythonControllerRealEnvTest.java index 68eebd72..f97315f1 100644 --- a/src/test/java/com/redhat/exhort/utils/PythonControllerRealEnvTest.java +++ b/src/test/java/com/redhat/exhort/utils/PythonControllerRealEnvTest.java @@ -15,276 +15,295 @@ */ package com.redhat.exhort.utils; +import static com.redhat.exhort.utils.PythonControllerBaseTest.matchCommandPipFreeze; +import static com.redhat.exhort.utils.PythonControllerBaseTest.matchCommandPipShow; +import static org.junit.jupiter.api.Assertions.*; +import static org.mockito.ArgumentMatchers.*; + import com.redhat.exhort.ExhortTest; import com.redhat.exhort.tools.Operations; +import java.nio.file.Path; +import java.util.*; +import java.util.stream.Collectors; import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.junit.jupiter.params.ParameterizedTest; import org.junit.jupiter.params.provider.ValueSource; -import org.mockito.ArgumentMatcher; import org.mockito.MockedStatic; import org.mockito.Mockito; -import java.nio.file.Path; -import java.util.*; -import java.util.stream.Collectors; - -import static com.redhat.exhort.utils.PythonControllerBaseTest.matchCommandPipFreeze; -import static com.redhat.exhort.utils.PythonControllerBaseTest.matchCommandPipShow; -import static org.junit.jupiter.api.Assertions.*; -import static org.mockito.ArgumentMatchers.*; - class PythonControllerRealEnvTest extends ExhortTest { - private static PythonControllerRealEnv pythonControllerRealEnv; - private final String PIP_FREEZE_LINES_CYCLIC = getStringFromFile("msc","python","pip_freeze_lines_cyclic.txt"); - private final String PIP_SHOW_LINES_CYCLIC = getStringFromFile("msc","python","pip_show_lines_cyclic.txt"); - -// ArgumentMatcher matchCommandPipFreeze = new ArgumentMatcher() { -// @Override -// public boolean matches(String[] command) { -// return Arrays.stream(command).anyMatch(word -> word.contains("freeze")); -// } -// // in var args, must override type default method' void.class in argumentMatcher interface in order to let custom ArgumentMatcher work correctly. -// @Override -// public Class type() -// { -// return String[].class; -// } -// -// }; -// -// ArgumentMatcher matchCommandPipShow = new ArgumentMatcher() { -// @Override -// public boolean matches(String[] command) { -// return Arrays.stream(command).anyMatch(word -> word.contains("show")); -// } -// -// @Override -// public Class type() -// { -// return String[].class; -// } -// -// }; - - - @BeforeEach - void setUp() { - pythonControllerRealEnv = new PythonControllerRealEnv("python3","pip3"); - } - - @AfterEach - void tearDown() { - } - - - @ParameterizedTest - @ValueSource(booleans = { true,false }) - void get_Dependencies_With_Match_Manifest_Versions(boolean MatchManifestVersionsEnabled) { - Set expectedSetOfPackages = Set.of("click", "flask", "importlib-metadata", "zipp", "itsdangerous", "jinja2", "MarkupSafe", "Werkzeug", "dataclasses", "typing-extensions"); - MockedStatic operationsMockedStatic = Mockito.mockStatic(Operations.class); - String requirementsPath = getFileFromString("requirements.txt", "Flask==2.0.3\nclick==8.0.5\n"); - String pipFreeze = "click==8.0.4\nflask==2.0.3\nimportlib-metadata==4.8.3\nzipp==3.6.0\nitsdangerous==2.0.1\njinja2==3.0.3\nMarkupSafe==2.0.1\nWerkzeug==2.0.3\ndataclasses==0.8\ntyping_extensions==4.1.1\n"; - String pipShowResults = "Name: click\n" + - "Version: 8.0.4\n" + - "Summary: Composable command line interface toolkit\n" + - "Home-page: https://palletsprojects.com/p/click/\n" + - "Author: Armin Ronacher\n" + - "Author-email: armin.ronacher@active-4.com\n" + - "License: BSD-3-Clause\n" + - "Location: /usr/local/lib/python3.6/site-packages\n" + - "Requires: importlib-metadata\n" + - "Required-by: Flask, uvicorn\n" + - "---\n" + - "Name: Flask\n" + - "Version: 2.0.3\n" + - "Summary: A simple framework for building complex web applications.\n" + - "Home-page: https://palletsprojects.com/p/flask\n" + - "Author: Armin Ronacher\n" + - "Author-email: armin.ronacher@active-4.com\n" + - "License: BSD-3-Clause\n" + - "Location: /usr/local/lib/python3.6/site-packages\n" + - "Requires: click, itsdangerous, Jinja2, Werkzeug\n" + - "Required-by: \n" + - "---\n" + - "Name: importlib-metadata\n" + - "Version: 4.8.3\n" + - "Summary: Read metadata from Python packages\n" + - "Home-page: https://github.com/python/importlib_metadata\n" + - "Author: Jason R. Coombs\n" + - "Author-email: jaraco@jaraco.com\n" + - "License: UNKNOWN\n" + - "Location: /usr/local/lib/python3.6/site-packages\n" + - "Requires: typing-extensions, zipp\n" + - "Required-by: click, cyclonedx-bom, cyclonedx-python-lib\n" + - "---\n" + - "Name: zipp\n" + - "Version: 3.6.0\n" + - "Summary: Backport of pathlib-compatible object wrapper for zip files\n" + - "Home-page: https://github.com/jaraco/zipp\n" + - "Author: Jason R. Coombs\n" + - "Author-email: jaraco@jaraco.com\n" + - "License: UNKNOWN\n" + - "Location: /usr/local/lib/python3.6/site-packages\n" + - "Requires: \n" + - "Required-by: importlib-metadata\n" + - "---\n" + - "Name: itsdangerous\n" + - "Version: 2.0.1\n" + - "Summary: Safely pass data to untrusted environments and back.\n" + - "Home-page: https://palletsprojects.com/p/itsdangerous/\n" + - "Author: Armin Ronacher\n" + - "Author-email: armin.ronacher@active-4.com\n" + - "License: BSD-3-Clause\n" + - "Location: /usr/local/lib/python3.6/site-packages\n" + - "Requires: \n" + - "Required-by: Flask\n" + - "---\n" + - "Name: Jinja2\n" + - "Version: 3.0.3\n" + - "Summary: A very fast and expressive template engine.\n" + - "Home-page: https://palletsprojects.com/p/jinja/\n" + - "Author: Armin Ronacher\n" + - "Author-email: armin.ronacher@active-4.com\n" + - "License: BSD-3-Clause\n" + - "Location: /home/zgrinber/.local/lib/python3.6/site-packages\n" + - "Requires: MarkupSafe\n" + - "Required-by: ansible-core, Flask\n" + - "---\n" + - "Name: MarkupSafe\n" + - "Version: 2.0.1\n" + - "Summary: Safely add untrusted strings to HTML/XML markup.\n" + - "Home-page: https://palletsprojects.com/p/markupsafe/\n" + - "Author: Armin Ronacher\n" + - "Author-email: armin.ronacher@active-4.com\n" + - "License: BSD-3-Clause\n" + - "Location: /home/zgrinber/.local/lib/python3.6/site-packages\n" + - "Requires: \n" + - "Required-by: Jinja2, Mako\n" + - "---\n" + - "Name: Werkzeug\n" + - "Version: 2.0.3\n" + - "Summary: The comprehensive WSGI web application library.\n" + - "Home-page: https://palletsprojects.com/p/werkzeug/\n" + - "Author: Armin Ronacher\n" + - "Author-email: armin.ronacher@active-4.com\n" + - "License: BSD-3-Clause\n" + - "Location: /usr/local/lib/python3.6/site-packages\n" + - "Requires: dataclasses\n" + - "Required-by: Flask\n" + - "---\n" + - "Name: dataclasses\n" + - "Version: 0.8\n" + - "Summary: A backport of the dataclasses module for Python 3.6\n" + - "Home-page: https://github.com/ericvsmith/dataclasses\n" + - "Author: Eric V. Smith\n" + - "Author-email: eric@python.org\n" + - "License: Apache\n" + - "Location: /usr/local/lib/python3.6/site-packages\n" + - "Requires: \n" + - "Required-by: anyio, h11, pydantic, Werkzeug\n" + - "---\n" + - "Name: typing_extensions\n" + - "Version: 4.1.1\n" + - "Summary: Backported and Experimental Type Hints for Python 3.6+\n" + - "Home-page: \n" + - "Author: \n" + - "Author-email: \"Guido van Rossum, Jukka Lehtosalo, Łukasz Langa, Michael Lee\" \n" + - "License: \n" + - "Location: /usr/local/lib/python3.6/site-packages\n" + - "Requires: \n" + - "Required-by: anyio, asgiref, h11, immutables, importlib-metadata, pydantic, starlette, uvicorn\n"; - - operationsMockedStatic.when(() -> Operations.runProcessGetOutput(any(Path.class), argThat(matchCommandPipFreeze))).thenReturn(pipFreeze); - operationsMockedStatic.when(() -> Operations.runProcessGetOutput(any(Path.class), argThat(matchCommandPipShow))).thenReturn(pipShowResults); - if (!MatchManifestVersionsEnabled) { - System.setProperty("MATCH_MANIFEST_VERSIONS", "false"); + private static PythonControllerRealEnv pythonControllerRealEnv; + private final String PIP_FREEZE_LINES_CYCLIC = getStringFromFile("msc", "python", "pip_freeze_lines_cyclic.txt"); + private final String PIP_SHOW_LINES_CYCLIC = getStringFromFile("msc", "python", "pip_show_lines_cyclic.txt"); + + // ArgumentMatcher matchCommandPipFreeze = new ArgumentMatcher() { + // @Override + // public boolean matches(String[] command) { + // return Arrays.stream(command).anyMatch(word -> word.contains("freeze")); + // } + // // in var args, must override type default method' void.class in argumentMatcher interface in order to let + // custom ArgumentMatcher work correctly. + // @Override + // public Class type() + // { + // return String[].class; + // } + // + // }; + // + // ArgumentMatcher matchCommandPipShow = new ArgumentMatcher() { + // @Override + // public boolean matches(String[] command) { + // return Arrays.stream(command).anyMatch(word -> word.contains("show")); + // } + // + // @Override + // public Class type() + // { + // return String[].class; + // } + // + // }; + + @BeforeEach + void setUp() { + pythonControllerRealEnv = new PythonControllerRealEnv("python3", "pip3"); } - if (MatchManifestVersionsEnabled) { - RuntimeException runtimeException = assertThrows(RuntimeException.class, () -> pythonControllerRealEnv.getDependencies(requirementsPath, true), "Expected getDependencies/2 to throw RuntimeException, due to version mismatch, but it didn't."); - operationsMockedStatic.close(); - assertTrue(runtimeException.getMessage().contains("Can't continue with analysis - versions mismatch for dependency name=click, manifest version=8.0.5, installed Version=8.0.4")); - } - else - { - - List> dependencies = pythonControllerRealEnv.getDependencies(requirementsPath, true); - System.clearProperty("MATCH_MANIFEST_VERSIONS"); - // collect all packages returned from getDependencies into Set. - System.out.println(dependencies); - Set actualSetOfPackages = new HashSet(); - dependencies.forEach( entry -> { - accumulateAllPackages(entry,actualSetOfPackages); - }); - - // Check that all actual collected packages are exactly the ones that are expected - Set expectedSetOfPackagesLC = expectedSetOfPackages.stream().map(packageName -> packageName.replace("_","-")).map(String::toLowerCase).collect(Collectors.toSet()); - - Set actualSetOfPackagesLC = actualSetOfPackages.stream().map(packageName -> packageName.replace("_","-")).map(String::toLowerCase).collect(Collectors.toSet()); - assertTrue(actualSetOfPackagesLC.containsAll(expectedSetOfPackagesLC)); - assertTrue(expectedSetOfPackagesLC.containsAll(actualSetOfPackagesLC)); - operationsMockedStatic.close(); + @AfterEach + void tearDown() {} + + @ParameterizedTest + @ValueSource(booleans = {true, false}) + void get_Dependencies_With_Match_Manifest_Versions(boolean MatchManifestVersionsEnabled) { + Set expectedSetOfPackages = Set.of( + "click", + "flask", + "importlib-metadata", + "zipp", + "itsdangerous", + "jinja2", + "MarkupSafe", + "Werkzeug", + "dataclasses", + "typing-extensions"); + MockedStatic operationsMockedStatic = Mockito.mockStatic(Operations.class); + String requirementsPath = getFileFromString("requirements.txt", "Flask==2.0.3\nclick==8.0.5\n"); + String pipFreeze = + "click==8.0.4\nflask==2.0.3\nimportlib-metadata==4.8.3\nzipp==3.6.0\nitsdangerous==2.0.1\njinja2==3.0.3\nMarkupSafe==2.0.1\nWerkzeug==2.0.3\ndataclasses==0.8\ntyping_extensions==4.1.1\n"; + String pipShowResults = "Name: click\n" + "Version: 8.0.4\n" + + "Summary: Composable command line interface toolkit\n" + + "Home-page: https://palletsprojects.com/p/click/\n" + + "Author: Armin Ronacher\n" + + "Author-email: armin.ronacher@active-4.com\n" + + "License: BSD-3-Clause\n" + + "Location: /usr/local/lib/python3.6/site-packages\n" + + "Requires: importlib-metadata\n" + + "Required-by: Flask, uvicorn\n" + + "---\n" + + "Name: Flask\n" + + "Version: 2.0.3\n" + + "Summary: A simple framework for building complex web applications.\n" + + "Home-page: https://palletsprojects.com/p/flask\n" + + "Author: Armin Ronacher\n" + + "Author-email: armin.ronacher@active-4.com\n" + + "License: BSD-3-Clause\n" + + "Location: /usr/local/lib/python3.6/site-packages\n" + + "Requires: click, itsdangerous, Jinja2, Werkzeug\n" + + "Required-by: \n" + + "---\n" + + "Name: importlib-metadata\n" + + "Version: 4.8.3\n" + + "Summary: Read metadata from Python packages\n" + + "Home-page: https://github.com/python/importlib_metadata\n" + + "Author: Jason R. Coombs\n" + + "Author-email: jaraco@jaraco.com\n" + + "License: UNKNOWN\n" + + "Location: /usr/local/lib/python3.6/site-packages\n" + + "Requires: typing-extensions, zipp\n" + + "Required-by: click, cyclonedx-bom, cyclonedx-python-lib\n" + + "---\n" + + "Name: zipp\n" + + "Version: 3.6.0\n" + + "Summary: Backport of pathlib-compatible object wrapper for zip files\n" + + "Home-page: https://github.com/jaraco/zipp\n" + + "Author: Jason R. Coombs\n" + + "Author-email: jaraco@jaraco.com\n" + + "License: UNKNOWN\n" + + "Location: /usr/local/lib/python3.6/site-packages\n" + + "Requires: \n" + + "Required-by: importlib-metadata\n" + + "---\n" + + "Name: itsdangerous\n" + + "Version: 2.0.1\n" + + "Summary: Safely pass data to untrusted environments and back.\n" + + "Home-page: https://palletsprojects.com/p/itsdangerous/\n" + + "Author: Armin Ronacher\n" + + "Author-email: armin.ronacher@active-4.com\n" + + "License: BSD-3-Clause\n" + + "Location: /usr/local/lib/python3.6/site-packages\n" + + "Requires: \n" + + "Required-by: Flask\n" + + "---\n" + + "Name: Jinja2\n" + + "Version: 3.0.3\n" + + "Summary: A very fast and expressive template engine.\n" + + "Home-page: https://palletsprojects.com/p/jinja/\n" + + "Author: Armin Ronacher\n" + + "Author-email: armin.ronacher@active-4.com\n" + + "License: BSD-3-Clause\n" + + "Location: /home/zgrinber/.local/lib/python3.6/site-packages\n" + + "Requires: MarkupSafe\n" + + "Required-by: ansible-core, Flask\n" + + "---\n" + + "Name: MarkupSafe\n" + + "Version: 2.0.1\n" + + "Summary: Safely add untrusted strings to HTML/XML markup.\n" + + "Home-page: https://palletsprojects.com/p/markupsafe/\n" + + "Author: Armin Ronacher\n" + + "Author-email: armin.ronacher@active-4.com\n" + + "License: BSD-3-Clause\n" + + "Location: /home/zgrinber/.local/lib/python3.6/site-packages\n" + + "Requires: \n" + + "Required-by: Jinja2, Mako\n" + + "---\n" + + "Name: Werkzeug\n" + + "Version: 2.0.3\n" + + "Summary: The comprehensive WSGI web application library.\n" + + "Home-page: https://palletsprojects.com/p/werkzeug/\n" + + "Author: Armin Ronacher\n" + + "Author-email: armin.ronacher@active-4.com\n" + + "License: BSD-3-Clause\n" + + "Location: /usr/local/lib/python3.6/site-packages\n" + + "Requires: dataclasses\n" + + "Required-by: Flask\n" + + "---\n" + + "Name: dataclasses\n" + + "Version: 0.8\n" + + "Summary: A backport of the dataclasses module for Python 3.6\n" + + "Home-page: https://github.com/ericvsmith/dataclasses\n" + + "Author: Eric V. Smith\n" + + "Author-email: eric@python.org\n" + + "License: Apache\n" + + "Location: /usr/local/lib/python3.6/site-packages\n" + + "Requires: \n" + + "Required-by: anyio, h11, pydantic, Werkzeug\n" + + "---\n" + + "Name: typing_extensions\n" + + "Version: 4.1.1\n" + + "Summary: Backported and Experimental Type Hints for Python 3.6+\n" + + "Home-page: \n" + + "Author: \n" + + "Author-email: \"Guido van Rossum, Jukka Lehtosalo, Łukasz Langa, Michael Lee\" \n" + + "License: \n" + + "Location: /usr/local/lib/python3.6/site-packages\n" + + "Requires: \n" + + "Required-by: anyio, asgiref, h11, immutables, importlib-metadata, pydantic, starlette, uvicorn\n"; + + operationsMockedStatic + .when(() -> Operations.runProcessGetOutput(any(Path.class), argThat(matchCommandPipFreeze))) + .thenReturn(pipFreeze); + operationsMockedStatic + .when(() -> Operations.runProcessGetOutput(any(Path.class), argThat(matchCommandPipShow))) + .thenReturn(pipShowResults); + if (!MatchManifestVersionsEnabled) { + System.setProperty("MATCH_MANIFEST_VERSIONS", "false"); + } + if (MatchManifestVersionsEnabled) { + RuntimeException runtimeException = assertThrows( + RuntimeException.class, + () -> pythonControllerRealEnv.getDependencies(requirementsPath, true), + "Expected getDependencies/2 to throw RuntimeException, due to version mismatch, but it didn't."); + operationsMockedStatic.close(); + assertTrue( + runtimeException + .getMessage() + .contains( + "Can't continue with analysis - versions mismatch for dependency name=click, manifest version=8.0.5, installed Version=8.0.4")); + } else { + + List> dependencies = pythonControllerRealEnv.getDependencies(requirementsPath, true); + System.clearProperty("MATCH_MANIFEST_VERSIONS"); + // collect all packages returned from getDependencies into Set. + System.out.println(dependencies); + Set actualSetOfPackages = new HashSet(); + dependencies.forEach(entry -> { + accumulateAllPackages(entry, actualSetOfPackages); + }); + + // Check that all actual collected packages are exactly the ones that are expected + Set expectedSetOfPackagesLC = expectedSetOfPackages.stream() + .map(packageName -> packageName.replace("_", "-")) + .map(String::toLowerCase) + .collect(Collectors.toSet()); + + Set actualSetOfPackagesLC = actualSetOfPackages.stream() + .map(packageName -> packageName.replace("_", "-")) + .map(String::toLowerCase) + .collect(Collectors.toSet()); + assertTrue(actualSetOfPackagesLC.containsAll(expectedSetOfPackagesLC)); + assertTrue(expectedSetOfPackagesLC.containsAll(actualSetOfPackagesLC)); + operationsMockedStatic.close(); + } } - } - - private void accumulateAllPackages(Map entry, Set actualSetOfPackages) { - actualSetOfPackages.add(entry.get("name")); - if(entry.get("dependencies") != null) - { - ((List>)entry.get("dependencies")).stream().forEach( record -> - { - accumulateAllPackages(record,actualSetOfPackages); - }); + private void accumulateAllPackages(Map entry, Set actualSetOfPackages) { + actualSetOfPackages.add(entry.get("name")); + if (entry.get("dependencies") != null) { + ((List>) entry.get("dependencies")).stream().forEach(record -> { + accumulateAllPackages(record, actualSetOfPackages); + }); + } } - } - - @Test - void get_Dependencies_from_Cyclic_Tree() { - MockedStatic operationsMockedStatic = Mockito.mockStatic(Operations.class); -// ArgumentMatcher matchCommandPipFreeze = command -> Arrays.stream(command).anyMatch(word -> word.contains("freeze")); - - operationsMockedStatic.when(() -> Operations.runProcessGetOutput(any(Path.class),argThat(matchCommandPipFreeze))).thenReturn(PIP_FREEZE_LINES_CYCLIC); -// operationsMockedStatic.when(() -> Operations.runProcessGetOutput(any(Path.class),any(String[].class))).thenReturn(PIP_FREEZE_LINES_CYCLIC); - operationsMockedStatic.when(() -> Operations.runProcessGetOutput(any(Path.class),argThat(matchCommandPipShow))).thenReturn(PIP_SHOW_LINES_CYCLIC); - String requirementsTxt = getFileFromResource("requirements.txt", "msc", "python", "requirements-cyclic-test.txt"); - System.setProperty("MATCH_MANIFEST_VERSIONS","false"); - List> dependencies = pythonControllerRealEnv.getDependencies(requirementsTxt, true); - System.clearProperty("MATCH_MANIFEST_VERSIONS"); - assertEquals(104,dependencies.size()); - - operationsMockedStatic.close(); - - } - - @Test - void get_Dependency_Name_requirements() { - - assertEquals("something",PythonControllerRealEnv.getDependencyName("something==2.0.5")); - assertEquals("something",PythonControllerRealEnv.getDependencyName("something == 2.0.5")); - assertEquals("something",PythonControllerRealEnv.getDependencyName("something>=2.0.5")); - - } + @Test + void get_Dependencies_from_Cyclic_Tree() { + MockedStatic operationsMockedStatic = Mockito.mockStatic(Operations.class); + // ArgumentMatcher matchCommandPipFreeze = command -> Arrays.stream(command).anyMatch(word -> + // word.contains("freeze")); + + operationsMockedStatic + .when(() -> Operations.runProcessGetOutput(any(Path.class), argThat(matchCommandPipFreeze))) + .thenReturn(PIP_FREEZE_LINES_CYCLIC); + // operationsMockedStatic.when(() -> + // Operations.runProcessGetOutput(any(Path.class),any(String[].class))).thenReturn(PIP_FREEZE_LINES_CYCLIC); + operationsMockedStatic + .when(() -> Operations.runProcessGetOutput(any(Path.class), argThat(matchCommandPipShow))) + .thenReturn(PIP_SHOW_LINES_CYCLIC); + String requirementsTxt = + getFileFromResource("requirements.txt", "msc", "python", "requirements-cyclic-test.txt"); + System.setProperty("MATCH_MANIFEST_VERSIONS", "false"); + List> dependencies = pythonControllerRealEnv.getDependencies(requirementsTxt, true); + System.clearProperty("MATCH_MANIFEST_VERSIONS"); + assertEquals(104, dependencies.size()); + + operationsMockedStatic.close(); + } + @Test + void get_Dependency_Name_requirements() { - @Test - void automaticallyInstallPackageOnEnvironment() { - assertFalse(this.pythonControllerRealEnv.automaticallyInstallPackageOnEnvironment()); - } + assertEquals("something", PythonControllerRealEnv.getDependencyName("something==2.0.5")); + assertEquals("something", PythonControllerRealEnv.getDependencyName("something == 2.0.5")); + assertEquals("something", PythonControllerRealEnv.getDependencyName("something>=2.0.5")); + } - @Test - void isRealEnv() { + @Test + void automaticallyInstallPackageOnEnvironment() { + assertFalse(this.pythonControllerRealEnv.automaticallyInstallPackageOnEnvironment()); + } - assertTrue(this.pythonControllerRealEnv.isRealEnv()); - } + @Test + void isRealEnv() { - @Test - void isVirtualEnv() { - assertFalse(this.pythonControllerRealEnv.isVirtualEnv()); - } + assertTrue(this.pythonControllerRealEnv.isRealEnv()); + } + @Test + void isVirtualEnv() { + assertFalse(this.pythonControllerRealEnv.isVirtualEnv()); + } } diff --git a/src/test/java/com/redhat/exhort/utils/PythonControllerVirtualEnvTest.java b/src/test/java/com/redhat/exhort/utils/PythonControllerVirtualEnvTest.java index 0dcda9d8..1ad54d69 100644 --- a/src/test/java/com/redhat/exhort/utils/PythonControllerVirtualEnvTest.java +++ b/src/test/java/com/redhat/exhort/utils/PythonControllerVirtualEnvTest.java @@ -15,90 +15,84 @@ */ package com.redhat.exhort.utils; +import static org.junit.jupiter.api.Assertions.*; +import static org.mockito.Mockito.*; + import com.fasterxml.jackson.core.JsonProcessingException; import com.fasterxml.jackson.databind.ObjectMapper; import com.redhat.exhort.ExhortTest; -import com.redhat.exhort.tools.Operations; -import org.junit.jupiter.api.BeforeAll; -import org.junit.jupiter.api.BeforeEach; -import org.junit.jupiter.api.Test; -import org.mockito.MockedStatic; -import org.mockito.Mockito; -import org.mockito.Spy; - import java.io.IOException; import java.nio.file.Files; import java.nio.file.Path; import java.util.List; import java.util.Map; - -import static org.junit.jupiter.api.Assertions.*; -import static org.mockito.Mockito.*; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.Test; +import org.mockito.Mockito; class PythonControllerVirtualEnvTest extends ExhortTest { - private static PythonControllerVirtualEnv pythonControllerVirtualEnv; - private static PythonControllerVirtualEnv spiedPythonControllerVirtualEnv; - - private ObjectMapper om = new ObjectMapper(); - @BeforeAll - static void setUp() { - - pythonControllerVirtualEnv = new PythonControllerVirtualEnv("python3"); - spiedPythonControllerVirtualEnv = Mockito.spy(pythonControllerVirtualEnv); - - } - - @Test - void test_Virtual_Environment_Install_Best_Efforts() throws JsonProcessingException { - System.setProperty("EXHORT_PYTHON_INSTALL_BEST_EFFORTS","true"); - System.setProperty("MATCH_MANIFEST_VERSIONS","false"); - String requirementsTxt = getFileFromString("requirements.txt", "flask==9.9.9\ndeprecated==15.15.99\n"); - List> dependencies = spiedPythonControllerVirtualEnv.getDependencies(requirementsTxt, true); - - System.out.println(om.writerWithDefaultPrettyPrinter().writeValueAsString(dependencies)); - System.clearProperty("EXHORT_PYTHON_INSTALL_BEST_EFFORTS"); - System.clearProperty("MATCH_MANIFEST_VERSIONS"); - } - - @Test - void test_Virtual_Environment_Install_Best_Efforts_Conflict_MMV_Should_Throw_Runtime_Exception() { - System.setProperty("EXHORT_PYTHON_INSTALL_BEST_EFFORTS","true"); - String requirementsTxt = getFileFromString("requirements.txt", "flask==9.9.9\ndeprecated==15.15.99\n"); - RuntimeException runtimeException = assertThrows(RuntimeException.class, () -> spiedPythonControllerVirtualEnv.getDependencies(requirementsTxt, true)); - assertTrue(runtimeException.getMessage().contains("Conflicting settings")); - System.clearProperty("EXHORT_PYTHON_INSTALL_BEST_EFFORTS"); - - } - - @Test - void test_Virtual_Environment_Flow() throws IOException { -// Mockito - String requirementsTxt = "Jinja2==3.0.3"; - Path requirementsFilePath = Path.of(System.getProperty("user.dir").toString(), "requirements.txt"); - Files.write(requirementsFilePath, requirementsTxt.getBytes()); -// MockedStatic operationsMockedStatic = mockStatic(Operations.class); -// when(spiedPythonControllerVirtualEnv.) - List> dependencies = spiedPythonControllerVirtualEnv.getDependencies(requirementsFilePath.toString(), true); - verify(spiedPythonControllerVirtualEnv).prepareEnvironment(anyString()); - verify(spiedPythonControllerVirtualEnv).installPackages(anyString()); - verify(spiedPythonControllerVirtualEnv).cleanEnvironment(anyBoolean()); - verify(spiedPythonControllerVirtualEnv).cleanEnvironment(anyBoolean()); - verify(spiedPythonControllerVirtualEnv).automaticallyInstallPackageOnEnvironment(); - verify(spiedPythonControllerVirtualEnv,never()).isRealEnv(); - verify(spiedPythonControllerVirtualEnv,times(2)).isVirtualEnv(); - - - } - - @Test - void isRealEnv() { - - assertFalse(this.spiedPythonControllerVirtualEnv.isRealEnv()); - } - - @Test - void isVirtualEnv() { - assertTrue(this.spiedPythonControllerVirtualEnv.isVirtualEnv()); - } + private static PythonControllerVirtualEnv pythonControllerVirtualEnv; + private static PythonControllerVirtualEnv spiedPythonControllerVirtualEnv; + + private ObjectMapper om = new ObjectMapper(); + + @BeforeAll + static void setUp() { + + pythonControllerVirtualEnv = new PythonControllerVirtualEnv("python3"); + spiedPythonControllerVirtualEnv = Mockito.spy(pythonControllerVirtualEnv); + } + + @Test + void test_Virtual_Environment_Install_Best_Efforts() throws JsonProcessingException { + System.setProperty("EXHORT_PYTHON_INSTALL_BEST_EFFORTS", "true"); + System.setProperty("MATCH_MANIFEST_VERSIONS", "false"); + String requirementsTxt = getFileFromString("requirements.txt", "flask==9.9.9\ndeprecated==15.15.99\n"); + List> dependencies = spiedPythonControllerVirtualEnv.getDependencies(requirementsTxt, true); + + System.out.println(om.writerWithDefaultPrettyPrinter().writeValueAsString(dependencies)); + System.clearProperty("EXHORT_PYTHON_INSTALL_BEST_EFFORTS"); + System.clearProperty("MATCH_MANIFEST_VERSIONS"); + } + + @Test + void test_Virtual_Environment_Install_Best_Efforts_Conflict_MMV_Should_Throw_Runtime_Exception() { + System.setProperty("EXHORT_PYTHON_INSTALL_BEST_EFFORTS", "true"); + String requirementsTxt = getFileFromString("requirements.txt", "flask==9.9.9\ndeprecated==15.15.99\n"); + RuntimeException runtimeException = assertThrows( + RuntimeException.class, () -> spiedPythonControllerVirtualEnv.getDependencies(requirementsTxt, true)); + assertTrue(runtimeException.getMessage().contains("Conflicting settings")); + System.clearProperty("EXHORT_PYTHON_INSTALL_BEST_EFFORTS"); + } + + @Test + void test_Virtual_Environment_Flow() throws IOException { + // Mockito + String requirementsTxt = "Jinja2==3.0.3"; + Path requirementsFilePath = Path.of(System.getProperty("user.dir").toString(), "requirements.txt"); + Files.write(requirementsFilePath, requirementsTxt.getBytes()); + // MockedStatic operationsMockedStatic = mockStatic(Operations.class); + // when(spiedPythonControllerVirtualEnv.) + List> dependencies = + spiedPythonControllerVirtualEnv.getDependencies(requirementsFilePath.toString(), true); + verify(spiedPythonControllerVirtualEnv).prepareEnvironment(anyString()); + verify(spiedPythonControllerVirtualEnv).installPackages(anyString()); + verify(spiedPythonControllerVirtualEnv).cleanEnvironment(anyBoolean()); + verify(spiedPythonControllerVirtualEnv).cleanEnvironment(anyBoolean()); + verify(spiedPythonControllerVirtualEnv).automaticallyInstallPackageOnEnvironment(); + verify(spiedPythonControllerVirtualEnv, never()).isRealEnv(); + verify(spiedPythonControllerVirtualEnv, times(2)).isVirtualEnv(); + } + + @Test + void isRealEnv() { + + assertFalse(this.spiedPythonControllerVirtualEnv.isRealEnv()); + } + + @Test + void isVirtualEnv() { + assertTrue(this.spiedPythonControllerVirtualEnv.isVirtualEnv()); + } }