Skip to content

Commit cd5765a

Browse files
authored
[IO-883] ByteArraySeekableByteChannel should optionally configure a read-only channel (#834)
* [IO-856] Try test on all OSs for GitHub CI * [IO-883] ByteArraySeekableByteChannel should optionally configure a read-only channel - AbstractStreamBuilder.setOpenOptions(OpenOption...) now makes a defensive copy of its input array. - Add ByteArraySeekableByteChannel.Builder and builder(). - Add AbstractStreamBuilder.getByteArray().
1 parent 6fde092 commit cd5765a

6 files changed

Lines changed: 267 additions & 15 deletions

File tree

src/changes/changes.xml

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -63,14 +63,17 @@ The <action> type attribute can be add,update,fix,remove.
6363
<action type="fix" dev="ggregory" due-to="Martin Wiesner">Fix typos in Javadoc of FileUtils and related test classes #833.</action>
6464
<action type="fix" dev="ggregory" due-to="Daniel Vega, Gary Gregory" issue="IO-887">WriterOutputStream from a builder fails on malformed or unmappable input bytes.</action>
6565
<action type="fix" dev="ggregory" due-to="Gary Gregory">BoundedReader now extends ProxyReader.</action>
66+
<action type="fix" dev="ggregory" due-to="Gary Gregory">AbstractStreamBuilder.setOpenOptions(OpenOption...) now makes a defensive copy of its input array.</action>
6667
<action type="fix" dev="ggregory" due-to="Peter De Maeyer, Gary Gregory" issue="IO-885">Path visits follow links #832.</action>
6768
<!-- ADD -->
6869
<action type="add" dev="ggregory" due-to="Gary Gregory, Piotr P. Karwasz">Add and use IOUtils.closeQuietlySuppress(Closeable, Throwable) #818.</action>
6970
<action type="add" dev="ggregory" due-to="Gary Gregory">Add ProxyWriter.setReference(Writer).</action>
7071
<action type="add" dev="ggregory" due-to="Gary Gregory">Add ProxyWriter.unwrap().</action>
7172
<action type="add" dev="ggregory" due-to="Gary Gregory">Add ProxyReader.setReference(Reader).</action>
7273
<action type="add" dev="ggregory" due-to="Gary Gregory">Add ProxyReader.unrwap().</action>
73-
<action type="add" dev="ggregory" due-to="Gary Gregory">Make ProxyOutputStream.unrwap() public.</action>
74+
<action type="add" dev="ggregory" due-to="Konrad Windszus, Gary Gregory, Makarand Hinge" issue="IO-883">ByteArraySeekableByteChannel should optionally configure a read-only channel.</action>
75+
<action type="add" dev="ggregory" due-to="Gary Gregory" issue="IO-883">Add ByteArraySeekableByteChannel.Builder and builder().</action>
76+
<action type="add" dev="ggregory" due-to="Gary Gregory" issue="IO-883">Add AbstractStreamBuilder.getByteArray().</action>
7477
<!-- UPDATE -->
7578
<action type="update" dev="ggregory" due-to="Gary Gregory, Dependabot">Bump org.apache.commons:commons-parent from 91 to 97 #816.</action>
7679
<action type="update" dev="ggregory" due-to="Gary Gregory, Dependabot">Bump commons-codec:commons-codec from 1.19.0 to 1.21.0 #812.</action>

src/main/java/org/apache/commons/io/build/AbstractStreamBuilder.java

Lines changed: 18 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -122,6 +122,20 @@ public int getBufferSizeDefault() {
122122
return bufferSizeDefault;
123123
}
124124

125+
/**
126+
* Gets a byte array from the origin.
127+
*
128+
* @return A byte array.
129+
* @throws IllegalStateException if the {@code origin} is {@code null}.
130+
* @throws UnsupportedOperationException if the origin cannot be converted to a byte array.
131+
* @throws IOException if an I/O error occurs.
132+
* @see AbstractOrigin#getByteArray()
133+
* @since 2.22.0
134+
*/
135+
public byte[] getByteArray() throws IOException {
136+
return checkOrigin().getByteArray();
137+
}
138+
125139
/**
126140
* Gets a Channel from the origin with OpenOption[].
127141
*
@@ -202,7 +216,7 @@ public InputStream getInputStream() throws IOException {
202216
/**
203217
* Gets the OpenOption array.
204218
*
205-
* @return the OpenOption array.
219+
* @return the OpenOption array, this is not a defensive copy, modify at your own risk.
206220
*/
207221
public OpenOption[] getOpenOptions() {
208222
return openOptions;
@@ -388,23 +402,23 @@ protected B setCharsetDefault(final Charset defaultCharset) {
388402
}
389403

390404
/**
391-
* Sets the OpenOption[].
405+
* Sets the OpenOption array.
392406
* <p>
393407
* Normally used with InputStream, OutputStream, and Writer.
394408
* </p>
395409
* <p>
396410
* Subclasses may ignore this setting.
397411
* </p>
398412
*
399-
* @param openOptions the OpenOption[] name, null resets to the default.
413+
* @param openOptions the OpenOption[] name, null resets to the default, a defensive copy is made.
400414
* @return {@code this} instance.
401415
* @since 2.13.0
402416
* @see #setInputStream(InputStream)
403417
* @see #setOutputStream(OutputStream)
404418
* @see #setWriter(Writer)
405419
*/
406420
public B setOpenOptions(final OpenOption... openOptions) {
407-
this.openOptions = openOptions != null ? openOptions : DEFAULT_OPEN_OPTIONS;
421+
this.openOptions = openOptions != null ? openOptions.clone() : DEFAULT_OPEN_OPTIONS;
408422
return asThis();
409423
}
410424

src/main/java/org/apache/commons/io/channels/ByteArraySeekableByteChannel.java

Lines changed: 77 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,12 +24,16 @@
2424
import java.io.IOException;
2525
import java.nio.ByteBuffer;
2626
import java.nio.channels.ClosedChannelException;
27+
import java.nio.channels.NonWritableChannelException;
2728
import java.nio.channels.SeekableByteChannel;
29+
import java.nio.file.OpenOption;
30+
import java.nio.file.StandardOpenOption;
2831
import java.util.Arrays;
2932
import java.util.Objects;
3033
import java.util.concurrent.locks.ReentrantLock;
3134

3235
import org.apache.commons.io.IOUtils;
36+
import org.apache.commons.io.build.AbstractStreamBuilder;
3337

3438
/**
3539
* A {@link SeekableByteChannel} implementation backed by a byte array.
@@ -38,13 +42,65 @@
3842
* and it's not possible to {@link #position(long) set the position} or {@link #truncate(long) truncate} to a value bigger than that. The raw internal buffer is
3943
* accessed via {@link ByteArraySeekableByteChannel#array()}.
4044
* </p>
45+
* <p>
46+
* Building a read-only channel from an existing byte array is supported with:
47+
* </p>
48+
* <pre>{@code
49+
* try (ByteArraySeekableByteChannel channel = ByteArraySeekableByteChannel.builder()
50+
* .setByteArray(...)
51+
* .setOpenOptions(StandardOpenOption.READ)
52+
* .get()) {
53+
* // read from channel
54+
* }
55+
* }</pre>
4156
*
4257
* @since 2.21.0
4358
*/
4459
public class ByteArraySeekableByteChannel implements SeekableByteChannel {
4560

61+
/**
62+
* Builds for {@link ByteArraySeekableByteChannel}.
63+
* <p>
64+
* Building a read-only channel from an existing byte array is supported with:
65+
* </p>
66+
* <pre>{@code
67+
* try (ByteArraySeekableByteChannel channel = ByteArraySeekableByteChannel.builder()
68+
* .setByteArray(...)
69+
* .setOpenOptions(StandardOpenOption.READ)
70+
* .get()) {
71+
* // read from channel
72+
* }
73+
* }</pre>
74+
*
75+
* @since 2.22.0
76+
*/
77+
public static class Builder extends AbstractStreamBuilder<ByteArraySeekableByteChannel, Builder> {
78+
79+
/**
80+
* Constructs a new builder for {@link ByteArraySeekableByteChannel}.
81+
*/
82+
public Builder() {
83+
setByteArray(IOUtils.EMPTY_BYTE_ARRAY);
84+
}
85+
86+
@Override
87+
public ByteArraySeekableByteChannel get() throws IOException {
88+
return new ByteArraySeekableByteChannel(this);
89+
}
90+
}
91+
4692
private static final int RESIZE_LIMIT = Integer.MAX_VALUE >> 1;
4793

94+
/**
95+
* Constructs a new builder for {@link ByteArraySeekableByteChannel}.
96+
*
97+
* @return a new builder for {@link ByteArraySeekableByteChannel}.
98+
* @since 2.22.0
99+
*/
100+
public static Builder builder() {
101+
return new Builder();
102+
}
103+
48104
/**
49105
* Constructs a new channel backed directly by the given byte array.
50106
*
@@ -65,11 +121,11 @@ public static ByteArraySeekableByteChannel wrap(final byte[] bytes) {
65121
Objects.requireNonNull(bytes, "bytes");
66122
return new ByteArraySeekableByteChannel(bytes);
67123
}
68-
69124
private byte[] data;
70125
private volatile boolean closed;
71126
private long position;
72127
private int size;
128+
private final boolean isWritable;
73129
private final ReentrantLock lock = new ReentrantLock();
74130

75131
/**
@@ -84,9 +140,19 @@ public ByteArraySeekableByteChannel() {
84140
this(IOUtils.DEFAULT_BUFFER_SIZE);
85141
}
86142

143+
private ByteArraySeekableByteChannel(final Builder builder) throws IOException {
144+
this.data = builder.getByteArray();
145+
this.size = data.length;
146+
final OpenOption[] openOptions = builder.getOpenOptions();
147+
Arrays.sort(openOptions);
148+
this.isWritable = openOptions.length == 0 || Arrays.binarySearch(openOptions, StandardOpenOption.WRITE) >= 0
149+
|| Arrays.binarySearch(openOptions, StandardOpenOption.APPEND) >= 0;
150+
}
151+
87152
private ByteArraySeekableByteChannel(final byte[] data) {
88153
this.data = data;
89154
this.size = data.length;
155+
this.isWritable = true;
90156
}
91157

92158
/**
@@ -103,6 +169,7 @@ public ByteArraySeekableByteChannel(final int size) {
103169
throw new IllegalArgumentException("Size must be non-negative");
104170
}
105171
this.data = new byte[size];
172+
this.isWritable = true;
106173
}
107174

108175
/**
@@ -129,6 +196,12 @@ private void checkRange(final long newSize, final String method) {
129196
}
130197
}
131198

199+
private void checkWritable() {
200+
if (!isWritable) {
201+
throw new NonWritableChannelException();
202+
}
203+
}
204+
132205
@Override
133206
public void close() {
134207
closed = true;
@@ -237,6 +310,7 @@ public byte[] toByteArray() {
237310
@Override
238311
public SeekableByteChannel truncate(final long newSize) throws ClosedChannelException {
239312
checkOpen();
313+
checkWritable();
240314
checkRange(newSize, "truncate()");
241315
lock.lock();
242316
try {
@@ -255,6 +329,8 @@ public SeekableByteChannel truncate(final long newSize) throws ClosedChannelExce
255329
@Override
256330
public int write(final ByteBuffer b) throws IOException {
257331
checkOpen();
332+
checkWritable();
333+
//
258334
if (position > Integer.MAX_VALUE) {
259335
throw new IOException("position > Integer.MAX_VALUE");
260336
}

src/main/java/org/apache/commons/io/input/UnsynchronizedByteArrayInputStream.java

Lines changed: 1 addition & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -92,10 +92,6 @@ public Builder() {
9292
// empty
9393
}
9494

95-
private byte[] checkOriginByteArray() throws IOException {
96-
return checkOrigin().getByteArray();
97-
}
98-
9995
/**
10096
* Builds a new {@link UnsynchronizedByteArrayInputStream}.
10197
* <p>
@@ -207,7 +203,7 @@ private static int requireNonNegative(final int value, final String name) {
207203
private int markedOffset;
208204

209205
private UnsynchronizedByteArrayInputStream(final Builder builder) throws IOException {
210-
this(builder.checkOriginByteArray(), builder.offset, builder.length);
206+
this(builder.getByteArray(), builder.offset, builder.length);
211207
}
212208

213209
/**

src/test/java/org/apache/commons/io/build/AbstractStreamBuilderTest.java

Lines changed: 62 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@
1717

1818
package org.apache.commons.io.build;
1919

20+
import static org.junit.jupiter.api.Assertions.assertArrayEquals;
2021
import static org.junit.jupiter.api.Assertions.assertDoesNotThrow;
2122
import static org.junit.jupiter.api.Assertions.assertEquals;
2223
import static org.junit.jupiter.api.Assertions.assertNotNull;
@@ -31,17 +32,21 @@
3132
import java.nio.channels.ReadableByteChannel;
3233
import java.nio.channels.SeekableByteChannel;
3334
import java.nio.file.Files;
35+
import java.nio.file.OpenOption;
3436
import java.nio.file.Path;
3537
import java.nio.file.Paths;
38+
import java.nio.file.StandardOpenOption;
3639
import java.util.Arrays;
3740
import java.util.Objects;
3841
import java.util.stream.Stream;
3942

4043
import org.apache.commons.io.function.IOConsumer;
4144
import org.apache.commons.lang3.ArrayUtils;
45+
import org.apache.commons.lang3.RandomUtils;
4246
import org.junit.jupiter.api.Test;
4347
import org.junit.jupiter.params.ParameterizedTest;
4448
import org.junit.jupiter.params.provider.MethodSource;
49+
import org.junit.jupiter.params.provider.ValueSource;
4550

4651
/**
4752
* Tests {@link AbstractStreamBuilder}.
@@ -118,16 +123,71 @@ void testBufferSizeChecker() {
118123
assertResult(builder().setBufferSizeMax(2).setBufferSizeChecker(i -> 100).setBufferSize(3).get(), 100);
119124
}
120125

126+
/**
127+
* Tests various ways to obtain a byte array.
128+
*
129+
* @param configurer configures a builder.
130+
*/
131+
@ParameterizedTest
132+
@MethodSource("fileBasedConfigurers")
133+
void testGetByteArray(final IOConsumer<Builder> configurer) throws Exception {
134+
final Builder builder = builder();
135+
configurer.accept(builder);
136+
assertNotNull(builder.getByteArray());
137+
}
138+
121139
/**
122140
* Tests various ways to obtain a {@link InputStream}.
123141
*
124-
* @param configurer Lambda to configure the builder.
142+
* @param configurer configures a builder.
125143
*/
126144
@ParameterizedTest
127145
@MethodSource("fileBasedConfigurers")
128146
void testGetInputStream(final IOConsumer<Builder> configurer) throws Exception {
129147
final Builder builder = builder();
130148
configurer.accept(builder);
131-
assertNotNull(builder.getInputStream());
149+
try (InputStream inputStream = builder.getInputStream()) {
150+
assertNotNull(inputStream);
151+
}
152+
}
153+
154+
@ParameterizedTest
155+
@ValueSource(ints = { 0, 1, 2, 4 })
156+
void testSetByteArrayGetByteArray(final int size) throws Exception {
157+
final Builder builder = builder();
158+
final byte[] randomBytes = RandomUtils.insecure().randomBytes(size);
159+
builder.setByteArray(randomBytes);
160+
assertArrayEquals(randomBytes, builder.getByteArray());
161+
}
162+
163+
@Test
164+
void testSetFileGetByteArray() throws Exception {
165+
final Builder builder = builder();
166+
final Path path = Paths.get(AbstractOriginTest.FILE_NAME_RO);
167+
builder.setFile(path.toFile());
168+
assertArrayEquals(Files.readAllBytes(path), builder.getByteArray());
169+
}
170+
171+
@Test
172+
void testSetOpenOptions() {
173+
final Builder builder = builder();
174+
assertEquals(0, builder.setOpenOptions().getOpenOptions().length);
175+
assertEquals(0, builder.setOpenOptions((OpenOption[]) null).getOpenOptions().length);
176+
assertEquals(1, builder.setOpenOptions(StandardOpenOption.READ).getOpenOptions().length);
177+
final OpenOption[] options = { StandardOpenOption.READ, StandardOpenOption.WRITE };
178+
assertArrayEquals(options, builder.setOpenOptions(options).getOpenOptions());
179+
// Check that the builder makes a defensive copy of the array.
180+
options[0] = null;
181+
options[1] = null;
182+
assertEquals(StandardOpenOption.READ, builder.getOpenOptions()[0]);
183+
assertEquals(StandardOpenOption.WRITE, builder.getOpenOptions()[1]);
184+
}
185+
186+
@Test
187+
void testSetPathGetByteArray() throws Exception {
188+
final Builder builder = builder();
189+
final Path path = Paths.get(AbstractOriginTest.FILE_NAME_RO);
190+
builder.setPath(path);
191+
assertArrayEquals(Files.readAllBytes(path), builder.getByteArray());
132192
}
133193
}

0 commit comments

Comments
 (0)