Skip to content

Commit 02cd55b

Browse files
committed
Throw checked IOException in input/output stream methods
1 parent 60a47af commit 02cd55b

4 files changed

Lines changed: 137 additions & 45 deletions

File tree

‎java-does-usb/src/main/java/net/codecrete/usb/common/EndpointInputStream.java‎

Lines changed: 46 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -15,9 +15,12 @@
1515
import java.io.InputStream;
1616
import java.lang.foreign.Arena;
1717
import java.lang.foreign.MemorySegment;
18+
import java.util.Objects;
1819
import java.util.concurrent.ArrayBlockingQueue;
1920
import java.util.concurrent.TimeUnit;
2021

22+
import static net.codecrete.usb.common.EndpointStreams.toIOException;
23+
2124
import static java.lang.System.Logger.Level.WARNING;
2225
import static java.lang.foreign.ValueLayout.JAVA_BYTE;
2326

@@ -134,44 +137,66 @@ public void close() throws IOException {
134137

135138
@Override
136139
public int read() throws IOException {
137-
if (isClosed())
138-
return -1;
140+
ensureOpen();
141+
142+
try {
143+
if (bufferedBytes() == 0)
144+
receiveMoreData();
139145

140-
if (available() == 0)
141-
receiveMoreData();
146+
var b = currentTransfer.data().get(JAVA_BYTE, readOffset) & 0xff;
147+
readOffset += 1;
148+
return b;
142149

143-
var b = currentTransfer.data().get(JAVA_BYTE, readOffset) & 0xff;
144-
readOffset += 1;
145-
return b;
150+
} catch (UsbException e) {
151+
throw toIOException(e);
152+
}
146153
}
147154

148155
@Override
149156
public int read(byte @NotNull [] b, int off, int len) throws IOException {
150-
if (isClosed())
151-
return -1;
157+
Objects.checkFromIndexSize(off, len, b.length);
158+
ensureOpen();
159+
if (len == 0)
160+
return 0;
152161

153-
var numRead = 0;
154-
do {
155-
if (available() == 0)
156-
receiveMoreData();
162+
try {
163+
var numRead = 0;
164+
do {
165+
if (bufferedBytes() == 0)
166+
receiveMoreData();
167+
168+
// copy data to receiving buffer
169+
var n = Math.min(len - numRead, currentTransfer.resultSize() - readOffset);
170+
MemorySegment.copy(currentTransfer.data(), readOffset, MemorySegment.ofArray(b), (long) off + numRead, n);
171+
readOffset += n;
172+
numRead += n;
157173

158-
// copy data to receiving buffer
159-
var n = Math.min(len - numRead, currentTransfer.resultSize() - readOffset);
160-
MemorySegment.copy(currentTransfer.data(), readOffset, MemorySegment.ofArray(b), (long) off + numRead, n);
161-
readOffset += n;
162-
numRead += n;
174+
} while (numRead < len && hasMoreTransfers());
163175

164-
} while (numRead < len && hasMoreTransfers());
176+
return numRead;
165177

166-
return numRead;
178+
} catch (UsbException e) {
179+
throw toIOException(e);
180+
}
167181
}
168182

169-
@SuppressWarnings("RedundantThrows")
170183
@Override
171184
public int available() throws IOException {
185+
ensureOpen();
186+
return bufferedBytes();
187+
}
188+
189+
// Bytes buffered in the current transfer, without a closed-stream check.
190+
// Callers on the read path guard with ensureOpen() first.
191+
private int bufferedBytes() {
172192
return currentTransfer.resultSize() - readOffset;
173193
}
174194

195+
private void ensureOpen() throws IOException {
196+
if (isClosed())
197+
throw new IOException("input stream has been closed");
198+
}
199+
175200
private boolean hasMoreTransfers() {
176201
return !completedTransferQueue.isEmpty();
177202
}

‎java-does-usb/src/main/java/net/codecrete/usb/common/EndpointOutputStream.java‎

Lines changed: 42 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -8,16 +8,20 @@
88
package net.codecrete.usb.common;
99

1010
import net.codecrete.usb.UsbDirection;
11+
import net.codecrete.usb.UsbException;
1112
import org.jetbrains.annotations.NotNull;
1213

1314
import java.io.IOException;
1415
import java.io.OutputStream;
1516
import java.lang.foreign.Arena;
1617
import java.lang.foreign.MemorySegment;
1718
import java.util.Arrays;
19+
import java.util.Objects;
1820
import java.util.concurrent.ArrayBlockingQueue;
1921
import java.util.concurrent.TimeUnit;
2022

23+
import static net.codecrete.usb.common.EndpointStreams.toIOException;
24+
2125
import static java.lang.System.Logger.Level.WARNING;
2226
import static java.lang.foreign.ValueLayout.JAVA_BYTE;
2327

@@ -239,41 +243,57 @@ private Transfer pollAvailableTransfer(long deadline) {
239243

240244
@Override
241245
public void write(int b) throws IOException {
242-
checkIsOpen();
246+
ensureOpen();
243247

244-
currentTransfer.data().set(JAVA_BYTE, writeOffset, (byte) b);
245-
writeOffset += 1;
246-
if (writeOffset == transferSize)
247-
submitTransfer(writeOffset);
248+
try {
249+
currentTransfer.data().set(JAVA_BYTE, writeOffset, (byte) b);
250+
writeOffset += 1;
251+
if (writeOffset == transferSize)
252+
submitTransfer(writeOffset);
253+
254+
} catch (UsbException e) {
255+
throw toIOException(e);
256+
}
248257
}
249258

250259
@Override
251260
public void write(byte @NotNull [] b, int off, int len) throws IOException {
252-
checkIsOpen();
261+
Objects.checkFromIndexSize(off, len, b.length);
262+
ensureOpen();
253263

254-
while (len > 0) {
255-
var chunkSize = Math.min(len, transferSize - writeOffset);
256-
MemorySegment.copy(b, off, currentTransfer.data(), JAVA_BYTE, writeOffset, chunkSize);
257-
writeOffset += chunkSize;
258-
off += chunkSize;
259-
len -= chunkSize;
264+
try {
265+
while (len > 0) {
266+
var chunkSize = Math.min(len, transferSize - writeOffset);
267+
MemorySegment.copy(b, off, currentTransfer.data(), JAVA_BYTE, writeOffset, chunkSize);
268+
writeOffset += chunkSize;
269+
off += chunkSize;
270+
len -= chunkSize;
271+
272+
if (writeOffset == transferSize)
273+
submitTransfer(writeOffset);
274+
}
260275

261-
if (writeOffset == transferSize)
262-
submitTransfer(writeOffset);
276+
} catch (UsbException e) {
277+
throw toIOException(e);
263278
}
264279
}
265280

266281
@Override
267282
public void flush() throws IOException {
268-
checkIsOpen();
283+
ensureOpen();
269284

270-
if (writeOffset > 0)
271-
submitTransfer(writeOffset);
285+
try {
286+
if (writeOffset > 0)
287+
submitTransfer(writeOffset);
288+
289+
if (needsZlp)
290+
submitTransfer(0);
272291

273-
if (needsZlp)
274-
submitTransfer(0);
292+
waitForOutstandingTransfers();
275293

276-
waitForOutstandingTransfers();
294+
} catch (UsbException e) {
295+
throw toIOException(e);
296+
}
277297
}
278298

279299
/**
@@ -387,8 +407,8 @@ private synchronized void onCompletion(Transfer transfer) {
387407
protected void configureEndpoint() {
388408
}
389409

390-
private void checkIsOpen() throws IOException {
410+
private void ensureOpen() throws IOException {
391411
if (isClosed())
392-
throw new IOException("endpoint output stream has been closed");
412+
throw new IOException("output stream has been closed");
393413
}
394414
}
Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
//
2+
// Java Does USB
3+
// Copyright (c) 2023 Manuel Bleichenbacher
4+
// Licensed under MIT License
5+
// https://opensource.org/licenses/MIT
6+
//
7+
8+
package net.codecrete.usb.common;
9+
10+
import net.codecrete.usb.UsbException;
11+
import net.codecrete.usb.UsbTimeoutException;
12+
13+
import java.io.IOException;
14+
import java.io.InterruptedIOException;
15+
16+
/**
17+
* Helpers shared by {@link EndpointInputStream} and {@link EndpointOutputStream}.
18+
*/
19+
final class EndpointStreams {
20+
21+
private EndpointStreams() {
22+
}
23+
24+
/**
25+
* Wraps a USB error in an {@link IOException} so it is surfaced through the
26+
* {@link java.io.InputStream}/{@link java.io.OutputStream} contract.
27+
* <p>
28+
* A transfer timeout is mapped to {@link InterruptedIOException} (java.io's "I/O timed out").
29+
* All other USB errors become a plain {@link IOException} with the {@link UsbException} as cause,
30+
* which preserves the USB error code and any stall/timeout subtype for callers that inspect it.
31+
* </p>
32+
*
33+
* @param e the USB error
34+
* @return the corresponding I/O exception
35+
*/
36+
static IOException toIOException(UsbException e) {
37+
if (e instanceof UsbTimeoutException) {
38+
var ioException = new InterruptedIOException(e.getMessage());
39+
ioException.initCause(e);
40+
return ioException;
41+
}
42+
return new IOException(e.getMessage(), e);
43+
}
44+
}

‎java-does-usb/src/test/java/net/codecrete/usb/StreamTest.java‎

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -86,7 +86,7 @@ void largeTransferBigChunks_succeeds() {
8686
void blockedWriter_canBeAborted() throws InterruptedException {
8787
// A writer that fills the pipe faster than it is drained eventually blocks in write().
8888
// Aborting the outstanding transfers from another thread must terminate it promptly and
89-
// safely (with a USB exception) rather than leave it wedged forever.
89+
// safely (with an IOException wrapping the USB error) rather than leave it wedged forever.
9090

9191
final var data = generateRandomBytes(1_000_000, 0x5c7f10ebL);
9292

@@ -152,7 +152,10 @@ void blockedWriter_canBeAborted() throws InterruptedException {
152152
assertFalse(writer.isAlive(), "writer thread did not terminate within 1.5 s after abort");
153153

154154
// It must have unwound because of the abort, not by finishing or dying some other way.
155-
assertInstanceOf(UsbException.class, writerError.get());
155+
// The stream surfaces the USB error as an IOException (with the UsbException as cause).
156+
var err = writerError.get();
157+
assertInstanceOf(IOException.class, err);
158+
assertInstanceOf(UsbException.class, err.getCause());
156159

157160
} finally {
158161
// Restore a clean device state so subsequent tests are not affected.

0 commit comments

Comments
 (0)