diff --git a/app/proguard-rules.pro b/app/proguard-rules.pro index c99850285..8d689678e 100644 --- a/app/proguard-rules.pro +++ b/app/proguard-rules.pro @@ -69,6 +69,10 @@ # Top-level functions that can only be used by Kotlin. -dontwarn retrofit2.-KotlinExtensions +# Ignore R8 warnings for unused optional XZ and Zstandard support in Apache Commons Compress. +-dontwarn org.apache.commons.compress.compressors.xz.** +-dontwarn org.apache.commons.compress.compressors.zstandard.** + # OkHTTP # JSR 305 annotations are for embedding nullability information. -dontwarn javax.annotation.** diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index b7313312d..f95eebac9 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -33,6 +33,7 @@ threegpp-telecom-charsets = "1.0.1" commons-io = "2.20.0" commons-text = "1.14.0" commonsCodec = "1.19.0" +commons-compress = "1.28.0" bouncy-castle = "1.82" okhttp = "5.2.1" retrofit = "3.0.0" @@ -90,6 +91,7 @@ threegpp-telecom-charsets = { group = "com.github.brake.threegpp", name = "telec commons-io = { group = "commons-io", name = "commons-io", version.ref = "commons-io" } commons-text = { group = "org.apache.commons", name = "commons-text", version.ref = "commons-text" } commons-codec = { module = "commons-codec:commons-codec", version.ref = "commonsCodec" } +commons-compress = { module = "org.apache.commons:commons-compress", version.ref = "commons-compress" } bouncy-castle = { group = "org.bouncycastle", name = "bcpkix-jdk18on", version.ref = "bouncy-castle" } cdoc4j = { group = "org.open-eid.cdoc4j", name = "cdoc4j", version.ref = "cdoc4j" } unboundid-ldapsdk = { group = "com.unboundid", name = "unboundid-ldapsdk", version.ref = "unboundid-ldapsdk" } diff --git a/utils-lib/build.gradle.kts b/utils-lib/build.gradle.kts index 4dbe0675a..1c55473a1 100644 --- a/utils-lib/build.gradle.kts +++ b/utils-lib/build.gradle.kts @@ -55,6 +55,7 @@ dependencies { api(libs.commons.io) api(libs.commons.text) api(libs.commons.codec) + api(libs.commons.compress) implementation(libs.guava) implementation(libs.gson) implementation(libs.threegpp.telecom.charsets) diff --git a/utils-lib/src/androidTest/kotlin/ee/ria/DigiDoc/utilsLib/extensions/FileTest.kt b/utils-lib/src/androidTest/kotlin/ee/ria/DigiDoc/utilsLib/extensions/FileTest.kt index b8784e39d..669e219ef 100644 --- a/utils-lib/src/androidTest/kotlin/ee/ria/DigiDoc/utilsLib/extensions/FileTest.kt +++ b/utils-lib/src/androidTest/kotlin/ee/ria/DigiDoc/utilsLib/extensions/FileTest.kt @@ -220,14 +220,14 @@ class FileTest { mimeType: String, ): File { val tempFile = File.createTempFile(fileName, ".$fileExtension", context.cacheDir) + tempFile.deleteOnExit() - val file = mock(File::class.java) - `when`(file.name).thenReturn("$fileName.$fileExtension") - `when`(file.path).thenReturn(tempFile.path) - `when`(mimeTypeMap.getMimeTypeFromExtension(file.extension.lowercase())).thenReturn(mimeType) + `when`( + mimeTypeMap.getMimeTypeFromExtension(fileExtension.lowercase()) + ).thenReturn(mimeType) - return file + return tempFile } private fun createSignedPDF(): File? { diff --git a/utils-lib/src/main/kotlin/ee/ria/DigiDoc/utilsLib/extensions/FileExtensions.kt b/utils-lib/src/main/kotlin/ee/ria/DigiDoc/utilsLib/extensions/FileExtensions.kt index 537fd4c7a..417b86b61 100644 --- a/utils-lib/src/main/kotlin/ee/ria/DigiDoc/utilsLib/extensions/FileExtensions.kt +++ b/utils-lib/src/main/kotlin/ee/ria/DigiDoc/utilsLib/extensions/FileExtensions.kt @@ -41,12 +41,12 @@ import ee.ria.DigiDoc.utilsLib.file.FileUtil.parseXMLFile import ee.ria.DigiDoc.utilsLib.file.FileUtil.readFileAsString import ee.ria.DigiDoc.utilsLib.logging.LoggingUtil.Companion.debugLog import org.apache.commons.codec.digest.DigestUtils +import org.apache.commons.compress.archivers.zip.ZipFile import java.io.File import java.io.FileInputStream import java.io.IOException import java.util.Locale import java.util.zip.ZipException -import java.util.zip.ZipFile private const val FILE_EXTENSIONS_LOG_TAG = "FileExtensions" @@ -56,8 +56,7 @@ fun File.isXades(context: Context): Boolean { val tempContainerFiles = File(context.filesDir, "tempContainerFiles") try { - // Check if file is a zip file. If not, throw ZipException - ZipFile(this) + checkIsZipFile(this) val signaturesXmlFile = getFileInContainerZip(this, "signatures.xml", tempContainerFiles) val fileExists = signaturesXmlFile?.exists() @@ -74,8 +73,7 @@ fun File.isCades(context: Context): Boolean { val tempContainerFiles = File(context.filesDir, "tempContainerFiles") try { - // Check if file is a zip file. If not, throw ZipException - ZipFile(this) + checkIsZipFile(this) val signaturesXmlFile = getFileInContainerZip(this, "p7s", tempContainerFiles) val fileExists = signaturesXmlFile?.exists() @@ -93,8 +91,7 @@ fun File.mimeType(context: Context): String { val tempContainerFiles = File(context.filesDir, "tempContainerFiles") try { - // Check if file is a zip file. If not, throw ZipException - ZipFile(this) + checkIsZipFile(this) val mimetypeFile = getFileInContainerZip(this, "mimetype", tempContainerFiles) mimetypeFile?.let { @@ -156,3 +153,12 @@ fun File.md5Hash(): String { fun File.saveAs(destinationPath: String) { this.copyTo(File(destinationPath), overwrite = true) } + +// Check if file is a zip file. If not, throw ZipException +@Throws(ZipException::class) +private fun checkIsZipFile(file: File) { + ZipFile + .Builder() + .setFile(file) + .get() +} \ No newline at end of file diff --git a/utils-lib/src/main/kotlin/ee/ria/DigiDoc/utilsLib/file/FileUtil.kt b/utils-lib/src/main/kotlin/ee/ria/DigiDoc/utilsLib/file/FileUtil.kt index cf046a5a3..ece0cff4e 100644 --- a/utils-lib/src/main/kotlin/ee/ria/DigiDoc/utilsLib/file/FileUtil.kt +++ b/utils-lib/src/main/kotlin/ee/ria/DigiDoc/utilsLib/file/FileUtil.kt @@ -33,6 +33,7 @@ import ee.ria.DigiDoc.utilsLib.logging.LoggingUtil.Companion.errorLog import ee.ria.DigiDoc.utilsLib.logging.LoggingUtil.Companion.infoLog import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.withContext +import org.apache.commons.compress.archivers.zip.ZipFile import org.apache.commons.io.FileUtils import org.apache.commons.io.FilenameUtils import org.apache.commons.lang3.StringUtils @@ -55,8 +56,6 @@ import java.nio.charset.Charset import java.nio.charset.StandardCharsets import java.nio.file.Files import java.util.Objects -import java.util.zip.ZipEntry -import java.util.zip.ZipInputStream import javax.xml.parsers.DocumentBuilderFactory import javax.xml.parsers.SAXParserFactory @@ -120,7 +119,9 @@ object FileUtil { replacement: String, ): String { var trimmed = input.trim { it <= ' ' } - if (trimmed.startsWith(".")) { + if (trimmed.startsWith("..")) { + trimmed = trimmed.removePrefix("..") + } else if (trimmed.startsWith(".")) { trimmed = DEFAULT_FILENAME + trimmed } val sb = StringBuilder(trimmed.length) @@ -359,22 +360,56 @@ object FileUtil { ): File? { createDirectoryIfNotExist(outputFolder.path) - ZipInputStream(containerFile.inputStream()).use { zipInputStream -> - var zipEntry: ZipEntry? + val resolvedDestDir = outputFolder.canonicalFile - while (zipInputStream.nextEntry.also { zipEntry = it } != null) { - val entryName = zipEntry?.name - if (entryName != null && File(entryName).name.contains(fileNameToFind)) { - val outputFile = File(outputFolder, File(entryName).name) - FileOutputStream(outputFile).use { outputStream -> - zipInputStream.copyTo(outputStream) - } - return outputFile + val matches = { entryName: String -> + val file = File(entryName) + val fileName = file.name + val nameWithoutExt = file.nameWithoutExtension + val extension = file.extension + + when { + fileNameToFind.contains(".") -> { + fileName.equals(fileNameToFind, ignoreCase = true) + } + + else -> { + extension.equals(fileNameToFind, ignoreCase = true) + || (extension.isEmpty() && + nameWithoutExt.equals(fileNameToFind, ignoreCase = true)) } } } - return null + val zip = + ZipFile + .Builder() + .setFile(containerFile) + .get() + + zip.use { + val entry = + it.entries + .asSequence() + .filterNot { archiveEntry -> archiveEntry.isDirectory } + .firstOrNull { archiveEntry -> matches(archiveEntry.name) } + ?: return null + + val outputFile = File(outputFolder, File(entry.name).name) + val resolvedOutFile = outputFile.canonicalFile + + if (!resolvedOutFile.path.startsWith(resolvedDestDir.path + File.separator)) { + return null + } + + it.getInputStream(entry).use { input -> + resolvedOutFile.outputStream().use { output -> + input.copyTo(output) + } + } + + return resolvedOutFile + } } fun readFileAsString(file: File): String = file.readText()