From 25947449affe1387a6101d4f2a3c11d911fadf9a Mon Sep 17 00:00:00 2001 From: Roman Pytkov Date: Wed, 1 Jan 2025 22:28:16 +0300 Subject: [PATCH] =?UTF-8?q?=D0=A8=D0=B8=D1=84=D1=80=D0=BE=D0=B2=D0=B0?= =?UTF-8?q?=D0=BD=D0=B8=D0=B5=20=D0=BF=D1=83=D1=82=D0=B5=D0=B9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../github/nullptroma/wallenc/app/Logger.kt | 11 + .../app/di/modules/app/SingletonModule.kt | 19 ++ .../app/di/modules/data/SingletonModule.kt | 4 +- .../app/di/modules/domain/UseCasesModule.kt | 9 +- .../wallenc/data/vaults/VaultsManager.kt | 4 +- .../wallenc/data/vaults/local/LocalStorage.kt | 4 +- .../data/vaults/local/LocalStorageAccessor.kt | 14 +- .../wallenc/data/vaults/local/LocalVault.kt | 4 +- .../vaults/local/entity/LocalDirectory.kt | 2 +- .../data/vaults/local/entity/LocalFile.kt | 2 +- .../data/vaults/local/entity/LocalMetaInfo.kt | 2 +- .../wallenc/domain/datatypes/DataPackage.kt | 2 +- .../wallenc/domain/datatypes/EncryptKey.kt | 7 +- .../domain/encrypt/EncryptedStorage.kt | 33 +++ .../encrypt/EncryptedStorageAccessor.kt | 191 ++++++++++++++++++ .../encrypt/entity/EncryptedDirectory.kt | 8 + .../domain/encrypt/entity/EncryptedFile.kt | 5 + .../encrypt/entity/EncryptedMetaInfo.kt | 13 ++ .../{models => interfaces}/IDirectory.kt | 2 +- .../wallenc/domain/interfaces/IFile.kt | 5 + .../wallenc/domain/interfaces/ILogger.kt | 5 + .../{models => interfaces}/IMetaInfo.kt | 2 +- .../domain/{models => interfaces}/IStorage.kt | 2 +- .../IStorageAccessor.kt | 6 +- .../IStorageExplorer.kt | 2 +- .../domain/{models => interfaces}/IVault.kt | 2 +- .../{models => interfaces}/IVaultInfo.kt | 2 +- .../{models => interfaces}/IVaultsManager.kt | 2 +- .../nullptroma/wallenc/domain/models/IFile.kt | 5 - .../usecases/GetAllRawStoragesUseCase.kt | 6 +- .../usecases/ManageLocalVaultUseCase.kt | 12 ++ .../usecases/StorageFileManagementUseCase.kt | 6 +- .../wallenc/domain/usecases/TestUseCase.kt | 2 +- .../screens/local/vault/LocalVaultScreen.kt | 50 +++-- .../local/vault/LocalVaultScreenState.kt | 2 +- .../local/vault/LocalVaultViewModel.kt | 23 ++- 36 files changed, 402 insertions(+), 68 deletions(-) create mode 100644 app/src/main/java/com/github/nullptroma/wallenc/app/Logger.kt create mode 100644 app/src/main/java/com/github/nullptroma/wallenc/app/di/modules/app/SingletonModule.kt create mode 100644 domain/src/main/java/com/github/nullptroma/wallenc/domain/encrypt/EncryptedStorage.kt create mode 100644 domain/src/main/java/com/github/nullptroma/wallenc/domain/encrypt/EncryptedStorageAccessor.kt create mode 100644 domain/src/main/java/com/github/nullptroma/wallenc/domain/encrypt/entity/EncryptedDirectory.kt create mode 100644 domain/src/main/java/com/github/nullptroma/wallenc/domain/encrypt/entity/EncryptedFile.kt create mode 100644 domain/src/main/java/com/github/nullptroma/wallenc/domain/encrypt/entity/EncryptedMetaInfo.kt rename domain/src/main/java/com/github/nullptroma/wallenc/domain/{models => interfaces}/IDirectory.kt (59%) create mode 100644 domain/src/main/java/com/github/nullptroma/wallenc/domain/interfaces/IFile.kt create mode 100644 domain/src/main/java/com/github/nullptroma/wallenc/domain/interfaces/ILogger.kt rename domain/src/main/java/com/github/nullptroma/wallenc/domain/{models => interfaces}/IMetaInfo.kt (75%) rename domain/src/main/java/com/github/nullptroma/wallenc/domain/{models => interfaces}/IStorage.kt (85%) rename domain/src/main/java/com/github/nullptroma/wallenc/domain/{models => interfaces}/IStorageAccessor.kt (88%) rename domain/src/main/java/com/github/nullptroma/wallenc/domain/{models => interfaces}/IStorageExplorer.kt (76%) rename domain/src/main/java/com/github/nullptroma/wallenc/domain/{models => interfaces}/IVault.kt (85%) rename domain/src/main/java/com/github/nullptroma/wallenc/domain/{models => interfaces}/IVaultInfo.kt (86%) rename domain/src/main/java/com/github/nullptroma/wallenc/domain/{models => interfaces}/IVaultsManager.kt (77%) delete mode 100644 domain/src/main/java/com/github/nullptroma/wallenc/domain/models/IFile.kt create mode 100644 domain/src/main/java/com/github/nullptroma/wallenc/domain/usecases/ManageLocalVaultUseCase.kt diff --git a/app/src/main/java/com/github/nullptroma/wallenc/app/Logger.kt b/app/src/main/java/com/github/nullptroma/wallenc/app/Logger.kt new file mode 100644 index 0000000..5b71d4d --- /dev/null +++ b/app/src/main/java/com/github/nullptroma/wallenc/app/Logger.kt @@ -0,0 +1,11 @@ +package com.github.nullptroma.wallenc.app + +import com.github.nullptroma.wallenc.domain.interfaces.ILogger +import timber.log.Timber + +class Logger: ILogger { + override fun debug(tag: String, msg: String) { + Timber.tag(tag) + Timber.d(msg) + } +} \ No newline at end of file diff --git a/app/src/main/java/com/github/nullptroma/wallenc/app/di/modules/app/SingletonModule.kt b/app/src/main/java/com/github/nullptroma/wallenc/app/di/modules/app/SingletonModule.kt new file mode 100644 index 0000000..335fd9c --- /dev/null +++ b/app/src/main/java/com/github/nullptroma/wallenc/app/di/modules/app/SingletonModule.kt @@ -0,0 +1,19 @@ +package com.github.nullptroma.wallenc.app.di.modules.app + +import com.github.nullptroma.wallenc.app.Logger +import com.github.nullptroma.wallenc.domain.interfaces.ILogger +import dagger.Module +import dagger.Provides +import dagger.hilt.InstallIn +import dagger.hilt.components.SingletonComponent +import javax.inject.Singleton + +@Module +@InstallIn(SingletonComponent::class) +class SingletonModule { + @Provides + @Singleton + fun provideLogger(): ILogger { + return Logger() + } +} \ No newline at end of file diff --git a/app/src/main/java/com/github/nullptroma/wallenc/app/di/modules/data/SingletonModule.kt b/app/src/main/java/com/github/nullptroma/wallenc/app/di/modules/data/SingletonModule.kt index 46fcf31..a4e51e1 100644 --- a/app/src/main/java/com/github/nullptroma/wallenc/app/di/modules/data/SingletonModule.kt +++ b/app/src/main/java/com/github/nullptroma/wallenc/app/di/modules/data/SingletonModule.kt @@ -2,9 +2,9 @@ package com.github.nullptroma.wallenc.app.di.modules.data import android.content.Context import com.github.nullptroma.wallenc.app.di.modules.app.IoDispatcher -import com.github.nullptroma.wallenc.data.vaults.local.LocalVault import com.github.nullptroma.wallenc.data.vaults.VaultsManager -import com.github.nullptroma.wallenc.domain.models.IVaultsManager +import com.github.nullptroma.wallenc.data.vaults.local.LocalVault +import com.github.nullptroma.wallenc.domain.interfaces.IVaultsManager import dagger.Module import dagger.Provides import dagger.hilt.InstallIn diff --git a/app/src/main/java/com/github/nullptroma/wallenc/app/di/modules/domain/UseCasesModule.kt b/app/src/main/java/com/github/nullptroma/wallenc/app/di/modules/domain/UseCasesModule.kt index 0f15b66..f7a544a 100644 --- a/app/src/main/java/com/github/nullptroma/wallenc/app/di/modules/domain/UseCasesModule.kt +++ b/app/src/main/java/com/github/nullptroma/wallenc/app/di/modules/domain/UseCasesModule.kt @@ -1,7 +1,8 @@ package com.github.nullptroma.wallenc.app.di.modules.domain -import com.github.nullptroma.wallenc.domain.models.IVaultsManager +import com.github.nullptroma.wallenc.domain.interfaces.IVaultsManager import com.github.nullptroma.wallenc.domain.usecases.GetAllRawStoragesUseCase +import com.github.nullptroma.wallenc.domain.usecases.ManageLocalVaultUseCase import com.github.nullptroma.wallenc.domain.usecases.StorageFileManagementUseCase import dagger.Module import dagger.Provides @@ -18,6 +19,12 @@ class UseCasesModule { return GetAllRawStoragesUseCase(vaultsManager) } + @Provides + @Singleton + fun provideManageLocalVaultUseCase(vaultsManager: IVaultsManager): ManageLocalVaultUseCase { + return ManageLocalVaultUseCase(vaultsManager) + } + @Provides @Singleton fun provideStorageFileManagementUseCase(): StorageFileManagementUseCase { diff --git a/data/src/main/java/com/github/nullptroma/wallenc/data/vaults/VaultsManager.kt b/data/src/main/java/com/github/nullptroma/wallenc/data/vaults/VaultsManager.kt index 857b27d..02833aa 100644 --- a/data/src/main/java/com/github/nullptroma/wallenc/data/vaults/VaultsManager.kt +++ b/data/src/main/java/com/github/nullptroma/wallenc/data/vaults/VaultsManager.kt @@ -1,8 +1,8 @@ package com.github.nullptroma.wallenc.data.vaults import com.github.nullptroma.wallenc.data.vaults.local.LocalVault -import com.github.nullptroma.wallenc.domain.models.IVault -import com.github.nullptroma.wallenc.domain.models.IVaultsManager +import com.github.nullptroma.wallenc.domain.interfaces.IVault +import com.github.nullptroma.wallenc.domain.interfaces.IVaultsManager import kotlinx.coroutines.flow.StateFlow class VaultsManager(override val localVault: LocalVault) : IVaultsManager { diff --git a/data/src/main/java/com/github/nullptroma/wallenc/data/vaults/local/LocalStorage.kt b/data/src/main/java/com/github/nullptroma/wallenc/data/vaults/local/LocalStorage.kt index d18a5f1..dc434a8 100644 --- a/data/src/main/java/com/github/nullptroma/wallenc/data/vaults/local/LocalStorage.kt +++ b/data/src/main/java/com/github/nullptroma/wallenc/data/vaults/local/LocalStorage.kt @@ -1,7 +1,7 @@ package com.github.nullptroma.wallenc.data.vaults.local -import com.github.nullptroma.wallenc.domain.models.IStorage -import com.github.nullptroma.wallenc.domain.models.IStorageAccessor +import com.github.nullptroma.wallenc.domain.interfaces.IStorage +import com.github.nullptroma.wallenc.domain.interfaces.IStorageAccessor import kotlinx.coroutines.CoroutineDispatcher import kotlinx.coroutines.flow.StateFlow import java.util.UUID diff --git a/data/src/main/java/com/github/nullptroma/wallenc/data/vaults/local/LocalStorageAccessor.kt b/data/src/main/java/com/github/nullptroma/wallenc/data/vaults/local/LocalStorageAccessor.kt index 32cf6b4..15360c7 100644 --- a/data/src/main/java/com/github/nullptroma/wallenc/data/vaults/local/LocalStorageAccessor.kt +++ b/data/src/main/java/com/github/nullptroma/wallenc/data/vaults/local/LocalStorageAccessor.kt @@ -8,9 +8,9 @@ import com.github.nullptroma.wallenc.data.vaults.local.entity.LocalFile import com.github.nullptroma.wallenc.data.vaults.local.entity.LocalMetaInfo import com.github.nullptroma.wallenc.domain.datatypes.DataPackage import com.github.nullptroma.wallenc.domain.datatypes.DataPage -import com.github.nullptroma.wallenc.domain.models.IDirectory -import com.github.nullptroma.wallenc.domain.models.IFile -import com.github.nullptroma.wallenc.domain.models.IStorageAccessor +import com.github.nullptroma.wallenc.domain.interfaces.IDirectory +import com.github.nullptroma.wallenc.domain.interfaces.IFile +import com.github.nullptroma.wallenc.domain.interfaces.IStorageAccessor import kotlinx.coroutines.CoroutineDispatcher import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.flow.Flow @@ -50,11 +50,11 @@ class LocalStorageAccessor( private val _isAvailable = MutableStateFlow(false) override val isAvailable: StateFlow = _isAvailable - private val _filesUpdates = MutableSharedFlow>() - override val filesUpdates: SharedFlow> = _filesUpdates + private val _filesUpdates = MutableSharedFlow>() + override val filesUpdates: SharedFlow> = _filesUpdates - private val _dirsUpdates = MutableSharedFlow>() - override val dirsUpdates: SharedFlow> = _dirsUpdates + private val _dirsUpdates = MutableSharedFlow>() + override val dirsUpdates: SharedFlow> = _dirsUpdates init { // запускам сканирование хранилища diff --git a/data/src/main/java/com/github/nullptroma/wallenc/data/vaults/local/LocalVault.kt b/data/src/main/java/com/github/nullptroma/wallenc/data/vaults/local/LocalVault.kt index ab7a921..c09f79e 100644 --- a/data/src/main/java/com/github/nullptroma/wallenc/data/vaults/local/LocalVault.kt +++ b/data/src/main/java/com/github/nullptroma/wallenc/data/vaults/local/LocalVault.kt @@ -3,8 +3,8 @@ package com.github.nullptroma.wallenc.data.vaults.local import android.content.Context import com.github.nullptroma.wallenc.domain.datatypes.EncryptKey import com.github.nullptroma.wallenc.domain.enums.VaultType -import com.github.nullptroma.wallenc.domain.models.IStorage -import com.github.nullptroma.wallenc.domain.models.IVault +import com.github.nullptroma.wallenc.domain.interfaces.IStorage +import com.github.nullptroma.wallenc.domain.interfaces.IVault import kotlinx.coroutines.CoroutineDispatcher import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.flow.MutableStateFlow diff --git a/data/src/main/java/com/github/nullptroma/wallenc/data/vaults/local/entity/LocalDirectory.kt b/data/src/main/java/com/github/nullptroma/wallenc/data/vaults/local/entity/LocalDirectory.kt index 37d1797..a98b8d3 100644 --- a/data/src/main/java/com/github/nullptroma/wallenc/data/vaults/local/entity/LocalDirectory.kt +++ b/data/src/main/java/com/github/nullptroma/wallenc/data/vaults/local/entity/LocalDirectory.kt @@ -1,6 +1,6 @@ package com.github.nullptroma.wallenc.data.vaults.local.entity -import com.github.nullptroma.wallenc.domain.models.IDirectory +import com.github.nullptroma.wallenc.domain.interfaces.IDirectory data class LocalDirectory( override val metaInfo: LocalMetaInfo, diff --git a/data/src/main/java/com/github/nullptroma/wallenc/data/vaults/local/entity/LocalFile.kt b/data/src/main/java/com/github/nullptroma/wallenc/data/vaults/local/entity/LocalFile.kt index 471cb7c..9076819 100644 --- a/data/src/main/java/com/github/nullptroma/wallenc/data/vaults/local/entity/LocalFile.kt +++ b/data/src/main/java/com/github/nullptroma/wallenc/data/vaults/local/entity/LocalFile.kt @@ -1,5 +1,5 @@ package com.github.nullptroma.wallenc.data.vaults.local.entity -import com.github.nullptroma.wallenc.domain.models.IFile +import com.github.nullptroma.wallenc.domain.interfaces.IFile data class LocalFile(override val metaInfo: LocalMetaInfo) : IFile \ No newline at end of file diff --git a/data/src/main/java/com/github/nullptroma/wallenc/data/vaults/local/entity/LocalMetaInfo.kt b/data/src/main/java/com/github/nullptroma/wallenc/data/vaults/local/entity/LocalMetaInfo.kt index a7397db..81c3734 100644 --- a/data/src/main/java/com/github/nullptroma/wallenc/data/vaults/local/entity/LocalMetaInfo.kt +++ b/data/src/main/java/com/github/nullptroma/wallenc/data/vaults/local/entity/LocalMetaInfo.kt @@ -1,6 +1,6 @@ package com.github.nullptroma.wallenc.data.vaults.local.entity -import com.github.nullptroma.wallenc.domain.models.IMetaInfo +import com.github.nullptroma.wallenc.domain.interfaces.IMetaInfo import java.time.Instant diff --git a/domain/src/main/java/com/github/nullptroma/wallenc/domain/datatypes/DataPackage.kt b/domain/src/main/java/com/github/nullptroma/wallenc/domain/datatypes/DataPackage.kt index cf572a0..3401113 100644 --- a/domain/src/main/java/com/github/nullptroma/wallenc/domain/datatypes/DataPackage.kt +++ b/domain/src/main/java/com/github/nullptroma/wallenc/domain/datatypes/DataPackage.kt @@ -1,6 +1,6 @@ package com.github.nullptroma.wallenc.domain.datatypes -sealed class DataPackage( +class DataPackage( val data: T, val isLoading: Boolean? = false, val isError: Boolean? = false diff --git a/domain/src/main/java/com/github/nullptroma/wallenc/domain/datatypes/EncryptKey.kt b/domain/src/main/java/com/github/nullptroma/wallenc/domain/datatypes/EncryptKey.kt index ccc94e6..286f442 100644 --- a/domain/src/main/java/com/github/nullptroma/wallenc/domain/datatypes/EncryptKey.kt +++ b/domain/src/main/java/com/github/nullptroma/wallenc/domain/datatypes/EncryptKey.kt @@ -1,5 +1,10 @@ package com.github.nullptroma.wallenc.domain.datatypes -class EncryptKey(val key: String) { +import java.security.MessageDigest +class EncryptKey(val key: String) { + fun to32Bytes(): ByteArray { + val digest = MessageDigest.getInstance("SHA-256") + return digest.digest(key.toByteArray(Charsets.UTF_8)) + } } \ No newline at end of file diff --git a/domain/src/main/java/com/github/nullptroma/wallenc/domain/encrypt/EncryptedStorage.kt b/domain/src/main/java/com/github/nullptroma/wallenc/domain/encrypt/EncryptedStorage.kt new file mode 100644 index 0000000..2e3e48b --- /dev/null +++ b/domain/src/main/java/com/github/nullptroma/wallenc/domain/encrypt/EncryptedStorage.kt @@ -0,0 +1,33 @@ +package com.github.nullptroma.wallenc.domain.encrypt + +import com.github.nullptroma.wallenc.domain.datatypes.EncryptKey +import com.github.nullptroma.wallenc.domain.interfaces.ILogger +import com.github.nullptroma.wallenc.domain.interfaces.IStorage +import com.github.nullptroma.wallenc.domain.interfaces.IStorageAccessor +import kotlinx.coroutines.CoroutineDispatcher +import kotlinx.coroutines.flow.StateFlow +import java.util.UUID + +class EncryptedStorage( + source: IStorage, + key: EncryptKey, + logger: ILogger, + ioDispatcher: CoroutineDispatcher +) : IStorage { + override val size: StateFlow + get() = TODO("Not yet implemented") + override val numberOfFiles: StateFlow + get() = TODO("Not yet implemented") + override val uuid: UUID + get() = TODO("Not yet implemented") + override val name: StateFlow + get() = TODO("Not yet implemented") + override val isAvailable: StateFlow + get() = TODO("Not yet implemented") + override val accessor: IStorageAccessor = + EncryptedStorageAccessor(source.accessor, key, logger, ioDispatcher) + + override suspend fun rename(newName: String) { + TODO("Not yet implemented") + } +} \ No newline at end of file diff --git a/domain/src/main/java/com/github/nullptroma/wallenc/domain/encrypt/EncryptedStorageAccessor.kt b/domain/src/main/java/com/github/nullptroma/wallenc/domain/encrypt/EncryptedStorageAccessor.kt new file mode 100644 index 0000000..7ee9733 --- /dev/null +++ b/domain/src/main/java/com/github/nullptroma/wallenc/domain/encrypt/EncryptedStorageAccessor.kt @@ -0,0 +1,191 @@ +package com.github.nullptroma.wallenc.domain.encrypt + +import com.github.nullptroma.wallenc.domain.datatypes.DataPackage +import com.github.nullptroma.wallenc.domain.datatypes.EncryptKey +import com.github.nullptroma.wallenc.domain.encrypt.entity.EncryptedDirectory +import com.github.nullptroma.wallenc.domain.encrypt.entity.EncryptedFile +import com.github.nullptroma.wallenc.domain.encrypt.entity.EncryptedMetaInfo +import com.github.nullptroma.wallenc.domain.interfaces.IDirectory +import com.github.nullptroma.wallenc.domain.interfaces.IFile +import com.github.nullptroma.wallenc.domain.interfaces.ILogger +import com.github.nullptroma.wallenc.domain.interfaces.IStorageAccessor +import kotlinx.coroutines.CoroutineDispatcher +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.MutableSharedFlow +import kotlinx.coroutines.flow.SharedFlow +import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.launch +import java.io.InputStream +import java.io.OutputStream +import javax.crypto.Cipher +import javax.crypto.spec.IvParameterSpec +import javax.crypto.spec.SecretKeySpec +import kotlin.io.encoding.Base64 +import kotlin.io.encoding.ExperimentalEncodingApi +import kotlin.io.path.Path +import kotlin.io.path.pathString +import kotlin.random.Random + +class EncryptedStorageAccessor( + private val source: IStorageAccessor, + private val key: EncryptKey, + private val logger: ILogger, + ioDispatcher: CoroutineDispatcher +) : IStorageAccessor { + override val size: StateFlow = source.size + override val numberOfFiles: StateFlow = source.numberOfFiles + override val isAvailable: StateFlow = source.isAvailable + + private val _filesUpdates = MutableSharedFlow>>() + override val filesUpdates: SharedFlow>> = _filesUpdates + + private val _dirsUpdates = MutableSharedFlow>>() + override val dirsUpdates: SharedFlow>> = _dirsUpdates + + private val _secretKey = SecretKeySpec(key.to32Bytes(), "AES") + + init { + val enc = encryptPath("/hello/world/test.txt") + val dec = decryptPath(enc) + collectSourceState(CoroutineScope(ioDispatcher)) + } + + private fun collectSourceState(coroutineScope: CoroutineScope) { + coroutineScope.launch { + launch { + source.filesUpdates.collect { + val files = it.data.map { + val meta = it.metaInfo + EncryptedFile(EncryptedMetaInfo( + size = meta.size, + isDeleted = meta.isDeleted, + isHidden = meta.isHidden, + lastModified = meta.lastModified, + path = decryptPath(meta.path) + )) + } + _filesUpdates.emit(DataPackage( + data = files, + isLoading = it.isLoading, + isError = it.isError + )) + } + } + + launch { + source.dirsUpdates.collect { + val dirs = it.data.map { + val meta = it.metaInfo + EncryptedDirectory(EncryptedMetaInfo( + size = meta.size, + isDeleted = meta.isDeleted, + isHidden = meta.isHidden, + lastModified = meta.lastModified, + path = decryptPath(meta.path) + ), it.elementsCount) + } + _dirsUpdates.emit(DataPackage( + data = dirs, + isLoading = it.isLoading, + isError = it.isError + )) + } + } + } + } + + @OptIn(ExperimentalEncodingApi::class) + private fun encryptString(str: String): String { + val cipher = Cipher.getInstance(AES_FOR_STRINGS) + val iv = IvParameterSpec(Random.nextBytes(IV_LEN)) + cipher.init(Cipher.ENCRYPT_MODE, _secretKey, iv) + val bytesToEncrypt = iv.iv + str.toByteArray(Charsets.UTF_8) + val encryptedBytes = cipher.doFinal(bytesToEncrypt) + return Base64.Default.encode(encryptedBytes).replace("/", ".") + } + + @OptIn(ExperimentalEncodingApi::class) + private fun decryptString(str: String): String { + val cipher = Cipher.getInstance(AES_FOR_STRINGS) + val bytesToDecrypt = Base64.Default.decode(str.replace(".", "/")) + val iv = IvParameterSpec(bytesToDecrypt.take(IV_LEN).toByteArray()) + cipher.init(Cipher.DECRYPT_MODE, _secretKey, iv) + val decryptedBytes = cipher.doFinal(bytesToDecrypt.drop(IV_LEN).toByteArray()) + return String(decryptedBytes, Charsets.UTF_8) + } + + private fun encryptPath(pathStr: String): String { + val path = Path(pathStr) + val segments = mutableListOf() + for (segment in path) + segments.add(encryptString(segment.pathString)) + val res = Path("/",*(segments.toTypedArray())) + logger.debug("encryptPath", "$pathStr to $res") + return res.pathString + } + + private fun decryptPath(pathStr: String): String { + val path = Path(pathStr) + val segments = mutableListOf() + for (segment in path) + segments.add(decryptString(segment.pathString)) + val res = Path("/",*(segments.toTypedArray())) + logger.debug("decryptPath", "$pathStr to $res") + return res.pathString + } + + override suspend fun getAllFiles(): List { + TODO("Not yet implemented") + } + + override suspend fun getFiles(path: String): List { + TODO("Not yet implemented") + } + + override fun getFilesFlow(path: String): Flow>> { + TODO("Not yet implemented") + } + + override suspend fun getAllDirs(): List { + TODO("Not yet implemented") + } + + override suspend fun getDirs(path: String): List { + TODO("Not yet implemented") + } + + override fun getDirsFlow(path: String): Flow>> { + TODO("Not yet implemented") + } + + override suspend fun touchFile(path: String) { + TODO("Not yet implemented") + } + + override suspend fun touchDir(path: String) { + TODO("Not yet implemented") + } + + override suspend fun delete(path: String) { + TODO("Not yet implemented") + } + + override suspend fun openWrite(path: String): OutputStream { + TODO("Not yet implemented") + } + + override suspend fun openRead(path: String): InputStream { + TODO("Not yet implemented") + } + + override suspend fun moveToTrash(path: String) { + TODO("Not yet implemented") + } + + + companion object { + private const val IV_LEN = 16 + private const val AES_FOR_STRINGS = "AES/CBC/PKCS5Padding" + } +} \ No newline at end of file diff --git a/domain/src/main/java/com/github/nullptroma/wallenc/domain/encrypt/entity/EncryptedDirectory.kt b/domain/src/main/java/com/github/nullptroma/wallenc/domain/encrypt/entity/EncryptedDirectory.kt new file mode 100644 index 0000000..ac7acd8 --- /dev/null +++ b/domain/src/main/java/com/github/nullptroma/wallenc/domain/encrypt/entity/EncryptedDirectory.kt @@ -0,0 +1,8 @@ +package com.github.nullptroma.wallenc.domain.encrypt.entity + +import com.github.nullptroma.wallenc.domain.interfaces.IDirectory + +data class EncryptedDirectory( + override val metaInfo: EncryptedMetaInfo, + override val elementsCount: Int? +) : IDirectory \ No newline at end of file diff --git a/domain/src/main/java/com/github/nullptroma/wallenc/domain/encrypt/entity/EncryptedFile.kt b/domain/src/main/java/com/github/nullptroma/wallenc/domain/encrypt/entity/EncryptedFile.kt new file mode 100644 index 0000000..15c47dc --- /dev/null +++ b/domain/src/main/java/com/github/nullptroma/wallenc/domain/encrypt/entity/EncryptedFile.kt @@ -0,0 +1,5 @@ +package com.github.nullptroma.wallenc.domain.encrypt.entity + +import com.github.nullptroma.wallenc.domain.interfaces.IFile + +data class EncryptedFile(override val metaInfo: EncryptedMetaInfo) : IFile \ No newline at end of file diff --git a/domain/src/main/java/com/github/nullptroma/wallenc/domain/encrypt/entity/EncryptedMetaInfo.kt b/domain/src/main/java/com/github/nullptroma/wallenc/domain/encrypt/entity/EncryptedMetaInfo.kt new file mode 100644 index 0000000..9857a98 --- /dev/null +++ b/domain/src/main/java/com/github/nullptroma/wallenc/domain/encrypt/entity/EncryptedMetaInfo.kt @@ -0,0 +1,13 @@ +package com.github.nullptroma.wallenc.domain.encrypt.entity + +import com.github.nullptroma.wallenc.domain.interfaces.IMetaInfo +import java.time.Instant + + +data class EncryptedMetaInfo( + override val size: Long, + override val isDeleted: Boolean = false, + override val isHidden: Boolean = false, + override val lastModified: Instant = java.time.Clock.systemUTC().instant(), + override val path: String +) : IMetaInfo \ No newline at end of file diff --git a/domain/src/main/java/com/github/nullptroma/wallenc/domain/models/IDirectory.kt b/domain/src/main/java/com/github/nullptroma/wallenc/domain/interfaces/IDirectory.kt similarity index 59% rename from domain/src/main/java/com/github/nullptroma/wallenc/domain/models/IDirectory.kt rename to domain/src/main/java/com/github/nullptroma/wallenc/domain/interfaces/IDirectory.kt index 28fcbd9..626f007 100644 --- a/domain/src/main/java/com/github/nullptroma/wallenc/domain/models/IDirectory.kt +++ b/domain/src/main/java/com/github/nullptroma/wallenc/domain/interfaces/IDirectory.kt @@ -1,4 +1,4 @@ -package com.github.nullptroma.wallenc.domain.models +package com.github.nullptroma.wallenc.domain.interfaces interface IDirectory { val metaInfo: IMetaInfo diff --git a/domain/src/main/java/com/github/nullptroma/wallenc/domain/interfaces/IFile.kt b/domain/src/main/java/com/github/nullptroma/wallenc/domain/interfaces/IFile.kt new file mode 100644 index 0000000..5638623 --- /dev/null +++ b/domain/src/main/java/com/github/nullptroma/wallenc/domain/interfaces/IFile.kt @@ -0,0 +1,5 @@ +package com.github.nullptroma.wallenc.domain.interfaces + +interface IFile { + val metaInfo: IMetaInfo +} \ No newline at end of file diff --git a/domain/src/main/java/com/github/nullptroma/wallenc/domain/interfaces/ILogger.kt b/domain/src/main/java/com/github/nullptroma/wallenc/domain/interfaces/ILogger.kt new file mode 100644 index 0000000..ad9d374 --- /dev/null +++ b/domain/src/main/java/com/github/nullptroma/wallenc/domain/interfaces/ILogger.kt @@ -0,0 +1,5 @@ +package com.github.nullptroma.wallenc.domain.interfaces + +interface ILogger { + fun debug(tag: String, msg: String) +} \ No newline at end of file diff --git a/domain/src/main/java/com/github/nullptroma/wallenc/domain/models/IMetaInfo.kt b/domain/src/main/java/com/github/nullptroma/wallenc/domain/interfaces/IMetaInfo.kt similarity index 75% rename from domain/src/main/java/com/github/nullptroma/wallenc/domain/models/IMetaInfo.kt rename to domain/src/main/java/com/github/nullptroma/wallenc/domain/interfaces/IMetaInfo.kt index 5113acb..c45c02e 100644 --- a/domain/src/main/java/com/github/nullptroma/wallenc/domain/models/IMetaInfo.kt +++ b/domain/src/main/java/com/github/nullptroma/wallenc/domain/interfaces/IMetaInfo.kt @@ -1,4 +1,4 @@ -package com.github.nullptroma.wallenc.domain.models +package com.github.nullptroma.wallenc.domain.interfaces import java.time.Instant diff --git a/domain/src/main/java/com/github/nullptroma/wallenc/domain/models/IStorage.kt b/domain/src/main/java/com/github/nullptroma/wallenc/domain/interfaces/IStorage.kt similarity index 85% rename from domain/src/main/java/com/github/nullptroma/wallenc/domain/models/IStorage.kt rename to domain/src/main/java/com/github/nullptroma/wallenc/domain/interfaces/IStorage.kt index 5a92576..f2104cf 100644 --- a/domain/src/main/java/com/github/nullptroma/wallenc/domain/models/IStorage.kt +++ b/domain/src/main/java/com/github/nullptroma/wallenc/domain/interfaces/IStorage.kt @@ -1,4 +1,4 @@ -package com.github.nullptroma.wallenc.domain.models +package com.github.nullptroma.wallenc.domain.interfaces import kotlinx.coroutines.flow.StateFlow import java.util.UUID diff --git a/domain/src/main/java/com/github/nullptroma/wallenc/domain/models/IStorageAccessor.kt b/domain/src/main/java/com/github/nullptroma/wallenc/domain/interfaces/IStorageAccessor.kt similarity index 88% rename from domain/src/main/java/com/github/nullptroma/wallenc/domain/models/IStorageAccessor.kt rename to domain/src/main/java/com/github/nullptroma/wallenc/domain/interfaces/IStorageAccessor.kt index 310fab5..6d8df9a 100644 --- a/domain/src/main/java/com/github/nullptroma/wallenc/domain/models/IStorageAccessor.kt +++ b/domain/src/main/java/com/github/nullptroma/wallenc/domain/interfaces/IStorageAccessor.kt @@ -1,4 +1,4 @@ -package com.github.nullptroma.wallenc.domain.models +package com.github.nullptroma.wallenc.domain.interfaces import com.github.nullptroma.wallenc.domain.datatypes.DataPackage import kotlinx.coroutines.flow.Flow @@ -11,8 +11,8 @@ interface IStorageAccessor { val size: StateFlow val numberOfFiles: StateFlow val isAvailable: StateFlow - val filesUpdates: SharedFlow> - val dirsUpdates: SharedFlow> + val filesUpdates: SharedFlow>> + val dirsUpdates: SharedFlow>> suspend fun getAllFiles(): List suspend fun getFiles(path: String): List diff --git a/domain/src/main/java/com/github/nullptroma/wallenc/domain/models/IStorageExplorer.kt b/domain/src/main/java/com/github/nullptroma/wallenc/domain/interfaces/IStorageExplorer.kt similarity index 76% rename from domain/src/main/java/com/github/nullptroma/wallenc/domain/models/IStorageExplorer.kt rename to domain/src/main/java/com/github/nullptroma/wallenc/domain/interfaces/IStorageExplorer.kt index 9e28361..83fa9c1 100644 --- a/domain/src/main/java/com/github/nullptroma/wallenc/domain/models/IStorageExplorer.kt +++ b/domain/src/main/java/com/github/nullptroma/wallenc/domain/interfaces/IStorageExplorer.kt @@ -1,4 +1,4 @@ -package com.github.nullptroma.wallenc.domain.models +package com.github.nullptroma.wallenc.domain.interfaces import kotlinx.coroutines.flow.StateFlow diff --git a/domain/src/main/java/com/github/nullptroma/wallenc/domain/models/IVault.kt b/domain/src/main/java/com/github/nullptroma/wallenc/domain/interfaces/IVault.kt similarity index 85% rename from domain/src/main/java/com/github/nullptroma/wallenc/domain/models/IVault.kt rename to domain/src/main/java/com/github/nullptroma/wallenc/domain/interfaces/IVault.kt index 79e7329..1f7151b 100644 --- a/domain/src/main/java/com/github/nullptroma/wallenc/domain/models/IVault.kt +++ b/domain/src/main/java/com/github/nullptroma/wallenc/domain/interfaces/IVault.kt @@ -1,4 +1,4 @@ -package com.github.nullptroma.wallenc.domain.models +package com.github.nullptroma.wallenc.domain.interfaces import com.github.nullptroma.wallenc.domain.datatypes.EncryptKey import java.util.UUID diff --git a/domain/src/main/java/com/github/nullptroma/wallenc/domain/models/IVaultInfo.kt b/domain/src/main/java/com/github/nullptroma/wallenc/domain/interfaces/IVaultInfo.kt similarity index 86% rename from domain/src/main/java/com/github/nullptroma/wallenc/domain/models/IVaultInfo.kt rename to domain/src/main/java/com/github/nullptroma/wallenc/domain/interfaces/IVaultInfo.kt index a5b7105..ca74d68 100644 --- a/domain/src/main/java/com/github/nullptroma/wallenc/domain/models/IVaultInfo.kt +++ b/domain/src/main/java/com/github/nullptroma/wallenc/domain/interfaces/IVaultInfo.kt @@ -1,4 +1,4 @@ -package com.github.nullptroma.wallenc.domain.models +package com.github.nullptroma.wallenc.domain.interfaces import com.github.nullptroma.wallenc.domain.enums.VaultType import kotlinx.coroutines.flow.StateFlow diff --git a/domain/src/main/java/com/github/nullptroma/wallenc/domain/models/IVaultsManager.kt b/domain/src/main/java/com/github/nullptroma/wallenc/domain/interfaces/IVaultsManager.kt similarity index 77% rename from domain/src/main/java/com/github/nullptroma/wallenc/domain/models/IVaultsManager.kt rename to domain/src/main/java/com/github/nullptroma/wallenc/domain/interfaces/IVaultsManager.kt index 950f2bb..248ffa2 100644 --- a/domain/src/main/java/com/github/nullptroma/wallenc/domain/models/IVaultsManager.kt +++ b/domain/src/main/java/com/github/nullptroma/wallenc/domain/interfaces/IVaultsManager.kt @@ -1,4 +1,4 @@ -package com.github.nullptroma.wallenc.domain.models +package com.github.nullptroma.wallenc.domain.interfaces import kotlinx.coroutines.flow.StateFlow diff --git a/domain/src/main/java/com/github/nullptroma/wallenc/domain/models/IFile.kt b/domain/src/main/java/com/github/nullptroma/wallenc/domain/models/IFile.kt deleted file mode 100644 index b6ea2ce..0000000 --- a/domain/src/main/java/com/github/nullptroma/wallenc/domain/models/IFile.kt +++ /dev/null @@ -1,5 +0,0 @@ -package com.github.nullptroma.wallenc.domain.models - -interface IFile { - val metaInfo: IMetaInfo -} \ No newline at end of file diff --git a/domain/src/main/java/com/github/nullptroma/wallenc/domain/usecases/GetAllRawStoragesUseCase.kt b/domain/src/main/java/com/github/nullptroma/wallenc/domain/usecases/GetAllRawStoragesUseCase.kt index c82552f..742cb7e 100644 --- a/domain/src/main/java/com/github/nullptroma/wallenc/domain/usecases/GetAllRawStoragesUseCase.kt +++ b/domain/src/main/java/com/github/nullptroma/wallenc/domain/usecases/GetAllRawStoragesUseCase.kt @@ -1,6 +1,6 @@ package com.github.nullptroma.wallenc.domain.usecases -import com.github.nullptroma.wallenc.domain.models.IVaultsManager +import com.github.nullptroma.wallenc.domain.interfaces.IVaultsManager class GetAllRawStoragesUseCase(private val manager: IVaultsManager) { // fun getStoragesFlow() = manager.remoteVaults.combine(manager.localVault) { remote, local -> @@ -9,6 +9,6 @@ class GetAllRawStoragesUseCase(private val manager: IVaultsManager) { // add(local) // } // } - val localStorage - get() = manager.localVault + val localStorages + get() = manager.localVault.storages } \ No newline at end of file diff --git a/domain/src/main/java/com/github/nullptroma/wallenc/domain/usecases/ManageLocalVaultUseCase.kt b/domain/src/main/java/com/github/nullptroma/wallenc/domain/usecases/ManageLocalVaultUseCase.kt new file mode 100644 index 0000000..a43e288 --- /dev/null +++ b/domain/src/main/java/com/github/nullptroma/wallenc/domain/usecases/ManageLocalVaultUseCase.kt @@ -0,0 +1,12 @@ +package com.github.nullptroma.wallenc.domain.usecases + +import com.github.nullptroma.wallenc.domain.interfaces.IVaultsManager + +class ManageLocalVaultUseCase(private val manager: IVaultsManager) { + val localStorages + get() = manager.localVault.storages + + suspend fun createStorage() { + manager.localVault.createStorage() + } +} \ No newline at end of file diff --git a/domain/src/main/java/com/github/nullptroma/wallenc/domain/usecases/StorageFileManagementUseCase.kt b/domain/src/main/java/com/github/nullptroma/wallenc/domain/usecases/StorageFileManagementUseCase.kt index 82f71f7..6fbb55d 100644 --- a/domain/src/main/java/com/github/nullptroma/wallenc/domain/usecases/StorageFileManagementUseCase.kt +++ b/domain/src/main/java/com/github/nullptroma/wallenc/domain/usecases/StorageFileManagementUseCase.kt @@ -1,8 +1,8 @@ package com.github.nullptroma.wallenc.domain.usecases -import com.github.nullptroma.wallenc.domain.models.IDirectory -import com.github.nullptroma.wallenc.domain.models.IFile -import com.github.nullptroma.wallenc.domain.models.IStorage +import com.github.nullptroma.wallenc.domain.interfaces.IDirectory +import com.github.nullptroma.wallenc.domain.interfaces.IFile +import com.github.nullptroma.wallenc.domain.interfaces.IStorage class StorageFileManagementUseCase { private var _storage: IStorage? = null diff --git a/domain/src/main/java/com/github/nullptroma/wallenc/domain/usecases/TestUseCase.kt b/domain/src/main/java/com/github/nullptroma/wallenc/domain/usecases/TestUseCase.kt index 061fd0c..febe4bb 100644 --- a/domain/src/main/java/com/github/nullptroma/wallenc/domain/usecases/TestUseCase.kt +++ b/domain/src/main/java/com/github/nullptroma/wallenc/domain/usecases/TestUseCase.kt @@ -1,6 +1,6 @@ package com.github.nullptroma.wallenc.domain.usecases -import com.github.nullptroma.wallenc.domain.models.IMetaInfo +import com.github.nullptroma.wallenc.domain.interfaces.IMetaInfo class TestUseCase (val meta: IMetaInfo, val id: Int) { diff --git a/presentation/src/main/java/com/github/nullptroma/wallenc/presentation/screens/main/screens/local/vault/LocalVaultScreen.kt b/presentation/src/main/java/com/github/nullptroma/wallenc/presentation/screens/main/screens/local/vault/LocalVaultScreen.kt index 9ae766e..68e9350 100644 --- a/presentation/src/main/java/com/github/nullptroma/wallenc/presentation/screens/main/screens/local/vault/LocalVaultScreen.kt +++ b/presentation/src/main/java/com/github/nullptroma/wallenc/presentation/screens/main/screens/local/vault/LocalVaultScreen.kt @@ -1,16 +1,24 @@ package com.github.nullptroma.wallenc.presentation.screens.main.screens.local.vault -import androidx.compose.foundation.clickable +import androidx.compose.foundation.gestures.detectTapGestures import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.WindowInsets +import androidx.compose.foundation.layout.padding import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.foundation.lazy.items +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.filled.Add import androidx.compose.material3.Card import androidx.compose.material3.ExperimentalMaterial3Api +import androidx.compose.material3.FloatingActionButton +import androidx.compose.material3.Icon +import androidx.compose.material3.Scaffold import androidx.compose.material3.Text import androidx.compose.runtime.Composable -import androidx.compose.runtime.collectAsState import androidx.compose.runtime.getValue import androidx.compose.ui.Modifier +import androidx.compose.ui.input.pointer.pointerInput +import androidx.compose.ui.unit.dp import androidx.hilt.navigation.compose.hiltViewModel import androidx.lifecycle.compose.collectAsStateWithLifecycle @@ -20,19 +28,31 @@ fun LocalVaultScreen(modifier: Modifier = Modifier, viewModel: LocalVaultViewModel = hiltViewModel()) { val uiState by viewModel.state.collectAsStateWithLifecycle() - LazyColumn(modifier = modifier) { - items(uiState.storagesList) { - Card(modifier = Modifier.clickable { - viewModel.printAllFilesToLog(it) - }) { - val available by it.isAvailable.collectAsStateWithLifecycle() - val numOfFiles by it.numberOfFiles.collectAsStateWithLifecycle() - val size by it.size.collectAsStateWithLifecycle() - Column { - Text(it.uuid.toString()) - Text("IsAvailable: $available") - Text("Files: $numOfFiles") - Text("Size: $size") + Scaffold(modifier = modifier, contentWindowInsets = WindowInsets(0.dp), floatingActionButton = { + FloatingActionButton( + onClick = { + viewModel.createStorage() + }, + ) { + Icon(Icons.Filled.Add, "Floating action button.") + } + }) { innerPadding -> + LazyColumn(modifier = Modifier.padding(innerPadding)) { + items(uiState.storagesList) { + Card(modifier = Modifier.pointerInput(Unit) { + detectTapGestures( + onTap = { _ -> viewModel.printAllFilesToLog(it) } + ) + }) { + val available by it.isAvailable.collectAsStateWithLifecycle() + val numOfFiles by it.numberOfFiles.collectAsStateWithLifecycle() + val size by it.size.collectAsStateWithLifecycle() + Column { + Text(it.uuid.toString()) + Text("IsAvailable: $available") + Text("Files: $numOfFiles") + Text("Size: $size") + } } } } diff --git a/presentation/src/main/java/com/github/nullptroma/wallenc/presentation/screens/main/screens/local/vault/LocalVaultScreenState.kt b/presentation/src/main/java/com/github/nullptroma/wallenc/presentation/screens/main/screens/local/vault/LocalVaultScreenState.kt index cabc6c8..123c848 100644 --- a/presentation/src/main/java/com/github/nullptroma/wallenc/presentation/screens/main/screens/local/vault/LocalVaultScreenState.kt +++ b/presentation/src/main/java/com/github/nullptroma/wallenc/presentation/screens/main/screens/local/vault/LocalVaultScreenState.kt @@ -1,5 +1,5 @@ package com.github.nullptroma.wallenc.presentation.screens.main.screens.local.vault -import com.github.nullptroma.wallenc.domain.models.IStorage +import com.github.nullptroma.wallenc.domain.interfaces.IStorage data class LocalVaultScreenState(val storagesList: List) \ No newline at end of file diff --git a/presentation/src/main/java/com/github/nullptroma/wallenc/presentation/screens/main/screens/local/vault/LocalVaultViewModel.kt b/presentation/src/main/java/com/github/nullptroma/wallenc/presentation/screens/main/screens/local/vault/LocalVaultViewModel.kt index aadd6e1..b845bc8 100644 --- a/presentation/src/main/java/com/github/nullptroma/wallenc/presentation/screens/main/screens/local/vault/LocalVaultViewModel.kt +++ b/presentation/src/main/java/com/github/nullptroma/wallenc/presentation/screens/main/screens/local/vault/LocalVaultViewModel.kt @@ -1,12 +1,11 @@ package com.github.nullptroma.wallenc.presentation.screens.main.screens.local.vault -import android.app.Activity -import android.widget.Toast import androidx.lifecycle.viewModelScope -import com.github.nullptroma.wallenc.domain.models.IDirectory -import com.github.nullptroma.wallenc.domain.models.IFile -import com.github.nullptroma.wallenc.domain.models.IStorage -import com.github.nullptroma.wallenc.domain.usecases.GetAllRawStoragesUseCase +import com.github.nullptroma.wallenc.domain.interfaces.IDirectory +import com.github.nullptroma.wallenc.domain.interfaces.IFile +import com.github.nullptroma.wallenc.domain.interfaces.ILogger +import com.github.nullptroma.wallenc.domain.interfaces.IStorage +import com.github.nullptroma.wallenc.domain.usecases.ManageLocalVaultUseCase import com.github.nullptroma.wallenc.domain.usecases.StorageFileManagementUseCase import com.github.nullptroma.wallenc.presentation.viewmodel.ViewModelBase import dagger.hilt.android.lifecycle.HiltViewModel @@ -17,13 +16,13 @@ import kotlin.system.measureTimeMillis @HiltViewModel class LocalVaultViewModel @Inject constructor( - private val _getAllRawStoragesUseCase: GetAllRawStoragesUseCase, - private val _storageFileManagementUseCase: StorageFileManagementUseCase + private val _manageLocalVaultUseCase: ManageLocalVaultUseCase, + private val _storageFileManagementUseCase: StorageFileManagementUseCase, ) : ViewModelBase(LocalVaultScreenState(listOf())) { init { viewModelScope.launch { - _getAllRawStoragesUseCase.localStorage.storages.collect { + _manageLocalVaultUseCase.localStorages.collect { val newState = state.value.copy( storagesList = it ) @@ -52,4 +51,10 @@ class LocalVaultViewModel @Inject constructor( Timber.d("Time: $time ms") } } + + fun createStorage() { + viewModelScope.launch { + _manageLocalVaultUseCase.createStorage() + } + } } \ No newline at end of file