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>
This commit is contained in:
FenjuFu 2026-08-24 19:58:18 +08:00 committed by GitHub
parent d5c6411ce6
commit 954dfce7a4
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
4 changed files with 49 additions and 2 deletions

View file

@ -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;

View file

@ -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;
}

View file

@ -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<PackageEntry> 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(

View file

@ -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<PackageEntry> entries = extractor.extract(file);
assertEquals(1, entries.size());
assertEquals("SKILL.md", entries.get(0).path());
}
private byte[] createZip(Map<String, byte[]> entries) throws Exception {
ByteArrayOutputStream baos = new ByteArrayOutputStream();
try (ZipOutputStream zos = new ZipOutputStream(baos)) {