/* * Copyright (c) Meta Platforms, Inc. and affiliates. * * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. */ package com.facebook.react.modules.fresco import android.content.Context import android.graphics.drawable.Drawable import android.net.Uri import com.facebook.common.logging.FLog import com.facebook.imageformat.ImageFormat import com.facebook.imageformat.ImageFormat.FormatChecker import com.facebook.imageformat.ImageFormatCheckerUtils import com.facebook.imagepipeline.common.ImageDecodeOptions import com.facebook.imagepipeline.decoder.ImageDecoder import com.facebook.imagepipeline.decoder.ImageDecoderConfig import com.facebook.imagepipeline.drawable.DrawableFactory import com.facebook.imagepipeline.image.CloseableImage import com.facebook.imagepipeline.image.DefaultCloseableImage import com.facebook.imagepipeline.image.EncodedImage import com.facebook.imagepipeline.image.QualityInfo public object XmlFormat { public fun addDecodingCapability( builder: ImageDecoderConfig.Builder, context: Context, ): ImageDecoderConfig.Builder { return builder.addDecodingCapability( XmlFormat.FORMAT, XmlFormat.XmlFormatChecker(), XmlFormat.XmlFormatDecoder(context), ) } public fun getDrawableFactory(): DrawableFactory { return XmlDrawableFactory() } private val FORMAT: ImageFormat = ImageFormat("XML", "xml") private const val TAG: String = "XmlFormat" /** * These are the first 4 bytes of a binary XML file. We can only support binary XML files and not * raw XML files because Android explicitly disallows raw XML files when inflating drawables. * Binary XML files are created at build time by Android's AAPT. * * @see * https://developer.android.com/reference/android/view/LayoutInflater#inflate(org.xmlpull.v1.XmlPullParser,%20android.view.ViewGroup) */ private val BINARY_XML_HEADER: ByteArray = byteArrayOf( 3.toByte(), 0.toByte(), 8.toByte(), 0.toByte(), ) private class XmlFormatChecker : FormatChecker { override val headerSize: Int = BINARY_XML_HEADER.size override fun determineFormat(headerBytes: ByteArray, headerSize: Int): ImageFormat { return when { headerSize < BINARY_XML_HEADER.size -> ImageFormat.UNKNOWN ImageFormatCheckerUtils.startsWithPattern(headerBytes, BINARY_XML_HEADER) -> FORMAT else -> ImageFormat.UNKNOWN } } } private class CloseableXmlImage(val name: String, val drawable: Drawable) : DefaultCloseableImage() { private var closed = false override fun getSizeInBytes(): Int { return getWidth() * getHeight() * 4 // 4 bytes ARGB per pixel } override fun close() { closed = true } override fun isClosed(): Boolean { return closed } override fun getWidth(): Int { return drawable.intrinsicWidth.takeIf { it >= 0 } ?: 0 } override fun getHeight(): Int { return drawable.intrinsicHeight.takeIf { it >= 0 } ?: 0 } } private class XmlFormatDecoder(private val context: Context) : ImageDecoder { override fun decode( encodedImage: EncodedImage, length: Int, qualityInfo: QualityInfo, options: ImageDecodeOptions ): CloseableImage? { return try { val xmlResourceName = encodedImage.source ?: error("No source in encoded image") // Use insecure URI parser since we do not care about the validity of the URI val xmlResource = Uri.parse(xmlResourceName) // Only support binary XML files from resources, not assets or raw files val xmlResourceId = parseImageSourceResourceId(xmlResource) // Use application context to avoid leaking the activity val drawable = context.applicationContext.resources.getDrawable(xmlResourceId, null) CloseableXmlImage(xmlResourceName, drawable) } catch (error: Throwable) { FLog.e(TAG, "Cannot decode xml ${error}", error) null } } /** * This parsing implementation is only designed to work with URI's that have been generated by * the ResourceDrawableIdHelper that ImageSource uses. It will ignore package names and schemes * in its quest to extract a basic integer resource ID. * * ResourceDrawableIdHelper generates URIs in the format of res:/[resourceId] * * @throws IllegalStateException if the resource ID cannot be parsed from the provided uri */ private fun parseImageSourceResourceId(xmlResource: Uri): Int { return xmlResource.pathSegments.lastOrNull()?.toIntOrNull() ?: error("Invalid resource id") } } private class XmlDrawableFactory : DrawableFactory { override fun supportsImageType(image: CloseableImage): Boolean { return image is CloseableXmlImage } override fun createDrawable(image: CloseableImage): Drawable? { return (image as CloseableXmlImage).drawable } } }