@onekeyfe/react-native-image 3.0.98
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/OneKeyImage.podspec +32 -0
- package/README.md +88 -0
- package/android/CMakeLists.txt +12 -0
- package/android/build.gradle +85 -0
- package/android/consumer-rules.pro +10 -0
- package/android/gradle.properties +5 -0
- package/android/src/main/AndroidManifest.xml +1 -0
- package/android/src/main/cpp/cpp-adapter.cpp +6 -0
- package/android/src/main/java/com/margelo/nitro/onekeyimage/OneKeyImage.kt +616 -0
- package/android/src/main/java/com/margelo/nitro/onekeyimage/OneKeyImageCache.kt +131 -0
- package/android/src/main/java/com/margelo/nitro/onekeyimage/OneKeyImageDecodeDimensions.kt +169 -0
- package/android/src/main/java/com/margelo/nitro/onekeyimage/OneKeyImageGlideRegistry.kt +285 -0
- package/android/src/main/java/com/margelo/nitro/onekeyimage/OneKeyImageModel.kt +68 -0
- package/android/src/main/java/com/margelo/nitro/onekeyimage/OneKeyImagePackage.kt +28 -0
- package/android/src/main/java/com/margelo/nitro/onekeyimage/OneKeyImageSafety.kt +1120 -0
- package/android/src/main/java/com/margelo/nitro/onekeyimage/OneKeyTosUrl.kt +93 -0
- package/android/src/test/java/com/margelo/nitro/onekeyimage/OneKeyAnimationTimelineInspectorTest.kt +210 -0
- package/android/src/test/java/com/margelo/nitro/onekeyimage/OneKeyImageCacheTypeTest.kt +16 -0
- package/android/src/test/java/com/margelo/nitro/onekeyimage/OneKeyImageDecodeDimensionsTest.kt +70 -0
- package/android/src/test/java/com/margelo/nitro/onekeyimage/OneKeyImageModelTest.kt +65 -0
- package/android/src/test/java/com/margelo/nitro/onekeyimage/OneKeyImageRequestSignatureTest.kt +33 -0
- package/android/src/test/java/com/margelo/nitro/onekeyimage/OneKeyImageSafetyTest.kt +346 -0
- package/android/src/test/java/com/margelo/nitro/onekeyimage/OneKeyResourceCleanupTest.kt +43 -0
- package/android/src/test/java/com/margelo/nitro/onekeyimage/OneKeyTerminalEventPairTest.kt +38 -0
- package/android/src/test/java/com/margelo/nitro/onekeyimage/OneKeyTosUrlTest.kt +82 -0
- package/ios/OneKeyImage.swift +636 -0
- package/ios/OneKeyImageCache.swift +215 -0
- package/ios/OneKeyImageRequestContext.swift +781 -0
- package/ios/OneKeyTosURL.swift +69 -0
- package/ios/tests/OneKeyImageInfrastructureTests.swift +555 -0
- package/ios/tests/OneKeyTosURLTests.swift +142 -0
- package/lib/module/OneKeyImage.nitro.js +30 -0
- package/lib/module/index.js +223 -0
- package/lib/module/package.json +1 -0
- package/lib/nitrogen/generated/android/c++/JFunc_void.hpp +75 -0
- package/lib/nitrogen/generated/android/c++/JFunc_void_double_double_OneKeyImageCacheType.hpp +77 -0
- package/lib/nitrogen/generated/android/c++/JFunc_void_std__string.hpp +76 -0
- package/lib/nitrogen/generated/android/c++/JHybridOneKeyImageCacheSpec.cpp +130 -0
- package/lib/nitrogen/generated/android/c++/JHybridOneKeyImageCacheSpec.hpp +66 -0
- package/lib/nitrogen/generated/android/c++/JHybridOneKeyImageSpec.cpp +255 -0
- package/lib/nitrogen/generated/android/c++/JHybridOneKeyImageSpec.hpp +93 -0
- package/lib/nitrogen/generated/android/c++/JOneKeyImageCachePolicy.hpp +64 -0
- package/lib/nitrogen/generated/android/c++/JOneKeyImageCacheType.hpp +61 -0
- package/lib/nitrogen/generated/android/c++/JOneKeyImageContentFit.hpp +64 -0
- package/lib/nitrogen/generated/android/c++/JOneKeyImageLoadingStrategy.hpp +61 -0
- package/lib/nitrogen/generated/android/c++/JOneKeyImagePreloadSource.hpp +88 -0
- package/lib/nitrogen/generated/android/c++/JOneKeyImageVariant.hpp +64 -0
- package/lib/nitrogen/generated/android/c++/views/JHybridOneKeyImageStateUpdater.cpp +144 -0
- package/lib/nitrogen/generated/android/c++/views/JHybridOneKeyImageStateUpdater.hpp +54 -0
- package/lib/nitrogen/generated/android/kotlin/com/margelo/nitro/onekeyimage/Func_void.kt +78 -0
- package/lib/nitrogen/generated/android/kotlin/com/margelo/nitro/onekeyimage/Func_void_double_double_OneKeyImageCacheType.kt +78 -0
- package/lib/nitrogen/generated/android/kotlin/com/margelo/nitro/onekeyimage/Func_void_std__string.kt +78 -0
- package/lib/nitrogen/generated/android/kotlin/com/margelo/nitro/onekeyimage/HybridOneKeyImageCacheSpec.kt +69 -0
- package/lib/nitrogen/generated/android/kotlin/com/margelo/nitro/onekeyimage/HybridOneKeyImageSpec.kt +189 -0
- package/lib/nitrogen/generated/android/kotlin/com/margelo/nitro/onekeyimage/OneKeyImageCachePolicy.kt +25 -0
- package/lib/nitrogen/generated/android/kotlin/com/margelo/nitro/onekeyimage/OneKeyImageCacheType.kt +24 -0
- package/lib/nitrogen/generated/android/kotlin/com/margelo/nitro/onekeyimage/OneKeyImageContentFit.kt +25 -0
- package/lib/nitrogen/generated/android/kotlin/com/margelo/nitro/onekeyimage/OneKeyImageLoadingStrategy.kt +24 -0
- package/lib/nitrogen/generated/android/kotlin/com/margelo/nitro/onekeyimage/OneKeyImagePreloadSource.kt +86 -0
- package/lib/nitrogen/generated/android/kotlin/com/margelo/nitro/onekeyimage/OneKeyImageVariant.kt +25 -0
- package/lib/nitrogen/generated/android/kotlin/com/margelo/nitro/onekeyimage/onekeyimageOnLoad.kt +35 -0
- package/lib/nitrogen/generated/android/kotlin/com/margelo/nitro/onekeyimage/views/HybridOneKeyImageManager.kt +96 -0
- package/lib/nitrogen/generated/android/kotlin/com/margelo/nitro/onekeyimage/views/HybridOneKeyImageStateUpdater.kt +23 -0
- package/lib/nitrogen/generated/android/onekeyimage+autolinking.cmake +85 -0
- package/lib/nitrogen/generated/android/onekeyimage+autolinking.gradle +27 -0
- package/lib/nitrogen/generated/android/onekeyimageOnLoad.cpp +78 -0
- package/lib/nitrogen/generated/android/onekeyimageOnLoad.hpp +34 -0
- package/lib/nitrogen/generated/ios/OneKeyImage+autolinking.rb +62 -0
- package/lib/nitrogen/generated/ios/OneKeyImage-Swift-Cxx-Bridge.cpp +90 -0
- package/lib/nitrogen/generated/ios/OneKeyImage-Swift-Cxx-Bridge.hpp +405 -0
- package/lib/nitrogen/generated/ios/OneKeyImage-Swift-Cxx-Umbrella.hpp +71 -0
- package/lib/nitrogen/generated/ios/OneKeyImageAutolinking.mm +41 -0
- package/lib/nitrogen/generated/ios/OneKeyImageAutolinking.swift +38 -0
- package/lib/nitrogen/generated/ios/c++/HybridOneKeyImageCacheSpecSwift.cpp +11 -0
- package/lib/nitrogen/generated/ios/c++/HybridOneKeyImageCacheSpecSwift.hpp +114 -0
- package/lib/nitrogen/generated/ios/c++/HybridOneKeyImageSpecSwift.cpp +11 -0
- package/lib/nitrogen/generated/ios/c++/HybridOneKeyImageSpecSwift.hpp +206 -0
- package/lib/nitrogen/generated/ios/c++/views/HybridOneKeyImageComponent.mm +228 -0
- package/lib/nitrogen/generated/ios/swift/Func_void.swift +46 -0
- package/lib/nitrogen/generated/ios/swift/Func_void_bool.swift +46 -0
- package/lib/nitrogen/generated/ios/swift/Func_void_double_double_OneKeyImageCacheType.swift +46 -0
- package/lib/nitrogen/generated/ios/swift/Func_void_std__exception_ptr.swift +46 -0
- package/lib/nitrogen/generated/ios/swift/Func_void_std__string.swift +46 -0
- package/lib/nitrogen/generated/ios/swift/HybridOneKeyImageCacheSpec.swift +58 -0
- package/lib/nitrogen/generated/ios/swift/HybridOneKeyImageCacheSpec_cxx.swift +202 -0
- package/lib/nitrogen/generated/ios/swift/HybridOneKeyImageSpec.swift +70 -0
- package/lib/nitrogen/generated/ios/swift/HybridOneKeyImageSpec_cxx.swift +539 -0
- package/lib/nitrogen/generated/ios/swift/OneKeyImageCachePolicy.swift +48 -0
- package/lib/nitrogen/generated/ios/swift/OneKeyImageCacheType.swift +44 -0
- package/lib/nitrogen/generated/ios/swift/OneKeyImageContentFit.swift +48 -0
- package/lib/nitrogen/generated/ios/swift/OneKeyImageLoadingStrategy.swift +44 -0
- package/lib/nitrogen/generated/ios/swift/OneKeyImagePreloadSource.swift +148 -0
- package/lib/nitrogen/generated/ios/swift/OneKeyImageVariant.swift +48 -0
- package/lib/nitrogen/generated/shared/c++/HybridOneKeyImageCacheSpec.cpp +24 -0
- package/lib/nitrogen/generated/shared/c++/HybridOneKeyImageCacheSpec.hpp +68 -0
- package/lib/nitrogen/generated/shared/c++/HybridOneKeyImageSpec.cpp +52 -0
- package/lib/nitrogen/generated/shared/c++/HybridOneKeyImageSpec.hpp +108 -0
- package/lib/nitrogen/generated/shared/c++/OneKeyImageCachePolicy.hpp +84 -0
- package/lib/nitrogen/generated/shared/c++/OneKeyImageCacheType.hpp +80 -0
- package/lib/nitrogen/generated/shared/c++/OneKeyImageContentFit.hpp +84 -0
- package/lib/nitrogen/generated/shared/c++/OneKeyImageLoadingStrategy.hpp +80 -0
- package/lib/nitrogen/generated/shared/c++/OneKeyImagePreloadSource.hpp +114 -0
- package/lib/nitrogen/generated/shared/c++/OneKeyImageVariant.hpp +84 -0
- package/lib/nitrogen/generated/shared/c++/views/HybridOneKeyImageComponent.cpp +62 -0
- package/lib/nitrogen/generated/shared/c++/views/HybridOneKeyImageComponent.hpp +132 -0
- package/lib/nitrogen/generated/shared/json/OneKeyImageConfig.json +24 -0
- package/lib/typescript/package.json +1 -0
- package/lib/typescript/src/OneKeyImage.nitro.d.ts +67 -0
- package/lib/typescript/src/index.d.ts +55 -0
- package/nitro.json +31 -0
- package/nitrogen/generated/android/c++/JFunc_void.hpp +75 -0
- package/nitrogen/generated/android/c++/JFunc_void_double_double_OneKeyImageCacheType.hpp +77 -0
- package/nitrogen/generated/android/c++/JFunc_void_std__string.hpp +76 -0
- package/nitrogen/generated/android/c++/JHybridOneKeyImageCacheSpec.cpp +130 -0
- package/nitrogen/generated/android/c++/JHybridOneKeyImageCacheSpec.hpp +66 -0
- package/nitrogen/generated/android/c++/JHybridOneKeyImageSpec.cpp +255 -0
- package/nitrogen/generated/android/c++/JHybridOneKeyImageSpec.hpp +93 -0
- package/nitrogen/generated/android/c++/JOneKeyImageCachePolicy.hpp +64 -0
- package/nitrogen/generated/android/c++/JOneKeyImageCacheType.hpp +61 -0
- package/nitrogen/generated/android/c++/JOneKeyImageContentFit.hpp +64 -0
- package/nitrogen/generated/android/c++/JOneKeyImageLoadingStrategy.hpp +61 -0
- package/nitrogen/generated/android/c++/JOneKeyImagePreloadSource.hpp +88 -0
- package/nitrogen/generated/android/c++/JOneKeyImageVariant.hpp +64 -0
- package/nitrogen/generated/android/c++/views/JHybridOneKeyImageStateUpdater.cpp +144 -0
- package/nitrogen/generated/android/c++/views/JHybridOneKeyImageStateUpdater.hpp +54 -0
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/onekeyimage/Func_void.kt +78 -0
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/onekeyimage/Func_void_double_double_OneKeyImageCacheType.kt +78 -0
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/onekeyimage/Func_void_std__string.kt +78 -0
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/onekeyimage/HybridOneKeyImageCacheSpec.kt +69 -0
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/onekeyimage/HybridOneKeyImageSpec.kt +189 -0
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/onekeyimage/OneKeyImageCachePolicy.kt +25 -0
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/onekeyimage/OneKeyImageCacheType.kt +24 -0
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/onekeyimage/OneKeyImageContentFit.kt +25 -0
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/onekeyimage/OneKeyImageLoadingStrategy.kt +24 -0
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/onekeyimage/OneKeyImagePreloadSource.kt +86 -0
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/onekeyimage/OneKeyImageVariant.kt +25 -0
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/onekeyimage/onekeyimageOnLoad.kt +35 -0
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/onekeyimage/views/HybridOneKeyImageManager.kt +96 -0
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/onekeyimage/views/HybridOneKeyImageStateUpdater.kt +23 -0
- package/nitrogen/generated/android/onekeyimage+autolinking.cmake +85 -0
- package/nitrogen/generated/android/onekeyimage+autolinking.gradle +27 -0
- package/nitrogen/generated/android/onekeyimageOnLoad.cpp +78 -0
- package/nitrogen/generated/android/onekeyimageOnLoad.hpp +34 -0
- package/nitrogen/generated/ios/OneKeyImage+autolinking.rb +62 -0
- package/nitrogen/generated/ios/OneKeyImage-Swift-Cxx-Bridge.cpp +90 -0
- package/nitrogen/generated/ios/OneKeyImage-Swift-Cxx-Bridge.hpp +405 -0
- package/nitrogen/generated/ios/OneKeyImage-Swift-Cxx-Umbrella.hpp +71 -0
- package/nitrogen/generated/ios/OneKeyImageAutolinking.mm +41 -0
- package/nitrogen/generated/ios/OneKeyImageAutolinking.swift +38 -0
- package/nitrogen/generated/ios/c++/HybridOneKeyImageCacheSpecSwift.cpp +11 -0
- package/nitrogen/generated/ios/c++/HybridOneKeyImageCacheSpecSwift.hpp +114 -0
- package/nitrogen/generated/ios/c++/HybridOneKeyImageSpecSwift.cpp +11 -0
- package/nitrogen/generated/ios/c++/HybridOneKeyImageSpecSwift.hpp +206 -0
- package/nitrogen/generated/ios/c++/views/HybridOneKeyImageComponent.mm +228 -0
- package/nitrogen/generated/ios/swift/Func_void.swift +46 -0
- package/nitrogen/generated/ios/swift/Func_void_bool.swift +46 -0
- package/nitrogen/generated/ios/swift/Func_void_double_double_OneKeyImageCacheType.swift +46 -0
- package/nitrogen/generated/ios/swift/Func_void_std__exception_ptr.swift +46 -0
- package/nitrogen/generated/ios/swift/Func_void_std__string.swift +46 -0
- package/nitrogen/generated/ios/swift/HybridOneKeyImageCacheSpec.swift +58 -0
- package/nitrogen/generated/ios/swift/HybridOneKeyImageCacheSpec_cxx.swift +202 -0
- package/nitrogen/generated/ios/swift/HybridOneKeyImageSpec.swift +70 -0
- package/nitrogen/generated/ios/swift/HybridOneKeyImageSpec_cxx.swift +539 -0
- package/nitrogen/generated/ios/swift/OneKeyImageCachePolicy.swift +48 -0
- package/nitrogen/generated/ios/swift/OneKeyImageCacheType.swift +44 -0
- package/nitrogen/generated/ios/swift/OneKeyImageContentFit.swift +48 -0
- package/nitrogen/generated/ios/swift/OneKeyImageLoadingStrategy.swift +44 -0
- package/nitrogen/generated/ios/swift/OneKeyImagePreloadSource.swift +148 -0
- package/nitrogen/generated/ios/swift/OneKeyImageVariant.swift +48 -0
- package/nitrogen/generated/shared/c++/HybridOneKeyImageCacheSpec.cpp +24 -0
- package/nitrogen/generated/shared/c++/HybridOneKeyImageCacheSpec.hpp +68 -0
- package/nitrogen/generated/shared/c++/HybridOneKeyImageSpec.cpp +52 -0
- package/nitrogen/generated/shared/c++/HybridOneKeyImageSpec.hpp +108 -0
- package/nitrogen/generated/shared/c++/OneKeyImageCachePolicy.hpp +84 -0
- package/nitrogen/generated/shared/c++/OneKeyImageCacheType.hpp +80 -0
- package/nitrogen/generated/shared/c++/OneKeyImageContentFit.hpp +84 -0
- package/nitrogen/generated/shared/c++/OneKeyImageLoadingStrategy.hpp +80 -0
- package/nitrogen/generated/shared/c++/OneKeyImagePreloadSource.hpp +114 -0
- package/nitrogen/generated/shared/c++/OneKeyImageVariant.hpp +84 -0
- package/nitrogen/generated/shared/c++/views/HybridOneKeyImageComponent.cpp +62 -0
- package/nitrogen/generated/shared/c++/views/HybridOneKeyImageComponent.hpp +132 -0
- package/nitrogen/generated/shared/json/OneKeyImageConfig.json +24 -0
- package/package.json +128 -0
- package/src/OneKeyImage.nitro.ts +86 -0
- package/src/index.tsx +376 -0
|
@@ -0,0 +1,1120 @@
|
|
|
1
|
+
package com.margelo.nitro.onekeyimage
|
|
2
|
+
|
|
3
|
+
import android.content.Context
|
|
4
|
+
import android.net.Uri
|
|
5
|
+
import com.bumptech.glide.Priority
|
|
6
|
+
import com.bumptech.glide.integration.okhttp3.OkHttpUrlLoader
|
|
7
|
+
import com.bumptech.glide.load.DataSource
|
|
8
|
+
import com.bumptech.glide.load.Key
|
|
9
|
+
import com.bumptech.glide.load.Options
|
|
10
|
+
import com.bumptech.glide.load.data.DataFetcher
|
|
11
|
+
import com.bumptech.glide.load.engine.GlideException
|
|
12
|
+
import com.bumptech.glide.load.model.ModelLoader
|
|
13
|
+
import com.bumptech.glide.load.model.ModelLoaderFactory
|
|
14
|
+
import com.bumptech.glide.load.model.MultiModelLoaderFactory
|
|
15
|
+
import okhttp3.Call
|
|
16
|
+
import okhttp3.Interceptor
|
|
17
|
+
import okhttp3.MediaType
|
|
18
|
+
import okhttp3.ResponseBody
|
|
19
|
+
import okio.Buffer
|
|
20
|
+
import okio.BufferedSource
|
|
21
|
+
import okio.ForwardingSource
|
|
22
|
+
import okio.buffer
|
|
23
|
+
import java.io.File
|
|
24
|
+
import java.io.FilterInputStream
|
|
25
|
+
import java.io.BufferedInputStream
|
|
26
|
+
import java.io.ByteArrayInputStream
|
|
27
|
+
import java.io.ByteArrayOutputStream
|
|
28
|
+
import java.io.IOException
|
|
29
|
+
import java.io.InputStream
|
|
30
|
+
import java.nio.ByteBuffer
|
|
31
|
+
import java.util.ArrayDeque
|
|
32
|
+
import java.util.Collections
|
|
33
|
+
import java.util.IdentityHashMap
|
|
34
|
+
import java.security.MessageDigest
|
|
35
|
+
|
|
36
|
+
internal class OneKeyImageSafetyException(message: String) : IOException(message)
|
|
37
|
+
|
|
38
|
+
internal object OneKeyImageSafety {
|
|
39
|
+
const val MAX_ENCODED_BYTES = 32L * 1024L * 1024L
|
|
40
|
+
const val MAX_DATA_URI_DECODED_BYTES = 8L * 1024L * 1024L
|
|
41
|
+
const val MAX_ANIMATED_ENCODED_BYTES = 16L * 1024L * 1024L
|
|
42
|
+
const val MAX_STATIC_PIXELS = 100_000_000L
|
|
43
|
+
const val MAX_STATIC_SIDE = 32_768
|
|
44
|
+
const val MAX_ANIMATED_CANVAS_PIXELS = 4_194_304L
|
|
45
|
+
const val MAX_ANIMATED_SIDE = 8_192
|
|
46
|
+
const val MAX_ANIMATION_FRAMES = 1_000
|
|
47
|
+
const val MAX_ANIMATION_DURATION_MS = 60_000L
|
|
48
|
+
|
|
49
|
+
fun requireEncodedLength(length: Long, maximum: Long = MAX_ENCODED_BYTES) {
|
|
50
|
+
if (length > maximum) {
|
|
51
|
+
throw OneKeyImageSafetyException(
|
|
52
|
+
"Encoded image exceeds the ${maximum / (1024L * 1024L)} MiB limit",
|
|
53
|
+
)
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
fun requireStaticDimensions(width: Int, height: Int) =
|
|
58
|
+
requireStaticDimensions(width.toLong(), height.toLong())
|
|
59
|
+
|
|
60
|
+
fun requireStaticDimensions(width: Long, height: Long) {
|
|
61
|
+
requireDimensions(
|
|
62
|
+
width,
|
|
63
|
+
height,
|
|
64
|
+
MAX_STATIC_SIDE,
|
|
65
|
+
MAX_STATIC_PIXELS,
|
|
66
|
+
"Static image",
|
|
67
|
+
)
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
fun requireAnimatedDimensions(width: Int, height: Int) =
|
|
71
|
+
requireAnimatedDimensions(width.toLong(), height.toLong())
|
|
72
|
+
|
|
73
|
+
fun requireAnimatedDimensions(width: Long, height: Long) {
|
|
74
|
+
if (width <= 0L || height <= 0L) {
|
|
75
|
+
throw OneKeyImageSafetyException("Animated image dimensions are invalid")
|
|
76
|
+
}
|
|
77
|
+
requireDimensions(
|
|
78
|
+
width,
|
|
79
|
+
height,
|
|
80
|
+
MAX_ANIMATED_SIDE,
|
|
81
|
+
MAX_ANIMATED_CANVAS_PIXELS,
|
|
82
|
+
"Animated image",
|
|
83
|
+
)
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
fun requireAnimationFrameCount(frameCount: Long) {
|
|
87
|
+
if (frameCount > MAX_ANIMATION_FRAMES) {
|
|
88
|
+
throw OneKeyImageSafetyException(
|
|
89
|
+
"Animated image exceeds the $MAX_ANIMATION_FRAMES frame limit",
|
|
90
|
+
)
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
/** Returns the decoded byte count without allocating the decoded payload. */
|
|
95
|
+
fun dataUriDecodedByteCount(dataUri: String, payloadStart: Int): Long {
|
|
96
|
+
require(payloadStart in 0..dataUri.length) { "Invalid data URI payload" }
|
|
97
|
+
val rawLength = dataUri.length - payloadStart
|
|
98
|
+
val maximumEncodedCharacters = ((MAX_DATA_URI_DECODED_BYTES + 2L) / 3L) * 4L
|
|
99
|
+
if (rawLength.toLong() > maximumEncodedCharacters) {
|
|
100
|
+
throw OneKeyImageSafetyException("Base64 image data exceeds the 8 MiB decoded limit")
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
var characterCount = 0L
|
|
104
|
+
var secondLast = '\u0000'
|
|
105
|
+
var last = '\u0000'
|
|
106
|
+
for (index in payloadStart until dataUri.length) {
|
|
107
|
+
val character = dataUri[index]
|
|
108
|
+
if (character == ' ' || character == '\t' || character == '\r' || character == '\n') {
|
|
109
|
+
continue
|
|
110
|
+
}
|
|
111
|
+
characterCount += 1L
|
|
112
|
+
secondLast = last
|
|
113
|
+
last = character
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
val completeGroups = characterCount / 4L
|
|
117
|
+
val remainder = (characterCount % 4L).toInt()
|
|
118
|
+
var decodedBytes = completeGroups * 3L
|
|
119
|
+
decodedBytes += when (remainder) {
|
|
120
|
+
0 -> 0L
|
|
121
|
+
2 -> 1L
|
|
122
|
+
3 -> 2L
|
|
123
|
+
else -> 3L // Invalid Base64 will fail in the decoder; never under-estimate it here.
|
|
124
|
+
}
|
|
125
|
+
if (remainder == 0) {
|
|
126
|
+
if (last == '=') decodedBytes -= 1L
|
|
127
|
+
if (secondLast == '=') decodedBytes -= 1L
|
|
128
|
+
}
|
|
129
|
+
return decodedBytes.coerceAtLeast(0L)
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
fun isSafetyFailure(error: Throwable?): Boolean {
|
|
133
|
+
if (error == null) return false
|
|
134
|
+
val seen = Collections.newSetFromMap(IdentityHashMap<Throwable, Boolean>())
|
|
135
|
+
val pending = ArrayDeque<Throwable>()
|
|
136
|
+
pending.add(error)
|
|
137
|
+
while (pending.isNotEmpty()) {
|
|
138
|
+
val current = pending.removeFirst()
|
|
139
|
+
if (!seen.add(current)) continue
|
|
140
|
+
if (current is OneKeyImageSafetyException) return true
|
|
141
|
+
current.cause?.let(pending::addLast)
|
|
142
|
+
if (current is GlideException) {
|
|
143
|
+
current.rootCauses.forEach(pending::addLast)
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
return false
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
private fun requireDimensions(
|
|
150
|
+
width: Long,
|
|
151
|
+
height: Long,
|
|
152
|
+
maximumSide: Int,
|
|
153
|
+
maximumPixels: Long,
|
|
154
|
+
label: String,
|
|
155
|
+
) {
|
|
156
|
+
if (width <= 0 || height <= 0) return
|
|
157
|
+
if (
|
|
158
|
+
width > maximumSide ||
|
|
159
|
+
height > maximumSide ||
|
|
160
|
+
width * height > maximumPixels
|
|
161
|
+
) {
|
|
162
|
+
throw OneKeyImageSafetyException("$label dimensions exceed the safety limit")
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
internal data class OneKeyEncodedImageMetadata(
|
|
169
|
+
val animated: Boolean,
|
|
170
|
+
val width: Int = 0,
|
|
171
|
+
val height: Int = 0,
|
|
172
|
+
val frameCount: Int? = null,
|
|
173
|
+
val animatedFormat: OneKeyAnimatedFormat? = null,
|
|
174
|
+
)
|
|
175
|
+
|
|
176
|
+
internal enum class OneKeyAnimatedFormat {
|
|
177
|
+
GIF,
|
|
178
|
+
WEBP,
|
|
179
|
+
APNG,
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
internal object OneKeyEncodedImageInspector {
|
|
183
|
+
private const val MAX_METADATA_BYTES = 256 * 1024
|
|
184
|
+
private val BMFF_LEADING_BOXES = setOf(
|
|
185
|
+
"ftyp", "free", "skip", "wide", "uuid", "styp", "sidx", "meta", "moov", "mdat",
|
|
186
|
+
)
|
|
187
|
+
private val PNG_SIGNATURE = byteArrayOf(
|
|
188
|
+
0x89.toByte(), 0x50, 0x4e, 0x47, 0x0d, 0x0a, 0x1a, 0x0a,
|
|
189
|
+
)
|
|
190
|
+
|
|
191
|
+
fun inspect(input: InputStream): OneKeyEncodedImageMetadata {
|
|
192
|
+
require(input.markSupported()) { "Image metadata input must support mark/reset" }
|
|
193
|
+
input.mark(MAX_METADATA_BYTES + 1)
|
|
194
|
+
return try {
|
|
195
|
+
val header = ByteArray(64)
|
|
196
|
+
val headerLength = readUpTo(input, header)
|
|
197
|
+
input.reset()
|
|
198
|
+
when {
|
|
199
|
+
looksLikeBmff(header, headerLength) -> inspectBmff(input)
|
|
200
|
+
isGif(header, headerLength) -> inspectGif(header, headerLength)
|
|
201
|
+
isWebP(header, headerLength) -> inspectWebP(header, headerLength)
|
|
202
|
+
startsWith(header, headerLength, PNG_SIGNATURE) -> inspectPng(input)
|
|
203
|
+
else -> OneKeyEncodedImageMetadata(animated = false)
|
|
204
|
+
}
|
|
205
|
+
} finally {
|
|
206
|
+
input.reset()
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
private fun inspectBmff(input: InputStream): OneKeyEncodedImageMetadata {
|
|
211
|
+
val reader = BoundedMetadataReader(input, MAX_METADATA_BYTES)
|
|
212
|
+
while (true) {
|
|
213
|
+
val size32 = reader.readUnsignedIntBe()
|
|
214
|
+
val boxType = reader.readAscii4()
|
|
215
|
+
var headerSize = 8L
|
|
216
|
+
val boxSize = when (size32) {
|
|
217
|
+
0L -> throw OneKeyImageSafetyException("Unbounded BMFF metadata is rejected")
|
|
218
|
+
1L -> {
|
|
219
|
+
headerSize = 16L
|
|
220
|
+
reader.readBoundedUnsignedLongBe()
|
|
221
|
+
}
|
|
222
|
+
else -> size32
|
|
223
|
+
}
|
|
224
|
+
if (boxSize < headerSize) {
|
|
225
|
+
throw OneKeyImageSafetyException("BMFF box size is invalid")
|
|
226
|
+
}
|
|
227
|
+
val payloadLength = boxSize - headerSize
|
|
228
|
+
if (payloadLength > reader.remainingLimit()) {
|
|
229
|
+
throw OneKeyImageSafetyException("BMFF metadata exceeds the scan limit")
|
|
230
|
+
}
|
|
231
|
+
if (boxType == "ftyp") {
|
|
232
|
+
if (payloadLength < 8L || (payloadLength - 8L) % 4L != 0L) {
|
|
233
|
+
throw OneKeyImageSafetyException("BMFF ftyp metadata is invalid")
|
|
234
|
+
}
|
|
235
|
+
val payload = reader.readExact(payloadLength.toInt())
|
|
236
|
+
if (containsAvifBrand(payload)) {
|
|
237
|
+
throw OneKeyImageSafetyException(
|
|
238
|
+
"AVIF is rejected because Glide 5.0.5 cannot enforce the target decode size",
|
|
239
|
+
)
|
|
240
|
+
}
|
|
241
|
+
return OneKeyEncodedImageMetadata(animated = false)
|
|
242
|
+
}
|
|
243
|
+
reader.skipExact(payloadLength)
|
|
244
|
+
}
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
private fun containsAvifBrand(ftypPayload: ByteArray): Boolean {
|
|
248
|
+
if (ascii(ftypPayload, 0) == "avif" || ascii(ftypPayload, 0) == "avis") return true
|
|
249
|
+
var offset = 8
|
|
250
|
+
while (offset + 4 <= ftypPayload.size) {
|
|
251
|
+
val brand = ascii(ftypPayload, offset)
|
|
252
|
+
if (brand == "avif" || brand == "avis") return true
|
|
253
|
+
offset += 4
|
|
254
|
+
}
|
|
255
|
+
return false
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
private fun looksLikeBmff(bytes: ByteArray, length: Int): Boolean {
|
|
259
|
+
if (length < 8 || !isAsciiBoxType(bytes, 4)) return false
|
|
260
|
+
val boxType = ascii(bytes, 4)
|
|
261
|
+
val size = unsignedIntBe(bytes, 0)
|
|
262
|
+
return boxType in BMFF_LEADING_BOXES ||
|
|
263
|
+
size == 0L ||
|
|
264
|
+
size == 1L ||
|
|
265
|
+
size in 8L..OneKeyImageSafety.MAX_ENCODED_BYTES
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
private fun isAsciiBoxType(bytes: ByteArray, offset: Int): Boolean =
|
|
269
|
+
(offset until offset + 4).all { (bytes[it].toInt() and 0xff) in 0x20..0x7e }
|
|
270
|
+
|
|
271
|
+
private fun inspectGif(header: ByteArray, length: Int): OneKeyEncodedImageMetadata {
|
|
272
|
+
if (length < 10) throw OneKeyImageSafetyException("GIF metadata is incomplete")
|
|
273
|
+
val width = unsignedShortLe(header, 6)
|
|
274
|
+
val height = unsignedShortLe(header, 8)
|
|
275
|
+
OneKeyImageSafety.requireAnimatedDimensions(width, height)
|
|
276
|
+
return OneKeyEncodedImageMetadata(
|
|
277
|
+
animated = true,
|
|
278
|
+
width = width,
|
|
279
|
+
height = height,
|
|
280
|
+
animatedFormat = OneKeyAnimatedFormat.GIF,
|
|
281
|
+
)
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
private fun inspectWebP(header: ByteArray, length: Int): OneKeyEncodedImageMetadata {
|
|
285
|
+
if (length < 16) throw OneKeyImageSafetyException("WebP metadata is incomplete")
|
|
286
|
+
val chunkType = ascii(header, 12)
|
|
287
|
+
if (chunkType != "VP8X") {
|
|
288
|
+
if (chunkType != "VP8 " && chunkType != "VP8L") {
|
|
289
|
+
throw OneKeyImageSafetyException("WebP metadata is ambiguous")
|
|
290
|
+
}
|
|
291
|
+
return OneKeyEncodedImageMetadata(animated = false)
|
|
292
|
+
}
|
|
293
|
+
if (length < 30 || unsignedIntLe(header, 16) < 10L) {
|
|
294
|
+
throw OneKeyImageSafetyException("WebP VP8X metadata is incomplete")
|
|
295
|
+
}
|
|
296
|
+
val animated = header[20].toInt() and 0x02 != 0
|
|
297
|
+
if (!animated) return OneKeyEncodedImageMetadata(animated = false)
|
|
298
|
+
val width = unsigned24Le(header, 24) + 1
|
|
299
|
+
val height = unsigned24Le(header, 27) + 1
|
|
300
|
+
OneKeyImageSafety.requireAnimatedDimensions(width, height)
|
|
301
|
+
return OneKeyEncodedImageMetadata(
|
|
302
|
+
animated = true,
|
|
303
|
+
width = width,
|
|
304
|
+
height = height,
|
|
305
|
+
animatedFormat = OneKeyAnimatedFormat.WEBP,
|
|
306
|
+
)
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
private fun inspectPng(input: InputStream): OneKeyEncodedImageMetadata {
|
|
310
|
+
val reader = BoundedMetadataReader(input, MAX_METADATA_BYTES)
|
|
311
|
+
reader.readExact(8) // signature
|
|
312
|
+
val ihdrLength = reader.readUnsignedIntBe()
|
|
313
|
+
val ihdrType = reader.readAscii4()
|
|
314
|
+
if (ihdrType != "IHDR" || ihdrLength != 13L) {
|
|
315
|
+
throw OneKeyImageSafetyException("PNG IHDR metadata is invalid")
|
|
316
|
+
}
|
|
317
|
+
val width = reader.readUnsignedIntBe()
|
|
318
|
+
val height = reader.readUnsignedIntBe()
|
|
319
|
+
reader.skipExact(5L + 4L) // remaining IHDR payload and CRC
|
|
320
|
+
|
|
321
|
+
while (true) {
|
|
322
|
+
val chunkLength = reader.readUnsignedIntBe()
|
|
323
|
+
val chunkType = reader.readAscii4()
|
|
324
|
+
when (chunkType) {
|
|
325
|
+
"acTL" -> {
|
|
326
|
+
if (chunkLength != 8L) {
|
|
327
|
+
throw OneKeyImageSafetyException("APNG acTL metadata is invalid")
|
|
328
|
+
}
|
|
329
|
+
val frameCount = reader.readUnsignedIntBe()
|
|
330
|
+
reader.skipExact(4L + 4L) // play count and CRC
|
|
331
|
+
if (frameCount == 0L) {
|
|
332
|
+
throw OneKeyImageSafetyException("APNG frame count exceeds the safety limit")
|
|
333
|
+
}
|
|
334
|
+
OneKeyImageSafety.requireAnimationFrameCount(frameCount)
|
|
335
|
+
OneKeyImageSafety.requireAnimatedDimensions(width, height)
|
|
336
|
+
return OneKeyEncodedImageMetadata(
|
|
337
|
+
animated = true,
|
|
338
|
+
width = width.toInt(),
|
|
339
|
+
height = height.toInt(),
|
|
340
|
+
frameCount = frameCount.toInt(),
|
|
341
|
+
animatedFormat = OneKeyAnimatedFormat.APNG,
|
|
342
|
+
)
|
|
343
|
+
}
|
|
344
|
+
"IDAT", "IEND" -> {
|
|
345
|
+
OneKeyImageSafety.requireStaticDimensions(width, height)
|
|
346
|
+
if (width > Int.MAX_VALUE || height > Int.MAX_VALUE) {
|
|
347
|
+
throw OneKeyImageSafetyException("PNG dimensions exceed the safety limit")
|
|
348
|
+
}
|
|
349
|
+
return OneKeyEncodedImageMetadata(
|
|
350
|
+
animated = false,
|
|
351
|
+
width = width.toInt(),
|
|
352
|
+
height = height.toInt(),
|
|
353
|
+
)
|
|
354
|
+
}
|
|
355
|
+
else -> reader.skipExact(chunkLength + 4L)
|
|
356
|
+
}
|
|
357
|
+
}
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
private fun isGif(bytes: ByteArray, length: Int): Boolean =
|
|
361
|
+
length >= 6 && (ascii(bytes, 0, 6) == "GIF87a" || ascii(bytes, 0, 6) == "GIF89a")
|
|
362
|
+
|
|
363
|
+
private fun isWebP(bytes: ByteArray, length: Int): Boolean =
|
|
364
|
+
length >= 12 && ascii(bytes, 0) == "RIFF" && ascii(bytes, 8) == "WEBP"
|
|
365
|
+
|
|
366
|
+
private fun startsWith(bytes: ByteArray, length: Int, prefix: ByteArray): Boolean {
|
|
367
|
+
if (length < prefix.size) return false
|
|
368
|
+
return prefix.indices.all { bytes[it] == prefix[it] }
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
private fun ascii(bytes: ByteArray, offset: Int, length: Int = 4): String =
|
|
372
|
+
String(bytes, offset, length, Charsets.US_ASCII)
|
|
373
|
+
|
|
374
|
+
private fun unsignedShortLe(bytes: ByteArray, offset: Int): Int =
|
|
375
|
+
(bytes[offset].toInt() and 0xff) or ((bytes[offset + 1].toInt() and 0xff) shl 8)
|
|
376
|
+
|
|
377
|
+
private fun unsigned24Le(bytes: ByteArray, offset: Int): Int =
|
|
378
|
+
(bytes[offset].toInt() and 0xff) or
|
|
379
|
+
((bytes[offset + 1].toInt() and 0xff) shl 8) or
|
|
380
|
+
((bytes[offset + 2].toInt() and 0xff) shl 16)
|
|
381
|
+
|
|
382
|
+
private fun unsignedIntLe(bytes: ByteArray, offset: Int): Long =
|
|
383
|
+
(bytes[offset].toLong() and 0xffL) or
|
|
384
|
+
((bytes[offset + 1].toLong() and 0xffL) shl 8) or
|
|
385
|
+
((bytes[offset + 2].toLong() and 0xffL) shl 16) or
|
|
386
|
+
((bytes[offset + 3].toLong() and 0xffL) shl 24)
|
|
387
|
+
|
|
388
|
+
private fun unsignedIntBe(bytes: ByteArray, offset: Int): Long =
|
|
389
|
+
((bytes[offset].toLong() and 0xffL) shl 24) or
|
|
390
|
+
((bytes[offset + 1].toLong() and 0xffL) shl 16) or
|
|
391
|
+
((bytes[offset + 2].toLong() and 0xffL) shl 8) or
|
|
392
|
+
(bytes[offset + 3].toLong() and 0xffL)
|
|
393
|
+
|
|
394
|
+
private fun readUpTo(input: InputStream, bytes: ByteArray): Int {
|
|
395
|
+
var total = 0
|
|
396
|
+
while (total < bytes.size) {
|
|
397
|
+
val count = input.read(bytes, total, bytes.size - total)
|
|
398
|
+
if (count <= 0) break
|
|
399
|
+
total += count
|
|
400
|
+
}
|
|
401
|
+
return total
|
|
402
|
+
}
|
|
403
|
+
|
|
404
|
+
private class BoundedMetadataReader(
|
|
405
|
+
private val input: InputStream,
|
|
406
|
+
private val maximumBytes: Int,
|
|
407
|
+
) {
|
|
408
|
+
private var consumedBytes = 0L
|
|
409
|
+
|
|
410
|
+
fun readExact(length: Int): ByteArray {
|
|
411
|
+
requireWithinLimit(length.toLong())
|
|
412
|
+
val bytes = ByteArray(length)
|
|
413
|
+
var offset = 0
|
|
414
|
+
while (offset < length) {
|
|
415
|
+
val count = input.read(bytes, offset, length - offset)
|
|
416
|
+
if (count <= 0) throw OneKeyImageSafetyException("PNG metadata is truncated")
|
|
417
|
+
offset += count
|
|
418
|
+
}
|
|
419
|
+
consumedBytes += length.toLong()
|
|
420
|
+
return bytes
|
|
421
|
+
}
|
|
422
|
+
|
|
423
|
+
fun readUnsignedIntBe(): Long {
|
|
424
|
+
val bytes = readExact(4)
|
|
425
|
+
return ((bytes[0].toLong() and 0xffL) shl 24) or
|
|
426
|
+
((bytes[1].toLong() and 0xffL) shl 16) or
|
|
427
|
+
((bytes[2].toLong() and 0xffL) shl 8) or
|
|
428
|
+
(bytes[3].toLong() and 0xffL)
|
|
429
|
+
}
|
|
430
|
+
|
|
431
|
+
fun readAscii4(): String = String(readExact(4), Charsets.US_ASCII)
|
|
432
|
+
|
|
433
|
+
fun readBoundedUnsignedLongBe(): Long {
|
|
434
|
+
val high = readUnsignedIntBe()
|
|
435
|
+
val low = readUnsignedIntBe()
|
|
436
|
+
if (high != 0L || low > maximumBytes.toLong()) {
|
|
437
|
+
throw OneKeyImageSafetyException("Extended BMFF box exceeds the scan limit")
|
|
438
|
+
}
|
|
439
|
+
return low
|
|
440
|
+
}
|
|
441
|
+
|
|
442
|
+
fun remainingLimit(): Long = maximumBytes.toLong() - consumedBytes
|
|
443
|
+
|
|
444
|
+
fun skipExact(length: Long) {
|
|
445
|
+
requireWithinLimit(length)
|
|
446
|
+
var remaining = length
|
|
447
|
+
while (remaining > 0L) {
|
|
448
|
+
val skipped = input.skip(remaining)
|
|
449
|
+
if (skipped > 0L) {
|
|
450
|
+
remaining -= skipped
|
|
451
|
+
} else if (input.read() >= 0) {
|
|
452
|
+
remaining -= 1L
|
|
453
|
+
} else {
|
|
454
|
+
throw OneKeyImageSafetyException("PNG metadata is truncated")
|
|
455
|
+
}
|
|
456
|
+
}
|
|
457
|
+
consumedBytes += length
|
|
458
|
+
}
|
|
459
|
+
|
|
460
|
+
private fun requireWithinLimit(additionalBytes: Long) {
|
|
461
|
+
if (additionalBytes < 0L || consumedBytes + additionalBytes > maximumBytes.toLong()) {
|
|
462
|
+
throw OneKeyImageSafetyException("PNG animation metadata exceeds the scan limit")
|
|
463
|
+
}
|
|
464
|
+
}
|
|
465
|
+
}
|
|
466
|
+
}
|
|
467
|
+
|
|
468
|
+
internal object OneKeyAnimationTimelineInspector {
|
|
469
|
+
fun validate(
|
|
470
|
+
format: OneKeyAnimatedFormat,
|
|
471
|
+
bytes: ByteArray,
|
|
472
|
+
length: Int = bytes.size,
|
|
473
|
+
) {
|
|
474
|
+
if (length !in 1..bytes.size) fail("Animated image data is empty or invalid")
|
|
475
|
+
when (format) {
|
|
476
|
+
OneKeyAnimatedFormat.GIF -> validateGif(bytes, length)
|
|
477
|
+
OneKeyAnimatedFormat.WEBP -> validateWebP(bytes, length)
|
|
478
|
+
OneKeyAnimatedFormat.APNG -> validateApng(bytes, length)
|
|
479
|
+
}
|
|
480
|
+
}
|
|
481
|
+
|
|
482
|
+
private fun validateGif(bytes: ByteArray, length: Int) {
|
|
483
|
+
val cursor = ByteCursor(bytes, length)
|
|
484
|
+
val signature = cursor.ascii(6)
|
|
485
|
+
if (signature != "GIF87a" && signature != "GIF89a") fail("GIF signature is invalid")
|
|
486
|
+
val canvasWidth = cursor.u16Le()
|
|
487
|
+
val canvasHeight = cursor.u16Le()
|
|
488
|
+
OneKeyImageSafety.requireAnimatedDimensions(canvasWidth, canvasHeight)
|
|
489
|
+
val packed = cursor.u8()
|
|
490
|
+
cursor.skip(2) // background index and pixel aspect ratio
|
|
491
|
+
if (packed and 0x80 != 0) cursor.skip(colorTableBytes(packed))
|
|
492
|
+
|
|
493
|
+
var frameCount = 0L
|
|
494
|
+
var totalDurationMs = 0L
|
|
495
|
+
var currentFrameDurationMs = 0L
|
|
496
|
+
var trailerFound = false
|
|
497
|
+
while (cursor.hasRemaining()) {
|
|
498
|
+
when (cursor.u8()) {
|
|
499
|
+
0x3b -> {
|
|
500
|
+
trailerFound = true
|
|
501
|
+
break
|
|
502
|
+
}
|
|
503
|
+
0x21 -> {
|
|
504
|
+
when (cursor.u8()) {
|
|
505
|
+
0xf9 -> {
|
|
506
|
+
if (cursor.u8() != 4) fail("GIF graphic control extension is invalid")
|
|
507
|
+
cursor.u8() // packed fields
|
|
508
|
+
val delayCentiseconds = cursor.u16Le()
|
|
509
|
+
cursor.u8() // transparent color index
|
|
510
|
+
if (cursor.u8() != 0) fail("GIF graphic control extension is truncated")
|
|
511
|
+
val effectiveDelayCentiseconds = if (delayCentiseconds == 0) 10 else delayCentiseconds
|
|
512
|
+
currentFrameDurationMs = effectiveDelayCentiseconds.toLong() * 10L
|
|
513
|
+
}
|
|
514
|
+
else -> cursor.skipSubBlocks()
|
|
515
|
+
}
|
|
516
|
+
}
|
|
517
|
+
0x2c -> {
|
|
518
|
+
val frameX = cursor.u16Le()
|
|
519
|
+
val frameY = cursor.u16Le()
|
|
520
|
+
val frameWidth = cursor.u16Le()
|
|
521
|
+
val frameHeight = cursor.u16Le()
|
|
522
|
+
if (
|
|
523
|
+
frameWidth <= 0 ||
|
|
524
|
+
frameHeight <= 0 ||
|
|
525
|
+
frameX.toLong() + frameWidth.toLong() > canvasWidth.toLong() ||
|
|
526
|
+
frameY.toLong() + frameHeight.toLong() > canvasHeight.toLong()
|
|
527
|
+
) {
|
|
528
|
+
fail("GIF frame dimensions are invalid")
|
|
529
|
+
}
|
|
530
|
+
val framePacked = cursor.u8()
|
|
531
|
+
if (framePacked and 0x80 != 0) cursor.skip(colorTableBytes(framePacked))
|
|
532
|
+
val minimumCodeSize = cursor.u8()
|
|
533
|
+
if (minimumCodeSize !in 2..12) fail("GIF LZW metadata is invalid")
|
|
534
|
+
cursor.skipSubBlocks()
|
|
535
|
+
frameCount += 1L
|
|
536
|
+
OneKeyImageSafety.requireAnimationFrameCount(frameCount)
|
|
537
|
+
totalDurationMs = safeDurationAdd(totalDurationMs, currentFrameDurationMs)
|
|
538
|
+
}
|
|
539
|
+
else -> fail("GIF block marker is invalid")
|
|
540
|
+
}
|
|
541
|
+
}
|
|
542
|
+
if (!trailerFound || frameCount == 0L) fail("GIF animation is truncated")
|
|
543
|
+
}
|
|
544
|
+
|
|
545
|
+
private fun validateWebP(bytes: ByteArray, length: Int) {
|
|
546
|
+
val cursor = ByteCursor(bytes, length)
|
|
547
|
+
if (cursor.ascii(4) != "RIFF") fail("WebP RIFF signature is invalid")
|
|
548
|
+
val declaredSize = cursor.u32Le()
|
|
549
|
+
if (declaredSize + 8L != length.toLong()) fail("WebP RIFF size is invalid")
|
|
550
|
+
if (cursor.ascii(4) != "WEBP") fail("WebP signature is invalid")
|
|
551
|
+
|
|
552
|
+
var canvasWidth = 0L
|
|
553
|
+
var canvasHeight = 0L
|
|
554
|
+
var animationFlag = false
|
|
555
|
+
var frameCount = 0L
|
|
556
|
+
var totalDurationMs = 0L
|
|
557
|
+
while (cursor.hasRemaining()) {
|
|
558
|
+
if (cursor.remaining() < 8) fail("WebP chunk header is truncated")
|
|
559
|
+
val chunkType = cursor.ascii(4)
|
|
560
|
+
val chunkSize = cursor.u32Le()
|
|
561
|
+
if (chunkSize > cursor.remaining().toLong()) fail("WebP chunk is truncated")
|
|
562
|
+
val payloadStart = cursor.position
|
|
563
|
+
when (chunkType) {
|
|
564
|
+
"VP8X" -> {
|
|
565
|
+
if (chunkSize < 10L) fail("WebP VP8X metadata is invalid")
|
|
566
|
+
val flags = cursor.u8()
|
|
567
|
+
animationFlag = flags and 0x02 != 0
|
|
568
|
+
cursor.skip(3)
|
|
569
|
+
canvasWidth = cursor.u24Le().toLong() + 1L
|
|
570
|
+
canvasHeight = cursor.u24Le().toLong() + 1L
|
|
571
|
+
OneKeyImageSafety.requireAnimatedDimensions(canvasWidth, canvasHeight)
|
|
572
|
+
}
|
|
573
|
+
"ANMF" -> {
|
|
574
|
+
if (!animationFlag || canvasWidth <= 0L || canvasHeight <= 0L || chunkSize < 16L) {
|
|
575
|
+
fail("Animated WebP frame metadata is invalid")
|
|
576
|
+
}
|
|
577
|
+
val frameX = cursor.u24Le().toLong() * 2L
|
|
578
|
+
val frameY = cursor.u24Le().toLong() * 2L
|
|
579
|
+
val frameWidth = cursor.u24Le().toLong() + 1L
|
|
580
|
+
val frameHeight = cursor.u24Le().toLong() + 1L
|
|
581
|
+
val durationMs = cursor.u24Le().toLong()
|
|
582
|
+
cursor.u8() // blend/dispose flags
|
|
583
|
+
if (
|
|
584
|
+
frameX + frameWidth > canvasWidth ||
|
|
585
|
+
frameY + frameHeight > canvasHeight
|
|
586
|
+
) {
|
|
587
|
+
fail("Animated WebP frame exceeds its canvas")
|
|
588
|
+
}
|
|
589
|
+
frameCount += 1L
|
|
590
|
+
OneKeyImageSafety.requireAnimationFrameCount(frameCount)
|
|
591
|
+
totalDurationMs = safeDurationAdd(totalDurationMs, if (durationMs == 0L) 100L else durationMs)
|
|
592
|
+
}
|
|
593
|
+
}
|
|
594
|
+
cursor.position = checkedEnd(payloadStart, chunkSize, length)
|
|
595
|
+
if (chunkSize and 1L != 0L) cursor.skip(1)
|
|
596
|
+
}
|
|
597
|
+
if (!animationFlag || canvasWidth <= 0L || canvasHeight <= 0L || frameCount == 0L) {
|
|
598
|
+
fail("Animated WebP metadata is incomplete")
|
|
599
|
+
}
|
|
600
|
+
}
|
|
601
|
+
|
|
602
|
+
private fun validateApng(bytes: ByteArray, length: Int) {
|
|
603
|
+
val cursor = ByteCursor(bytes, length)
|
|
604
|
+
val signature = byteArrayOf(
|
|
605
|
+
0x89.toByte(), 0x50, 0x4e, 0x47, 0x0d, 0x0a, 0x1a, 0x0a,
|
|
606
|
+
)
|
|
607
|
+
if (!cursor.bytesEqual(signature)) fail("PNG signature is invalid")
|
|
608
|
+
|
|
609
|
+
var canvasWidth = 0L
|
|
610
|
+
var canvasHeight = 0L
|
|
611
|
+
var declaredFrames = 0L
|
|
612
|
+
var frameCount = 0L
|
|
613
|
+
var totalDurationMs = 0L
|
|
614
|
+
var sawIhdr = false
|
|
615
|
+
var sawActl = false
|
|
616
|
+
var sawIdat = false
|
|
617
|
+
var sawIend = false
|
|
618
|
+
while (cursor.hasRemaining()) {
|
|
619
|
+
if (cursor.remaining() < 12) fail("PNG chunk header is truncated")
|
|
620
|
+
val chunkSize = cursor.u32Be()
|
|
621
|
+
val chunkType = cursor.ascii(4)
|
|
622
|
+
if (chunkSize + 4L > cursor.remaining().toLong()) fail("PNG chunk is truncated")
|
|
623
|
+
val payloadStart = cursor.position
|
|
624
|
+
when (chunkType) {
|
|
625
|
+
"IHDR" -> {
|
|
626
|
+
if (sawIhdr || payloadStart != 16 || chunkSize != 13L) fail("PNG IHDR is invalid")
|
|
627
|
+
canvasWidth = cursor.u32Be()
|
|
628
|
+
canvasHeight = cursor.u32Be()
|
|
629
|
+
OneKeyImageSafety.requireAnimatedDimensions(canvasWidth, canvasHeight)
|
|
630
|
+
sawIhdr = true
|
|
631
|
+
}
|
|
632
|
+
"acTL" -> {
|
|
633
|
+
if (!sawIhdr || sawActl || sawIdat || chunkSize != 8L) fail("APNG acTL is invalid")
|
|
634
|
+
declaredFrames = cursor.u32Be()
|
|
635
|
+
cursor.u32Be() // play count
|
|
636
|
+
if (declaredFrames == 0L) fail("APNG frame count is invalid")
|
|
637
|
+
OneKeyImageSafety.requireAnimationFrameCount(declaredFrames)
|
|
638
|
+
sawActl = true
|
|
639
|
+
}
|
|
640
|
+
"fcTL" -> {
|
|
641
|
+
if (!sawActl || chunkSize != 26L) fail("APNG fcTL is invalid")
|
|
642
|
+
cursor.u32Be() // sequence number
|
|
643
|
+
val frameWidth = cursor.u32Be()
|
|
644
|
+
val frameHeight = cursor.u32Be()
|
|
645
|
+
val frameX = cursor.u32Be()
|
|
646
|
+
val frameY = cursor.u32Be()
|
|
647
|
+
val delayNumerator = cursor.u16Be()
|
|
648
|
+
val rawDelayDenominator = cursor.u16Be()
|
|
649
|
+
val delayDenominator = if (rawDelayDenominator == 0) 100 else rawDelayDenominator
|
|
650
|
+
cursor.u8()
|
|
651
|
+
cursor.u8()
|
|
652
|
+
if (
|
|
653
|
+
frameWidth <= 0L ||
|
|
654
|
+
frameHeight <= 0L ||
|
|
655
|
+
frameX + frameWidth > canvasWidth ||
|
|
656
|
+
frameY + frameHeight > canvasHeight
|
|
657
|
+
) {
|
|
658
|
+
fail("APNG frame dimensions are invalid")
|
|
659
|
+
}
|
|
660
|
+
frameCount += 1L
|
|
661
|
+
OneKeyImageSafety.requireAnimationFrameCount(frameCount)
|
|
662
|
+
val decodedDurationMs = delayNumerator.toLong() * 1000L / delayDenominator.toLong()
|
|
663
|
+
// penfeizhou stores these fields as signed shorts and normalizes sub-10ms
|
|
664
|
+
// frame durations to 100ms. Ambiguous signed values use the larger duration
|
|
665
|
+
// so the safety accounting never undercounts the decoder's result.
|
|
666
|
+
val signedValueIsAmbiguous =
|
|
667
|
+
delayNumerator > Short.MAX_VALUE ||
|
|
668
|
+
(rawDelayDenominator != 0 && rawDelayDenominator > Short.MAX_VALUE)
|
|
669
|
+
val effectiveDurationMs =
|
|
670
|
+
if (decodedDurationMs < 10L || signedValueIsAmbiguous) {
|
|
671
|
+
maxOf(decodedDurationMs, 100L)
|
|
672
|
+
} else {
|
|
673
|
+
decodedDurationMs
|
|
674
|
+
}
|
|
675
|
+
totalDurationMs = safeDurationAdd(totalDurationMs, effectiveDurationMs)
|
|
676
|
+
}
|
|
677
|
+
"IDAT" -> {
|
|
678
|
+
if (!sawActl || frameCount == 0L) fail("APNG IDAT appears before frame metadata")
|
|
679
|
+
sawIdat = true
|
|
680
|
+
}
|
|
681
|
+
"IEND" -> {
|
|
682
|
+
if (chunkSize != 0L) fail("PNG IEND is invalid")
|
|
683
|
+
sawIend = true
|
|
684
|
+
}
|
|
685
|
+
}
|
|
686
|
+
cursor.position = checkedEnd(payloadStart, chunkSize, length)
|
|
687
|
+
cursor.skip(4) // CRC
|
|
688
|
+
if (sawIend) break
|
|
689
|
+
}
|
|
690
|
+
if (
|
|
691
|
+
!sawIhdr ||
|
|
692
|
+
!sawActl ||
|
|
693
|
+
!sawIdat ||
|
|
694
|
+
!sawIend ||
|
|
695
|
+
frameCount == 0L ||
|
|
696
|
+
frameCount != declaredFrames ||
|
|
697
|
+
cursor.hasRemaining()
|
|
698
|
+
) {
|
|
699
|
+
fail("APNG animation is truncated or inconsistent")
|
|
700
|
+
}
|
|
701
|
+
}
|
|
702
|
+
|
|
703
|
+
private fun colorTableBytes(packed: Int): Int = 3 * (1 shl ((packed and 0x07) + 1))
|
|
704
|
+
|
|
705
|
+
private fun checkedEnd(start: Int, size: Long, limit: Int): Int {
|
|
706
|
+
if (size < 0L || size > Int.MAX_VALUE.toLong()) fail("Image chunk size is invalid")
|
|
707
|
+
val end = start.toLong() + size
|
|
708
|
+
if (end > limit.toLong()) fail("Image chunk is truncated")
|
|
709
|
+
return end.toInt()
|
|
710
|
+
}
|
|
711
|
+
|
|
712
|
+
private fun safeDurationAdd(currentMs: Long, additionalMs: Long): Long {
|
|
713
|
+
if (additionalMs < 0L || currentMs > Long.MAX_VALUE - additionalMs) {
|
|
714
|
+
fail("Animation duration overflows")
|
|
715
|
+
}
|
|
716
|
+
val result = currentMs + additionalMs
|
|
717
|
+
if (result > OneKeyImageSafety.MAX_ANIMATION_DURATION_MS) {
|
|
718
|
+
fail("Animated image exceeds the 60 second limit")
|
|
719
|
+
}
|
|
720
|
+
return result
|
|
721
|
+
}
|
|
722
|
+
|
|
723
|
+
private fun fail(message: String): Nothing = throw OneKeyImageSafetyException(message)
|
|
724
|
+
|
|
725
|
+
private class ByteCursor(
|
|
726
|
+
private val bytes: ByteArray,
|
|
727
|
+
private val limit: Int,
|
|
728
|
+
) {
|
|
729
|
+
var position: Int = 0
|
|
730
|
+
set(value) {
|
|
731
|
+
if (value !in 0..limit) fail("Image metadata position is invalid")
|
|
732
|
+
field = value
|
|
733
|
+
}
|
|
734
|
+
|
|
735
|
+
fun hasRemaining(): Boolean = position < limit
|
|
736
|
+
fun remaining(): Int = limit - position
|
|
737
|
+
|
|
738
|
+
fun u8(): Int {
|
|
739
|
+
requireBytes(1)
|
|
740
|
+
return bytes[position++].toInt() and 0xff
|
|
741
|
+
}
|
|
742
|
+
|
|
743
|
+
fun u16Le(): Int = u8() or (u8() shl 8)
|
|
744
|
+
fun u16Be(): Int = (u8() shl 8) or u8()
|
|
745
|
+
fun u24Le(): Int = u8() or (u8() shl 8) or (u8() shl 16)
|
|
746
|
+
fun u32Le(): Long =
|
|
747
|
+
u8().toLong() or
|
|
748
|
+
(u8().toLong() shl 8) or
|
|
749
|
+
(u8().toLong() shl 16) or
|
|
750
|
+
(u8().toLong() shl 24)
|
|
751
|
+
|
|
752
|
+
fun u32Be(): Long =
|
|
753
|
+
(u8().toLong() shl 24) or
|
|
754
|
+
(u8().toLong() shl 16) or
|
|
755
|
+
(u8().toLong() shl 8) or
|
|
756
|
+
u8().toLong()
|
|
757
|
+
|
|
758
|
+
fun ascii(length: Int): String {
|
|
759
|
+
requireBytes(length)
|
|
760
|
+
return String(bytes, position, length, Charsets.US_ASCII).also { position += length }
|
|
761
|
+
}
|
|
762
|
+
|
|
763
|
+
fun skip(length: Int) {
|
|
764
|
+
requireBytes(length)
|
|
765
|
+
position += length
|
|
766
|
+
}
|
|
767
|
+
|
|
768
|
+
fun skipSubBlocks() {
|
|
769
|
+
while (true) {
|
|
770
|
+
val blockLength = u8()
|
|
771
|
+
if (blockLength == 0) return
|
|
772
|
+
skip(blockLength)
|
|
773
|
+
}
|
|
774
|
+
}
|
|
775
|
+
|
|
776
|
+
fun bytesEqual(expected: ByteArray): Boolean {
|
|
777
|
+
requireBytes(expected.size)
|
|
778
|
+
for (index in expected.indices) {
|
|
779
|
+
if (bytes[position + index] != expected[index]) return false
|
|
780
|
+
}
|
|
781
|
+
position += expected.size
|
|
782
|
+
return true
|
|
783
|
+
}
|
|
784
|
+
|
|
785
|
+
private fun requireBytes(count: Int) {
|
|
786
|
+
if (count < 0 || count > remaining()) fail("Animated image metadata is truncated")
|
|
787
|
+
}
|
|
788
|
+
}
|
|
789
|
+
}
|
|
790
|
+
|
|
791
|
+
internal class OneKeyImageSafetyVersionedKey(
|
|
792
|
+
private val delegate: Key,
|
|
793
|
+
) : Key {
|
|
794
|
+
override fun updateDiskCacheKey(messageDigest: MessageDigest) {
|
|
795
|
+
delegate.updateDiskCacheKey(messageDigest)
|
|
796
|
+
messageDigest.update(VERSION_BYTES)
|
|
797
|
+
}
|
|
798
|
+
|
|
799
|
+
override fun equals(other: Any?): Boolean =
|
|
800
|
+
other is OneKeyImageSafetyVersionedKey && delegate == other.delegate
|
|
801
|
+
|
|
802
|
+
override fun hashCode(): Int = 31 * delegate.hashCode() + CACHE_VERSION.hashCode()
|
|
803
|
+
|
|
804
|
+
companion object {
|
|
805
|
+
internal const val CACHE_VERSION = "onekey-image-safety-v2"
|
|
806
|
+
private val VERSION_BYTES = CACHE_VERSION.toByteArray(Key.CHARSET)
|
|
807
|
+
}
|
|
808
|
+
}
|
|
809
|
+
|
|
810
|
+
internal class OneKeyImageRemoteSourceKey(
|
|
811
|
+
private val requestUrl: String,
|
|
812
|
+
private val headersDigest: String?,
|
|
813
|
+
) : Key {
|
|
814
|
+
override fun updateDiskCacheKey(messageDigest: MessageDigest) {
|
|
815
|
+
val urlBytes = requestUrl.toByteArray(Key.CHARSET)
|
|
816
|
+
messageDigest.update(REMOTE_KEY_MARKER_BYTES)
|
|
817
|
+
messageDigest.update(ByteBuffer.allocate(Int.SIZE_BYTES).putInt(urlBytes.size).array())
|
|
818
|
+
messageDigest.update(urlBytes)
|
|
819
|
+
if (headersDigest != null) {
|
|
820
|
+
messageDigest.update(HEADERS_MARKER_BYTES)
|
|
821
|
+
messageDigest.update(headersDigest.toByteArray(Key.CHARSET))
|
|
822
|
+
}
|
|
823
|
+
}
|
|
824
|
+
|
|
825
|
+
override fun equals(other: Any?): Boolean =
|
|
826
|
+
other is OneKeyImageRemoteSourceKey &&
|
|
827
|
+
requestUrl == other.requestUrl &&
|
|
828
|
+
headersDigest == other.headersDigest
|
|
829
|
+
|
|
830
|
+
override fun hashCode(): Int = 31 * requestUrl.hashCode() + headersDigest.hashCode()
|
|
831
|
+
|
|
832
|
+
companion object {
|
|
833
|
+
private val REMOTE_KEY_MARKER_BYTES = "onekey-remote-source-url".toByteArray(Key.CHARSET)
|
|
834
|
+
private val HEADERS_MARKER_BYTES = "onekey-remote-headers-sha256".toByteArray(Key.CHARSET)
|
|
835
|
+
}
|
|
836
|
+
}
|
|
837
|
+
|
|
838
|
+
internal class OneKeyLimitedInputStream(
|
|
839
|
+
private val source: InputStream,
|
|
840
|
+
private val maximumBytes: Long = OneKeyImageSafety.MAX_ENCODED_BYTES,
|
|
841
|
+
) : FilterInputStream(source) {
|
|
842
|
+
private var position = 0L
|
|
843
|
+
private var furthestPosition = 0L
|
|
844
|
+
private var markedPosition = 0L
|
|
845
|
+
|
|
846
|
+
override fun read(): Int {
|
|
847
|
+
val value = source.read()
|
|
848
|
+
if (value >= 0) advance(1L)
|
|
849
|
+
return value
|
|
850
|
+
}
|
|
851
|
+
|
|
852
|
+
override fun read(buffer: ByteArray, offset: Int, length: Int): Int {
|
|
853
|
+
val count = source.read(buffer, offset, length)
|
|
854
|
+
if (count > 0) advance(count.toLong())
|
|
855
|
+
return count
|
|
856
|
+
}
|
|
857
|
+
|
|
858
|
+
override fun skip(byteCount: Long): Long {
|
|
859
|
+
val skipped = source.skip(byteCount)
|
|
860
|
+
if (skipped > 0L) advance(skipped)
|
|
861
|
+
return skipped
|
|
862
|
+
}
|
|
863
|
+
|
|
864
|
+
override fun mark(readLimit: Int) {
|
|
865
|
+
source.mark(readLimit)
|
|
866
|
+
markedPosition = position
|
|
867
|
+
}
|
|
868
|
+
|
|
869
|
+
override fun reset() {
|
|
870
|
+
source.reset()
|
|
871
|
+
position = markedPosition
|
|
872
|
+
}
|
|
873
|
+
|
|
874
|
+
private fun advance(byteCount: Long) {
|
|
875
|
+
position += byteCount
|
|
876
|
+
furthestPosition = maxOf(furthestPosition, position)
|
|
877
|
+
OneKeyImageSafety.requireEncodedLength(furthestPosition, maximumBytes)
|
|
878
|
+
}
|
|
879
|
+
}
|
|
880
|
+
|
|
881
|
+
internal class OneKeyImageRemoteModelLoaderFactory(
|
|
882
|
+
callFactory: Call.Factory,
|
|
883
|
+
) : ModelLoaderFactory<OneKeyImageRemoteModel, InputStream> {
|
|
884
|
+
private val delegateFactory = OkHttpUrlLoader.Factory(callFactory)
|
|
885
|
+
|
|
886
|
+
override fun build(
|
|
887
|
+
multiFactory: MultiModelLoaderFactory,
|
|
888
|
+
): ModelLoader<OneKeyImageRemoteModel, InputStream> =
|
|
889
|
+
OneKeyImageRemoteModelLoader(delegateFactory.build(multiFactory))
|
|
890
|
+
|
|
891
|
+
override fun teardown() = delegateFactory.teardown()
|
|
892
|
+
}
|
|
893
|
+
|
|
894
|
+
private class OneKeyImageRemoteModelLoader(
|
|
895
|
+
private val delegate: ModelLoader<com.bumptech.glide.load.model.GlideUrl, InputStream>,
|
|
896
|
+
) : ModelLoader<OneKeyImageRemoteModel, InputStream> {
|
|
897
|
+
override fun handles(model: OneKeyImageRemoteModel): Boolean = delegate.handles(model.glideUrl)
|
|
898
|
+
|
|
899
|
+
override fun buildLoadData(
|
|
900
|
+
model: OneKeyImageRemoteModel,
|
|
901
|
+
width: Int,
|
|
902
|
+
height: Int,
|
|
903
|
+
options: Options,
|
|
904
|
+
): ModelLoader.LoadData<InputStream>? {
|
|
905
|
+
val loadData = delegate.buildLoadData(model.glideUrl, width, height, options) ?: return null
|
|
906
|
+
val sourceKey = OneKeyImageSafetyVersionedKey(
|
|
907
|
+
OneKeyImageRemoteSourceKey(model.glideUrl.cacheKey, model.headersDigest),
|
|
908
|
+
)
|
|
909
|
+
return ModelLoader.LoadData(
|
|
910
|
+
sourceKey,
|
|
911
|
+
emptyList(),
|
|
912
|
+
OneKeySafetyInspectingFetcher(loadData.fetcher, limitInput = false),
|
|
913
|
+
)
|
|
914
|
+
}
|
|
915
|
+
}
|
|
916
|
+
|
|
917
|
+
internal class OneKeyImageLocalModelLoaderFactory(
|
|
918
|
+
context: Context,
|
|
919
|
+
) : ModelLoaderFactory<OneKeyImageLocalModel, InputStream> {
|
|
920
|
+
private val appContext = context.applicationContext
|
|
921
|
+
|
|
922
|
+
override fun build(
|
|
923
|
+
multiFactory: MultiModelLoaderFactory,
|
|
924
|
+
): ModelLoader<OneKeyImageLocalModel, InputStream> = OneKeyImageLocalModelLoader(
|
|
925
|
+
appContext,
|
|
926
|
+
multiFactory.build(Uri::class.java, InputStream::class.java),
|
|
927
|
+
)
|
|
928
|
+
|
|
929
|
+
override fun teardown() = Unit
|
|
930
|
+
}
|
|
931
|
+
|
|
932
|
+
private class OneKeyImageLocalModelLoader(
|
|
933
|
+
private val context: Context,
|
|
934
|
+
private val delegate: ModelLoader<Uri, InputStream>,
|
|
935
|
+
) : ModelLoader<OneKeyImageLocalModel, InputStream> {
|
|
936
|
+
override fun handles(model: OneKeyImageLocalModel): Boolean = delegate.handles(model.uri)
|
|
937
|
+
|
|
938
|
+
override fun buildLoadData(
|
|
939
|
+
model: OneKeyImageLocalModel,
|
|
940
|
+
width: Int,
|
|
941
|
+
height: Int,
|
|
942
|
+
options: Options,
|
|
943
|
+
): ModelLoader.LoadData<InputStream>? {
|
|
944
|
+
val loadData = delegate.buildLoadData(model.uri, width, height, options) ?: return null
|
|
945
|
+
return ModelLoader.LoadData(
|
|
946
|
+
OneKeyImageSafetyVersionedKey(loadData.sourceKey),
|
|
947
|
+
loadData.alternateKeys.map(::OneKeyImageSafetyVersionedKey),
|
|
948
|
+
OneKeyLimitedLocalFetcher(context, model.uri, loadData.fetcher),
|
|
949
|
+
)
|
|
950
|
+
}
|
|
951
|
+
}
|
|
952
|
+
|
|
953
|
+
private class OneKeyLimitedLocalFetcher(
|
|
954
|
+
private val context: Context,
|
|
955
|
+
private val uri: Uri,
|
|
956
|
+
private val delegate: DataFetcher<InputStream>,
|
|
957
|
+
) : DataFetcher<InputStream> {
|
|
958
|
+
override fun cleanup() = delegate.cleanup()
|
|
959
|
+
override fun cancel() = delegate.cancel()
|
|
960
|
+
override fun getDataClass(): Class<InputStream> = InputStream::class.java
|
|
961
|
+
override fun getDataSource(): DataSource = delegate.dataSource
|
|
962
|
+
|
|
963
|
+
override fun loadData(
|
|
964
|
+
priority: Priority,
|
|
965
|
+
callback: DataFetcher.DataCallback<in InputStream>,
|
|
966
|
+
) {
|
|
967
|
+
val knownLength = knownLocalLength(context, uri)
|
|
968
|
+
if (knownLength > OneKeyImageSafety.MAX_ENCODED_BYTES) {
|
|
969
|
+
callback.onLoadFailed(
|
|
970
|
+
OneKeyImageSafetyException("Encoded image exceeds the 32 MiB limit"),
|
|
971
|
+
)
|
|
972
|
+
return
|
|
973
|
+
}
|
|
974
|
+
delegate.loadData(priority, object : DataFetcher.DataCallback<InputStream> {
|
|
975
|
+
override fun onDataReady(data: InputStream?) {
|
|
976
|
+
if (data == null) {
|
|
977
|
+
callback.onDataReady(null)
|
|
978
|
+
return
|
|
979
|
+
}
|
|
980
|
+
try {
|
|
981
|
+
callback.onDataReady(inspectInput(data, limitInput = true))
|
|
982
|
+
} catch (error: IOException) {
|
|
983
|
+
callback.onLoadFailed(error)
|
|
984
|
+
}
|
|
985
|
+
}
|
|
986
|
+
|
|
987
|
+
override fun onLoadFailed(error: Exception) = callback.onLoadFailed(error)
|
|
988
|
+
})
|
|
989
|
+
}
|
|
990
|
+
|
|
991
|
+
private fun knownLocalLength(context: Context, uri: Uri): Long = try {
|
|
992
|
+
when (uri.scheme) {
|
|
993
|
+
"file" -> {
|
|
994
|
+
val path = uri.path
|
|
995
|
+
if (path != null && path.startsWith("/android_asset/")) {
|
|
996
|
+
context.assets.openFd(path.removePrefix("/android_asset/")).use { it.length }
|
|
997
|
+
} else {
|
|
998
|
+
path?.let(::File)?.takeIf(File::isFile)?.length() ?: -1L
|
|
999
|
+
}
|
|
1000
|
+
}
|
|
1001
|
+
"content", "android.resource" ->
|
|
1002
|
+
context.contentResolver.openAssetFileDescriptor(uri, "r")?.use { it.length } ?: -1L
|
|
1003
|
+
else -> -1L
|
|
1004
|
+
}
|
|
1005
|
+
} catch (_: Exception) {
|
|
1006
|
+
-1L
|
|
1007
|
+
}
|
|
1008
|
+
}
|
|
1009
|
+
|
|
1010
|
+
private class OneKeySafetyInspectingFetcher(
|
|
1011
|
+
private val delegate: DataFetcher<InputStream>,
|
|
1012
|
+
private val limitInput: Boolean,
|
|
1013
|
+
) : DataFetcher<InputStream> {
|
|
1014
|
+
override fun cleanup() = delegate.cleanup()
|
|
1015
|
+
override fun cancel() = delegate.cancel()
|
|
1016
|
+
override fun getDataClass(): Class<InputStream> = InputStream::class.java
|
|
1017
|
+
override fun getDataSource(): DataSource = delegate.dataSource
|
|
1018
|
+
|
|
1019
|
+
override fun loadData(
|
|
1020
|
+
priority: Priority,
|
|
1021
|
+
callback: DataFetcher.DataCallback<in InputStream>,
|
|
1022
|
+
) {
|
|
1023
|
+
delegate.loadData(priority, object : DataFetcher.DataCallback<InputStream> {
|
|
1024
|
+
override fun onDataReady(data: InputStream?) {
|
|
1025
|
+
if (data == null) {
|
|
1026
|
+
callback.onDataReady(null)
|
|
1027
|
+
return
|
|
1028
|
+
}
|
|
1029
|
+
try {
|
|
1030
|
+
callback.onDataReady(inspectInput(data, limitInput))
|
|
1031
|
+
} catch (error: IOException) {
|
|
1032
|
+
callback.onLoadFailed(error)
|
|
1033
|
+
}
|
|
1034
|
+
}
|
|
1035
|
+
|
|
1036
|
+
override fun onLoadFailed(error: Exception) = callback.onLoadFailed(error)
|
|
1037
|
+
})
|
|
1038
|
+
}
|
|
1039
|
+
}
|
|
1040
|
+
|
|
1041
|
+
private fun inspectInput(input: InputStream, limitInput: Boolean): InputStream {
|
|
1042
|
+
val source = if (limitInput) OneKeyLimitedInputStream(input) else input
|
|
1043
|
+
val buffered = BufferedInputStream(source)
|
|
1044
|
+
return try {
|
|
1045
|
+
val metadata = OneKeyEncodedImageInspector.inspect(buffered)
|
|
1046
|
+
if (metadata.animated) bufferReplayableAnimation(buffered, metadata) else buffered
|
|
1047
|
+
} catch (error: IOException) {
|
|
1048
|
+
buffered.close()
|
|
1049
|
+
throw error
|
|
1050
|
+
}
|
|
1051
|
+
}
|
|
1052
|
+
|
|
1053
|
+
private fun bufferReplayableAnimation(
|
|
1054
|
+
input: InputStream,
|
|
1055
|
+
metadata: OneKeyEncodedImageMetadata,
|
|
1056
|
+
): InputStream {
|
|
1057
|
+
val output = OneKeyReplayBuffer()
|
|
1058
|
+
val buffer = ByteArray(16 * 1024)
|
|
1059
|
+
while (true) {
|
|
1060
|
+
val count = input.read(buffer)
|
|
1061
|
+
if (count < 0) break
|
|
1062
|
+
if (count == 0) continue
|
|
1063
|
+
OneKeyImageSafety.requireEncodedLength(
|
|
1064
|
+
output.size().toLong() + count.toLong(),
|
|
1065
|
+
OneKeyImageSafety.MAX_ANIMATED_ENCODED_BYTES,
|
|
1066
|
+
)
|
|
1067
|
+
output.write(buffer, 0, count)
|
|
1068
|
+
}
|
|
1069
|
+
val format = metadata.animatedFormat
|
|
1070
|
+
?: throw OneKeyImageSafetyException("Animated image format is unavailable")
|
|
1071
|
+
output.validate(format)
|
|
1072
|
+
return output.inputStream()
|
|
1073
|
+
}
|
|
1074
|
+
|
|
1075
|
+
private class OneKeyReplayBuffer : ByteArrayOutputStream(16 * 1024) {
|
|
1076
|
+
fun validate(format: OneKeyAnimatedFormat) {
|
|
1077
|
+
OneKeyAnimationTimelineInspector.validate(format, buf, count)
|
|
1078
|
+
}
|
|
1079
|
+
|
|
1080
|
+
fun inputStream(): InputStream = ByteArrayInputStream(buf, 0, count)
|
|
1081
|
+
}
|
|
1082
|
+
|
|
1083
|
+
internal class OneKeyImageEncodedDataInterceptor : Interceptor {
|
|
1084
|
+
override fun intercept(chain: Interceptor.Chain): okhttp3.Response {
|
|
1085
|
+
val response = chain.proceed(chain.request())
|
|
1086
|
+
val body = response.body ?: return response
|
|
1087
|
+
try {
|
|
1088
|
+
OneKeyImageSafety.requireEncodedLength(body.contentLength())
|
|
1089
|
+
} catch (error: OneKeyImageSafetyException) {
|
|
1090
|
+
response.close()
|
|
1091
|
+
throw error
|
|
1092
|
+
}
|
|
1093
|
+
return response.newBuilder()
|
|
1094
|
+
.body(OneKeyLimitedResponseBody(body))
|
|
1095
|
+
.build()
|
|
1096
|
+
}
|
|
1097
|
+
}
|
|
1098
|
+
|
|
1099
|
+
private class OneKeyLimitedResponseBody(
|
|
1100
|
+
private val delegate: ResponseBody,
|
|
1101
|
+
) : ResponseBody() {
|
|
1102
|
+
private val limitedSource: BufferedSource by lazy {
|
|
1103
|
+
object : ForwardingSource(delegate.source()) {
|
|
1104
|
+
private var receivedBytes = 0L
|
|
1105
|
+
|
|
1106
|
+
override fun read(sink: Buffer, byteCount: Long): Long {
|
|
1107
|
+
val count = super.read(sink, byteCount)
|
|
1108
|
+
if (count > 0L) {
|
|
1109
|
+
receivedBytes += count
|
|
1110
|
+
OneKeyImageSafety.requireEncodedLength(receivedBytes)
|
|
1111
|
+
}
|
|
1112
|
+
return count
|
|
1113
|
+
}
|
|
1114
|
+
}.buffer()
|
|
1115
|
+
}
|
|
1116
|
+
|
|
1117
|
+
override fun contentType(): MediaType? = delegate.contentType()
|
|
1118
|
+
override fun contentLength(): Long = delegate.contentLength()
|
|
1119
|
+
override fun source(): BufferedSource = limitedSource
|
|
1120
|
+
}
|