From 954dfce7a46e2d96353a797f72e8855b6ed80012 Mon Sep 17 00:00:00 2001 From: FenjuFu <92919259+FenjuFu@users.noreply.github.com> Date: Mon, 24 Aug 2026 19:58:18 +0800 Subject: [PATCH] fix(publish): accept Windows zip directory entries (#742) * fix(publish): accept Windows zip directory entries Signed-off-by: FenjuFu <92919259+FenjuFu@users.noreply.github.com> * chore: restore repository line endings Signed-off-by: FenjuFu <92919259+FenjuFu@users.noreply.github.com> --------- Signed-off-by: FenjuFu <92919259+FenjuFu@users.noreply.github.com> --- .../support/SkillPackageArchiveExtractor.java | 10 +++++++++- .../support/ZipPackageExtractor.java | 2 +- .../SkillPackageArchiveExtractorTest.java | 19 ++++++++++++++++++ .../support/ZipPackageExtractorTest.java | 20 +++++++++++++++++++ 4 files changed, 49 insertions(+), 2 deletions(-) diff --git a/server/skillhub-app/src/main/java/com/iflytek/skillhub/controller/support/SkillPackageArchiveExtractor.java b/server/skillhub-app/src/main/java/com/iflytek/skillhub/controller/support/SkillPackageArchiveExtractor.java index e2aa41c3..86e2bdfb 100644 --- a/server/skillhub-app/src/main/java/com/iflytek/skillhub/controller/support/SkillPackageArchiveExtractor.java +++ b/server/skillhub-app/src/main/java/com/iflytek/skillhub/controller/support/SkillPackageArchiveExtractor.java @@ -44,7 +44,7 @@ public class SkillPackageArchiveExtractor { try (ZipInputStream zis = new ZipInputStream(file.getInputStream())) { ZipEntry zipEntry; while ((zipEntry = zis.getNextEntry()) != null) { - if (zipEntry.isDirectory()) { + if (isDirectoryEntry(zipEntry)) { zis.closeEntry(); continue; } @@ -164,6 +164,14 @@ public class SkillPackageArchiveExtractor { return new ExtractionResult(promoted, warnings); } + /** + * {@link ZipEntry#isDirectory()} only recognizes the ZIP-standard forward slash. Some Windows + * archive tools emit directory entries whose names end in a backslash instead. + */ + static boolean isDirectoryEntry(ZipEntry entry) { + return entry.isDirectory() || entry.getName().endsWith("\\"); + } + private static boolean isOsMetadataEntry(String name) { String normalized = name.replace('\\', '/'); if (normalized.startsWith("__MACOSX/") || normalized.equals("__MACOSX")) return true; diff --git a/server/skillhub-app/src/main/java/com/iflytek/skillhub/controller/support/ZipPackageExtractor.java b/server/skillhub-app/src/main/java/com/iflytek/skillhub/controller/support/ZipPackageExtractor.java index a23e8cc5..4f8a36b2 100644 --- a/server/skillhub-app/src/main/java/com/iflytek/skillhub/controller/support/ZipPackageExtractor.java +++ b/server/skillhub-app/src/main/java/com/iflytek/skillhub/controller/support/ZipPackageExtractor.java @@ -40,7 +40,7 @@ public class ZipPackageExtractor { try (ZipInputStream zis = new ZipInputStream(file.getInputStream())) { ZipEntry zipEntry; while ((zipEntry = zis.getNextEntry()) != null) { - if (zipEntry.isDirectory()) { + if (SkillPackageArchiveExtractor.isDirectoryEntry(zipEntry)) { zis.closeEntry(); continue; } diff --git a/server/skillhub-app/src/test/java/com/iflytek/skillhub/controller/support/SkillPackageArchiveExtractorTest.java b/server/skillhub-app/src/test/java/com/iflytek/skillhub/controller/support/SkillPackageArchiveExtractorTest.java index c849fc4c..46dd4f72 100644 --- a/server/skillhub-app/src/test/java/com/iflytek/skillhub/controller/support/SkillPackageArchiveExtractorTest.java +++ b/server/skillhub-app/src/test/java/com/iflytek/skillhub/controller/support/SkillPackageArchiveExtractorTest.java @@ -129,6 +129,25 @@ class SkillPackageArchiveExtractorTest { assertEquals("SKILL.md", entries.get(0).path()); } + @Test + void skipsWindowsStyleDirectoryEntries() throws Exception { + ByteArrayOutputStream baos = new ByteArrayOutputStream(); + try (ZipOutputStream zos = new ZipOutputStream(baos)) { + zos.putNextEntry(new ZipEntry("my-skill\\")); + zos.closeEntry(); + zos.putNextEntry(new ZipEntry("my-skill\\SKILL.md")); + zos.write("---\nname: test\n---".getBytes()); + zos.closeEntry(); + } + MockMultipartFile file = new MockMultipartFile( + "file", "test.zip", "application/zip", baos.toByteArray()); + + List entries = extractor.extract(file); + + assertEquals(1, entries.size()); + assertEquals("SKILL.md", entries.get(0).path()); + } + @Test void doesNotStripWhenMultipleRootDirectories() throws Exception { byte[] zipBytes = createZip(Map.of( diff --git a/server/skillhub-app/src/test/java/com/iflytek/skillhub/controller/support/ZipPackageExtractorTest.java b/server/skillhub-app/src/test/java/com/iflytek/skillhub/controller/support/ZipPackageExtractorTest.java index 02f26214..31dc4dce 100644 --- a/server/skillhub-app/src/test/java/com/iflytek/skillhub/controller/support/ZipPackageExtractorTest.java +++ b/server/skillhub-app/src/test/java/com/iflytek/skillhub/controller/support/ZipPackageExtractorTest.java @@ -32,6 +32,26 @@ class ZipPackageExtractorTest { assertTrue(entries.stream().noneMatch(e -> e.path().equals("skill.md"))); } + @Test + void skipsWindowsStyleDirectoryEntries() throws Exception { + ZipPackageExtractor extractor = new ZipPackageExtractor(new SkillPublishProperties()); + ByteArrayOutputStream baos = new ByteArrayOutputStream(); + try (ZipOutputStream zos = new ZipOutputStream(baos)) { + zos.putNextEntry(new ZipEntry("my-skill\\")); + zos.closeEntry(); + zos.putNextEntry(new ZipEntry("my-skill/SKILL.md")); + zos.write("---\nname: test\n---\n".getBytes()); + zos.closeEntry(); + } + MockMultipartFile file = new MockMultipartFile( + "file", "test.zip", "application/zip", baos.toByteArray()); + + List entries = extractor.extract(file); + + assertEquals(1, entries.size()); + assertEquals("SKILL.md", entries.get(0).path()); + } + private byte[] createZip(Map entries) throws Exception { ByteArrayOutputStream baos = new ByteArrayOutputStream(); try (ZipOutputStream zos = new ZipOutputStream(baos)) {