package com.aryan.reader import org.junit.Assert.assertArrayEquals import org.junit.Assert.assertEquals import org.junit.Assert.assertNotNull import org.junit.Test import org.junit.runner.RunWith import org.robolectric.RobolectricTestRunner import java.io.ByteArrayInputStream import java.io.ByteArrayOutputStream import java.util.Base64 import java.util.zip.ZipEntry import java.util.zip.ZipOutputStream @RunWith(RobolectricTestRunner::class) class EmbeddedEbookMetadataExtractorTest { @Test fun `epub extracts text metadata and explicitly referenced cover image`() { val coverBytes = onePixelPngBytes() val epubBytes = zipBytes( "META-INF/container.xml" to """ """.trimIndent().toByteArray(Charsets.UTF_8), "OEBPS/content.opf" to """ Folder EPUB Octavia Butler <p>Folder summary</p> """.trimIndent().toByteArray(Charsets.UTF_8), "OEBPS/images/cover.png" to coverBytes ) val metadata = EmbeddedEbookMetadataExtractor.extract( type = FileType.EPUB, displayName = "folder.epub", openStream = { ByteArrayInputStream(epubBytes) } ) assertEquals("Folder EPUB", metadata.title) assertEquals("Octavia Butler", metadata.author) assertEquals("

Folder summary

", metadata.description) assertEquals("Patternist", metadata.seriesName) assertEquals(3.0, metadata.seriesIndex) val cover = metadata.cover assertNotNull(cover) assertEquals("png", cover!!.extension) assertArrayEquals(coverBytes, cover.bytes) } @Test fun `fb2 extracts coverpage binary without parsing book body`() { val coverBytes = onePixelPngBytes() val fb2 = """ Folder FB2 Ursula Le Guin

Body text should not matter.

${Base64.getEncoder().encodeToString(ByteArray(0))} ${Base64.getEncoder().encodeToString(coverBytes)}
""".trimIndent() val metadata = EmbeddedEbookMetadataExtractor.extract( type = FileType.FB2, displayName = "folder.fb2", openStream = { ByteArrayInputStream(fb2.toByteArray(Charsets.UTF_8)) } ) assertEquals("Folder FB2", metadata.title) assertEquals("Ursula Le Guin", metadata.author) val cover = metadata.cover assertNotNull(cover) assertEquals("png", cover!!.extension) assertArrayEquals(coverBytes, cover.bytes) } @Test fun `mobi extracts EXTH text metadata and embedded cover record`() { val coverBytes = onePixelPngBytes() val mobiBytes = minimalMobiBytes( title = "Folder MOBI", author = "N K Jemisin", coverBytes = coverBytes ) val metadata = EmbeddedEbookMetadataExtractor.extract( type = FileType.MOBI, displayName = "folder.mobi", openStream = { ByteArrayInputStream(mobiBytes) } ) assertEquals("Folder MOBI", metadata.title) assertEquals("N K Jemisin", metadata.author) val cover = metadata.cover assertNotNull(cover) assertEquals("png", cover!!.extension) assertArrayEquals(coverBytes, cover.bytes) } private fun zipBytes(vararg entries: Pair): ByteArray { val out = ByteArrayOutputStream() ZipOutputStream(out).use { zip -> entries.forEach { (name, content) -> zip.putNextEntry(ZipEntry(name)) zip.write(content) zip.closeEntry() } } return out.toByteArray() } private fun minimalMobiBytes(title: String, author: String, coverBytes: ByteArray): ByteArray { val exthRecords = listOf( exthStringRecord(99, title), exthStringRecord(100, author), exthIntRecord(201, 0) ) val exthSize = 12 + exthRecords.sumOf { it.size } val mobiHeaderLength = 232 val record0 = ByteArray(16 + mobiHeaderLength + exthSize) putU16(record0, 0, 1) putU32(record0, 4, 0) putU16(record0, 8, 0) putU16(record0, 12, 0) putAscii(record0, 16, "MOBI") putU32(record0, 20, mobiHeaderLength) putU32(record0, 16 + 12, 65001) putU32(record0, 16 + 68, 0) putU32(record0, 16 + 72, 0) putU32(record0, 16 + 92, 1) val exthOffset = 16 + mobiHeaderLength putAscii(record0, exthOffset, "EXTH") putU32(record0, exthOffset + 4, exthSize) putU32(record0, exthOffset + 8, exthRecords.size) var cursor = exthOffset + 12 exthRecords.forEach { record -> record.copyInto(record0, cursor) cursor += record.size } val palmHeader = ByteArray(78 + 8 * 2) putU16(palmHeader, 76, 2) val record0Offset = palmHeader.size val coverOffset = record0Offset + record0.size putU32(palmHeader, 78, record0Offset) putU32(palmHeader, 86, coverOffset) return palmHeader + record0 + coverBytes } private fun exthStringRecord(type: Int, value: String): ByteArray { val data = value.toByteArray(Charsets.UTF_8) return exthRecord(type, data) } private fun exthIntRecord(type: Int, value: Int): ByteArray { val data = ByteArray(4) putU32(data, 0, value) return exthRecord(type, data) } private fun exthRecord(type: Int, data: ByteArray): ByteArray { val record = ByteArray(8 + data.size) putU32(record, 0, type) putU32(record, 4, record.size) data.copyInto(record, 8) return record } private fun putAscii(target: ByteArray, offset: Int, value: String) { value.toByteArray(Charsets.US_ASCII).copyInto(target, offset) } private fun putU16(target: ByteArray, offset: Int, value: Int) { target[offset] = ((value ushr 8) and 0xFF).toByte() target[offset + 1] = (value and 0xFF).toByte() } private fun putU32(target: ByteArray, offset: Int, value: Int) { target[offset] = ((value ushr 24) and 0xFF).toByte() target[offset + 1] = ((value ushr 16) and 0xFF).toByte() target[offset + 2] = ((value ushr 8) and 0xFF).toByte() target[offset + 3] = (value and 0xFF).toByte() } private fun onePixelPngBytes(): ByteArray { return Base64.getDecoder().decode( "iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAQAAAC1HAwCAAAAC0lEQVR42mP8/x8AAwMCAO+/p9sAAAAASUVORK5CYII=" ) } }