final private static function streamDecrypt(
        ReadOnlyFile $input,
        MutableFile $output,
        EncryptionKey $encKey,
        string $nonce,
        string $mac,
        Config $config,
        array &$chunk_macs
    ): bool {
        $start = $input->getPos();
        /** @var int $cipher_end */
        $cipher_end = $input->getSize() - (int) $config->MAC_SIZE;
        // Begin the streaming decryption
        $input->reset($start);

        while ($input->remainingBytes() > (int) $config->MAC_SIZE) {
            /**
             * Would a full BUFFER read put it past the end of the
             * ciphertext? If so, only return a portion of the file.
             */
            if (($input->getPos() + (int) $config->BUFFER) > $cipher_end) {
                $read = $input->readBytes(
                    $cipher_end - $input->getPos()
                );
            } else {
                // @codeCoverageIgnoreStart
                $read = $input->readBytes((int) $config->BUFFER);
                // @codeCoverageIgnoreEnd
            }

            // Version 2+ uses a keyed BLAKE2b hash instead of HMAC
            \sodium_crypto_generichash_update($mac, $read);
            /** @var string $mac */
            $calcMAC = Util::safeStrcpy($mac);
            $calc = \sodium_crypto_generichash_final($calcMAC, (int) $config->MAC_SIZE);

            if (empty($chunk_macs)) {
                // @codeCoverageIgnoreStart
                // Someone attempted to add a chunk at the end.
                throw new InvalidMessage(
                    'Invalid message authentication code'
                );
                // @codeCoverageIgnoreEnd
            } else {
                /** @var string $chunkMAC */
                $chunkMAC = \array_shift($chunk_macs);
                if (!\hash_equals($chunkMAC, $calc)) {
                    // This chunk was altered after the original MAC was verified
                    // @codeCoverageIgnoreStart
                    throw new InvalidMessage(
                        'Invalid message authentication code'
                    );
                    // @codeCoverageIgnoreEnd
                }
            }

            // This is where the decryption actually occurs:
            $decrypted = \sodium_crypto_stream_xor(
                $read,
                (string) $nonce,
                $encKey->getRawKeyMaterial()
            );
            $output->writeBytes($decrypted);
            \sodium_increment($nonce);
        }
        if (\is_string($nonce)) {
            \sodium_memzero($nonce);
        }
        return true;
    }