fixed crash when cataloguing PSD with large XMP
This commit is contained in:
parent
123e7c67cb
commit
1a89a28866
7 changed files with 244 additions and 4 deletions
|
@ -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
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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 {
|
||||
|
|
|
@ -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
|
||||
}
|
||||
}
|
|
@ -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)
|
||||
}
|
||||
}
|
|
@ -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)
|
||||
|
|
Loading…
Reference in a new issue