Update NestedByteChannel.read to read all possible data when

Update `NestedByteChannel.read` so that it loops until all
remaining data has been read into the buffer. Prior to this
commit, it was possible for to read only some bytes into the
buffer. Although it looks like this should be OK according to
the API documentation, the `ZipFileSystem` relies on all
remaining bytes being returned.

Fixes gh-38595
This commit is contained in:
Phillip Webb
2023-11-28 14:16:35 -08:00
parent 75a8955659
commit 9a0f95420a
3 changed files with 77 additions and 3 deletions

View File

@@ -81,11 +81,16 @@ class NestedByteChannel implements SeekableByteChannel {
@Override
public int read(ByteBuffer dst) throws IOException {
assertNotClosed();
int count = this.resources.getData().read(dst, this.position);
if (count > 0) {
int total = 0;
while (dst.remaining() > 0) {
int count = this.resources.getData().read(dst, this.position);
if (count <= 0) {
return (total != 0) ? 0 : count;
}
total += count;
this.position += count;
}
return count;
return total;
}
@Override

View File

@@ -17,11 +17,16 @@
package org.springframework.boot.loader.nio.file;
import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.lang.ref.Cleaner.Cleanable;
import java.nio.ByteBuffer;
import java.nio.channels.ClosedChannelException;
import java.nio.channels.NonWritableChannelException;
import java.util.jar.JarEntry;
import java.util.jar.JarOutputStream;
import java.util.zip.CRC32;
import java.util.zip.ZipEntry;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.BeforeEach;
@@ -32,6 +37,7 @@ import org.mockito.ArgumentCaptor;
import org.springframework.boot.loader.ref.Cleaner;
import org.springframework.boot.loader.testsupport.TestJar;
import org.springframework.boot.loader.zip.AssertFileChannelDataBlocksClosed;
import org.springframework.boot.loader.zip.FileChannelDataBlockManagedFileChannel;
import org.springframework.boot.loader.zip.ZipContent;
import static org.assertj.core.api.Assertions.assertThat;
@@ -117,6 +123,36 @@ class NestedByteChannelTests {
assertThat(dst.array()).isNotEqualTo(ByteBuffer.allocate(10).array());
}
@Test // gh-38592
void readReadsAsManyBytesAsPossible() throws Exception {
// ZipFileSystem checks that the number of bytes read matches an expected value
// ...if (readFullyAt(cen, 0, cen.length, cenpos) != end.cenlen + ENDHDR)
// but the readFullyAt assumes that all remaining bytes are attempted to be read
// This doesn't seem to exactly match the contract of ReadableByteChannel.read
// which states "A read operation might not fill the buffer, and in fact it might
// not read any bytes at all", but we need to match ZipFileSystem's expectations
int size = FileChannelDataBlockManagedFileChannel.BUFFER_SIZE * 2;
byte[] data = new byte[size];
this.file = new File(this.temp, "testread.jar");
FileOutputStream fileOutputStream = new FileOutputStream(this.file);
try (JarOutputStream jarOutputStream = new JarOutputStream(fileOutputStream)) {
JarEntry nestedEntry = new JarEntry("data");
nestedEntry.setSize(size);
nestedEntry.setCompressedSize(size);
CRC32 crc32 = new CRC32();
crc32.update(data);
nestedEntry.setCrc(crc32.getValue());
nestedEntry.setMethod(ZipEntry.STORED);
jarOutputStream.putNextEntry(nestedEntry);
jarOutputStream.write(data);
jarOutputStream.closeEntry();
}
this.channel = new NestedByteChannel(this.file.toPath(), null);
ByteBuffer buffer = ByteBuffer.allocate((int) this.file.length());
assertThat(this.channel.read(buffer)).isEqualTo(buffer.capacity());
assertThat(this.file).binaryContent().isEqualTo(buffer.array());
}
@Test
void writeThrowsException() {
assertThatExceptionOfType(NonWritableChannelException.class)

View File

@@ -0,0 +1,33 @@
/*
* 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.springframework.boot.loader.zip.FileChannelDataBlock.ManagedFileChannel;
/**
* Test access to {@link ManagedFileChannel} details.
*
* @author Phillip Webb
*/
public final class FileChannelDataBlockManagedFileChannel {
private FileChannelDataBlockManagedFileChannel() {
}
public static int BUFFER_SIZE = FileChannelDataBlock.ManagedFileChannel.BUFFER_SIZE;
}