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
|
- remember whether to show the title filter when picking albums
|
||||||
- upgraded Flutter to stable v3.10.0
|
- 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
|
## <a id="v1.8.6"></a>[v1.8.6] - 2023-04-30
|
||||||
|
|
||||||
### Added
|
### Added
|
||||||
|
@ -859,7 +863,7 @@ All notable changes to this project will be documented in this file.
|
||||||
|
|
||||||
### Fixed
|
### Fixed
|
||||||
|
|
||||||
- fixed crash when cataloguing large MP4/PSD
|
- crash when cataloguing large MP4/PSD
|
||||||
- prevent videos playing in the background when quickly switching entries
|
- prevent videos playing in the background when quickly switching entries
|
||||||
|
|
||||||
## [v1.4.0] - 2021-04-16
|
## [v1.4.0] - 2021-04-16
|
||||||
|
|
|
@ -67,14 +67,16 @@ object Helper {
|
||||||
|
|
||||||
val metadata = when (fileType) {
|
val metadata = when (fileType) {
|
||||||
FileType.Jpeg -> safeReadJpeg(inputStream)
|
FileType.Jpeg -> safeReadJpeg(inputStream)
|
||||||
|
FileType.Mp4 -> safeReadMp4(inputStream)
|
||||||
FileType.Png -> safeReadPng(inputStream)
|
FileType.Png -> safeReadPng(inputStream)
|
||||||
|
FileType.Psd -> safeReadPsd(inputStream)
|
||||||
FileType.Tiff,
|
FileType.Tiff,
|
||||||
FileType.Arw,
|
FileType.Arw,
|
||||||
FileType.Cr2,
|
FileType.Cr2,
|
||||||
FileType.Nef,
|
FileType.Nef,
|
||||||
FileType.Orf,
|
FileType.Orf,
|
||||||
FileType.Rw2 -> safeReadTiff(inputStream)
|
FileType.Rw2 -> safeReadTiff(inputStream)
|
||||||
FileType.Mp4 -> safeReadMp4(inputStream)
|
|
||||||
else -> ImageMetadataReader.readMetadata(inputStream, safeReadStreamLength, fileType)
|
else -> ImageMetadataReader.readMetadata(inputStream, safeReadStreamLength, fileType)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -100,6 +102,10 @@ object Helper {
|
||||||
return SafePngMetadataReader.readMetadata(input)
|
return SafePngMetadataReader.readMetadata(input)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private fun safeReadPsd(input: InputStream): com.drew.metadata.Metadata {
|
||||||
|
return SafePsdMetadataReader.readMetadata(input)
|
||||||
|
}
|
||||||
|
|
||||||
@Throws(IOException::class, TiffProcessingException::class)
|
@Throws(IOException::class, TiffProcessingException::class)
|
||||||
fun safeReadTiff(input: InputStream): com.drew.metadata.Metadata {
|
fun safeReadTiff(input: InputStream): com.drew.metadata.Metadata {
|
||||||
val reader = RandomAccessStreamReader(input, RandomAccessStreamReader.DEFAULT_CHUNK_LENGTH, safeReadStreamLength)
|
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)
|
ExifReader().extract(ByteArrayReader(dataBytes), metadata, ExifReader.JPEG_SEGMENT_PREAMBLE.length)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
PNG_RAW_PROFILE_IPTC -> {
|
PNG_RAW_PROFILE_IPTC -> {
|
||||||
val start = dataBytes.indexOf(Metadata.IPTC_MARKER_BYTE)
|
val start = dataBytes.indexOf(Metadata.IPTC_MARKER_BYTE)
|
||||||
if (start != -1) {
|
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.InflaterInputStream
|
||||||
import java.util.zip.ZipException
|
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
|
// as of `metadata-extractor` v2.18.0, there is no way to customize the reader
|
||||||
// without copying `desiredChunkTypes` and the whole `processChunk` function
|
// without copying `desiredChunkTypes` and the whole `processChunk` function
|
||||||
object SafePngMetadataReader {
|
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
|
// 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?) {
|
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()
|
val directory = XmpDirectory()
|
||||||
if (parentDirectory != null) directory.parent = parentDirectory
|
if (parentDirectory != null) directory.parent = parentDirectory
|
||||||
|
|
||||||
try {
|
try {
|
||||||
val xmpMeta: XMPMeta = if (offset == 0 && length == xmpBytes.size) {
|
val xmpMeta: XMPMeta = if (offset == 0 && length == totalSize) {
|
||||||
XMPMetaFactory.parseFromBuffer(xmpBytes, PARSE_OPTIONS)
|
XMPMetaFactory.parseFromBuffer(xmpBytes, PARSE_OPTIONS)
|
||||||
} else {
|
} else {
|
||||||
val buffer = ByteBuffer(xmpBytes, offset, length)
|
val buffer = ByteBuffer(xmpBytes, offset, length)
|
||||||
|
|
Loading…
Reference in a new issue