@capgo/capacitor-updater 7.0.18 → 7.0.25

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.
@@ -235,6 +235,7 @@ public class CapacitorUpdater {
235
235
 
236
236
  boolean success = finishDownload(id, dest, version, sessionKey, checksum, true, isManifest);
237
237
  if (!success) {
238
+ Log.e(TAG, "Finish download failed: " + version);
238
239
  saveBundleInfo(
239
240
  id,
240
241
  new BundleInfo(id, version, BundleStatus.ERROR, new Date(System.currentTimeMillis()), "")
@@ -249,6 +250,7 @@ public class CapacitorUpdater {
249
250
  case FAILED:
250
251
  Data failedData = workInfo.getOutputData();
251
252
  String error = failedData.getString(DownloadService.ERROR);
253
+ Log.e(TAG, "Download failed: " + error + " " + workInfo.getState());
252
254
  String failedVersion = failedData.getString(DownloadService.VERSION);
253
255
  saveBundleInfo(
254
256
  id,
@@ -57,7 +57,7 @@ public class CapacitorUpdaterPlugin extends Plugin {
57
57
  private static final String statsUrlDefault = "https://plugin.capgo.app/stats";
58
58
  private static final String channelUrlDefault = "https://plugin.capgo.app/channel_self";
59
59
 
60
- private final String PLUGIN_VERSION = "7.0.18";
60
+ private final String PLUGIN_VERSION = "7.0.25";
61
61
  private static final String DELAY_CONDITION_PREFERENCES = "";
62
62
 
63
63
  private SharedPreferences.Editor editor;
@@ -16,6 +16,7 @@ import java.io.FileInputStream;
16
16
  import java.net.HttpURLConnection;
17
17
  import java.net.URL;
18
18
  import java.nio.channels.FileChannel;
19
+ import java.nio.file.Files;
19
20
  import java.security.MessageDigest;
20
21
  import java.util.ArrayList;
21
22
  import java.util.Arrays;
@@ -109,7 +110,6 @@ public class DownloadService extends Worker {
109
110
  return createSuccessResult(dest, version, sessionKey, checksum, false);
110
111
  }
111
112
  } catch (Exception e) {
112
- Log.e(TAG, "Error in doWork", e);
113
113
  return createFailureResult(e.getMessage());
114
114
  }
115
115
  }
@@ -163,25 +163,38 @@ public class DownloadService extends Worker {
163
163
  String fileHash = entry.getString("file_hash");
164
164
  String downloadUrl = entry.getString("download_url");
165
165
 
166
+ if (!publicKey.isEmpty() && sessionKey != null && !sessionKey.isEmpty()) {
167
+ try {
168
+ fileHash = CryptoCipherV2.decryptChecksum(fileHash, publicKey);
169
+ } catch (Exception e) {
170
+ Log.e(TAG, "Error decrypting checksum for " + fileName, e);
171
+ hasError.set(true);
172
+ continue;
173
+ }
174
+ }
175
+
176
+ final String finalFileHash = fileHash;
166
177
  File targetFile = new File(destFolder, fileName);
167
- File cacheFile = new File(cacheFolder, fileHash + "_" + new File(fileName).getName());
178
+ File cacheFile = new File(cacheFolder, finalFileHash + "_" + new File(fileName).getName());
168
179
  File builtinFile = new File(builtinFolder, fileName);
169
180
 
170
181
  // Ensure parent directories of the target file exist
171
182
  if (!Objects.requireNonNull(targetFile.getParentFile()).exists() && !targetFile.getParentFile().mkdirs()) {
172
- throw new IOException("Failed to create parent directory for: " + targetFile.getAbsolutePath());
183
+ Log.e(TAG, "Failed to create parent directory for: " + targetFile.getAbsolutePath());
184
+ hasError.set(true);
185
+ continue;
173
186
  }
174
187
 
175
188
  Future<?> future = executor.submit(() -> {
176
189
  try {
177
- if (builtinFile.exists() && verifyChecksum(builtinFile, fileHash)) {
190
+ if (builtinFile.exists() && verifyChecksum(builtinFile, finalFileHash)) {
178
191
  copyFile(builtinFile, targetFile);
179
192
  Log.d(TAG, "using builtin file " + fileName);
180
- } else if (cacheFile.exists() && verifyChecksum(cacheFile, fileHash)) {
193
+ } else if (cacheFile.exists() && verifyChecksum(cacheFile, finalFileHash)) {
181
194
  copyFile(cacheFile, targetFile);
182
195
  Log.d(TAG, "already cached " + fileName);
183
196
  } else {
184
- downloadAndVerify(downloadUrl, targetFile, cacheFile, fileHash, sessionKey, publicKey);
197
+ downloadAndVerify(downloadUrl, targetFile, cacheFile, finalFileHash, sessionKey, publicKey);
185
198
  }
186
199
 
187
200
  long completed = completedFiles.incrementAndGet();
@@ -216,10 +229,12 @@ public class DownloadService extends Worker {
216
229
  }
217
230
 
218
231
  if (hasError.get()) {
232
+ Log.e(TAG, "One or more files failed to download");
219
233
  throw new IOException("One or more files failed to download");
220
234
  }
221
235
  } catch (Exception e) {
222
236
  Log.e(TAG, "Error in handleManifestDownload", e);
237
+ throw new RuntimeException(e.getLocalizedMessage());
223
238
  }
224
239
  }
225
240
 
@@ -317,10 +332,8 @@ public class DownloadService extends Worker {
317
332
  }
318
333
  }
319
334
  } catch (OutOfMemoryError e) {
320
- e.printStackTrace();
321
335
  throw new RuntimeException("low_mem_fail");
322
336
  } catch (Exception e) {
323
- e.printStackTrace();
324
337
  throw new RuntimeException(e.getLocalizedMessage());
325
338
  }
326
339
  }
@@ -334,7 +347,8 @@ public class DownloadService extends Worker {
334
347
  infoFile.createNewFile();
335
348
  tempFile.createNewFile();
336
349
  } catch (IOException e) {
337
- e.printStackTrace();
350
+ Log.e(TAG, "Error in clearDownloadData", e);
351
+ // not a fatal error, so we don't throw an exception
338
352
  }
339
353
  }
340
354
 
@@ -394,18 +408,10 @@ public class DownloadService extends Worker {
394
408
  decryptedExpectedHash = CryptoCipherV2.decryptChecksum(decryptedExpectedHash, publicKey);
395
409
  }
396
410
 
397
- // Decompress the file
398
- try (
399
- FileInputStream fis = new FileInputStream(compressedFile);
400
- BrotliInputStream brotliInputStream = new BrotliInputStream(fis);
401
- FileOutputStream fos = new FileOutputStream(targetFile)
402
- ) {
403
- byte[] buffer = new byte[8192];
404
- int len;
405
- while ((len = brotliInputStream.read(buffer)) != -1) {
406
- fos.write(buffer, 0, len);
407
- }
408
- }
411
+ // Use new decompression method
412
+ byte[] compressedData = Files.readAllBytes(compressedFile.toPath());
413
+ byte[] decompressedData = decompressBrotli(compressedData, targetFile.getName());
414
+ Files.write(targetFile.toPath(), decompressedData);
409
415
 
410
416
  // Delete the compressed file
411
417
  compressedFile.delete();
@@ -417,10 +423,18 @@ public class DownloadService extends Worker {
417
423
  copyFile(targetFile, cacheFile);
418
424
  } else {
419
425
  targetFile.delete();
420
- throw new IOException("Checksum verification failed for " + targetFile.getName());
426
+ throw new IOException(
427
+ "Checksum verification failed for: " +
428
+ downloadUrl +
429
+ " " +
430
+ targetFile.getName() +
431
+ " expected: " +
432
+ decryptedExpectedHash +
433
+ " calculated: " +
434
+ calculatedHash
435
+ );
421
436
  }
422
437
  } catch (Exception e) {
423
- Log.e(TAG, "Error in downloadAndVerify", e);
424
438
  throw new IOException("Error in downloadAndVerify: " + e.getMessage());
425
439
  }
426
440
  }
@@ -453,4 +467,63 @@ public class DownloadService extends Worker {
453
467
  }
454
468
  return sb.toString();
455
469
  }
470
+
471
+ private byte[] decompressBrotli(byte[] data, String fileName) throws IOException {
472
+ // Validate input
473
+ if (data == null) {
474
+ Log.e(TAG, "Error: Null data received for " + fileName);
475
+ throw new IOException("Null data received");
476
+ }
477
+
478
+ // Handle empty files
479
+ if (data.length == 0) {
480
+ return new byte[0];
481
+ }
482
+
483
+ // Handle the special EMPTY_BROTLI_STREAM case
484
+ if (data.length == 3 && data[0] == 0x1B && data[1] == 0x00 && data[2] == 0x06) {
485
+ return new byte[0];
486
+ }
487
+
488
+ // For small files, check if it's a minimal Brotli wrapper
489
+ if (data.length > 3) {
490
+ try {
491
+ // Handle our minimal wrapper pattern
492
+ if (data[0] == 0x1B && data[1] == 0x00 && data[2] == 0x06 && data[data.length - 1] == 0x03) {
493
+ return Arrays.copyOfRange(data, 3, data.length - 1);
494
+ }
495
+
496
+ // Handle brotli.compress minimal wrapper (quality 0)
497
+ if (data[0] == 0x0b && data[1] == 0x02 && data[2] == (byte) 0x80 && data[data.length - 1] == 0x03) {
498
+ return Arrays.copyOfRange(data, 3, data.length - 1);
499
+ }
500
+ } catch (ArrayIndexOutOfBoundsException e) {
501
+ Log.e(TAG, "Error: Malformed data for " + fileName);
502
+ throw new IOException("Malformed data structure");
503
+ }
504
+ }
505
+
506
+ // For all other cases, try standard decompression
507
+ try (
508
+ ByteArrayInputStream bis = new ByteArrayInputStream(data);
509
+ BrotliInputStream brotliInputStream = new BrotliInputStream(bis);
510
+ ByteArrayOutputStream bos = new ByteArrayOutputStream()
511
+ ) {
512
+ byte[] buffer = new byte[8192];
513
+ int len;
514
+ while ((len = brotliInputStream.read(buffer)) != -1) {
515
+ bos.write(buffer, 0, len);
516
+ }
517
+ return bos.toByteArray();
518
+ } catch (IOException e) {
519
+ Log.e(TAG, "Error: Brotli process failed for " + fileName + ". Status: " + e.getMessage());
520
+ // Add hex dump for debugging
521
+ StringBuilder hexDump = new StringBuilder();
522
+ for (int i = 0; i < Math.min(32, data.length); i++) {
523
+ hexDump.append(String.format("%02x ", data[i]));
524
+ }
525
+ Log.e(TAG, "Error: Raw data (" + fileName + "): " + hexDump.toString());
526
+ throw e;
527
+ }
528
+ }
456
529
  }
@@ -347,7 +347,7 @@ import UIKit
347
347
  return FileManager.default.urls(for: .documentDirectory, in: .userDomainMask).first!.appendingPathComponent("update.dat")
348
348
  }
349
349
  private var tempData = Data()
350
-
350
+
351
351
  private func verifyChecksum(file: URL, expectedHash: String) -> Bool {
352
352
  let actualHash = CryptoCipherV2.calcChecksum(filePath: file)
353
353
  return actualHash == expectedHash
@@ -446,9 +446,9 @@ import UIKit
446
446
  let statusCode = response.response?.statusCode ?? 200
447
447
  if statusCode < 200 || statusCode >= 300 {
448
448
  if let stringData = String(data: data, encoding: .utf8) {
449
- throw NSError(domain: "StatusCodeError", code: 2, userInfo: [NSLocalizedDescriptionKey: "Failed to fetch. Status code (\(statusCode)) invalid. Data: \(stringData)"])
449
+ throw NSError(domain: "StatusCodeError", code: 2, userInfo: [NSLocalizedDescriptionKey: "Failed to fetch. Status code (\(statusCode)) invalid. Data: \(stringData) for file \(fileName) at url \(downloadUrl)"])
450
450
  } else {
451
- throw NSError(domain: "StatusCodeError", code: 2, userInfo: [NSLocalizedDescriptionKey: "Failed to fetch. Status code (\(statusCode)) invalid"])
451
+ throw NSError(domain: "StatusCodeError", code: 2, userInfo: [NSLocalizedDescriptionKey: "Failed to fetch. Status code (\(statusCode)) invalid for file \(fileName) at url \(downloadUrl)"])
452
452
  }
453
453
  }
454
454
 
@@ -470,7 +470,7 @@ import UIKit
470
470
 
471
471
  // Decompress the Brotli data
472
472
  guard let decompressedData = self.decompressBrotli(data: finalData, fileName: fileName) else {
473
- throw NSError(domain: "BrotliDecompressionError", code: 1, userInfo: [NSLocalizedDescriptionKey: "Failed to decompress Brotli data"])
473
+ throw NSError(domain: "BrotliDecompressionError", code: 1, userInfo: [NSLocalizedDescriptionKey: "Failed to decompress Brotli data for file \(fileName) at url \(downloadUrl)"])
474
474
  }
475
475
  finalData = decompressedData
476
476
 
@@ -479,7 +479,7 @@ import UIKit
479
479
  // assume that calcChecksum != null
480
480
  let calculatedChecksum = CryptoCipherV2.calcChecksum(filePath: destFilePath)
481
481
  if calculatedChecksum != fileHash {
482
- throw NSError(domain: "ChecksumError", code: 1, userInfo: [NSLocalizedDescriptionKey: "Computed checksum is not equal to required checksum (\(calculatedChecksum) != \(fileHash))"])
482
+ throw NSError(domain: "ChecksumError", code: 1, userInfo: [NSLocalizedDescriptionKey: "Computed checksum is not equal to required checksum (\(calculatedChecksum) != \(fileHash)) for file \(fileName) at url \(downloadUrl)"])
483
483
  }
484
484
  }
485
485
 
@@ -491,10 +491,10 @@ import UIKit
491
491
  print("\(CapacitorUpdater.TAG) downloadManifest \(id) \(fileName) downloaded, decompressed\(!self.publicKey.isEmpty && !sessionKey.isEmpty ? ", decrypted" : ""), and cached")
492
492
  } catch {
493
493
  downloadError = error
494
- print("\(CapacitorUpdater.TAG) downloadManifest \(id) \(fileName) error: \(error)")
494
+ NSLog("\(CapacitorUpdater.TAG) downloadManifest \(id) \(fileName) error: \(error.localizedDescription)")
495
495
  }
496
496
  case .failure(let error):
497
- print("\(CapacitorUpdater.TAG) downloadManifest \(id) \(fileName) download error: \(error). Debug response: \(response.debugDescription).")
497
+ NSLog("\(CapacitorUpdater.TAG) downloadManifest \(id) \(fileName) download error: \(error.localizedDescription). Debug response: \(response.debugDescription).")
498
498
  }
499
499
  }
500
500
  }
@@ -518,14 +518,43 @@ import UIKit
518
518
  }
519
519
 
520
520
  private func decompressBrotli(data: Data, fileName: String) -> Data? {
521
+ // Handle empty files
522
+ if data.count == 0 {
523
+ return data
524
+ }
525
+
526
+ // Handle the special EMPTY_BROTLI_STREAM case
527
+ if data.count == 3 && data[0] == 0x1B && data[1] == 0x00 && data[2] == 0x06 {
528
+ return Data()
529
+ }
530
+
531
+ // For small files, check if it's a minimal Brotli wrapper
532
+ if data.count > 3 {
533
+ let maxBytes = min(32, data.count)
534
+ let hexDump = data.prefix(maxBytes).map { String(format: "%02x", $0) }.joined(separator: " ")
535
+ // Handle our minimal wrapper pattern
536
+ if data[0] == 0x1B && data[1] == 0x00 && data[2] == 0x06 && data.last == 0x03 {
537
+ let range = data.index(data.startIndex, offsetBy: 3)..<data.index(data.endIndex, offsetBy: -1)
538
+ return data[range]
539
+ }
540
+
541
+ // Handle brotli.compress minimal wrapper (quality 0)
542
+ if data[0] == 0x0b && data[1] == 0x02 && data[2] == 0x80 && data.last == 0x03 {
543
+ let range = data.index(data.startIndex, offsetBy: 3)..<data.index(data.endIndex, offsetBy: -1)
544
+ return data[range]
545
+ }
546
+ }
547
+
548
+ // For all other cases, try standard decompression
521
549
  let outputBufferSize = 65536
522
550
  var outputBuffer = [UInt8](repeating: 0, count: outputBufferSize)
523
551
  var decompressedData = Data()
524
552
 
525
553
  let streamPointer = UnsafeMutablePointer<compression_stream>.allocate(capacity: 1)
526
554
  var status = compression_stream_init(streamPointer, COMPRESSION_STREAM_DECODE, COMPRESSION_BROTLI)
555
+
527
556
  guard status != COMPRESSION_STATUS_ERROR else {
528
- print("\(CapacitorUpdater.TAG) Unable to initialize the decompression stream. \(fileName)")
557
+ print("\(CapacitorUpdater.TAG) Error: Failed to initialize Brotli stream for \(fileName). Status: \(status)")
529
558
  return nil
530
559
  }
531
560
 
@@ -547,7 +576,7 @@ import UIKit
547
576
  if let baseAddress = rawBufferPointer.baseAddress {
548
577
  streamPointer.pointee.src_ptr = baseAddress.assumingMemoryBound(to: UInt8.self)
549
578
  } else {
550
- print("\(CapacitorUpdater.TAG) Error: Unable to get base address of input data. \(fileName)")
579
+ print("\(CapacitorUpdater.TAG) Error: Failed to get base address for \(fileName)")
551
580
  status = COMPRESSION_STATUS_ERROR
552
581
  return
553
582
  }
@@ -555,6 +584,9 @@ import UIKit
555
584
  }
556
585
 
557
586
  if status == COMPRESSION_STATUS_ERROR {
587
+ let maxBytes = min(32, data.count)
588
+ let hexDump = data.prefix(maxBytes).map { String(format: "%02x", $0) }.joined(separator: " ")
589
+ print("\(CapacitorUpdater.TAG) Error: Brotli decompression failed for \(fileName). First \(maxBytes) bytes: \(hexDump)")
558
590
  break
559
591
  }
560
592
 
@@ -568,15 +600,19 @@ import UIKit
568
600
  if status == COMPRESSION_STATUS_END {
569
601
  break
570
602
  } else if status == COMPRESSION_STATUS_ERROR {
571
- print("\(CapacitorUpdater.TAG) Error during Brotli decompression. \(fileName)")
572
- // Try to decode as text if mostly ASCII
603
+ print("\(CapacitorUpdater.TAG) Error: Brotli process failed for \(fileName). Status: \(status)")
573
604
  if let text = String(data: data, encoding: .utf8) {
574
605
  let asciiCount = text.unicodeScalars.filter { $0.isASCII }.count
575
606
  let totalCount = text.unicodeScalars.count
576
607
  if totalCount > 0 && Double(asciiCount) / Double(totalCount) >= 0.8 {
577
- print("\(CapacitorUpdater.TAG) Compressed data as text: \(text)")
608
+ print("\(CapacitorUpdater.TAG) Error: Input appears to be plain text: \(text)")
578
609
  }
579
610
  }
611
+
612
+ let maxBytes = min(32, data.count)
613
+ let hexDump = data.prefix(maxBytes).map { String(format: "%02x", $0) }.joined(separator: " ")
614
+ print("\(CapacitorUpdater.TAG) Error: Raw data (\(fileName)): \(hexDump)")
615
+
580
616
  return nil
581
617
  }
582
618
 
@@ -586,6 +622,7 @@ import UIKit
586
622
  }
587
623
 
588
624
  if input.count == 0 {
625
+ print("\(CapacitorUpdater.TAG) Error: Zero input size for \(fileName)")
589
626
  break
590
627
  }
591
628
  }
@@ -45,7 +45,7 @@ public class CapacitorUpdaterPlugin: CAPPlugin, CAPBridgedPlugin {
45
45
  CAPPluginMethod(name: "getNextBundle", returnType: CAPPluginReturnPromise)
46
46
  ]
47
47
  public var implementation = CapacitorUpdater()
48
- private let PLUGIN_VERSION: String = "7.0.18"
48
+ private let PLUGIN_VERSION: String = "7.0.25"
49
49
  static let updateUrlDefault = "https://plugin.capgo.app/updates"
50
50
  static let statsUrlDefault = "https://plugin.capgo.app/stats"
51
51
  static let channelUrlDefault = "https://plugin.capgo.app/channel_self"
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@capgo/capacitor-updater",
3
- "version": "7.0.18",
3
+ "version": "7.0.25",
4
4
  "license": "MPL-2.0",
5
5
  "description": "Live update for capacitor apps",
6
6
  "main": "dist/plugin.cjs.js",