diff --git a/org.eclipse.jgit.test/tst/org/eclipse/jgit/util/io/ByteBufferInputStreamTest.java b/org.eclipse.jgit.test/tst/org/eclipse/jgit/util/io/ByteBufferInputStreamTest.java new file mode 100644 index 000000000..ec9f96ed9 --- /dev/null +++ b/org.eclipse.jgit.test/tst/org/eclipse/jgit/util/io/ByteBufferInputStreamTest.java @@ -0,0 +1,152 @@ +/* + * Copyright (C) 2023, SAP SE and others + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Distribution License v. 1.0 which is available at + * https://www.eclipse.org/org/documents/edl-v10.php. + * + * SPDX-License-Identifier: BSD-3-Clause + */ + +package org.eclipse.jgit.util.io; + +import static org.junit.Assert.assertArrayEquals; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertThrows; +import static org.junit.Assert.assertTrue; + +import java.io.IOException; +import java.nio.ByteBuffer; +import java.util.Arrays; + +import org.eclipse.jgit.internal.JGitText; +import org.junit.After; +import org.junit.Before; +import org.junit.Test; + +public class ByteBufferInputStreamTest { + + private static final byte data[] = { 0x00, 0x01, 0x02, 0x03, 0x04, 0x05, + 0x06, 0x07, 0x08, 0x09, 0x0A, 0x0B, 0x0C, 0x0D, 0x0E, 0x0F }; + + private ByteBuffer buf; + + private ByteBufferInputStream is; + + @Before + public void setup() { + buf = ByteBuffer.wrap(data); + is = new ByteBufferInputStream(buf); + } + + @After + public void tearDown() { + is.close(); + } + + @Test + public void testRead() throws IOException { + assertEquals(0x00, is.read()); + assertEquals(0x01, is.read()); + assertEquals(0x02, is.read()); + assertEquals(0x03, is.read()); + assertEquals(0x04, is.read()); + assertEquals(0x05, is.read()); + assertEquals(0x06, is.read()); + assertEquals(0x07, is.read()); + assertEquals(0x08, is.read()); + assertEquals(0x09, is.read()); + assertEquals(0x0A, is.read()); + assertEquals(0x0B, is.read()); + assertEquals(0x0C, is.read()); + assertEquals(0x0D, is.read()); + assertEquals(0x0E, is.read()); + assertEquals(0x0F, is.read()); + assertEquals(-1, is.read()); + } + + @Test + public void testReadMultiple() throws IOException { + byte[] x = new byte[5]; + int n = is.read(x); + assertEquals(5, n); + assertArrayEquals(new byte[] { 0x00, 0x01, 0x02, 0x03, 0x04 }, x); + } + + @Test + public void testReadMultipleOffset() throws IOException { + byte[] x = new byte[7]; + int n = is.read(x, 4, 3); + assertEquals(3, n); + assertArrayEquals( + new byte[] { 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, 0x02 }, + x); + } + + @Test + public void testReadAll() throws IOException { + byte[] x = is.readAllBytes(); + assertEquals(16, x.length); + assertArrayEquals(data, x); + } + + @Test + public void testMarkReset() throws IOException { + byte[] x = new byte[5]; + int n = is.read(x); + assertEquals(11, is.available()); + assertTrue(is.markSupported()); + is.mark(is.available()); + is.reset(); + byte[] y = new byte[5]; + int m = is.read(y); + assertEquals(n, m); + assertArrayEquals(new byte[] { 0x05, 0x06, 0x07, 0x08, 0x09 }, y); + } + + @Test + public void testClosed() { + is.close(); + Exception e = assertThrows(IOException.class, () -> is.read()); + assertEquals(JGitText.get().inputStreamClosed, e.getMessage()); + } + + @Test + public void testReadNBytes() throws IOException { + byte[] x = is.readNBytes(4); + assertArrayEquals(new byte[] { 0x00, 0x01, 0x02, 0x03 }, x); + } + + @Test + public void testReadNBytesOffset() throws IOException { + byte[] x = new byte[10]; + Arrays.fill(x, (byte) 0x0F); + is.readNBytes(x, 3, 4); + assertArrayEquals(new byte[] { 0x0F, 0x0F, 0x0F, 0x00, 0x01, 0x02, 0x03, + 0x0F, 0x0F, 0x0F }, x); + } + + @Test + public void testRead0() throws IOException { + byte[] x = new byte[7]; + int n = is.read(x, 4, 0); + assertEquals(0, n); + + is.readAllBytes(); + n = is.read(x, 4, 3); + assertEquals(-1, n); + } + + @Test + public void testSkip() throws IOException { + assertEquals(15, is.skip(15)); + assertEquals(0x0F, is.read()); + assertEquals(-1, is.read()); + } + + @Test + public void testSkip0() throws IOException { + assertEquals(0, is.skip(0)); + assertEquals(0x00, is.read()); + } +} diff --git a/org.eclipse.jgit/resources/org/eclipse/jgit/internal/JGitText.properties b/org.eclipse.jgit/resources/org/eclipse/jgit/internal/JGitText.properties index a7a7f1880..6def50b17 100644 --- a/org.eclipse.jgit/resources/org/eclipse/jgit/internal/JGitText.properties +++ b/org.eclipse.jgit/resources/org/eclipse/jgit/internal/JGitText.properties @@ -389,6 +389,7 @@ initFailedGitDirIsNoDirectory=Cannot set git-dir to ''{0}'' which is not a direc initFailedNonBareRepoSameDirs=When initializing a non-bare repo with directory {0} and separate git-dir {1} specified both folders should not point to the same location inMemoryBufferLimitExceeded=In-memory buffer limit exceeded inputDidntMatchLength=Input did not match supplied length. {0} bytes are missing. +inputStreamClosed=InputStream was closed inputStreamMustSupportMark=InputStream must support mark() integerValueNotInRange=Integer value {0}.{1} = {2} not in range {3}..{4} integerValueNotInRangeSubSection=Integer value {0}.{1}.{2} = {3} not in range {4}..{5} diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/internal/JGitText.java b/org.eclipse.jgit/src/org/eclipse/jgit/internal/JGitText.java index c3b46a315..6c2c850d7 100644 --- a/org.eclipse.jgit/src/org/eclipse/jgit/internal/JGitText.java +++ b/org.eclipse.jgit/src/org/eclipse/jgit/internal/JGitText.java @@ -419,6 +419,7 @@ public static JGitText get() { /***/ public String initFailedNonBareRepoSameDirs; /***/ public String inMemoryBufferLimitExceeded; /***/ public String inputDidntMatchLength; + /***/ public String inputStreamClosed; /***/ public String inputStreamMustSupportMark; /***/ public String integerValueNotInRange; /***/ public String integerValueNotInRangeSubSection; diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/treewalk/WorkingTreeIterator.java b/org.eclipse.jgit/src/org/eclipse/jgit/treewalk/WorkingTreeIterator.java index 98d084661..73a3ddaae 100644 --- a/org.eclipse.jgit/src/org/eclipse/jgit/treewalk/WorkingTreeIterator.java +++ b/org.eclipse.jgit/src/org/eclipse/jgit/treewalk/WorkingTreeIterator.java @@ -15,7 +15,6 @@ import static java.nio.charset.StandardCharsets.UTF_8; -import java.io.ByteArrayInputStream; import java.io.File; import java.io.FileInputStream; import java.io.FileNotFoundException; @@ -72,6 +71,7 @@ import org.eclipse.jgit.util.SystemReader; import org.eclipse.jgit.util.TemporaryBuffer; import org.eclipse.jgit.util.TemporaryBuffer.LocalFile; +import org.eclipse.jgit.util.io.ByteBufferInputStream; import org.eclipse.jgit.util.io.EolStreamTypeUtil; import org.eclipse.jgit.util.sha1.SHA1; @@ -407,9 +407,9 @@ private long possiblyFilteredLength(Entry e, long len) throws IOException { if (len <= MAXIMUM_FILE_SIZE_TO_READ_FULLY) { InputStream is = e.openInputStream(); try { - ByteBuffer rawbuf = IO.readWholeStream(is, (int) len); - rawbuf = filterClean(rawbuf.array(), rawbuf.limit()); - return rawbuf.limit(); + ByteBuffer filteredData = IO.readWholeStream(filterClean(is), + (int) len); + return filteredData.remaining(); } finally { safeClose(is); } @@ -438,10 +438,9 @@ && getEolStreamType( } if (len <= MAXIMUM_FILE_SIZE_TO_READ_FULLY) { - ByteBuffer rawbuf = IO.readWholeStream(is, (int) len); - rawbuf = filterClean(rawbuf.array(), rawbuf.limit()); - canonLen = rawbuf.limit(); - return new ByteArrayInputStream(rawbuf.array(), 0, (int) canonLen); + ByteBuffer filteredData = IO.readWholeStream(filterClean(is), (int) len); + canonLen = filteredData.remaining(); + return new ByteBufferInputStream(filteredData); } if (getCleanFilterCommand() == null && isBinary(e)) { @@ -477,16 +476,6 @@ private static boolean isBinary(Entry entry) throws IOException { } } - private ByteBuffer filterClean(byte[] src, int n) - throws IOException { - InputStream in = new ByteArrayInputStream(src); - try { - return IO.readWholeStream(filterClean(in), n); - } finally { - safeClose(in); - } - } - private InputStream filterClean(InputStream in) throws IOException { in = EolStreamTypeUtil.wrapInputStream(in, diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/util/io/ByteBufferInputStream.java b/org.eclipse.jgit/src/org/eclipse/jgit/util/io/ByteBufferInputStream.java new file mode 100644 index 000000000..9c00329ba --- /dev/null +++ b/org.eclipse.jgit/src/org/eclipse/jgit/util/io/ByteBufferInputStream.java @@ -0,0 +1,135 @@ +/* + * Copyright (C) 2023, SAP SE and others + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Distribution License v. 1.0 which is available at + * https://www.eclipse.org/org/documents/edl-v10.php. + * + * SPDX-License-Identifier: BSD-3-Clause + */ + +package org.eclipse.jgit.util.io; + +import java.io.IOException; +import java.io.InputStream; +import java.nio.ByteBuffer; +import java.nio.InvalidMarkException; +import java.util.Objects; + +import org.eclipse.jgit.annotations.NonNull; +import org.eclipse.jgit.internal.JGitText; + +/** + * An {@link InputStream} backed by a {@link ByteBuffer}. + */ +public class ByteBufferInputStream extends InputStream { + + private ByteBuffer buf; + + /** + * Creates a {@link ByteBufferInputStream} + * + * @param buf + * the ByteBuffer backing the stream + */ + public ByteBufferInputStream(@NonNull ByteBuffer buf) { + this.buf = buf; + } + + @Override + public int read() throws IOException { + nullCheck(); + if (buf.hasRemaining()) { + return buf.get() & 0xFF; + } + return -1; + } + + @Override + public int read(byte[] b) throws IOException { + nullCheck(); + return read(b, 0, b.length); + } + + @Override + public int read(byte[] b, int off, int len) throws IOException { + nullCheck(); + Objects.checkFromIndexSize(off, len, b.length); + if (len == 0) { + return 0; + } + int length = Math.min(buf.remaining(), len); + if (length == 0) { + return -1; + } + buf.get(b, off, length); + return length; + } + + @Override + public byte[] readAllBytes() throws IOException { + return readNBytes(buf.remaining()); + } + + @Override + public byte[] readNBytes(int len) throws IOException { + int l = Math.min(len, buf.remaining()); + byte[] b = new byte[l]; + read(b); + return b; + } + + @Override + public int readNBytes(byte[] b, int off, int len) throws IOException { + return read(b, off, len); + } + + @Override + public long skip(long n) throws IOException { + nullCheck(); + if (n <= 0) { + return 0; + } + // ByteBuffer index has type int + int delta = n > Integer.MAX_VALUE ? buf.remaining() + : Math.min((int) n, buf.remaining()); + buf.position(buf.position() + delta); + return delta; + } + + @Override + public int available() throws IOException { + nullCheck(); + return buf.remaining(); + } + + @Override + public void close() { + buf = null; + } + + @Override + public synchronized void mark(int readlimit) { + buf.mark(); + } + + @Override + public synchronized void reset() throws IOException { + try { + buf.reset(); + } catch (InvalidMarkException e) { + throw new IOException(e); + } + } + + @Override + public boolean markSupported() { + return true; + } + + private void nullCheck() throws IOException { + if (buf == null) { + throw new IOException(JGitText.get().inputStreamClosed); + } + } +}