safer svg parsing
This commit is contained in:
parent
eb3acaa307
commit
7d05fb6aef
11 changed files with 77 additions and 40 deletions
|
@ -68,6 +68,7 @@ class MediaFetchBytesHandler(private val context: Context) : MethodCallHandler {
|
|||
val uri = call.argument<String>("uri")?.let { Uri.parse(it) }
|
||||
val mimeType = call.argument<String>("mimeType")
|
||||
val pageId = call.argument<Int>("pageId")
|
||||
val sizeBytes = call.argument<Number>("sizeBytes")?.toLong()
|
||||
val sampleSize = call.argument<Int>("sampleSize")
|
||||
val x = call.argument<Int>("regionX")
|
||||
val y = call.argument<Int>("regionY")
|
||||
|
@ -85,6 +86,7 @@ class MediaFetchBytesHandler(private val context: Context) : MethodCallHandler {
|
|||
when (mimeType) {
|
||||
MimeTypes.SVG -> SvgRegionFetcher(context).fetch(
|
||||
uri = uri,
|
||||
sizeBytes = sizeBytes,
|
||||
regionRect = regionRect,
|
||||
imageWidth = imageWidth,
|
||||
imageHeight = imageHeight,
|
||||
|
|
|
@ -23,11 +23,21 @@ class SvgRegionFetcher internal constructor(
|
|||
|
||||
suspend fun fetch(
|
||||
uri: Uri,
|
||||
sizeBytes: Long?,
|
||||
regionRect: Rect,
|
||||
imageWidth: Int,
|
||||
imageHeight: Int,
|
||||
result: MethodChannel.Result,
|
||||
) {
|
||||
if (sizeBytes != null && sizeBytes > FILE_SIZE_DANGER_THRESHOLD) {
|
||||
val availableHeapSize = Runtime.getRuntime().let { it.maxMemory() - (it.totalMemory() - it.freeMemory()) }
|
||||
if (sizeBytes > availableHeapSize) {
|
||||
// opening an SVG that large would yield an OOM during parsing from `com.caverock.androidsvg.SVGParser`
|
||||
result.error("fetch-read-large", "SVG too large at $sizeBytes bytes, with only $availableHeapSize free bytes, for uri=$uri regionRect=$regionRect", null)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
var currentSvgRef = lastSvgRef
|
||||
if (currentSvgRef != null && currentSvgRef.uri != uri) {
|
||||
currentSvgRef = null
|
||||
|
@ -103,4 +113,9 @@ class SvgRegionFetcher internal constructor(
|
|||
val uri: Uri,
|
||||
val svg: SVG,
|
||||
)
|
||||
|
||||
companion object {
|
||||
// arbitrary size to detect files that may yield an OOM
|
||||
private const val FILE_SIZE_DANGER_THRESHOLD = 10 * (1 shl 20) // MB
|
||||
}
|
||||
}
|
||||
|
|
|
@ -117,16 +117,16 @@ object Metadata {
|
|||
return date.time + parseSubSecond(subSecond)
|
||||
}
|
||||
|
||||
// opening large PSD/TIFF files yields an OOM (both with `metadata-extractor` v2.15.0 and `ExifInterface` v1.3.1),
|
||||
// Opening large PSD/TIFF files yields an OOM (both with `metadata-extractor` v2.15.0 and `ExifInterface` v1.3.1),
|
||||
// so we define an arbitrary threshold to avoid a crash on launch.
|
||||
// It is not clear whether it is because of the file itself or its metadata.
|
||||
private const val fileSizeBytesMax = 100 * (1 shl 20) // MB
|
||||
private const val FILE_SIZE_MAX = 100 * (1 shl 20) // MB
|
||||
|
||||
fun isDangerouslyLarge(sizeBytes: Long?) = sizeBytes == null || sizeBytes > fileSizeBytesMax
|
||||
fun isDangerouslyLarge(sizeBytes: Long?) = sizeBytes == null || sizeBytes > FILE_SIZE_MAX
|
||||
|
||||
// we try and read metadata from large files by copying an arbitrary amount from its beginning
|
||||
// to a temporary file, and reusing that preview file for all metadata reading purposes
|
||||
private const val previewSize: Long = 5 * (1 shl 20) // MB
|
||||
private const val PREVIEW_SIZE: Long = 5 * (1 shl 20) // MB
|
||||
|
||||
private val previewFiles = HashMap<Uri, File>()
|
||||
|
||||
|
@ -159,7 +159,7 @@ object Metadata {
|
|||
fun createPreviewFile(context: Context, uri: Uri): File {
|
||||
return File.createTempFile("aves", null, context.cacheDir).apply {
|
||||
deleteOnExit()
|
||||
transferFrom(StorageUtils.openInputStream(context, uri), previewSize)
|
||||
transferFrom(StorageUtils.openInputStream(context, uri), PREVIEW_SIZE)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -45,9 +45,9 @@ object BitmapUtils {
|
|||
|
||||
val bufferSize = stream.size()
|
||||
if (bufferSize > BUFFER_SIZE_DANGER_THRESHOLD) {
|
||||
val availHeapSize = Runtime.getRuntime().let { it.maxMemory() - (it.totalMemory() - it.freeMemory()) }
|
||||
if (bufferSize > availHeapSize) {
|
||||
throw Exception("compressed bitmap to $bufferSize bytes, which cannot be allocated to a new byte array, with only $availHeapSize free bytes")
|
||||
val availableHeapSize = Runtime.getRuntime().let { it.maxMemory() - (it.totalMemory() - it.freeMemory()) }
|
||||
if (bufferSize > availableHeapSize) {
|
||||
throw Exception("compressed bitmap to $bufferSize bytes, which cannot be allocated to a new byte array, with only $availableHeapSize free bytes")
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -42,6 +42,7 @@ class RegionProvider extends ImageProvider<RegionProviderKey> {
|
|||
key.region,
|
||||
key.imageSize,
|
||||
pageId: pageId,
|
||||
sizeBytes: key.sizeBytes,
|
||||
taskKey: key,
|
||||
);
|
||||
if (bytes.isEmpty) {
|
||||
|
@ -70,7 +71,7 @@ class RegionProviderKey extends Equatable {
|
|||
// do not store the entry as it is, because the key should be constant
|
||||
// but the entry attributes may change over time
|
||||
final String uri, mimeType;
|
||||
final int? pageId;
|
||||
final int? pageId, sizeBytes;
|
||||
final int rotationDegrees, sampleSize;
|
||||
final bool isFlipped;
|
||||
final Rectangle<int> region;
|
||||
|
@ -83,13 +84,11 @@ class RegionProviderKey extends Equatable {
|
|||
required this.uri,
|
||||
required this.mimeType,
|
||||
required this.pageId,
|
||||
required this.sizeBytes,
|
||||
required this.rotationDegrees,
|
||||
required this.isFlipped,
|
||||
required this.sampleSize,
|
||||
required this.region,
|
||||
required this.imageSize,
|
||||
});
|
||||
|
||||
@override
|
||||
String toString() => '$runtimeType#${shortHash(this)}{uri=$uri, mimeType=$mimeType, pageId=$pageId, rotationDegrees=$rotationDegrees, isFlipped=$isFlipped, sampleSize=$sampleSize, region=$region, imageSize=$imageSize}';
|
||||
}
|
||||
|
|
|
@ -52,8 +52,8 @@ class UriImage extends ImageProvider<UriImage> with EquatableMixin {
|
|||
final bytes = await mediaFetchService.getImage(
|
||||
uri,
|
||||
mimeType,
|
||||
rotationDegrees,
|
||||
isFlipped,
|
||||
rotationDegrees: rotationDegrees,
|
||||
isFlipped: isFlipped,
|
||||
pageId: pageId,
|
||||
sizeBytes: sizeBytes,
|
||||
onBytesReceived: (cumulative, total) {
|
||||
|
@ -76,7 +76,4 @@ class UriImage extends ImageProvider<UriImage> with EquatableMixin {
|
|||
unawaited(chunkEvents.close());
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
String toString() => '$runtimeType#${shortHash(this)}{uri=$uri, mimeType=$mimeType, rotationDegrees=$rotationDegrees, isFlipped=$isFlipped, pageId=$pageId, scale=$scale}';
|
||||
}
|
||||
|
|
|
@ -34,6 +34,7 @@ extension ExtraAvesEntryImages on AvesEntry {
|
|||
uri: uri,
|
||||
mimeType: mimeType,
|
||||
pageId: pageId,
|
||||
sizeBytes: sizeBytes,
|
||||
rotationDegrees: rotationDegrees,
|
||||
isFlipped: isFlipped,
|
||||
sampleSize: sampleSize,
|
||||
|
|
|
@ -17,17 +17,17 @@ abstract class MediaFetchService {
|
|||
Future<Uint8List> getSvg(
|
||||
String uri,
|
||||
String mimeType, {
|
||||
int? expectedContentLength,
|
||||
required int? sizeBytes,
|
||||
BytesReceivedCallback? onBytesReceived,
|
||||
});
|
||||
|
||||
Future<Uint8List> getImage(
|
||||
String uri,
|
||||
String mimeType,
|
||||
int? rotationDegrees,
|
||||
bool isFlipped, {
|
||||
int? pageId,
|
||||
int? sizeBytes,
|
||||
String mimeType, {
|
||||
required int? rotationDegrees,
|
||||
required bool isFlipped,
|
||||
required int? pageId,
|
||||
required int? sizeBytes,
|
||||
BytesReceivedCallback? onBytesReceived,
|
||||
});
|
||||
|
||||
|
@ -40,7 +40,8 @@ abstract class MediaFetchService {
|
|||
int sampleSize,
|
||||
Rectangle<int> regionRect,
|
||||
Size imageSize, {
|
||||
int? pageId,
|
||||
required int? pageId,
|
||||
required int? sizeBytes,
|
||||
Object? taskKey,
|
||||
int? priority,
|
||||
});
|
||||
|
@ -93,26 +94,27 @@ class PlatformMediaFetchService implements MediaFetchService {
|
|||
Future<Uint8List> getSvg(
|
||||
String uri,
|
||||
String mimeType, {
|
||||
int? expectedContentLength,
|
||||
required int? sizeBytes,
|
||||
BytesReceivedCallback? onBytesReceived,
|
||||
}) =>
|
||||
getImage(
|
||||
uri,
|
||||
mimeType,
|
||||
0,
|
||||
false,
|
||||
sizeBytes: expectedContentLength,
|
||||
rotationDegrees: 0,
|
||||
isFlipped: false,
|
||||
pageId: null,
|
||||
sizeBytes: sizeBytes,
|
||||
onBytesReceived: onBytesReceived,
|
||||
);
|
||||
|
||||
@override
|
||||
Future<Uint8List> getImage(
|
||||
String uri,
|
||||
String mimeType,
|
||||
int? rotationDegrees,
|
||||
bool isFlipped, {
|
||||
int? pageId,
|
||||
int? sizeBytes,
|
||||
String mimeType, {
|
||||
required int? rotationDegrees,
|
||||
required bool isFlipped,
|
||||
required int? pageId,
|
||||
required int? sizeBytes,
|
||||
BytesReceivedCallback? onBytesReceived,
|
||||
}) async {
|
||||
try {
|
||||
|
@ -166,7 +168,8 @@ class PlatformMediaFetchService implements MediaFetchService {
|
|||
int sampleSize,
|
||||
Rectangle<int> regionRect,
|
||||
Size imageSize, {
|
||||
int? pageId,
|
||||
required int? pageId,
|
||||
required int? sizeBytes,
|
||||
Object? taskKey,
|
||||
int? priority,
|
||||
}) {
|
||||
|
@ -176,6 +179,7 @@ class PlatformMediaFetchService implements MediaFetchService {
|
|||
final result = await _platformBytes.invokeMethod('getRegion', <String, dynamic>{
|
||||
'uri': uri,
|
||||
'mimeType': mimeType,
|
||||
'sizeBytes': sizeBytes,
|
||||
'pageId': pageId,
|
||||
'sampleSize': sampleSize,
|
||||
'regionX': regionRect.left,
|
||||
|
|
|
@ -17,7 +17,11 @@ class SvgMetadataService {
|
|||
|
||||
static Future<Size?> getSize(AvesEntry entry) async {
|
||||
try {
|
||||
final data = await mediaFetchService.getSvg(entry.uri, entry.mimeType);
|
||||
final data = await mediaFetchService.getSvg(
|
||||
entry.uri,
|
||||
entry.mimeType,
|
||||
sizeBytes: entry.sizeBytes,
|
||||
);
|
||||
|
||||
final document = XmlDocument.parse(utf8.decode(data));
|
||||
final root = document.rootElement;
|
||||
|
@ -63,7 +67,11 @@ class SvgMetadataService {
|
|||
}
|
||||
|
||||
try {
|
||||
final data = await mediaFetchService.getSvg(entry.uri, entry.mimeType);
|
||||
final data = await mediaFetchService.getSvg(
|
||||
entry.uri,
|
||||
entry.mimeType,
|
||||
sizeBytes: entry.sizeBytes,
|
||||
);
|
||||
|
||||
final document = XmlDocument.parse(utf8.decode(data));
|
||||
final root = document.rootElement;
|
||||
|
|
|
@ -335,7 +335,14 @@ class EntryActionDelegate with FeedbackMixin, PermissionAwareMixin, SizeAwareMix
|
|||
MaterialPageRoute(
|
||||
settings: const RouteSettings(name: SourceViewerPage.routeName),
|
||||
builder: (context) => SourceViewerPage(
|
||||
loader: () => mediaFetchService.getSvg(entry.uri, entry.mimeType).then(utf8.decode),
|
||||
loader: () async {
|
||||
final data = await mediaFetchService.getSvg(
|
||||
entry.uri,
|
||||
entry.mimeType,
|
||||
sizeBytes: entry.sizeBytes,
|
||||
);
|
||||
return utf8.decode(data);
|
||||
},
|
||||
),
|
||||
),
|
||||
);
|
||||
|
|
|
@ -74,9 +74,13 @@ class EntryPrinter with FeedbackMixin {
|
|||
|
||||
Future<pdf.Widget?> _buildPageImage(AvesEntry entry) async {
|
||||
if (entry.isSvg) {
|
||||
final bytes = await mediaFetchService.getSvg(entry.uri, entry.mimeType);
|
||||
if (bytes.isNotEmpty) {
|
||||
return pdf.SvgImage(svg: utf8.decode(bytes));
|
||||
final data = await mediaFetchService.getSvg(
|
||||
entry.uri,
|
||||
entry.mimeType,
|
||||
sizeBytes: entry.sizeBytes,
|
||||
);
|
||||
if (data.isNotEmpty) {
|
||||
return pdf.SvgImage(svg: utf8.decode(data));
|
||||
}
|
||||
} else {
|
||||
return pdf.Image(await flutterImageProvider(entry.uriImage));
|
||||
|
|
Loading…
Reference in a new issue