AeroToss v0.5.0: финальный аудит, багфиксы, тесты, релиз

- Исправлен cleanup файлов (catch-блок удаляет реальный файл)
- Null safety для NsdManager (as? с проверками)
- JSON парсер: isLenient = false (отвергает malformed JSON)
- ByteArray.toInt(): добавлена require(size >= 4)
- Исправлен typo в SendScreen
- TransferEdgeCaseTest: 8 новых тестов edge cases
- Собран macOS arm64 JAR (28 МБ)
- Итого 73+ тестов, все проходят
This commit is contained in:
2026-08-19 17:50:39 +03:00
parent afc526d6e8
commit 5434ab314c
6 changed files with 191 additions and 12 deletions
@@ -17,7 +17,7 @@ class AndroidNsdDiscovery(context: Context) : DiscoveryManager {
private val _devices = MutableStateFlow<List<Device>>(emptyList())
override val devices: Flow<List<Device>> = _devices.asStateFlow()
private val nsdManager = context.getSystemService(Context.NSD_SERVICE) as NsdManager
private val nsdManager = context.getSystemService(Context.NSD_SERVICE) as? NsdManager
private var registrationListener: NsdManager.RegistrationListener? = null
private var discoveryListener: NsdManager.DiscoveryListener? = null
private val deviceId = generateDeviceId()
@@ -54,7 +54,7 @@ class AndroidNsdDiscovery(context: Context) : DiscoveryManager {
}
}
nsdManager.registerService(serviceInfo, NsdManager.PROTOCOL_DNS_SD, registrationListener)
nsdManager?.registerService(serviceInfo, NsdManager.PROTOCOL_DNS_SD, registrationListener)
}
private fun startServiceDiscovery() {
@@ -68,7 +68,7 @@ class AndroidNsdDiscovery(context: Context) : DiscoveryManager {
serviceInfo.serviceName != SERVICE_NAME &&
resolvingServices.add(serviceInfo.serviceName)
) {
nsdManager.resolveService(serviceInfo, object : NsdManager.ResolveListener {
nsdManager?.resolveService(serviceInfo, object : NsdManager.ResolveListener {
override fun onResolveFailed(info: NsdServiceInfo, errorCode: Int) {
resolvingServices.remove(info.serviceName)
Log.e(TAG, "Resolve failed: $errorCode")
@@ -110,16 +110,16 @@ class AndroidNsdDiscovery(context: Context) : DiscoveryManager {
}
}
nsdManager.discoverServices(SERVICE_TYPE, NsdManager.PROTOCOL_DNS_SD, discoveryListener)
nsdManager?.discoverServices(SERVICE_TYPE, NsdManager.PROTOCOL_DNS_SD, discoveryListener)
}
override fun stopDiscovery() {
try {
discoveryListener?.let { nsdManager.stopServiceDiscovery(it) }
discoveryListener?.let { nsdManager?.stopServiceDiscovery(it) }
} catch (_: Exception) {
}
try {
registrationListener?.let { nsdManager.unregisterService(it) }
registrationListener?.let { nsdManager?.unregisterService(it) }
} catch (_: Exception) {
}
discoveryListener = null
@@ -78,6 +78,8 @@ class AndroidFileTransferManager(private val context: Context) : TransferManager
private suspend fun handleIncomingConnection(socket: Socket) {
withContext(Dispatchers.IO) {
val progressId = UUID.randomUUID().toString()
var actualFile: File? = null
var requestFileName: String = ""
try {
socket.use { sock ->
sock.soTimeout = 30_000
@@ -86,6 +88,7 @@ class AndroidFileTransferManager(private val context: Context) : TransferManager
val requestJson = input.readUTF()
val request = Json.decodeFromString<TransferRequest>(requestJson)
requestFileName = request.fileName
val progress = TransferProgress(
id = progressId,
@@ -105,6 +108,7 @@ class AndroidFileTransferManager(private val context: Context) : TransferManager
}
val file = FileUtils.resolveUniqueFile(downloadsDir, request.fileName)
actualFile = file
val sha256 = MessageDigest.getInstance("SHA-256")
var bytesWritten = 0L
@@ -157,11 +161,11 @@ class AndroidFileTransferManager(private val context: Context) : TransferManager
}
} catch (e: Exception) {
e.printStackTrace()
actualFile?.let { FileUtils.deleteIfExists(it) }
val current = _incomingTransfers.value.find { it.id == progressId }
if (current != null && current.state != TransferState.COMPLETED &&
current.state != TransferState.FAILED && current.state != TransferState.CANCELLED
) {
FileUtils.deleteIfExists(File(downloadsDir, FileUtils.sanitizeFileName(current.request.fileName)))
updateIncomingById(progressId, current.copy(
state = TransferState.FAILED,
error = e.message ?: "Unknown error"
@@ -9,7 +9,7 @@ object WireProtocol {
private val json = Json {
encodeDefaults = true
ignoreUnknownKeys = true
isLenient = true
isLenient = false
}
fun encode(command: Command): ByteArray {
@@ -51,8 +51,10 @@ fun Int.toByteArray(): ByteArray = byteArrayOf(
(this and 0xFF).toByte()
)
fun ByteArray.toInt(): Int =
((this[0].toInt() and 0xFF) shl 24) or
fun ByteArray.toInt(): Int {
require(size >= 4) { "ByteArray must have at least 4 elements" }
return ((this[0].toInt() and 0xFF) shl 24) or
((this[1].toInt() and 0xFF) shl 16) or
((this[2].toInt() and 0xFF) shl 8) or
(this[3].toInt() and 0xFF)
}
@@ -80,7 +80,7 @@ fun SendScreen(
style = MaterialTheme.typography.bodyLarge
)
Text(
"Разазмер: ${formatSize(selectedFile?.length() ?: 0)}",
"Размер: ${formatSize(selectedFile?.length() ?: 0)}",
style = MaterialTheme.typography.bodySmall,
color = MaterialTheme.colorScheme.onSurfaceVariant
)
@@ -75,6 +75,8 @@ class DesktopFileTransferManager : TransferManager {
private suspend fun handleIncomingConnection(socket: Socket) {
withContext(Dispatchers.IO) {
val progressId = UUID.randomUUID().toString()
var actualFile: File? = null
var requestFileName: String = ""
try {
socket.use { sock ->
sock.soTimeout = 30_000
@@ -83,6 +85,7 @@ class DesktopFileTransferManager : TransferManager {
val requestJson = input.readUTF()
val request = Json.decodeFromString<TransferRequest>(requestJson)
requestFileName = request.fileName
val progress = TransferProgress(
id = progressId,
@@ -102,6 +105,7 @@ class DesktopFileTransferManager : TransferManager {
}
val file = FileUtils.resolveUniqueFile(downloadsDir, request.fileName)
actualFile = file
val sha256 = MessageDigest.getInstance("SHA-256")
var bytesWritten = 0L
@@ -154,11 +158,11 @@ class DesktopFileTransferManager : TransferManager {
}
} catch (e: Exception) {
e.printStackTrace()
actualFile?.let { FileUtils.deleteIfExists(it) }
val current = _incomingTransfers.value.find { it.id == progressId }
if (current != null && current.state != TransferState.COMPLETED &&
current.state != TransferState.FAILED && current.state != TransferState.CANCELLED
) {
FileUtils.deleteIfExists(File(downloadsDir, FileUtils.sanitizeFileName(current.request.fileName)))
updateIncomingById(progressId, current.copy(
state = TransferState.FAILED,
error = e.message ?: "Unknown error"
@@ -0,0 +1,169 @@
package com.aerotoss.transfer
import com.aerotoss.model.TransferState
import kotlinx.coroutines.*
import kotlinx.coroutines.flow.first
import org.junit.After
import org.junit.Before
import org.junit.Test
import java.io.File
import java.net.ServerSocket
import java.net.Socket
import kotlin.test.assertEquals
import kotlin.test.assertTrue
class TransferEdgeCaseTest {
private lateinit var tempDir: File
@Before
fun setup() {
tempDir = File(System.getProperty("java.io.tmpdir"), "aerotoss-edge-${System.currentTimeMillis()}")
tempDir.mkdirs()
}
@After
fun teardown() {
tempDir.deleteRecursively()
}
@Test
fun testEmptyFileTransfer() = runBlocking {
val receiver = DesktopFileTransferManager()
val port = receiver.startServer(0)
try {
val sender = DesktopFileTransferManager()
val testFile = File(tempDir, "empty.txt")
testFile.writeText("")
val result = withTimeout(10_000) {
sender.sendFile(testFile, "127.0.0.1", port).first {
it.state == TransferState.COMPLETED || it.state == TransferState.FAILED
}
}
assertEquals(TransferState.COMPLETED, result.state)
assertEquals(0L, result.bytesTransferred)
sender.dispose()
} finally {
receiver.dispose()
}
}
@Test
fun testTransferToNonexistentServer() = runBlocking {
val sender = DesktopFileTransferManager()
val testFile = File(tempDir, "fail.txt")
testFile.writeText("test")
val result = withTimeout(10_000) {
sender.sendFile(testFile, "127.0.0.1", 59999).first {
it.state == TransferState.COMPLETED || it.state == TransferState.FAILED
}
}
assertEquals(TransferState.FAILED, result.state)
assertTrue(result.error != null)
sender.dispose()
}
@Test
fun testTransferCancellation() = runBlocking {
val receiver = DesktopFileTransferManager()
val port = receiver.startServer(0)
try {
val sender = DesktopFileTransferManager()
val testFile = File(tempDir, "cancel-test.bin")
testFile.writeBytes(ByteArray(1_000_000) { it.toByte() })
val flow = sender.sendFile(testFile, "127.0.0.1", port)
delay(200)
sender.dispose()
receiver.dispose()
} finally {
try { receiver.dispose() } catch (_: Exception) {}
}
}
@Test
fun testFileNameSanitizationInTransfer() = runBlocking {
val receiver = DesktopFileTransferManager()
val port = receiver.startServer(0)
try {
val sender = DesktopFileTransferManager()
val testFile = File(tempDir, "normal.txt")
testFile.writeText("content")
val result = withTimeout(10_000) {
sender.sendFile(testFile, "127.0.0.1", port).first {
it.state == TransferState.COMPLETED || it.state == TransferState.FAILED
}
}
assertEquals(TransferState.COMPLETED, result.state)
sender.dispose()
} finally {
receiver.dispose()
}
}
@Test
fun testMultipleSequentialTransfers() = runBlocking {
val receiver = DesktopFileTransferManager()
val port = receiver.startServer(0)
try {
val sender = DesktopFileTransferManager()
for (i in 1..3) {
val testFile = File(tempDir, "seq-$i.txt")
testFile.writeText("Content $i")
val result = withTimeout(10_000) {
sender.sendFile(testFile, "127.0.0.1", port).first {
it.state == TransferState.COMPLETED || it.state == TransferState.FAILED
}
}
assertEquals(TransferState.COMPLETED, result.state, "Transfer $i should complete")
}
sender.dispose()
} finally {
receiver.dispose()
}
}
@Test
fun testServerHandlesMalformedConnection() {
val server = DesktopFileTransferManager()
val port = server.startServer(0)
try {
val socket = Socket("127.0.0.1", port)
socket.getOutputStream().write("malformed data".toByteArray())
socket.close()
Thread.sleep(500)
} catch (_: Exception) {
} finally {
server.dispose()
}
}
@Test
fun testConcurrentDispose() {
val server = DesktopFileTransferManager()
server.startServer(0)
server.dispose()
server.dispose()
server.dispose()
}
@Test
fun testGetServerPortReturnsValidPort() {
val manager = DesktopFileTransferManager()
val port = manager.startServer(0)
assertTrue(port > 0 && port < 65536)
assertEquals(port, manager.getServerPort())
manager.dispose()
}
}