Skip to content

Commit cfd3ab7

Browse files
committed
Add Add org.apache.commons.io.channels.ByteArraySeekableByteChannel
Ported from Apache Commons Compress
1 parent b366503 commit cfd3ab7

File tree

4 files changed

+584
-0
lines changed

4 files changed

+584
-0
lines changed

src/changes/changes.xml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -63,6 +63,7 @@ The <action> type attribute can be add,update,fix,remove.
6363
<action dev="pkarwasz" type="add" due-to="Piotr P. Karwasz">Add length unit support in FileSystem limits.</action>
6464
<action dev="pkarwasz" type="add" due-to="Piotr P. Karwasz">Add IOUtils.toByteArray(InputStream, int, int) for safer chunked reading with size validation.</action>
6565
<action dev="pkarwasz" type="add" due-to="Piotr P. Karwasz">Add org.apache.commons.io.file.PathUtils.getPath(String, String).</action>
66+
<action dev="ggregory" type="add" due-to="Gary Gregory">Add org.apache.commons.io.channels.ByteArraySeekableByteChannel.</action>
6667
<!-- UPDATE -->
6768
<action type="update" dev="ggregory" due-to="Gary Gregory, Dependabot">Bump org.apache.commons:commons-parent from 85 to 88 #774, #783.</action>
6869
<action type="update" dev="ggregory" due-to="Gary Gregory">[test] Bump commons-codec:commons-codec from 1.18.0 to 1.19.0.</action>

src/conf/spotbugs-exclude-filter.xml

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -111,4 +111,9 @@
111111
<Class name="org.apache.commons.io.input.BOMInputStream" />
112112
<Bug pattern="AT_STALE_THREAD_WRITE_OF_PRIMITIVE" />
113113
</Match>
114+
<!-- This class is not thread-safe. Should this be a TO-DO?. -->
115+
<Match>
116+
<Class name="org.apache.commons.io.channels.ByteArraySeekableByteChannel" />
117+
<Bug pattern="AT_STALE_THREAD_WRITE_OF_PRIMITIVE, AT_NONATOMIC_OPERATIONS_ON_SHARED_VARIABLE" />
118+
</Match>
114119
</FindBugsFilter>
Lines changed: 207 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,207 @@
1+
/*
2+
* Licensed to the Apache Software Foundation (ASF) under one
3+
* or more contributor license agreements. See the NOTICE file
4+
* distributed with this work for additional information
5+
* regarding copyright ownership. The ASF licenses this file
6+
* to you under the Apache License, Version 2.0 (the
7+
* "License"); you may not use this file except in compliance
8+
* with the License. You may obtain a copy of the License at
9+
*
10+
* https://www.apache.org/licenses/LICENSE-2.0
11+
*
12+
* Unless required by applicable law or agreed to in writing,
13+
* software distributed under the License is distributed on an
14+
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
15+
* KIND, either express or implied. See the License for the
16+
* specific language governing permissions and limitations
17+
* under the License.
18+
*/
19+
20+
package org.apache.commons.io.channels;
21+
22+
import java.io.IOException;
23+
import java.nio.ByteBuffer;
24+
import java.nio.channels.ClosedChannelException;
25+
import java.nio.channels.SeekableByteChannel;
26+
import java.util.Arrays;
27+
import java.util.concurrent.atomic.AtomicBoolean;
28+
29+
import org.apache.commons.io.IOUtils;
30+
31+
/**
32+
* A {@link SeekableByteChannel} implementation backed by a byte array.
33+
* <p>
34+
* When this channel is used for writing, an internal buffer grows to accommodate incoming data. The natural size limit is the value of
35+
* {@link Integer#MAX_VALUE} and it's not possible to {@link #position(long) set the position} or {@link #truncate(long) truncate} to a value bigger than that.
36+
* The raw internal buffer is accessed via {@link ByteArraySeekableByteChannel#array()}.
37+
* </p>
38+
* <p>
39+
* This class never throws {@link ClosedChannelException} because a byte array is not a resource you open or close.
40+
* </p>
41+
* <p>
42+
* This class isn't thread-safe.
43+
* </p>
44+
*
45+
* @since 2.19.0
46+
*/
47+
public class ByteArraySeekableByteChannel implements SeekableByteChannel {
48+
49+
private static final int NAIVE_RESIZE_LIMIT = Integer.MAX_VALUE >> 1;
50+
private byte[] data;
51+
private final AtomicBoolean closed = new AtomicBoolean();
52+
private int position;
53+
private int size;
54+
55+
/**
56+
* Constructs a new instance using a default empty buffer.
57+
*/
58+
public ByteArraySeekableByteChannel() {
59+
this(IOUtils.DEFAULT_BUFFER_SIZE);
60+
}
61+
62+
/**
63+
* Constructs a new instance from a byte array.
64+
*
65+
* @param data input data or pre-allocated array.
66+
*/
67+
public ByteArraySeekableByteChannel(final byte[] data) {
68+
this.data = data;
69+
this.size = data.length;
70+
}
71+
72+
/**
73+
* Constructs a new instance from a size of storage to be allocated.
74+
*
75+
* @param size size of internal buffer to allocate, in bytes.
76+
*/
77+
public ByteArraySeekableByteChannel(final int size) {
78+
this(new byte[size]);
79+
}
80+
81+
/**
82+
* Gets the raw byte array backing this channel, <em>this is not a copy</em>.
83+
* <p>
84+
* NOTE: The returned buffer is not aligned with containing data, use {@link #size()} to obtain the size of data stored in the buffer.
85+
* </p>
86+
*
87+
* @return internal byte array.
88+
*/
89+
public byte[] array() {
90+
return data;
91+
}
92+
93+
@Override
94+
public void close() {
95+
closed.set(true);
96+
}
97+
98+
private void ensureOpen() throws ClosedChannelException {
99+
if (!isOpen()) {
100+
throw new ClosedChannelException();
101+
}
102+
}
103+
104+
/**
105+
* Like {@link #size()} but never throws {@link ClosedChannelException}.
106+
*
107+
* @return See {@link #size()}.
108+
*/
109+
public long getSize() {
110+
return size;
111+
}
112+
113+
@Override
114+
public boolean isOpen() {
115+
return !closed.get();
116+
}
117+
118+
@Override
119+
public long position() throws ClosedChannelException {
120+
ensureOpen();
121+
return position;
122+
}
123+
124+
@Override
125+
public SeekableByteChannel position(final long newPosition) throws IOException {
126+
ensureOpen();
127+
if (newPosition < 0L || newPosition > Integer.MAX_VALUE) {
128+
throw new IOException("Position must be in range [0.." + Integer.MAX_VALUE + "]");
129+
}
130+
position = (int) newPosition;
131+
return this;
132+
}
133+
134+
@Override
135+
public int read(final ByteBuffer buf) throws IOException {
136+
ensureOpen();
137+
int wanted = buf.remaining();
138+
final int possible = size - position;
139+
if (possible <= 0) {
140+
return -1;
141+
}
142+
if (wanted > possible) {
143+
wanted = possible;
144+
}
145+
buf.put(data, position, wanted);
146+
position += wanted;
147+
return wanted;
148+
}
149+
150+
private void resize(final int newLength) {
151+
int len = data.length;
152+
if (len <= 0) {
153+
len = 1;
154+
}
155+
if (newLength < NAIVE_RESIZE_LIMIT) {
156+
while (len < newLength) {
157+
len <<= 1;
158+
}
159+
} else { // avoid overflow
160+
len = newLength;
161+
}
162+
data = Arrays.copyOf(data, len);
163+
}
164+
165+
@Override
166+
public long size() throws ClosedChannelException {
167+
ensureOpen();
168+
return size;
169+
}
170+
171+
@Override
172+
public SeekableByteChannel truncate(final long newSize) throws ClosedChannelException {
173+
ensureOpen();
174+
if (newSize < 0L || newSize > Integer.MAX_VALUE) {
175+
throw new IllegalArgumentException("Size must be range [0.." + Integer.MAX_VALUE + "]");
176+
}
177+
if (size > newSize) {
178+
size = (int) newSize;
179+
}
180+
if (position > newSize) {
181+
position = (int) newSize;
182+
}
183+
return this;
184+
}
185+
186+
@Override
187+
public int write(final ByteBuffer b) throws IOException {
188+
ensureOpen();
189+
int wanted = b.remaining();
190+
final int possibleWithoutResize = size - position;
191+
if (wanted > possibleWithoutResize) {
192+
final int newSize = position + wanted;
193+
if (newSize < 0) { // overflow
194+
resize(Integer.MAX_VALUE);
195+
wanted = Integer.MAX_VALUE - position;
196+
} else {
197+
resize(newSize);
198+
}
199+
}
200+
b.get(data, position, wanted);
201+
position += wanted;
202+
if (size < position) {
203+
size = position;
204+
}
205+
return wanted;
206+
}
207+
}

0 commit comments

Comments
 (0)