Skip to content

save and load close the stream they are given #109

Description

@jorabin

Database.save(...) closes the OutputStream passed to it, and load(...) closes the InputStream. A method shouldn't close a stream it didn't open. The caller opened it and should close it, usually with try-with-resources. As things stand:

  • Writing XML to System.out closes standard output, so anything printed afterwards is silently lost. Callers have to wrap it, e.g. new PrintStream(System.out) { public void close() {} }.
  • A caller can't write anything after the database, such as a checksum or a second document, or keep using a socket or a zip entry.
  • With try-with-resources, the stream gets closed twice. That's usually harmless, but it's misleading.

Where streams are closed

  • XML save (StreamFormat.None): KdbxSerializableDatabase.save closes its OutputStreamWriter, which closes the caller's stream. Then StreamFormat.None.save calls flush() on the closed stream and closes it again.
  • XML load: KdbxSerializableDatabase.load calls mapper.readValue(inputStream, ...), and Jackson closes the source by default (AUTO_CLOSE_SOURCE). Then StreamFormat.None.load closes it again.
  • KDBX save and load: KdbxStreamFormat closes the encrypting or decrypting chain, and closing the chain closes the caller's stream underneath it.
  • KDB load: KdbSerializer.createKdbDatabase closes its DigestInputStream, which closes the decrypting stream and then the caller's stream.
  • BasicDatabaseSerializer.Xml: Jackson's writeValue and readValue close the stream by default (AUTO_CLOSE_TARGET, AUTO_CLOSE_SOURCE).

Proposal (3.1.0)

Existing code may rely on the current behaviour, for example code that passes new FileOutputStream(...) without closing it. So save and load keep their behaviour but are deprecated, and new write and read methods do the work:

  • write(...), with the same signatures as save(...): writes, flushes and leaves the stream open.
  • read(...), with the same signatures as load(...): reads and leaves the stream open.
  • save is write followed by close(), and load is read followed by close(), both @Deprecated, pointing to the new methods.

How the internals stop closing the caller's stream:

  • XML: flush instead of close. After writeEndDocument(), flush the XMLStreamWriter and the OutputStreamWriter and don't close them; an OutputStreamWriter holds only a buffer, not system resources, so that's safe. Turn off Jackson's AUTO_CLOSE_SOURCE and AUTO_CLOSE_TARGET, and drop the close() calls in StreamFormat.None.
  • KDBX and KDB: flushing isn't enough. These layers only finish the file when they are closed: GZIPOutputStream writes its trailer, CipherOutputStream writes the final encrypted block, and HmacBlockOutputStream and HashedBlockOutputStream write the final empty block. Each also closes what's underneath it, and two of them are JDK classes. So the chain is still closed as now, but the caller's stream sits under a small shield at the bottom of it. The shield's close() only flushes (on output) or does nothing (on input), so the formats finish properly and the caller's stream stays open.

read leaves the stream open, but buffering layers such as the decryptor and gzip may read past the end of the database, so its position afterwards isn't defined. The javadoc should say so.

The Nx methods

saveNx and loadNx were added in March 2025 (b41301d) for the shared test code. DatabaseTestBase takes the database's load and save as functional interfaces, which can't throw checked exceptions, so the tests use KdbxDatabase::loadNx, KdbDatabase::loadNx and Database::saveNx. They're public API all the same: saveNx is on the Database interface and in AbstractDatabase, loadNx is in KdbxDatabase and KdbDatabase, and BasicDatabaseSerializer has both.

Since they exist only for the tests, deprecate all of them (no writeNx or readNx), and have the tests wrap the checked exception themselves, e.g. with a small helper in the test module.

Also

Update the examples, the readme and the tests to open streams with try-with-resources and call write and read.

2.x won't change.

Activity

  1. jorabin commented on Oct 3, 2026

    @jorabin
    OwnerAuthor

    XML (StreamFormat.None) closes the stream at two levels in each direction:

    • Save: KdbxSerializableDatabase.save closes its OutputStreamWriter, which closes the caller's stream. Then StreamFormat.None.save calls flush() on the closed stream and closes it again.
    • Load: KdbxSerializableDatabase.load calls mapper.readValue(inputStream, ...), and Jackson closes the source by default (AUTO_CLOSE_SOURCE). Then StreamFormat.None.load closes it again.

    Also, KdbxDatabase.loadNx wraps load. It needs a readNx counterpart, or should be deprecated along with load.

  2. jorabin commented on Oct 3, 2026

    @jorabin
    OwnerAuthor

    When the Nx methods are deprecated, the javadoc (and the readme) should say what to do instead. A caller who needs an unchecked version, e.g. for a lambda or method reference, can write a small local wrapper that rethrows as UncheckedIOException:

    static Database read(Credentials credentials, InputStream inputStream) {
        try {
            return KdbxDatabase.read(credentials, inputStream);
        } catch (IOException e) {
            throw new UncheckedIOException(e);
        }
    }

    and then use it as stream.map(in -> read(credentials, in)). The tests can do the same, with one such helper in the test module.

  3. jorabin commented on Oct 5, 2026

    @jorabin
    OwnerAuthor

    Released in 3.1.0: read and write leave the stream open, and save/load are deprecated.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions