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:
@@ -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()
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user