diff --git a/CHANGELOG.md b/CHANGELOG.md index 5c27ee26a..38ae27c17 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -17,6 +17,10 @@ All notable changes to this project will be documented in this file. - remember whether to show the title filter when picking albums - upgraded Flutter to stable v3.10.0 +### Fixed + +- crash when cataloguing PSD with large XMP + ## [v1.8.6] - 2023-04-30 ### Added @@ -859,7 +863,7 @@ All notable changes to this project will be documented in this file. ### Fixed -- fixed crash when cataloguing large MP4/PSD +- crash when cataloguing large MP4/PSD - prevent videos playing in the background when quickly switching entries ## [v1.4.0] - 2021-04-16 diff --git a/android/app/src/main/kotlin/deckers/thibault/aves/metadata/metadataextractor/Helper.kt b/android/app/src/main/kotlin/deckers/thibault/aves/metadata/metadataextractor/Helper.kt index ed247f4cf..2c71d16ed 100644 --- a/android/app/src/main/kotlin/deckers/thibault/aves/metadata/metadataextractor/Helper.kt +++ b/android/app/src/main/kotlin/deckers/thibault/aves/metadata/metadataextractor/Helper.kt @@ -67,14 +67,16 @@ object Helper { val metadata = when (fileType) { FileType.Jpeg -> safeReadJpeg(inputStream) + FileType.Mp4 -> safeReadMp4(inputStream) FileType.Png -> safeReadPng(inputStream) + FileType.Psd -> safeReadPsd(inputStream) FileType.Tiff, FileType.Arw, FileType.Cr2, FileType.Nef, FileType.Orf, FileType.Rw2 -> safeReadTiff(inputStream) - FileType.Mp4 -> safeReadMp4(inputStream) + else -> ImageMetadataReader.readMetadata(inputStream, safeReadStreamLength, fileType) } @@ -100,6 +102,10 @@ object Helper { return SafePngMetadataReader.readMetadata(input) } + private fun safeReadPsd(input: InputStream): com.drew.metadata.Metadata { + return SafePsdMetadataReader.readMetadata(input) + } + @Throws(IOException::class, TiffProcessingException::class) fun safeReadTiff(input: InputStream): com.drew.metadata.Metadata { val reader = RandomAccessStreamReader(input, RandomAccessStreamReader.DEFAULT_CHUNK_LENGTH, safeReadStreamLength) @@ -262,6 +268,7 @@ object Helper { ExifReader().extract(ByteArrayReader(dataBytes), metadata, ExifReader.JPEG_SEGMENT_PREAMBLE.length) } } + PNG_RAW_PROFILE_IPTC -> { val start = dataBytes.indexOf(Metadata.IPTC_MARKER_BYTE) if (start != -1) { diff --git a/android/app/src/main/kotlin/deckers/thibault/aves/metadata/metadataextractor/SafePhotoshopReader.kt b/android/app/src/main/kotlin/deckers/thibault/aves/metadata/metadataextractor/SafePhotoshopReader.kt new file mode 100644 index 000000000..d312569bd --- /dev/null +++ b/android/app/src/main/kotlin/deckers/thibault/aves/metadata/metadataextractor/SafePhotoshopReader.kt @@ -0,0 +1,116 @@ +package deckers.thibault.aves.metadata.metadataextractor + +import com.drew.imaging.ImageProcessingException +import com.drew.lang.ByteArrayReader +import com.drew.lang.SequentialByteArrayReader +import com.drew.lang.SequentialReader +import com.drew.metadata.Directory +import com.drew.metadata.Metadata +import com.drew.metadata.exif.ExifReader +import com.drew.metadata.icc.IccReader +import com.drew.metadata.iptc.IptcReader +import com.drew.metadata.photoshop.PhotoshopDirectory +import com.drew.metadata.photoshop.PhotoshopReader +import java.util.Arrays + +// adapted from `PhotoshopReader` to prevent OOM from reading large XMP +// as of `metadata-extractor` v2.18.0, there is no way to customize the Photoshop reader +// without copying the whole `extract` function +class SafePhotoshopReader : PhotoshopReader() { + override fun extract(reader: SequentialReader, length: Int, metadata: Metadata, parentDirectory: Directory?) { + val directory = PhotoshopDirectory() + metadata.addDirectory(directory) + + if (parentDirectory != null) { + directory.parent = parentDirectory + } + + // Data contains a sequence of Image Resource Blocks (IRBs): + // + // 4 bytes - Signature; mostly "8BIM" but "PHUT", "AgHg" and "DCSR" are also found + // 2 bytes - Resource identifier + // String - Pascal string, padded to make length even + // 4 bytes - Size of resource data which follows + // Data - The resource data, padded to make size even + // + // http://www.adobe.com/devnet-apps/photoshop/fileformatashtml/#50577409_pgfId-1037504 + + var pos = 0 + var clippingPathCount = 0 + while (pos < length) { + try { + // 4 bytes for the signature ("8BIM", "PHUT", etc.) + val signature = reader.getString(4) + pos += 4 + + // 2 bytes for the resource identifier (tag type). + val tagType = reader.uInt16 // segment type + pos += 2 + + // A variable number of bytes holding a pascal string (two leading bytes for length). + var descriptionLength = reader.uInt8.toInt() + pos += 1 + // Some basic bounds checking + if (descriptionLength < 0 || descriptionLength + pos > length) { + throw ImageProcessingException("Invalid string length") + } + + // Get name (important for paths) + val description = StringBuilder() + descriptionLength += pos + // Loop through each byte and append to string + while (pos < descriptionLength) { + description.append(Char(reader.uInt8.toUShort())) + pos++ + } + + // The number of bytes is padded with a trailing zero, if needed, to make the size even. + if (pos % 2 != 0) { + reader.skip(1) + pos++ + } + + // 4 bytes for the size of the resource data that follows. + val byteCount = reader.int32 + pos += 4 + // The resource data. + var tagBytes = reader.getBytes(byteCount) + pos += byteCount + // The number of bytes is padded with a trailing zero, if needed, to make the size even. + if (pos % 2 != 0) { + reader.skip(1) + pos++ + } + + if (signature == "8BIM") { + when (tagType) { + PhotoshopDirectory.TAG_IPTC -> IptcReader().extract(SequentialByteArrayReader(tagBytes), metadata, tagBytes.size.toLong(), directory) + PhotoshopDirectory.TAG_ICC_PROFILE_BYTES -> IccReader().extract(ByteArrayReader(tagBytes), metadata, directory) + PhotoshopDirectory.TAG_EXIF_DATA_1, + PhotoshopDirectory.TAG_EXIF_DATA_3 -> ExifReader().extract(ByteArrayReader(tagBytes), metadata, 0, directory) + + PhotoshopDirectory.TAG_XMP_DATA -> SafeXmpReader().extract(tagBytes, metadata, directory) + in 0x07D0..0x0BB6 -> { + clippingPathCount++ + tagBytes = Arrays.copyOf(tagBytes, tagBytes.size + description.length + 1) + // Append description(name) to end of byte array with 1 byte before the description representing the length + for (i in tagBytes.size - description.length - 1 until tagBytes.size) { + if (i % (tagBytes.size - description.length - 1 + description.length) == 0) tagBytes[i] = description.length.toByte() else tagBytes[i] = description[i - (tagBytes.size - description.length - 1)].code.toByte() + } +// PhotoshopDirectory._tagNameMap[0x07CF + clippingPathCount] = "Path Info $clippingPathCount" + directory.setByteArray(0x07CF + clippingPathCount, tagBytes) + } + + else -> directory.setByteArray(tagType, tagBytes) + } +// if (tagType in 0x0fa0..0x1387) { +// PhotoshopDirectory._tagNameMap[tagType] = String.format("Plug-in %d Data", tagType - 0x0fa0 + 1) +// } + } + } catch (ex: Exception) { + directory.addError(ex.message) + return + } + } + } +} \ No newline at end of file diff --git a/android/app/src/main/kotlin/deckers/thibault/aves/metadata/metadataextractor/SafePngMetadataReader.kt b/android/app/src/main/kotlin/deckers/thibault/aves/metadata/metadataextractor/SafePngMetadataReader.kt index 1012a6477..d8b5452a4 100644 --- a/android/app/src/main/kotlin/deckers/thibault/aves/metadata/metadataextractor/SafePngMetadataReader.kt +++ b/android/app/src/main/kotlin/deckers/thibault/aves/metadata/metadataextractor/SafePngMetadataReader.kt @@ -34,7 +34,7 @@ import java.io.InputStream import java.util.zip.InflaterInputStream import java.util.zip.ZipException -// adapted from `PngMetadataReader` to prevent reading OOM from large chunks +// adapted from `PngMetadataReader` to prevent OOM from reading large chunks // as of `metadata-extractor` v2.18.0, there is no way to customize the reader // without copying `desiredChunkTypes` and the whole `processChunk` function object SafePngMetadataReader { diff --git a/android/app/src/main/kotlin/deckers/thibault/aves/metadata/metadataextractor/SafePsdMetadataReader.kt b/android/app/src/main/kotlin/deckers/thibault/aves/metadata/metadataextractor/SafePsdMetadataReader.kt new file mode 100644 index 000000000..6f93a23f8 --- /dev/null +++ b/android/app/src/main/kotlin/deckers/thibault/aves/metadata/metadataextractor/SafePsdMetadataReader.kt @@ -0,0 +1,13 @@ +package deckers.thibault.aves.metadata.metadataextractor + +import com.drew.lang.StreamReader +import com.drew.metadata.Metadata +import java.io.InputStream + +object SafePsdMetadataReader { + fun readMetadata(inputStream: InputStream): Metadata { + val metadata = Metadata() + SafePsdReader().extract(StreamReader(inputStream), metadata) + return metadata + } +} \ No newline at end of file diff --git a/android/app/src/main/kotlin/deckers/thibault/aves/metadata/metadataextractor/SafePsdReader.kt b/android/app/src/main/kotlin/deckers/thibault/aves/metadata/metadataextractor/SafePsdReader.kt new file mode 100644 index 000000000..91dcf8444 --- /dev/null +++ b/android/app/src/main/kotlin/deckers/thibault/aves/metadata/metadataextractor/SafePsdReader.kt @@ -0,0 +1,93 @@ +package deckers.thibault.aves.metadata.metadataextractor + +import com.drew.lang.SequentialReader +import com.drew.metadata.Metadata +import com.drew.metadata.photoshop.PsdHeaderDirectory +import java.io.IOException + +// adapted from `PsdReader` to prevent OOM from reading large XMP +// as of `metadata-extractor` v2.18.0, there is no way to customize the Photoshop reader +// without copying the whole `extract` function +class SafePsdReader { + fun extract(reader: SequentialReader, metadata: Metadata) { + val directory = PsdHeaderDirectory() + metadata.addDirectory(directory) + + // FILE HEADER SECTION + + try { + val signature = reader.int32 + if (signature != 0x38425053) // "8BPS" + { + directory.addError("Invalid PSD file signature") + return + } + + val version = reader.uInt16 + if (version != 1 && version != 2) { + directory.addError("Invalid PSD file version (must be 1 or 2)") + return + } + + // 6 reserved bytes are skipped here. They should be zero. + reader.skip(6) + + val channelCount = reader.uInt16 + directory.setInt(PsdHeaderDirectory.TAG_CHANNEL_COUNT, channelCount) + + // even though this is probably an unsigned int, the max height in practice is 300,000 + val imageHeight = reader.int32 + directory.setInt(PsdHeaderDirectory.TAG_IMAGE_HEIGHT, imageHeight) + + // even though this is probably an unsigned int, the max width in practice is 300,000 + val imageWidth = reader.int32 + directory.setInt(PsdHeaderDirectory.TAG_IMAGE_WIDTH, imageWidth) + + val bitsPerChannel = reader.uInt16 + directory.setInt(PsdHeaderDirectory.TAG_BITS_PER_CHANNEL, bitsPerChannel) + + val colorMode = reader.uInt16 + directory.setInt(PsdHeaderDirectory.TAG_COLOR_MODE, colorMode) + } catch (e: IOException) { + directory.addError("Unable to read PSD header") + return + } + + // COLOR MODE DATA SECTION + + try { + val sectionLength = reader.uInt32 + + /* + * Only indexed color and duotone (see the mode field in the File header section) have color mode data. + * For all other modes, this section is just the 4-byte length field, which is set to zero. + * + * Indexed color images: length is 768; color data contains the color table for the image, + * in non-interleaved order. + * Duotone images: color data contains the duotone specification (the format of which is not documented). + * Other applications that read Photoshop files can treat a duotone image as a gray image, + * and just preserve the contents of the duotone information when reading and writing the + * file. + */ + reader.skip(sectionLength) + } catch (e: IOException) { + return + } + + // IMAGE RESOURCES SECTION + + try { + val sectionLength = reader.uInt32 + + assert(sectionLength <= Int.MAX_VALUE) + + SafePhotoshopReader().extract(reader, sectionLength.toInt(), metadata) + } catch (e: IOException) { + // ignore + } + + // LAYER AND MASK INFORMATION SECTION (skipped) + + // IMAGE DATA SECTION (skipped) + } +} \ No newline at end of file diff --git a/android/app/src/main/kotlin/deckers/thibault/aves/metadata/metadataextractor/SafeXmpReader.kt b/android/app/src/main/kotlin/deckers/thibault/aves/metadata/metadataextractor/SafeXmpReader.kt index 8eb67202e..e36402907 100644 --- a/android/app/src/main/kotlin/deckers/thibault/aves/metadata/metadataextractor/SafeXmpReader.kt +++ b/android/app/src/main/kotlin/deckers/thibault/aves/metadata/metadataextractor/SafeXmpReader.kt @@ -61,12 +61,19 @@ class SafeXmpReader : XmpReader() { } // adapted from `XmpReader` to provide different parsing options + // and to detect large XMP when extracted directly (e.g. from Photoshop reader) override fun extract(xmpBytes: ByteArray, offset: Int, length: Int, metadata: Metadata, parentDirectory: Directory?) { + val totalSize = xmpBytes.size + if (totalSize > SEGMENT_TYPE_SIZE_DANGER_THRESHOLD) { + logError(metadata, totalSize) + return + } + val directory = XmpDirectory() if (parentDirectory != null) directory.parent = parentDirectory try { - val xmpMeta: XMPMeta = if (offset == 0 && length == xmpBytes.size) { + val xmpMeta: XMPMeta = if (offset == 0 && length == totalSize) { XMPMetaFactory.parseFromBuffer(xmpBytes, PARSE_OPTIONS) } else { val buffer = ByteBuffer(xmpBytes, offset, length)