diff --git a/spring-boot-project/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/nio/file/NestedByteChannel.java b/spring-boot-project/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/nio/file/NestedByteChannel.java index e41b27fd65..3e51924ed3 100644 --- a/spring-boot-project/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/nio/file/NestedByteChannel.java +++ b/spring-boot-project/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/nio/file/NestedByteChannel.java @@ -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 diff --git a/spring-boot-project/spring-boot-tools/spring-boot-loader/src/test/java/org/springframework/boot/loader/nio/file/NestedByteChannelTests.java b/spring-boot-project/spring-boot-tools/spring-boot-loader/src/test/java/org/springframework/boot/loader/nio/file/NestedByteChannelTests.java index d9ff2e83a1..7fef82d351 100644 --- a/spring-boot-project/spring-boot-tools/spring-boot-loader/src/test/java/org/springframework/boot/loader/nio/file/NestedByteChannelTests.java +++ b/spring-boot-project/spring-boot-tools/spring-boot-loader/src/test/java/org/springframework/boot/loader/nio/file/NestedByteChannelTests.java @@ -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) diff --git a/spring-boot-project/spring-boot-tools/spring-boot-loader/src/test/java/org/springframework/boot/loader/zip/FileChannelDataBlockManagedFileChannel.java b/spring-boot-project/spring-boot-tools/spring-boot-loader/src/test/java/org/springframework/boot/loader/zip/FileChannelDataBlockManagedFileChannel.java new file mode 100644 index 0000000000..6e945dbd43 --- /dev/null +++ b/spring-boot-project/spring-boot-tools/spring-boot-loader/src/test/java/org/springframework/boot/loader/zip/FileChannelDataBlockManagedFileChannel.java @@ -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; + +}