From e5316a5382728a66196b679057974dc1ab64f437 Mon Sep 17 00:00:00 2001 From: Denys Almazov Date: Sun, 9 Aug 2026 18:20:56 +0300 Subject: [PATCH 1/3] fix: fixing bug with unpacking new eim version on linux --- .../espressif/idf/core/tools/EimLoader.java | 30 +-- .../idf/core/tools/EimZipExtractor.java | 233 ++++++++++++++++++ .../core/tools/test/EimZipExtractorTest.java | 135 ++++++++++ 3 files changed, 369 insertions(+), 29 deletions(-) create mode 100644 bundles/com.espressif.idf.core/src/com/espressif/idf/core/tools/EimZipExtractor.java create mode 100644 tests/com.espressif.idf.core.test/src/com/espressif/idf/core/tools/test/EimZipExtractorTest.java diff --git a/bundles/com.espressif.idf.core/src/com/espressif/idf/core/tools/EimLoader.java b/bundles/com.espressif.idf.core/src/com/espressif/idf/core/tools/EimLoader.java index ea5218bb5..62a8f4509 100644 --- a/bundles/com.espressif.idf.core/src/com/espressif/idf/core/tools/EimLoader.java +++ b/bundles/com.espressif.idf.core/src/com/espressif/idf/core/tools/EimLoader.java @@ -6,7 +6,6 @@ import java.io.BufferedReader; import java.io.File; -import java.io.FileInputStream; import java.io.FileNotFoundException; import java.io.IOException; import java.io.InputStream; @@ -21,8 +20,6 @@ import java.nio.file.Paths; import java.nio.file.StandardCopyOption; import java.util.Optional; -import java.util.zip.ZipEntry; -import java.util.zip.ZipInputStream; import org.eclipse.core.runtime.IProgressMonitor; import org.eclipse.core.runtime.IStatus; @@ -389,32 +386,7 @@ private void cleanupDownloadDirectory() private Path unzip(Path zipPath, Path destDir) throws IOException { - Files.createDirectories(destDir); - Path firstExecutable = null; - - try (ZipInputStream zis = new ZipInputStream(new FileInputStream(zipPath.toFile()))) - { - ZipEntry entry; - while ((entry = zis.getNextEntry()) != null) - { - Path newPath = destDir.resolve(entry.getName()); - if (entry.isDirectory()) - { - Files.createDirectories(newPath); - } - else - { - Files.createDirectories(newPath.getParent()); - Files.copy(zis, newPath, StandardCopyOption.REPLACE_EXISTING); - if (firstExecutable == null && Files.isRegularFile(newPath)) - { - newPath.toFile().setExecutable(true); - firstExecutable = newPath; - } - } - } - } - return firstExecutable != null ? firstExecutable : destDir; + return EimZipExtractor.extract(zipPath, destDir); } private String readProcessOutput(Process p) throws IOException diff --git a/bundles/com.espressif.idf.core/src/com/espressif/idf/core/tools/EimZipExtractor.java b/bundles/com.espressif.idf.core/src/com/espressif/idf/core/tools/EimZipExtractor.java new file mode 100644 index 000000000..ac41f44e2 --- /dev/null +++ b/bundles/com.espressif.idf.core/src/com/espressif/idf/core/tools/EimZipExtractor.java @@ -0,0 +1,233 @@ +/******************************************************************************* + * Copyright 2026 Espressif Systems (Shanghai) PTE LTD. All rights reserved. + * Use is subject to license terms. + *******************************************************************************/ +package com.espressif.idf.core.tools; + +import java.io.IOException; +import java.io.InputStream; +import java.nio.charset.StandardCharsets; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.StandardCopyOption; +import java.util.Comparator; +import java.util.Optional; +import java.util.stream.Stream; +import java.util.zip.ZipEntry; +import java.util.zip.ZipInputStream; + +import org.eclipse.core.runtime.Platform; + +import com.espressif.idf.core.logging.Logger; + +/** + * Extracts EIM release zip archives. + *

+ * Newer Linux/macOS packages ship a versioned binary ({@code eim_vX.Y.Z}) plus an {@code eim} + * symlink. {@link ZipInputStream} materializes that symlink as a tiny regular file whose contents + * are the link target. Running that file with arguments (e.g. {@code eim select …}) does not + * forward argv to the real binary and can open the GUI. This extractor detects that case and + * recreates a real symlink (or copies the target if symlinks are unsupported). + *

+ * Older packages ship only a plain {@code eim} binary — left unchanged. + */ +public final class EimZipExtractor +{ + /** + * Symlink targets stored as ZIP entry payloads are short path strings (e.g. {@code eim_v0.17.4}). + * Real EIM binaries are multi‑MB; keep this well below any plausible binary size. + */ + private static final long MAX_SYMLINK_PAYLOAD_BYTES = 512; + + private EimZipExtractor() + { + } + + /** + * Extracts {@code zipPath} into {@code destDir} and returns the preferred EIM launch path + * ({@code eim} / {@code eim.exe} when present, otherwise a versioned {@code eim_v*} binary). + */ + public static Path extract(Path zipPath, Path destDir) throws IOException + { + Files.createDirectories(destDir); + Path firstRegularFile = null; + + try (InputStream fileIn = Files.newInputStream(zipPath); ZipInputStream zis = new ZipInputStream(fileIn)) + { + ZipEntry entry; + while ((entry = zis.getNextEntry()) != null) + { + Path newPath = destDir.resolve(entry.getName()).normalize(); + if (!newPath.startsWith(destDir.normalize())) + { + throw new IOException("ZIP entry is outside target dir: " + entry.getName()); //$NON-NLS-1$ + } + + if (entry.isDirectory()) + { + Files.createDirectories(newPath); + continue; + } + + Files.createDirectories(newPath.getParent()); + Files.copy(zis, newPath, StandardCopyOption.REPLACE_EXISTING); + if (shouldMarkExecutable(newPath)) + { + newPath.toFile().setExecutable(true); + } + if (firstRegularFile == null) + { + firstRegularFile = newPath; + } + } + } + + repairMaterializedEimSymlink(destDir); + return resolvePreferredLaunchPath(destDir, firstRegularFile); + } + + /** + * If {@code eim} is a tiny text file whose content names an existing sibling binary (the usual + * result of extracting a ZIP symlink with {@link ZipInputStream}), replace it with a real + * symlink or a copy of that target. + */ + public static void repairMaterializedEimSymlink(Path destDir) throws IOException + { + Path eim = destDir.resolve("eim"); //$NON-NLS-1$ + if (!Files.isRegularFile(eim) || Files.isSymbolicLink(eim)) + { + return; + } + + long size = Files.size(eim); + if (size == 0 || size > MAX_SYMLINK_PAYLOAD_BYTES) + { + return; + } + + byte[] bytes = Files.readAllBytes(eim); + if (containsNullByte(bytes)) + { + return; + } + + String target = new String(bytes, StandardCharsets.UTF_8).trim(); + if (target.isEmpty() || target.contains("\n") || target.contains("\r") || target.contains("/") //$NON-NLS-1$ //$NON-NLS-2$ //$NON-NLS-3$ + || target.contains("\\") || target.contains("..")) //$NON-NLS-1$ //$NON-NLS-2$ + { + return; + } + + Path targetPath = destDir.resolve(target).normalize(); + if (!targetPath.startsWith(destDir.normalize()) || !Files.isRegularFile(targetPath) + || Files.isSameFile(eim, targetPath)) + { + return; + } + + if (!looksLikeEimBinaryName(targetPath.getFileName().toString())) + { + return; + } + + Files.delete(eim); + try + { + Files.createSymbolicLink(eim, Path.of(target)); + Logger.log("Restored EIM symlink " + eim + " -> " + target); //$NON-NLS-1$ //$NON-NLS-2$ + } + catch (UnsupportedOperationException | IOException e) + { + Logger.log("Could not create EIM symlink; copying target instead: " + e.getMessage()); //$NON-NLS-1$ + Files.copy(targetPath, eim, StandardCopyOption.REPLACE_EXISTING); + eim.toFile().setExecutable(true); + } + } + + public static boolean looksLikeMaterializedSymlinkPayload(Path file, Path destDir) throws IOException + { + if (!Files.isRegularFile(file) || Files.isSymbolicLink(file)) + { + return false; + } + long size = Files.size(file); + if (size == 0 || size > MAX_SYMLINK_PAYLOAD_BYTES) + { + return false; + } + byte[] bytes = Files.readAllBytes(file); + if (containsNullByte(bytes)) + { + return false; + } + String target = new String(bytes, StandardCharsets.UTF_8).trim(); + if (target.isEmpty() || target.contains("\n") || target.contains("/") || target.contains("\\")) //$NON-NLS-1$ //$NON-NLS-2$ //$NON-NLS-3$ + { + return false; + } + Path targetPath = destDir.resolve(target).normalize(); + return targetPath.startsWith(destDir.normalize()) && Files.isRegularFile(targetPath) + && looksLikeEimBinaryName(targetPath.getFileName().toString()); + } + + private static boolean shouldMarkExecutable(Path extractedFile) + { + if (Platform.OS_WIN32.equals(Platform.getOS())) + { + return false; + } + return looksLikeEimBinaryName(extractedFile.getFileName().toString()); + } + + private static boolean looksLikeEimBinaryName(String name) + { + String lower = name.toLowerCase(); + return lower.equals("eim") || lower.startsWith("eim_v") || lower.startsWith("eim-"); //$NON-NLS-1$ //$NON-NLS-2$ //$NON-NLS-3$ + } + + private static boolean containsNullByte(byte[] bytes) + { + for (byte b : bytes) + { + if (b == 0) + { + return true; + } + } + return false; + } + + private static Path resolvePreferredLaunchPath(Path destDir, Path firstRegularFile) throws IOException + { + Path stableEim = destDir.resolve("eim"); //$NON-NLS-1$ + if (Files.exists(stableEim)) + { + return stableEim; + } + + Path stableEimExe = destDir.resolve("eim.exe"); //$NON-NLS-1$ + if (Files.exists(stableEimExe)) + { + return stableEimExe; + } + + Optional versioned = findVersionedEimBinary(destDir); + if (versioned.isPresent()) + { + return versioned.get(); + } + + return firstRegularFile != null ? firstRegularFile : destDir; + } + + private static Optional findVersionedEimBinary(Path destDir) throws IOException + { + try (Stream entries = Files.list(destDir)) + { + return entries.filter(Files::isRegularFile) + .filter(p -> p.getFileName().toString().matches("(?i)eim_v.+")) //$NON-NLS-1$ + .sorted(Comparator.comparing(p -> p.getFileName().toString())) + .findFirst(); + } + } +} diff --git a/tests/com.espressif.idf.core.test/src/com/espressif/idf/core/tools/test/EimZipExtractorTest.java b/tests/com.espressif.idf.core.test/src/com/espressif/idf/core/tools/test/EimZipExtractorTest.java new file mode 100644 index 000000000..67ee7b161 --- /dev/null +++ b/tests/com.espressif.idf.core.test/src/com/espressif/idf/core/tools/test/EimZipExtractorTest.java @@ -0,0 +1,135 @@ +/******************************************************************************* + * Copyright 2026 Espressif Systems (Shanghai) PTE LTD. All rights reserved. + * Use is subject to license terms. + *******************************************************************************/ +package com.espressif.idf.core.tools.test; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import java.io.IOException; +import java.io.OutputStream; +import java.nio.charset.StandardCharsets; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.zip.ZipEntry; +import java.util.zip.ZipOutputStream; + +import org.eclipse.core.runtime.Platform; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.io.TempDir; + +import com.espressif.idf.core.tools.EimZipExtractor; + +class EimZipExtractorTest +{ + @TempDir + Path tempDir; + + @Test + void extractsPlainEimBinaryFromLegacyZip() throws Exception + { + Path zip = tempDir.resolve("legacy-eim.zip"); + Path dest = tempDir.resolve("out-legacy"); + // Payload larger than the symlink heuristic threshold and not a sibling path name. + String payload = "A".repeat(1024); + writeSingleFileZip(zip, "eim", payload); + + Path launchPath = EimZipExtractor.extract(zip, dest); + + Path eim = dest.resolve("eim"); + assertEquals(eim, launchPath); + assertTrue(Files.isRegularFile(eim)); + assertFalse(Files.isSymbolicLink(eim)); + assertEquals(payload, Files.readString(eim)); + if (!Platform.OS_WIN32.equals(Platform.getOS())) + { + assertTrue(Files.isExecutable(eim)); + } + } + + @Test + void repairsMaterializedSymlinkFromVersionedZip() throws Exception + { + Path zip = tempDir.resolve("versioned-eim.zip"); + Path dest = tempDir.resolve("out-versioned"); + // Simulate ZipInputStream behavior: symlink stored as a tiny regular file with target text. + writeTwoFileZip(zip, "eim_v0.17.4", "real-binary-bytes", "eim", "eim_v0.17.4"); + + Path launchPath = EimZipExtractor.extract(zip, dest); + + Path versioned = dest.resolve("eim_v0.17.4"); + Path eim = dest.resolve("eim"); + assertTrue(Files.isRegularFile(versioned)); + assertEquals(eim, launchPath); + assertEquals("real-binary-bytes", Files.readString(versioned)); + + if (Platform.OS_WIN32.equals(Platform.getOS())) + { + // Fallback on Windows: copy of the versioned binary (symlink may be unavailable). + assertTrue(Files.isRegularFile(eim)); + assertEquals("real-binary-bytes", Files.readString(eim)); + } + else + { + assertTrue(Files.isSymbolicLink(eim)); + assertEquals(Path.of("eim_v0.17.4"), Files.readSymbolicLink(eim)); + assertEquals("real-binary-bytes", Files.readString(eim)); + assertTrue(Files.isExecutable(versioned)); + } + } + + @Test + void doesNotTreatLargeEimFileAsSymlinkPayload() throws Exception + { + Path dest = tempDir.resolve("probe"); + Files.createDirectories(dest); + Path versioned = dest.resolve("eim_v0.17.4"); + Files.writeString(versioned, "real-binary"); + Path eim = dest.resolve("eim"); + Files.writeString(eim, "X".repeat(1024)); + + assertFalse(EimZipExtractor.looksLikeMaterializedSymlinkPayload(eim, dest)); + EimZipExtractor.repairMaterializedEimSymlink(dest); + assertFalse(Files.isSymbolicLink(eim)); + assertEquals("X".repeat(1024), Files.readString(eim)); + } + + @Test + void detectsTinySiblingPathPayloadAsMaterializedSymlink() throws Exception + { + Path dest = tempDir.resolve("probe2"); + Files.createDirectories(dest); + Files.writeString(dest.resolve("eim_v0.17.4"), "real-binary"); + Path eim = dest.resolve("eim"); + Files.writeString(eim, "eim_v0.17.4"); + + assertTrue(EimZipExtractor.looksLikeMaterializedSymlinkPayload(eim, dest)); + } + + private static void writeSingleFileZip(Path zipPath, String entryName, String payload) throws IOException + { + try (OutputStream out = Files.newOutputStream(zipPath); ZipOutputStream zos = new ZipOutputStream(out)) + { + zos.putNextEntry(new ZipEntry(entryName)); + zos.write(payload.getBytes(StandardCharsets.UTF_8)); + zos.closeEntry(); + } + } + + private static void writeTwoFileZip(Path zipPath, String firstName, String firstPayload, String secondName, + String secondPayload) throws IOException + { + try (OutputStream out = Files.newOutputStream(zipPath); ZipOutputStream zos = new ZipOutputStream(out)) + { + zos.putNextEntry(new ZipEntry(firstName)); + zos.write(firstPayload.getBytes(StandardCharsets.UTF_8)); + zos.closeEntry(); + + zos.putNextEntry(new ZipEntry(secondName)); + zos.write(secondPayload.getBytes(StandardCharsets.UTF_8)); + zos.closeEntry(); + } + } +} From 50ae58795e14abb58e207d56f840ccbd4e30598d Mon Sep 17 00:00:00 2001 From: Denys Almazov Date: Mon, 10 Aug 2026 10:42:06 +0300 Subject: [PATCH 2/3] fix: add isSafeSymlinkTargetName and unit test --- .../idf/core/tools/EimZipExtractor.java | 15 ++++++++++++--- .../core/tools/test/EimZipExtractorTest.java | 19 +++++++++++++++++++ 2 files changed, 31 insertions(+), 3 deletions(-) diff --git a/bundles/com.espressif.idf.core/src/com/espressif/idf/core/tools/EimZipExtractor.java b/bundles/com.espressif.idf.core/src/com/espressif/idf/core/tools/EimZipExtractor.java index ac41f44e2..35c9283ed 100644 --- a/bundles/com.espressif.idf.core/src/com/espressif/idf/core/tools/EimZipExtractor.java +++ b/bundles/com.espressif.idf.core/src/com/espressif/idf/core/tools/EimZipExtractor.java @@ -112,8 +112,7 @@ public static void repairMaterializedEimSymlink(Path destDir) throws IOException } String target = new String(bytes, StandardCharsets.UTF_8).trim(); - if (target.isEmpty() || target.contains("\n") || target.contains("\r") || target.contains("/") //$NON-NLS-1$ //$NON-NLS-2$ //$NON-NLS-3$ - || target.contains("\\") || target.contains("..")) //$NON-NLS-1$ //$NON-NLS-2$ + if (!isSafeSymlinkTargetName(target)) { return; } @@ -161,7 +160,7 @@ public static boolean looksLikeMaterializedSymlinkPayload(Path file, Path destDi return false; } String target = new String(bytes, StandardCharsets.UTF_8).trim(); - if (target.isEmpty() || target.contains("\n") || target.contains("/") || target.contains("\\")) //$NON-NLS-1$ //$NON-NLS-2$ //$NON-NLS-3$ + if (!isSafeSymlinkTargetName(target)) { return false; } @@ -170,6 +169,16 @@ public static boolean looksLikeMaterializedSymlinkPayload(Path file, Path destDi && looksLikeEimBinaryName(targetPath.getFileName().toString()); } + /** + * Shared validation for ZIP-symlink payloads: reject empty names, path separators, parent + * references, and stray newlines so detection and repair stay aligned. + */ + public static boolean isSafeSymlinkTargetName(String target) + { + return !target.isEmpty() && !target.contains("\n") && !target.contains("\r") //$NON-NLS-1$ //$NON-NLS-2$ + && !target.contains("/") && !target.contains("\\") && !target.contains(".."); //$NON-NLS-1$ //$NON-NLS-2$ //$NON-NLS-3$ + } + private static boolean shouldMarkExecutable(Path extractedFile) { if (Platform.OS_WIN32.equals(Platform.getOS())) diff --git a/tests/com.espressif.idf.core.test/src/com/espressif/idf/core/tools/test/EimZipExtractorTest.java b/tests/com.espressif.idf.core.test/src/com/espressif/idf/core/tools/test/EimZipExtractorTest.java index 67ee7b161..dfd98fd4e 100644 --- a/tests/com.espressif.idf.core.test/src/com/espressif/idf/core/tools/test/EimZipExtractorTest.java +++ b/tests/com.espressif.idf.core.test/src/com/espressif/idf/core/tools/test/EimZipExtractorTest.java @@ -108,6 +108,25 @@ void detectsTinySiblingPathPayloadAsMaterializedSymlink() throws Exception assertTrue(EimZipExtractor.looksLikeMaterializedSymlinkPayload(eim, dest)); } + @Test + void rejectsUnsafeSymlinkTargetNamesInDetectionAndRepair() throws Exception + { + Path dest = tempDir.resolve("probe-unsafe"); + Files.createDirectories(dest); + Files.writeString(dest.resolve("eim_v..0"), "real-binary"); + + Path eim = dest.resolve("eim"); + Files.writeString(eim, "eim_v..0"); + + assertFalse(EimZipExtractor.isSafeSymlinkTargetName("eim_v..0")); + assertFalse(EimZipExtractor.isSafeSymlinkTargetName("eim\r_v0.17.4")); + assertFalse(EimZipExtractor.looksLikeMaterializedSymlinkPayload(eim, dest)); + + EimZipExtractor.repairMaterializedEimSymlink(dest); + assertFalse(Files.isSymbolicLink(eim)); + assertEquals("eim_v..0", Files.readString(eim)); + } + private static void writeSingleFileZip(Path zipPath, String entryName, String payload) throws IOException { try (OutputStream out = Files.newOutputStream(zipPath); ZipOutputStream zos = new ZipOutputStream(out)) From 8a915ae76429b10e3ce1d52a0e0a9635e05c1e2c Mon Sep 17 00:00:00 2001 From: Denys Almazov Date: Mon, 10 Aug 2026 10:47:33 +0300 Subject: [PATCH 3/3] fix: consider only extracted files when selecting EIM launch path --- .../idf/core/tools/EimZipExtractor.java | 59 ++++++++++++------- .../core/tools/test/EimZipExtractorTest.java | 38 ++++++++++++ 2 files changed, 75 insertions(+), 22 deletions(-) diff --git a/bundles/com.espressif.idf.core/src/com/espressif/idf/core/tools/EimZipExtractor.java b/bundles/com.espressif.idf.core/src/com/espressif/idf/core/tools/EimZipExtractor.java index 35c9283ed..65f848abf 100644 --- a/bundles/com.espressif.idf.core/src/com/espressif/idf/core/tools/EimZipExtractor.java +++ b/bundles/com.espressif.idf.core/src/com/espressif/idf/core/tools/EimZipExtractor.java @@ -10,9 +10,11 @@ import java.nio.file.Files; import java.nio.file.Path; import java.nio.file.StandardCopyOption; +import java.util.Collection; import java.util.Comparator; +import java.util.LinkedHashSet; import java.util.Optional; -import java.util.stream.Stream; +import java.util.Set; import java.util.zip.ZipEntry; import java.util.zip.ZipInputStream; @@ -30,6 +32,9 @@ * recreates a real symlink (or copies the target if symlinks are unsupported). *

* Older packages ship only a plain {@code eim} binary — left unchanged. + *

+ * Launch-path selection considers only files from the current archive, so leftover binaries in + * {@code destDir} from a previous install are not chosen. */ public final class EimZipExtractor { @@ -45,11 +50,13 @@ private EimZipExtractor() /** * Extracts {@code zipPath} into {@code destDir} and returns the preferred EIM launch path - * ({@code eim} / {@code eim.exe} when present, otherwise a versioned {@code eim_v*} binary). + * ({@code eim} / {@code eim.exe} when present in this archive, otherwise a versioned + * {@code eim_v*} binary from this archive). */ public static Path extract(Path zipPath, Path destDir) throws IOException { Files.createDirectories(destDir); + Set extractedFiles = new LinkedHashSet<>(); Path firstRegularFile = null; try (InputStream fileIn = Files.newInputStream(zipPath); ZipInputStream zis = new ZipInputStream(fileIn)) @@ -71,6 +78,7 @@ public static Path extract(Path zipPath, Path destDir) throws IOException Files.createDirectories(newPath.getParent()); Files.copy(zis, newPath, StandardCopyOption.REPLACE_EXISTING); + extractedFiles.add(newPath); if (shouldMarkExecutable(newPath)) { newPath.toFile().setExecutable(true); @@ -82,18 +90,28 @@ public static Path extract(Path zipPath, Path destDir) throws IOException } } - repairMaterializedEimSymlink(destDir); - return resolvePreferredLaunchPath(destDir, firstRegularFile); + repairMaterializedEimSymlink(destDir, extractedFiles); + return resolvePreferredLaunchPath(destDir, extractedFiles, firstRegularFile); } /** - * If {@code eim} is a tiny text file whose content names an existing sibling binary (the usual - * result of extracting a ZIP symlink with {@link ZipInputStream}), replace it with a real - * symlink or a copy of that target. + * If {@code eim} was extracted from this archive as a tiny text file whose content names an + * existing sibling binary (the usual result of extracting a ZIP symlink with + * {@link ZipInputStream}), replace it with a real symlink or a copy of that target. */ public static void repairMaterializedEimSymlink(Path destDir) throws IOException { - Path eim = destDir.resolve("eim"); //$NON-NLS-1$ + Path eim = destDir.resolve("eim").normalize(); //$NON-NLS-1$ + repairMaterializedEimSymlink(destDir, Set.of(eim)); + } + + static void repairMaterializedEimSymlink(Path destDir, Collection extractedFromArchive) throws IOException + { + Path eim = destDir.resolve("eim").normalize(); //$NON-NLS-1$ + if (!extractedFromArchive.contains(eim)) + { + return; + } if (!Files.isRegularFile(eim) || Files.isSymbolicLink(eim)) { return; @@ -206,21 +224,22 @@ private static boolean containsNullByte(byte[] bytes) return false; } - private static Path resolvePreferredLaunchPath(Path destDir, Path firstRegularFile) throws IOException + private static Path resolvePreferredLaunchPath(Path destDir, Collection extractedFromArchive, + Path firstRegularFile) { - Path stableEim = destDir.resolve("eim"); //$NON-NLS-1$ - if (Files.exists(stableEim)) + Path stableEim = destDir.resolve("eim").normalize(); //$NON-NLS-1$ + if (extractedFromArchive.contains(stableEim) && Files.exists(stableEim)) { return stableEim; } - Path stableEimExe = destDir.resolve("eim.exe"); //$NON-NLS-1$ - if (Files.exists(stableEimExe)) + Path stableEimExe = destDir.resolve("eim.exe").normalize(); //$NON-NLS-1$ + if (extractedFromArchive.contains(stableEimExe) && Files.exists(stableEimExe)) { return stableEimExe; } - Optional versioned = findVersionedEimBinary(destDir); + Optional versioned = findVersionedEimBinary(extractedFromArchive); if (versioned.isPresent()) { return versioned.get(); @@ -229,14 +248,10 @@ private static Path resolvePreferredLaunchPath(Path destDir, Path firstRegularFi return firstRegularFile != null ? firstRegularFile : destDir; } - private static Optional findVersionedEimBinary(Path destDir) throws IOException + private static Optional findVersionedEimBinary(Collection extractedFromArchive) { - try (Stream entries = Files.list(destDir)) - { - return entries.filter(Files::isRegularFile) - .filter(p -> p.getFileName().toString().matches("(?i)eim_v.+")) //$NON-NLS-1$ - .sorted(Comparator.comparing(p -> p.getFileName().toString())) - .findFirst(); - } + return extractedFromArchive.stream().filter(Files::isRegularFile) + .filter(p -> p.getFileName().toString().matches("(?i)eim_v.+")) //$NON-NLS-1$ + .sorted(Comparator.comparing(p -> p.getFileName().toString())).findFirst(); } } diff --git a/tests/com.espressif.idf.core.test/src/com/espressif/idf/core/tools/test/EimZipExtractorTest.java b/tests/com.espressif.idf.core.test/src/com/espressif/idf/core/tools/test/EimZipExtractorTest.java index dfd98fd4e..92a36ed48 100644 --- a/tests/com.espressif.idf.core.test/src/com/espressif/idf/core/tools/test/EimZipExtractorTest.java +++ b/tests/com.espressif.idf.core.test/src/com/espressif/idf/core/tools/test/EimZipExtractorTest.java @@ -127,6 +127,44 @@ void rejectsUnsafeSymlinkTargetNamesInDetectionAndRepair() throws Exception assertEquals("eim_v..0", Files.readString(eim)); } + @Test + void prefersLaunchPathFromCurrentArchiveOverPreexistingFiles() throws Exception + { + Path dest = tempDir.resolve("update"); + Files.createDirectories(dest); + Files.writeString(dest.resolve("eim"), "old-stable-eim"); + Files.writeString(dest.resolve("eim_v0.16.0"), "old-versioned"); + + Path zip = tempDir.resolve("new-only-versioned.zip"); + writeSingleFileZip(zip, "eim_v0.17.4", "new-binary"); + + Path launchPath = EimZipExtractor.extract(zip, dest); + + assertEquals(dest.resolve("eim_v0.17.4"), launchPath); + assertEquals("new-binary", Files.readString(launchPath)); + assertEquals("old-stable-eim", Files.readString(dest.resolve("eim"))); + assertEquals("old-versioned", Files.readString(dest.resolve("eim_v0.16.0"))); + } + + @Test + void doesNotRepairPreexistingEimWhenNotInCurrentArchive() throws Exception + { + Path dest = tempDir.resolve("leftover-symlink-payload"); + Files.createDirectories(dest); + Files.writeString(dest.resolve("eim_v0.16.0"), "old-binary"); + Path eim = dest.resolve("eim"); + Files.writeString(eim, "eim_v0.16.0"); + + Path zip = tempDir.resolve("new-version-only.zip"); + writeSingleFileZip(zip, "eim_v0.17.4", "new-binary"); + + Path launchPath = EimZipExtractor.extract(zip, dest); + + assertEquals(dest.resolve("eim_v0.17.4"), launchPath); + assertFalse(Files.isSymbolicLink(eim)); + assertEquals("eim_v0.16.0", Files.readString(eim)); + } + private static void writeSingleFileZip(Path zipPath, String entryName, String payload) throws IOException { try (OutputStream out = Files.newOutputStream(zipPath); ZipOutputStream zos = new ZipOutputStream(out))