Utilities for I/O
A Java module providing utilities for binary I/O, bit-level stream packing,
sub-stream framing and termination, terminal mode control, and subprocess
execution. See morimekta.net/utils for
procedures on releases.
Getting Started
To add to Maven, include the following dependency in pom.xml:
<dependency>
<groupId>net.morimekta.utils</groupId>
<artifactId>io</artifactId>
<version>4.6.1</version>
</dependency>
To add to Gradle, include the following in the dependencies block of
build.gradle:
implementation 'net.morimekta.utils:io:4.6.1'
Binary I/O Streams
The BinaryInputStream and BinaryOutputStream base classes provide
IO-optimized reading and writing of primitive values with explicit control
over byte order. Unlike the standard ObjectInputStream and
ObjectOutputStream, these classes never read more from the underlying stream
than is actually consumed, and they offer two flavors of each read method: one
that returns a default value at end-of-stream, and one that throws on
insufficient data.
Two concrete implementations are provided:
BigEndianBinaryInputStream/BigEndianBinaryOutputStream-- read and write integers in big-endian (network) byte order.LittleEndianBinaryInputStream/LittleEndianBinaryOutputStream-- read and write integers in little-endian byte order.
Both variants support fixed-width integers (16-bit through 64-bit), unsigned integers of arbitrary widths, and variable-length encodings including base-128 and zigzag.
var out = new BigEndianBinaryOutputStream(outputStream);
out.writeInt(42);
out.writeZigzag(-17);
out.writeBase128(123456);
var in = new BigEndianBinaryInputStream(inputStream);
int value = in.expectInt();
int zigzag = in.expectIntZigzag();
int base128 = in.expectIntBase128();
Byte Buffer Streams
ByteBufferInputStream and ByteBufferOutputStream adapt a ByteBuffer to
the standard InputStream and OutputStream interfaces. The output stream
throws an IOException if the buffer's capacity is exceeded.
Bit and Septet Packing Streams
-
BitPackingInputStream/BitPackingOutputStream-- read and write individual bits in groups of up to 31 bits at a time. Bits are assigned from most to least significant within each byte, and any partial byte is flushed when the stream is closed. -
SeptetPackingInputStream/SeptetPackingOutputStream-- read and write 7-bit values (septets) packed consecutively into the underlying byte stream. This is useful for protocols that reserve the high bit of each byte for framing or signaling purposes.
Stream Decorators
-
ForkingOutputStream-- writes the same data to multiple output streams. Each call is forwarded to every stream; if any stream throws anIOException, the remaining streams are still written to before the exception is propagated. -
NoCloseOutputStream-- wraps an output stream and prevents it from being closed. Closing the wrapper only prevents further writes without affecting the underlying stream. This is useful when passing a stream to a component that closes its output on completion, but the caller needs the underlying stream to remain open.
Sub-Streams
The sub package contains stream pairs that represent a portion of a larger
stream without consuming or closing it entirely.
Framed Streams
FramedInputStream and FramedOutputStream delimit a section of the stream
by a leading size field. The default encoding for the size is BEB128
(big-endian base-128), though a custom reader or writer can be supplied.
When writing, all content is buffered in memory until the stream is closed, at which point the frame size is written followed by the buffered data. Closing a framed stream does not close the underlying stream.
(size) (data)
Terminated Streams
TerminatedInputStream and TerminatedOutputStream delimit a section of the
stream using a sentinel byte instead of a leading size. The output stream
writes the terminating byte when it is closed, and the input stream reports
end-of-stream when the terminating byte is encountered. Closing a terminated
input stream discards any remaining bytes up to and including the terminator,
leaving the underlying stream positioned for the next read. Closing a
terminated output stream does not close the underlying stream.
Note that writing the terminating byte value as part of the regular content will cause the corresponding input stream to terminate prematurely.
Terminal Handling
The tty package provides control over terminal I/O modes and size detection
via stty.
-
TTY-- the main controller. It can switch the terminal between raw and cooked mode, detect the current terminal size, and check whether the session is interactive. Terminal size is cached briefly to avoid excessive subprocess calls. -
TTYMode-- an enum with two values:RAW(no echo, no line buffering) andCOOKED(line-buffered with echo, automatic CR on LF). -
TTYSize-- a simple record of the terminal's row and column count. -
TTYModeSwitcher-- aCloseablethat switches to a given mode and restores the previous mode when closed. Designed for use in try-with-resources blocks:
try (var ignored = new TTYModeSwitcher(tty, TTYMode.RAW)) {
// read raw key input
}
// previous mode is restored
Subprocess Execution
The proc package provides utilities for running external processes with
controlled I/O piping.
SubProcessRunner is the low-level helper that wires up input, output,
and error streams to a native Process. Streams are forwarded on dedicated
threads and properly closed when the process exits or reaches a configurable
deadline.
SubProcessRunner runner = new SubProcessRunner();
runner.setOut(System.out);
runner.setErr(System.err);
runner.exec(System.in, "sh", "-c", "cat file | less");
SubProcess offers a higher-level fluent API that captures standard
output and standard error as UTF-8 strings. It is built on top of
SubProcessRunner.
SubProcess result = SubProcess.newRunner("ls", "-1")
.withWorkingDir(Paths.get("/tmp"))
.run();
Arrays.stream(result.getOutput().split("\n"))
.filter(s -> !s.isEmpty())
.sorted()
.forEach(System.out::println);