From 2332623cdeb70a1a6bbf460f1400c8248dbc0326 Mon Sep 17 00:00:00 2001 From: davotoula Date: Sat, 25 Oct 2025 23:40:31 +0200 Subject: [PATCH] Create a CountingInputStream utility to avoid duplication Prevent INT overflow in BlossomUploader.kt Connection Cleanup in ImageDownloader.kt Added try-finally in Sha256Hasher.jvmAndroid.kt --- .../service/uploads/ImageDownloader.kt | 52 ++++++------------ .../uploads/blossom/BlossomUploader.kt | 39 +++++--------- .../sha256/CountingInputStream.jvmAndroid.kt | 53 +++++++++++++++++++ .../quartz/utils/sha256/Sha256.jvmAndroid.kt | 16 +++--- .../utils/sha256/Sha256Hasher.jvmAndroid.kt | 15 ++++-- 5 files changed, 103 insertions(+), 72 deletions(-) create mode 100644 quartz/src/jvmAndroid/kotlin/com/vitorpamplona/quartz/utils/sha256/CountingInputStream.jvmAndroid.kt diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/uploads/ImageDownloader.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/uploads/ImageDownloader.kt index 98426c9b5..4a1f60643 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/uploads/ImageDownloader.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/uploads/ImageDownloader.kt @@ -22,7 +22,7 @@ package com.vitorpamplona.amethyst.service.uploads import com.vitorpamplona.quartz.nip01Core.core.HexKey import com.vitorpamplona.quartz.nip01Core.core.toHexKey -import com.vitorpamplona.quartz.utils.sha256.sha256Stream +import com.vitorpamplona.quartz.utils.sha256.sha256StreamWithCount import kotlinx.coroutines.CancellationException import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.delay @@ -137,42 +137,24 @@ class ImageDownloader { responseCode = huc.responseCode } - return@withContext if (responseCode in 200..300) { - var totalBytes = 0L - - // Wrap the input stream to count bytes while hashing - val countingStream = - object : java.io.InputStream() { - val inner = huc.inputStream - - override fun read(): Int { - val byte = inner.read() - if (byte != -1) totalBytes++ - return byte + return@withContext try { + if (responseCode in 200..300) { + val (hash, totalBytes) = + huc.inputStream.use { + sha256StreamWithCount(it) } - override fun read( - b: ByteArray, - off: Int, - len: Int, - ): Int { - val bytesRead = inner.read(b, off, len) - if (bytesRead > 0) totalBytes += bytesRead - return bytesRead - } - - override fun close() = inner.close() - } - - val hash = countingStream.use { sha256Stream(it).toHexKey() } - - StreamVerification( - hash = hash, - size = totalBytes, - contentType = huc.headerFields.get("Content-Type")?.firstOrNull(), - ) - } else { - null + StreamVerification( + hash = hash.toHexKey(), + size = totalBytes, + contentType = huc.headerFields.get("Content-Type")?.firstOrNull(), + ) + } else { + null + } + } finally { + // Always disconnect to release connection resources + huc.disconnect() } } diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/uploads/blossom/BlossomUploader.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/uploads/blossom/BlossomUploader.kt index 032b1d91e..0cef56e1b 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/uploads/blossom/BlossomUploader.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/uploads/blossom/BlossomUploader.kt @@ -36,7 +36,7 @@ import com.vitorpamplona.quartz.nip01Core.core.toHexKey import com.vitorpamplona.quartz.nipB7Blossom.BlossomAuthorizationEvent import com.vitorpamplona.quartz.nipB7Blossom.BlossomUploadResult import com.vitorpamplona.quartz.utils.RandomInstance -import com.vitorpamplona.quartz.utils.sha256.sha256Stream +import com.vitorpamplona.quartz.utils.sha256.sha256StreamWithCount import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.withContext import okhttp3.MediaType.Companion.toMediaType @@ -61,30 +61,8 @@ class BlossomUploader { * to avoid loading the entire file into memory. */ private fun calculateHashAndSize(inputStream: InputStream): StreamInfo { - var totalBytes = 0L - - // Wrap the input stream to count bytes while hashing - val countingStream = - object : InputStream() { - override fun read(): Int { - val byte = inputStream.read() - if (byte != -1) totalBytes++ - return byte - } - - override fun read( - b: ByteArray, - off: Int, - len: Int, - ): Int { - val bytesRead = inputStream.read(b, off, len) - if (bytesRead > 0) totalBytes += bytesRead - return bytesRead - } - } - - val hash = sha256Stream(countingStream).toHexKey() - return StreamInfo(hash, totalBytes) + val (hash, size) = sha256StreamWithCount(inputStream) + return StreamInfo(hash.toHexKey(), size) } fun Context.getFileName(uri: Uri): String? = @@ -132,10 +110,19 @@ class BlossomUploader { checkNotNull(imageInputStream) { "Can't open the image input stream" } return imageInputStream.use { stream -> + // Validate file size to prevent overflow when converting to Int + val sizeInt = + streamInfo.size.let { + require(it <= Int.MAX_VALUE) { + "File too large: ${it / 1_048_576}MB exceeds maximum size of ${Int.MAX_VALUE / 1_048_576}MB" + } + it.toInt() + } + upload( stream, streamInfo.hash, - streamInfo.size.toInt(), + sizeInt, fileName, myContentType, alt, diff --git a/quartz/src/jvmAndroid/kotlin/com/vitorpamplona/quartz/utils/sha256/CountingInputStream.jvmAndroid.kt b/quartz/src/jvmAndroid/kotlin/com/vitorpamplona/quartz/utils/sha256/CountingInputStream.jvmAndroid.kt new file mode 100644 index 000000000..b5107061e --- /dev/null +++ b/quartz/src/jvmAndroid/kotlin/com/vitorpamplona/quartz/utils/sha256/CountingInputStream.jvmAndroid.kt @@ -0,0 +1,53 @@ +/** + * Copyright (c) 2025 Vitor Pamplona + * + * Permission is hereby granted, free of charge, to any person obtaining a copy of + * this software and associated documentation files (the "Software"), to deal in + * the Software without restriction, including without limitation the rights to use, + * copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the + * Software, and to permit persons to whom the Software is furnished to do so, + * subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS + * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR + * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN + * AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION + * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ +package com.vitorpamplona.quartz.utils.sha256 + +import java.io.FilterInputStream +import java.io.InputStream + +class CountingInputStream( + inputStream: InputStream, +) : FilterInputStream(inputStream) { + var bytesRead: Long = 0 + private set + + override fun read(): Int { + val byte = super.read() + if (byte != -1) bytesRead++ + return byte + } + + override fun read( + b: ByteArray, + off: Int, + len: Int, + ): Int { + val count = super.read(b, off, len) + if (count > 0) bytesRead += count + return count + } + + override fun skip(n: Long): Long { + val skipped = super.skip(n) + bytesRead += skipped + return skipped + } +} diff --git a/quartz/src/jvmAndroid/kotlin/com/vitorpamplona/quartz/utils/sha256/Sha256.jvmAndroid.kt b/quartz/src/jvmAndroid/kotlin/com/vitorpamplona/quartz/utils/sha256/Sha256.jvmAndroid.kt index c19a70269..17234cfa9 100644 --- a/quartz/src/jvmAndroid/kotlin/com/vitorpamplona/quartz/utils/sha256/Sha256.jvmAndroid.kt +++ b/quartz/src/jvmAndroid/kotlin/com/vitorpamplona/quartz/utils/sha256/Sha256.jvmAndroid.kt @@ -27,15 +27,19 @@ val pool = Sha256Pool(5) // max parallel operations actual fun sha256(data: ByteArray) = pool.hash(data) /** - * Calculate SHA256 hash by streaming the input in chunks. - * This avoids loading the entire input into memory at once. - * Useful for hashing large files without running out of memory. + * Calculate SHA256 hash while counting bytes read from the stream. + * Returns both the hash and the number of bytes processed. + * This is more efficient than reading the stream twice. * * @param inputStream The input stream to hash * @param bufferSize Size of chunks to read (default 8KB) - * @return SHA256 hash bytes + * @return Pair of (hash bytes, bytes read count) */ -fun sha256Stream( +fun sha256StreamWithCount( inputStream: InputStream, bufferSize: Int = 8192, -) = pool.hashStream(inputStream, bufferSize) +): Pair { + val countingStream = CountingInputStream(inputStream) + val hash = pool.hashStream(countingStream, bufferSize) + return Pair(hash, countingStream.bytesRead) +} diff --git a/quartz/src/jvmAndroid/kotlin/com/vitorpamplona/quartz/utils/sha256/Sha256Hasher.jvmAndroid.kt b/quartz/src/jvmAndroid/kotlin/com/vitorpamplona/quartz/utils/sha256/Sha256Hasher.jvmAndroid.kt index 9289c678b..04354963f 100644 --- a/quartz/src/jvmAndroid/kotlin/com/vitorpamplona/quartz/utils/sha256/Sha256Hasher.jvmAndroid.kt +++ b/quartz/src/jvmAndroid/kotlin/com/vitorpamplona/quartz/utils/sha256/Sha256Hasher.jvmAndroid.kt @@ -45,12 +45,17 @@ class Sha256Hasher { bufferSize: Int = 8192, ): ByteArray { val buffer = ByteArray(bufferSize) - var bytesRead: Int + try { + var bytesRead: Int - while (inputStream.read(buffer).also { bytesRead = it } != -1) { - digest.update(buffer, 0, bytesRead) + while (inputStream.read(buffer).also { bytesRead = it } != -1) { + digest.update(buffer, 0, bytesRead) + } + + return digest.digest() + } finally { + // Always reset digest to prevent contaminating the pool on exceptions + digest.reset() } - - return digest.digest().also { digest.reset() } } }