Copy zip data descriptor records when creating virtual zip data

The zip specification states that when 'bit 3' of the general purpose
flags is set then a data descriptor record must be present. Prior to
this commit, our `VirtualZipDataBlock` ignored such records and would
create invalid data.

Although the generated data would work for zip parsers that read the
central directory records, it causes problems with streaming reader
implementations such as `JarInputStream`.

This commit updates the code so that it now copies the data descriptor
records. It support both blocks that have a signature and those that
don't. It also updates the generation logic to correctly deal with
any extra data bytes present after the local file header record.

Fixes gh-38063
This commit is contained in:
Phillip Webb
2023-10-26 13:17:45 -07:00
parent 5ff4a961b1
commit bba323ba5f
6 changed files with 296 additions and 12 deletions

View File

@@ -25,6 +25,8 @@ import java.nio.file.Files;
import java.nio.file.NoSuchFileException;
import java.util.ArrayList;
import java.util.List;
import java.util.zip.ZipEntry;
import java.util.zip.ZipOutputStream;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
@@ -95,4 +97,42 @@ class VirtualZipDataBlockTests {
}
}
@Test // gh-38063
void createWithDescriptorRecordContainsValidZipContent() throws Exception {
try (ZipOutputStream zip = new ZipOutputStream(new FileOutputStream(this.file))) {
ZipEntry entry = new ZipEntry("META-INF/");
entry.setMethod(ZipEntry.DEFLATED);
zip.putNextEntry(entry);
zip.write(new byte[] { 0, 1, 2, 3, 4, 5, 6, 7, 8 });
zip.closeEntry();
}
byte[] bytes = Files.readAllBytes(this.file.toPath());
CloseableDataBlock data = new ByteArrayDataBlock(bytes);
List<ZipCentralDirectoryFileHeaderRecord> centralRecords = new ArrayList<>();
List<Long> centralRecordPositions = new ArrayList<>();
ZipEndOfCentralDirectoryRecord eocd = ZipEndOfCentralDirectoryRecord.load(data).endOfCentralDirectoryRecord();
long pos = eocd.offsetToStartOfCentralDirectory();
for (int i = 0; i < eocd.totalNumberOfCentralDirectoryEntries(); i++) {
ZipCentralDirectoryFileHeaderRecord centralRecord = ZipCentralDirectoryFileHeaderRecord.load(data, pos);
centralRecords.add(centralRecord);
centralRecordPositions.add(pos);
pos += centralRecord.size();
}
NameOffsetLookups nameOffsetLookups = new NameOffsetLookups(0, centralRecords.size());
for (int i = 0; i < centralRecords.size(); i++) {
nameOffsetLookups.enable(i, true);
}
nameOffsetLookups.enable(0, true);
File outputFile = new File(this.tempDir, "out.jar");
try (VirtualZipDataBlock block = new VirtualZipDataBlock(data, nameOffsetLookups,
centralRecords.toArray(ZipCentralDirectoryFileHeaderRecord[]::new),
centralRecordPositions.stream().mapToLong(Long::longValue).toArray())) {
try (FileOutputStream out = new FileOutputStream(outputFile)) {
block.asInputStream().transferTo(out);
}
}
byte[] virtualBytes = Files.readAllBytes(outputFile.toPath());
assertThat(bytes).isEqualTo(virtualBytes);
}
}

View File

@@ -0,0 +1,111 @@
/*
* Copyright 2012-2023 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.boot.loader.zip;
import org.junit.jupiter.api.Test;
import static org.assertj.core.api.Assertions.assertThat;
/**
* Tests for {@link ZipDataDescriptorRecord}.
*
* @author Phillip Webb
*/
class ZipDataDescriptorRecordTests {
private static final short S0 = 0;
@Test
void loadWhenHasSignatureLoadsData() throws Exception {
DataBlock dataBlock = new ByteArrayDataBlock(new byte[] { //
0x50, 0x4b, 0x07, 0x08, //
0x01, 0x00, 0x00, 0x00, //
0x02, 0x00, 0x00, 0x00, //
0x03, 0x00, 0x00, 0x00 }); //
ZipDataDescriptorRecord record = ZipDataDescriptorRecord.load(dataBlock, 0);
assertThat(record.includeSignature()).isTrue();
assertThat(record.crc32()).isEqualTo(1);
assertThat(record.compressedSize()).isEqualTo(2);
assertThat(record.uncompressedSize()).isEqualTo(3);
}
@Test
void loadWhenHasNoSignatureLoadsData() throws Exception {
DataBlock dataBlock = new ByteArrayDataBlock(new byte[] { //
0x01, 0x00, 0x00, 0x00, //
0x02, 0x00, 0x00, 0x00, //
0x03, 0x00, 0x00, 0x00 }); //
ZipDataDescriptorRecord record = ZipDataDescriptorRecord.load(dataBlock, 0);
assertThat(record.includeSignature()).isFalse();
assertThat(record.crc32()).isEqualTo(1);
assertThat(record.compressedSize()).isEqualTo(2);
assertThat(record.uncompressedSize()).isEqualTo(3);
}
@Test
void sizeWhenIncludeSignatureReturnsSize() {
ZipDataDescriptorRecord record = new ZipDataDescriptorRecord(true, 0, 0, 0);
assertThat(record.size()).isEqualTo(16);
}
@Test
void sizeWhenNotIncludeSignatureReturnsSize() {
ZipDataDescriptorRecord record = new ZipDataDescriptorRecord(false, 0, 0, 0);
assertThat(record.size()).isEqualTo(12);
}
@Test
void asByteArrayWhenIncludeSignatureReturnsByteArray() throws Exception {
byte[] bytes = new byte[] { //
0x50, 0x4b, 0x07, 0x08, //
0x01, 0x00, 0x00, 0x00, //
0x02, 0x00, 0x00, 0x00, //
0x03, 0x00, 0x00, 0x00 }; //
ZipDataDescriptorRecord record = ZipDataDescriptorRecord.load(new ByteArrayDataBlock(bytes), 0);
assertThat(record.asByteArray()).isEqualTo(bytes);
}
@Test
void asByteArrayWhenNotIncludeSignatureReturnsByteArray() throws Exception {
byte[] bytes = new byte[] { //
0x01, 0x00, 0x00, 0x00, //
0x02, 0x00, 0x00, 0x00, //
0x03, 0x00, 0x00, 0x00 }; //
ZipDataDescriptorRecord record = ZipDataDescriptorRecord.load(new ByteArrayDataBlock(bytes), 0);
assertThat(record.asByteArray()).isEqualTo(bytes);
}
@Test
void isPresentBasedOnFlagWhenPresentReturnsTrue() {
testIsPresentBasedOnFlag((short) 0x8, true);
}
@Test
void isPresentBasedOnFlagWhenNotPresentReturnsFalse() {
testIsPresentBasedOnFlag((short) 0x0, false);
}
private void testIsPresentBasedOnFlag(short flag, boolean expected) {
ZipCentralDirectoryFileHeaderRecord centralRecord = new ZipCentralDirectoryFileHeaderRecord(S0, S0, flag, S0,
S0, S0, S0, S0, S0, S0, S0, S0, S0, S0, S0, S0);
ZipLocalFileHeaderRecord localRecord = new ZipLocalFileHeaderRecord(S0, flag, S0, S0, S0, S0, S0, S0, S0, S0);
assertThat(ZipDataDescriptorRecord.isPresentBasedOnFlag(flag)).isEqualTo(expected);
assertThat(ZipDataDescriptorRecord.isPresentBasedOnFlag(centralRecord)).isEqualTo(expected);
assertThat(ZipDataDescriptorRecord.isPresentBasedOnFlag(localRecord)).isEqualTo(expected);
}
}