diff --git a/SECURITY.md b/SECURITY.md index 390bff04ed..c73ba27988 100644 --- a/SECURITY.md +++ b/SECURITY.md @@ -5,11 +5,16 @@ `v3.8.5` supports Android 4.0 and above. `v4.x.x` would only support Android 4.4 and above. -Andorid devices that runs Android versions less that Android 4.4 -(Android KitKat) would not recieve any more updates (the latest +Android devices that runs Android versions less that Android 4.4 +(Android KitKat) would not receive any more updates (the latest supported version would be `v3.8.5`). ## Reporting a Vulnerability Feel free to contact us via `support@teamamaze.xyz`. -- please CC the maintainers too: `vishalmeham2@gmail.com` `airwave209gt@gmail.com` `emmanuelbendavid@gmail.com` `t.v.s10123@gmail.com` + +Please CC the maintainers too: +- `vishalmeham2@gmail.com` +- `airwave209gt@gmail.com` +- `emmanuelbendavid@gmail.com` +- `t.v.s10123@gmail.com` \ No newline at end of file diff --git a/app/build.gradle b/app/build.gradle index 6d4e53e4a4..a1984d0db8 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -144,7 +144,7 @@ dependencies { testImplementation "androidx.test:rules:$androidXTestVersion" testImplementation "androidx.test.ext:junit:$androidXTestExtVersion" testImplementation "org.mockito:mockito-core:$mockitoVersion" - testImplementation "org.mockito:mockito-inline:$mockitoVersion" + testImplementation "org.mockito:mockito-inline:$mockitoInlineVersion" testImplementation "org.mockito.kotlin:mockito-kotlin:$mockitoKotlinVersion" testImplementation "org.apache.sshd:sshd-core:1.7.0" testImplementation "org.awaitility:awaitility:$awaitilityVersion" diff --git a/app/src/main/java/com/amaze/filemanager/filesystem/FilenameHelper.kt b/app/src/main/java/com/amaze/filemanager/filesystem/FilenameHelper.kt new file mode 100644 index 0000000000..d1e97ddc4d --- /dev/null +++ b/app/src/main/java/com/amaze/filemanager/filesystem/FilenameHelper.kt @@ -0,0 +1,239 @@ +/* + * Copyright (C) 2014-2023 Arpit Khurana , Vishal Nehra , + * Emmanuel Messulam, Raymond Lai and Contributors. + * + * This file is part of Amaze File Manager. + * + * Amaze File Manager is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package com.amaze.filemanager.filesystem + +import com.amaze.filemanager.application.AppConfig +import com.amaze.filemanager.filesystem.ftp.NetCopyConnectionInfo.Companion.SLASH +import kotlin.math.absoluteValue + +/** + * Convenient extension to return path element of a path string = the part before the last slash. + */ +fun String.pathDirname(): String = if (contains(SLASH)) { + substringBeforeLast(SLASH) +} else { + "" +} + +/** + * Convenient extension to return the name element of a path = the part after the last slash. + */ +fun String.pathBasename(): String = if (contains(SLASH)) { + substringAfterLast(SLASH) +} else { + this +} + +/** + * Convenient extension to return the basename element of a filename = the part after the last + * slash and before the extension (.). + */ +fun String.pathFileBasename(): String = if (contains('.')) { + pathBasename().substringBeforeLast('.') +} else { + pathBasename() +} + +/** + * Convenient extension to return the extension element of a filename = the part after the last + * slash and after the extension (.). Returns empty string if no extension dot exist. + */ +fun String.pathFileExtension(): String = if (contains('.')) { + pathBasename().substringAfterLast('.') +} else { + "" +} + +enum class FilenameFormatFlag { + DARWIN, DEFAULT, WINDOWS, LINUX +} + +object FilenameHelper { + + /* Don't split complex regexs into multiple lines. */ + + /* ktlint-disable max-line-length */ + private const val REGEX_RAW_NUMBERS = "| [0-9]+" + private const val REGEX_SOURCE = " \\((?:(another|[0-9]+(th|st|nd|rd)) )?copy\\)|copy( [0-9]+)?|\\.\\(incomplete\\)| \\([0-9]+\\)|[- ]+" + /* ktlint-enable max-line-length */ + + private val ordinals = arrayOf("th", "st", "nd", "rd") + + /** + * Strip the file path to one without increments or numbers. + * + * Default will not strip the raw numbers; specify removeRawNumbers = true to do so. + */ + @JvmStatic + fun strip(input: String, removeRawNumbers: Boolean = false): String { + val filepath = stripIncrementInternal(input, removeRawNumbers) + val extension = filepath.pathFileExtension() + val dirname = stripIncrementInternal(filepath.pathDirname(), removeRawNumbers) + val stem = stem(filepath, removeRawNumbers) + return StringBuilder().run { + if (dirname.isNotBlank()) { + append(dirname).append(SLASH) + } + append(stem) + if (extension.isNotBlank()) { + append('.').append(extension) + } + toString() + } + } + + /** + * Returns the ordinals of the given number. So that + * + * - toOrdinal(1) returns "1st" + * - toOrdinal(2) returns "2nd" + * - toOrdinal(10) returns "10th" + * - toOrdinal(11) returns "11th" + * - toOrdinal(12) returns "12th" + * - toOrdinal(21) returns "21st" + * - toOrdinal(22) returns "22nd" + * - toOrdinal(23) returns "23rd" + * + * etc. + */ + @JvmStatic + fun toOrdinal(n: Int): String = "$n${ordinal(n.absoluteValue)}" + + /** + * Increment the filename of a given [HybridFile]. + * + * Uses [HybridFile.exists] to check file existence and if it exists, returns a HybridFile + * with new filename which does not exist. + */ + @JvmStatic + fun increment( + file: HybridFile, + platform: FilenameFormatFlag = FilenameFormatFlag.DEFAULT, + strip: Boolean = true, + removeRawNumbers: Boolean = false, + startArg: Int = 1 + ): HybridFile { + var filename = file.getName(AppConfig.getInstance()) + var dirname = file.path.pathDirname() + var basename = filename.pathFileBasename() + val extension = filename.pathFileExtension() + + var start: Int = startArg + + if (strip) { + filename = stripIncrementInternal(filename, removeRawNumbers) + dirname = stripIncrementInternal(dirname, removeRawNumbers) + basename = strip(basename, removeRawNumbers) + } + + var retval = HybridFile( + file.mode, + dirname, + filename, + file.isDirectory(AppConfig.getInstance()) + ) + + while (retval.exists(AppConfig.getInstance())) { + filename = if (extension.isNotBlank()) { + format(platform, basename, start++) + ".$extension" + } else { + format(platform, basename, start++) + } + retval = HybridFile( + file.mode, + dirname, + filename, + file.isDirectory(AppConfig.getInstance()) + ) + } + + return retval + } + + private fun stripIncrementInternal(input: String, removeRawNumbers: Boolean = false): String { + val source = StringBuilder().run { + append(REGEX_SOURCE) + if (removeRawNumbers) { + append(REGEX_RAW_NUMBERS) + } + toString() + } + return Regex("($source)+$", RegexOption.IGNORE_CASE).replace(input, "") + } + + private fun stem(filepath: String, removeRawNumbers: Boolean = false): String { + val extension = filepath.pathFileExtension() + return stripIncrementInternal( + filepath.pathBasename().substringBefore(".$extension"), + removeRawNumbers + ) + } + + private fun ordinal(n: Int): String { + var retval = ordinals.getOrNull(((n % 100) - 20) % 10) + if (retval == null) { + retval = ordinals.getOrNull(n % 100) + } + if (retval == null) { + retval = ordinals[0] + } + return retval + } + + // TODO: i18n + private fun format(flag: FilenameFormatFlag, stem: String, n: Int): String { + return when (flag) { + FilenameFormatFlag.DARWIN -> { + if (n == 1) { + "$stem copy" + } else if (n > 1) { + "$stem copy $n" + } else { + stem + } + } + FilenameFormatFlag.LINUX -> { + when (n) { + 0 -> { + stem + } + 1 -> { + "$stem (copy)" + } + 2 -> { + "$stem (another copy)" + } + else -> { + "$stem (${toOrdinal(n)} copy)" + } + } + } + // Windows and default formatting are the same. + else -> { + if (n >= 1) { + "$stem ($n)" + } else { + stem + } + } + } + } +} diff --git a/app/src/main/java/com/amaze/filemanager/filesystem/files/FileListSorter.java b/app/src/main/java/com/amaze/filemanager/filesystem/files/FileListSorter.java index e97e83b59a..df4bdb7e06 100644 --- a/app/src/main/java/com/amaze/filemanager/filesystem/files/FileListSorter.java +++ b/app/src/main/java/com/amaze/filemanager/filesystem/files/FileListSorter.java @@ -120,7 +120,7 @@ public int compare(LayoutElementParcelable file1, LayoutElementParcelable file2) return 0; } - private static String getExtension(String a) { + public static String getExtension(String a) { return a.substring(a.lastIndexOf(".") + 1).toLowerCase(); } } diff --git a/app/src/test/java/com/amaze/filemanager/filesystem/AbstractFilenameHelperIncrementNameTests.kt b/app/src/test/java/com/amaze/filemanager/filesystem/AbstractFilenameHelperIncrementNameTests.kt new file mode 100644 index 0000000000..1b199e3cff --- /dev/null +++ b/app/src/test/java/com/amaze/filemanager/filesystem/AbstractFilenameHelperIncrementNameTests.kt @@ -0,0 +1,164 @@ +/* + * Copyright (C) 2014-2023 Arpit Khurana , Vishal Nehra , + * Emmanuel Messulam, Raymond Lai and Contributors. + * + * This file is part of Amaze File Manager. + * + * Amaze File Manager is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package com.amaze.filemanager.filesystem + +import android.os.Build +import android.os.Build.VERSION_CODES.KITKAT +import android.os.Build.VERSION_CODES.P +import androidx.test.ext.junit.runners.AndroidJUnit4 +import com.amaze.filemanager.application.AppConfig +import com.amaze.filemanager.fileoperations.filesystem.OpenMode +import com.amaze.filemanager.shadows.ShadowMultiDex +import io.mockk.every +import io.mockk.mockkConstructor +import io.mockk.unmockkConstructor +import org.junit.Assert.assertEquals +import org.junit.Assert.assertFalse +import org.junit.Assert.assertTrue +import org.junit.Test +import org.junit.runner.RunWith +import org.mockito.Mockito +import org.mockito.Mockito.`when` +import org.robolectric.annotation.Config + +@RunWith(AndroidJUnit4::class) +@Config( + shadows = [ShadowMultiDex::class], + sdk = [KITKAT, P, Build.VERSION_CODES.R] +) +@Suppress("StringLiteralDuplication") +abstract class AbstractFilenameHelperIncrementNameTests { + + protected abstract val formatFlag: FilenameFormatFlag + + protected lateinit var file: HybridFile + + private val existingFiles = arrayOf( + "/test/afile", + "/test/abc (2) - Copy - Copy.txt", + "/test/abc (2) - Copy.txt", + "/test/abc.txt", + "/test/bar.txt", + "/test/foo (2).txt", + "/test/foo 2 2.txt", + "/test/foo 2.txt", + "/test/foo 22.txt", + "/test/foo 3 copy.txt", + "/test/foo copy 2.txt", + "/test/foo copy 3.txt", + "/test/foo copy 4.txt", + "/test/foo copy 5.txt", + "/test/foo copy 6.txt", + "/test/foo copy.txt", + "/test/foo.txt", + "/test/one (copy).txt", + "/test/one.txt", + "/test/qux (2).txt", + "/test/qux 2.txt", + "/test/qux.txt", + "/test/sub/foo.txt", + "/test/sub/bar.txt", + "/test/sub/nested/bar.txt", + "/test/sub/nested/foo.txt", + "/test/sub/nested/foo copy.txt", + "/test/sub/nested/qux.txt", + "/test/sub/nested/qux 2.txt", + "/test/sub/nested/qux (2).txt" + ) + + /** + * Sanity check. + */ + @Test + fun testSanityCheck() { + mockkConstructor(HybridFile::class) + file = HybridFile(OpenMode.UNKNOWN, "/test/file1.txt") + every { file.exists(AppConfig.getInstance()) } answers { + file.path == "/test/file1.txt" + } + assertEquals(OpenMode.UNKNOWN, file.mode) + assertEquals("/test/file1.txt", file.path) + assertTrue("file.path is ${file.path}", file.exists(AppConfig.getInstance())) + file = HybridFile(OpenMode.UNKNOWN, "/test/file2.txt") + assertEquals(OpenMode.UNKNOWN, file.mode) + assertEquals("/test/file2.txt", file.path) + assertFalse("file.path is ${file.path}", file.exists(AppConfig.getInstance())) + unmockkConstructor(HybridFile::class) + } + + /** + * Ensure [FilenameHelper.increment] will have no effect when [HybridFile.exists] is false. + */ + @Test + fun testIncrementShouldNotHaveEffectIfNotExist() { + mockkConstructor(HybridFile::class) + file = HybridFile(OpenMode.UNKNOWN, "/test/file1.txt") + every { file.exists(AppConfig.getInstance()) } answers { + false + } + assertEquals(OpenMode.UNKNOWN, file.mode) + assertEquals("/test/file1.txt", file.path) + assertFalse("file.path is ${file.path}", file.exists(AppConfig.getInstance())) + val retval = FilenameHelper.increment(file = file, formatFlag) + assertEquals("file1.txt", retval.getName(AppConfig.getInstance())) + unmockkConstructor(HybridFile::class) + } + + protected fun performTest( + pairs: Array>, + strip: Boolean = false, + removeRawNumbers: Boolean = false, + start: Int = 1 + ) { + for (pair in pairs) performTest(pair, strip, removeRawNumbers, start) + } + + protected fun performTest( + pair: Pair, + strip: Boolean = false, + removeRawNumbers: Boolean = false, + start: Int = 1 + ) { + Mockito.mockConstruction(HybridFile::class.java) { file, context -> + file.mode = context.arguments()[0] as OpenMode + file.path = context.arguments()[1] as String + if (context.arguments().size == 4) { + file.name = context.arguments()[2] as String + file.path += "/${context.arguments()[2] as String}" + } + `when`(file.exists(AppConfig.getInstance())).thenAnswer { + val c = existingFiles + file.path == pair.first || c.contains(file.path) + } + `when`(file.getName(AppConfig.getInstance())).thenAnswer { + file.path.pathBasename() + } + }.run { + file = HybridFile(OpenMode.UNKNOWN, pair.first) + assertEquals(OpenMode.UNKNOWN, file.mode) + assertEquals(pair.first, file.path) + assertTrue("file.path is ${file.path}", file.exists(AppConfig.getInstance())) + val retval = FilenameHelper.increment(file, formatFlag, strip, removeRawNumbers, start) + assertEquals(pair.second, retval.path) + this.close() + } + } +} diff --git a/app/src/test/java/com/amaze/filemanager/filesystem/FilenameHelperDarwinIncrementNameTest.kt b/app/src/test/java/com/amaze/filemanager/filesystem/FilenameHelperDarwinIncrementNameTest.kt new file mode 100644 index 0000000000..534820812a --- /dev/null +++ b/app/src/test/java/com/amaze/filemanager/filesystem/FilenameHelperDarwinIncrementNameTest.kt @@ -0,0 +1,127 @@ +/* + * Copyright (C) 2014-2023 Arpit Khurana , Vishal Nehra , + * Emmanuel Messulam, Raymond Lai and Contributors. + * + * This file is part of Amaze File Manager. + * + * Amaze File Manager is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package com.amaze.filemanager.filesystem + +import org.junit.Test + +/** + * Tests against [FilenameHelper] for its increment capability. + * + * @see FilenameHelper.increment + */ +@Suppress("StringLiteralDuplication") +class FilenameHelperDarwinIncrementNameTest : AbstractFilenameHelperIncrementNameTests() { + + override val formatFlag: FilenameFormatFlag + get() = FilenameFormatFlag.DARWIN + + /** + * Test [FilenameHelper.increment] with [FilenameFormatFlag.DARWIN] formatting scheme. + */ + @Test + fun testDarwinIncrementSimple() { + val pairs = arrayOf( + Pair("/test/file.txt", "/test/file copy.txt"), + Pair("/test/sub/foo.txt", "/test/sub/foo copy.txt"), + Pair("/test/sub/nested/foo.txt", "/test/sub/nested/foo copy 2.txt"), + Pair("/test/afile", "/test/afile copy") + ) + performTest(pairs, true) + } + + /** + * Test [FilenameHelper.increment] with [FilenameFormatFlag.LINUX] formatting scheme, strip + * before increment and removeRawNumbers set to true. + */ + @Test + fun testDarwinIncrementStripExistingRawNumbersBeforeIncrement() { + val pairs = arrayOf( + Pair("/test/foo.txt", "/test/foo copy 7.txt"), + Pair("/test/foo 2.txt", "/test/foo copy 7.txt"), + Pair("/test/foo copy.txt", "/test/foo copy 7.txt"), + Pair("/test/qux 2.txt", "/test/qux copy.txt"), + Pair("/test/abc (2) - Copy.txt", "/test/abc copy.txt"), + Pair("/test/abc (2) - Copy Copy.txt", "/test/abc copy.txt"), + Pair("/test/sub/nested/foo copy.txt", "/test/sub/nested/foo copy 2.txt"), + Pair("/test/sub/nested/foo copy 3.txt", "/test/sub/nested/foo copy 2.txt") + ) + performTest(pairs, strip = true, removeRawNumbers = true) + } + + /** + * Test [FilenameHelper.increment] with [FilenameFormatFlag.DARWIN] formatting scheme, strip + * before increment and removeRawNumbers set to false. + */ + @Test + fun testDarwinIncrementStripExistingNumbersBeforeIncrement() { + val pairs = arrayOf( + Pair("/test/file.txt", "/test/file copy.txt"), + Pair("/test/foo 2.txt", "/test/foo 2 copy.txt"), + Pair("/test/foo copy.txt", "/test/foo copy 7.txt"), + Pair("/test/qux 2.txt", "/test/qux 2 copy.txt"), + Pair("/test/abc (2) - Copy.txt", "/test/abc copy.txt"), + Pair("/test/abc (2) - Copy Copy.txt", "/test/abc copy.txt"), + Pair("/test/sub/nested/foo copy.txt", "/test/sub/nested/foo copy 2.txt"), + Pair("/test/sub/nested/foo copy 3.txt", "/test/sub/nested/foo copy 2.txt") + ) + performTest(pairs, strip = true, removeRawNumbers = false) + } + + /** + * Test [FilenameHelper.increment] with [FilenameFormatFlag.DARWIN] formatting scheme and + * start at specified number. + */ + @Test + fun testDarwinIncrementStartWithSpecifiedNumber() { + val pairs = arrayOf( + Pair("/test/foo copy 7.txt", 1), + Pair("/test/foo copy 7.txt", 2), + Pair("/test/foo copy 7.txt", 3), + Pair("/test/foo copy 7.txt", 4), + Pair("/test/foo copy 7.txt", 5), + Pair("/test/foo copy 7.txt", 6), + Pair("/test/foo copy 7.txt", 7), + Pair("/test/foo copy 8.txt", 8), + Pair("/test/foo copy 101.txt", 101) + ) + for (pair in pairs) { + performTest( + Pair("/test/foo.txt", pair.first), + true, + start = pair.second + ) + } + } + + /** + * Test [FilenameHelper.increment] with [FilenameFormatFlag.DARWIN] formatting scheme and + * without stripping. + */ + @Test + fun testDarwinIncrementStripOff() { + val pairs = arrayOf( + Pair("/test/foo.txt", "/test/foo copy 7.txt"), + Pair("/test/foo 2.txt", "/test/foo 2 copy.txt"), + Pair("/test/foo copy.txt", "/test/foo copy copy.txt") + ) + performTest(pairs, false) + } +} diff --git a/app/src/test/java/com/amaze/filemanager/filesystem/FilenameHelperLinuxIncrementNameTest.kt b/app/src/test/java/com/amaze/filemanager/filesystem/FilenameHelperLinuxIncrementNameTest.kt new file mode 100644 index 0000000000..dcd5f98f89 --- /dev/null +++ b/app/src/test/java/com/amaze/filemanager/filesystem/FilenameHelperLinuxIncrementNameTest.kt @@ -0,0 +1,142 @@ +/* + * Copyright (C) 2014-2023 Arpit Khurana , Vishal Nehra , + * Emmanuel Messulam, Raymond Lai and Contributors. + * + * This file is part of Amaze File Manager. + * + * Amaze File Manager is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package com.amaze.filemanager.filesystem + +import org.junit.Test + +/** + * Tests against [FilenameHelper] for its increment capability. + * + * @see FilenameHelper.increment + */ +@Suppress("StringLiteralDuplication") +class FilenameHelperLinuxIncrementNameTest : AbstractFilenameHelperIncrementNameTests() { + + override val formatFlag: FilenameFormatFlag + get() = FilenameFormatFlag.LINUX + + /** + * Test [FilenameHelper.increment] with [FilenameFormatFlag.LINUX] formatting scheme. + */ + @Test + fun testLinuxIncrementSimple() { + val pairs = arrayOf( + Pair("/test/file.txt", "/test/file (copy).txt"), + Pair("sub/foo.txt", "sub/foo (copy).txt"), + Pair("sub/nested/foo.txt", "sub/nested/foo (copy).txt"), + Pair("/test/afile", "/test/afile (copy)") + ) + performTest(pairs, true) + } + + /** + * Test [FilenameHelper.increment] with [FilenameFormatFlag.LINUX] formatting scheme, strip + * before increment and removeRawNumbers set to true. + */ + @Test + fun testLinuxIncrementStripExistingNumbersBeforeIncrement() { + val pairs = arrayOf( + Pair("/test/file.txt", "/test/file (copy).txt"), + Pair("/test/file 2.txt", "/test/file (copy).txt"), + Pair("/test/foo copy.txt", "/test/foo (copy).txt"), + Pair("/test/one (copy).txt", "/test/one (another copy).txt"), + Pair("/test/qux 2.txt", "/test/qux (copy).txt"), + Pair("/test/abc (2) - Copy.txt", "/test/abc (copy).txt"), + Pair("/test/abc (2) - Copy Copy.txt", "/test/abc (copy).txt"), + Pair("/test/sub/nested/foo copy.txt", "/test/sub/nested/foo (copy).txt"), + Pair("/test/sub/nested/foo copy 2.txt", "/test/sub/nested/foo (copy).txt") + ) + performTest(pairs, strip = true, removeRawNumbers = true) + } + + /** + * Test [FilenameHelper.increment] with [FilenameFormatFlag.LINUX] formatting scheme, strip + * before increment and removeRawNumbers set to false. + */ + @Test + fun testLinuxIncrementNotStripExistingNumbersBeforeIncrement() { + val pairs = arrayOf( + Pair("/test/file.txt", "/test/file (copy).txt"), + Pair("/test/file 2.txt", "/test/file 2 (copy).txt"), + Pair("/test/foo copy.txt", "/test/foo (copy).txt"), + Pair("/test/one (copy).txt", "/test/one (another copy).txt"), + Pair("/test/qux 2.txt", "/test/qux 2 (copy).txt"), + Pair("/test/abc (2) - Copy.txt", "/test/abc (copy).txt"), + Pair("/test/abc (2) - Copy Copy.txt", "/test/abc (copy).txt"), + Pair("/test/sub/nested/foo copy.txt", "/test/sub/nested/foo (copy).txt"), + Pair("/test/sub/nested/foo copy 2.txt", "/test/sub/nested/foo (copy).txt") + ) + performTest(pairs, strip = true, removeRawNumbers = false) + } + + /** + * Test [FilenameHelper.increment] with [FilenameFormatFlag.LINUX] formatting scheme and + * specifying starting numbers. + */ + @Test + fun testLinuxIncrementWithSpecifiedNumbers() { + performTest( + Pair("/test/file.txt", "/test/file (copy).txt"), + start = 0 + ) + val pairs = arrayOf( + Pair(3, "3rd"), + Pair(4, "4th"), + Pair(5, "5th"), + Pair(6, "6th"), + Pair(7, "7th"), + Pair(8, "8th"), + Pair(9, "9th"), + Pair(10, "10th"), + Pair(11, "11th"), + Pair(12, "12th"), + Pair(13, "13th"), + Pair(14, "14th"), + Pair(112, "112th"), + Pair(1112, "1112th"), + Pair(22, "22nd"), + Pair(122, "122nd"), + Pair(1122, "1122nd"), + Pair(102, "102nd"), + Pair(103, "103rd") + ) + for (pair in pairs) { + performTest( + Pair("/test/file.txt", "/test/file (${pair.second} copy).txt"), + start = pair.first + ) + } + } + + /** + * Test [FilenameHelper.increment] with [FilenameFormatFlag.LINUX] formatting scheme and + * without stripping. + */ + @Test + fun testLinuxIncrementStripOff() { + val pairs = arrayOf( + Pair("/test/file.txt", "/test/file (copy).txt"), + Pair("/test/foo 2.txt", "/test/foo 2 (copy).txt"), + Pair("/test/foo copy.txt", "/test/foo copy (copy).txt") + ) + performTest(pairs, false) + } +} diff --git a/app/src/test/java/com/amaze/filemanager/filesystem/FilenameHelperTest.kt b/app/src/test/java/com/amaze/filemanager/filesystem/FilenameHelperTest.kt new file mode 100644 index 0000000000..921eee9653 --- /dev/null +++ b/app/src/test/java/com/amaze/filemanager/filesystem/FilenameHelperTest.kt @@ -0,0 +1,348 @@ +/* + * Copyright (C) 2014-2023 Arpit Khurana , Vishal Nehra , + * Emmanuel Messulam, Raymond Lai and Contributors. + * + * This file is part of Amaze File Manager. + * + * Amaze File Manager is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package com.amaze.filemanager.filesystem + +import com.amaze.filemanager.filesystem.FilenameHelper.strip +import com.amaze.filemanager.filesystem.FilenameHelper.toOrdinal +import org.junit.Assert.assertEquals +import org.junit.Test + +@Suppress("StringLiteralDuplication") +class FilenameHelperTest { + + /** + * [FilenameHelper.strip] should remove linux-style increments from a filename. + */ + @Test + fun testStripLinuxFilename() { + assertEquals("foo.txt", strip("foo (copy).txt")) + assertEquals("foo.txt", strip("foo (another copy).txt")) + assertEquals("foo.txt", strip("foo (3rd copy).txt")) + assertEquals("foo.txt", strip("foo (4th copy).txt")) + assertEquals("foo.txt", strip("foo (5th copy).txt")) + assertEquals("foo.txt", strip("foo (111th copy).txt")) + } + + /** + * [FilenameHelper.strip] should remove linux-style increments from a folder name. + */ + @Test + fun testStripLinuxFolderName() { + assertEquals("foo", strip("foo (copy)")) + assertEquals("foo", strip("foo (another copy)")) + assertEquals("foo", strip("foo (3rd copy)")) + assertEquals("foo", strip("foo (4th copy)")) + assertEquals("foo", strip("foo (5th copy)")) + assertEquals("foo", strip("foo (111th copy)")) + } + + /** + * [FilenameHelper.strip] should not remove non-incremental numbers from a folder name. + */ + @Test + fun testStripLinuxFolderNameWithNumbering() { + assertEquals("foo 1", strip("foo 1 (copy)")) + assertEquals("foo 1", strip("foo 1 (another copy)")) + assertEquals("foo 1", strip("foo 1 (3rd copy)")) + assertEquals("foo 1", strip("foo 1 (4th copy)")) + assertEquals("foo 1", strip("foo 1 (5th copy)")) + assertEquals("foo 1", strip("foo 1 (111th copy)")) + } + + /** + * [FilenameHelper.strip] should not remove raw numbers from file names by default. + */ + @Test + fun testStripNonStandardShouldNotRemoveNumbersByDefault() { + assertEquals("foo 1", strip("foo 1")) + assertEquals("foo 2", strip("foo 2")) + } + + /** + * [FilenameHelper.strip] should remove raw numbers from file names when specified on options. + */ + @Test + fun testStripNonStandardShouldRemoveNumbersWhenSpecified() { + assertEquals("foo", strip("foo 1", true)) + assertEquals("foo", strip("foo 2", true)) + } + + /** + * [FilenameHelper.strip] should remove (incomplete) from a file name. + */ + @Test + fun testStripNonStandardShouldStripIncomplete() { + assertEquals("foo", strip("foo.(incomplete)")) + assertEquals("foo", strip("foo copy 219.(incomplete)")) + assertEquals("foo", strip("foo copy 219.(incomplete).(incomplete).(incomplete)")) + assertEquals("foo", strip("foo.(incomplete).(incomplete)")) + } + + /** + * [FilenameHelper.strip] should remove (incomplete) from a file name with extension. + */ + @Test + fun testStripNonStandardShouldStripIncompleteWhenWithExtension() { + assertEquals("foo.txt", strip("foo.(incomplete).txt")) + assertEquals("foo.txt", strip("foo copy 219.(incomplete).txt")) + assertEquals("foo.txt", strip("foo copy 219.(incomplete).(incomplete).(incomplete).txt")) + assertEquals("foo.txt", strip("foo.(incomplete).(incomplete).txt")) + } + + /** + * [FilenameHelper.strip] should not remove a non-increment from a file or folder name. + */ + @Test + fun testStripNonStandardShouldNotStripNumbersInNonIncrementFilenames() { + assertEquals("foo [1]", strip("foo [1]")) + assertEquals("foo [1].txt", strip("foo [1].txt")) + assertEquals("bar [1]/foo [1].txt", strip("bar [1]/foo [1].txt")) + assertEquals("bar[1]/foo[1].txt", posix(strip("bar[1]/foo[1].txt"))) + } + + /** + * [FilenameHelper.strip] should not remove a non-increment from a basename. + */ + @Test + fun testStripNonStandardShouldNotStripNonIncrementsFromBasename() { + assertEquals("foo [1].txt", strip("foo [1].txt")) + } + + /** + * [FilenameHelper.strip] should remove mac-OS-style increments from a file name. + */ + @Test + fun testDarwinStripMacOsStyleIncrementsFilename() { + assertEquals("foo.txt", strip("foo copy.txt")) + assertEquals("foo.txt", strip("foo copy 1.txt")) + assertEquals("foo.txt", strip("foo copy 2.txt")) + assertEquals("foo.txt", strip("foo copy 21.txt")) + assertEquals("foo.txt", strip("foo copy 219 copy 219.txt")) + assertEquals("foo.txt", strip("foo copy 219 (2).txt")) + } + + /** + * [FilenameHelper.strip] should remove mac-OS-style increments from a folder name. + */ + @Test + fun testDarwinStripMacOsStyleIncrementsFolderName() { + assertEquals("foo", strip("foo copy")) + assertEquals("foo", strip("foo copy 1")) + assertEquals("foo", strip("foo copy 2")) + assertEquals("foo", strip("foo copy 21")) + assertEquals("foo", strip("foo copy 219 copy 219")) + assertEquals("foo", strip("foo copy 219 (2)")) + assertEquals("foo", strip("foo Copy")) + assertEquals("foo", strip("foo Copy 1")) + assertEquals("foo", strip("foo Copy 2")) + assertEquals("foo", strip("foo Copy 21")) + assertEquals("foo", strip("foo Copy 219 copy 219")) + assertEquals("foo", strip("foo Copy 219 (2)")) + } + + /** + * [FilenameHelper.strip] should remove mac-OS-style increments from folder and file name. + */ + @Test + fun testDarwinStripMacOsStyleIncrementsFileAndFolderNames() { + assertEquals("bar/foo.txt", posix(strip("bar copy/foo copy 1.txt"))) + assertEquals("bar/foo.txt", posix(strip("bar copy/foo copy 2.txt"))) + assertEquals("bar/foo.txt", posix(strip("bar copy/foo copy 21.txt"))) + assertEquals("bar/foo.txt", posix(strip("bar copy/foo copy 219 (2).txt"))) + assertEquals("bar/foo.txt", posix(strip("bar copy/foo copy 219 copy 219.txt"))) + assertEquals("bar/foo.txt", posix(strip("bar copy/foo copy.txt"))) + } + + /** + * [FilenameHelper.strip] should remove mac-OS-style increments from a basename. + */ + @Test + fun testDarwinStripMacOsStyleIncrementsBasename() { + assertEquals("foo.txt", strip("foo copy.txt")) + assertEquals("foo.txt", strip("foo copy 1.txt")) + assertEquals("foo.txt", strip("foo copy 2.txt")) + assertEquals("foo.txt", strip("foo copy 21.txt")) + assertEquals("foo.txt", strip("foo copy 219 copy 219.txt")) + assertEquals("foo.txt", strip("foo copy 219 (2).txt")) + } + + /** + * [FilenameHelper.strip] should remove mac-OS-style increments from a basename. + */ + @Test + fun testDarwinStripMacOsStyleIncrementsBasename2() { + assertEquals("foo.txt", strip("foo.(incomplete).txt")) + assertEquals("foo.txt", strip("foo copy 219.(incomplete).txt")) + assertEquals("foo.txt", strip("foo copy 219.(incomplete).(incomplete).(incomplete).txt")) + assertEquals("foo.txt", strip("foo.(incomplete).(incomplete).txt")) + } + + /** + * [FilenameHelper.strip] should remove windows-style increments from a file name. + */ + @Test + fun testWindowsStripIncrementsFromFilename() { + assertEquals("foo", strip("foo (1)")) + assertEquals("foo", strip("foo (2)")) + assertEquals("foo", strip("foo (22)")) + } + + /** + * [FilenameHelper.strip] should not remove non-increments. + */ + @Test + fun testWindowsStripShouldNotRemoveNonIncrements() { + assertEquals("foo 1", strip("foo 1")) + assertEquals("foo (1) 1", strip("foo (1) 1")) + assertEquals("foo [1]", strip("foo [1]")) + } + + /** + * [FilenameHelper.strip] should not remove non-increments. + */ + @Test + fun testWindowsStripShouldNotRemoveNonIncrementsEvenRemoveRawNumbersIsTrue() { + assertEquals("foo", strip("foo 1", true)) + assertEquals("foo", strip("foo (1) 1", true)) + assertEquals("foo [1]", strip("foo [1]", true)) + } + + /** + * [FilenameHelper.strip] should remove windows-style increments from absolute paths. + */ + @Test + fun testWindowsStripIncrementsInWindowsPaths() { + assertEquals(strip("\\foo (1)"), "\\foo") + assertEquals(strip("\\foo (2)"), "\\foo") + assertEquals(strip("\\foo (22)"), "\\foo") + } + + /** + * [FilenameHelper.strip] should remove dash-separated windows-style increments. + */ + @Test + fun testWindowsStripDashSeparatedWindowsIncrements() { + assertEquals("foo", strip("foo (3) - Copy")) + assertEquals("foo", strip("foo (31) - Copy - Copy")) + } + + /** + * [FilenameHelper.strip] should remove windows-style increments from a basename. + */ + @Test + fun testWindowsStripWindowsIncrementsInBasename() { + assertEquals("foo.txt", strip("foo (1).txt")) + assertEquals("foo.txt", strip("foo (2).txt")) + assertEquals("foo.txt", strip("foo (22).txt")) + assertEquals("foo.txt", strip("foo copy (22).txt")) + assertEquals("foo.txt", strip("foo Copy (22).txt")) + } + + /** + * Test [FilenameHelper.toOrdinal] for 0. + */ + @Test + fun testToOrdinalForZero() { + assertEquals("0th", toOrdinal(0)) + assertEquals("0th", toOrdinal(-0)) + } + + /** + * Test [FilenameHelper.toOrdinal] for 1s. + */ + @Test + fun testToOrdinalForOnes() { + assertEquals("1st", toOrdinal(1)) + assertEquals("11th", toOrdinal(11)) + assertEquals("21st", toOrdinal(21)) + assertEquals("31st", toOrdinal(31)) + assertEquals("41st", toOrdinal(41)) + assertEquals("51st", toOrdinal(51)) + assertEquals("61st", toOrdinal(61)) + assertEquals("71st", toOrdinal(71)) + assertEquals("81st", toOrdinal(81)) + assertEquals("91st", toOrdinal(91)) + assertEquals("111th", toOrdinal(111)) + assertEquals("121st", toOrdinal(121)) + assertEquals("211th", toOrdinal(211)) + assertEquals("311th", toOrdinal(311)) + assertEquals("321st", toOrdinal(321)) + assertEquals("10011th", toOrdinal(10011)) + assertEquals("10111th", toOrdinal(10111)) + } + + /** + * Test [FilenameHelper.toOrdinal] for 2s. + */ + @Test + fun testToOrdinalForTwos() { + assertEquals("2nd", toOrdinal(2)) + assertEquals("12th", toOrdinal(12)) + assertEquals("22nd", toOrdinal(22)) + assertEquals("32nd", toOrdinal(32)) + assertEquals("42nd", toOrdinal(42)) + assertEquals("52nd", toOrdinal(52)) + assertEquals("62nd", toOrdinal(62)) + assertEquals("72nd", toOrdinal(72)) + assertEquals("82nd", toOrdinal(82)) + assertEquals("92nd", toOrdinal(92)) + assertEquals("112th", toOrdinal(112)) + assertEquals("212th", toOrdinal(212)) + assertEquals("1012th", toOrdinal(1012)) + assertEquals("10012th", toOrdinal(10012)) + } + + /** + * Test [FilenameHelper.toOrdinal] for 3s. + */ + @Test + fun testToOrdinalForThrees() { + assertEquals("3rd", toOrdinal(3)) + assertEquals("13th", toOrdinal(13)) + assertEquals("23rd", toOrdinal(23)) + assertEquals("33rd", toOrdinal(33)) + assertEquals("43rd", toOrdinal(43)) + assertEquals("53rd", toOrdinal(53)) + assertEquals("63rd", toOrdinal(63)) + assertEquals("73rd", toOrdinal(73)) + assertEquals("83rd", toOrdinal(83)) + assertEquals("93rd", toOrdinal(93)) + assertEquals("103rd", toOrdinal(103)) + assertEquals("113th", toOrdinal(113)) + assertEquals("123rd", toOrdinal(123)) + assertEquals("213th", toOrdinal(213)) + assertEquals("1013th", toOrdinal(1013)) + assertEquals("10013th", toOrdinal(10013)) + } + + /** + * Test [FilenameHelper.toOrdinal] for negative numbers. + */ + @Test + fun testToOrdinalsForNegativeNumbers() { + assertEquals("0th", toOrdinal(-0)) + assertEquals("-1st", toOrdinal(-1)) + assertEquals("-2nd", toOrdinal(-2)) + assertEquals("-3rd", toOrdinal(-3)) + } + + private fun posix(str: String): String = str.replace(Regex("\\\\"), "/") +} diff --git a/app/src/test/java/com/amaze/filemanager/filesystem/FilenameHelperWindowsIncrementNameTest.kt b/app/src/test/java/com/amaze/filemanager/filesystem/FilenameHelperWindowsIncrementNameTest.kt new file mode 100644 index 0000000000..acb824380a --- /dev/null +++ b/app/src/test/java/com/amaze/filemanager/filesystem/FilenameHelperWindowsIncrementNameTest.kt @@ -0,0 +1,127 @@ +/* + * Copyright (C) 2014-2023 Arpit Khurana , Vishal Nehra , + * Emmanuel Messulam, Raymond Lai and Contributors. + * + * This file is part of Amaze File Manager. + * + * Amaze File Manager is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package com.amaze.filemanager.filesystem + +import org.junit.Test + +/** + * Tests against [FilenameHelper] for its increment capability. + * + * @see FilenameHelper.increment + */ +@Suppress("StringLiteralDuplication") +class FilenameHelperWindowsIncrementNameTest : AbstractFilenameHelperIncrementNameTests() { + + override val formatFlag: FilenameFormatFlag + get() = FilenameFormatFlag.WINDOWS + + /** + * Test [FilenameHelper.increment] with [FilenameFormatFlag.WINDOWS] formatting scheme. + */ + @Test + fun testWindowsIncrementSimple() { + val pairs = arrayOf( + Pair("/test/file.txt", "/test/file (1).txt"), + Pair("sub/foo.txt", "sub/foo (1).txt"), + Pair("sub/nested/foo.txt", "sub/nested/foo (1).txt"), + Pair("/test/afile", "/test/afile (1)") + ) + performTest(pairs, true) + } + + /** + * Test [FilenameHelper.increment] with [FilenameFormatFlag.WINDOWS] formatting scheme, strip + * before increment and removeRawNumbers set to true. + */ + @Test + fun testWindowsStripRawNumbersAndIncrementsBeforeUpdatingIncrement() { + val pairs = arrayOf( + Pair("/test/foo.txt", "/test/foo (1).txt"), + Pair("/test/foo 2.txt", "/test/foo (1).txt"), + Pair("/test/foo copy.txt", "/test/foo (1).txt"), + Pair("/test/qux 2.txt", "/test/qux (1).txt"), + Pair("/test/abc (2) - Copy.txt", "/test/abc (1).txt"), + Pair("/test/abc (2) - Copy Copy.txt", "/test/abc (1).txt"), + Pair("/test/sub/nested/foo copy.txt", "/test/sub/nested/foo (1).txt"), + Pair("/test/sub/nested/foo copy 2.txt", "/test/sub/nested/foo (1).txt") + ) + performTest(pairs, strip = true, removeRawNumbers = true) + } + + /** + * Test [FilenameHelper.increment] with [FilenameFormatFlag.WINDOWS] formatting scheme, strip + * before increment and removeRawNumbers set to false. + */ + @Test + fun testWindowsStripBeforeUpdatingIncrement() { + val pairs = arrayOf( + Pair("/test/foo.txt", "/test/foo (1).txt"), + Pair("/test/foo 2.txt", "/test/foo 2 (1).txt"), + Pair("/test/foo copy.txt", "/test/foo (1).txt"), + Pair("/test/qux 2.txt", "/test/qux 2 (1).txt"), + Pair("/test/abc (2) - Copy.txt", "/test/abc (1).txt"), + Pair("/test/abc (2) - Copy Copy.txt", "/test/abc (1).txt"), + Pair("/test/sub/nested/foo copy.txt", "/test/sub/nested/foo (1).txt"), + Pair("/test/sub/nested/foo copy 2.txt", "/test/sub/nested/foo (1).txt") + ) + performTest(pairs, strip = true, removeRawNumbers = false) + } + + /** + * Test [FilenameHelper.increment] with [FilenameFormatFlag.WINDOWS] formatting scheme and + * start at specified number. + */ + @Test + fun testWindowsIncrementStartWithSpecifiedNumber() { + val pairs = arrayOf( + Pair("/test/foo (1).txt", 1), + Pair("/test/foo (3).txt", 2), + Pair("/test/foo (3).txt", 3), + Pair("/test/foo (4).txt", 4), + Pair("/test/foo (5).txt", 5), + Pair("/test/foo (6).txt", 6), + Pair("/test/foo (7).txt", 7), + Pair("/test/foo (101).txt", 101), + Pair("/test/foo (102).txt", 102) + ) + for (pair in pairs) { + performTest( + Pair("/test/foo.txt", pair.first), + true, + start = pair.second + ) + } + } + + /** + * Test [FilenameHelper.increment] with [FilenameFormatFlag.WINDOWS] formatting scheme and + * without stripping. + */ + @Test + fun testWindowsIncrementStripOff() { + val pairs = arrayOf( + Pair("/test/foo.txt", "/test/foo (1).txt"), + Pair("/test/foo 2.txt", "/test/foo 2 (1).txt"), + Pair("/test/foo copy.txt", "/test/foo copy (1).txt") + ) + performTest(pairs, false) + } +} diff --git a/build.gradle b/build.gradle index 842e651fb4..be887ac69a 100644 --- a/build.gradle +++ b/build.gradle @@ -22,8 +22,9 @@ buildscript { uiAutomatorVersion = "2.2.0" junitVersion = "4.13.2" slf4jVersion = "1.7.25" - mockitoVersion = "3.9.0" - mockitoKotlinVersion = "3.2.0" + mockitoVersion = "4.11.0" + mockitoInlineVersion = "4.11.0" + mockitoKotlinVersion = "4.1.0" androidBillingVersion = "5.0.0" junrarVersion = "7.4.0" zip4jVersion = "2.6.4"