@hot-updater/react-native 1.0.0-rc.3 → 1.0.0-rc.31

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 (300) hide show
  1. package/android/proguard-rules.pro +17 -2
  2. package/android/src/main/java/com/hotupdater/BuiltInAssetResolver.kt +286 -0
  3. package/android/src/main/java/com/hotupdater/BundleFileStorageService.kt +954 -837
  4. package/android/src/main/java/com/hotupdater/BundleMetadata.kt +15 -11
  5. package/android/src/main/java/com/hotupdater/FileManagerService.kt +45 -0
  6. package/android/src/main/java/com/hotupdater/HotUpdaterException.kt +287 -22
  7. package/android/src/main/java/com/hotupdater/HotUpdaterImpl.kt +43 -31
  8. package/android/src/main/java/com/hotupdater/HotUpdaterRecoveryManager.kt +37 -54
  9. package/android/src/main/java/com/hotupdater/HotUpdaterRecoveryState.kt +64 -0
  10. package/android/src/main/java/com/hotupdater/InstallIdentityService.kt +37 -0
  11. package/android/src/main/java/com/hotupdater/KeyValueStorageService.kt +86 -0
  12. package/android/src/main/java/com/hotupdater/NetworkState.kt +31 -0
  13. package/android/src/main/java/com/hotupdater/NoBackupStorage.kt +17 -0
  14. package/android/src/main/java/com/hotupdater/OkHttpDownloadService.kt +127 -50
  15. package/android/src/main/java/com/hotupdater/PreviousProcessExit.kt +89 -0
  16. package/android/src/main/java/com/hotupdater/ReactNativeValueConverters.kt +34 -0
  17. package/android/src/main/java/com/hotupdater/TarArchiveInputStream.kt +214 -375
  18. package/android/src/main/java/com/hotupdater/TarBrArchiveExtractor.kt +119 -0
  19. package/android/src/newarch/HotUpdater.kt +7 -9
  20. package/android/src/newarch/HotUpdaterModule.kt +109 -84
  21. package/android/src/oldarch/HotUpdater.kt +7 -9
  22. package/android/src/oldarch/HotUpdaterModule.kt +98 -74
  23. package/android/src/oldarch/HotUpdaterSpec.kt +4 -6
  24. package/ios/HotUpdater/Internal/BrotliFileDecompressor.swift +167 -0
  25. package/ios/HotUpdater/Internal/BuiltInAssetResolver.swift +196 -0
  26. package/ios/HotUpdater/Internal/BundleFileStorageService.swift +950 -1112
  27. package/ios/HotUpdater/Internal/BundleMetadata.swift +8 -53
  28. package/ios/HotUpdater/Internal/FileManagerService.swift +48 -0
  29. package/ios/HotUpdater/Internal/FileUtilities.swift +64 -0
  30. package/ios/HotUpdater/Internal/HotUpdater-Bridging-Header.h +0 -1
  31. package/ios/HotUpdater/Internal/HotUpdater.mm +13 -45
  32. package/ios/HotUpdater/Internal/HotUpdaterImpl.swift +190 -73
  33. package/ios/HotUpdater/Internal/KeyValueStorageService.swift +71 -0
  34. package/ios/HotUpdater/Internal/NotificationExtension.swift +0 -2
  35. package/ios/HotUpdater/Internal/TarArchiveExtractor.swift +182 -311
  36. package/ios/HotUpdater/Internal/URLSessionDownloadService.swift +119 -71
  37. package/ios/HotUpdater/Internal/UpdateFailure.swift +390 -0
  38. package/ios/HotUpdater/Recovery/HotUpdaterRecovery.h +16 -0
  39. package/ios/HotUpdater/Recovery/HotUpdaterRecovery.mm +83 -0
  40. package/ios/HotUpdater/Recovery/HotUpdaterRecoveryHooks.mm +49 -0
  41. package/lib/commonjs/appReady.js +111 -0
  42. package/lib/commonjs/appReady.js.map +1 -0
  43. package/lib/commonjs/checkForUpdate.js +124 -69
  44. package/lib/commonjs/checkForUpdate.js.map +1 -1
  45. package/lib/commonjs/clientPlugin.js +13 -0
  46. package/lib/commonjs/clientPlugin.js.map +1 -0
  47. package/lib/commonjs/error.js +4 -13
  48. package/lib/commonjs/error.js.map +1 -1
  49. package/lib/commonjs/fetchJSON.js +16 -27
  50. package/lib/commonjs/fetchJSON.js.map +1 -1
  51. package/lib/commonjs/fetchUpdateResponse.js +54 -0
  52. package/lib/commonjs/fetchUpdateResponse.js.map +1 -0
  53. package/lib/commonjs/httpClient.js +63 -94
  54. package/lib/commonjs/httpClient.js.map +1 -1
  55. package/lib/commonjs/index.js +71 -18
  56. package/lib/commonjs/index.js.map +1 -1
  57. package/lib/commonjs/native.js +78 -60
  58. package/lib/commonjs/native.js.map +1 -1
  59. package/lib/commonjs/pluginHost.js +44 -0
  60. package/lib/commonjs/pluginHost.js.map +1 -0
  61. package/lib/commonjs/releaseCatalogCache.js +36 -87
  62. package/lib/commonjs/releaseCatalogCache.js.map +1 -1
  63. package/lib/commonjs/sdkVersion.js +1 -1
  64. package/lib/commonjs/sdkVersion.js.map +1 -1
  65. package/lib/commonjs/specs/NativeHotUpdater.js.map +1 -1
  66. package/lib/commonjs/store.js +3 -10
  67. package/lib/commonjs/store.js.map +1 -1
  68. package/lib/commonjs/updateError.js +132 -0
  69. package/lib/commonjs/updateError.js.map +1 -0
  70. package/lib/commonjs/wrap.js +43 -26
  71. package/lib/commonjs/wrap.js.map +1 -1
  72. package/lib/module/appReady.js +103 -0
  73. package/lib/module/appReady.js.map +1 -0
  74. package/lib/module/checkForUpdate.js +117 -64
  75. package/lib/module/checkForUpdate.js.map +1 -1
  76. package/lib/module/clientPlugin.js +4 -0
  77. package/lib/module/clientPlugin.js.map +1 -0
  78. package/lib/module/error.js +4 -13
  79. package/lib/module/error.js.map +1 -1
  80. package/lib/module/fetchJSON.js +14 -26
  81. package/lib/module/fetchJSON.js.map +1 -1
  82. package/lib/module/fetchUpdateResponse.js +49 -0
  83. package/lib/module/fetchUpdateResponse.js.map +1 -0
  84. package/lib/module/httpClient.js +62 -93
  85. package/lib/module/httpClient.js.map +1 -1
  86. package/lib/module/index.js +58 -20
  87. package/lib/module/index.js.map +1 -1
  88. package/lib/module/native.js +71 -53
  89. package/lib/module/native.js.map +1 -1
  90. package/lib/module/pluginHost.js +35 -0
  91. package/lib/module/pluginHost.js.map +1 -0
  92. package/lib/module/releaseCatalogCache.js +32 -82
  93. package/lib/module/releaseCatalogCache.js.map +1 -1
  94. package/lib/module/sdkVersion.js +1 -1
  95. package/lib/module/sdkVersion.js.map +1 -1
  96. package/lib/module/specs/NativeHotUpdater.js.map +1 -1
  97. package/lib/module/store.js +3 -10
  98. package/lib/module/store.js.map +1 -1
  99. package/lib/module/updateError.js +126 -0
  100. package/lib/module/updateError.js.map +1 -0
  101. package/lib/module/wrap.js +43 -26
  102. package/lib/module/wrap.js.map +1 -1
  103. package/lib/typescript/commonjs/appReady.d.ts +23 -0
  104. package/lib/typescript/commonjs/appReady.d.ts.map +1 -0
  105. package/lib/typescript/commonjs/checkForUpdate.d.ts +16 -4
  106. package/lib/typescript/commonjs/checkForUpdate.d.ts.map +1 -1
  107. package/lib/typescript/commonjs/clientPlugin.d.ts +2 -0
  108. package/lib/typescript/commonjs/clientPlugin.d.ts.map +1 -0
  109. package/lib/typescript/commonjs/error.d.ts +4 -13
  110. package/lib/typescript/commonjs/error.d.ts.map +1 -1
  111. package/lib/typescript/commonjs/fetchJSON.d.ts +5 -5
  112. package/lib/typescript/commonjs/fetchJSON.d.ts.map +1 -1
  113. package/lib/typescript/commonjs/fetchUpdateResponse.d.ts +14 -0
  114. package/lib/typescript/commonjs/fetchUpdateResponse.d.ts.map +1 -0
  115. package/lib/typescript/commonjs/httpClient.d.ts +4 -33
  116. package/lib/typescript/commonjs/httpClient.d.ts.map +1 -1
  117. package/lib/typescript/commonjs/index.d.ts +4 -8
  118. package/lib/typescript/commonjs/index.d.ts.map +1 -1
  119. package/lib/typescript/commonjs/native.d.ts +31 -22
  120. package/lib/typescript/commonjs/native.d.ts.map +1 -1
  121. package/lib/typescript/commonjs/pluginHost.d.ts +4 -0
  122. package/lib/typescript/commonjs/pluginHost.d.ts.map +1 -0
  123. package/lib/typescript/commonjs/releaseCatalogCache.d.ts +8 -14
  124. package/lib/typescript/commonjs/releaseCatalogCache.d.ts.map +1 -1
  125. package/lib/typescript/commonjs/sdkVersion.d.ts +1 -1
  126. package/lib/typescript/commonjs/sdkVersion.d.ts.map +1 -1
  127. package/lib/typescript/commonjs/specs/NativeHotUpdater.d.ts +42 -47
  128. package/lib/typescript/commonjs/specs/NativeHotUpdater.d.ts.map +1 -1
  129. package/lib/typescript/commonjs/store.d.ts +1 -3
  130. package/lib/typescript/commonjs/store.d.ts.map +1 -1
  131. package/lib/typescript/commonjs/types.d.ts +1 -1
  132. package/lib/typescript/commonjs/types.d.ts.map +1 -1
  133. package/lib/typescript/commonjs/updateError.d.ts +26 -0
  134. package/lib/typescript/commonjs/updateError.d.ts.map +1 -0
  135. package/lib/typescript/commonjs/wrap.d.ts.map +1 -1
  136. package/lib/typescript/commonjs/wrap.types.d.ts +6 -7
  137. package/lib/typescript/commonjs/wrap.types.d.ts.map +1 -1
  138. package/lib/typescript/module/appReady.d.ts +23 -0
  139. package/lib/typescript/module/appReady.d.ts.map +1 -0
  140. package/lib/typescript/module/checkForUpdate.d.ts +16 -4
  141. package/lib/typescript/module/checkForUpdate.d.ts.map +1 -1
  142. package/lib/typescript/module/clientPlugin.d.ts +2 -0
  143. package/lib/typescript/module/clientPlugin.d.ts.map +1 -0
  144. package/lib/typescript/module/error.d.ts +4 -13
  145. package/lib/typescript/module/error.d.ts.map +1 -1
  146. package/lib/typescript/module/fetchJSON.d.ts +5 -5
  147. package/lib/typescript/module/fetchJSON.d.ts.map +1 -1
  148. package/lib/typescript/module/fetchUpdateResponse.d.ts +14 -0
  149. package/lib/typescript/module/fetchUpdateResponse.d.ts.map +1 -0
  150. package/lib/typescript/module/httpClient.d.ts +4 -33
  151. package/lib/typescript/module/httpClient.d.ts.map +1 -1
  152. package/lib/typescript/module/index.d.ts +4 -8
  153. package/lib/typescript/module/index.d.ts.map +1 -1
  154. package/lib/typescript/module/native.d.ts +31 -22
  155. package/lib/typescript/module/native.d.ts.map +1 -1
  156. package/lib/typescript/module/pluginHost.d.ts +4 -0
  157. package/lib/typescript/module/pluginHost.d.ts.map +1 -0
  158. package/lib/typescript/module/releaseCatalogCache.d.ts +8 -14
  159. package/lib/typescript/module/releaseCatalogCache.d.ts.map +1 -1
  160. package/lib/typescript/module/sdkVersion.d.ts +1 -1
  161. package/lib/typescript/module/sdkVersion.d.ts.map +1 -1
  162. package/lib/typescript/module/specs/NativeHotUpdater.d.ts +42 -47
  163. package/lib/typescript/module/specs/NativeHotUpdater.d.ts.map +1 -1
  164. package/lib/typescript/module/store.d.ts +1 -3
  165. package/lib/typescript/module/store.d.ts.map +1 -1
  166. package/lib/typescript/module/types.d.ts +1 -1
  167. package/lib/typescript/module/types.d.ts.map +1 -1
  168. package/lib/typescript/module/updateError.d.ts +26 -0
  169. package/lib/typescript/module/updateError.d.ts.map +1 -0
  170. package/lib/typescript/module/wrap.d.ts.map +1 -1
  171. package/lib/typescript/module/wrap.types.d.ts +6 -7
  172. package/lib/typescript/module/wrap.types.d.ts.map +1 -1
  173. package/package.json +7 -4
  174. package/src/appReady.ts +149 -0
  175. package/src/checkForUpdate.ts +151 -88
  176. package/src/clientPlugin.ts +18 -0
  177. package/src/error.ts +4 -14
  178. package/src/fetchJSON.ts +14 -30
  179. package/src/fetchUpdateResponse.ts +54 -0
  180. package/src/httpClient.ts +88 -159
  181. package/src/index.ts +90 -24
  182. package/src/native.ts +106 -102
  183. package/src/pluginHost.ts +45 -0
  184. package/src/releaseCatalogCache.ts +43 -172
  185. package/src/sdkVersion.ts +1 -1
  186. package/src/specs/NativeHotUpdater.ts +41 -48
  187. package/src/store.ts +2 -23
  188. package/src/types.ts +1 -1
  189. package/src/updateError.ts +153 -0
  190. package/src/wrap.tsx +57 -26
  191. package/src/wrap.types.ts +6 -7
  192. package/android/src/main/java/com/hotupdater/DecompressService.kt +0 -89
  193. package/android/src/main/java/com/hotupdater/DecompressionStrategy.kt +0 -26
  194. package/android/src/main/java/com/hotupdater/TarBrDecompressionStrategy.kt +0 -117
  195. package/android/src/main/java/com/hotupdater/TarGzDecompressionStrategy.kt +0 -117
  196. package/android/src/main/java/com/hotupdater/ZipDecompressionStrategy.kt +0 -192
  197. package/android/src/test/java/com/hotupdater/BsdiffPatchTest.kt +0 -46
  198. package/android/src/test/java/com/hotupdater/BundleFileStorageServiceTest.kt +0 -799
  199. package/android/src/test/java/com/hotupdater/HotUpdaterImplTest.kt +0 -273
  200. package/android/src/test/java/com/hotupdater/NativeConfigUtilsTest.kt +0 -20
  201. package/android/src/test/java/com/hotupdater/OkHttpDownloadServiceTest.kt +0 -111
  202. package/android/src/test/java/com/hotupdater/RelativePathResolverTest.kt +0 -41
  203. package/android/src/test/java/com/hotupdater/ReleaseCatalogCacheServiceTest.kt +0 -118
  204. package/android/src/test/java/com/hotupdater/TarArchiveInputStreamTest.kt +0 -68
  205. package/ios/HotUpdater/Internal/ArchiveExtractionUtilities.swift +0 -178
  206. package/ios/HotUpdater/Internal/DecompressService.swift +0 -112
  207. package/ios/HotUpdater/Internal/DecompressionStrategy.swift +0 -22
  208. package/ios/HotUpdater/Internal/StreamingTarArchiveExtractor.swift +0 -370
  209. package/ios/HotUpdater/Internal/TarBrDecompressionStrategy.swift +0 -39
  210. package/ios/HotUpdater/Internal/TarGzDecompressionStrategy.swift +0 -59
  211. package/ios/HotUpdater/Internal/ZipArchiveExtractor.swift +0 -482
  212. package/ios/HotUpdater/Internal/ZipDecompressionStrategy.swift +0 -56
  213. package/lib/commonjs/catalogCacheNative.spec.js +0 -31
  214. package/lib/commonjs/catalogCacheNative.spec.js.map +0 -1
  215. package/lib/commonjs/checkForUpdate.releaseCatalog.spec.js +0 -615
  216. package/lib/commonjs/checkForUpdate.releaseCatalog.spec.js.map +0 -1
  217. package/lib/commonjs/httpClient.spec.js +0 -102
  218. package/lib/commonjs/httpClient.spec.js.map +0 -1
  219. package/lib/commonjs/index.spec.js +0 -302
  220. package/lib/commonjs/index.spec.js.map +0 -1
  221. package/lib/commonjs/iosInterop.spec.js +0 -18
  222. package/lib/commonjs/iosInterop.spec.js.map +0 -1
  223. package/lib/commonjs/native.spec.js +0 -883
  224. package/lib/commonjs/native.spec.js.map +0 -1
  225. package/lib/commonjs/notifyAppReadyInsights.boundary.spec.js +0 -218
  226. package/lib/commonjs/notifyAppReadyInsights.boundary.spec.js.map +0 -1
  227. package/lib/commonjs/notifyAppReadyInsights.js +0 -133
  228. package/lib/commonjs/notifyAppReadyInsights.js.map +0 -1
  229. package/lib/commonjs/notifyAppReadyInsights.spec.js +0 -246
  230. package/lib/commonjs/notifyAppReadyInsights.spec.js.map +0 -1
  231. package/lib/commonjs/notifyAppReadyInsights.test-utils.js +0 -24
  232. package/lib/commonjs/notifyAppReadyInsights.test-utils.js.map +0 -1
  233. package/lib/commonjs/notifyAppReadyInsights.transport.spec.js +0 -117
  234. package/lib/commonjs/notifyAppReadyInsights.transport.spec.js.map +0 -1
  235. package/lib/commonjs/releaseCatalogCache.spec.js +0 -195
  236. package/lib/commonjs/releaseCatalogCache.spec.js.map +0 -1
  237. package/lib/commonjs/releaseCatalogCacheNativeContract.spec.js +0 -64
  238. package/lib/commonjs/releaseCatalogCacheNativeContract.spec.js.map +0 -1
  239. package/lib/commonjs/store.notification.spec.js +0 -76
  240. package/lib/commonjs/store.notification.spec.js.map +0 -1
  241. package/lib/commonjs/store.spec.js +0 -316
  242. package/lib/commonjs/store.spec.js.map +0 -1
  243. package/lib/commonjs/wrap.spec.js +0 -329
  244. package/lib/commonjs/wrap.spec.js.map +0 -1
  245. package/lib/module/catalogCacheNative.spec.js +0 -31
  246. package/lib/module/catalogCacheNative.spec.js.map +0 -1
  247. package/lib/module/checkForUpdate.releaseCatalog.spec.js +0 -614
  248. package/lib/module/checkForUpdate.releaseCatalog.spec.js.map +0 -1
  249. package/lib/module/httpClient.spec.js +0 -102
  250. package/lib/module/httpClient.spec.js.map +0 -1
  251. package/lib/module/index.spec.js +0 -301
  252. package/lib/module/index.spec.js.map +0 -1
  253. package/lib/module/iosInterop.spec.js +0 -18
  254. package/lib/module/iosInterop.spec.js.map +0 -1
  255. package/lib/module/native.spec.js +0 -882
  256. package/lib/module/native.spec.js.map +0 -1
  257. package/lib/module/notifyAppReadyInsights.boundary.spec.js +0 -217
  258. package/lib/module/notifyAppReadyInsights.boundary.spec.js.map +0 -1
  259. package/lib/module/notifyAppReadyInsights.js +0 -128
  260. package/lib/module/notifyAppReadyInsights.js.map +0 -1
  261. package/lib/module/notifyAppReadyInsights.spec.js +0 -245
  262. package/lib/module/notifyAppReadyInsights.spec.js.map +0 -1
  263. package/lib/module/notifyAppReadyInsights.test-utils.js +0 -18
  264. package/lib/module/notifyAppReadyInsights.test-utils.js.map +0 -1
  265. package/lib/module/notifyAppReadyInsights.transport.spec.js +0 -116
  266. package/lib/module/notifyAppReadyInsights.transport.spec.js.map +0 -1
  267. package/lib/module/releaseCatalogCache.spec.js +0 -195
  268. package/lib/module/releaseCatalogCache.spec.js.map +0 -1
  269. package/lib/module/releaseCatalogCacheNativeContract.spec.js +0 -64
  270. package/lib/module/releaseCatalogCacheNativeContract.spec.js.map +0 -1
  271. package/lib/module/store.notification.spec.js +0 -77
  272. package/lib/module/store.notification.spec.js.map +0 -1
  273. package/lib/module/store.spec.js +0 -315
  274. package/lib/module/store.spec.js.map +0 -1
  275. package/lib/module/wrap.spec.js +0 -330
  276. package/lib/module/wrap.spec.js.map +0 -1
  277. package/lib/typescript/commonjs/notifyAppReadyInsights.d.ts +0 -13
  278. package/lib/typescript/commonjs/notifyAppReadyInsights.d.ts.map +0 -1
  279. package/lib/typescript/commonjs/notifyAppReadyInsights.test-utils.d.ts +0 -8
  280. package/lib/typescript/commonjs/notifyAppReadyInsights.test-utils.d.ts.map +0 -1
  281. package/lib/typescript/module/notifyAppReadyInsights.d.ts +0 -13
  282. package/lib/typescript/module/notifyAppReadyInsights.d.ts.map +0 -1
  283. package/lib/typescript/module/notifyAppReadyInsights.test-utils.d.ts +0 -8
  284. package/lib/typescript/module/notifyAppReadyInsights.test-utils.d.ts.map +0 -1
  285. package/src/catalogCacheNative.spec.ts +0 -63
  286. package/src/checkForUpdate.releaseCatalog.spec.ts +0 -611
  287. package/src/httpClient.spec.ts +0 -132
  288. package/src/index.spec.ts +0 -337
  289. package/src/iosInterop.spec.ts +0 -30
  290. package/src/native.spec.ts +0 -945
  291. package/src/notifyAppReadyInsights.boundary.spec.ts +0 -234
  292. package/src/notifyAppReadyInsights.spec.ts +0 -243
  293. package/src/notifyAppReadyInsights.test-utils.ts +0 -31
  294. package/src/notifyAppReadyInsights.transport.spec.ts +0 -130
  295. package/src/notifyAppReadyInsights.ts +0 -194
  296. package/src/releaseCatalogCache.spec.ts +0 -282
  297. package/src/releaseCatalogCacheNativeContract.spec.ts +0 -80
  298. package/src/store.notification.spec.ts +0 -92
  299. package/src/store.spec.ts +0 -372
  300. package/src/wrap.spec.ts +0 -406
@@ -3,480 +3,319 @@ package com.hotupdater
3
3
  import java.io.EOFException
4
4
  import java.io.IOException
5
5
  import java.io.InputStream
6
+ import java.nio.ByteBuffer
7
+ import java.nio.charset.CodingErrorAction
6
8
 
7
- /**
8
- * Secure TAR archive input stream for Android API 21+
9
- * Replaces Apache Commons Compress to avoid java.nio.file dependencies
10
- */
11
- class TarArchiveInputStream(
9
+ internal data class TarArchiveEntry(
10
+ val name: String,
11
+ val size: Long,
12
+ val typeFlag: Char,
13
+ ) {
14
+ val isRegularFile: Boolean
15
+ get() = typeFlag == '0' || typeFlag == '\u0000'
16
+ }
17
+
18
+ /** Small, strict TAR reader for the deterministic bundle.tar.br transport. */
19
+ internal class TarArchiveInputStream(
12
20
  private val input: InputStream,
13
21
  ) : InputStream() {
14
- private var currentEntry: TarArchiveEntry? = null
15
- private var currentEntryBytesRead: Long = 0
16
- private var pendingName: String? = null
17
-
18
22
  companion object {
19
- private const val TAG = "TarInputStream"
20
23
  private const val BLOCK_SIZE = 512
21
- private const val NAME_OFFSET = 0
22
- private const val NAME_LENGTH = 100
23
- private const val MODE_OFFSET = 100
24
- private const val SIZE_OFFSET = 124
25
- private const val SIZE_LENGTH = 12
26
- private const val CHECKSUM_OFFSET = 148
27
- private const val CHECKSUM_LENGTH = 8
28
- private const val TYPEFLAG_OFFSET = 156
29
- private const val LINKNAME_OFFSET = 157
30
- private const val LINKNAME_LENGTH = 100
31
- private const val MAGIC_OFFSET = 257
32
- private const val PREFIX_OFFSET = 345
33
- private const val PREFIX_LENGTH = 155
34
-
35
- // Maximum file size: 1GB per file
36
- private const val MAX_FILE_SIZE = 1_073_741_824L
24
+ private const val MAX_EXTENSION_SIZE = 1024 * 1024L
37
25
  }
38
26
 
39
- /**
40
- * Get the next TAR entry
41
- */
42
- fun getNextEntry(): TarArchiveEntry? {
43
- // Skip remaining bytes of current entry
44
- if (currentEntry != null) {
45
- val remaining = currentEntry!!.size - currentEntryBytesRead
46
- if (remaining > 0) {
47
- skipBytes(remaining)
48
- }
49
- skipPadding(currentEntry!!.size)
50
- }
27
+ private var currentEntry: TarArchiveEntry? = null
28
+ private var currentEntryBytesRead = 0L
29
+ private var pendingPaxHeaders: Map<String, String>? = null
30
+ private var finished = false
51
31
 
52
- currentEntryBytesRead = 0
32
+ fun getNextEntry(): TarArchiveEntry? {
33
+ if (finished) return null
34
+ finishCurrentEntry()
53
35
 
54
36
  while (true) {
55
- val headerBytes = readBlock() ?: return null
56
-
57
- // Check for end of archive (all zeros)
58
- if (isAllZeros(headerBytes)) {
37
+ val header = readBlock(required = true)
38
+ if (header.all { it == 0.toByte() }) {
39
+ val secondEndBlock = readBlock(required = true)
40
+ if (!secondEndBlock.all { it == 0.toByte() } || pendingPaxHeaders != null) {
41
+ throw IOException("Invalid TAR termination")
42
+ }
43
+ consumeTrailingZeroBlocks()
44
+ finished = true
59
45
  return null
60
46
  }
61
47
 
62
- // Verify header
63
- if (!isValidHeader(headerBytes)) {
64
- throw IOException("Invalid TAR header")
65
- }
66
-
67
- if (!verifyChecksum(headerBytes)) {
68
- throw IOException("TAR header checksum verification failed")
69
- }
70
-
71
- // Parse header
72
- val entry = parseHeader(headerBytes)
73
-
74
- // Handle GNU long filename extension
75
- if (entry.typeFlag == 'L') {
76
- pendingName = readLongName(entry.size)
77
- continue
78
- }
48
+ verifyHeader(header)
49
+ val entry = parseHeader(header)
50
+ when (entry.typeFlag) {
51
+ 'x' -> {
52
+ if (pendingPaxHeaders != null) throw IOException("Consecutive PAX headers are not supported")
53
+ pendingPaxHeaders = readPaxHeaders(entry.size)
54
+ continue
55
+ }
79
56
 
80
- // Handle POSIX PAX extended header
81
- if (entry.typeFlag == 'x') {
82
- readPaxPath(entry.size)?.let { pendingName = it }
83
- continue
84
- }
57
+ 'g' -> {
58
+ throw IOException("Global PAX headers are not supported")
59
+ }
85
60
 
86
- // Apply extended name if present
87
- if (pendingName != null) {
88
- entry.name = pendingName!!
89
- pendingName = null
61
+ 'L' -> {
62
+ throw IOException("GNU long names are not supported")
63
+ }
90
64
  }
91
65
 
92
- // Validate entry
93
- validateEntry(entry)
94
-
95
- currentEntry = entry
96
- return entry
66
+ val paxHeaders = pendingPaxHeaders.orEmpty()
67
+ pendingPaxHeaders = null
68
+ if (paxHeaders.containsKey("linkpath")) throw IOException("TAR links are not allowed")
69
+ val resolvedEntry =
70
+ entry.copy(
71
+ name = paxHeaders["path"] ?: entry.name,
72
+ size = paxHeaders["size"]?.let(::parsePaxSize) ?: entry.size,
73
+ )
74
+ currentEntry = resolvedEntry
75
+ currentEntryBytesRead = 0
76
+ return resolvedEntry
97
77
  }
98
78
  }
99
79
 
100
80
  override fun read(): Int {
101
- val b = ByteArray(1)
102
- val n = read(b, 0, 1)
103
- return if (n <= 0) -1 else b[0].toInt() and 0xFF
81
+ val byte = ByteArray(1)
82
+ return if (read(byte, 0, 1) == -1) -1 else byte[0].toInt() and 0xff
104
83
  }
105
84
 
106
85
  override fun read(
107
- b: ByteArray,
108
- off: Int,
109
- len: Int,
86
+ buffer: ByteArray,
87
+ offset: Int,
88
+ length: Int,
110
89
  ): Int {
111
- if (currentEntry == null) {
112
- throw IllegalStateException("No current entry")
113
- }
114
-
115
- val remaining = currentEntry!!.size - currentEntryBytesRead
116
- if (remaining <= 0) {
117
- return -1
118
- }
119
-
120
- val toRead = minOf(len.toLong(), remaining).toInt()
121
- val bytesRead = input.read(b, off, toRead)
122
-
123
- if (bytesRead > 0) {
124
- currentEntryBytesRead += bytesRead
125
- }
126
-
127
- return bytesRead
90
+ val entry = currentEntry ?: throw IllegalStateException("No current TAR entry")
91
+ val remaining = entry.size - currentEntryBytesRead
92
+ if (remaining == 0L) return -1
93
+ val count = input.read(buffer, offset, minOf(length.toLong(), remaining).toInt())
94
+ if (count < 0) throw EOFException("TAR entry ${entry.name} is truncated")
95
+ if (count == 0) throw IOException("TAR entry read made no progress")
96
+ currentEntryBytesRead += count
97
+ return count
128
98
  }
129
99
 
130
- override fun close() {
131
- input.close()
100
+ private fun finishCurrentEntry() {
101
+ val entry = currentEntry ?: return
102
+ skipExactly(entry.size - currentEntryBytesRead)
103
+ readPadding(entry.size)
104
+ currentEntry = null
105
+ currentEntryBytesRead = 0
132
106
  }
133
107
 
134
- /**
135
- * Read a 512-byte block from input
136
- */
137
- private fun readBlock(): ByteArray? {
108
+ private fun readBlock(required: Boolean): ByteArray {
138
109
  val block = ByteArray(BLOCK_SIZE)
139
110
  var offset = 0
140
-
141
- while (offset < BLOCK_SIZE) {
142
- val n = input.read(block, offset, BLOCK_SIZE - offset)
143
- if (n < 0) {
144
- return if (offset == 0) null else throw EOFException("Unexpected end of TAR archive")
111
+ while (offset < block.size) {
112
+ val count = input.read(block, offset, block.size - offset)
113
+ if (count < 0) {
114
+ if (!required && offset == 0) return ByteArray(0)
115
+ throw EOFException("Unexpected end of TAR archive")
145
116
  }
146
- offset += n
117
+ if (count == 0) throw IOException("TAR stream made no progress")
118
+ offset += count
147
119
  }
148
-
149
120
  return block
150
121
  }
151
122
 
152
- /**
153
- * Check if block is all zeros
154
- */
155
- private fun isAllZeros(block: ByteArray): Boolean = block.all { it == 0.toByte() }
156
-
157
- /**
158
- * Verify TAR header has valid magic number
159
- */
160
- private fun isValidHeader(header: ByteArray): Boolean {
161
- // Check for "ustar" magic (may have \0 or space after)
162
- val magic = String(header, MAGIC_OFFSET, 5, Charsets.US_ASCII)
163
- return magic == "ustar"
123
+ private fun consumeTrailingZeroBlocks() {
124
+ while (true) {
125
+ val block = readBlock(required = false)
126
+ if (block.isEmpty()) return
127
+ if (!block.all { it == 0.toByte() }) {
128
+ throw IOException("TAR archive has trailing non-zero data")
129
+ }
130
+ }
164
131
  }
165
132
 
166
- /**
167
- * Verify header checksum
168
- */
169
- private fun verifyChecksum(header: ByteArray): Boolean {
170
- val storedChecksum = parseOctal(header, CHECKSUM_OFFSET, CHECKSUM_LENGTH).toInt()
171
-
172
- // Calculate checksums (both signed and unsigned for compatibility)
173
- var unsignedSum = 0
174
- var signedSum = 0
175
-
176
- for (i in 0 until BLOCK_SIZE) {
177
- val value =
178
- if (i in CHECKSUM_OFFSET until CHECKSUM_OFFSET + CHECKSUM_LENGTH) {
179
- 32 // Space character
180
- } else {
181
- header[i].toInt()
182
- }
183
-
184
- unsignedSum += value and 0xFF
185
- signedSum += value.toByte().toInt()
133
+ private fun verifyHeader(header: ByteArray) {
134
+ val magic = String(header, 257, 5, Charsets.US_ASCII)
135
+ if (magic != "ustar") throw IOException("Invalid TAR magic")
136
+
137
+ val storedChecksum = parseOctal(header, 148, 8)
138
+ var unsignedChecksum = 0L
139
+ var signedChecksum = 0L
140
+ header.forEachIndexed { index, byte ->
141
+ val value = if (index in 148 until 156) 32 else byte.toInt()
142
+ unsignedChecksum += value and 0xff
143
+ signedChecksum += value.toByte().toInt()
144
+ }
145
+ if (storedChecksum != unsignedChecksum && storedChecksum != signedChecksum) {
146
+ throw IOException("TAR header checksum mismatch")
186
147
  }
187
-
188
- return storedChecksum == unsignedSum || storedChecksum == signedSum
189
148
  }
190
149
 
191
- /**
192
- * Parse TAR header into TarArchiveEntry
193
- */
194
150
  private fun parseHeader(header: ByteArray): TarArchiveEntry {
195
- val name = parseString(header, NAME_OFFSET, NAME_LENGTH)
196
- val mode = parseOctal(header, MODE_OFFSET, 8).toInt()
197
- val size = parseNumeric(header, SIZE_OFFSET, SIZE_LENGTH)
198
- val typeFlag = header[TYPEFLAG_OFFSET].toInt().toChar()
199
- val linkName = parseString(header, LINKNAME_OFFSET, LINKNAME_LENGTH)
200
- val prefix = parseString(header, PREFIX_OFFSET, PREFIX_LENGTH)
201
-
202
- // Combine prefix and name
203
- val fullName = if (prefix.isNotEmpty()) "$prefix/$name" else name
204
-
151
+ val name = parseString(header, 0, 100)
152
+ val prefix = parseString(header, 345, 155)
153
+ val fullName =
154
+ when {
155
+ prefix.isEmpty() -> name
156
+ name.isEmpty() -> prefix
157
+ else -> "$prefix/$name"
158
+ }
205
159
  return TarArchiveEntry(
206
160
  name = fullName,
207
- mode = mode,
208
- size = size,
209
- typeFlag = typeFlag,
210
- linkName = linkName,
161
+ size = parseNumeric(header, 124, 12),
162
+ typeFlag = header[156].toInt().toChar(),
211
163
  )
212
164
  }
213
165
 
214
- /**
215
- * Parse string field from header
216
- */
217
166
  private fun parseString(
218
167
  bytes: ByteArray,
219
168
  offset: Int,
220
169
  length: Int,
221
170
  ): String {
222
171
  var end = offset
223
- while (end < offset + length && bytes[end] != 0.toByte()) {
224
- end++
225
- }
226
- return String(bytes, offset, end - offset, Charsets.UTF_8).trim()
172
+ while (end < offset + length && bytes[end] != 0.toByte()) end++
173
+ return decodeUtf8(bytes, offset, end - offset)
227
174
  }
228
175
 
229
- /**
230
- * Parse octal number from header field
231
- */
232
- private fun parseOctal(
176
+ private fun parseNumeric(
233
177
  bytes: ByteArray,
234
178
  offset: Int,
235
179
  length: Int,
236
180
  ): Long {
181
+ if ((bytes[offset].toInt() and 0x80) == 0) {
182
+ return parseOctal(bytes, offset, length)
183
+ }
184
+ if ((bytes[offset].toInt() and 0x40) != 0) {
185
+ throw IOException("Negative TAR sizes are not supported")
186
+ }
237
187
  var result = 0L
238
- var i = offset
239
- val end = offset + length
240
-
241
- // Skip leading spaces
242
- while (i < end && bytes[i] == ' '.code.toByte()) i++
243
-
244
- // Parse octal digits
245
- while (i < end) {
246
- val b = bytes[i]
247
- if (b == 0.toByte() || b == ' '.code.toByte()) break
248
- if (b < '0'.code.toByte() || b > '7'.code.toByte()) {
249
- throw IOException("Invalid octal digit: ${b.toInt()}")
188
+ for (index in offset until offset + length) {
189
+ val value = bytes[index].toInt() and 0xff
190
+ val payload = if (index == offset) value and 0x7f else value
191
+ if (result > (Long.MAX_VALUE - payload) / 256) {
192
+ throw IOException("TAR size overflows Long")
250
193
  }
251
- result = result * 8 + (b - '0'.code.toByte())
252
- i++
194
+ result = result * 256 + payload
253
195
  }
254
-
255
196
  return result
256
197
  }
257
198
 
258
- /**
259
- * Parse numeric field (supports both octal and base-256 encoding)
260
- */
261
- private fun parseNumeric(
262
- bytes: ByteArray,
263
- offset: Int,
264
- length: Int,
265
- ): Long {
266
- // Check for base-256 encoding (high bit set)
267
- if ((bytes[offset].toInt() and 0x80) != 0) {
268
- return parseBase256(bytes, offset, length)
269
- }
270
- return parseOctal(bytes, offset, length)
271
- }
272
-
273
- /**
274
- * Parse base-256 encoded number (for files > 8GB)
275
- */
276
- private fun parseBase256(
199
+ private fun parseOctal(
277
200
  bytes: ByteArray,
278
201
  offset: Int,
279
202
  length: Int,
280
203
  ): Long {
281
204
  var result = 0L
282
-
283
- // Skip first byte (marker) and read big-endian
284
- for (i in 1 until length) {
285
- result = (result shl 8) or (bytes[offset + i].toInt() and 0xFF).toLong()
205
+ var started = false
206
+ for (index in offset until offset + length) {
207
+ val value = bytes[index]
208
+ if (!started && (value == 0.toByte() || value == ' '.code.toByte())) continue
209
+ if (value == 0.toByte() || value == ' '.code.toByte()) break
210
+ if (value !in '0'.code.toByte()..'7'.code.toByte()) {
211
+ throw IOException("Invalid TAR octal value")
212
+ }
213
+ started = true
214
+ val digit = value - '0'.code.toByte()
215
+ if (result > (Long.MAX_VALUE - digit) / 8) {
216
+ throw IOException("TAR octal value overflows Long")
217
+ }
218
+ result = result * 8 + digit
286
219
  }
287
-
288
220
  return result
289
221
  }
290
222
 
291
- /**
292
- * Read GNU long filename extension
293
- */
294
- private fun readLongName(size: Long): String {
295
- val nameBytes = ByteArray(size.toInt())
296
- var offset = 0
297
-
298
- while (offset < size) {
299
- val n = input.read(nameBytes, offset, size.toInt() - offset)
300
- if (n < 0) throw EOFException("Unexpected end reading long name")
301
- offset += n
302
- }
303
-
304
- skipPadding(size)
305
-
306
- // Remove trailing NUL
307
- val nameLength =
308
- nameBytes
309
- .indexOfFirst { it == 0.toByte() }
310
- .takeIf { it >= 0 } ?: nameBytes.size
311
-
312
- return String(nameBytes, 0, nameLength, Charsets.UTF_8)
313
- }
314
-
315
- /**
316
- * Read the path attribute from a POSIX PAX extended header.
317
- * Record lengths are measured in bytes and include the length itself.
318
- */
319
- private fun readPaxPath(size: Long): String? {
320
- if (size < 0 || size > MAX_FILE_SIZE) {
321
- throw SecurityException("Invalid PAX header size: $size")
322
- }
323
-
324
- val data = ByteArray(size.toInt())
325
- var bytesRead = 0
326
- while (bytesRead < data.size) {
327
- val count = input.read(data, bytesRead, data.size - bytesRead)
328
- if (count < 0) throw EOFException("Unexpected end reading PAX header")
329
- bytesRead += count
330
- }
331
- skipPadding(size)
332
-
223
+ private fun readPaxHeaders(size: Long): Map<String, String> {
224
+ val data = readExtension(size)
333
225
  var offset = 0
334
- var path: String? = null
226
+ val headers = linkedMapOf<String, String>()
335
227
  while (offset < data.size) {
336
- val space = findByte(data, ' '.code.toByte(), offset, data.size)
337
- if (space <= offset) throw IOException("Invalid PAX record length")
338
-
339
- val recordLength = parsePaxRecordLength(data, offset, space)
340
- if (recordLength <= 0 || recordLength > data.size - offset) {
341
- throw IOException("Invalid PAX record length: $recordLength")
342
- }
343
-
344
- val recordEnd = offset + recordLength
345
- if (recordEnd <= space + 2 || data[recordEnd - 1] != '\n'.code.toByte()) {
346
- throw IOException("Invalid PAX record")
228
+ val separator = data.indexOf(' '.code.toByte(), offset)
229
+ if (separator <= offset) throw IOException("Invalid PAX record length")
230
+ if ((offset until separator).any { data[it] !in '0'.code.toByte()..'9'.code.toByte() }) {
231
+ throw IOException("Invalid PAX record length")
347
232
  }
348
-
349
- val valueEnd = recordEnd - 1
350
- val equals = findByte(data, '='.code.toByte(), space + 1, valueEnd)
351
- if (equals <= space + 1) throw IOException("Invalid PAX record")
352
-
353
- val key =
354
- String(
355
- data,
356
- space + 1,
357
- equals - space - 1,
358
- Charsets.UTF_8,
359
- )
360
- if (key == "path") {
361
- path =
362
- String(
363
- data,
364
- equals + 1,
365
- valueEnd - equals - 1,
366
- Charsets.UTF_8,
367
- )
233
+ val recordLength =
234
+ String(data, offset, separator - offset, Charsets.US_ASCII).toIntOrNull()
235
+ ?: throw IOException("Invalid PAX record length")
236
+ if (recordLength <= separator - offset + 2 || recordLength > data.size - offset) {
237
+ throw IOException("Invalid PAX record length")
368
238
  }
369
-
239
+ val recordEnd = offset + recordLength
240
+ if (data[recordEnd - 1] != '\n'.code.toByte()) throw IOException("Invalid PAX record")
241
+ val equals = data.indexOf('='.code.toByte(), separator + 1, recordEnd - 1)
242
+ if (equals <= separator + 1) throw IOException("Invalid PAX record")
243
+ val key = decodeUtf8(data, separator + 1, equals - separator - 1)
244
+ if (headers.containsKey(key)) throw IOException("Duplicate PAX key: $key")
245
+ headers[key] = decodeUtf8(data, equals + 1, recordEnd - equals - 2)
370
246
  offset = recordEnd
371
247
  }
372
-
373
- return path
248
+ return headers
374
249
  }
375
250
 
376
- private fun parsePaxRecordLength(
377
- bytes: ByteArray,
378
- start: Int,
379
- end: Int,
380
- ): Int {
381
- var result = 0
382
- for (index in start until end) {
383
- val digit = bytes[index].toInt() - '0'.code
384
- if (digit !in 0..9 || result > (Int.MAX_VALUE - digit) / 10) {
385
- throw IOException("Invalid PAX record length")
386
- }
387
- result = result * 10 + digit
251
+ private fun parsePaxSize(value: String): Long {
252
+ if (value.isEmpty() || value.any { it !in '0'..'9' }) {
253
+ throw IOException("Invalid PAX entry size")
388
254
  }
389
- return result
255
+ return value.toLongOrNull() ?: throw IOException("Invalid PAX entry size")
390
256
  }
391
257
 
392
- private fun findByte(
258
+ private fun decodeUtf8(
393
259
  bytes: ByteArray,
260
+ offset: Int,
261
+ length: Int,
262
+ ): String =
263
+ try {
264
+ Charsets.UTF_8
265
+ .newDecoder()
266
+ .onMalformedInput(CodingErrorAction.REPORT)
267
+ .onUnmappableCharacter(CodingErrorAction.REPORT)
268
+ .decode(ByteBuffer.wrap(bytes, offset, length))
269
+ .toString()
270
+ } catch (error: Exception) {
271
+ throw IOException("Invalid UTF-8 in TAR metadata", error)
272
+ }
273
+
274
+ private fun ByteArray.indexOf(
394
275
  value: Byte,
395
276
  start: Int,
396
- end: Int,
277
+ endExclusive: Int = size,
397
278
  ): Int {
398
- for (index in start until end) {
399
- if (bytes[index] == value) return index
400
- }
279
+ for (index in start until endExclusive) if (this[index] == value) return index
401
280
  return -1
402
281
  }
403
282
 
404
- /**
405
- * Skip padding to 512-byte boundary
406
- */
407
- private fun skipPadding(size: Long) {
408
- val remainder = size % BLOCK_SIZE
409
- if (remainder != 0L) {
410
- skipBytes(BLOCK_SIZE - remainder)
283
+ private fun readExtension(size: Long): ByteArray {
284
+ if (size < 0 || size > MAX_EXTENSION_SIZE) {
285
+ throw IOException("TAR extension is too large")
411
286
  }
287
+ val data = ByteArray(size.toInt())
288
+ readExactly(data)
289
+ readPadding(size)
290
+ return data
412
291
  }
413
292
 
414
- /**
415
- * Skip specified number of bytes
416
- */
417
- private fun skipBytes(n: Long) {
418
- var remaining = n
419
- val buffer = ByteArray(8192)
420
-
421
- while (remaining > 0) {
422
- val toSkip = minOf(buffer.size.toLong(), remaining).toInt()
423
- val skipped = input.read(buffer, 0, toSkip)
424
- if (skipped < 0) throw EOFException("Unexpected end of stream")
425
- remaining -= skipped
293
+ private fun readExactly(buffer: ByteArray) {
294
+ var offset = 0
295
+ while (offset < buffer.size) {
296
+ val count = input.read(buffer, offset, buffer.size - offset)
297
+ if (count < 0) throw EOFException("Unexpected end of TAR extension")
298
+ if (count == 0) throw IOException("TAR extension read made no progress")
299
+ offset += count
426
300
  }
427
301
  }
428
302
 
429
- /**
430
- * Validate entry for security issues
431
- */
432
- private fun validateEntry(entry: TarArchiveEntry) {
433
- // Check for negative or excessive file size
434
- if (entry.size < 0) {
435
- throw SecurityException("Negative file size: ${entry.size}")
436
- }
437
-
438
- if (entry.size > MAX_FILE_SIZE) {
439
- throw SecurityException("File size ${entry.size} exceeds maximum $MAX_FILE_SIZE")
440
- }
441
-
442
- // Check for absolute paths
443
- if (entry.name.startsWith("/")) {
444
- throw SecurityException("Absolute path not allowed: ${entry.name}")
445
- }
446
-
447
- // Check for path traversal
448
- val normalized = entry.name.replace('\\', '/')
449
- if (normalized.contains("../") ||
450
- normalized.contains("/..") ||
451
- normalized == ".." ||
452
- normalized.startsWith("../")
453
- ) {
454
- throw SecurityException("Path traversal detected: ${entry.name}")
455
- }
303
+ private fun readPadding(size: Long) {
304
+ val remainder = size % BLOCK_SIZE
305
+ if (remainder == 0L) return
306
+ val padding = ByteArray((BLOCK_SIZE - remainder).toInt())
307
+ readExactly(padding)
308
+ if (!padding.all { it == 0.toByte() }) throw IOException("Invalid TAR entry padding")
309
+ }
456
310
 
457
- // Check for null bytes in filename
458
- if (entry.name.contains('\u0000')) {
459
- throw SecurityException("Null byte in filename: ${entry.name}")
311
+ private fun skipExactly(count: Long) {
312
+ var remaining = count
313
+ val buffer = ByteArray(8192)
314
+ while (remaining > 0) {
315
+ val read = input.read(buffer, 0, minOf(buffer.size.toLong(), remaining).toInt())
316
+ if (read < 0) throw EOFException("Unexpected end of TAR archive")
317
+ if (read == 0) throw IOException("TAR skip made no progress")
318
+ remaining -= read
460
319
  }
461
320
  }
462
321
  }
463
-
464
- /**
465
- * TAR archive entry
466
- */
467
- data class TarArchiveEntry(
468
- var name: String,
469
- val mode: Int,
470
- val size: Long,
471
- val typeFlag: Char,
472
- val linkName: String,
473
- ) {
474
- val isDirectory: Boolean
475
- get() = typeFlag == '5' || name.endsWith('/')
476
-
477
- val isFile: Boolean
478
- get() = typeFlag == '0' || typeFlag == '\u0000'
479
-
480
- val isSymbolicLink: Boolean
481
- get() = typeFlag == '2'
482
- }