@hot-updater/react-native 1.0.8 → 1.1.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/HotUpdater.podspec +14 -2
- package/Package.swift +24 -3
- package/android/build.gradle +2 -6
- package/android/hot-updater-fingerprint.gradle +135 -0
- package/android/src/main/cpp/HotUpdaterRecovery.cpp +291 -21
- package/android/src/main/java/com/hotupdater/ActivityStartGate.kt +176 -15
- package/android/src/main/java/com/hotupdater/ArtifactFileResolver.kt +20 -8
- package/android/src/main/java/com/hotupdater/BundleFileStorageService.kt +145 -31
- package/android/src/main/java/com/hotupdater/CohortService.kt +10 -2
- package/android/src/main/java/com/hotupdater/FileManagerService.kt +3 -1
- package/android/src/main/java/com/hotupdater/HotUpdaterConfig.kt +249 -21
- package/android/src/main/java/com/hotupdater/HotUpdaterException.kt +8 -4
- package/android/src/main/java/com/hotupdater/HotUpdaterHeldContent.kt +92 -0
- package/android/src/main/java/com/hotupdater/HotUpdaterImpl.kt +319 -113
- package/android/src/main/java/com/hotupdater/HotUpdaterInstance.kt +87 -0
- package/android/src/main/java/com/hotupdater/HotUpdaterInstanceEnvironment.kt +323 -0
- package/android/src/main/java/com/hotupdater/HotUpdaterMainThread.kt +46 -0
- package/android/src/main/java/com/hotupdater/HotUpdaterProcess.kt +30 -0
- package/android/src/main/java/com/hotupdater/HotUpdaterRecoveryAttribution.kt +493 -0
- package/android/src/main/java/com/hotupdater/HotUpdaterRecoveryManager.kt +1541 -159
- package/android/src/main/java/com/hotupdater/HotUpdaterRecoverySlots.kt +137 -0
- package/android/src/main/java/com/hotupdater/HotUpdaterRecoveryState.kt +4 -0
- package/android/src/main/java/com/hotupdater/HotUpdaterRuntimeInstance.kt +53 -0
- package/android/src/main/java/com/hotupdater/HotUpdaterUpdateCheck.kt +149 -0
- package/android/src/main/java/com/hotupdater/InstallIdentityService.kt +12 -2
- package/android/src/main/java/com/hotupdater/NativeConfigUtils.kt +5 -6
- package/android/src/main/java/com/hotupdater/NativeUpdateCheck.kt +172 -0
- package/android/src/main/java/com/hotupdater/NativeUpdateChecker.kt +1007 -0
- package/android/src/main/java/com/hotupdater/NativeUpdateEventQueue.kt +111 -0
- package/android/src/main/java/com/hotupdater/OkHttpUpdateCheckTransport.kt +88 -0
- package/android/src/main/java/com/hotupdater/ReleaseCatalog.kt +491 -0
- package/android/src/main/java/com/hotupdater/ReleaseCatalogClient.kt +405 -0
- package/android/src/main/java/com/hotupdater/ReleaseSelection.kt +474 -0
- package/android/src/main/java/com/hotupdater/SignatureVerifier.kt +32 -10
- package/android/src/main/java/com/hotupdater/StrictJson.kt +392 -0
- package/android/src/main/java/com/hotupdater/VersionedPreferencesService.kt +24 -9
- package/android/src/main/java/com/hotupdater/WeakIdentitySet.kt +76 -0
- package/android/src/main/java/com/hotupdater/vendor/brotli/dec/BitReader.java +314 -0
- package/android/src/main/java/com/hotupdater/vendor/brotli/dec/BrotliError.java +47 -0
- package/android/src/main/java/com/hotupdater/vendor/brotli/dec/BrotliInputStream.java +174 -0
- package/android/src/main/java/com/hotupdater/vendor/brotli/dec/BrotliRuntimeException.java +21 -0
- package/android/src/main/java/com/hotupdater/vendor/brotli/dec/Context.java +58 -0
- package/android/src/main/java/com/hotupdater/vendor/brotli/dec/Decode.java +1574 -0
- package/android/src/main/java/com/hotupdater/vendor/brotli/dec/Decoder.java +72 -0
- package/android/src/main/java/com/hotupdater/vendor/brotli/dec/Dictionary.java +103 -0
- package/android/src/main/java/com/hotupdater/vendor/brotli/dec/DictionaryData.java +82 -0
- package/android/src/main/java/com/hotupdater/vendor/brotli/dec/Huffman.java +143 -0
- package/{LICENSE → android/src/main/java/com/hotupdater/vendor/brotli/dec/LICENSE} +6 -20
- package/android/src/main/java/com/hotupdater/vendor/brotli/dec/README.md +39 -0
- package/android/src/main/java/com/hotupdater/vendor/brotli/dec/State.java +100 -0
- package/android/src/main/java/com/hotupdater/vendor/brotli/dec/Transform.java +240 -0
- package/android/src/main/java/com/hotupdater/vendor/brotli/dec/Utils.java +163 -0
- package/android/src/main/resources/META-INF/LICENSE-brotli +19 -0
- package/android/src/newarch/HotUpdater.kt +53 -15
- package/android/src/newarch/HotUpdaterModule.kt +42 -17
- package/android/src/newarch/JavaScriptExceptionHooks.kt +45 -8
- package/android/src/oldarch/HotUpdater.kt +51 -12
- package/android/src/oldarch/HotUpdaterModule.kt +43 -17
- package/android/src/oldarch/HotUpdaterSpec.kt +2 -0
- package/android/src/oldarch/JavaScriptExceptionHooks.kt +25 -16
- package/ios/HotUpdater/Interface/HotUpdaterInterface.h +13 -0
- package/ios/HotUpdater/Internal/ArtifactFileResolver.swift +38 -7
- package/ios/HotUpdater/Internal/BuiltInAssetResolver.swift +10 -4
- package/ios/HotUpdater/Internal/BundleFileStorageService.swift +186 -48
- package/ios/HotUpdater/Internal/CohortService.swift +8 -0
- package/ios/HotUpdater/Internal/HotUpdater.mm +821 -71
- package/ios/HotUpdater/Internal/HotUpdaterAsync.swift +7 -0
- package/ios/HotUpdater/Internal/HotUpdaterConfig.swift +208 -25
- package/ios/HotUpdater/Internal/HotUpdaterFingerprintHash.m +10 -0
- package/ios/HotUpdater/Internal/HotUpdaterImpl.swift +940 -261
- package/ios/HotUpdater/Internal/HotUpdaterInstanceEnvironment.swift +492 -0
- package/ios/HotUpdater/Internal/HotUpdaterInstanceRouter.swift +83 -0
- package/ios/HotUpdater/Internal/HotUpdaterInterface.swift +28 -1
- package/ios/HotUpdater/Internal/HotUpdaterLaunchRecovery.swift +1553 -0
- package/ios/HotUpdater/Internal/HotUpdaterNativeUpdates.swift +353 -0
- package/ios/HotUpdater/Internal/LaunchPreparationLock.swift +54 -0
- package/ios/HotUpdater/Internal/NativeUpdateCheck.swift +2051 -0
- package/ios/HotUpdater/Internal/ReleaseCatalog.swift +634 -0
- package/ios/HotUpdater/Internal/ReleaseCatalogCacheService.swift +1 -1
- package/ios/HotUpdater/Internal/ReleaseSelection.swift +482 -0
- package/ios/HotUpdater/Internal/SignatureVerifier.swift +27 -10
- package/ios/HotUpdater/Internal/StrictJson.swift +363 -0
- package/ios/HotUpdater/Internal/URLSessionDownloadService.swift +4 -2
- package/ios/HotUpdater/Internal/UpdateFailure.swift +2 -0
- package/ios/HotUpdater/Package.swift +11 -0
- package/ios/HotUpdater/Public/HotUpdater/HotUpdater.h +4 -0
- package/ios/HotUpdater/Public/HotUpdater.h +21 -2
- package/ios/HotUpdater/Recovery/HotUpdaterRecoveryHooks.mm +66 -0
- package/ios/Plugins/HotUpdaterFingerprintPlugin/HotUpdaterFingerprintPlugin.swift +45 -0
- package/ios/scripts/generate-fingerprint-hash.zsh +85 -0
- package/lib/commonjs/checkForUpdate.js +3 -1
- package/lib/commonjs/checkForUpdate.js.map +1 -1
- package/lib/commonjs/index.js +9 -12
- package/lib/commonjs/index.js.map +1 -1
- package/lib/commonjs/native.js +24 -79
- package/lib/commonjs/native.js.map +1 -1
- package/lib/commonjs/nativeUpdateEvents.js +66 -0
- package/lib/commonjs/nativeUpdateEvents.js.map +1 -0
- package/lib/commonjs/sdkVersion.js +1 -1
- package/lib/commonjs/specs/NativeHotUpdater.js.map +1 -1
- package/lib/module/checkForUpdate.js +3 -1
- package/lib/module/checkForUpdate.js.map +1 -1
- package/lib/module/index.js +10 -13
- package/lib/module/index.js.map +1 -1
- package/lib/module/native.js +19 -73
- package/lib/module/native.js.map +1 -1
- package/lib/module/nativeUpdateEvents.js +61 -0
- package/lib/module/nativeUpdateEvents.js.map +1 -0
- package/lib/module/sdkVersion.js +1 -1
- package/lib/module/specs/NativeHotUpdater.js.map +1 -1
- package/lib/typescript/commonjs/checkForUpdate.d.ts.map +1 -1
- package/lib/typescript/commonjs/index.d.ts +4 -14
- package/lib/typescript/commonjs/index.d.ts.map +1 -1
- package/lib/typescript/commonjs/native.d.ts +9 -46
- package/lib/typescript/commonjs/native.d.ts.map +1 -1
- package/lib/typescript/commonjs/nativeUpdateEvents.d.ts +12 -0
- package/lib/typescript/commonjs/nativeUpdateEvents.d.ts.map +1 -0
- package/lib/typescript/commonjs/sdkVersion.d.ts +1 -1
- package/lib/typescript/commonjs/specs/NativeHotUpdater.d.ts +19 -3
- package/lib/typescript/commonjs/specs/NativeHotUpdater.d.ts.map +1 -1
- package/lib/typescript/module/checkForUpdate.d.ts.map +1 -1
- package/lib/typescript/module/index.d.ts +4 -14
- package/lib/typescript/module/index.d.ts.map +1 -1
- package/lib/typescript/module/native.d.ts +9 -46
- package/lib/typescript/module/native.d.ts.map +1 -1
- package/lib/typescript/module/nativeUpdateEvents.d.ts +12 -0
- package/lib/typescript/module/nativeUpdateEvents.d.ts.map +1 -0
- package/lib/typescript/module/sdkVersion.d.ts +1 -1
- package/lib/typescript/module/specs/NativeHotUpdater.d.ts +19 -3
- package/lib/typescript/module/specs/NativeHotUpdater.d.ts.map +1 -1
- package/package.json +4 -4
- package/src/checkForUpdate.ts +7 -1
- package/src/index.ts +9 -17
- package/src/native.ts +19 -91
- package/src/nativeUpdateEvents.ts +77 -0
- package/src/sdkVersion.ts +1 -1
- package/src/specs/NativeHotUpdater.ts +20 -3
- package/android/libs/README.md +0 -50
- package/android/libs/hot-updater-brotli-dec-1.2.0.jar +0 -0
- package/android/src/bridge/BridgeReactInstance.kt +0 -38
- package/android/src/bridgeless/BridgeReactInstance.kt +0 -24
- package/android/src/main/java/com/hotupdater/ContentAppearedLog.kt +0 -31
- package/android/src/main/java/com/hotupdater/ReactIntegrationManagerBase.kt +0 -34
- package/android/src/newarch/ReactIntegrationManager.kt +0 -212
- package/android/src/oldarch/ReactIntegrationManager.kt +0 -120
|
@@ -0,0 +1,363 @@
|
|
|
1
|
+
import Foundation
|
|
2
|
+
|
|
3
|
+
/// Parses exactly one JSON value (RFC 8259) as `JSON.parse` does, into
|
|
4
|
+
/// Foundation values: `[String: Any]`, `[Any]`, `String`, `NSNumber` (a
|
|
5
|
+
/// `Double`, or a Boolean) and `NSNull`. `JSONSerialization` accepts some
|
|
6
|
+
/// non-JSON syntax, such as a trailing comma in an object, and rejects some
|
|
7
|
+
/// JSON, such as an unpaired `\ud800` escape or a number that overflows to
|
|
8
|
+
/// infinity, so update responses are parsed here instead.
|
|
9
|
+
///
|
|
10
|
+
/// Like `JSON.parse`, a duplicate key keeps its last value and an overflowing
|
|
11
|
+
/// number is infinite. Parsing is iterative, but releasing nested Swift arrays
|
|
12
|
+
/// and dictionaries recurses. So a value nested deeper than `maxDepth`, far
|
|
13
|
+
/// deeper than any field Hot Updater reads, is checked but not built: it
|
|
14
|
+
/// parses as `NSNull`, and the text is accepted as `JSON.parse` accepts it.
|
|
15
|
+
///
|
|
16
|
+
/// JavaScript compares strings by UTF-16 code units, but Swift strings
|
|
17
|
+
/// compare canonically: `"scope\u{212A}ey"` (Kelvin sign) equals `"scopeKey"`,
|
|
18
|
+
/// and a Swift string cannot hold an unpaired surrogate at all.
|
|
19
|
+
/// - An object key that is not its own canonical decomposition, or that has
|
|
20
|
+
/// an unpaired surrogate, is stored escaped. Lookups by plain names match
|
|
21
|
+
/// only that exact key, and `originalKey` recovers the escaped one.
|
|
22
|
+
/// - A string value with an unpaired surrogate is an `UnpairedString`, its
|
|
23
|
+
/// code units as `JSON.parse` keeps them; every other value is a `String`
|
|
24
|
+
/// exactly as the JSON spells it.
|
|
25
|
+
enum StrictJson {
|
|
26
|
+
/// Starts every escaped key. A key that starts with it is escaped too.
|
|
27
|
+
private static let escapedKeyPrefix = "\u{E000}"
|
|
28
|
+
|
|
29
|
+
/// The deepest nesting of arrays and objects that is built.
|
|
30
|
+
static let maxDepth = 128
|
|
31
|
+
|
|
32
|
+
/// The object key under which `key` is stored.
|
|
33
|
+
static func storedKey(_ key: String) -> String {
|
|
34
|
+
storedKey(Array(key.utf16))
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
private static func storedKey(_ units: [UInt16]) -> String {
|
|
38
|
+
let key = String(decoding: units, as: UTF16.self)
|
|
39
|
+
if !needsEscape(units),
|
|
40
|
+
key.utf16.elementsEqual(key.decomposedStringWithCanonicalMapping.utf16) {
|
|
41
|
+
return key
|
|
42
|
+
}
|
|
43
|
+
return escaped(units)
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/// A string value with an unpaired surrogate, which a Swift `String`
|
|
47
|
+
/// cannot hold: its UTF-16 code units, as `JSON.parse` keeps them.
|
|
48
|
+
struct UnpairedString: Hashable {
|
|
49
|
+
let units: [UInt16]
|
|
50
|
+
|
|
51
|
+
/// The text, each unpaired surrogate as U+FFFD.
|
|
52
|
+
var text: String { String(decoding: units, as: UTF16.self) }
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/// The parsed value of a string with `units`.
|
|
56
|
+
private static func storedValue(_ units: [UInt16]) -> Any {
|
|
57
|
+
hasUnpairedSurrogate(units) ? UnpairedString(units: units) : String(decoding: units, as: UTF16.self)
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/// Whether a Swift string would lose `units`, or they could be mistaken
|
|
61
|
+
/// for an escaped key.
|
|
62
|
+
private static func needsEscape(_ units: [UInt16]) -> Bool {
|
|
63
|
+
units.first == 0xE000 || hasUnpairedSurrogate(units)
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
private static func hasUnpairedSurrogate(_ units: [UInt16]) -> Bool {
|
|
67
|
+
var index = 0
|
|
68
|
+
while index < units.count {
|
|
69
|
+
let unit = units[index]
|
|
70
|
+
if (0xD800...0xDBFF).contains(unit) {
|
|
71
|
+
guard index + 1 < units.count, (0xDC00...0xDFFF).contains(units[index + 1]) else {
|
|
72
|
+
return true
|
|
73
|
+
}
|
|
74
|
+
index += 2
|
|
75
|
+
} else if (0xDC00...0xDFFF).contains(unit) {
|
|
76
|
+
return true
|
|
77
|
+
} else {
|
|
78
|
+
index += 1
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
return false
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
private static func escaped(_ units: [UInt16]) -> String {
|
|
85
|
+
escapedKeyPrefix + units.map { String(format: "%04x", $0) }.joined()
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
/// The key as the JSON text spelled it, for a key read from a parsed
|
|
89
|
+
/// object; an unpaired surrogate becomes U+FFFD.
|
|
90
|
+
static func originalKey(_ storedKey: String) -> String {
|
|
91
|
+
guard storedKey.hasPrefix(escapedKeyPrefix) else { return storedKey }
|
|
92
|
+
let hex = Array(storedKey.dropFirst(escapedKeyPrefix.count))
|
|
93
|
+
var units: [UInt16] = []
|
|
94
|
+
var index = 0
|
|
95
|
+
while index + 4 <= hex.count, let unit = UInt16(String(hex[index..<(index + 4)]), radix: 16) {
|
|
96
|
+
units.append(unit)
|
|
97
|
+
index += 4
|
|
98
|
+
}
|
|
99
|
+
return String(decoding: units, as: UTF16.self)
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
static func isValid(_ text: String) -> Bool {
|
|
103
|
+
parse(text) != nil
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
/// The value of `text`, or nil when `text` is not exactly one JSON value.
|
|
107
|
+
static func parse(_ text: String) -> Any? {
|
|
108
|
+
var parser = Parser(bytes: Array(text.utf8))
|
|
109
|
+
return parser.parse()
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
private final class Container {
|
|
113
|
+
let isObject: Bool
|
|
114
|
+
/// Past `maxDepth`: checked, but neither built nor kept.
|
|
115
|
+
let discards: Bool
|
|
116
|
+
var object: [String: Any] = [:]
|
|
117
|
+
var array: [Any] = []
|
|
118
|
+
var key = ""
|
|
119
|
+
|
|
120
|
+
init(isObject: Bool, depth: Int) {
|
|
121
|
+
self.isObject = isObject
|
|
122
|
+
discards = depth >= StrictJson.maxDepth
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
var value: Any {
|
|
126
|
+
if discards { return NSNull() }
|
|
127
|
+
return isObject ? object : array
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
func add(_ value: Any) {
|
|
131
|
+
if discards { return }
|
|
132
|
+
if isObject {
|
|
133
|
+
object[key] = value
|
|
134
|
+
} else {
|
|
135
|
+
array.append(value)
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
private struct Parser {
|
|
141
|
+
let bytes: [UInt8]
|
|
142
|
+
var index = 0
|
|
143
|
+
var containers: [Container] = []
|
|
144
|
+
|
|
145
|
+
init(bytes: [UInt8]) {
|
|
146
|
+
self.bytes = bytes
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
mutating func parse() -> Any? {
|
|
150
|
+
var expectValue = true
|
|
151
|
+
var result: Any?
|
|
152
|
+
// Adds a finished value to the open container, or ends the parse.
|
|
153
|
+
func finish(_ value: Any, _ containers: [Container]) -> Bool {
|
|
154
|
+
if let container = containers.last {
|
|
155
|
+
container.add(value)
|
|
156
|
+
return false
|
|
157
|
+
}
|
|
158
|
+
result = value
|
|
159
|
+
return true
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
while true {
|
|
163
|
+
skipWhitespace()
|
|
164
|
+
if expectValue {
|
|
165
|
+
let value: Any
|
|
166
|
+
switch peek() {
|
|
167
|
+
case UInt8(ascii: "{"):
|
|
168
|
+
index += 1
|
|
169
|
+
skipWhitespace()
|
|
170
|
+
if peek() == UInt8(ascii: "}") {
|
|
171
|
+
index += 1
|
|
172
|
+
value = [String: Any]()
|
|
173
|
+
} else {
|
|
174
|
+
let container = Container(isObject: true, depth: containers.count)
|
|
175
|
+
guard let key = member() else { return nil }
|
|
176
|
+
container.key = key
|
|
177
|
+
containers.append(container)
|
|
178
|
+
continue
|
|
179
|
+
}
|
|
180
|
+
case UInt8(ascii: "["):
|
|
181
|
+
index += 1
|
|
182
|
+
skipWhitespace()
|
|
183
|
+
if peek() == UInt8(ascii: "]") {
|
|
184
|
+
index += 1
|
|
185
|
+
value = [Any]()
|
|
186
|
+
} else {
|
|
187
|
+
containers.append(Container(isObject: false, depth: containers.count))
|
|
188
|
+
continue
|
|
189
|
+
}
|
|
190
|
+
case UInt8(ascii: "\""):
|
|
191
|
+
guard let units = string() else { return nil }
|
|
192
|
+
value = StrictJson.storedValue(units)
|
|
193
|
+
case UInt8(ascii: "t"):
|
|
194
|
+
guard literal("true") else { return nil }
|
|
195
|
+
value = NSNumber(value: true)
|
|
196
|
+
case UInt8(ascii: "f"):
|
|
197
|
+
guard literal("false") else { return nil }
|
|
198
|
+
value = NSNumber(value: false)
|
|
199
|
+
case UInt8(ascii: "n"):
|
|
200
|
+
guard literal("null") else { return nil }
|
|
201
|
+
value = NSNull()
|
|
202
|
+
default:
|
|
203
|
+
guard let number = number() else { return nil }
|
|
204
|
+
value = NSNumber(value: number)
|
|
205
|
+
}
|
|
206
|
+
expectValue = false
|
|
207
|
+
if finish(value, containers) {
|
|
208
|
+
skipWhitespace()
|
|
209
|
+
return index == bytes.count ? result : nil
|
|
210
|
+
}
|
|
211
|
+
} else {
|
|
212
|
+
guard let container = containers.last else { return nil }
|
|
213
|
+
switch peek() {
|
|
214
|
+
case UInt8(ascii: ","):
|
|
215
|
+
index += 1
|
|
216
|
+
if container.isObject {
|
|
217
|
+
skipWhitespace()
|
|
218
|
+
guard let key = member() else { return nil }
|
|
219
|
+
container.key = key
|
|
220
|
+
}
|
|
221
|
+
expectValue = true
|
|
222
|
+
case UInt8(ascii: "}") where container.isObject,
|
|
223
|
+
UInt8(ascii: "]") where !container.isObject:
|
|
224
|
+
index += 1
|
|
225
|
+
containers.removeLast()
|
|
226
|
+
if finish(container.value, containers) {
|
|
227
|
+
skipWhitespace()
|
|
228
|
+
return index == bytes.count ? result : nil
|
|
229
|
+
}
|
|
230
|
+
default:
|
|
231
|
+
return nil
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
}
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
/// An object member's stored key and colon; its value comes next.
|
|
238
|
+
private mutating func member() -> String? {
|
|
239
|
+
guard peek() == UInt8(ascii: "\""), let key = string() else { return nil }
|
|
240
|
+
skipWhitespace()
|
|
241
|
+
guard peek() == UInt8(ascii: ":") else { return nil }
|
|
242
|
+
index += 1
|
|
243
|
+
return StrictJson.storedKey(key)
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
/// A string's UTF-16 code units, as JSON.parse keeps them.
|
|
247
|
+
private mutating func string() -> [UInt16]? {
|
|
248
|
+
index += 1
|
|
249
|
+
var units: [UInt16] = []
|
|
250
|
+
var runStart = index
|
|
251
|
+
// Appends the unescaped UTF-8 bytes read since the last escape.
|
|
252
|
+
func flush(_ end: Int) {
|
|
253
|
+
if end > runStart {
|
|
254
|
+
units.append(contentsOf: String(decoding: bytes[runStart..<end], as: UTF8.self).utf16)
|
|
255
|
+
}
|
|
256
|
+
}
|
|
257
|
+
while index < bytes.count {
|
|
258
|
+
let byte = bytes[index]
|
|
259
|
+
if byte == UInt8(ascii: "\"") {
|
|
260
|
+
flush(index)
|
|
261
|
+
index += 1
|
|
262
|
+
return units
|
|
263
|
+
}
|
|
264
|
+
if byte < 0x20 { return nil }
|
|
265
|
+
if byte != UInt8(ascii: "\\") {
|
|
266
|
+
index += 1
|
|
267
|
+
continue
|
|
268
|
+
}
|
|
269
|
+
flush(index)
|
|
270
|
+
index += 1
|
|
271
|
+
guard index < bytes.count else { return nil }
|
|
272
|
+
let escape = bytes[index]
|
|
273
|
+
index += 1
|
|
274
|
+
switch escape {
|
|
275
|
+
case UInt8(ascii: "\""): units.append(0x22)
|
|
276
|
+
case UInt8(ascii: "\\"): units.append(0x5C)
|
|
277
|
+
case UInt8(ascii: "/"): units.append(0x2F)
|
|
278
|
+
case UInt8(ascii: "b"): units.append(0x08)
|
|
279
|
+
case UInt8(ascii: "f"): units.append(0x0C)
|
|
280
|
+
case UInt8(ascii: "n"): units.append(0x0A)
|
|
281
|
+
case UInt8(ascii: "r"): units.append(0x0D)
|
|
282
|
+
case UInt8(ascii: "t"): units.append(0x09)
|
|
283
|
+
case UInt8(ascii: "u"):
|
|
284
|
+
guard index + 4 <= bytes.count else { return nil }
|
|
285
|
+
var unit: UInt16 = 0
|
|
286
|
+
for _ in 0..<4 {
|
|
287
|
+
guard let digit = hexValue(bytes[index]) else { return nil }
|
|
288
|
+
unit = unit << 4 | UInt16(digit)
|
|
289
|
+
index += 1
|
|
290
|
+
}
|
|
291
|
+
units.append(unit)
|
|
292
|
+
default:
|
|
293
|
+
return nil
|
|
294
|
+
}
|
|
295
|
+
runStart = index
|
|
296
|
+
}
|
|
297
|
+
return nil
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
private mutating func number() -> Double? {
|
|
301
|
+
let start = index
|
|
302
|
+
if peek() == UInt8(ascii: "-") { index += 1 }
|
|
303
|
+
if peek() == UInt8(ascii: "0") {
|
|
304
|
+
index += 1
|
|
305
|
+
} else if let byte = peek(), (UInt8(ascii: "1")...UInt8(ascii: "9")).contains(byte) {
|
|
306
|
+
skipDigits()
|
|
307
|
+
} else {
|
|
308
|
+
return nil
|
|
309
|
+
}
|
|
310
|
+
if peek() == UInt8(ascii: ".") {
|
|
311
|
+
index += 1
|
|
312
|
+
guard isDigit(peek()) else { return nil }
|
|
313
|
+
skipDigits()
|
|
314
|
+
}
|
|
315
|
+
if peek() == UInt8(ascii: "e") || peek() == UInt8(ascii: "E") {
|
|
316
|
+
index += 1
|
|
317
|
+
if peek() == UInt8(ascii: "+") || peek() == UInt8(ascii: "-") { index += 1 }
|
|
318
|
+
guard isDigit(peek()) else { return nil }
|
|
319
|
+
skipDigits()
|
|
320
|
+
}
|
|
321
|
+
// An overflowing literal is infinite, as in JSON.parse.
|
|
322
|
+
return Double(String(decoding: bytes[start..<index], as: UTF8.self))
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
private mutating func literal(_ word: String) -> Bool {
|
|
326
|
+
let wordBytes = Array(word.utf8)
|
|
327
|
+
guard index + wordBytes.count <= bytes.count,
|
|
328
|
+
Array(bytes[index..<(index + wordBytes.count)]) == wordBytes else { return false }
|
|
329
|
+
index += wordBytes.count
|
|
330
|
+
return true
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
private mutating func skipWhitespace() {
|
|
334
|
+
while let byte = peek(),
|
|
335
|
+
byte == UInt8(ascii: " ") || byte == UInt8(ascii: "\t") ||
|
|
336
|
+
byte == UInt8(ascii: "\n") || byte == UInt8(ascii: "\r") {
|
|
337
|
+
index += 1
|
|
338
|
+
}
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
private mutating func skipDigits() {
|
|
342
|
+
while isDigit(peek()) { index += 1 }
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
private func peek() -> UInt8? {
|
|
346
|
+
index < bytes.count ? bytes[index] : nil
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
private func isDigit(_ byte: UInt8?) -> Bool {
|
|
350
|
+
guard let byte else { return false }
|
|
351
|
+
return (UInt8(ascii: "0")...UInt8(ascii: "9")).contains(byte)
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
private func hexValue(_ byte: UInt8) -> UInt8? {
|
|
355
|
+
switch byte {
|
|
356
|
+
case UInt8(ascii: "0")...UInt8(ascii: "9"): return byte - UInt8(ascii: "0")
|
|
357
|
+
case UInt8(ascii: "a")...UInt8(ascii: "f"): return byte - UInt8(ascii: "a") + 10
|
|
358
|
+
case UInt8(ascii: "A")...UInt8(ascii: "F"): return byte - UInt8(ascii: "A") + 10
|
|
359
|
+
default: return nil
|
|
360
|
+
}
|
|
361
|
+
}
|
|
362
|
+
}
|
|
363
|
+
}
|
|
@@ -162,7 +162,9 @@ class URLSessionDownloadService: NSObject, DownloadService {
|
|
|
162
162
|
private var destinations: [URLSessionTask: String] = [:]
|
|
163
163
|
private var fileSizeHandlers: [URLSessionTask: (Int64) -> Void] = [:]
|
|
164
164
|
|
|
165
|
-
|
|
165
|
+
/// Each instance passes its own `backgroundSessionIdentifier`: a process
|
|
166
|
+
/// must not create two background sessions with one identifier.
|
|
167
|
+
init(backgroundSessionIdentifier: String = HotUpdaterInstanceEnvironment.main.backgroundSessionIdentifier) {
|
|
166
168
|
super.init()
|
|
167
169
|
|
|
168
170
|
// Foreground session (existing behavior)
|
|
@@ -171,7 +173,7 @@ class URLSessionDownloadService: NSObject, DownloadService {
|
|
|
171
173
|
|
|
172
174
|
// Background session for persistent downloads
|
|
173
175
|
let backgroundConfig = URLSessionConfiguration.background(
|
|
174
|
-
withIdentifier:
|
|
176
|
+
withIdentifier: backgroundSessionIdentifier
|
|
175
177
|
)
|
|
176
178
|
backgroundConfig.isDiscretionary = false
|
|
177
179
|
if #available(macOS 11.0, *) {
|
|
@@ -278,6 +278,8 @@ enum UpdateFailureClassifier {
|
|
|
278
278
|
return classifySignatureError(error, during: phase)
|
|
279
279
|
case let error as DownloadError:
|
|
280
280
|
return classifyDownloadError(error)
|
|
281
|
+
case is ArtifactFileResolverError:
|
|
282
|
+
return .invalidResponse
|
|
281
283
|
case let error as URLError:
|
|
282
284
|
return classifyURLError(error)
|
|
283
285
|
default:
|
|
@@ -12,9 +12,17 @@ let coreSources = [
|
|
|
12
12
|
"FileManagerService.swift",
|
|
13
13
|
"HashUtils.swift",
|
|
14
14
|
"HotUpdaterConfig.swift",
|
|
15
|
+
"HotUpdaterInstanceEnvironment.swift",
|
|
16
|
+
"HotUpdaterInstanceRouter.swift",
|
|
17
|
+
"HotUpdaterLaunchRecovery.swift",
|
|
15
18
|
"KeyValueStorageService.swift",
|
|
19
|
+
"LaunchPreparationLock.swift",
|
|
20
|
+
"NativeUpdateCheck.swift",
|
|
16
21
|
"NotificationExtension.swift",
|
|
22
|
+
"ReleaseCatalog.swift",
|
|
17
23
|
"ReleaseCatalogCacheService.swift",
|
|
24
|
+
"ReleaseSelection.swift",
|
|
25
|
+
"StrictJson.swift",
|
|
18
26
|
"SignatureVerifier.swift",
|
|
19
27
|
"UpdateFailure.swift",
|
|
20
28
|
"URLSessionDownloadService.swift",
|
|
@@ -27,9 +35,12 @@ let coreExcludedFiles = [
|
|
|
27
35
|
"CohortService.swift",
|
|
28
36
|
"HotUpdater-Bridging-Header.h",
|
|
29
37
|
"HotUpdater.mm",
|
|
38
|
+
"HotUpdaterAsync.swift",
|
|
30
39
|
"HotUpdaterImpl.swift",
|
|
31
40
|
"HotUpdaterInterface.swift",
|
|
32
41
|
"HotUpdaterMinBundleId.m",
|
|
42
|
+
"HotUpdaterFingerprintHash.m",
|
|
43
|
+
"HotUpdaterNativeUpdates.swift",
|
|
33
44
|
]
|
|
34
45
|
|
|
35
46
|
let bsdiffPatchBridgeExcludedFiles =
|
|
@@ -19,7 +19,9 @@
|
|
|
19
19
|
/**
|
|
20
20
|
* Returns the currently active bundle URL with specific bundle from the default (static) instance.
|
|
21
21
|
* Callable from Objective-C (e.g., AppDelegate).
|
|
22
|
-
*
|
|
22
|
+
* The bundle holds the embedded main.jsbundle, such as a brownfield XCFramework's
|
|
23
|
+
* bundle. Hot Updater reuses it for reloads and reads its HOT_UPDATER_* Info.plist
|
|
24
|
+
* keys before the main bundle's.
|
|
23
25
|
*/
|
|
24
26
|
+ (NSURL *)bundleURLWithBundle:(NSBundle *)bundle NS_SWIFT_NAME(bundleURL(bundle:));
|
|
25
27
|
|
|
@@ -36,13 +38,30 @@
|
|
|
36
38
|
- (NSURL *)bundleURLWithBundle:(NSBundle *)bundle NS_SWIFT_NAME(bundleURL(bundle:));
|
|
37
39
|
|
|
38
40
|
/**
|
|
41
|
+
* Deprecated: pass `channel`, `fingerprintHash`, and `publicKey` to the
|
|
42
|
+
* `HotUpdaterOptions` of the `main` `HotUpdaterInstance` instead. Still
|
|
43
|
+
* honored for main; named instances never read it.
|
|
44
|
+
*
|
|
39
45
|
* Programmatic configuration for brownfield / XCFramework setups where
|
|
40
46
|
* Info.plist (host) or string resources (Android) are not desirable.
|
|
47
|
+
* A nil argument means no override. Each call replaces all four values.
|
|
48
|
+
*
|
|
49
|
+
* `channel` is the app's fixed default channel, not a runtime switch: keep it
|
|
50
|
+
* the same from the first launch on. It is part of the storage isolation
|
|
51
|
+
* key, so changing it deletes the downloaded bundles. To check another
|
|
52
|
+
* channel at runtime, pass `channel` to `HotUpdaterInstance.checkForUpdate`,
|
|
53
|
+
* and undo that with its `resetChannel`.
|
|
54
|
+
*
|
|
55
|
+
* `isolationKey`, when set, replaces main's storage isolation key verbatim.
|
|
56
|
+
* It is no longer needed: under the fingerprint strategy the default key is
|
|
57
|
+
* the fingerprint, the build's MIN_BUNDLE_ID, and the channel, so a native
|
|
58
|
+
* release that ships the same artifact keeps its downloads.
|
|
41
59
|
*/
|
|
42
60
|
+ (void)configureWithFingerprintHash:(nullable NSString *)fingerprintHash
|
|
43
61
|
publicKey:(nullable NSString *)publicKey
|
|
44
62
|
channel:(nullable NSString *)channel
|
|
45
|
-
isolationKey:(nullable NSString *)isolationKey
|
|
63
|
+
isolationKey:(nullable NSString *)isolationKey
|
|
64
|
+
DEPRECATED_MSG_ATTRIBUTE("Pass channel, fingerprintHash, and publicKey to HotUpdaterOptions. Fingerprint downloads now survive native releases that ship the same artifact.");
|
|
46
65
|
|
|
47
66
|
/**
|
|
48
67
|
* 다운로드 진행 상황 업데이트 시간을 추적하는 속성
|
|
@@ -4,6 +4,9 @@
|
|
|
4
4
|
#ifdef RCT_NEW_ARCH_ENABLED
|
|
5
5
|
#import <React/RCTFabricSurface.h>
|
|
6
6
|
#import <React/RCTSurfacePresenter.h>
|
|
7
|
+
#import <React/RCTSurfaceView.h>
|
|
8
|
+
#else
|
|
9
|
+
#import <React/RCTRootView.h>
|
|
7
10
|
#endif
|
|
8
11
|
|
|
9
12
|
void HotUpdaterInstallRecoveryHooks(void)
|
|
@@ -42,8 +45,71 @@ void HotUpdaterInstallRecoveryHooks(void)
|
|
|
42
45
|
// No coordination lock crosses the original call: older RN versions
|
|
43
46
|
// invoke the completion synchronously on this same thread.
|
|
44
47
|
((void (*)(id, SEL))originalStart)(surface, startSelector);
|
|
48
|
+
// A surface of this runtime started: crash recovery starts the
|
|
49
|
+
// launch of the instance the runtime belongs to once its view is
|
|
50
|
+
// shown (see the attachment hook below).
|
|
51
|
+
[[NSNotificationCenter defaultCenter] postNotificationName:@"HotUpdaterSurfaceWillStart" object:surface];
|
|
45
52
|
}
|
|
46
53
|
}));
|
|
54
|
+
|
|
55
|
+
// A surface started for pre-rendering is shown only once its view moves
|
|
56
|
+
// into a window: reported as another start, which crash recovery checks
|
|
57
|
+
// for being shown. RCTSurfaceView doesn't override didMoveToWindow: add
|
|
58
|
+
// an override that calls UIView's, never exchanging UIView's own.
|
|
59
|
+
Class surfaceViewClass = RCTSurfaceView.class;
|
|
60
|
+
SEL moveSelector = @selector(didMoveToWindow);
|
|
61
|
+
Class surfaceViewSuperclass = class_getSuperclass(surfaceViewClass);
|
|
62
|
+
Method inheritedMove = class_getInstanceMethod(surfaceViewClass, moveSelector);
|
|
63
|
+
if (inheritedMove && surfaceViewSuperclass) {
|
|
64
|
+
IMP currentMove = class_getMethodImplementation(surfaceViewClass, moveSelector);
|
|
65
|
+
IMP ownMove = currentMove != class_getMethodImplementation(surfaceViewSuperclass, moveSelector) ? currentMove : NULL;
|
|
66
|
+
IMP moveReplacement = imp_implementationWithBlock(^(RCTSurfaceView *view) {
|
|
67
|
+
IMP next = ownMove ?: class_getMethodImplementation(surfaceViewSuperclass, moveSelector);
|
|
68
|
+
((void (*)(id, SEL))next)(view, moveSelector);
|
|
69
|
+
id surface = view.surface;
|
|
70
|
+
if (view.window && surface) {
|
|
71
|
+
[[NSNotificationCenter defaultCenter] postNotificationName:@"HotUpdaterSurfaceWillStart" object:surface];
|
|
72
|
+
}
|
|
73
|
+
});
|
|
74
|
+
if (ownMove) {
|
|
75
|
+
method_setImplementation(inheritedMove, moveReplacement);
|
|
76
|
+
} else {
|
|
77
|
+
class_addMethod(surfaceViewClass, moveSelector, moveReplacement, method_getTypeEncoding(inheritedMove));
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
});
|
|
81
|
+
#else
|
|
82
|
+
// Old architecture: a root view in a window shows its bridge's runtime,
|
|
83
|
+
// which starts the launch of the instance the bridge runs (attributed by
|
|
84
|
+
// its bundle URL). Repeats are harmless: a launch starts once.
|
|
85
|
+
static dispatch_once_t onceToken;
|
|
86
|
+
dispatch_once(&onceToken, ^{
|
|
87
|
+
#pragma clang diagnostic push
|
|
88
|
+
#pragma clang diagnostic ignored "-Wdeprecated-declarations"
|
|
89
|
+
Class rootViewClass = RCTRootView.class;
|
|
90
|
+
#pragma clang diagnostic pop
|
|
91
|
+
SEL selector = @selector(didMoveToWindow);
|
|
92
|
+
Class superclass = class_getSuperclass(rootViewClass);
|
|
93
|
+
Method inherited = class_getInstanceMethod(rootViewClass, selector);
|
|
94
|
+
if (!inherited || !superclass) {
|
|
95
|
+
return;
|
|
96
|
+
}
|
|
97
|
+
// RCTRootView doesn't override didMoveToWindow today: add an override
|
|
98
|
+
// that calls UIView's, never exchanging UIView's own implementation.
|
|
99
|
+
IMP current = class_getMethodImplementation(rootViewClass, selector);
|
|
100
|
+
IMP own = current != class_getMethodImplementation(superclass, selector) ? current : NULL;
|
|
101
|
+
IMP replacement = imp_implementationWithBlock(^(UIView *view) {
|
|
102
|
+
IMP next = own ?: class_getMethodImplementation(superclass, selector);
|
|
103
|
+
((void (*)(id, SEL))next)(view, selector);
|
|
104
|
+
if (view.window) {
|
|
105
|
+
[[NSNotificationCenter defaultCenter] postNotificationName:@"HotUpdaterSurfaceWillStart" object:view];
|
|
106
|
+
}
|
|
107
|
+
});
|
|
108
|
+
if (own) {
|
|
109
|
+
method_setImplementation(inherited, replacement);
|
|
110
|
+
} else {
|
|
111
|
+
class_addMethod(rootViewClass, selector, replacement, method_getTypeEncoding(inherited));
|
|
112
|
+
}
|
|
47
113
|
});
|
|
48
114
|
#endif
|
|
49
115
|
}
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import Foundation
|
|
2
|
+
import PackagePlugin
|
|
3
|
+
|
|
4
|
+
// Runs ios/scripts/generate-fingerprint-hash.zsh before every build of the
|
|
5
|
+
// target, the SwiftPM counterpart of the podspec's script phase. The script
|
|
6
|
+
// writes a Swift file defining HotUpdaterBuildFingerprintHash(), which
|
|
7
|
+
// HotUpdater.mm calls.
|
|
8
|
+
@main
|
|
9
|
+
struct HotUpdaterFingerprintPlugin: BuildToolPlugin {
|
|
10
|
+
func createBuildCommands(context: PluginContext, target: Target) throws -> [Command] {
|
|
11
|
+
let script = context.package.directoryURL
|
|
12
|
+
.appending(path: "ios/scripts/generate-fingerprint-hash.zsh")
|
|
13
|
+
let outputDirectory = context.pluginWorkDirectoryURL
|
|
14
|
+
.appending(path: "Fingerprint")
|
|
15
|
+
// React Native's autolinker references this package through
|
|
16
|
+
// <app>/ios/build/generated/autolinking/libs/HotUpdater, as
|
|
17
|
+
// Package.swift's relative paths assume.
|
|
18
|
+
let appRoot = context.package.directoryURL
|
|
19
|
+
.appending(path: "../../../../../..")
|
|
20
|
+
.standardized
|
|
21
|
+
// A prebuild command gets only this environment. PATH lets the
|
|
22
|
+
// script find node, as React Native's own script phases do, when
|
|
23
|
+
// ios/.xcode.env.local sets no NODE_BINARY.
|
|
24
|
+
var environment: [String: String] = [:]
|
|
25
|
+
for key in ["HOT_UPDATER_APP_ROOT", "CONFIGURATION", "NODE_BINARY", "PATH"] {
|
|
26
|
+
if let value = ProcessInfo.processInfo.environment[key] {
|
|
27
|
+
environment[key] = value
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
return [
|
|
31
|
+
.prebuildCommand(
|
|
32
|
+
displayName: "[HotUpdater] Generate fingerprint",
|
|
33
|
+
executable: URL(filePath: "/bin/zsh"),
|
|
34
|
+
arguments: [
|
|
35
|
+
script.path(percentEncoded: false),
|
|
36
|
+
"--swift",
|
|
37
|
+
outputDirectory.path(percentEncoded: false),
|
|
38
|
+
appRoot.path(percentEncoded: false),
|
|
39
|
+
],
|
|
40
|
+
environment: environment,
|
|
41
|
+
outputFilesDirectory: outputDirectory
|
|
42
|
+
),
|
|
43
|
+
]
|
|
44
|
+
}
|
|
45
|
+
}
|