react-native-sherpa-onnx 0.3.5 → 0.3.6

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 (43) hide show
  1. package/README.md +2 -1
  2. package/android/prebuilt-download.gradle +1 -1
  3. package/android/prebuilt-versions.gradle +1 -1
  4. package/android/src/main/cpp/jni/archive/sherpa-onnx-archive-helper.cpp +306 -6
  5. package/android/src/main/cpp/jni/archive/sherpa-onnx-archive-helper.h +33 -4
  6. package/android/src/main/cpp/jni/archive/sherpa-onnx-archive-jni.cpp +266 -7
  7. package/android/src/main/java/com/sherpaonnx/SherpaOnnxArchiveHelper.kt +100 -0
  8. package/android/src/main/java/com/sherpaonnx/SherpaOnnxAssetHelper.kt +51 -6
  9. package/android/src/main/java/com/sherpaonnx/SherpaOnnxModule.kt +60 -0
  10. package/ios/SherpaOnnx.mm +54 -1
  11. package/ios/archive/sherpa-onnx-archive-helper.h +7 -0
  12. package/ios/archive/sherpa-onnx-archive-helper.mm +18 -0
  13. package/lib/module/NativeSherpaOnnx.js.map +1 -1
  14. package/lib/module/download/extractTarZst.js +52 -0
  15. package/lib/module/download/extractTarZst.js.map +1 -0
  16. package/lib/module/download/index.js +1 -0
  17. package/lib/module/download/index.js.map +1 -1
  18. package/lib/module/extraction/index.js +191 -0
  19. package/lib/module/extraction/index.js.map +1 -0
  20. package/lib/module/extraction/types.js +2 -0
  21. package/lib/module/extraction/types.js.map +1 -0
  22. package/lib/module/index.js +1 -0
  23. package/lib/module/index.js.map +1 -1
  24. package/lib/typescript/src/NativeSherpaOnnx.d.ts +39 -0
  25. package/lib/typescript/src/NativeSherpaOnnx.d.ts.map +1 -1
  26. package/lib/typescript/src/download/extractTarZst.d.ts +14 -0
  27. package/lib/typescript/src/download/extractTarZst.d.ts.map +1 -0
  28. package/lib/typescript/src/download/index.d.ts +2 -0
  29. package/lib/typescript/src/download/index.d.ts.map +1 -1
  30. package/lib/typescript/src/extraction/index.d.ts +50 -0
  31. package/lib/typescript/src/extraction/index.d.ts.map +1 -0
  32. package/lib/typescript/src/extraction/types.d.ts +60 -0
  33. package/lib/typescript/src/extraction/types.d.ts.map +1 -0
  34. package/lib/typescript/src/index.d.ts.map +1 -1
  35. package/package.json +6 -1
  36. package/src/NativeSherpaOnnx.ts +56 -0
  37. package/src/download/extractTarZst.ts +77 -0
  38. package/src/download/index.ts +2 -0
  39. package/src/extraction/index.ts +273 -0
  40. package/src/extraction/types.ts +63 -0
  41. package/src/index.tsx +1 -0
  42. package/third_party/libarchive_prebuilt/ANDROID_RELEASE_TAG +1 -1
  43. package/third_party/libarchive_prebuilt/IOS_RELEASE_TAG +1 -1
package/README.md CHANGED
@@ -96,7 +96,7 @@ To build the sherpa-onnx iOS XCFramework yourself (e.g. custom version or patche
96
96
  | Text-to-Speech | ✅ **Supported** | Multiple model types (VITS, Matcha, Kokoro, etc.). See [Supported Model Types](#supported-model-types) and [TTS documentation](./docs/tts.md). |
97
97
  | Streaming Text-to-Speech | ✅ **Supported** | Incremental speech generation for low time-to-first-byte and playback while generating. See [Streaming TTS](./docs/tts-streaming.md). |
98
98
  | Execution providers (CPU, NNAPI, XNNPACK, Core ML, QNN) | ✅ **Supported** | See [Execution provider support](./docs/execution-providers.md). |
99
- | Play Asset Delivery (PAD) | ✅ **Supported** | Android only. See [Model Setup](./docs/model-setup.md). |
99
+ | Play Asset Delivery (PAD) | ✅ **Supported** | Android only. See [Model Setup](./docs/model-setup.md) & [Extraction API](./docs/extraction.md). |
100
100
  | Automatic Model type detection | ✅ **Supported** | `detectSttModel()` and `detectTtsModel()` for a path. See [Model Setup: Model type detection](./docs/model-setup.md#model-detection). |
101
101
  | Model quantization | ✅ **Supported** | Automatic detection and preference for quantized (int8) models. |
102
102
  | Flexible model loading | ✅ **Supported** | Asset models, file system models, or auto-detection. |
@@ -166,6 +166,7 @@ For **streaming TTS** (incremental generation, low latency), use `createStreamin
166
166
  - [Source Separation](./docs/separation.md)
167
167
  - [Model Setup](./docs/model-setup.md) – Bundled assets, Play Asset Delivery (PAD), model discovery APIs, and troubleshooting
168
168
  - [Model Download Manager](./docs/download-manager.md)
169
+ - [Extraction API](./docs/extraction.md)
169
170
  - [Disable FFMPEG](./docs/disable-ffmpeg.md)
170
171
  - [Disable LIBARCHIVE](./docs/disable-libarchive.md)
171
172
 
@@ -14,7 +14,7 @@ def requiredFfmpegSoFiles = [
14
14
  def requiredSherpaOnnxSoFiles = [
15
15
  'libsherpa-onnx-jni.so', 'libsherpa-onnx-c-api.so', 'libsherpa-onnx-cxx-api.so', 'libonnxruntime.so'
16
16
  ]
17
- def requiredLibarchiveSoFiles = ['libarchive.so']
17
+ def requiredLibarchiveSoFiles = ['libarchive.so', 'libzstd.so']
18
18
  def requiredOnnxruntimeJniSoFiles = ['libonnxruntime4j_jni.so']
19
19
 
20
20
  def jniLibsDir = file("${project.projectDir}/src/main/jniLibs")
@@ -36,7 +36,7 @@ println "[react-native-sherpa-onnx] ffmpeg version (extracted/used): ${ffmpegVer
36
36
  def libarchiveVersion = System.getenv('LIBARCHIVE_VERSION')
37
37
  if (!libarchiveVersion) {
38
38
  def v = readVersionFromTagFile(new File(moduleRoot, 'third_party/libarchive_prebuilt/ANDROID_RELEASE_TAG'), 'libarchive-android-v')
39
- libarchiveVersion = v ?: (project.hasProperty('libarchiveVersion') ? project.libarchiveVersion : '3.8.5')
39
+ libarchiveVersion = v ?: (project.hasProperty('libarchiveVersion') ? project.libarchiveVersion : '3.8.5-2')
40
40
  }
41
41
  project.ext.libarchiveVersion = libarchiveVersion
42
42
  println "[react-native-sherpa-onnx] libarchive version (extracted/used): ${libarchiveVersion}"
@@ -75,6 +75,38 @@ static void DrainRemainingAndClose(ArchiveReadContext* ctx) {
75
75
  fclose(ctx->file);
76
76
  ctx->file = nullptr;
77
77
  }
78
+
79
+ struct StreamReadContext {
80
+ std::array<unsigned char, 64 * 1024> buffer{};
81
+ Sha256Context sha_ctx{};
82
+ long long bytes_read = 0;
83
+ ArchiveHelper::StreamReadCallback read_cb = nullptr;
84
+ void* user_data = nullptr;
85
+ };
86
+
87
+ static la_ssize_t ArchiveStreamReadCallback(struct archive* archive, void* client_data, const void** buff) {
88
+ auto* ctx = static_cast<StreamReadContext*>(client_data);
89
+ if (!ctx || !ctx->read_cb) {
90
+ archive_set_error(archive, EINVAL, "Invalid stream read context");
91
+ return -1;
92
+ }
93
+ std::ptrdiff_t n = ctx->read_cb(ctx->buffer.data(), ctx->buffer.size(), ctx->user_data);
94
+ if (n > 0) {
95
+ sha256_update(&ctx->sha_ctx, ctx->buffer.data(), static_cast<size_t>(n));
96
+ ctx->bytes_read += static_cast<long long>(n);
97
+ *buff = ctx->buffer.data();
98
+ return static_cast<la_ssize_t>(n);
99
+ }
100
+ if (n == 0) return 0;
101
+ archive_set_error(archive, EINVAL, "Stream read error");
102
+ return -1;
103
+ }
104
+
105
+ // No-op close callback for stream mode: the stream lifetime is managed by the
106
+ // caller (e.g. a JNI InputStream), so libarchive must not close it.
107
+ static int ArchiveStreamCloseCallback(struct archive* /* archive */, void* /* client_data */) {
108
+ return ARCHIVE_OK;
109
+ }
78
110
  #endif // HAVE_LIBARCHIVE
79
111
 
80
112
  static std::string ToHex(const unsigned char* data, size_t size) {
@@ -122,13 +154,15 @@ bool ArchiveHelper::ExtractTarBz2(
122
154
  return false;
123
155
  }
124
156
 
125
- // Check target directory
157
+ // If target exists and is a directory, extract into it (merge). Otherwise require empty or force-remove.
126
158
  if (std::filesystem::exists(target_path)) {
127
- if (force) {
159
+ if (std::filesystem::is_directory(target_path)) {
160
+ // Merge: extract into existing directory (e.g. multiple archives → same base path)
161
+ } else if (force) {
128
162
  std::error_code ec;
129
163
  std::filesystem::remove_all(target_path, ec);
130
164
  if (ec) {
131
- if (out_error) *out_error = "Failed to remove target directory: " + ec.message();
165
+ if (out_error) *out_error = "Failed to remove target path: " + ec.message();
132
166
  return false;
133
167
  }
134
168
  } else {
@@ -137,7 +171,6 @@ bool ArchiveHelper::ExtractTarBz2(
137
171
  }
138
172
  }
139
173
 
140
- // Create target directory
141
174
  std::error_code ec;
142
175
  std::filesystem::create_directories(target_path, ec);
143
176
  if (ec) {
@@ -167,11 +200,12 @@ bool ArchiveHelper::ExtractTarBz2(
167
200
  return false;
168
201
  }
169
202
 
170
- // Configure archive to support tar and bzip2
203
+ // Configure archive to support tar and common compression (bzip2, gzip, xz, zstd)
171
204
  archive_read_support_format_tar(archive);
172
205
  archive_read_support_filter_bzip2(archive);
173
206
  archive_read_support_filter_gzip(archive); // Also support gzip for compatibility
174
- archive_read_support_filter_xz(archive); // And xz
207
+ archive_read_support_filter_xz(archive); // And xz
208
+ archive_read_support_filter_zstd(archive); // And zstd (.tar.zst)
175
209
 
176
210
  ArchiveReadContext read_ctx;
177
211
  read_ctx.file = fopen(source_path.c_str(), "rb");
@@ -354,6 +388,18 @@ bool ArchiveHelper::ExtractTarBz2(
354
388
  close_reader();
355
389
  return false;
356
390
  }
391
+
392
+ result = archive_write_finish_entry(disk);
393
+ if (result != ARCHIVE_OK && result != ARCHIVE_WARN) {
394
+ const char* err = archive_error_string(disk);
395
+ if (out_error) {
396
+ *out_error = err ? std::string("Failed to finish entry: ") + err : "Failed to finish entry";
397
+ }
398
+ archive_read_free(archive);
399
+ archive_write_free(disk);
400
+ close_reader();
401
+ return false;
402
+ }
357
403
  }
358
404
 
359
405
  archive_read_free(archive);
@@ -376,6 +422,260 @@ bool ArchiveHelper::ExtractTarBz2(
376
422
  #endif // HAVE_LIBARCHIVE
377
423
  }
378
424
 
425
+ bool ArchiveHelper::ExtractTarZst(
426
+ const std::string& source_path,
427
+ const std::string& target_path,
428
+ bool force,
429
+ std::function<void(long long, long long, double)> on_progress,
430
+ std::string* out_error,
431
+ std::string* out_sha256) {
432
+ return ExtractTarBz2(source_path, target_path, force, on_progress, out_error, out_sha256);
433
+ }
434
+
435
+ bool ArchiveHelper::ExtractFromStream(
436
+ StreamReadCallback read_cb,
437
+ void* read_user_data,
438
+ const std::string& target_path,
439
+ bool force,
440
+ std::function<void(long long, long long, double)> on_progress,
441
+ std::string* out_error,
442
+ std::string* out_sha256) {
443
+ cancel_requested_.store(false);
444
+
445
+ #ifndef HAVE_LIBARCHIVE
446
+ (void)read_cb;
447
+ (void)read_user_data;
448
+ (void)target_path;
449
+ (void)force;
450
+ (void)on_progress;
451
+ (void)out_sha256;
452
+ if (out_error) *out_error = "libarchive not available. Build with libarchive or set sherpaOnnxDisableLibarchive=false in gradle.properties. See docs/disable-libarchive.md.";
453
+ return false;
454
+ #else
455
+ if (!read_cb) {
456
+ if (out_error) *out_error = "Stream read callback is null";
457
+ return false;
458
+ }
459
+
460
+ if (std::filesystem::exists(target_path)) {
461
+ if (std::filesystem::is_directory(target_path)) {
462
+ // Merge: extract into existing directory (e.g. multiple archives → same base path)
463
+ } else if (force) {
464
+ std::error_code ec;
465
+ std::filesystem::remove_all(target_path, ec);
466
+ if (ec) {
467
+ if (out_error) *out_error = "Failed to remove target path: " + ec.message();
468
+ return false;
469
+ }
470
+ } else {
471
+ if (out_error) *out_error = "Target path already exists";
472
+ return false;
473
+ }
474
+ }
475
+
476
+ std::error_code ec;
477
+ std::filesystem::create_directories(target_path, ec);
478
+ if (ec) {
479
+ if (out_error) *out_error = "Failed to create target directory: " + ec.message();
480
+ return false;
481
+ }
482
+
483
+ #ifndef NDEBUG
484
+ __android_log_print(ANDROID_LOG_INFO, "SherpaOnnx",
485
+ "ExtractFromStream target_path=%s", target_path.c_str());
486
+ #endif
487
+
488
+ std::string canonical_target = std::filesystem::canonical(target_path).string();
489
+ if (canonical_target.back() != '/') canonical_target += '/';
490
+
491
+ const long long total_bytes = 0;
492
+
493
+ struct archive* archive = archive_read_new();
494
+ if (!archive) {
495
+ if (out_error) *out_error = "Failed to create archive reader";
496
+ return false;
497
+ }
498
+
499
+ archive_read_support_format_tar(archive);
500
+ archive_read_support_filter_bzip2(archive);
501
+ archive_read_support_filter_gzip(archive);
502
+ archive_read_support_filter_xz(archive);
503
+ archive_read_support_filter_zstd(archive);
504
+
505
+ StreamReadContext stream_ctx;
506
+ stream_ctx.read_cb = read_cb;
507
+ stream_ctx.user_data = read_user_data;
508
+ sha256_init(&stream_ctx.sha_ctx);
509
+
510
+ if (archive_read_open(archive, &stream_ctx, nullptr, ArchiveStreamReadCallback, ArchiveStreamCloseCallback) != ARCHIVE_OK) {
511
+ const char* err = archive_error_string(archive);
512
+ if (out_error) *out_error = err ? std::string("Failed to open archive: ") + err : "Failed to open archive";
513
+ archive_read_free(archive);
514
+ return false;
515
+ }
516
+
517
+ struct archive* disk = archive_write_disk_new();
518
+ if (!disk) {
519
+ if (out_error) *out_error = "Failed to create disk writer";
520
+ archive_read_free(archive);
521
+ return false;
522
+ }
523
+
524
+ archive_write_disk_set_options(disk,
525
+ ARCHIVE_EXTRACT_TIME |
526
+ ARCHIVE_EXTRACT_PERM |
527
+ ARCHIVE_EXTRACT_ACL |
528
+ ARCHIVE_EXTRACT_FFLAGS);
529
+ archive_write_disk_set_standard_lookup(disk);
530
+
531
+ struct archive_entry* entry = nullptr;
532
+ int result = ARCHIVE_OK;
533
+ long long extracted_bytes = 0;
534
+ int last_percent = -1;
535
+ long long last_emit_bytes = 0;
536
+ int entry_index = 0;
537
+
538
+ while ((result = archive_read_next_header(archive, &entry)) == ARCHIVE_OK) {
539
+ if (cancel_requested_.load()) {
540
+ if (out_error) *out_error = "Extraction cancelled";
541
+ archive_read_free(archive);
542
+ archive_write_free(disk);
543
+ return false;
544
+ }
545
+
546
+ const char* current_path = archive_entry_pathname(entry);
547
+ if (!current_path) {
548
+ archive_read_free(archive);
549
+ archive_write_free(disk);
550
+ if (out_error) *out_error = "Invalid entry path";
551
+ return false;
552
+ }
553
+
554
+ std::string entry_path(current_path);
555
+ std::string full_path = target_path;
556
+ if (full_path.back() != '/') full_path += '/';
557
+ full_path += entry_path;
558
+
559
+ std::string canonical_entry;
560
+ try {
561
+ std::filesystem::path p(full_path);
562
+ std::filesystem::path parent = p.parent_path();
563
+ if (std::filesystem::exists(parent)) {
564
+ canonical_entry = std::filesystem::canonical(parent).string();
565
+ } else {
566
+ while (!std::filesystem::exists(parent) && parent != parent.parent_path()) {
567
+ parent = parent.parent_path();
568
+ }
569
+ if (std::filesystem::exists(parent)) {
570
+ canonical_entry = std::filesystem::canonical(parent).string();
571
+ } else {
572
+ canonical_entry = canonical_target;
573
+ }
574
+ }
575
+ canonical_entry += '/';
576
+ canonical_entry += p.filename().string();
577
+ } catch (const std::exception&) {
578
+ canonical_entry = full_path;
579
+ }
580
+
581
+ if (canonical_entry.find(canonical_target) != 0) {
582
+ archive_read_free(archive);
583
+ archive_write_free(disk);
584
+ if (out_error) *out_error = "Blocked path traversal: " + entry_path;
585
+ return false;
586
+ }
587
+
588
+ archive_entry_set_pathname(entry, full_path.c_str());
589
+
590
+ result = archive_write_header(disk, entry);
591
+ if (result != ARCHIVE_OK) {
592
+ const char* err = archive_error_string(disk);
593
+ if (out_error) *out_error = err ? std::string("Failed to write entry: ") + err : "Failed to write entry";
594
+ archive_read_free(archive);
595
+ archive_write_free(disk);
596
+ return false;
597
+ }
598
+
599
+ const void* buff = nullptr;
600
+ size_t size = 0;
601
+ la_int64_t offset = 0;
602
+
603
+ while ((result = archive_read_data_block(archive, &buff, &size, &offset)) == ARCHIVE_OK) {
604
+ if (cancel_requested_.load()) {
605
+ if (out_error) *out_error = "Extraction cancelled";
606
+ archive_read_free(archive);
607
+ archive_write_free(disk);
608
+ return false;
609
+ }
610
+
611
+ result = archive_write_data_block(disk, buff, size, offset);
612
+ if (result != ARCHIVE_OK) {
613
+ const char* err = archive_error_string(disk);
614
+ if (out_error) *out_error = err ? std::string("Failed to write data: ") + err : "Failed to write data";
615
+ archive_read_free(archive);
616
+ archive_write_free(disk);
617
+ return false;
618
+ }
619
+
620
+ extracted_bytes += static_cast<long long>(size);
621
+
622
+ if (on_progress) {
623
+ if (total_bytes > 0) {
624
+ long long compressed_bytes = archive_filter_bytes(archive, -1);
625
+ int percent = static_cast<int>(total_bytes > 0 ? (compressed_bytes * 100) / total_bytes : 0);
626
+ percent = (percent > 100) ? 100 : ((percent < 0) ? 0 : percent);
627
+ if (percent != last_percent) {
628
+ last_percent = percent;
629
+ on_progress(compressed_bytes, total_bytes, static_cast<double>(percent));
630
+ }
631
+ } else if (stream_ctx.bytes_read - last_emit_bytes >= 1024 * 1024) {
632
+ last_emit_bytes = stream_ctx.bytes_read;
633
+ on_progress(stream_ctx.bytes_read, 0, 0.0);
634
+ }
635
+ }
636
+ }
637
+
638
+ if (result != ARCHIVE_EOF && result != ARCHIVE_OK) {
639
+ const char* err = archive_error_string(archive);
640
+ if (out_error) *out_error = err ? std::string("Failed to read data: ") + err : "Failed to read data";
641
+ archive_read_free(archive);
642
+ archive_write_free(disk);
643
+ return false;
644
+ }
645
+
646
+ result = archive_write_finish_entry(disk);
647
+ if (result != ARCHIVE_OK && result != ARCHIVE_WARN) {
648
+ const char* err = archive_error_string(disk);
649
+ if (out_error) *out_error = err ? std::string("Failed to finish entry: ") + err : "Failed to finish entry";
650
+ archive_read_free(archive);
651
+ archive_write_free(disk);
652
+ return false;
653
+ }
654
+ entry_index++;
655
+ }
656
+
657
+ #ifndef NDEBUG
658
+ __android_log_print(ANDROID_LOG_INFO, "SherpaOnnx",
659
+ "ExtractFromStream done entries=%d", entry_index);
660
+ #endif
661
+
662
+ archive_read_free(archive);
663
+ archive_write_free(disk);
664
+
665
+ if (out_sha256) {
666
+ unsigned char digest[32];
667
+ sha256_final(&stream_ctx.sha_ctx, digest);
668
+ *out_sha256 = ToHex(digest, sizeof(digest));
669
+ }
670
+
671
+ if (on_progress) {
672
+ on_progress(stream_ctx.bytes_read, stream_ctx.bytes_read, 100.0);
673
+ }
674
+
675
+ return true;
676
+ #endif
677
+ }
678
+
379
679
  bool ArchiveHelper::ComputeFileSha256(
380
680
  const std::string& file_path,
381
681
  std::string* out_error,
@@ -1,5 +1,6 @@
1
1
  #pragma once
2
2
 
3
+ #include <cstddef>
3
4
  #include <string>
4
5
  #include <functional>
5
6
  #include <atomic>
@@ -10,14 +11,17 @@
10
11
  */
11
12
  class ArchiveHelper {
12
13
  public:
14
+ /** Callback to read bytes from a stream (e.g. Java InputStream via JNI). Returns bytes read, 0 on EOF, -1 on error. */
15
+ using StreamReadCallback = std::ptrdiff_t (*)(void* buf, size_t len, void* user_data);
16
+
13
17
  /**
14
- * Extract tar.bz2 file to target directory
18
+ * Extract tar.bz2 (or .tar.zst, .tar.gz, .tar.xz) file to target directory.
15
19
  *
16
- * @param sourcePath Path to the .tar.bz2 file
20
+ * @param sourcePath Path to the archive file
17
21
  * @param targetPath Destination directory path
18
22
  * @param force Whether to overwrite existing target directory
19
23
  * @param onProgress Callback for progress updates (bytesExtracted, totalBytes, percent)
20
- * @param outSha256 Optional output SHA-256 hex of the archive file
24
+ * @param outSha256 Optional output SHA-256 hex of the archive file
21
25
  * @return true if extraction succeeded, false otherwise
22
26
  */
23
27
  static bool ExtractTarBz2(
@@ -28,7 +32,32 @@ class ArchiveHelper {
28
32
  std::string* out_error = nullptr,
29
33
  std::string* out_sha256 = nullptr);
30
34
 
31
- /**
35
+ /**
36
+ * Extract .tar.zst (or other supported tar compression) file to target directory.
37
+ * Uses the same implementation as ExtractTarBz2 (libarchive supports zstd when built with ENABLE_ZSTD).
38
+ */
39
+ static bool ExtractTarZst(
40
+ const std::string& source_path,
41
+ const std::string& target_path,
42
+ bool force,
43
+ std::function<void(long long, long long, double)> on_progress = nullptr,
44
+ std::string* out_error = nullptr,
45
+ std::string* out_sha256 = nullptr);
46
+
47
+ /**
48
+ * Extract a tar archive (tar.zst or tar.bz2) from a stream via read_cb.
49
+ * Used for Android AssetManager streams; total_bytes can be 0 (progress then uses compressed bytes or periodic emit).
50
+ */
51
+ static bool ExtractFromStream(
52
+ StreamReadCallback read_cb,
53
+ void* read_user_data,
54
+ const std::string& target_path,
55
+ bool force,
56
+ std::function<void(long long, long long, double)> on_progress = nullptr,
57
+ std::string* out_error = nullptr,
58
+ std::string* out_sha256 = nullptr);
59
+
60
+ /**
32
61
  * Compute SHA-256 of a file.
33
62
  *
34
63
  * @param file_path Path to the file