|
25 | 25 | import jakarta.servlet.http.HttpServletRequestWrapper; |
26 | 26 | import jakarta.servlet.http.HttpServletResponse; |
27 | 27 | import java.io.ByteArrayInputStream; |
| 28 | +import java.io.DataOutputStream; |
28 | 29 | import java.io.FileInputStream; |
29 | 30 | import java.io.IOException; |
30 | 31 | import java.io.InputStream; |
@@ -1002,6 +1003,32 @@ public void testUploadWithForbiddenContent() throws Exception { |
1002 | 1003 | assertEquals(400, res); |
1003 | 1004 | } |
1004 | 1005 |
|
| 1006 | + @Test |
| 1007 | + public void testUploadWithBlankFile() throws Exception { |
| 1008 | + // Uploads a zip containing a blank (0-byte) file using STORED method with an EXT descriptor. |
| 1009 | + // Java's ZipInputStream cannot read this format, but ZipFile can. |
| 1010 | + // Verifies the upload succeeds and the empty file is stored in the configset. |
| 1011 | + final String configSetName = "blank-file-configset"; |
| 1012 | + final String suffix = "-suffix"; |
| 1013 | + final Path zipFile = createTempZipWithStoredEntryAndExtDescriptor(); |
| 1014 | + try (SolrZkClient zkClient = |
| 1015 | + new SolrZkClient.Builder() |
| 1016 | + .withUrl(cluster.getZkServer().getZkAddress()) |
| 1017 | + .withTimeout(AbstractZkTestCase.TIMEOUT, TimeUnit.MILLISECONDS) |
| 1018 | + .withConnTimeOut(45000, TimeUnit.MILLISECONDS) |
| 1019 | + .build()) { |
| 1020 | + long res = uploadGivenConfigSet(zipFile, configSetName, suffix, null, true, false, true); |
| 1021 | + assertEquals("Upload of configset with blank file should succeed", 0L, res); |
| 1022 | + assertTrue( |
| 1023 | + "blank.txt should have been uploaded to the configset", |
| 1024 | + zkClient.exists("/configs/" + configSetName + suffix + "/blank.txt")); |
| 1025 | + assertArrayEquals( |
| 1026 | + "blank.txt in configset should be empty", |
| 1027 | + new byte[0], |
| 1028 | + zkClient.getData("/configs/" + configSetName + suffix + "/blank.txt", null, null)); |
| 1029 | + } |
| 1030 | + } |
| 1031 | + |
1005 | 1032 | @Test |
1006 | 1033 | public void testGetFile() throws Exception { |
1007 | 1034 | String configSetName = "regular"; |
@@ -1331,6 +1358,99 @@ private Path createTempZipFileWithForbiddenContent(String resourcePath) { |
1331 | 1358 | } |
1332 | 1359 | } |
1333 | 1360 |
|
| 1361 | + /** |
| 1362 | + * Creates a zip file (in the temp directory) containing an empty file entry that uses the STORED |
| 1363 | + * compression method with the EXT descriptor flag set. Some zip tools produce this format for |
| 1364 | + * empty (0-byte) files, e.g., when using {@code touch conf/blank.txt} followed by {@code zip -r |
| 1365 | + * ...}. Java's {@link java.util.zip.ZipInputStream} cannot read this combination, but {@link |
| 1366 | + * java.util.zip.ZipFile} handles it correctly by reading from the central directory. |
| 1367 | + */ |
| 1368 | + private Path createTempZipWithStoredEntryAndExtDescriptor() throws IOException { |
| 1369 | + final Path zipFile = createTempFile("configset-blank", "zip"); |
| 1370 | + // Build a valid ZIP file manually with one STORED entry that has the EXT (data descriptor) |
| 1371 | + // flag set (flag bit 3 = 0x08). Java's ZipInputStream rejects this combination. |
| 1372 | + // All multi-byte fields are little-endian. |
| 1373 | + byte[] fileName = "blank.txt".getBytes(UTF_8); |
| 1374 | + int fileNameLen = fileName.length; // 9 |
| 1375 | + |
| 1376 | + // Offsets for computing central directory offset |
| 1377 | + // Local file header size: 30 + fileNameLen |
| 1378 | + int localHeaderSize = 30 + fileNameLen; |
| 1379 | + // Data descriptor size: 16 (with signature) |
| 1380 | + int dataDescriptorSize = 16; |
| 1381 | + // Central directory header size: 46 + fileNameLen |
| 1382 | + int centralDirHeaderSize = 46 + fileNameLen; |
| 1383 | + int centralDirOffset = localHeaderSize + dataDescriptorSize; // = 55 |
| 1384 | + |
| 1385 | + try (DataOutputStream dos = new DataOutputStream(Files.newOutputStream(zipFile))) { |
| 1386 | + // --- Local file header --- |
| 1387 | + dos.write(new byte[] {0x50, 0x4b, 0x03, 0x04}); // signature PK\x03\x04 |
| 1388 | + dos.write(new byte[] {0x14, 0x00}); // version needed = 20 |
| 1389 | + dos.write(new byte[] {0x08, 0x00}); // flag: bit 3 (data descriptor / EXT) |
| 1390 | + dos.write(new byte[] {0x00, 0x00}); // compression method: STORED |
| 1391 | + dos.write(new byte[] {0x00, 0x00}); // last mod time |
| 1392 | + dos.write(new byte[] {0x00, 0x00}); // last mod date |
| 1393 | + dos.write(new byte[] {0x00, 0x00, 0x00, 0x00}); // CRC-32 (0, deferred to data descriptor) |
| 1394 | + dos.write(new byte[] {0x00, 0x00, 0x00, 0x00}); // compressed size (deferred) |
| 1395 | + dos.write(new byte[] {0x00, 0x00, 0x00, 0x00}); // uncompressed size (deferred) |
| 1396 | + dos.write(new byte[] {(byte) fileNameLen, 0x00}); // file name length |
| 1397 | + dos.write(new byte[] {0x00, 0x00}); // extra field length |
| 1398 | + dos.write(fileName); // file name "blank.txt" |
| 1399 | + // (no file data — the file is empty) |
| 1400 | + |
| 1401 | + // --- Data descriptor (EXT record) --- |
| 1402 | + dos.write(new byte[] {0x50, 0x4b, 0x07, 0x08}); // signature PK\x07\x08 |
| 1403 | + dos.write(new byte[] {0x00, 0x00, 0x00, 0x00}); // CRC-32 (0 for empty file) |
| 1404 | + dos.write(new byte[] {0x00, 0x00, 0x00, 0x00}); // compressed size |
| 1405 | + dos.write(new byte[] {0x00, 0x00, 0x00, 0x00}); // uncompressed size |
| 1406 | + |
| 1407 | + // --- Central directory header --- |
| 1408 | + dos.write(new byte[] {0x50, 0x4b, 0x01, 0x02}); // signature PK\x01\x02 |
| 1409 | + dos.write(new byte[] {0x14, 0x00}); // version made by |
| 1410 | + dos.write(new byte[] {0x14, 0x00}); // version needed |
| 1411 | + dos.write(new byte[] {0x08, 0x00}); // flag (same as local header) |
| 1412 | + dos.write(new byte[] {0x00, 0x00}); // compression method: STORED |
| 1413 | + dos.write(new byte[] {0x00, 0x00}); // last mod time |
| 1414 | + dos.write(new byte[] {0x00, 0x00}); // last mod date |
| 1415 | + dos.write(new byte[] {0x00, 0x00, 0x00, 0x00}); // CRC-32 |
| 1416 | + dos.write(new byte[] {0x00, 0x00, 0x00, 0x00}); // compressed size |
| 1417 | + dos.write(new byte[] {0x00, 0x00, 0x00, 0x00}); // uncompressed size |
| 1418 | + dos.write(new byte[] {(byte) fileNameLen, 0x00}); // file name length |
| 1419 | + dos.write(new byte[] {0x00, 0x00}); // extra field length |
| 1420 | + dos.write(new byte[] {0x00, 0x00}); // file comment length |
| 1421 | + dos.write(new byte[] {0x00, 0x00}); // disk number start |
| 1422 | + dos.write(new byte[] {0x00, 0x00}); // internal file attributes |
| 1423 | + dos.write(new byte[] {0x00, 0x00, 0x00, 0x00}); // external file attributes |
| 1424 | + dos.write(new byte[] {0x00, 0x00, 0x00, 0x00}); // local header relative offset (= 0) |
| 1425 | + dos.write(fileName); // file name "blank.txt" |
| 1426 | + |
| 1427 | + // --- End of central directory record --- |
| 1428 | + dos.write(new byte[] {0x50, 0x4b, 0x05, 0x06}); // signature PK\x05\x06 |
| 1429 | + dos.write(new byte[] {0x00, 0x00}); // disk number |
| 1430 | + dos.write(new byte[] {0x00, 0x00}); // disk with start of central directory |
| 1431 | + dos.write(new byte[] {0x01, 0x00}); // entries on this disk |
| 1432 | + dos.write(new byte[] {0x01, 0x00}); // total entries |
| 1433 | + // size of central directory |
| 1434 | + dos.write( |
| 1435 | + new byte[] { |
| 1436 | + (byte) (centralDirHeaderSize & 0xFF), |
| 1437 | + (byte) ((centralDirHeaderSize >> 8) & 0xFF), |
| 1438 | + (byte) ((centralDirHeaderSize >> 16) & 0xFF), |
| 1439 | + (byte) ((centralDirHeaderSize >> 24) & 0xFF) |
| 1440 | + }); |
| 1441 | + // offset of central directory |
| 1442 | + dos.write( |
| 1443 | + new byte[] { |
| 1444 | + (byte) (centralDirOffset & 0xFF), |
| 1445 | + (byte) ((centralDirOffset >> 8) & 0xFF), |
| 1446 | + (byte) ((centralDirOffset >> 16) & 0xFF), |
| 1447 | + (byte) ((centralDirOffset >> 24) & 0xFF) |
| 1448 | + }); |
| 1449 | + dos.write(new byte[] {0x00, 0x00}); // comment length |
| 1450 | + } |
| 1451 | + return zipFile; |
| 1452 | + } |
| 1453 | + |
1334 | 1454 | private static void zipWithForbiddenContent(Path directory, Path zipfile) throws IOException { |
1335 | 1455 | OutputStream out = Files.newOutputStream(zipfile); |
1336 | 1456 | assertTrue(Files.isDirectory(directory)); |
|
0 commit comments