@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.
Files changed (186) hide show
  1. package/LICENSE +21 -0
  2. package/OneKeyImage.podspec +32 -0
  3. package/README.md +88 -0
  4. package/android/CMakeLists.txt +12 -0
  5. package/android/build.gradle +85 -0
  6. package/android/consumer-rules.pro +10 -0
  7. package/android/gradle.properties +5 -0
  8. package/android/src/main/AndroidManifest.xml +1 -0
  9. package/android/src/main/cpp/cpp-adapter.cpp +6 -0
  10. package/android/src/main/java/com/margelo/nitro/onekeyimage/OneKeyImage.kt +616 -0
  11. package/android/src/main/java/com/margelo/nitro/onekeyimage/OneKeyImageCache.kt +131 -0
  12. package/android/src/main/java/com/margelo/nitro/onekeyimage/OneKeyImageDecodeDimensions.kt +169 -0
  13. package/android/src/main/java/com/margelo/nitro/onekeyimage/OneKeyImageGlideRegistry.kt +285 -0
  14. package/android/src/main/java/com/margelo/nitro/onekeyimage/OneKeyImageModel.kt +68 -0
  15. package/android/src/main/java/com/margelo/nitro/onekeyimage/OneKeyImagePackage.kt +28 -0
  16. package/android/src/main/java/com/margelo/nitro/onekeyimage/OneKeyImageSafety.kt +1120 -0
  17. package/android/src/main/java/com/margelo/nitro/onekeyimage/OneKeyTosUrl.kt +93 -0
  18. package/android/src/test/java/com/margelo/nitro/onekeyimage/OneKeyAnimationTimelineInspectorTest.kt +210 -0
  19. package/android/src/test/java/com/margelo/nitro/onekeyimage/OneKeyImageCacheTypeTest.kt +16 -0
  20. package/android/src/test/java/com/margelo/nitro/onekeyimage/OneKeyImageDecodeDimensionsTest.kt +70 -0
  21. package/android/src/test/java/com/margelo/nitro/onekeyimage/OneKeyImageModelTest.kt +65 -0
  22. package/android/src/test/java/com/margelo/nitro/onekeyimage/OneKeyImageRequestSignatureTest.kt +33 -0
  23. package/android/src/test/java/com/margelo/nitro/onekeyimage/OneKeyImageSafetyTest.kt +346 -0
  24. package/android/src/test/java/com/margelo/nitro/onekeyimage/OneKeyResourceCleanupTest.kt +43 -0
  25. package/android/src/test/java/com/margelo/nitro/onekeyimage/OneKeyTerminalEventPairTest.kt +38 -0
  26. package/android/src/test/java/com/margelo/nitro/onekeyimage/OneKeyTosUrlTest.kt +82 -0
  27. package/ios/OneKeyImage.swift +636 -0
  28. package/ios/OneKeyImageCache.swift +215 -0
  29. package/ios/OneKeyImageRequestContext.swift +781 -0
  30. package/ios/OneKeyTosURL.swift +69 -0
  31. package/ios/tests/OneKeyImageInfrastructureTests.swift +555 -0
  32. package/ios/tests/OneKeyTosURLTests.swift +142 -0
  33. package/lib/module/OneKeyImage.nitro.js +30 -0
  34. package/lib/module/index.js +223 -0
  35. package/lib/module/package.json +1 -0
  36. package/lib/nitrogen/generated/android/c++/JFunc_void.hpp +75 -0
  37. package/lib/nitrogen/generated/android/c++/JFunc_void_double_double_OneKeyImageCacheType.hpp +77 -0
  38. package/lib/nitrogen/generated/android/c++/JFunc_void_std__string.hpp +76 -0
  39. package/lib/nitrogen/generated/android/c++/JHybridOneKeyImageCacheSpec.cpp +130 -0
  40. package/lib/nitrogen/generated/android/c++/JHybridOneKeyImageCacheSpec.hpp +66 -0
  41. package/lib/nitrogen/generated/android/c++/JHybridOneKeyImageSpec.cpp +255 -0
  42. package/lib/nitrogen/generated/android/c++/JHybridOneKeyImageSpec.hpp +93 -0
  43. package/lib/nitrogen/generated/android/c++/JOneKeyImageCachePolicy.hpp +64 -0
  44. package/lib/nitrogen/generated/android/c++/JOneKeyImageCacheType.hpp +61 -0
  45. package/lib/nitrogen/generated/android/c++/JOneKeyImageContentFit.hpp +64 -0
  46. package/lib/nitrogen/generated/android/c++/JOneKeyImageLoadingStrategy.hpp +61 -0
  47. package/lib/nitrogen/generated/android/c++/JOneKeyImagePreloadSource.hpp +88 -0
  48. package/lib/nitrogen/generated/android/c++/JOneKeyImageVariant.hpp +64 -0
  49. package/lib/nitrogen/generated/android/c++/views/JHybridOneKeyImageStateUpdater.cpp +144 -0
  50. package/lib/nitrogen/generated/android/c++/views/JHybridOneKeyImageStateUpdater.hpp +54 -0
  51. package/lib/nitrogen/generated/android/kotlin/com/margelo/nitro/onekeyimage/Func_void.kt +78 -0
  52. package/lib/nitrogen/generated/android/kotlin/com/margelo/nitro/onekeyimage/Func_void_double_double_OneKeyImageCacheType.kt +78 -0
  53. package/lib/nitrogen/generated/android/kotlin/com/margelo/nitro/onekeyimage/Func_void_std__string.kt +78 -0
  54. package/lib/nitrogen/generated/android/kotlin/com/margelo/nitro/onekeyimage/HybridOneKeyImageCacheSpec.kt +69 -0
  55. package/lib/nitrogen/generated/android/kotlin/com/margelo/nitro/onekeyimage/HybridOneKeyImageSpec.kt +189 -0
  56. package/lib/nitrogen/generated/android/kotlin/com/margelo/nitro/onekeyimage/OneKeyImageCachePolicy.kt +25 -0
  57. package/lib/nitrogen/generated/android/kotlin/com/margelo/nitro/onekeyimage/OneKeyImageCacheType.kt +24 -0
  58. package/lib/nitrogen/generated/android/kotlin/com/margelo/nitro/onekeyimage/OneKeyImageContentFit.kt +25 -0
  59. package/lib/nitrogen/generated/android/kotlin/com/margelo/nitro/onekeyimage/OneKeyImageLoadingStrategy.kt +24 -0
  60. package/lib/nitrogen/generated/android/kotlin/com/margelo/nitro/onekeyimage/OneKeyImagePreloadSource.kt +86 -0
  61. package/lib/nitrogen/generated/android/kotlin/com/margelo/nitro/onekeyimage/OneKeyImageVariant.kt +25 -0
  62. package/lib/nitrogen/generated/android/kotlin/com/margelo/nitro/onekeyimage/onekeyimageOnLoad.kt +35 -0
  63. package/lib/nitrogen/generated/android/kotlin/com/margelo/nitro/onekeyimage/views/HybridOneKeyImageManager.kt +96 -0
  64. package/lib/nitrogen/generated/android/kotlin/com/margelo/nitro/onekeyimage/views/HybridOneKeyImageStateUpdater.kt +23 -0
  65. package/lib/nitrogen/generated/android/onekeyimage+autolinking.cmake +85 -0
  66. package/lib/nitrogen/generated/android/onekeyimage+autolinking.gradle +27 -0
  67. package/lib/nitrogen/generated/android/onekeyimageOnLoad.cpp +78 -0
  68. package/lib/nitrogen/generated/android/onekeyimageOnLoad.hpp +34 -0
  69. package/lib/nitrogen/generated/ios/OneKeyImage+autolinking.rb +62 -0
  70. package/lib/nitrogen/generated/ios/OneKeyImage-Swift-Cxx-Bridge.cpp +90 -0
  71. package/lib/nitrogen/generated/ios/OneKeyImage-Swift-Cxx-Bridge.hpp +405 -0
  72. package/lib/nitrogen/generated/ios/OneKeyImage-Swift-Cxx-Umbrella.hpp +71 -0
  73. package/lib/nitrogen/generated/ios/OneKeyImageAutolinking.mm +41 -0
  74. package/lib/nitrogen/generated/ios/OneKeyImageAutolinking.swift +38 -0
  75. package/lib/nitrogen/generated/ios/c++/HybridOneKeyImageCacheSpecSwift.cpp +11 -0
  76. package/lib/nitrogen/generated/ios/c++/HybridOneKeyImageCacheSpecSwift.hpp +114 -0
  77. package/lib/nitrogen/generated/ios/c++/HybridOneKeyImageSpecSwift.cpp +11 -0
  78. package/lib/nitrogen/generated/ios/c++/HybridOneKeyImageSpecSwift.hpp +206 -0
  79. package/lib/nitrogen/generated/ios/c++/views/HybridOneKeyImageComponent.mm +228 -0
  80. package/lib/nitrogen/generated/ios/swift/Func_void.swift +46 -0
  81. package/lib/nitrogen/generated/ios/swift/Func_void_bool.swift +46 -0
  82. package/lib/nitrogen/generated/ios/swift/Func_void_double_double_OneKeyImageCacheType.swift +46 -0
  83. package/lib/nitrogen/generated/ios/swift/Func_void_std__exception_ptr.swift +46 -0
  84. package/lib/nitrogen/generated/ios/swift/Func_void_std__string.swift +46 -0
  85. package/lib/nitrogen/generated/ios/swift/HybridOneKeyImageCacheSpec.swift +58 -0
  86. package/lib/nitrogen/generated/ios/swift/HybridOneKeyImageCacheSpec_cxx.swift +202 -0
  87. package/lib/nitrogen/generated/ios/swift/HybridOneKeyImageSpec.swift +70 -0
  88. package/lib/nitrogen/generated/ios/swift/HybridOneKeyImageSpec_cxx.swift +539 -0
  89. package/lib/nitrogen/generated/ios/swift/OneKeyImageCachePolicy.swift +48 -0
  90. package/lib/nitrogen/generated/ios/swift/OneKeyImageCacheType.swift +44 -0
  91. package/lib/nitrogen/generated/ios/swift/OneKeyImageContentFit.swift +48 -0
  92. package/lib/nitrogen/generated/ios/swift/OneKeyImageLoadingStrategy.swift +44 -0
  93. package/lib/nitrogen/generated/ios/swift/OneKeyImagePreloadSource.swift +148 -0
  94. package/lib/nitrogen/generated/ios/swift/OneKeyImageVariant.swift +48 -0
  95. package/lib/nitrogen/generated/shared/c++/HybridOneKeyImageCacheSpec.cpp +24 -0
  96. package/lib/nitrogen/generated/shared/c++/HybridOneKeyImageCacheSpec.hpp +68 -0
  97. package/lib/nitrogen/generated/shared/c++/HybridOneKeyImageSpec.cpp +52 -0
  98. package/lib/nitrogen/generated/shared/c++/HybridOneKeyImageSpec.hpp +108 -0
  99. package/lib/nitrogen/generated/shared/c++/OneKeyImageCachePolicy.hpp +84 -0
  100. package/lib/nitrogen/generated/shared/c++/OneKeyImageCacheType.hpp +80 -0
  101. package/lib/nitrogen/generated/shared/c++/OneKeyImageContentFit.hpp +84 -0
  102. package/lib/nitrogen/generated/shared/c++/OneKeyImageLoadingStrategy.hpp +80 -0
  103. package/lib/nitrogen/generated/shared/c++/OneKeyImagePreloadSource.hpp +114 -0
  104. package/lib/nitrogen/generated/shared/c++/OneKeyImageVariant.hpp +84 -0
  105. package/lib/nitrogen/generated/shared/c++/views/HybridOneKeyImageComponent.cpp +62 -0
  106. package/lib/nitrogen/generated/shared/c++/views/HybridOneKeyImageComponent.hpp +132 -0
  107. package/lib/nitrogen/generated/shared/json/OneKeyImageConfig.json +24 -0
  108. package/lib/typescript/package.json +1 -0
  109. package/lib/typescript/src/OneKeyImage.nitro.d.ts +67 -0
  110. package/lib/typescript/src/index.d.ts +55 -0
  111. package/nitro.json +31 -0
  112. package/nitrogen/generated/android/c++/JFunc_void.hpp +75 -0
  113. package/nitrogen/generated/android/c++/JFunc_void_double_double_OneKeyImageCacheType.hpp +77 -0
  114. package/nitrogen/generated/android/c++/JFunc_void_std__string.hpp +76 -0
  115. package/nitrogen/generated/android/c++/JHybridOneKeyImageCacheSpec.cpp +130 -0
  116. package/nitrogen/generated/android/c++/JHybridOneKeyImageCacheSpec.hpp +66 -0
  117. package/nitrogen/generated/android/c++/JHybridOneKeyImageSpec.cpp +255 -0
  118. package/nitrogen/generated/android/c++/JHybridOneKeyImageSpec.hpp +93 -0
  119. package/nitrogen/generated/android/c++/JOneKeyImageCachePolicy.hpp +64 -0
  120. package/nitrogen/generated/android/c++/JOneKeyImageCacheType.hpp +61 -0
  121. package/nitrogen/generated/android/c++/JOneKeyImageContentFit.hpp +64 -0
  122. package/nitrogen/generated/android/c++/JOneKeyImageLoadingStrategy.hpp +61 -0
  123. package/nitrogen/generated/android/c++/JOneKeyImagePreloadSource.hpp +88 -0
  124. package/nitrogen/generated/android/c++/JOneKeyImageVariant.hpp +64 -0
  125. package/nitrogen/generated/android/c++/views/JHybridOneKeyImageStateUpdater.cpp +144 -0
  126. package/nitrogen/generated/android/c++/views/JHybridOneKeyImageStateUpdater.hpp +54 -0
  127. package/nitrogen/generated/android/kotlin/com/margelo/nitro/onekeyimage/Func_void.kt +78 -0
  128. package/nitrogen/generated/android/kotlin/com/margelo/nitro/onekeyimage/Func_void_double_double_OneKeyImageCacheType.kt +78 -0
  129. package/nitrogen/generated/android/kotlin/com/margelo/nitro/onekeyimage/Func_void_std__string.kt +78 -0
  130. package/nitrogen/generated/android/kotlin/com/margelo/nitro/onekeyimage/HybridOneKeyImageCacheSpec.kt +69 -0
  131. package/nitrogen/generated/android/kotlin/com/margelo/nitro/onekeyimage/HybridOneKeyImageSpec.kt +189 -0
  132. package/nitrogen/generated/android/kotlin/com/margelo/nitro/onekeyimage/OneKeyImageCachePolicy.kt +25 -0
  133. package/nitrogen/generated/android/kotlin/com/margelo/nitro/onekeyimage/OneKeyImageCacheType.kt +24 -0
  134. package/nitrogen/generated/android/kotlin/com/margelo/nitro/onekeyimage/OneKeyImageContentFit.kt +25 -0
  135. package/nitrogen/generated/android/kotlin/com/margelo/nitro/onekeyimage/OneKeyImageLoadingStrategy.kt +24 -0
  136. package/nitrogen/generated/android/kotlin/com/margelo/nitro/onekeyimage/OneKeyImagePreloadSource.kt +86 -0
  137. package/nitrogen/generated/android/kotlin/com/margelo/nitro/onekeyimage/OneKeyImageVariant.kt +25 -0
  138. package/nitrogen/generated/android/kotlin/com/margelo/nitro/onekeyimage/onekeyimageOnLoad.kt +35 -0
  139. package/nitrogen/generated/android/kotlin/com/margelo/nitro/onekeyimage/views/HybridOneKeyImageManager.kt +96 -0
  140. package/nitrogen/generated/android/kotlin/com/margelo/nitro/onekeyimage/views/HybridOneKeyImageStateUpdater.kt +23 -0
  141. package/nitrogen/generated/android/onekeyimage+autolinking.cmake +85 -0
  142. package/nitrogen/generated/android/onekeyimage+autolinking.gradle +27 -0
  143. package/nitrogen/generated/android/onekeyimageOnLoad.cpp +78 -0
  144. package/nitrogen/generated/android/onekeyimageOnLoad.hpp +34 -0
  145. package/nitrogen/generated/ios/OneKeyImage+autolinking.rb +62 -0
  146. package/nitrogen/generated/ios/OneKeyImage-Swift-Cxx-Bridge.cpp +90 -0
  147. package/nitrogen/generated/ios/OneKeyImage-Swift-Cxx-Bridge.hpp +405 -0
  148. package/nitrogen/generated/ios/OneKeyImage-Swift-Cxx-Umbrella.hpp +71 -0
  149. package/nitrogen/generated/ios/OneKeyImageAutolinking.mm +41 -0
  150. package/nitrogen/generated/ios/OneKeyImageAutolinking.swift +38 -0
  151. package/nitrogen/generated/ios/c++/HybridOneKeyImageCacheSpecSwift.cpp +11 -0
  152. package/nitrogen/generated/ios/c++/HybridOneKeyImageCacheSpecSwift.hpp +114 -0
  153. package/nitrogen/generated/ios/c++/HybridOneKeyImageSpecSwift.cpp +11 -0
  154. package/nitrogen/generated/ios/c++/HybridOneKeyImageSpecSwift.hpp +206 -0
  155. package/nitrogen/generated/ios/c++/views/HybridOneKeyImageComponent.mm +228 -0
  156. package/nitrogen/generated/ios/swift/Func_void.swift +46 -0
  157. package/nitrogen/generated/ios/swift/Func_void_bool.swift +46 -0
  158. package/nitrogen/generated/ios/swift/Func_void_double_double_OneKeyImageCacheType.swift +46 -0
  159. package/nitrogen/generated/ios/swift/Func_void_std__exception_ptr.swift +46 -0
  160. package/nitrogen/generated/ios/swift/Func_void_std__string.swift +46 -0
  161. package/nitrogen/generated/ios/swift/HybridOneKeyImageCacheSpec.swift +58 -0
  162. package/nitrogen/generated/ios/swift/HybridOneKeyImageCacheSpec_cxx.swift +202 -0
  163. package/nitrogen/generated/ios/swift/HybridOneKeyImageSpec.swift +70 -0
  164. package/nitrogen/generated/ios/swift/HybridOneKeyImageSpec_cxx.swift +539 -0
  165. package/nitrogen/generated/ios/swift/OneKeyImageCachePolicy.swift +48 -0
  166. package/nitrogen/generated/ios/swift/OneKeyImageCacheType.swift +44 -0
  167. package/nitrogen/generated/ios/swift/OneKeyImageContentFit.swift +48 -0
  168. package/nitrogen/generated/ios/swift/OneKeyImageLoadingStrategy.swift +44 -0
  169. package/nitrogen/generated/ios/swift/OneKeyImagePreloadSource.swift +148 -0
  170. package/nitrogen/generated/ios/swift/OneKeyImageVariant.swift +48 -0
  171. package/nitrogen/generated/shared/c++/HybridOneKeyImageCacheSpec.cpp +24 -0
  172. package/nitrogen/generated/shared/c++/HybridOneKeyImageCacheSpec.hpp +68 -0
  173. package/nitrogen/generated/shared/c++/HybridOneKeyImageSpec.cpp +52 -0
  174. package/nitrogen/generated/shared/c++/HybridOneKeyImageSpec.hpp +108 -0
  175. package/nitrogen/generated/shared/c++/OneKeyImageCachePolicy.hpp +84 -0
  176. package/nitrogen/generated/shared/c++/OneKeyImageCacheType.hpp +80 -0
  177. package/nitrogen/generated/shared/c++/OneKeyImageContentFit.hpp +84 -0
  178. package/nitrogen/generated/shared/c++/OneKeyImageLoadingStrategy.hpp +80 -0
  179. package/nitrogen/generated/shared/c++/OneKeyImagePreloadSource.hpp +114 -0
  180. package/nitrogen/generated/shared/c++/OneKeyImageVariant.hpp +84 -0
  181. package/nitrogen/generated/shared/c++/views/HybridOneKeyImageComponent.cpp +62 -0
  182. package/nitrogen/generated/shared/c++/views/HybridOneKeyImageComponent.hpp +132 -0
  183. package/nitrogen/generated/shared/json/OneKeyImageConfig.json +24 -0
  184. package/package.json +128 -0
  185. package/src/OneKeyImage.nitro.ts +86 -0
  186. 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
+ }