@protobase/mobile 0.57.3 → 0.57.4
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/android/src/liveupdate/java/net/protobase/mobile/BlockedBundles.java +30 -0
- package/android/src/liveupdate/java/net/protobase/mobile/GuardedLiveUpdate.java +78 -27
- package/android/src/liveupdate/java/net/protobase/mobile/OwnFiles.java +38 -0
- package/android/src/liveupdate/java/net/protobase/mobile/ReleaseDownload.java +9 -29
- package/android/src/liveupdate/java/net/protobase/mobile/ReleaseOrder.java +34 -4
- package/package.json +2 -2
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
package net.protobase.mobile;
|
|
2
|
+
|
|
3
|
+
import java.util.Arrays;
|
|
4
|
+
import java.util.regex.Pattern;
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* The bundles the stock plugin blocked after rolling them back (`autoBlockRolledBackBundles`): ready() adds the id
|
|
8
|
+
* of a bundle that never got ready (LiveUpdate.java:460-462 of @capawesome/capacitor-live-update 8.7.1) to a comma
|
|
9
|
+
* separated list in its preferences (LiveUpdatePreferences.java:19, LiveUpdate.java:1510-1537). The stock code reads it
|
|
10
|
+
* only in sync(), which the guard refuses, so the guard reads it itself.
|
|
11
|
+
*/
|
|
12
|
+
final class BlockedBundles {
|
|
13
|
+
|
|
14
|
+
/** The stock plugin's key, which its source marks "DO NOT CHANGE". */
|
|
15
|
+
static final String KEY = "blockedBundleIds";
|
|
16
|
+
|
|
17
|
+
// The ids the guard lets reach the stock code are bundle ids (BundlesFolder), so a list holding anything else is
|
|
18
|
+
// one the guard cannot read, as after a plugin upgrade that changed its format.
|
|
19
|
+
private static final Pattern ID = Pattern.compile("[A-Za-z0-9_-]{1,64}");
|
|
20
|
+
|
|
21
|
+
private BlockedBundles() {}
|
|
22
|
+
|
|
23
|
+
/** Whether `list`, the stock plugin's, blocks `bundleId`; any id when the list cannot be read, so it fails closed. */
|
|
24
|
+
static boolean blocks(String list, String bundleId) {
|
|
25
|
+
if (list == null || list.isEmpty()) return false;
|
|
26
|
+
String[] ids = list.split(",", -1);
|
|
27
|
+
for (String id : ids) if (!ID.matcher(id).matches()) return true;
|
|
28
|
+
return Arrays.asList(ids).contains(bundleId);
|
|
29
|
+
}
|
|
30
|
+
}
|
|
@@ -1,8 +1,10 @@
|
|
|
1
1
|
package net.protobase.mobile;
|
|
2
2
|
|
|
3
3
|
import android.app.Activity;
|
|
4
|
+
import android.content.res.AssetManager;
|
|
4
5
|
import android.content.SharedPreferences;
|
|
5
6
|
import android.util.Base64;
|
|
7
|
+
import com.getcapacitor.Bridge;
|
|
6
8
|
import com.getcapacitor.JSObject;
|
|
7
9
|
import com.getcapacitor.Logger;
|
|
8
10
|
import com.getcapacitor.Plugin;
|
|
@@ -12,7 +14,9 @@ import com.getcapacitor.PluginMethod;
|
|
|
12
14
|
import com.getcapacitor.annotation.CapacitorPlugin;
|
|
13
15
|
import io.capawesome.capacitorjs.plugins.liveupdate.LiveUpdatePlugin;
|
|
14
16
|
import java.io.File;
|
|
17
|
+
import java.io.FileNotFoundException;
|
|
15
18
|
import java.io.IOException;
|
|
19
|
+
import java.io.InputStream;
|
|
16
20
|
import java.lang.reflect.Method;
|
|
17
21
|
import java.net.URI;
|
|
18
22
|
import java.net.URISyntaxException;
|
|
@@ -23,13 +27,14 @@ import java.security.interfaces.RSAPublicKey;
|
|
|
23
27
|
import java.security.spec.X509EncodedKeySpec;
|
|
24
28
|
import java.util.ArrayList;
|
|
25
29
|
import java.util.Arrays;
|
|
30
|
+
import java.util.HashMap;
|
|
26
31
|
import java.util.Iterator;
|
|
27
32
|
import java.util.List;
|
|
33
|
+
import java.util.Map;
|
|
28
34
|
import java.util.UUID;
|
|
29
35
|
import java.util.concurrent.ExecutorService;
|
|
30
36
|
import java.util.concurrent.Executors;
|
|
31
37
|
import java.util.concurrent.atomic.AtomicInteger;
|
|
32
|
-
import org.json.JSONObject;
|
|
33
38
|
|
|
34
39
|
/**
|
|
35
40
|
* Takes the stock LiveUpdate plugin's (8.7.1) place, so page script cannot steer the live updater. The CLI lists this
|
|
@@ -44,16 +49,21 @@ import org.json.JSONObject;
|
|
|
44
49
|
* or loopback http, and downloads it itself (ReleaseDownload), bound by the release's statement to the id, the pinned
|
|
45
50
|
* `publicKey`, this binary and its order; a caller's checksum and signature are dropped. A few per run of the app, so
|
|
46
51
|
* a loop cannot drain the data plan.
|
|
52
|
+
* - Both take a release only above the order of the binary's own files, `builtInOrder`, which the CLI computes as the
|
|
53
|
+
* server computes a release's, so a reinstalled app cannot be taken below its own files; and neither takes a bundle
|
|
54
|
+
* the stock code blocked after rolling it back (BlockedBundles), which it checks only in the refused sync().
|
|
47
55
|
* - setNextBundle checks the bundle on disk (BundleCheck), then takes it only above the running bundle's order and not
|
|
48
56
|
* below the highest the app downloaded; a refused bundle that is neither running nor next is deleted, as the stock
|
|
49
|
-
* code deletes an unused one (LiveUpdate.java:803-810). It is the only way to a downloaded bundle: reset, sync
|
|
50
|
-
* rest are refused, reload only runs the next bundle and a rollback
|
|
57
|
+
* code deletes an unused one (LiveUpdate.java:803-810). It is the only way to a downloaded bundle: reset, sync, a
|
|
58
|
+
* next bundle of null (the built-in one) and the rest are refused, reload only runs the next bundle and a rollback
|
|
59
|
+
* goes to the built-in one inside the stock code (LiveUpdate.java:1411).
|
|
51
60
|
* - Ids are a single safe component naming a direct child of the bundles folder: the stock code joins them to its path
|
|
52
61
|
* unchecked (LiveUpdate.java:665-667,1299-1302).
|
|
53
62
|
* - Everything else is refused: deleting, resetting, the vendor's cloud, config, channel and ids.
|
|
54
63
|
* - load() also refuses background updates, which sync with the vendor's cloud outside every method here
|
|
55
|
-
* (LiveUpdate.java:420-433), and needs `autoDeleteBundles` and
|
|
56
|
-
* current and next one and a bundle that never gets ready rolls back
|
|
64
|
+
* (LiveUpdate.java:420-433), and needs `autoDeleteBundles`, a `readyTimeout` and `autoBlockRolledBackBundles`, so
|
|
65
|
+
* ready() drops every bundle but the current and next one, and a bundle that never gets ready rolls back and is not
|
|
66
|
+
* taken again: page script cannot pin an older one.
|
|
57
67
|
*
|
|
58
68
|
* The config comes from capacitor.config.json, which ships in the binary outside the web part, beyond page script and
|
|
59
69
|
* a live update. This source set compiles only when the app has the plugin's Gradle project, which the CLI's swap and
|
|
@@ -75,6 +85,7 @@ public class GuardedLiveUpdate extends LiveUpdatePlugin {
|
|
|
75
85
|
private String releasesUrl;
|
|
76
86
|
private PublicKey publicKey;
|
|
77
87
|
private ReleaseBinary binary;
|
|
88
|
+
private long[] builtInOrder;
|
|
78
89
|
|
|
79
90
|
@Override
|
|
80
91
|
public void load() {
|
|
@@ -94,6 +105,13 @@ public class GuardedLiveUpdate extends LiveUpdatePlugin {
|
|
|
94
105
|
if (config.getInt("readyTimeout", 0) <= 0) {
|
|
95
106
|
refuseToLoad("plugins.LiveUpdate.readyTimeout must be above 0 in a Protobase app");
|
|
96
107
|
}
|
|
108
|
+
if (!config.getBoolean("autoBlockRolledBackBundles", false)) {
|
|
109
|
+
refuseToLoad("plugins.LiveUpdate.autoBlockRolledBackBundles must be true in a Protobase app");
|
|
110
|
+
}
|
|
111
|
+
builtInOrder = ReleaseOrder.parseBuiltIn(config.getString("builtInOrder"));
|
|
112
|
+
if (builtInOrder == null) {
|
|
113
|
+
refuseToLoad("plugins.LiveUpdate needs the order of the binary's own files, builtInOrder, in a Protobase app: run protobase mobile again");
|
|
114
|
+
}
|
|
97
115
|
releasesUrl = config.getString("releasesUrl");
|
|
98
116
|
if (!ownSource(releasesUrl)) {
|
|
99
117
|
refuseToLoad("plugins.LiveUpdate.releasesUrl must be an https URL, or http to the device itself, in a Protobase app");
|
|
@@ -147,6 +165,10 @@ public class GuardedLiveUpdate extends LiveUpdatePlugin {
|
|
|
147
165
|
Refusal.rejectMethod(call, "LiveUpdate", "downloadBundle");
|
|
148
166
|
return;
|
|
149
167
|
}
|
|
168
|
+
if (blocked(bundleId)) {
|
|
169
|
+
call.reject("LiveUpdate.downloadBundle refused release " + bundleId + ": it was rolled back");
|
|
170
|
+
return;
|
|
171
|
+
}
|
|
150
172
|
if (downloads.incrementAndGet() > MAX_DOWNLOADS) {
|
|
151
173
|
call.reject("LiveUpdate.downloadBundle was called too often in this run of the app");
|
|
152
174
|
return;
|
|
@@ -155,42 +177,51 @@ public class GuardedLiveUpdate extends LiveUpdatePlugin {
|
|
|
155
177
|
downloader.execute(() -> download(call, bundleId));
|
|
156
178
|
}
|
|
157
179
|
|
|
180
|
+
// Answers the call once what the download left is deleted, so its caller never sees it.
|
|
158
181
|
private void download(PluginCall call, String bundleId) {
|
|
159
|
-
File bundles = BundlesFolder.of(getContext());
|
|
160
182
|
File downloads = new File(getContext().getFilesDir(), DOWNLOADS);
|
|
161
183
|
File dir = new File(downloads, UUID.randomUUID().toString());
|
|
184
|
+
String refusal = null;
|
|
162
185
|
try {
|
|
163
|
-
|
|
164
|
-
String current = bundleIdOf(getBridge().getServerBasePath());
|
|
165
|
-
ReleaseStatement statement = new ReleaseDownload(releasesUrl, publicKey, binary).download(bundleId, dir, current == null ? null : new File(bundles, current), highestOrder());
|
|
166
|
-
File target = new File(bundles, bundleId);
|
|
167
|
-
// The same release downloaded before stays; setNextBundle checks it anyway.
|
|
168
|
-
if (!target.exists() && !dir.renameTo(target)) throw new IOException("Cannot move " + dir);
|
|
169
|
-
synchronized (order) {
|
|
170
|
-
if (ReleaseOrder.compare(statement.order, highestOrder()) > 0) {
|
|
171
|
-
prefs().edit().putString(HIGHEST_ORDER, ReleaseOrder.format(statement.order)).commit();
|
|
172
|
-
}
|
|
173
|
-
}
|
|
174
|
-
call.resolve();
|
|
186
|
+
downloadInto(dir, bundleId);
|
|
175
187
|
} catch (BundleRefused e) {
|
|
176
|
-
|
|
188
|
+
refusal = "LiveUpdate.downloadBundle refused release " + bundleId + ": " + e.getMessage();
|
|
177
189
|
} catch (IOException e) {
|
|
178
|
-
|
|
190
|
+
refusal = "LiveUpdate.downloadBundle could not download release " + bundleId;
|
|
179
191
|
} finally {
|
|
180
192
|
BundleRemoval.delete(downloads, dir);
|
|
181
193
|
}
|
|
194
|
+
if (refusal == null) call.resolve();
|
|
195
|
+
else call.reject(refusal);
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
// Downloads release `bundleId` into `dir`, then moves it into the bundles folder and raises the highest order.
|
|
199
|
+
private void downloadInto(File dir, String bundleId) throws BundleRefused, IOException {
|
|
200
|
+
File bundles = BundlesFolder.of(getContext());
|
|
201
|
+
if (!dir.mkdirs() || !(bundles.isDirectory() || bundles.mkdirs())) throw new IOException("Cannot create " + dir);
|
|
202
|
+
String current = bundleIdOf(getBridge().getServerBasePath());
|
|
203
|
+
OwnFiles own = current == null ? builtInFiles() : OwnFiles.inFolder(new File(bundles, current));
|
|
204
|
+
long[] highest;
|
|
205
|
+
synchronized (order) {
|
|
206
|
+
highest = highestOrder();
|
|
207
|
+
}
|
|
208
|
+
ReleaseStatement statement = new ReleaseDownload(releasesUrl, publicKey, binary).download(bundleId, dir, own, builtInOrder, highest);
|
|
209
|
+
File target = new File(bundles, bundleId);
|
|
210
|
+
// The same release downloaded before stays; setNextBundle checks it anyway.
|
|
211
|
+
if (!target.exists() && !dir.renameTo(target)) throw new IOException("Cannot move " + dir);
|
|
212
|
+
synchronized (order) {
|
|
213
|
+
if (ReleaseOrder.compare(statement.order, highestOrder()) > 0) {
|
|
214
|
+
prefs().edit().putString(HIGHEST_ORDER, ReleaseOrder.format(statement.order)).commit();
|
|
215
|
+
}
|
|
216
|
+
}
|
|
182
217
|
}
|
|
183
218
|
|
|
184
219
|
@Override
|
|
185
220
|
@PluginMethod
|
|
186
221
|
public void setNextBundle(PluginCall call) {
|
|
222
|
+
// A null id, which the stock code takes for the built-in bundle, is refused too: it would take the app below
|
|
223
|
+
// what it downloaded, and only the stock rollback goes there.
|
|
187
224
|
Object bundleId = call.getData().opt("bundleId");
|
|
188
|
-
if (bundleId == null || bundleId == JSONObject.NULL) {
|
|
189
|
-
// The stock code resets to the built-in bundle.
|
|
190
|
-
clear(call);
|
|
191
|
-
super.setNextBundle(call);
|
|
192
|
-
return;
|
|
193
|
-
}
|
|
194
225
|
if (!(bundleId instanceof String) || !BundlesFolder.isBundleId(BundlesFolder.of(getContext()), (String) bundleId)) {
|
|
195
226
|
Refusal.rejectMethod(call, "LiveUpdate", "setNextBundle");
|
|
196
227
|
return;
|
|
@@ -199,6 +230,7 @@ public class GuardedLiveUpdate extends LiveUpdatePlugin {
|
|
|
199
230
|
File bundles = BundlesFolder.of(getContext());
|
|
200
231
|
synchronized (order) {
|
|
201
232
|
try {
|
|
233
|
+
if (blocked(id)) throw new BundleRefused("it was rolled back");
|
|
202
234
|
ReleaseStatement next = BundleCheck.check(new File(bundles, id), id, publicKey, binary);
|
|
203
235
|
ReleaseOrder.acceptNext(next.order, currentOrder(bundles), highestOrder());
|
|
204
236
|
} catch (BundleRefused e) {
|
|
@@ -213,10 +245,24 @@ public class GuardedLiveUpdate extends LiveUpdatePlugin {
|
|
|
213
245
|
}
|
|
214
246
|
}
|
|
215
247
|
|
|
248
|
+
// The binary's own files, in the assets folder Capacitor serves them from, as the manifest the CLI writes beside them lists them; none without one, as in a binary built before it.
|
|
249
|
+
private OwnFiles builtInFiles() throws IOException {
|
|
250
|
+
AssetManager assets = getContext().getAssets();
|
|
251
|
+
ReleaseManifest manifest;
|
|
252
|
+
try (InputStream in = assets.open(Bridge.DEFAULT_WEB_ASSET_DIR + "/" + ReleaseManifest.FILE)) {
|
|
253
|
+
manifest = ReleaseManifest.parse(Bounded.read(in, ReleaseManifest.MAX_BYTES, "the built-in manifest"), new File(Bridge.DEFAULT_WEB_ASSET_DIR));
|
|
254
|
+
} catch (FileNotFoundException | BundleRefused e) {
|
|
255
|
+
return OwnFiles.NONE;
|
|
256
|
+
}
|
|
257
|
+
Map<String, String> hrefs = new HashMap<>();
|
|
258
|
+
for (ReleaseManifest.Item item : manifest.items) hrefs.put(item.checksum, item.href);
|
|
259
|
+
return checksum -> hrefs.containsKey(checksum) ? assets.open(Bridge.DEFAULT_WEB_ASSET_DIR + "/" + hrefs.get(checksum)) : null;
|
|
260
|
+
}
|
|
261
|
+
|
|
216
262
|
// The running bundle's order: the built-in one's, or what its statement says, checked against the key.
|
|
217
263
|
private long[] currentOrder(File bundles) throws BundleRefused {
|
|
218
264
|
String current = bundleIdOf(getBridge().getServerBasePath());
|
|
219
|
-
if (current == null) return
|
|
265
|
+
if (current == null) return builtInOrder;
|
|
220
266
|
try {
|
|
221
267
|
byte[] statement = Bounded.read(new File(new File(bundles, current), ReleaseStatement.FILE), ReleaseStatement.MAX_BYTES, "the running bundle's statement");
|
|
222
268
|
return ReleaseStatement.verify(statement, current, publicKey).order;
|
|
@@ -229,6 +275,11 @@ public class GuardedLiveUpdate extends LiveUpdatePlugin {
|
|
|
229
275
|
return ReleaseOrder.parse(prefs().getString(HIGHEST_ORDER, null));
|
|
230
276
|
}
|
|
231
277
|
|
|
278
|
+
private boolean blocked(String bundleId) {
|
|
279
|
+
String list = getContext().getSharedPreferences(LiveUpdatePlugin.SHARED_PREFERENCES_NAME, Activity.MODE_PRIVATE).getString(BlockedBundles.KEY, null);
|
|
280
|
+
return BlockedBundles.blocks(list, bundleId);
|
|
281
|
+
}
|
|
282
|
+
|
|
232
283
|
private SharedPreferences prefs() {
|
|
233
284
|
return getContext().getSharedPreferences(ORDER_PREFS, Activity.MODE_PRIVATE);
|
|
234
285
|
}
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
package net.protobase.mobile;
|
|
2
|
+
|
|
3
|
+
import java.io.File;
|
|
4
|
+
import java.io.FileInputStream;
|
|
5
|
+
import java.io.IOException;
|
|
6
|
+
import java.io.InputStream;
|
|
7
|
+
import java.util.HashMap;
|
|
8
|
+
import java.util.Map;
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* The files of the bundle the app runs, by checksum, which a download copies instead of fetching: a downloaded
|
|
12
|
+
* bundle's folder, or the binary's own files. A copy is hashed again as it is written, so it is bound by its content
|
|
13
|
+
* to the release's manifest, never by where it came from.
|
|
14
|
+
*/
|
|
15
|
+
interface OwnFiles {
|
|
16
|
+
|
|
17
|
+
/** None: every file is fetched. */
|
|
18
|
+
OwnFiles NONE = checksum -> null;
|
|
19
|
+
|
|
20
|
+
/** The file of checksum `checksum`, opened, or null when the bundle has none. */
|
|
21
|
+
InputStream open(String checksum) throws IOException;
|
|
22
|
+
|
|
23
|
+
/** The files of the bundle folder `dir` as its own manifest lists them; none when it has none that parses. */
|
|
24
|
+
static OwnFiles inFolder(File dir) throws IOException {
|
|
25
|
+
ReleaseManifest manifest;
|
|
26
|
+
try {
|
|
27
|
+
manifest = ReleaseManifest.parse(Bounded.read(new File(dir, ReleaseManifest.FILE), ReleaseManifest.MAX_BYTES, "its manifest"), dir);
|
|
28
|
+
} catch (BundleRefused e) {
|
|
29
|
+
return NONE;
|
|
30
|
+
}
|
|
31
|
+
Map<String, File> files = new HashMap<>();
|
|
32
|
+
for (ReleaseManifest.Item item : manifest.items) {
|
|
33
|
+
File file = new File(dir, item.href);
|
|
34
|
+
if (file.isFile() && !BundleRemoval.isLink(file)) files.put(item.checksum, file);
|
|
35
|
+
}
|
|
36
|
+
return checksum -> files.containsKey(checksum) ? new FileInputStream(files.get(checksum)) : null;
|
|
37
|
+
}
|
|
38
|
+
}
|
|
@@ -1,7 +1,6 @@
|
|
|
1
1
|
package net.protobase.mobile;
|
|
2
2
|
|
|
3
3
|
import java.io.File;
|
|
4
|
-
import java.io.FileInputStream;
|
|
5
4
|
import java.io.FileOutputStream;
|
|
6
5
|
import java.io.IOException;
|
|
7
6
|
import java.io.InputStream;
|
|
@@ -11,8 +10,6 @@ import java.net.HttpURLConnection;
|
|
|
11
10
|
import java.net.URI;
|
|
12
11
|
import java.net.URLEncoder;
|
|
13
12
|
import java.security.PublicKey;
|
|
14
|
-
import java.util.HashMap;
|
|
15
|
-
import java.util.Map;
|
|
16
13
|
|
|
17
14
|
/**
|
|
18
15
|
* Downloads release `bundleId` from `releasesUrl/<id>?href=<file>` into a folder, in place of the stock downloader,
|
|
@@ -20,8 +17,8 @@ import java.util.Map;
|
|
|
20
17
|
* (LiveUpdate.java:817-872 of @capawesome/capacitor-live-update 8.7.1). First the statement, checked against the id,
|
|
21
18
|
* the key, this binary and the highest order the app downloaded; then the manifest, checked against the statement and
|
|
22
19
|
* every href against the folder before any file is fetched; then each file, hashed as it is written and refused unless
|
|
23
|
-
* it is the one the manifest names. A file the running bundle has with the same checksum is copied from it
|
|
24
|
-
* again, so it is bound by its content, never by its href.
|
|
20
|
+
* it is the one the manifest names. A file the running bundle has with the same checksum (OwnFiles) is copied from it
|
|
21
|
+
* and hashed again, so it is bound by its content, never by its href.
|
|
25
22
|
*/
|
|
26
23
|
final class ReleaseDownload {
|
|
27
24
|
|
|
@@ -38,23 +35,23 @@ final class ReleaseDownload {
|
|
|
38
35
|
}
|
|
39
36
|
|
|
40
37
|
/**
|
|
41
|
-
* Fills the empty folder `dir` with release `bundleId`, `
|
|
42
|
-
*
|
|
38
|
+
* Fills the empty folder `dir` with release `bundleId`, copying what `own`, the running bundle's files, has, and
|
|
39
|
+
* checks it as a whole: its order above the built-in files' `builtIn` and not below `highest`, the highest
|
|
40
|
+
* the app downloaded. Returns its statement.
|
|
43
41
|
*/
|
|
44
|
-
ReleaseStatement download(String bundleId, File dir,
|
|
42
|
+
ReleaseStatement download(String bundleId, File dir, OwnFiles own, long[] builtIn, long[] highest) throws BundleRefused, IOException {
|
|
45
43
|
byte[] statementBytes = fetch(bundleId, ReleaseStatement.FILE, ReleaseStatement.MAX_BYTES);
|
|
46
44
|
ReleaseStatement statement = ReleaseStatement.verify(statementBytes, bundleId, key);
|
|
47
45
|
binary.accept(statement);
|
|
48
|
-
|
|
46
|
+
ReleaseOrder.acceptDownload(statement.order, builtIn, highest);
|
|
49
47
|
byte[] manifestBytes = fetch(bundleId, ReleaseManifest.FILE, ReleaseManifest.MAX_BYTES);
|
|
50
48
|
ReleaseManifest manifest = ReleaseManifest.verify(manifestBytes, statement, dir);
|
|
51
|
-
Map<String, File> own = filesOf(current);
|
|
52
49
|
for (ReleaseManifest.Item item : manifest.items) {
|
|
53
50
|
File target = new File(dir, item.href);
|
|
54
51
|
File parent = target.getParentFile();
|
|
55
52
|
if (!parent.isDirectory() && !parent.mkdirs()) throw new IOException("Cannot create " + parent);
|
|
56
|
-
|
|
57
|
-
if (copy != null && item.checksum.equals(write(
|
|
53
|
+
InputStream copy = own.open(item.checksum);
|
|
54
|
+
if (copy != null && item.checksum.equals(write(copy, target, item.size))) continue;
|
|
58
55
|
if (!item.checksum.equals(write(open(bundleId, item.href), target, item.size))) {
|
|
59
56
|
throw new BundleRefused("its " + item.href + " is not the file its manifest names");
|
|
60
57
|
}
|
|
@@ -64,23 +61,6 @@ final class ReleaseDownload {
|
|
|
64
61
|
return BundleCheck.check(dir, bundleId, key, binary);
|
|
65
62
|
}
|
|
66
63
|
|
|
67
|
-
// The running bundle's files by checksum, as its own manifest lists them; none when it has none that parses.
|
|
68
|
-
private static Map<String, File> filesOf(File current) throws IOException {
|
|
69
|
-
Map<String, File> files = new HashMap<>();
|
|
70
|
-
if (current == null) return files;
|
|
71
|
-
ReleaseManifest manifest;
|
|
72
|
-
try {
|
|
73
|
-
manifest = ReleaseManifest.parse(Bounded.read(new File(current, ReleaseManifest.FILE), ReleaseManifest.MAX_BYTES, "its manifest"), current);
|
|
74
|
-
} catch (BundleRefused e) {
|
|
75
|
-
return files;
|
|
76
|
-
}
|
|
77
|
-
for (ReleaseManifest.Item item : manifest.items) {
|
|
78
|
-
File file = new File(current, item.href);
|
|
79
|
-
if (file.isFile() && !BundleRemoval.isLink(file)) files.put(item.checksum, file);
|
|
80
|
-
}
|
|
81
|
-
return files;
|
|
82
|
-
}
|
|
83
|
-
|
|
84
64
|
// Writes at most `size` bytes of `in` to `target`; returns their sha256, or null when there were more or fewer.
|
|
85
65
|
private static String write(InputStream in, File target, long size) throws IOException {
|
|
86
66
|
try (InputStream source = in; OutputStream out = new FileOutputStream(target)) {
|
|
@@ -1,13 +1,18 @@
|
|
|
1
1
|
package net.protobase.mobile;
|
|
2
2
|
|
|
3
|
+
import java.util.regex.Pattern;
|
|
4
|
+
|
|
3
5
|
/**
|
|
4
6
|
* Release orders, compared element by element, a missing element counting as 0, so a later server can add one without
|
|
5
7
|
* breaking binaries already shipped. Kept as text such as `1800000000000,0`.
|
|
6
8
|
*/
|
|
7
9
|
final class ReleaseOrder {
|
|
8
10
|
|
|
9
|
-
/**
|
|
10
|
-
static final long[]
|
|
11
|
+
/** No order: below every release, the highest order downloaded before the app downloaded any. */
|
|
12
|
+
static final long[] NONE = { 0 };
|
|
13
|
+
|
|
14
|
+
// An order the CLI writes: up to 8 elements of up to 19 digits, which parse() still checks for overflow.
|
|
15
|
+
private static final Pattern TEXT = Pattern.compile("[0-9]{1,19}(,[0-9]{1,19}){0,7}");
|
|
11
16
|
|
|
12
17
|
private ReleaseOrder() {}
|
|
13
18
|
|
|
@@ -29,15 +34,40 @@ final class ReleaseOrder {
|
|
|
29
34
|
if (compare(next, highest) < 0) throw new BundleRefused("it is older than a release the app downloaded");
|
|
30
35
|
}
|
|
31
36
|
|
|
37
|
+
/**
|
|
38
|
+
* Refuses to download a release of order `order` unless it is above the app's built-in files, `builtIn`, and not
|
|
39
|
+
* below the highest order the app downloaded, `highest`: equal to it is the same release again, which the app
|
|
40
|
+
* downloaded and may still have to set as the next bundle.
|
|
41
|
+
*/
|
|
42
|
+
static void acceptDownload(long[] order, long[] builtIn, long[] highest) throws BundleRefused {
|
|
43
|
+
if (compare(order, builtIn) <= 0) throw new BundleRefused("it is not newer than the app's built-in files");
|
|
44
|
+
if (compare(order, highest) < 0) throw new BundleRefused("it is older than a release the app downloaded");
|
|
45
|
+
}
|
|
46
|
+
|
|
32
47
|
static String format(long[] order) {
|
|
33
48
|
StringBuilder text = new StringBuilder();
|
|
34
49
|
for (int i = 0; i < order.length; i++) text.append(i == 0 ? "" : ",").append(order[i]);
|
|
35
50
|
return text.toString();
|
|
36
51
|
}
|
|
37
52
|
|
|
38
|
-
/**
|
|
53
|
+
/**
|
|
54
|
+
* The order the CLI wrote, `plugins.LiveUpdate.builtInOrder`, or null when it is missing, malformed or no order
|
|
55
|
+
* above none, so the guard never runs with a floor of 0.
|
|
56
|
+
*/
|
|
57
|
+
static long[] parseBuiltIn(String text) {
|
|
58
|
+
if (text == null || !TEXT.matcher(text).matches()) return null;
|
|
59
|
+
long[] order;
|
|
60
|
+
try {
|
|
61
|
+
order = parse(text);
|
|
62
|
+
} catch (NumberFormatException e) {
|
|
63
|
+
return null;
|
|
64
|
+
}
|
|
65
|
+
return compare(order, NONE) > 0 ? order : null;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/** The order `text` holds, or none. Only this class writes it. */
|
|
39
69
|
static long[] parse(String text) {
|
|
40
|
-
if (text == null || text.isEmpty()) return
|
|
70
|
+
if (text == null || text.isEmpty()) return NONE;
|
|
41
71
|
String[] parts = text.split(",");
|
|
42
72
|
long[] order = new long[parts.length];
|
|
43
73
|
for (int i = 0; i < parts.length; i++) order[i] = Long.parseLong(parts[i]);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@protobase/mobile",
|
|
3
|
-
"version": "0.57.
|
|
3
|
+
"version": "0.57.4",
|
|
4
4
|
"description": "The Capacitor that protobase mobile builds a Protobase app's iOS and Android projects with, at fixed versions, and the native plugins that keep page script away from the session and give the iOS app its bounce; installed by the CLI on first use.",
|
|
5
5
|
"license": "Apache-2.0",
|
|
6
6
|
"repository": {
|
|
@@ -22,7 +22,7 @@
|
|
|
22
22
|
"android/build.gradle",
|
|
23
23
|
"icon"
|
|
24
24
|
],
|
|
25
|
-
"nativeContract":
|
|
25
|
+
"nativeContract": 7,
|
|
26
26
|
"capacitor": {
|
|
27
27
|
"ios": {
|
|
28
28
|
"src": "ios"
|