fixed crash when cataloguing PSD with large XMP

This commit is contained in:
Thibault Deckers 2023-05-14 22:59:03 +02:00
parent 123e7c67cb
commit 1a89a28866
7 changed files with 244 additions and 4 deletions

View file

@ -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
## <a id="v1.8.6"></a>[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

View file

@ -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) {

View file

@ -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
}
}
}
}

View file

@ -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 {

View file

@ -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
}
}

View file

@ -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)
}
}

View file

@ -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)