Skip to content

Commit f4f3058

Browse files
authored
Merge pull request #59 from tls-attacker/51-silent-output-streams
Add SilentByteArrayOutputStream class
2 parents 04971e3 + 1edf5bf commit f4f3058

File tree

2 files changed

+390
-0
lines changed

2 files changed

+390
-0
lines changed
Lines changed: 260 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,260 @@
1+
/*
2+
* Protocol-Attacker - A Framework to create Protocol Analysis Tools
3+
*
4+
* Copyright 2023-2025 Ruhr University Bochum, Paderborn University, Technology Innovation Institute, and Hackmanit GmbH
5+
*
6+
* Licensed under Apache License, Version 2.0
7+
* http://www.apache.org/licenses/LICENSE-2.0.txt
8+
*/
9+
package de.rub.nds.protocol.util;
10+
11+
import java.io.ByteArrayOutputStream;
12+
import java.io.IOException;
13+
import java.io.OutputStream;
14+
import java.io.UnsupportedEncodingException;
15+
import java.nio.charset.Charset;
16+
import org.apache.logging.log4j.LogManager;
17+
import org.apache.logging.log4j.Logger;
18+
19+
/**
20+
* A wrapper around {@link ByteArrayOutputStream} that suppresses IOExceptions in most operations
21+
* and logs them silently.
22+
*/
23+
public class SilentByteArrayOutputStream extends OutputStream {
24+
25+
private static final Logger LOGGER = LogManager.getLogger();
26+
private final ByteArrayOutputStream outputStream;
27+
28+
/**
29+
* Creates a new {@code SilentByteArrayOutputStream}. The buffer capacity is initially 32 bytes,
30+
* though its size increases if necessary.
31+
*/
32+
public SilentByteArrayOutputStream() {
33+
this.outputStream = new ByteArrayOutputStream(32);
34+
}
35+
36+
/**
37+
* Creates a new {@code ByteArrayOutputStream}, with a buffer capacity of the specified size, in
38+
* bytes.
39+
*
40+
* @param size the initial size.
41+
* @throws IllegalArgumentException if size is negative.
42+
*/
43+
public SilentByteArrayOutputStream(int size) {
44+
this.outputStream = new ByteArrayOutputStream(size);
45+
}
46+
47+
/**
48+
* Writes the specified byte to this {@code SilentByteArrayOutputStream}.
49+
*
50+
* @param b the byte to be written.
51+
*/
52+
@Override
53+
public void write(int b) {
54+
this.outputStream.write(b);
55+
}
56+
57+
/**
58+
* Writes {@code b.length} bytes from the specified byte array to this output stream. The
59+
* general contract for {@code write(b)} is that it should have exactly the same effect as the
60+
* call {@code write(b, 0, b.length)}.
61+
*
62+
* <p>This method does not throw IOException, it logs instead.
63+
*
64+
* @param b the data.
65+
* @see java.io.OutputStream#write(byte[], int, int)
66+
*/
67+
@Override
68+
public void write(byte[] b) {
69+
try {
70+
this.outputStream.write(b);
71+
} catch (IOException ex) {
72+
LOGGER.warn("Encountered exception while writing to ByteArrayOutputStream.");
73+
LOGGER.debug(ex);
74+
}
75+
}
76+
77+
/**
78+
* Writes {@code len} bytes from the specified byte array starting at offset {@code off} to this
79+
* {@code SilentByteArrayOutputStream}.
80+
*
81+
* @param b {@inheritDoc}
82+
* @param off {@inheritDoc}
83+
* @param len {@inheritDoc}
84+
* @throws NullPointerException if {@code b} is {@code null}.
85+
* @throws IndexOutOfBoundsException if {@code off} is negative, {@code len} is negative, or
86+
* {@code len} is greater than {@code b.length - off}
87+
*/
88+
@Override
89+
public void write(byte[] b, int off, int len) {
90+
this.outputStream.write(b, off, len);
91+
}
92+
93+
/**
94+
* Writes the complete contents of the specified byte array to this {@code
95+
* SilentByteArrayOutputStream}.
96+
*
97+
* <p>This method is equivalent to {@link #write(byte[],int,int) write(b, 0, b.length)}.
98+
*
99+
* @param b the data.
100+
* @throws NullPointerException if {@code b} is {@code null}.
101+
* @since 11
102+
*/
103+
public void writeBytes(byte[] b) {
104+
write(b, 0, b.length);
105+
}
106+
107+
/**
108+
* Writes the complete contents of this {@code SilentByteArrayOutputStream} to the specified
109+
* output stream argument, as if by calling the output stream's write method using {@code
110+
* out.write(buf, 0, count)}.
111+
*
112+
* <p>This method does not throw IOException, it logs instead.
113+
*
114+
* @param out the output stream to which to write the data.
115+
* @throws NullPointerException if {@code out} is {@code null}.
116+
*/
117+
public void writeTo(OutputStream out) {
118+
try {
119+
this.outputStream.writeTo(out);
120+
} catch (IOException ex) {
121+
LOGGER.error("Encountered exception while writing to OutputStream.", ex);
122+
throw new RuntimeException(ex);
123+
}
124+
}
125+
126+
/**
127+
* Resets the {@code count} field of this {@code SilentByteArrayOutputStream} to zero, so that
128+
* all currently accumulated output in the output stream is discarded. The output stream can be
129+
* used again, reusing the already allocated buffer space.
130+
*/
131+
public void reset() {
132+
this.outputStream.reset();
133+
}
134+
135+
/**
136+
* Creates a newly allocated byte array. Its size is the current size of this output stream and
137+
* the valid contents of the buffer have been copied into it.
138+
*
139+
* @return the current contents of this output stream, as a byte array.
140+
* @see java.io.ByteArrayOutputStream#size()
141+
*/
142+
public byte[] toByteArray() {
143+
return this.outputStream.toByteArray();
144+
}
145+
146+
/**
147+
* Returns the current size of the buffer.
148+
*
149+
* @return the value of the {@code count} field, which is the number of valid bytes in this
150+
* output stream.
151+
*/
152+
public int size() {
153+
return this.outputStream.size();
154+
}
155+
156+
/**
157+
* Converts the buffer's contents into a string decoding bytes using the default charset. The
158+
* length of the new {@code String} is a function of the charset, and hence may not be equal to
159+
* the size of the buffer.
160+
*
161+
* <p>This method always replaces malformed-input and unmappable-character sequences with the
162+
* default replacement string for the default charset. The {@linkplain
163+
* java.nio.charset.CharsetDecoder} class should be used when more control over the decoding
164+
* process is required.
165+
*
166+
* @see Charset#defaultCharset()
167+
* @return String decoded from the buffer's contents.
168+
* @since 1.1
169+
*/
170+
@Override
171+
public String toString() {
172+
return this.outputStream.toString();
173+
}
174+
175+
/**
176+
* Converts the buffer's contents into a string by decoding the bytes using the named {@link
177+
* Charset charset}.
178+
*
179+
* <p>This method is equivalent to {@code #toString(charset)} that takes a {@link Charset
180+
* charset}.
181+
*
182+
* <p>An invocation of this method of the form
183+
*
184+
* {@snippet lang=java :
185+
* ByteArrayOutputStream b;
186+
* b.toString("UTF-8")
187+
* }
188+
*
189+
* behaves in exactly the same way as the expression
190+
*
191+
* {@snippet lang=java :
192+
* ByteArrayOutputStream b;
193+
* b.toString(StandardCharsets.UTF_8)
194+
* }
195+
*
196+
* @param charsetName the name of a supported {@link Charset charset}
197+
* @return String decoded from the buffer's contents.
198+
* @throws IllegalArgumentException If the named charset is not supported
199+
* @since 1.1
200+
*/
201+
public String toString(String charsetName) {
202+
try {
203+
return this.outputStream.toString(charsetName);
204+
} catch (UnsupportedEncodingException ex) {
205+
LOGGER.warn("Unsupported encoding: {}", charsetName);
206+
LOGGER.debug(ex);
207+
throw new IllegalArgumentException("Unsupported encoding: " + charsetName, ex);
208+
}
209+
}
210+
211+
/**
212+
* Converts the buffer's contents into a string by decoding the bytes using the specified {@link
213+
* Charset charset}. The length of the new {@code String} is a function of the charset, and
214+
* hence may not be equal to the length of the byte array.
215+
*
216+
* <p>This method always replaces malformed-input and unmappable-character sequences with the
217+
* charset's default replacement string. The {@link java.nio.charset.CharsetDecoder} class
218+
* should be used when more control over the decoding process is required.
219+
*
220+
* @param charset the {@linkplain Charset charset} to be used to decode the {@code bytes}
221+
* @return String decoded from the buffer's contents.
222+
* @since 10
223+
*/
224+
public String toString(Charset charset) {
225+
return this.outputStream.toString(charset);
226+
}
227+
228+
/**
229+
* Creates a newly allocated string. Its size is the current size of the output stream and the
230+
* valid contents of the buffer have been copied into it. Each character <i>c</i> in the
231+
* resulting string is constructed from the corresponding element <i>b</i> in the byte array
232+
* such that:
233+
*
234+
* {@snippet lang=java :
235+
* c == (char)(((hibyte & 0xff) << 8) | (b & 0xff))
236+
* }
237+
*
238+
* @deprecated This method does not properly convert bytes into characters. As of JDK&nbsp;1.1,
239+
* the preferred way to do this is via the {@link #toString(String charsetName)} or {@link
240+
* #toString(Charset charset)} method, which takes an encoding-name or charset argument, or
241+
* the {@code toString()} method, which uses the default charset.
242+
* @param hibyte the high byte of each resulting Unicode character.
243+
* @return the current contents of the output stream, as a string.
244+
* @see java.io.ByteArrayOutputStream#size()
245+
* @see java.io.ByteArrayOutputStream#toString(String)
246+
* @see java.io.ByteArrayOutputStream#toString()
247+
* @see Charset#defaultCharset()
248+
*/
249+
@Deprecated
250+
public String toString(int hibyte) {
251+
return this.outputStream.toString(hibyte);
252+
}
253+
254+
/**
255+
* Closing a {@code SilentByteArrayOutputStream} has no effect. The methods in this class can be
256+
* called after the stream has been closed without generating an {@code IOException}.
257+
*/
258+
@Override
259+
public void close() {}
260+
}
Lines changed: 130 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,130 @@
1+
/*
2+
* Protocol-Attacker - A Framework to create Protocol Analysis Tools
3+
*
4+
* Copyright 2023-2025 Ruhr University Bochum, Paderborn University, Technology Innovation Institute, and Hackmanit GmbH
5+
*
6+
* Licensed under Apache License, Version 2.0
7+
* http://www.apache.org/licenses/LICENSE-2.0.txt
8+
*/
9+
package de.rub.nds.protocol.util;
10+
11+
import static org.junit.jupiter.api.Assertions.*;
12+
13+
import java.io.ByteArrayOutputStream;
14+
import java.nio.charset.StandardCharsets;
15+
import org.junit.jupiter.api.BeforeEach;
16+
import org.junit.jupiter.api.Test;
17+
18+
class SilentByteArrayOutputStreamTest {
19+
20+
private SilentByteArrayOutputStream stream;
21+
22+
@BeforeEach
23+
public void setUp() {
24+
stream = new SilentByteArrayOutputStream();
25+
}
26+
27+
@Test
28+
public void testDefaultConstructor() {
29+
assertNotNull(stream);
30+
assertEquals(0, stream.size());
31+
}
32+
33+
@Test
34+
public void testSizedConstructor() {
35+
SilentByteArrayOutputStream sizedStream = new SilentByteArrayOutputStream(64);
36+
assertNotNull(sizedStream);
37+
assertEquals(0, sizedStream.size());
38+
}
39+
40+
@Test
41+
public void testWriteSingleByte() {
42+
stream.write(0x04);
43+
assertEquals(1, stream.size());
44+
assertEquals((byte) 0x04, stream.toByteArray()[0]);
45+
}
46+
47+
@Test
48+
public void testWriteByteArray() {
49+
byte[] data = {0x01, 0x02, 0x03, 0x04};
50+
stream.write(data);
51+
assertEquals(4, stream.size());
52+
assertArrayEquals(data, stream.toByteArray());
53+
}
54+
55+
@Test
56+
public void testWriteByteArrayWithOffsetAndLength() {
57+
byte[] data = {0x01, 0x02, 0x03, 0x04};
58+
stream.write(data, 1, 2);
59+
assertArrayEquals(new byte[] {0x02, 0x03}, stream.toByteArray());
60+
}
61+
62+
@Test
63+
public void testWriteBytes() {
64+
byte[] data = {0x01, 0x02, 0x03, 0x04};
65+
stream.writeBytes(data);
66+
assertArrayEquals(data, stream.toByteArray());
67+
}
68+
69+
@Test
70+
public void testWriteTo() {
71+
byte[] data = {0x01, 0x02, 0x03, 0x04};
72+
stream.write(data);
73+
ByteArrayOutputStream out = new ByteArrayOutputStream();
74+
stream.writeTo(out);
75+
assertArrayEquals(data, out.toByteArray());
76+
}
77+
78+
@Test
79+
public void testReset() {
80+
byte[] data = {0x01, 0x02, 0x03, 0x04};
81+
stream.write(data);
82+
assertEquals(4, stream.size());
83+
stream.reset();
84+
assertEquals(0, stream.size());
85+
}
86+
87+
@Test
88+
public void testToByteArray() {
89+
stream.write(new byte[] {0x01, 0x02});
90+
byte[] result = stream.toByteArray();
91+
assertArrayEquals(new byte[] {0x01, 0x02}, result);
92+
}
93+
94+
@Test
95+
public void testToStringDefaultCharset() {
96+
String testString = "Test";
97+
stream.write(testString.getBytes(StandardCharsets.UTF_8));
98+
assertEquals(testString, stream.toString());
99+
}
100+
101+
@Test
102+
public void testToStringWithCharsetName() {
103+
String testString = "Test";
104+
stream.write(testString.getBytes(StandardCharsets.UTF_8));
105+
assertEquals(testString, stream.toString("UTF-8"));
106+
}
107+
108+
@Test
109+
public void testToStringWithCharset() {
110+
String testString = "Test";
111+
stream.write(testString.getBytes(StandardCharsets.UTF_8));
112+
assertEquals(testString, stream.toString(StandardCharsets.UTF_8));
113+
}
114+
115+
@Test
116+
@SuppressWarnings("deprecation")
117+
public void testToStringWithHighByte() {
118+
stream.write(new byte[] {0x54, 0x65, 0x73, 0x74}); // Test
119+
String result = stream.toString(0);
120+
assertEquals(4, result.length());
121+
}
122+
123+
@Test
124+
public void testCloseDoesNothing() {
125+
stream.write(0x01);
126+
assertDoesNotThrow(() -> stream.close());
127+
stream.write(0x02);
128+
assertEquals(2, stream.size());
129+
}
130+
}

0 commit comments

Comments
 (0)