diff --git a/org.eclipse.jdt.ls.core/build.properties b/org.eclipse.jdt.ls.core/build.properties index 741888bcb8..ca73aa3145 100644 --- a/org.eclipse.jdt.ls.core/build.properties +++ b/org.eclipse.jdt.ls.core/build.properties @@ -6,7 +6,6 @@ bin.includes = META-INF/,\ lifecycle-mapping-metadata.xml,\ plugin.properties,\ gradle/checksums/checksums.json,\ - gradle/checksums/versions.json,\ gradle/protobuf/init.gradle,\ gradle/init/init.gradle,\ gradle/android/init.gradle,\ diff --git a/org.eclipse.jdt.ls.core/pom.xml b/org.eclipse.jdt.ls.core/pom.xml index 0d97105293..f0b3740fb3 100644 --- a/org.eclipse.jdt.ls.core/pom.xml +++ b/org.eclipse.jdt.ls.core/pom.xml @@ -31,45 +31,23 @@ import groovy.json.JsonSlurper import groovy.json.JsonOutput - import java.net.http.HttpClient - import java.net.http.HttpClient.Redirect - import java.net.http.HttpRequest - import java.net.http.HttpResponse.BodyHandlers - import java.util.concurrent.CompletableFuture - import java.util.concurrent.ExecutorService - import java.util.concurrent.Executors - import java.util.stream.Collectors - - def checksumsFile = new File(project.basedir.absolutePath, "gradle/checksums/checksums.json") if (System.getProperty("eclipse.jdt.ls.skipGradleChecksums") != null && checksumsFile.exists()) { println "Skipping gradle wrapper validation checksums ..." return } - def versionUrl = new URL("https://services.gradle.org/versions/all") - def versionStr = versionUrl.text; - def versionsFile = new File(project.basedir.absolutePath, "gradle/checksums/versions.json") - versionsFile.parentFile.mkdirs() //just in case - versionsFile.write(versionStr) - println "Wrote to ${versionsFile}" - - def versions = new JsonSlurper().parseText(versionStr) + def versions = new JsonSlurper().parse(new URL("https://services.gradle.org/versions/all")) + def onlyReleases = Boolean.getBoolean("eclipse.jdt.ls.onlyGradleReleases") - class Checksum { - String wrapperChecksumUrl; - String sha256 + def checksums = versions.findAll { + it.wrapperChecksumUrl != null && + !(onlyReleases && (it.nightly || it.snapshot || it.rcFor != "" || it.broken)) + }.collect { + [version: it.version, wrapperChecksumUrl: it.wrapperChecksumUrl, sha256: it.wrapperChecksum] } - def checksums = [] - versions.each { - boolean isNonRelease = it.nightly || it.snapshot || (it.rcFor != "") || it.broken - if (it.wrapperChecksumUrl == null || (System.getProperty("eclipse.jdt.ls.onlyGradleReleases") && isNonRelease)) { - return - } - checksums.add(new Checksum(wrapperChecksumUrl: it.wrapperChecksumUrl, sha256: it.wrapperChecksum)); - } - def json = JsonOutput.toJson(checksums) - checksumsFile.write(JsonOutput.prettyPrint(json)) + + checksumsFile.write(JsonOutput.prettyPrint(JsonOutput.toJson(checksums))) println "Wrote to ${checksumsFile}" diff --git a/org.eclipse.jdt.ls.core/src/org/eclipse/jdt/ls/core/internal/JobHelpers.java b/org.eclipse.jdt.ls.core/src/org/eclipse/jdt/ls/core/internal/JobHelpers.java index eda8d31308..0275400cca 100644 --- a/org.eclipse.jdt.ls.core/src/org/eclipse/jdt/ls/core/internal/JobHelpers.java +++ b/org.eclipse.jdt.ls.core/src/org/eclipse/jdt/ls/core/internal/JobHelpers.java @@ -226,10 +226,6 @@ public static void waitForJobs(String jobFamily, IProgressMonitor monitor) { } } - public static void waitForLoadingGradleVersionJob() { - waitForJobs(LoadingGradleVersionJobMatcher.INSTANCE, MAX_TIME_MILLIS); - } - public static void waitForBuildOffJobs(int maxTimeMillis) { waitForJobs(BuildJobOffMatcher.INSTANCE, maxTimeMillis); } @@ -351,17 +347,6 @@ public boolean matches(Job job) { } - static class LoadingGradleVersionJobMatcher implements IJobMatcher { - - public static final IJobMatcher INSTANCE = new LoadingGradleVersionJobMatcher(); - - @Override - public boolean matches(Job job) { - return job.getClass().getName().matches("org.eclipse.buildship.core.internal.util.gradle.PublishedGradleVersionsWrapper.LoadVersionsJob"); - } - - } - static class DownloadSourcesJobMatcher implements IJobMatcher { public static final IJobMatcher INSTANCE = new DownloadSourcesJobMatcher(); diff --git a/org.eclipse.jdt.ls.core/src/org/eclipse/jdt/ls/core/internal/managers/GradlePreferenceChangeListener.java b/org.eclipse.jdt.ls.core/src/org/eclipse/jdt/ls/core/internal/managers/GradlePreferenceChangeListener.java index 079d41e3cc..fea3235fcc 100644 --- a/org.eclipse.jdt.ls.core/src/org/eclipse/jdt/ls/core/internal/managers/GradlePreferenceChangeListener.java +++ b/org.eclipse.jdt.ls.core/src/org/eclipse/jdt/ls/core/internal/managers/GradlePreferenceChangeListener.java @@ -20,12 +20,8 @@ import org.eclipse.buildship.core.BuildConfiguration; import org.eclipse.buildship.core.GradleBuild; import org.eclipse.buildship.core.GradleCore; -import org.eclipse.buildship.core.GradleDistribution; -import org.eclipse.buildship.core.WrapperGradleDistribution; import org.eclipse.buildship.core.internal.CorePlugin; -import org.eclipse.buildship.core.internal.configuration.ProjectConfiguration; import org.eclipse.core.resources.IProject; -import org.eclipse.core.runtime.CoreException; import org.eclipse.core.runtime.NullProgressMonitor; import org.eclipse.core.runtime.OperationCanceledException; import org.eclipse.core.runtime.jobs.Job; @@ -36,7 +32,6 @@ import org.eclipse.jdt.ls.core.internal.ProjectUtils; import org.eclipse.jdt.ls.core.internal.preferences.IPreferencesChangeListener; import org.eclipse.jdt.ls.core.internal.preferences.Preferences; -import org.eclipse.jdt.ls.internal.gradle.checksums.ValidationResult; import org.eclipse.jdt.ls.internal.gradle.checksums.WrapperValidator; /** @@ -112,20 +107,8 @@ private void updateProject(ProjectsManager projectsManager, IProject project, bo String projectDir = project.getLocation().toFile().getAbsolutePath(); Path projectPath = Paths.get(projectDir); if (gradleJavaHomeChanged || Files.exists(projectPath.resolve("gradlew"))) { - ProjectConfiguration configuration = CorePlugin.configurationManager().loadProjectConfiguration(project); - GradleDistribution distribution = configuration.getBuildConfiguration().getGradleDistribution(); - if (gradleJavaHomeChanged || !(distribution instanceof WrapperGradleDistribution)) { - projectsManager.updateProject(project, true); - } else { - try { - ValidationResult result = new WrapperValidator().checkWrapper(projectDir); - if (!result.isValid()) { - projectsManager.updateProject(project, true); - } - } catch (CoreException e) { - JavaLanguageServerPlugin.logException(e.getMessage(), e); - } - } + // Validation now happens inside getGradleDistribution(), so just trigger re-import + projectsManager.updateProject(project, true); } } diff --git a/org.eclipse.jdt.ls.core/src/org/eclipse/jdt/ls/core/internal/managers/GradleProjectImporter.java b/org.eclipse.jdt.ls.core/src/org/eclipse/jdt/ls/core/internal/managers/GradleProjectImporter.java index bfc98866d4..beac2ad1f5 100644 --- a/org.eclipse.jdt.ls.core/src/org/eclipse/jdt/ls/core/internal/managers/GradleProjectImporter.java +++ b/org.eclipse.jdt.ls.core/src/org/eclipse/jdt/ls/core/internal/managers/GradleProjectImporter.java @@ -60,7 +60,6 @@ import org.eclipse.core.runtime.Status; import org.eclipse.core.runtime.SubMonitor; import org.eclipse.core.runtime.URIUtil; -import org.eclipse.core.runtime.jobs.Job; import org.eclipse.debug.core.ILaunchManager; import org.eclipse.jdt.core.IJavaProject; import org.eclipse.jdt.internal.launching.StandardVMType; @@ -243,7 +242,6 @@ public void importToWorkspace(IProgressMonitor monitor) throws CoreException { } else if (GradleUtils.hasGradleInvalidTypeCodeException(importStatus, directory, monitor)) { gradleUpgradeWrapperStatus.add(new GradleUpgradeWrapperStatus(importStatus, GRADLE_INVALID_TYPE_CODE_MESSAGE, directory.toUri().toString())); } - checkWrapperChecksum(directory); } // store the digest for the imported gradle projects. ProjectUtils.getGradleProjects().forEach(project -> { @@ -367,50 +365,31 @@ private IStatus importDir(Path projectFolder, IProgressMonitor monitor) { return startSynchronization(projectFolder, monitor); } - public static void checkWrapperChecksum(Path rootFolder) { - PreferenceManager preferencesManager = JavaLanguageServerPlugin.getPreferencesManager(); - if (preferencesManager == null) { - return; - } - - if (Files.exists(rootFolder.resolve(WrapperValidator.GRADLE_WRAPPER_JAR))) { - Job checksumJob = new Job("Validating Gradle wrapper checksum...") { - @Override - protected IStatus run(IProgressMonitor monitor) { - WrapperValidator validator = new WrapperValidator(); - try { - ValidationResult result = validator.checkWrapper(rootFolder.toFile().getAbsolutePath()); - if (!result.isValid() && !WrapperValidator.contains(result.getChecksum())) { - ProjectsManager pm = JavaLanguageServerPlugin.getProjectsManager(); - if (pm != null && pm.getConnection() != null) { - if (preferencesManager.getClientPreferences().isGradleChecksumWrapperPromptSupport()) { - String id = "gradle/checksum/prompt"; - ExecuteCommandParams params = new ExecuteCommandParams(id, asList(result.getWrapperJar(), result.getChecksum())); - pm.getConnection().sendNotification(params); - } else { - //@formatter:off - String message = GRADLE_WRAPPER_CHEKSUM_WARNING_TEMPLATE.replaceAll("@wrapper@", result.getWrapperJar()) - .replaceAll("@checksum@", result.getChecksum()); - //@formatter:on - pm.getConnection().showMessage(new MessageParams(MessageType.Error, message)); - } - } - } - } catch (CoreException e) { - JavaLanguageServerPlugin.logException(e.getMessage(), e); - } - return Status.OK_STATUS; - } - }; - checksumJob.setPriority(Job.LONG); - checksumJob.schedule(); - } - } - public static GradleDistribution getGradleDistribution(Path rootFolder) { Preferences preferences = getPreferences(); if (preferences.isGradleWrapperEnabled() && Files.exists(rootFolder.resolve("gradle/wrapper/gradle-wrapper.properties"))) { - return GradleDistribution.fromBuild(); + if (Files.exists(rootFolder.resolve(WrapperValidator.GRADLE_WRAPPER_JAR))) { + try { + WrapperValidator validator = new WrapperValidator(); + ValidationResult result = validator.checkWrapper(rootFolder.toFile().getAbsolutePath()); + if (result.getStatus() == ValidationResult.Status.INVALID && !WrapperValidator.contains(result.getChecksum())) { + JavaLanguageServerPlugin.logError("Gradle wrapper checksum validation failed for " + rootFolder + ". Skipping wrapper."); + notifyInvalidChecksum(rootFolder, result); + // Fall through to next distribution option + } else if (result.isUnverifiable()) { + JavaLanguageServerPlugin.logInfo("Gradle wrapper checksum could not be verified for " + rootFolder + ". Skipping wrapper."); + notifyInvalidChecksum(rootFolder, result); + // Fall through to next distribution option + } else { + return GradleDistribution.fromBuild(); + } + } catch (CoreException e) { + JavaLanguageServerPlugin.logException(e.getMessage(), e); + // On error, fall through to next distribution option + } + } else { + return GradleDistribution.fromBuild(); + } } if (StringUtils.isNotBlank(preferences.getGradleVersion())) { List versions = CorePlugin.publishedGradleVersions().getVersions(); @@ -435,6 +414,25 @@ public static GradleDistribution getGradleDistribution(Path rootFolder) { return DEFAULT_DISTRIBUTION; } + private static void notifyInvalidChecksum(Path rootFolder, ValidationResult result) { + PreferenceManager preferencesManager = JavaLanguageServerPlugin.getPreferencesManager(); + ProjectsManager pm = JavaLanguageServerPlugin.getProjectsManager(); + if (preferencesManager == null || pm == null || pm.getConnection() == null) { + return; + } + if (preferencesManager.getClientPreferences().isGradleChecksumWrapperPromptSupport()) { + String id = "gradle/checksum/prompt"; + ExecuteCommandParams params = new ExecuteCommandParams(id, asList(result.getWrapperJar(), result.getChecksum())); + pm.getConnection().sendNotification(params); + } else { + //@formatter:off + String message = GRADLE_WRAPPER_CHEKSUM_WARNING_TEMPLATE.replaceAll("@wrapper@", result.getWrapperJar()) + .replaceAll("@checksum@", result.getChecksum()); + //@formatter:on + pm.getConnection().showMessage(new MessageParams(MessageType.Error, message)); + } + } + public static File getGradleHomeFile() { Map env = System.getenv(); Properties sysprops = System.getProperties(); diff --git a/org.eclipse.jdt.ls.core/src/org/eclipse/jdt/ls/internal/gradle/checksums/DownloadChecksumJob.java b/org.eclipse.jdt.ls.core/src/org/eclipse/jdt/ls/internal/gradle/checksums/DownloadChecksumJob.java deleted file mode 100644 index d8b8367b3c..0000000000 --- a/org.eclipse.jdt.ls.core/src/org/eclipse/jdt/ls/internal/gradle/checksums/DownloadChecksumJob.java +++ /dev/null @@ -1,118 +0,0 @@ -/******************************************************************************* - * Copyright (c) 2020 Red Hat Inc. and others. - * All rights reserved. This program and the accompanying materials - * are made available under the terms of the Eclipse Public License 2.0 - * which accompanies this distribution, and is available at - * https://www.eclipse.org/legal/epl-2.0/ - * - * SPDX-License-Identifier: EPL-2.0 - * - * Contributors: - * Red Hat Inc. - initial API and implementation - *******************************************************************************/ -package org.eclipse.jdt.ls.internal.gradle.checksums; - -import java.io.File; -import java.io.IOException; -import java.io.InputStreamReader; -import java.net.HttpURLConnection; -import java.net.MalformedURLException; -import java.net.URI; -import java.net.URISyntaxException; -import java.net.URL; -import java.nio.charset.StandardCharsets; -import java.nio.file.Files; -import java.nio.file.Paths; -import java.util.concurrent.BlockingQueue; -import java.util.concurrent.LinkedBlockingQueue; - -import org.eclipse.core.runtime.IProgressMonitor; -import org.eclipse.core.runtime.IStatus; -import org.eclipse.core.runtime.Status; -import org.eclipse.core.runtime.SubMonitor; -import org.eclipse.core.runtime.jobs.Job; -import org.eclipse.jdt.ls.core.internal.JavaLanguageServerPlugin; - -import com.google.common.io.CharStreams; - -/** - * - * @author snjeza - * - */ -public class DownloadChecksumJob extends Job { - - public static final String WRAPPER_VALIDATOR_JOBS = "WrapperValidatorJobs"; - - private final BlockingQueue queue = new LinkedBlockingQueue<>(); - - public DownloadChecksumJob() { - super("Download Gradle Wrapper checksums"); - } - - /* (non-Javadoc) - * @see org.eclipse.core.runtime.jobs.Job#run(org.eclipse.core.runtime.IProgressMonitor) - */ - @Override - protected IStatus run(IProgressMonitor monitor) { - int totalWork = 2 * queue.size(); - SubMonitor subMonitor = SubMonitor.convert(monitor, totalWork); - while (!queue.isEmpty() && !monitor.isCanceled()) { - String urlStr = queue.poll(); - URL url; - try { - url = new URI(urlStr).toURL(); - } catch (MalformedURLException | URISyntaxException e1) { - JavaLanguageServerPlugin.logInfo("Invalid wrapper URL " + urlStr); - continue; - } - subMonitor.setTaskName(url.toString()); - final HttpURLConnection connection; - try { - connection = (HttpURLConnection) url.openConnection(); - connection.setConnectTimeout(30000); - connection.setReadTimeout(30000); - } catch (IOException e) { - JavaLanguageServerPlugin.logException(e.getMessage(), e); - continue; - } - try (AutoCloseable closer = (() -> connection.disconnect()); InputStreamReader reader = new InputStreamReader(connection.getInputStream(), StandardCharsets.UTF_8);) { - String sha256 = CharStreams.toString(reader); - File sha256File = new File(WrapperValidator.getSha256CacheFile(), WrapperValidator.getFileName(urlStr)); - write(sha256File, sha256); - WrapperValidator.allow(sha256); - subMonitor.worked(2); - } catch (Exception e) { - JavaLanguageServerPlugin.logException("Cannot download Gradle sha256 checksum: " + url.toString(), e); - continue; - } - } - subMonitor.done(); - return Status.OK_STATUS; - } - - public void add(String urlStr) { - queue.add(urlStr); - } - - private void write(File sha256File, String sha256) { - try { - Files.write(Paths.get(sha256File.getAbsolutePath()), sha256.getBytes()); - } catch (IOException e) { - JavaLanguageServerPlugin.logException(e); - } - } - - /* (non-Javadoc) - * @see org.eclipse.core.runtime.jobs.Job#belongsTo(java.lang.Object) - */ - @Override - public boolean belongsTo(Object family) { - return WRAPPER_VALIDATOR_JOBS.equals(family); - } - - public boolean isEmpty() { - return queue.isEmpty(); - } -} - diff --git a/org.eclipse.jdt.ls.core/src/org/eclipse/jdt/ls/internal/gradle/checksums/ValidationResult.java b/org.eclipse.jdt.ls.core/src/org/eclipse/jdt/ls/internal/gradle/checksums/ValidationResult.java index 44dc2497b4..e7604d0f1e 100644 --- a/org.eclipse.jdt.ls.core/src/org/eclipse/jdt/ls/internal/gradle/checksums/ValidationResult.java +++ b/org.eclipse.jdt.ls.core/src/org/eclipse/jdt/ls/internal/gradle/checksums/ValidationResult.java @@ -19,14 +19,20 @@ */ public class ValidationResult { + public enum Status { + VALID, + INVALID, + UNVERIFIABLE + } + private String checksum; private String wrapperJar; - private boolean valid; + private Status status; - public ValidationResult(String wrapperJar, String checksum, boolean valid) { + public ValidationResult(String wrapperJar, String checksum, Status status) { this.wrapperJar = wrapperJar; this.checksum = checksum; - this.valid = valid; + this.status = status; } public String getChecksum() { @@ -34,7 +40,15 @@ public String getChecksum() { } public boolean isValid() { - return valid; + return status == Status.VALID; + } + + public boolean isUnverifiable() { + return status == Status.UNVERIFIABLE; + } + + public Status getStatus() { + return status; } public String getWrapperJar() { @@ -42,4 +56,3 @@ public String getWrapperJar() { } } - diff --git a/org.eclipse.jdt.ls.core/src/org/eclipse/jdt/ls/internal/gradle/checksums/WrapperValidator.java b/org.eclipse.jdt.ls.core/src/org/eclipse/jdt/ls/internal/gradle/checksums/WrapperValidator.java index c0608b227c..5c0a2fddca 100644 --- a/org.eclipse.jdt.ls.core/src/org/eclipse/jdt/ls/internal/gradle/checksums/WrapperValidator.java +++ b/org.eclipse.jdt.ls.core/src/org/eclipse/jdt/ls/internal/gradle/checksums/WrapperValidator.java @@ -14,12 +14,12 @@ import java.io.BufferedReader; import java.io.File; -import java.io.FileInputStream; -import java.io.FileOutputStream; import java.io.IOException; import java.io.InputStream; import java.io.InputStreamReader; import java.io.Reader; +import java.net.HttpURLConnection; +import java.net.URI; import java.net.URL; import java.nio.charset.StandardCharsets; import java.nio.file.Files; @@ -28,36 +28,28 @@ import java.util.ArrayList; import java.util.Collection; import java.util.Collections; +import java.util.HashMap; import java.util.HashSet; import java.util.List; import java.util.Map; -import java.util.Optional; +import java.util.Properties; import java.util.Set; -import java.util.concurrent.atomic.AtomicBoolean; +import java.util.regex.Matcher; +import java.util.regex.Pattern; import org.eclipse.core.runtime.CoreException; import org.eclipse.core.runtime.FileLocator; -import org.eclipse.core.runtime.NullProgressMonitor; -import org.eclipse.core.runtime.OperationCanceledException; import org.eclipse.core.runtime.Platform; import org.eclipse.jdt.ls.core.internal.ExceptionFactory; import org.eclipse.jdt.ls.core.internal.IConstants; import org.eclipse.jdt.ls.core.internal.JavaLanguageServerPlugin; -import org.eclipse.jdt.ls.core.internal.JobHelpers; +import org.eclipse.jdt.ls.internal.gradle.checksums.ValidationResult.Status; import org.osgi.framework.Bundle; -import com.google.common.base.Function; -import com.google.common.base.Predicate; -import com.google.common.collect.FluentIterable; -import com.google.common.collect.ImmutableList; -import com.google.common.io.CharStreams; -import com.google.gson.Gson; -import com.google.gson.GsonBuilder; import com.google.gson.JsonArray; import com.google.gson.JsonElement; import com.google.gson.JsonObject; import com.google.gson.JsonParser; -import com.google.gson.reflect.TypeToken; /** * @@ -67,111 +59,130 @@ public class WrapperValidator { public static final String GRADLE_CHECKSUMS = "/gradle/checksums/checksums.json"; - public static final String GRADLE_VERSIONS = "/gradle/checksums/versions.json"; public static final String GRADLE_WRAPPER_JAR = "gradle/wrapper/gradle-wrapper.jar"; - private static final int QUEUE_LENGTH = 20; - private static final String WRAPPER_CHECKSUM_URL = "wrapperChecksumUrl"; + public static final String GRADLE_WRAPPER_PROPERTIES = "gradle/wrapper/gradle-wrapper.properties"; + private static final int NETWORK_TIMEOUT_MS = 5000; + private static final Pattern VERSION_PATTERN = Pattern.compile("gradle-([^-/]+(-.+?)?)-(?:bin|all)\\.zip"); private static Set allowed = new HashSet<>(); private static Set disallowed = new HashSet<>(); - private static Set wrapperChecksumUrls = new HashSet<>(); - private static AtomicBoolean downloaded = new AtomicBoolean(false); + // version -> expected sha256 + private static Map bundledChecksums = new HashMap<>(); + private static boolean bundledLoaded = false; private HashProvider hashProvider; - private int queueLength; - - public WrapperValidator(int queueLength) { - this.hashProvider = new HashProvider(); - this.queueLength = queueLength; - } public WrapperValidator() { - this(QUEUE_LENGTH); + this.hashProvider = new HashProvider(); } public ValidationResult checkWrapper(String baseDir) throws CoreException { - Path wrapperJar = Paths.get(baseDir, GRADLE_WRAPPER_JAR); + Path basePath = Paths.get(baseDir); + Path wrapperJar = basePath.resolve(GRADLE_WRAPPER_JAR); if (!wrapperJar.toFile().exists()) { throw ExceptionFactory.newException(wrapperJar.toString() + " doesn't exist."); } - if (!downloaded.get() || allowed.isEmpty()) { + if (!bundledLoaded) { loadInternalChecksums(); - File versionFile = getVersionCacheFile(); - if (!versionFile.exists()) { - JobHelpers.waitForLoadingGradleVersionJob(); - } - if (versionFile.exists()) { - try (InputStreamReader reader = new InputStreamReader(new FileInputStream(versionFile), StandardCharsets.UTF_8);){ - String json = CharStreams.toString(reader); - Gson gson = new GsonBuilder().create(); - TypeToken>> typeToken = new TypeToken<>() { - }; - List> versions = gson.fromJson(json, typeToken.getType()); - //@formatter:off - ImmutableList urls = FluentIterable - .from(versions) - .filter(new Predicate>() { - @Override - public boolean apply(Map input) { - return input.get(WRAPPER_CHECKSUM_URL) != null; - } - }) - .transform(new Function, String>() { - @Override - public String apply(Map input) { - return input.get(WRAPPER_CHECKSUM_URL); - } - }) - .toList(); - // @formatter:on - DownloadChecksumJob downloadJob = new DownloadChecksumJob(); - int count = 0; - File cacheDir = getSha256CacheFile(); - for (String wrapperChecksumUrl : urls) { - try { - if (WrapperValidator.wrapperChecksumUrls.contains(wrapperChecksumUrl)) { - continue; - } - String fileName = getFileName(wrapperChecksumUrl); - if (fileName == null) { - continue; - } - File sha256File = new File(cacheDir, fileName); - if (!sha256File.exists() || sha256File.lastModified() < versionFile.lastModified()) { - count++; - if (count > queueLength) { - downloadJob.schedule(); - downloadJob = new DownloadChecksumJob(); - count = 0; - } - downloadJob.add(wrapperChecksumUrl); - } else { - String sha256 = read(sha256File); - allowed.add(sha256); - } - } catch (Exception e) { - JavaLanguageServerPlugin.logException(e.getMessage(), e); - } - } - if (!downloadJob.isEmpty()) { - downloadJob.schedule(); - } - JobHelpers.waitForJobs(DownloadChecksumJob.WRAPPER_VALIDATOR_JOBS, new NullProgressMonitor()); - downloaded.set(true); - } catch (IOException | OperationCanceledException e) { - throw ExceptionFactory.newException(e); - } - } else { - updateGradleVersionsFile(); - } + bundledLoaded = true; } try { String sha256 = hashProvider.getChecksum(wrapperJar.toFile()); - return new ValidationResult(wrapperJar.toString(), sha256, allowed.contains(sha256)); + String version = parseGradleVersion(basePath); + JavaLanguageServerPlugin.logInfo("Validating Gradle wrapper checksum for " + wrapperJar + " (version: " + version + ", sha256: " + sha256 + ")"); + + // 1. Check user-allowed checksums + if (allowed.contains(sha256)) { + JavaLanguageServerPlugin.logInfo("Gradle wrapper checksum matches user-allowed list: VALID"); + return new ValidationResult(wrapperJar.toString(), sha256, Status.VALID); + } + + // 2. Check user-disallowed checksums + if (disallowed.contains(sha256)) { + JavaLanguageServerPlugin.logInfo("Gradle wrapper checksum matches user-disallowed list: INVALID"); + return new ValidationResult(wrapperJar.toString(), sha256, Status.INVALID); + } + + // 3. If version can't be parsed, the distributionUrl may have been tampered with. + // Only allow if the jar matches a known bundled checksum. + if (version == null) { + if (bundledChecksums.containsValue(sha256)) { + JavaLanguageServerPlugin.logInfo("Gradle wrapper version not parseable but checksum matches a known bundled checksum: VALID"); + return new ValidationResult(wrapperJar.toString(), sha256, Status.VALID); + } + JavaLanguageServerPlugin.logInfo("Gradle wrapper version not parseable from distributionUrl and checksum is unknown: INVALID"); + return new ValidationResult(wrapperJar.toString(), sha256, Status.INVALID); + } + + // 4. Look up expected checksum from bundled checksums.json by version + String expectedChecksum = bundledChecksums.get(version); + if (expectedChecksum != null && expectedChecksum.equals(sha256)) { + JavaLanguageServerPlugin.logInfo("Gradle wrapper checksum matches bundled checksum for version " + version + ": VALID"); + return new ValidationResult(wrapperJar.toString(), sha256, Status.VALID); + } + + // 5. Check if sha256 matches ANY bundled version's checksum + // (wrapper jars are generic bootstraps, same jar may be used across versions) + if (bundledChecksums.containsValue(sha256)) { + JavaLanguageServerPlugin.logInfo("Gradle wrapper checksum matches a known bundled checksum: VALID"); + return new ValidationResult(wrapperJar.toString(), sha256, Status.VALID); + } + + // 6. Check disk cache + String cachedChecksum = readCachedChecksum(version); + if (cachedChecksum != null) { + Status status = cachedChecksum.equals(sha256) ? Status.VALID : Status.INVALID; + JavaLanguageServerPlugin.logInfo("Gradle wrapper checksum compared against disk cache for version " + version + ": " + status); + return new ValidationResult(wrapperJar.toString(), sha256, status); + } + + // 7. Network fetch with timeout + String checksumUrl = "https://services.gradle.org/distributions/gradle-" + version + "-wrapper.jar.sha256"; + JavaLanguageServerPlugin.logInfo("Fetching Gradle wrapper checksum from " + checksumUrl); + String fetchedChecksum = fetchChecksum(checksumUrl); + if (fetchedChecksum != null) { + boolean valid = fetchedChecksum.equals(sha256); + if (valid) { + // Cache successfully validated checksum to disk + writeCachedChecksum(version, fetchedChecksum); + allowed.add(sha256); + } + Status status = valid ? Status.VALID : Status.INVALID; + JavaLanguageServerPlugin.logInfo("Gradle wrapper checksum compared against fetched checksum for version " + version + ": " + status); + return new ValidationResult(wrapperJar.toString(), sha256, status); + } + + // 8. Version not in bundled data and network fetch failed -> unverifiable + JavaLanguageServerPlugin.logInfo("Gradle wrapper checksum could not be verified for version " + version + ": UNVERIFIABLE"); + return new ValidationResult(wrapperJar.toString(), sha256, Status.UNVERIFIABLE); } catch (IOException e) { throw ExceptionFactory.newException(e); } } + /** + * Parse the Gradle version from gradle-wrapper.properties distributionUrl. + */ + public static String parseGradleVersion(Path baseDir) { + Path propsPath = baseDir.resolve(GRADLE_WRAPPER_PROPERTIES); + if (!Files.exists(propsPath)) { + return null; + } + try (InputStream is = Files.newInputStream(propsPath)) { + Properties props = new Properties(); + props.load(is); + String distributionUrl = props.getProperty("distributionUrl"); + if (distributionUrl != null) { + Matcher matcher = VERSION_PATTERN.matcher(distributionUrl); + if (matcher.find()) { + return matcher.group(1); + } + } + } catch (IOException e) { + JavaLanguageServerPlugin.logException(e.getMessage(), e); + } + return null; + } + private void loadInternalChecksums() { Bundle bundle = Platform.getBundle(IConstants.PLUGIN_ID); URL url = FileLocator.find(bundle, new org.eclipse.core.runtime.Path(GRADLE_CHECKSUMS)); @@ -184,13 +195,18 @@ private void loadInternalChecksums() { continue; } JsonObject jsonObject = json.getAsJsonObject(); - String sha256 = jsonObject.get("sha256").getAsString(); - String wrapperChecksumUrl = jsonObject.get("wrapperChecksumUrl").getAsString(); - if (sha256 != null) { - allowed.add(sha256); + String sha256 = jsonObject.has("sha256") ? jsonObject.get("sha256").getAsString() : null; + if (sha256 == null) { + continue; + } + // Prefer explicit version field, fall back to extracting from URL + String version = jsonObject.has("version") ? jsonObject.get("version").getAsString() : null; + if (version == null) { + String wrapperChecksumUrl = jsonObject.has("wrapperChecksumUrl") ? jsonObject.get("wrapperChecksumUrl").getAsString() : null; + version = extractVersionFromChecksumUrl(wrapperChecksumUrl); } - if (wrapperChecksumUrl != null) { - wrapperChecksumUrls.add(wrapperChecksumUrl); + if (version != null) { + bundledChecksums.put(version, sha256); } } } @@ -200,58 +216,74 @@ private void loadInternalChecksums() { } } - private void updateGradleVersionsFile() { - File file = getVersionCacheFile(); - if (file.isFile()) { - return; + /** + * Extract version from a wrapperChecksumUrl like + * "https://services.gradle.org/distributions/gradle-8.5-wrapper.jar.sha256" + */ + public static String extractVersionFromChecksumUrl(String checksumUrl) { + if (checksumUrl == null) { + return null; } - file.getParentFile().mkdirs(); - Bundle bundle = Platform.getBundle(IConstants.PLUGIN_ID); - URL url = FileLocator.find(bundle, new org.eclipse.core.runtime.Path(GRADLE_VERSIONS)); - if (url != null) { - try (InputStream is = url.openStream(); InputStreamReader isr = new InputStreamReader(is); BufferedReader br = new BufferedReader(isr); FileOutputStream os = new FileOutputStream(file.getAbsolutePath())) { - br.lines().forEach(l -> { - try { - os.write(l.getBytes(StandardCharsets.UTF_8)); - os.write("\n".getBytes(StandardCharsets.UTF_8)); - } catch (IOException e) { - JavaLanguageServerPlugin.logException(e.getMessage(), e); - } - }); + Pattern pattern = Pattern.compile("gradle-([^/]+)-wrapper\\.jar\\.sha256$"); + Matcher matcher = pattern.matcher(checksumUrl); + if (matcher.find()) { + return matcher.group(1); + } + return null; + } + + private String readCachedChecksum(String version) { + File cacheDir = getSha256CacheFile(); + File cachedFile = new File(cacheDir, "gradle-" + version + "-wrapper.jar.sha256"); + if (cachedFile.exists()) { + try { + return Files.readString(cachedFile.toPath(), StandardCharsets.UTF_8).trim(); } catch (IOException e) { JavaLanguageServerPlugin.logException(e.getMessage(), e); } } + return null; } - public static String getFileName(String url) { - int index = url.lastIndexOf("/"); - if (index < 0 || url.length() < index + 1) { - JavaLanguageServerPlugin.logInfo("Invalid wrapper URL " + url); - return null; + private void writeCachedChecksum(String version, String checksum) { + File cacheDir = getSha256CacheFile(); + File cachedFile = new File(cacheDir, "gradle-" + version + "-wrapper.jar.sha256"); + try { + Files.writeString(cachedFile.toPath(), checksum, StandardCharsets.UTF_8); + } catch (IOException e) { + JavaLanguageServerPlugin.logException(e.getMessage(), e); } - return url.substring(index + 1); } - private static String read(File file) { - Optional firstLine; + private String fetchChecksum(String checksumUrl) { + HttpURLConnection connection = null; try { - firstLine = Files.lines(Paths.get(file.getAbsolutePath()), StandardCharsets.UTF_8).findFirst(); - } catch (IOException e) { - JavaLanguageServerPlugin.logException(e); - return null; - } - if (firstLine.isPresent()) { - return firstLine.get(); + URL url = new URI(checksumUrl).toURL(); + connection = (HttpURLConnection) url.openConnection(); + connection.setConnectTimeout(NETWORK_TIMEOUT_MS); + connection.setReadTimeout(NETWORK_TIMEOUT_MS); + int responseCode = connection.getResponseCode(); + if (responseCode == HttpURLConnection.HTTP_OK) { + try (InputStreamReader reader = new InputStreamReader(connection.getInputStream(), StandardCharsets.UTF_8)) { + StringBuilder sb = new StringBuilder(); + char[] buf = new char[256]; + int n; + while ((n = reader.read(buf)) != -1) { + sb.append(buf, 0, n); + } + return sb.toString().trim(); + } + } + } catch (Exception e) { + JavaLanguageServerPlugin.logInfo("Failed to fetch checksum from " + checksumUrl + ": " + e.getMessage()); + } finally { + if (connection != null) { + connection.disconnect(); + } } return null; } - private static File getVersionCacheFile() { - String xdgCache = getXdgCache(); - return new File(xdgCache, "tooling/gradle/versions.json"); - } - private static String getXdgCache() { String xdgCache = System.getenv("XDG_CACHE_HOME"); if (xdgCache == null) { @@ -276,7 +308,8 @@ public static File getSha256CacheFile() { public static void clear() { allowed.clear(); disallowed.clear(); - wrapperChecksumUrls.clear(); + bundledChecksums.clear(); + bundledLoaded = false; } public static void allow(Collection c) { @@ -304,7 +337,7 @@ public static Set getAllowed() { } public static Set getDisallowed() { - return Collections.unmodifiableSet(allowed); + return Collections.unmodifiableSet(disallowed); } public static void putSha256(List gradleWrapperList) { diff --git a/org.eclipse.jdt.ls.tests/projects/gradle/tampered-wrapper/gradle/wrapper/gradle-wrapper.jar b/org.eclipse.jdt.ls.tests/projects/gradle/tampered-wrapper/gradle/wrapper/gradle-wrapper.jar new file mode 100644 index 0000000000..2713e396b3 Binary files /dev/null and b/org.eclipse.jdt.ls.tests/projects/gradle/tampered-wrapper/gradle/wrapper/gradle-wrapper.jar differ diff --git a/org.eclipse.jdt.ls.tests/projects/gradle/tampered-wrapper/gradle/wrapper/gradle-wrapper.properties b/org.eclipse.jdt.ls.tests/projects/gradle/tampered-wrapper/gradle/wrapper/gradle-wrapper.properties new file mode 100644 index 0000000000..8aca403ac2 --- /dev/null +++ b/org.eclipse.jdt.ls.tests/projects/gradle/tampered-wrapper/gradle/wrapper/gradle-wrapper.properties @@ -0,0 +1,5 @@ +distributionBase=GRADLE_USER_HOME +distributionPath=wrapper/dists +distributionUrl=https\://evil.com/malware.zip +zipStoreBase=GRADLE_USER_HOME +zipStorePath=wrapper/dists diff --git a/org.eclipse.jdt.ls.tests/src/org/eclipse/jdt/ls/core/internal/managers/WrapperValidatorTest.java b/org.eclipse.jdt.ls.tests/src/org/eclipse/jdt/ls/core/internal/managers/WrapperValidatorTest.java index 38d41f6a5e..9f0f72ad59 100644 --- a/org.eclipse.jdt.ls.tests/src/org/eclipse/jdt/ls/core/internal/managers/WrapperValidatorTest.java +++ b/org.eclipse.jdt.ls.tests/src/org/eclipse/jdt/ls/core/internal/managers/WrapperValidatorTest.java @@ -15,41 +15,27 @@ import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertFalse; import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertNull; import static org.junit.jupiter.api.Assertions.assertTrue; -import java.io.BufferedReader; import java.io.File; import java.io.IOException; -import java.io.InputStream; -import java.io.InputStreamReader; -import java.io.Reader; -import java.net.URL; -import java.nio.charset.StandardCharsets; import java.nio.file.Files; -import java.nio.file.Paths; +import java.nio.file.Path; import java.util.ArrayList; import java.util.HashMap; import java.util.List; import java.util.Map; import java.util.Set; -import java.util.stream.Collectors; -import org.eclipse.core.runtime.FileLocator; -import org.eclipse.core.runtime.Platform; -import org.eclipse.jdt.ls.core.internal.IConstants; -import org.eclipse.jdt.ls.core.internal.JavaLanguageServerPlugin; import org.eclipse.jdt.ls.internal.gradle.checksums.ValidationResult; +import org.eclipse.jdt.ls.internal.gradle.checksums.ValidationResult.Status; import org.eclipse.jdt.ls.internal.gradle.checksums.WrapperValidator; import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; import org.mockito.junit.jupiter.MockitoExtension; -import org.osgi.framework.Bundle; - -import com.google.gson.JsonArray; -import com.google.gson.JsonElement; -import com.google.gson.JsonParser; /** * @author snjeza @@ -73,100 +59,140 @@ public void clearProperty() throws IOException { public void testGradleWrapper() throws Exception { File file = new File(getSourceProjectDirectory(), "gradle/simple-gradle"); assertTrue(file.isDirectory()); - File sha256Directory = WrapperValidator.getSha256CacheFile(); - assertTrue(sha256Directory.isDirectory()); - ValidationResult result = new WrapperValidator(100).checkWrapper(file.getAbsolutePath()); + ValidationResult result = new WrapperValidator().checkWrapper(file.getAbsolutePath()); assertTrue(result.isValid()); - // test cache - assertTrue(sha256Directory.isDirectory()); - String fileName = "gradle-6.3-wrapper.jar.sha256"; - Bundle bundle = Platform.getBundle(IConstants.PLUGIN_ID); - URL url = FileLocator.find(bundle, new org.eclipse.core.runtime.Path(WrapperValidator.GRADLE_CHECKSUMS)); - String sha256 = null; - if (url == null) { - String message = Files.list(Paths.get(sha256Directory.getAbsolutePath())).collect(Collectors.toList()).toString(); - file = new File(sha256Directory, fileName); - if (file.isFile()) { - assertTrue(file.isFile(), message); - sha256 = Files.lines(Paths.get(file.getAbsolutePath()), StandardCharsets.UTF_8).findFirst().get(); - } - } else { - try (InputStream inputStream = url.openStream(); InputStreamReader inputStreamReader = new InputStreamReader(inputStream); Reader reader = new BufferedReader(inputStreamReader)) { - JsonElement jsonElement = JsonParser.parseReader(reader); - if (jsonElement instanceof JsonArray) { - JsonArray array = (JsonArray) jsonElement; - for (JsonElement json : array) { - String wrapperChecksumUrl = json.getAsJsonObject().get("wrapperChecksumUrl").getAsString(); - if (wrapperChecksumUrl != null && wrapperChecksumUrl.endsWith("/" + fileName)) { - sha256 = json.getAsJsonObject().get("sha256").getAsString(); - break; - } - } - } - } catch (IOException e) { - JavaLanguageServerPlugin.logException(e.getMessage(), e); - } - } - assertEquals("1cef53de8dc192036e7b0cc47584449b0cf570a00d560bfaa6c9eabe06e1fc06", sha256); + assertEquals(Status.VALID, result.getStatus()); + assertNotNull(result.getChecksum()); + } + + @Test + public void testVersionParsing() throws Exception { + File file = new File(getSourceProjectDirectory(), "gradle/simple-gradle"); + String version = WrapperValidator.parseGradleVersion(file.toPath()); + assertEquals("8.5", version); + + File file2 = new File(getSourceProjectDirectory(), "gradle/gradle-4.0"); + String version2 = WrapperValidator.parseGradleVersion(file2.toPath()); + assertEquals("4.0", version2); + } + + @Test + public void testVersionExtractionFromChecksumUrl() throws Exception { + assertEquals("8.5", WrapperValidator.extractVersionFromChecksumUrl( + "https://services.gradle.org/distributions/gradle-8.5-wrapper.jar.sha256")); + assertEquals("9.5.0-milestone-4", WrapperValidator.extractVersionFromChecksumUrl( + "https://services.gradle.org/distributions/gradle-9.5.0-milestone-4-wrapper.jar.sha256")); + assertEquals("9.5.0-20260226004946+0000", WrapperValidator.extractVersionFromChecksumUrl( + "https://services.gradle.org/distributions-snapshots/gradle-9.5.0-20260226004946+0000-wrapper.jar.sha256")); } @Test public void testMissingSha256() throws Exception { - WrapperValidator wrapperValidator = new WrapperValidator(100); - Set allowed = WrapperValidator.getAllowed(); - Set disallowed = WrapperValidator.getDisallowed(); + WrapperValidator wrapperValidator = new WrapperValidator(); + File file = new File(getSourceProjectDirectory(), "gradle/gradle-4.0"); + // gradle-4.0 is not in bundled checksums, so should be UNVERIFIABLE (no network in tests) + ValidationResult result = wrapperValidator.checkWrapper(file.getAbsolutePath()); + assertNotNull(result.getChecksum()); + // Since gradle 4.0 is not in bundled checksums and network fetch will fail in tests, + // the result should be UNVERIFIABLE + assertEquals(Status.UNVERIFIABLE, result.getStatus()); + } + + @Test + public void testDisallowedChecksum() throws Exception { + WrapperValidator wrapperValidator = new WrapperValidator(); File file = new File(getSourceProjectDirectory(), "gradle/gradle-4.0"); - wrapperValidator.checkWrapper(file.getAbsolutePath()); - int size = WrapperValidator.size(); List sha256 = new ArrayList<>(); + sha256.add("41c8aa7a337a44af18d8cda0d632ebba469aef34f3041827624ef5c1a4e4419d"); try { - sha256.add("41c8aa7a337a44af18d8cda0d632ebba469aef34f3041827624ef5c1a4e4419d"); - WrapperValidator.clear(); WrapperValidator.disallow(sha256); - assertTrue(file.isDirectory()); ValidationResult result = wrapperValidator.checkWrapper(file.getAbsolutePath()); assertFalse(result.isValid()); + assertEquals(Status.INVALID, result.getStatus()); assertNotNull(result.getChecksum()); + } finally { WrapperValidator.clear(); + } + } + + @Test + public void testAllowedChecksum() throws Exception { + WrapperValidator wrapperValidator = new WrapperValidator(); + File file = new File(getSourceProjectDirectory(), "gradle/gradle-4.0"); + List sha256 = new ArrayList<>(); + sha256.add("41c8aa7a337a44af18d8cda0d632ebba469aef34f3041827624ef5c1a4e4419d"); + try { WrapperValidator.allow(sha256); - result = wrapperValidator.checkWrapper(file.getAbsolutePath()); + ValidationResult result = wrapperValidator.checkWrapper(file.getAbsolutePath()); assertTrue(result.isValid()); + assertEquals(Status.VALID, result.getStatus()); } finally { WrapperValidator.clear(); - WrapperValidator.allow(allowed); - WrapperValidator.disallow(disallowed); - wrapperValidator.checkWrapper(file.getAbsolutePath()); - assertEquals(size, WrapperValidator.size()); } } @Test public void testPreferences() throws Exception { - WrapperValidator wrapperValidator = new WrapperValidator(100); - Set allowed = WrapperValidator.getAllowed(); - Set disallowed = WrapperValidator.getDisallowed(); + WrapperValidator wrapperValidator = new WrapperValidator(); File file = new File(getSourceProjectDirectory(), "gradle/gradle-4.0"); - wrapperValidator.checkWrapper(file.getAbsolutePath()); - int size = WrapperValidator.size(); List list = new ArrayList(); Map map = new HashMap(); map.put("sha256", "41c8aa7a337a44af18d8cda0d632ebba469aef34f3041827624ef5c1a4e4419d"); map.put("allowed", Boolean.TRUE); list.add(map); try { - ValidationResult result = wrapperValidator.checkWrapper(file.getAbsolutePath()); - assertFalse(result.isValid()); - assertNotNull(result.getChecksum()); - WrapperValidator.clear(); WrapperValidator.putSha256(list); - result = wrapperValidator.checkWrapper(file.getAbsolutePath()); + ValidationResult result = wrapperValidator.checkWrapper(file.getAbsolutePath()); assertTrue(result.isValid()); + assertEquals(Status.VALID, result.getStatus()); } finally { WrapperValidator.clear(); - WrapperValidator.allow(allowed); - WrapperValidator.disallow(disallowed); - wrapperValidator.checkWrapper(file.getAbsolutePath()); - assertEquals(size, WrapperValidator.size()); + } + } + + @Test + public void testUnverifiable() throws Exception { + WrapperValidator wrapperValidator = new WrapperValidator(); + File file = new File(getSourceProjectDirectory(), "gradle/gradle-4.0"); + // gradle 4.0 is not in bundled checksums and network is not available in tests + ValidationResult result = wrapperValidator.checkWrapper(file.getAbsolutePath()); + assertTrue(result.isUnverifiable()); + assertEquals(Status.UNVERIFIABLE, result.getStatus()); + assertNotNull(result.getChecksum()); + } + + @Test + public void testTamperedDistributionUrlWithUnknownJar() throws Exception { + // tampered-wrapper has a non-standard distributionUrl and a jar whose + // checksum is not in bundled data -> should be INVALID + File file = new File(getSourceProjectDirectory(), "gradle/tampered-wrapper"); + assertTrue(file.isDirectory()); + String version = WrapperValidator.parseGradleVersion(file.toPath()); + assertNull(version, "Tampered distributionUrl should not be parseable"); + ValidationResult result = new WrapperValidator().checkWrapper(file.getAbsolutePath()); + assertFalse(result.isValid()); + assertEquals(Status.INVALID, result.getStatus()); + } + + @Test + public void testTamperedDistributionUrlWithKnownJar() throws Exception { + // simple-gradle has a jar whose checksum IS in bundled data; + // even with a tampered distributionUrl, the known-good jar should be accepted + File file = new File(getSourceProjectDirectory(), "gradle/simple-gradle"); + // Temporarily overwrite the properties to simulate a tampered URL + Path propsPath = file.toPath().resolve("gradle/wrapper/gradle-wrapper.properties"); + String original = Files.readString(propsPath); + try { + Files.writeString(propsPath, original.replace( + "https\\://services.gradle.org/distributions/gradle-8.5-bin.zip", + "https\\://evil.com/malware.zip")); + String version = WrapperValidator.parseGradleVersion(file.toPath()); + assertNull(version, "Tampered distributionUrl should not be parseable"); + ValidationResult result = new WrapperValidator().checkWrapper(file.getAbsolutePath()); + assertTrue(result.isValid(), "Known-good jar should still be accepted"); + assertEquals(Status.VALID, result.getStatus()); + } finally { + Files.writeString(propsPath, original); } } }