Skip to content

Commit a033048

Browse files
committed
Merge branch '1911-3-DoubleBufferedInputStreamTest' into 'main'
OpenPGP API DoubleBufferedInputStreamTest and DoubleBufferedInputStream See merge request root/bc-java!103
2 parents 62c55b0 + 46db1a0 commit a033048

File tree

3 files changed

+359
-1
lines changed

3 files changed

+359
-1
lines changed
Lines changed: 194 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,194 @@
1+
package org.bouncycastle.openpgp.api;
2+
3+
import java.io.IOException;
4+
import java.io.InputStream;
5+
6+
/**
7+
* Implementation of an {@link InputStream} that double-buffers data from an underlying input stream.
8+
* Upon reaching the end of the underlying data stream, the underlying data stream is
9+
* automatically closed.
10+
* Any exceptions while reading from the underlying input stream cause the {@link DoubleBufferedInputStream}
11+
* to withhold pending data.
12+
* This is done in order to minimize the risk of emitting unauthenticated plaintext, while at the same
13+
* time being somewhat resource-efficient.
14+
* The minimum number of bytes to withhold can be configured ({@link #BUFFER_SIZE} by default).
15+
*/
16+
public class DoubleBufferedInputStream<I extends InputStream>
17+
extends InputStream
18+
{
19+
private static final int BUFFER_SIZE = 1024 * 1024 * 32; // 32 MiB
20+
private byte[] buf1;
21+
private byte[] buf2;
22+
private int b1Pos;
23+
private int b1Max;
24+
private int b2Max;
25+
private final I in;
26+
private boolean closed = false;
27+
28+
/**
29+
* Create a {@link DoubleBufferedInputStream}, which buffers twice 32MiB.
30+
*
31+
* @param in input stream
32+
*/
33+
public DoubleBufferedInputStream(I in)
34+
{
35+
this(in, BUFFER_SIZE);
36+
}
37+
38+
/**
39+
* Create a {@link DoubleBufferedInputStream}, which buffers twice the given buffer size in bytes.
40+
*
41+
* @param in input stream
42+
* @param bufferSize buffer size
43+
*/
44+
public DoubleBufferedInputStream(I in, int bufferSize)
45+
{
46+
if (bufferSize <= 0)
47+
{
48+
throw new IllegalArgumentException("Buffer size cannot be zero nor negative.");
49+
}
50+
this.buf1 = new byte[bufferSize];
51+
this.buf2 = new byte[bufferSize];
52+
this.in = in;
53+
b1Pos = -1; // indicate to fill() that we need to initialize
54+
}
55+
56+
/**
57+
* Return the underlying {@link InputStream}.
58+
*
59+
* @return underlying input stream
60+
*/
61+
public I getInputStream()
62+
{
63+
return in;
64+
}
65+
66+
/**
67+
* Buffer some data from the underlying {@link InputStream}.
68+
*
69+
* @throws IOException re-throw exceptions from the underlying input stream
70+
*/
71+
private void fill()
72+
throws IOException
73+
{
74+
// init
75+
if (b1Pos == -1)
76+
{
77+
// fill both buffers with data
78+
b1Max = in.read(buf1);
79+
b2Max = in.read(buf2);
80+
81+
if (b2Max == -1)
82+
{
83+
// data fits into b1 -> close underlying stream
84+
close();
85+
}
86+
87+
b1Pos = 0;
88+
return;
89+
}
90+
91+
// no data
92+
if (b1Max <= 0)
93+
{
94+
return;
95+
}
96+
97+
// Reached end of buf1
98+
if (b1Pos == b1Max)
99+
{
100+
// swap buffers
101+
byte[] t = buf1;
102+
buf1 = buf2;
103+
buf2 = t;
104+
b1Max = b2Max;
105+
106+
// reset reader pos
107+
b1Pos = 0;
108+
109+
// fill buf2
110+
try
111+
{
112+
b2Max = in.read(buf2);
113+
// could not fill the buffer, or swallowed an IOException
114+
if (b2Max != buf2.length)
115+
{
116+
// provoke the IOException otherwise swallowed by read(buf)
117+
int i = in.read();
118+
// no exception was thrown, so either data became available, or EOF
119+
if (i != -1)
120+
{
121+
// data became available, push to buf2
122+
buf2[b2Max++] = (byte)i;
123+
}
124+
}
125+
}
126+
catch (IOException e)
127+
{
128+
// set buffer max's to -1 to indicate to stop emitting data immediately
129+
b1Max = -1;
130+
b2Max = -1;
131+
close();
132+
133+
throw e;
134+
}
135+
136+
// EOF
137+
if (b2Max == -1)
138+
{
139+
close();
140+
}
141+
}
142+
}
143+
144+
@Override
145+
public void close()
146+
throws IOException
147+
{
148+
// close the inner stream only once
149+
if (!closed)
150+
{
151+
closed = true;
152+
in.close();
153+
}
154+
}
155+
156+
@Override
157+
public int read()
158+
throws IOException
159+
{
160+
// fill the buffer(s)
161+
fill();
162+
163+
// EOF / exception?
164+
if (b1Max == -1)
165+
{
166+
close();
167+
return -1;
168+
}
169+
170+
// return byte from the buffer
171+
return buf1[b1Pos++];
172+
}
173+
174+
@Override
175+
public int read(byte[] b, int off, int len)
176+
throws IOException
177+
{
178+
// Fill the buffer(s)
179+
fill();
180+
181+
// EOF / exception?
182+
if (b1Max == -1)
183+
{
184+
close();
185+
return -1;
186+
}
187+
188+
int ret = Math.min(b1Max - b1Pos, len);
189+
// emit data from the buffer
190+
System.arraycopy(buf1, b1Pos, b, off, ret);
191+
b1Pos += ret;
192+
return ret;
193+
}
194+
}
Lines changed: 164 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,164 @@
1+
package org.bouncycastle.openpgp.api.test;
2+
3+
import org.bouncycastle.bcpg.test.AbstractPacketTest;
4+
import org.bouncycastle.openpgp.api.DoubleBufferedInputStream;
5+
import org.bouncycastle.util.io.Streams;
6+
7+
import java.io.ByteArrayInputStream;
8+
import java.io.ByteArrayOutputStream;
9+
import java.io.FilterInputStream;
10+
import java.io.IOException;
11+
import java.io.InputStream;
12+
13+
public class DoubleBufferedInputStreamTest
14+
extends AbstractPacketTest
15+
{
16+
17+
@Override
18+
public String getName()
19+
{
20+
return "RetainingInputStreamTest";
21+
}
22+
23+
@Override
24+
public void performTest()
25+
throws Exception
26+
{
27+
throwWhileReadingNthBlock();
28+
successfullyReadSmallerThanBuffer();
29+
successfullyReadGreaterThanBuffer();
30+
31+
throwWhileReadingFirstBlock();
32+
throwWhileClosing();
33+
}
34+
35+
private void successfullyReadSmallerThanBuffer()
36+
throws IOException
37+
{
38+
byte[] bytes = getSequentialBytes(400);
39+
ByteArrayInputStream bIn = new ByteArrayInputStream(bytes);
40+
DoubleBufferedInputStream<ByteArrayInputStream> retIn = new DoubleBufferedInputStream<>(bIn, 512);
41+
ByteArrayOutputStream bOut = new ByteArrayOutputStream();
42+
Streams.pipeAll(retIn, bOut);
43+
isEncodingEqual(bytes, bOut.toByteArray());
44+
}
45+
46+
private void successfullyReadGreaterThanBuffer()
47+
throws IOException
48+
{
49+
byte[] bytes = getSequentialBytes(2000);
50+
ByteArrayInputStream bIn = new ByteArrayInputStream(bytes);
51+
DoubleBufferedInputStream<ByteArrayInputStream> retIn = new DoubleBufferedInputStream<>(bIn, 512);
52+
ByteArrayOutputStream bOut = new ByteArrayOutputStream();
53+
Streams.pipeAll(retIn, bOut);
54+
isEncodingEqual(bytes, bOut.toByteArray());
55+
}
56+
57+
private void throwWhileReadingFirstBlock()
58+
{
59+
InputStream throwAfterNBytes = new InputStream()
60+
{
61+
int throwAt = 314;
62+
int r = 0;
63+
64+
@Override
65+
public int read()
66+
throws IOException
67+
{
68+
int i = r;
69+
if (r == throwAt)
70+
{
71+
throw new IOException("Oopsie");
72+
}
73+
r++;
74+
return i;
75+
}
76+
};
77+
DoubleBufferedInputStream<InputStream> retIn = new DoubleBufferedInputStream<>(throwAfterNBytes, 512);
78+
ByteArrayOutputStream bOut = new ByteArrayOutputStream();
79+
try
80+
{
81+
Streams.pipeAll(retIn, bOut);
82+
}
83+
catch (IOException e)
84+
{
85+
isEquals("Oopsie", e.getMessage());
86+
}
87+
isEquals("throwWhileReadingFirstBlock: expected no bytes emitted", 0, bOut.toByteArray().length);
88+
}
89+
90+
private void throwWhileReadingNthBlock()
91+
{
92+
InputStream throwAfterNBytes = new InputStream()
93+
{
94+
int throwAt = 10;
95+
int r = 0;
96+
97+
@Override
98+
public int read()
99+
throws IOException
100+
{
101+
int i = r;
102+
if (r == throwAt)
103+
{
104+
throw new IOException("Oopsie");
105+
}
106+
r++;
107+
return i;
108+
}
109+
};
110+
DoubleBufferedInputStream<InputStream> retIn = new DoubleBufferedInputStream<>(throwAfterNBytes, 4);
111+
ByteArrayOutputStream bOut = new ByteArrayOutputStream();
112+
try
113+
{
114+
Streams.pipeAll(retIn, bOut);
115+
}
116+
catch (IOException e)
117+
{
118+
isEquals("Oopsie", e.getMessage());
119+
}
120+
byte[] got = bOut.toByteArray();
121+
isEquals("throwWhileReadingNthBlock: expected 4 bytes emitted. Got " + got.length, 4, got.length);
122+
}
123+
124+
private void throwWhileClosing()
125+
{
126+
byte[] bytes = getSequentialBytes(100);
127+
ByteArrayInputStream bIn = new ByteArrayInputStream(bytes);
128+
FilterInputStream throwOnClose = new FilterInputStream(bIn)
129+
{
130+
@Override
131+
public void close()
132+
throws IOException
133+
{
134+
throw new IOException("Oopsie");
135+
}
136+
};
137+
DoubleBufferedInputStream<FilterInputStream> retIn = new DoubleBufferedInputStream<>(throwOnClose, 512);
138+
ByteArrayOutputStream bOut = new ByteArrayOutputStream();
139+
try
140+
{
141+
Streams.pipeAll(retIn, bOut);
142+
}
143+
catch (IOException e)
144+
{
145+
isEquals("Oopsie", e.getMessage());
146+
}
147+
isEquals("throwWhileClosing: len mismatch", 0, bOut.toByteArray().length);
148+
}
149+
150+
private byte[] getSequentialBytes(int n)
151+
{
152+
byte[] bytes = new byte[n];
153+
for (int i = 0; i < bytes.length; i++)
154+
{
155+
bytes[i] = (byte)(i % 128);
156+
}
157+
return bytes;
158+
}
159+
160+
public static void main(String[] args)
161+
{
162+
runTest(new DoubleBufferedInputStreamTest());
163+
}
164+
}

pg/src/test/java/org/bouncycastle/openpgp/api/test/RegressionTest.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ public class RegressionTest
99
{
1010
public static Test[] tests = {
1111
new ChangeKeyPassphraseTest(),
12-
// new DoubleBufferedInputStreamTest(),
12+
new DoubleBufferedInputStreamTest(),
1313
new OpenPGPCertificateTest(),
1414
new OpenPGPDetachedSignatureProcessorTest(),
1515
new OpenPGPKeyEditorTest(),

0 commit comments

Comments
 (0)