From 997190eedac0de8d817d13e3cba42412d85867b5 Mon Sep 17 00:00:00 2001 From: Dieter Mai Date: Sun, 28 Dec 2025 18:47:29 +0100 Subject: [PATCH] Rework CoreUtility.deleteContent() The previous implementation had the following flaws: - On Linux, soft links to directories were followed, causing files outside the workspace to be deleted. - On Windows, the read-only flag was not removed explicitly. Before Java 25, this was done implicitly by the File.delete() method. Since Java 25, the flag must be removed before calling File.delete() or Files.delete(). See JDK-8355954. This PR changes the implementation to: - Use the NIO file walker to iterate the root directory. - Avoid following symlinks. - Remove the Windows read-only flag explicitly. Signed-off-by: Dieter Mai --- .../pde/internal/core/util/CoreUtility.java | 22 +-- .../core/util/DeleteContentWalker.java | 135 ++++++++++++++++++ 2 files changed, 136 insertions(+), 21 deletions(-) create mode 100644 ui/org.eclipse.pde.core/src/org/eclipse/pde/internal/core/util/DeleteContentWalker.java diff --git a/ui/org.eclipse.pde.core/src/org/eclipse/pde/internal/core/util/CoreUtility.java b/ui/org.eclipse.pde.core/src/org/eclipse/pde/internal/core/util/CoreUtility.java index 89e3da79fe0..6e2865be943 100644 --- a/ui/org.eclipse.pde.core/src/org/eclipse/pde/internal/core/util/CoreUtility.java +++ b/ui/org.eclipse.pde.core/src/org/eclipse/pde/internal/core/util/CoreUtility.java @@ -34,7 +34,6 @@ import org.eclipse.core.runtime.IPath; import org.eclipse.core.runtime.IProgressMonitor; import org.eclipse.core.runtime.Platform; -import org.eclipse.core.runtime.SubMonitor; import org.eclipse.pde.internal.core.PDECore; public class CoreUtility { @@ -125,26 +124,7 @@ public static void deleteContent(File fileToDelete) { * @param monitor progress monitor for reporting and cancellation, can be null */ public static void deleteContent(File fileToDelete, IProgressMonitor monitor) { - // can be symlinks - if (!fileToDelete.exists()) { - fileToDelete.delete(); - } - if (fileToDelete.exists()) { - SubMonitor subMon = SubMonitor.convert(monitor, 100); - - if (fileToDelete.isDirectory()) { - File[] children = fileToDelete.listFiles(); - if (children != null && children.length > 0) { - SubMonitor childMon = SubMonitor.convert(subMon.split(90), children.length); - for (File element : children) { - deleteContent(element, childMon.split(1)); - } - } - } - fileToDelete.delete(); - - subMon.done(); - } + DeleteContentWalker.deleteDirectory(fileToDelete.toPath(), monitor); } public static boolean jarContainsResource(File file, String resource, boolean directory) { diff --git a/ui/org.eclipse.pde.core/src/org/eclipse/pde/internal/core/util/DeleteContentWalker.java b/ui/org.eclipse.pde.core/src/org/eclipse/pde/internal/core/util/DeleteContentWalker.java new file mode 100644 index 00000000000..08a96ce2df8 --- /dev/null +++ b/ui/org.eclipse.pde.core/src/org/eclipse/pde/internal/core/util/DeleteContentWalker.java @@ -0,0 +1,135 @@ +/******************************************************************************* + * Copyright (c) 2025 Dieter Mai and others. + * + * 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: + * Dieter Mai - initial API and implementation + *******************************************************************************/ +package org.eclipse.pde.internal.core.util; + +import java.io.IOException; +import java.nio.file.FileVisitResult; +import java.nio.file.FileVisitor; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.attribute.BasicFileAttributes; +import java.util.Objects; + +import org.eclipse.core.runtime.IProgressMonitor; +import org.eclipse.core.runtime.NullProgressMonitor; +import org.eclipse.core.runtime.Platform; +import org.eclipse.core.runtime.SubMonitor; + +/** + * Deletes content of a directory recursively. + */ +public class DeleteContentWalker implements FileVisitor { + + /** + * Deletes the given root directory and its content. If the deletion fails + * or is canceled by the user, the method returns silently. + * + * @param root + * The root directory to delete. + * @param monitor + * The monitor to report progress to. Can be null. + */ + public static void deleteDirectory(Path root, IProgressMonitor monitor) { + // if there is no file, no need to proceed + if (root == null || !Files.exists(root)) { + return; + } + + IProgressMonitor submonitor = createSubMonitor(root, monitor); + + try { + Files.walkFileTree(root, new DeleteContentWalker(root, submonitor)); + } catch (IOException e) { + // noting to do + } finally { + submonitor.done(); + } + } + + private static IProgressMonitor createSubMonitor(Path root, IProgressMonitor monitor) { + IProgressMonitor submonitor; + if (monitor == null || monitor instanceof NullProgressMonitor) { + submonitor = SubMonitor.convert(monitor); + } else { + try (var stream = Files.list(root)) { + // only use the root content for progress. anything else would + // be overkill. + long count = stream.count(); + submonitor = SubMonitor.convert(monitor, (int) count); + } catch (IOException e) { + // In case of error, just ignore the monitor; + submonitor = SubMonitor.convert(monitor); + } + } + return submonitor; + } + + private final Path root; + private final IProgressMonitor monitor; + + private DeleteContentWalker(Path root, IProgressMonitor monitor) { + this.root = root; + this.monitor = monitor; + } + + @Override + public FileVisitResult preVisitDirectory(Path path, BasicFileAttributes attrs) throws IOException { + if (Platform.OS.isWindows()) { + Files.setAttribute(path, "dos:readonly", false); //$NON-NLS-1$ + } + return resultIfNotCanceled(FileVisitResult.CONTINUE); + } + + @Override + public FileVisitResult visitFile(Path path, BasicFileAttributes attrs) throws IOException { + if (Platform.OS.isWindows()) { + Files.setAttribute(path, "dos:readonly", false); //$NON-NLS-1$ + } + Files.deleteIfExists(path); + + if (Objects.equals(path.getParent(), root)) { + monitor.worked(1); + } + return resultIfNotCanceled(FileVisitResult.CONTINUE); + } + + @Override + public FileVisitResult visitFileFailed(Path file, IOException exc) throws IOException { + if (exc != null) { + throw exc; + } + return resultIfNotCanceled(FileVisitResult.CONTINUE); + } + + @Override + public FileVisitResult postVisitDirectory(Path path, IOException exc) throws IOException { + Files.deleteIfExists(path); + + if (Objects.equals(path.getParent(), root)) { + monitor.worked(1); + } + return resultIfNotCanceled(FileVisitResult.CONTINUE); + } + + /** + * Returns the given result if not canceled. If canceled + * {@link FileVisitResult#TERMINATE} is returned. + */ + private FileVisitResult resultIfNotCanceled(FileVisitResult result) { + if (monitor.isCanceled()) { + return FileVisitResult.TERMINATE; + } + return result; + } +} \ No newline at end of file