From 202923fbd9eadcf04a8f2bab2b2a7102d96d2bf7 Mon Sep 17 00:00:00 2001 From: zarzet <42882290+zarzet@users.noreply.github.com> Date: Mon, 19 Oct 2026 04:45:14 +0700 Subject: [PATCH] test(android): pin the extension and MIME tables ExtensionMimeTablesTest fixes the outputs of extFromFileName, extFromMimeType, SafDownloadHandler.normalizeExt and mimeTypeForExt, and NativeFinalizationPolicy.normalizeExtension for every table entry plus unknown, blank, uppercase and dotted inputs. The expected values come from reading the code; the tables are not inverse maps, so each keeps its own entries. The two normalizers trim, lowercase with Locale.ROOT and add one leading dot, with "" for blank input; checking the dot before or after lowercasing cannot differ. normalizeExt now delegates to normalizeExtension, which becomes internal. The test passed against both original bodies before the delegation. --- .../spotiflac/NativeFinalizationPolicy.kt | 2 +- .../com/zarz/spotiflac/SafDownloadHandler.kt | 8 +- .../zarz/spotiflac/ExtensionMimeTablesTest.kt | 145 ++++++++++++++++++ 3 files changed, 149 insertions(+), 6 deletions(-) create mode 100644 android/app/src/test/kotlin/com/zarz/spotiflac/ExtensionMimeTablesTest.kt diff --git a/android/app/src/main/kotlin/com/zarz/spotiflac/NativeFinalizationPolicy.kt b/android/app/src/main/kotlin/com/zarz/spotiflac/NativeFinalizationPolicy.kt index fbb49e1f..563af219 100644 --- a/android/app/src/main/kotlin/com/zarz/spotiflac/NativeFinalizationPolicy.kt +++ b/android/app/src/main/kotlin/com/zarz/spotiflac/NativeFinalizationPolicy.kt @@ -462,7 +462,7 @@ internal object NativeFinalizationPolicy { return trimmed } - private fun normalizeExtension(extension: String?): String { + internal fun normalizeExtension(extension: String?): String { val trimmed = extension?.trim().orEmpty() if (trimmed.isEmpty()) return "" val lower = trimmed.lowercase(Locale.ROOT) diff --git a/android/app/src/main/kotlin/com/zarz/spotiflac/SafDownloadHandler.kt b/android/app/src/main/kotlin/com/zarz/spotiflac/SafDownloadHandler.kt index 8ee98f1c..e2fd3b3a 100644 --- a/android/app/src/main/kotlin/com/zarz/spotiflac/SafDownloadHandler.kt +++ b/android/app/src/main/kotlin/com/zarz/spotiflac/SafDownloadHandler.kt @@ -632,11 +632,9 @@ object SafDownloadHandler { } } - internal fun normalizeExt(ext: String?): String { - val trimmed = ext?.trim().orEmpty() - if (trimmed.isEmpty()) return "" - return if (trimmed.startsWith(".")) trimmed.lowercase(Locale.ROOT) else ".${trimmed.lowercase(Locale.ROOT)}" - } + // Same result for every input; pinned by ExtensionMimeTablesTest. + internal fun normalizeExt(ext: String?): String = + NativeFinalizationPolicy.normalizeExtension(ext) internal fun mimeTypeForExt(ext: String?): String { return when (normalizeExt(ext)) { diff --git a/android/app/src/test/kotlin/com/zarz/spotiflac/ExtensionMimeTablesTest.kt b/android/app/src/test/kotlin/com/zarz/spotiflac/ExtensionMimeTablesTest.kt new file mode 100644 index 00000000..abcf6543 --- /dev/null +++ b/android/app/src/test/kotlin/com/zarz/spotiflac/ExtensionMimeTablesTest.kt @@ -0,0 +1,145 @@ +package com.zarz.spotiflac + +import org.junit.Assert.assertEquals +import org.junit.Test + +// Pins the extension and MIME tables used by the SAF and finalizer paths. +// They are not inverse maps: "audio/ogg" names ".opus", while ".ogg" and +// ".opus" both map back to "audio/ogg", and ".aac" has no MIME entry. +class ExtensionMimeTablesTest { + @Test + fun extFromFileNameMatchesLowercaseSuffixesOnly() { + for ((input, expected) in listOf( + "song.m4a" to ".m4a", + "song.mp4" to ".mp4", + "song.aac" to ".aac", + "song.mp3" to ".mp3", + "song.opus" to ".opus", + "song.flac" to ".flac", + "song.ogg" to ".ogg", + "song.wave" to ".wav", + "song.wav" to ".wav", + "song.aiff" to ".aiff", + "song.aifc" to ".aifc", + "song.aif" to ".aif", + "song.dsf" to ".dsf", + "song.dff" to ".dff", + ".flac" to ".flac", + "my.mp3.flac" to ".flac", + "song.wv" to "", + "song.cue" to "", + "song.lrc" to "", + "song.flac.tmp" to "", + "song.flac " to "", + "SONG.FLAC" to "", + "song.Mp3" to "", + "flac" to "", + "" to "", + " " to "", + )) { + assertEquals(input, expected, extFromFileName(input)) + } + } + + @Test + fun extFromMimeTypeMatchesExactTypesOnly() { + for ((input, expected) in listOf( + "audio/mp4" to ".m4a", + "audio/aac" to ".aac", + "audio/eac3" to ".m4a", + "audio/ac3" to ".m4a", + "audio/ac4" to ".m4a", + "audio/mpeg" to ".mp3", + "audio/ogg" to ".opus", + "audio/flac" to ".flac", + "audio/wav" to ".wav", + "audio/x-wav" to ".wav", + "audio/wave" to ".wav", + "audio/vnd.wave" to ".wav", + "audio/aiff" to ".aiff", + "audio/x-aiff" to ".aiff", + null to "", + "" to "", + " " to "", + "AUDIO/FLAC" to "", + " audio/flac" to "", + "audio/x-flac" to "", + "audio/opus" to "", + "audio/mp3" to "", + "application/octet-stream" to "", + )) { + assertEquals(input.toString(), expected, extFromMimeType(input)) + } + } + + @Test + fun normalizersTrimLowercaseAndAddOneLeadingDot() { + for ((input, expected) in normalizerCases) { + assertEquals(input.toString(), expected, SafDownloadHandler.normalizeExt(input)) + assertEquals(input.toString(), expected, NativeFinalizationPolicy.normalizeExtension(input)) + } + } + + @Test + fun mimeTypeForExtNormalizesBeforeLookup() { + val octetStream = "application/octet-stream" + for ((input, expected) in listOf( + ".m4a" to "audio/mp4", + "m4a" to "audio/mp4", + "M4A" to "audio/mp4", + ".mp4" to "audio/mp4", + ".mp3" to "audio/mpeg", + ".opus" to "audio/ogg", + ".ogg" to "audio/ogg", + ".flac" to "audio/flac", + " FLAC " to "audio/flac", + ".wav" to "audio/wav", + ".aiff" to "audio/aiff", + ".aif" to "audio/aiff", + ".aifc" to "audio/aiff", + ".lrc" to octetStream, + ".aac" to octetStream, + ".wave" to octetStream, + ".dsf" to octetStream, + ".dff" to octetStream, + ".wv" to octetStream, + "..flac" to octetStream, + "audio/flac" to octetStream, + null to octetStream, + "" to octetStream, + " " to octetStream, + )) { + assertEquals(input.toString(), expected, SafDownloadHandler.mimeTypeForExt(input)) + } + } + + private val normalizerCases = listOf( + null to "", + "" to "", + " " to "", + "\t\n" to "", + "m4a" to ".m4a", + "mp4" to ".mp4", + "aac" to ".aac", + "mp3" to ".mp3", + "opus" to ".opus", + "ogg" to ".ogg", + "flac" to ".flac", + "wav" to ".wav", + "aiff" to ".aiff", + "aif" to ".aif", + "aifc" to ".aifc", + "lrc" to ".lrc", + ".flac" to ".flac", + "FLAC" to ".flac", + ".FLAC" to ".flac", + " .Mp3 " to ".mp3", + "\tFlac\n" to ".flac", + " m4a" to ".m4a", + "." to ".", + ".." to "..", + "..flac" to "..flac", + "tar.gz" to ".tar.gz", + "ÉXT" to ".éxt", + ) +}