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)) {