From 6c0ebbeee126a77299d4198bb5ab1f03391e774f Mon Sep 17 00:00:00 2001 From: Gauthier Roebroeck Date: Sat, 2 May 2020 22:20:22 +0800 Subject: [PATCH] fix(rar): unsupported rar archives are marked as such solid, encrypted, and multi-volumes rar4 archives are not supported closes #147 --- .../gotson/komga/domain/model/Exceptions.kt | 1 + .../komga/domain/service/BookAnalyzer.kt | 3 +++ .../mediacontainer/MediaContainerExtractor.kt | 4 +++ .../mediacontainer/RarExtractor.kt | 6 ++++- .../komga/domain/service/BookAnalyzerTest.kt | 23 +++++++++--------- .../test/resources/archives/rar4-solid.rar | Bin 3243 -> 3384 bytes komga/src/test/resources/archives/rar4.rar | Bin 3176 -> 9480 bytes .../test/resources/archives/rar5-solid.rar | Bin 3249 -> 3360 bytes 8 files changed, 24 insertions(+), 13 deletions(-) diff --git a/komga/src/main/kotlin/org/gotson/komga/domain/model/Exceptions.kt b/komga/src/main/kotlin/org/gotson/komga/domain/model/Exceptions.kt index 8e5dbaa5e..964f10b2b 100644 --- a/komga/src/main/kotlin/org/gotson/komga/domain/model/Exceptions.kt +++ b/komga/src/main/kotlin/org/gotson/komga/domain/model/Exceptions.kt @@ -1,6 +1,7 @@ package org.gotson.komga.domain.model class MediaNotReadyException : Exception() +class MediaUnsupportedException(message: String) : Exception(message) class ImageConversionException(message: String) : Exception(message) class DirectoryNotFoundException(message: String) : Exception(message) class DuplicateNameException(message: String) : Exception(message) diff --git a/komga/src/main/kotlin/org/gotson/komga/domain/service/BookAnalyzer.kt b/komga/src/main/kotlin/org/gotson/komga/domain/service/BookAnalyzer.kt index eec5ac401..8bc8ad648 100644 --- a/komga/src/main/kotlin/org/gotson/komga/domain/service/BookAnalyzer.kt +++ b/komga/src/main/kotlin/org/gotson/komga/domain/service/BookAnalyzer.kt @@ -5,6 +5,7 @@ import org.gotson.komga.domain.model.Book import org.gotson.komga.domain.model.BookPage import org.gotson.komga.domain.model.Media import org.gotson.komga.domain.model.MediaNotReadyException +import org.gotson.komga.domain.model.MediaUnsupportedException import org.gotson.komga.infrastructure.image.ImageConverter import org.gotson.komga.infrastructure.mediacontainer.ContentDetector import org.gotson.komga.infrastructure.mediacontainer.MediaContainerExtractor @@ -36,6 +37,8 @@ class BookAnalyzer( val entries = try { supportedMediaTypes.getValue(mediaType).getEntries(book.path()) + } catch (ex: MediaUnsupportedException) { + return Media(mediaType = mediaType, status = Media.Status.UNSUPPORTED, comment = ex.message) } catch (ex: Exception) { logger.error(ex) { "Error while analyzing book: $book" } return Media(mediaType = mediaType, status = Media.Status.ERROR, comment = ex.message) diff --git a/komga/src/main/kotlin/org/gotson/komga/infrastructure/mediacontainer/MediaContainerExtractor.kt b/komga/src/main/kotlin/org/gotson/komga/infrastructure/mediacontainer/MediaContainerExtractor.kt index 9fca1fcb2..9ba72dc6b 100644 --- a/komga/src/main/kotlin/org/gotson/komga/infrastructure/mediacontainer/MediaContainerExtractor.kt +++ b/komga/src/main/kotlin/org/gotson/komga/infrastructure/mediacontainer/MediaContainerExtractor.kt @@ -1,10 +1,14 @@ package org.gotson.komga.infrastructure.mediacontainer import org.gotson.komga.domain.model.MediaContainerEntry +import org.gotson.komga.domain.model.MediaUnsupportedException import java.nio.file.Path interface MediaContainerExtractor { fun mediaTypes(): List + + @Throws(MediaUnsupportedException::class) fun getEntries(path: Path): List + fun getEntryStream(path: Path, entryName: String): ByteArray } diff --git a/komga/src/main/kotlin/org/gotson/komga/infrastructure/mediacontainer/RarExtractor.kt b/komga/src/main/kotlin/org/gotson/komga/infrastructure/mediacontainer/RarExtractor.kt index 57248783f..8d7ae092e 100644 --- a/komga/src/main/kotlin/org/gotson/komga/infrastructure/mediacontainer/RarExtractor.kt +++ b/komga/src/main/kotlin/org/gotson/komga/infrastructure/mediacontainer/RarExtractor.kt @@ -4,6 +4,7 @@ import com.github.junrar.Archive import mu.KotlinLogging import net.greypanther.natsort.CaseInsensitiveSimpleNaturalComparator import org.gotson.komga.domain.model.MediaContainerEntry +import org.gotson.komga.domain.model.MediaUnsupportedException import org.springframework.stereotype.Service import java.nio.file.Files import java.nio.file.Path @@ -22,6 +23,9 @@ class RarExtractor( override fun getEntries(path: Path): List = Archive(Files.newInputStream(path)).use { rar -> + if (rar.mainHeader.isEncrypted) throw MediaUnsupportedException("Encrypted RAR archives are not supported") + if (rar.mainHeader.isSolid) throw MediaUnsupportedException("Solid RAR archives are not supported") + if (rar.mainHeader.isMultiVolume) throw MediaUnsupportedException("Multi-Volume RAR archives are not supported") rar.fileHeaders .filter { !it.isDirectory } .map { @@ -38,6 +42,6 @@ class RarExtractor( override fun getEntryStream(path: Path, entryName: String): ByteArray = Archive(Files.newInputStream(path)).use { rar -> val header = rar.fileHeaders.find { it.fileNameString == entryName } - rar.getInputStream(header).readBytes() + rar.getInputStream(header).readBytes() } } diff --git a/komga/src/test/kotlin/org/gotson/komga/domain/service/BookAnalyzerTest.kt b/komga/src/test/kotlin/org/gotson/komga/domain/service/BookAnalyzerTest.kt index 86291bb60..f47afc696 100644 --- a/komga/src/test/kotlin/org/gotson/komga/domain/service/BookAnalyzerTest.kt +++ b/komga/src/test/kotlin/org/gotson/komga/domain/service/BookAnalyzerTest.kt @@ -21,30 +21,30 @@ class BookAnalyzerTest( @Autowired private val bookAnalyzer: BookAnalyzer ) { - @ParameterizedTest - @ValueSource(strings = [ - "rar4.rar", "rar4-solid.rar" - ]) - fun `given rar4 archive when analyzing then media status is READY`(fileName: String) { - val file = ClassPathResource("archives/$fileName") + @Test + fun `given rar4 archive when analyzing then media status is READY`() { + val file = ClassPathResource("archives/rar4.rar") val book = Book("book", file.url, LocalDateTime.now()) val media = bookAnalyzer.analyze(book) assertThat(media.mediaType).isEqualTo("application/x-rar-compressed; version=4") assertThat(media.status).isEqualTo(Media.Status.READY) - assertThat(media.pages).hasSize(1) + assertThat(media.pages).hasSize(3) } - @Test - fun `given rar4 encrypted archive when analyzing then media status is ERROR`() { - val file = ClassPathResource("archives/rar4-encrypted.rar") + @ParameterizedTest + @ValueSource(strings = [ + "rar4-solid.rar", "rar4-encrypted.rar" + ]) + fun `given rar4 solid or encrypted archive when analyzing then media status is UNSUPPORTED`(fileName: String) { + val file = ClassPathResource("archives/rar4-solid.rar") val book = Book("book", file.url, LocalDateTime.now()) val media = bookAnalyzer.analyze(book) assertThat(media.mediaType).isEqualTo("application/x-rar-compressed; version=4") - assertThat(media.status).isEqualTo(Media.Status.ERROR) + assertThat(media.status).isEqualTo(Media.Status.UNSUPPORTED) } @ParameterizedTest @@ -112,5 +112,4 @@ class BookAnalyzerTest( assertThat(media.status).isEqualTo(Media.Status.READY) assertThat(media.pages).hasSize(1) } - } diff --git a/komga/src/test/resources/archives/rar4-solid.rar b/komga/src/test/resources/archives/rar4-solid.rar index 7d054814c4a6378a5f3886e423072a95cc1aae05..1c25018ab09824dacf861f88a1f9e90789e10f91 100644 GIT binary patch literal 3384 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 literal 3243 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 diff --git a/komga/src/test/resources/archives/rar4.rar b/komga/src/test/resources/archives/rar4.rar index 91d89064d81e2385ef26049145bf2a619c58ce81..f7fae45779055241389a18fe90a2af56a1e89276 100644 GIT binary patch delta 138 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 KmejN~6#oTfeR~D~ delta 3220 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 G1q1+NPES1m