Utilities for I/O

GitLab Docs Pipeline Coverage License
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 an IOException, 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) and COOKED (line-buffered with echo, automatic CR on LF).

  • TTYSize -- a simple record of the terminal's row and column count.

  • TTYModeSwitcher -- a Closeable that 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);