#1084 crashfix for png with large exif via ExifInterface,

fixed ExifInterface disambiguation,
improved safe mode
This commit is contained in:
Thibault Deckers 2024-07-17 21:11:36 +02:00
parent a38c5b72ee
commit 2d143f139f
22 changed files with 75 additions and 55 deletions

View file

@ -211,6 +211,7 @@ dependencies {
implementation 'com.github.deckerst.mp4parser:isoparser:4cc0c5d06c'
implementation 'com.github.deckerst.mp4parser:muxer:4cc0c5d06c'
implementation 'com.github.deckerst:pixymeta-android:9ec7097f17'
implementation project(':exifinterface')
testImplementation 'org.junit.jupiter:junit-jupiter-engine:5.10.2'

View file

@ -12,7 +12,7 @@ import android.os.Handler
import android.os.Looper
import android.provider.MediaStore
import android.util.Log
import androidx.exifinterface.media.ExifInterface
import androidx.exifinterface.media.ExifInterfaceFork as ExifInterface
import com.drew.metadata.file.FileTypeDirectory
import deckers.thibault.aves.channel.calls.Coresult.Companion.safe
import deckers.thibault.aves.metadata.ExifInterfaceHelper

View file

@ -6,7 +6,7 @@ import android.media.MediaMetadataRetriever
import android.net.Uri
import android.os.Build
import android.util.Log
import androidx.exifinterface.media.ExifInterface
import androidx.exifinterface.media.ExifInterfaceFork as ExifInterface
import com.adobe.internal.xmp.XMPException
import com.adobe.internal.xmp.XMPMeta
import com.adobe.internal.xmp.XMPMetaFactory

View file

@ -21,11 +21,13 @@ class MediaStoreStreamHandler(private val context: Context, arguments: Any?) : E
private var knownEntries: Map<Long?, Int?>? = null
private var directory: String? = null
private var safe: Boolean = false
init {
if (arguments is Map<*, *>) {
knownEntries = (arguments["knownEntries"] as? Map<*, *>?)?.map { (it.key as Number?)?.toLong() to it.value as Int? }?.toMap()
directory = arguments["directory"] as String?
safe = arguments.getOrDefault("safe", false) as Boolean
}
}
@ -59,7 +61,7 @@ class MediaStoreStreamHandler(private val context: Context, arguments: Any?) : E
}
private fun fetchAll() {
MediaStoreImageProvider().fetchAll(context, knownEntries ?: emptyMap(), directory) { success(it) }
MediaStoreImageProvider().fetchAll(context, knownEntries ?: emptyMap(), directory, safe) { success(it) }
endOfStream()
}

View file

@ -1,7 +1,7 @@
package deckers.thibault.aves.metadata
import android.util.Log
import androidx.exifinterface.media.ExifInterface
import androidx.exifinterface.media.ExifInterfaceFork as ExifInterface
import com.drew.lang.Rational
import com.drew.metadata.Directory
import com.drew.metadata.exif.ExifDirectoryBase

View file

@ -2,7 +2,7 @@ package deckers.thibault.aves.metadata
import android.content.Context
import android.net.Uri
import androidx.exifinterface.media.ExifInterface
import androidx.exifinterface.media.ExifInterfaceFork as ExifInterface
import deckers.thibault.aves.utils.FileUtils.transferFrom
import deckers.thibault.aves.utils.MimeTypes
import deckers.thibault.aves.utils.StorageUtils

View file

@ -9,7 +9,7 @@ import android.net.Uri
import android.os.Build
import android.os.ParcelFileDescriptor
import android.util.Log
import androidx.exifinterface.media.ExifInterface
import androidx.exifinterface.media.ExifInterfaceFork as ExifInterface
import com.adobe.internal.xmp.XMPMeta
import com.drew.imaging.jpeg.JpegSegmentType
import com.drew.metadata.exif.ExifDirectoryBase

View file

@ -5,7 +5,7 @@ import android.content.Context
import android.graphics.BitmapFactory
import android.media.MediaMetadataRetriever
import android.net.Uri
import androidx.exifinterface.media.ExifInterface
import androidx.exifinterface.media.ExifInterfaceFork as ExifInterface
import com.drew.metadata.avi.AviDirectory
import com.drew.metadata.exif.ExifIFD0Directory
import com.drew.metadata.jpeg.JpegDirectory
@ -116,8 +116,8 @@ class SourceEntry {
// metadata retrieval
// expects entry with: uri, mimeType
// finds: width, height, orientation/rotation, date, title, duration
fun fillPreCatalogMetadata(context: Context): SourceEntry {
if (isSvg) return this
fun fillPreCatalogMetadata(context: Context, safe: Boolean): SourceEntry {
if (isSvg || safe) return this
if (isVideo) {
fillVideoByMediaMetadataRetriever(context)
if (isSized && hasDuration) return this

View file

@ -52,7 +52,7 @@ internal class FileImageProvider : ImageProvider() {
callback.onFailure(e)
}
}
entry.fillPreCatalogMetadata(context)
entry.fillPreCatalogMetadata(context, safe = false)
if (allowUnsized || entry.isSized || entry.isSvg || entry.isVideo) {
callback.onSuccess(entry.toMap())

View file

@ -11,7 +11,7 @@ import android.net.Uri
import android.os.Binder
import android.os.Build
import android.util.Log
import androidx.exifinterface.media.ExifInterface
import androidx.exifinterface.media.ExifInterfaceFork as ExifInterface
import com.bumptech.glide.Glide
import com.bumptech.glide.load.DecodeFormat
import com.bumptech.glide.load.engine.DiskCacheStrategy

View file

@ -51,8 +51,10 @@ class MediaStoreImageProvider : ImageProvider() {
context: Context,
knownEntries: Map<Long?, Int?>,
directory: String?,
safe: Boolean,
handleNewEntry: NewEntryHandler,
) {
Log.d(LOG_TAG, "fetching all media store items for ${knownEntries.size} known entries, directory=$directory safe=$safe")
val isModified = fun(contentId: Long, dateModifiedSecs: Int): Boolean {
val knownDate = knownEntries[contentId]
return knownDate == null || knownDate < dateModifiedSecs
@ -82,8 +84,8 @@ class MediaStoreImageProvider : ImageProvider() {
} else {
handleNew = handleNewEntry
}
fetchFrom(context, isModified, handleNew, IMAGE_CONTENT_URI, IMAGE_PROJECTION, selection, selectionArgs)
fetchFrom(context, isModified, handleNew, VIDEO_CONTENT_URI, VIDEO_PROJECTION, selection, selectionArgs)
fetchFrom(context, isModified, handleNew, IMAGE_CONTENT_URI, IMAGE_PROJECTION, selection, selectionArgs, safe = safe)
fetchFrom(context, isModified, handleNew, VIDEO_CONTENT_URI, VIDEO_PROJECTION, selection, selectionArgs, safe = safe)
}
// the provided URI can point to the wrong media collection,
@ -206,6 +208,7 @@ class MediaStoreImageProvider : ImageProvider() {
selection: String? = null,
selectionArgs: Array<String>? = null,
fileMimeType: String? = null,
safe: Boolean = false,
): Boolean {
var found = false
val orderBy = "${MediaStore.MediaColumns.DATE_MODIFIED} DESC"
@ -299,7 +302,7 @@ class MediaStoreImageProvider : ImageProvider() {
// missing some attributes such as width, height, orientation.
// Also, the reported size of raw images is inconsistent across devices
// and Android versions (sometimes the raw size, sometimes the decoded size).
val entry = SourceEntry(entryMap).fillPreCatalogMetadata(context)
val entry = SourceEntry(entryMap).fillPreCatalogMetadata(context, safe)
entryMap = entry.toMap()
}

View file

@ -70,7 +70,7 @@ open class UnknownContentProvider : ImageProvider() {
return
}
val entry = SourceEntry(fields).fillPreCatalogMetadata(context)
val entry = SourceEntry(fields).fillPreCatalogMetadata(context, safe = false)
if (allowUnsized || entry.isSized || entry.isSvg || entry.isVideo) {
callback.onSuccess(entry.toMap())
} else {

View file

@ -1,7 +1,7 @@
package deckers.thibault.aves.utils
import android.webkit.MimeTypeMap
import androidx.exifinterface.media.ExifInterface
import androidx.exifinterface.media.ExifInterfaceFork as ExifInterface
import deckers.thibault.aves.decoder.MultiPageImage
object MimeTypes {

View file

@ -16,12 +16,12 @@
package androidx.exifinterface.media;
import static androidx.exifinterface.media.ExifInterfaceUtils.closeFileDescriptor;
import static androidx.exifinterface.media.ExifInterfaceUtils.closeQuietly;
import static androidx.exifinterface.media.ExifInterfaceUtils.convertToLongArray;
import static androidx.exifinterface.media.ExifInterfaceUtils.copy;
import static androidx.exifinterface.media.ExifInterfaceUtils.parseSubSeconds;
import static androidx.exifinterface.media.ExifInterfaceUtils.startsWith;
import static androidx.exifinterface.media.ExifInterfaceUtilsFork.closeFileDescriptor;
import static androidx.exifinterface.media.ExifInterfaceUtilsFork.closeQuietly;
import static androidx.exifinterface.media.ExifInterfaceUtilsFork.convertToLongArray;
import static androidx.exifinterface.media.ExifInterfaceUtilsFork.copy;
import static androidx.exifinterface.media.ExifInterfaceUtilsFork.parseSubSeconds;
import static androidx.exifinterface.media.ExifInterfaceUtilsFork.startsWith;
import static java.nio.ByteOrder.BIG_ENDIAN;
import static java.nio.ByteOrder.LITTLE_ENDIAN;
@ -41,8 +41,8 @@ import androidx.annotation.IntDef;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.annotation.RestrictTo;
import androidx.exifinterface.media.ExifInterfaceUtils.Api21Impl;
import androidx.exifinterface.media.ExifInterfaceUtils.Api23Impl;
import androidx.exifinterface.media.ExifInterfaceUtilsFork.Api21Impl;
import androidx.exifinterface.media.ExifInterfaceUtilsFork.Api23Impl;
import java.io.BufferedInputStream;
import java.io.BufferedOutputStream;
@ -84,6 +84,7 @@ import java.util.zip.CRC32;
/*
* Forked from 'androidx.exifinterface:exifinterface:1.3.7' on 2024/02/21
* Named differently to let ExifInterface be loaded as subdependency.
*/
/**
@ -97,7 +98,7 @@ import java.util.zip.CRC32;
* it. This class will search both locations for XMP data, but if XMP data exist both inside and
* outside Exif, will favor the XMP data inside Exif over the one outside.
*/
public class ExifInterface {
public class ExifInterfaceFork {
// TLAD threshold for safer Exif attribute parsing
private static final int ATTRIBUTE_SIZE_DANGER_THRESHOLD = 3 * (1 << 20); // MB
@ -3949,7 +3950,7 @@ public class ExifInterface {
* @throws IOException if an I/O error occurs while retrieving file descriptor via
* {@link FileInputStream#getFD()}.
*/
public ExifInterface(@NonNull File file) throws IOException {
public ExifInterfaceFork(@NonNull File file) throws IOException {
if (file == null) {
throw new NullPointerException("file cannot be null");
}
@ -3964,7 +3965,7 @@ public class ExifInterface {
* @throws IOException if an I/O error occurs while retrieving file descriptor via
* {@link FileInputStream#getFD()}.
*/
public ExifInterface(@NonNull String filename) throws IOException {
public ExifInterfaceFork(@NonNull String filename) throws IOException {
if (filename == null) {
throw new NullPointerException("filename cannot be null");
}
@ -3980,7 +3981,7 @@ public class ExifInterface {
* @throws NullPointerException if file descriptor is null
* @throws IOException if an error occurs while duplicating the file descriptor.
*/
public ExifInterface(@NonNull FileDescriptor fileDescriptor) throws IOException {
public ExifInterfaceFork(@NonNull FileDescriptor fileDescriptor) throws IOException {
if (fileDescriptor == null) {
throw new NullPointerException("fileDescriptor cannot be null");
}
@ -4023,7 +4024,7 @@ public class ExifInterface {
* @param inputStream the input stream that contains the image data
* @throws NullPointerException if the input stream is null
*/
public ExifInterface(@NonNull InputStream inputStream) throws IOException {
public ExifInterfaceFork(@NonNull InputStream inputStream) throws IOException {
this(inputStream, STREAM_TYPE_FULL_IMAGE_DATA);
}
@ -4039,7 +4040,7 @@ public class ExifInterface {
* @throws IOException if an I/O error occurs while retrieving file descriptor via
* {@link FileInputStream#getFD()}.
*/
public ExifInterface(@NonNull InputStream inputStream, @ExifStreamType int streamType)
public ExifInterfaceFork(@NonNull InputStream inputStream, @ExifStreamType int streamType)
throws IOException {
if (inputStream == null) {
throw new NullPointerException("inputStream cannot be null");
@ -5071,7 +5072,7 @@ public class ExifInterface {
if (location == null) {
return;
}
setAttribute(ExifInterface.TAG_GPS_PROCESSING_METHOD, location.getProvider());
setAttribute(ExifInterfaceFork.TAG_GPS_PROCESSING_METHOD, location.getProvider());
setLatLong(location.getLatitude(), location.getLongitude());
setAltitude(location.getAltitude());
// Location objects store speeds in m/sec. Translates it to km/hr here.
@ -5080,8 +5081,8 @@ public class ExifInterface {
* TimeUnit.HOURS.toSeconds(1) / 1000).toString());
String[] dateTime = sFormatterPrimary.format(
new Date(location.getTime())).split("\\s+", -1);
setAttribute(ExifInterface.TAG_GPS_DATESTAMP, dateTime[0]);
setAttribute(ExifInterface.TAG_GPS_TIMESTAMP, dateTime[1]);
setAttribute(ExifInterfaceFork.TAG_GPS_DATESTAMP, dateTime[0]);
setAttribute(ExifInterfaceFork.TAG_GPS_TIMESTAMP, dateTime[1]);
}
/**
@ -5158,11 +5159,11 @@ public class ExifInterface {
}
/**
* Returns parsed {@link ExifInterface#TAG_DATETIME} value as number of milliseconds since
* Returns parsed {@link ExifInterfaceFork#TAG_DATETIME} value as number of milliseconds since
* Jan. 1, 1970, midnight local time.
*
* <p>Note: The return value includes the first three digits (or less depending on the length
* of the string) of {@link ExifInterface#TAG_SUBSEC_TIME}.
* of the string) of {@link ExifInterfaceFork#TAG_SUBSEC_TIME}.
*
* @return null if date time information is unavailable or invalid.
*/
@ -5175,11 +5176,11 @@ public class ExifInterface {
}
/**
* Returns parsed {@link ExifInterface#TAG_DATETIME_DIGITIZED} value as number of
* Returns parsed {@link ExifInterfaceFork#TAG_DATETIME_DIGITIZED} value as number of
* milliseconds since Jan. 1, 1970, midnight local time.
*
* <p>Note: The return value includes the first three digits (or less depending on the length
* of the string) of {@link ExifInterface#TAG_SUBSEC_TIME_DIGITIZED}.
* of the string) of {@link ExifInterfaceFork#TAG_SUBSEC_TIME_DIGITIZED}.
*
* @return null if digitized date time information is unavailable or invalid.
*/
@ -5192,11 +5193,11 @@ public class ExifInterface {
}
/**
* Returns parsed {@link ExifInterface#TAG_DATETIME_ORIGINAL} value as number of
* Returns parsed {@link ExifInterfaceFork#TAG_DATETIME_ORIGINAL} value as number of
* milliseconds since Jan. 1, 1970, midnight local time.
*
* <p>Note: The return value includes the first three digits (or less depending on the length
* of the string) of {@link ExifInterface#TAG_SUBSEC_TIME_ORIGINAL}.
* of the string) of {@link ExifInterfaceFork#TAG_SUBSEC_TIME_ORIGINAL}.
*
* @return null if original date time information is unavailable or invalid.
*/
@ -5910,18 +5911,18 @@ public class ExifInterface {
}
if (rotation != null) {
int orientation = ExifInterface.ORIENTATION_NORMAL;
int orientation = ExifInterfaceFork.ORIENTATION_NORMAL;
// all rotation angles in CW
switch (Integer.parseInt(rotation)) {
case 90:
orientation = ExifInterface.ORIENTATION_ROTATE_90;
orientation = ExifInterfaceFork.ORIENTATION_ROTATE_90;
break;
case 180:
orientation = ExifInterface.ORIENTATION_ROTATE_180;
orientation = ExifInterfaceFork.ORIENTATION_ROTATE_180;
break;
case 270:
orientation = ExifInterface.ORIENTATION_ROTATE_270;
orientation = ExifInterfaceFork.ORIENTATION_ROTATE_270;
break;
}
@ -6175,7 +6176,11 @@ public class ExifInterface {
// IEND marks the end of the image.
break;
} else if (Arrays.equals(type, PNG_CHUNK_TYPE_EXIF)) {
// TODO: Need to handle potential OutOfMemoryError
// TLAD start
if (length > ATTRIBUTE_SIZE_DANGER_THRESHOLD) {
throw new IOException("dangerous exif chunk size=" + length);
}
// TLAD end
byte[] data = new byte[length];
in.readFully(data);
@ -6976,9 +6981,11 @@ public class ExifInterface {
}
final int bytesOffset = dataInputStream.position() + mOffsetToExifData;
if (byteCount > 0 && byteCount < ATTRIBUTE_SIZE_DANGER_THRESHOLD) {
// TLAD start
if (byteCount > ATTRIBUTE_SIZE_DANGER_THRESHOLD) {
throw new IOException("dangerous attribute size=" + byteCount);
}
// TLAD end
final byte[] bytes = new byte[(int) byteCount];
dataInputStream.readFully(bytes);
ExifAttribute attribute = new ExifAttribute(dataFormat, numberOfComponents,

View file

@ -32,10 +32,10 @@ import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
class ExifInterfaceUtils {
class ExifInterfaceUtilsFork {
private static final String TAG = "ExifInterfaceUtils";
private ExifInterfaceUtils() {
private ExifInterfaceUtilsFork() {
// Prevent instantiation
}
/**

View file

@ -10,7 +10,7 @@ pluginManagement {
settings.ext.kotlin_version = '1.9.24'
settings.ext.ksp_version = "$kotlin_version-1.0.20"
settings.ext.agp_version = '8.5.0'
settings.ext.agp_version = '8.5.1'
includeBuild("${settings.ext.flutterSdkPath}/packages/flutter_tools/gradle")

View file

@ -93,6 +93,8 @@ abstract class CollectionSource with SourceBase, AlbumMixin, CountryMixin, Place
_rawEntries.forEach((v) => v.dispose());
}
set safeMode(bool enabled);
final EventBus _eventBus = EventBus();
@override

View file

@ -23,6 +23,10 @@ class MediaStoreSource extends CollectionSource {
final Set<String> _changedUris = {};
int? _lastGeneration;
SourceInitializationState _initState = SourceInitializationState.none;
bool _safeMode = false;
@override
set safeMode(bool enabled) => _safeMode = enabled;
@override
SourceInitializationState get initState => _initState;
@ -46,7 +50,7 @@ class MediaStoreSource extends CollectionSource {
analysisController: analysisController,
directory: directory,
loadTopEntriesFirst: loadTopEntriesFirst,
canAnalyze: canAnalyze,
canAnalyze: canAnalyze && _safeMode,
));
}
@ -175,7 +179,7 @@ class MediaStoreSource extends CollectionSource {
pendingNewEntries.clear();
}
mediaStoreService.getEntries(knownDateByContentId, directory: directory).listen(
mediaStoreService.getEntries(_safeMode, knownDateByContentId, directory: directory).listen(
(entry) {
// when discovering modified entry with known content ID,
// reuse known entry ID to overwrite it while preserving favourites, etc.

View file

@ -15,7 +15,7 @@ abstract class MediaStoreService {
Future<int?> getGeneration();
// knownEntries: map of contentId -> dateModifiedSecs
Stream<AvesEntry> getEntries(Map<int?, int?> knownEntries, {String? directory});
Stream<AvesEntry> getEntries(bool safe, Map<int?, int?> knownEntries, {String? directory});
// returns media URI
Future<Uri?> scanFile(String path, String mimeType);
@ -75,12 +75,13 @@ class PlatformMediaStoreService implements MediaStoreService {
}
@override
Stream<AvesEntry> getEntries(Map<int?, int?> knownEntries, {String? directory}) {
Stream<AvesEntry> getEntries(bool safe, Map<int?, int?> knownEntries, {String? directory}) {
try {
return _stream
.receiveBroadcastStream(<String, dynamic>{
'knownEntries': knownEntries,
'directory': directory,
'safe': safe,
})
.where((event) => event is Map)
.map((event) => AvesEntry.fromMap(event as Map));

View file

@ -203,10 +203,10 @@ class _HomePageState extends State<HomePage> {
unawaited(GlobalSearch.registerCallback());
unawaited(AnalysisService.registerCallback());
final source = context.read<CollectionSource>();
source.safeMode = safeMode;
if (source.initState != SourceInitializationState.full) {
await source.init(
loadTopEntriesFirst: settings.homePage == HomePageSetting.collection && settings.homeCustomCollection.isEmpty,
canAnalyze: !safeMode,
);
}
case AppMode.screenSaver:

View file

@ -4,7 +4,7 @@ version '1.0-SNAPSHOT'
buildscript {
ext {
kotlin_version = '1.9.24'
agp_version = '8.5.0'
agp_version = '8.5.1'
}
repositories {

View file

@ -33,7 +33,7 @@ class FakeMediaStoreService extends Fake implements MediaStoreService {
}
@override
Stream<AvesEntry> getEntries(Map<int?, int?> knownEntries, {String? directory}) => Stream.fromIterable(entries);
Stream<AvesEntry> getEntries(bool safe, Map<int?, int?> knownEntries, {String? directory}) => Stream.fromIterable(entries);
static var _lastId = 1;