electron-cli 0.3.0-alpha.6 → 0.3.0-alpha.8

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.
@@ -1,17 +1,19 @@
1
1
  use std::{
2
2
  fs,
3
3
  fs::File,
4
- io::{self, BufWriter},
4
+ io::{self, BufWriter, Write},
5
5
  path::{Path, PathBuf},
6
6
  };
7
7
 
8
8
  use anyhow::{bail, Context, Result};
9
9
  use camino::Utf8PathBuf;
10
+ use flate2::{write::GzEncoder, Compression};
10
11
  use serde::Serialize;
12
+ use tar::{Builder as TarBuilder, Header as TarHeader};
11
13
  use zip::{write::SimpleFileOptions, CompressionMethod, ZipWriter};
12
14
 
13
15
  use crate::{
14
- cli::{MakeArgs, PackageArgs},
16
+ cli::{MakeArgs, MakeTarget, PackageArgs},
15
17
  commands::package::{self, PackageReport},
16
18
  output,
17
19
  };
@@ -67,14 +69,15 @@ pub(crate) fn build_report(args: &MakeArgs) -> Result<MakeReport> {
67
69
  .join(args.target.as_str())
68
70
  .join(package.platform())
69
71
  .join(package.arch());
70
- let artifact = make_dir.join(format!(
71
- "{}-{}-{}.zip",
72
- package.artifact_stem(),
73
- package.platform(),
74
- package.arch()
75
- ));
72
+ let artifact = make_artifact_path(&make_dir, &package, args.target);
76
73
 
77
74
  let mut warnings = package.warnings().to_vec();
75
+ if args.target == MakeTarget::Deb && package.platform() != "linux" {
76
+ warnings.push(format!(
77
+ "Deb maker only supports linux packages; target platform is {}.",
78
+ package.platform()
79
+ ));
80
+ }
78
81
  if args.skip_package && !Path::new(package.bundle_dir().as_str()).exists() {
79
82
  warnings.push(format!(
80
83
  "Package output does not exist: {}.",
@@ -128,7 +131,12 @@ pub(crate) fn execute_make(report: &mut MakeReport, args: &MakeArgs) -> Result<(
128
131
 
129
132
  fs::create_dir_all(report.make_dir.as_str())
130
133
  .with_context(|| format!("Could not create {}", report.make_dir))?;
131
- write_zip_archive(Path::new(report.package.bundle_dir().as_str()), artifact)?;
134
+ match args.target {
135
+ MakeTarget::Zip => {
136
+ write_zip_archive(Path::new(report.package.bundle_dir().as_str()), artifact)?
137
+ }
138
+ MakeTarget::Deb => write_deb_archive(&report.package, artifact)?,
139
+ }
132
140
 
133
141
  Ok(())
134
142
  }
@@ -173,6 +181,23 @@ fn print_report(report: &MakeReport, json: bool) -> Result<()> {
173
181
  Ok(())
174
182
  }
175
183
 
184
+ fn make_artifact_path(make_dir: &Path, package: &PackageReport, target: MakeTarget) -> PathBuf {
185
+ match target {
186
+ MakeTarget::Zip => make_dir.join(format!(
187
+ "{}-{}-{}.zip",
188
+ package.artifact_stem(),
189
+ package.platform(),
190
+ package.arch()
191
+ )),
192
+ MakeTarget::Deb => make_dir.join(format!(
193
+ "{}_{}_{}.deb",
194
+ debian_package_name(&package.artifact_stem()),
195
+ debian_version(package.project().version.as_deref()),
196
+ debian_arch(package.arch())
197
+ )),
198
+ }
199
+ }
200
+
176
201
  fn write_zip_archive(source: &Path, artifact: &Path) -> Result<()> {
177
202
  if !source.exists() {
178
203
  bail!("Package output does not exist: {}", source.display());
@@ -263,6 +288,386 @@ fn directory_options(metadata: &fs::Metadata) -> SimpleFileOptions {
263
288
  .unix_permissions(unix_mode(metadata, 0o755))
264
289
  }
265
290
 
291
+ fn write_deb_archive(package: &PackageReport, artifact: &Path) -> Result<()> {
292
+ if package.platform() != "linux" {
293
+ bail!(
294
+ "Deb maker only supports linux packages. Requested {}.",
295
+ package.platform()
296
+ );
297
+ }
298
+
299
+ let source = Path::new(package.bundle_dir().as_str());
300
+ if !source.exists() {
301
+ bail!("Package output does not exist: {}", source.display());
302
+ }
303
+
304
+ let parent = artifact
305
+ .parent()
306
+ .with_context(|| format!("Artifact path has no parent: {}", artifact.display()))?;
307
+ fs::create_dir_all(parent).with_context(|| format!("Could not create {}", parent.display()))?;
308
+
309
+ let deb_package = debian_package_name(&package.artifact_stem());
310
+ let version = debian_version(package.project().version.as_deref());
311
+ let arch = debian_arch(package.arch());
312
+ let installed_size = directory_size(source)?.div_ceil(1024).max(1);
313
+ let control = debian_control_file(package, &deb_package, &version, &arch, installed_size);
314
+ let control_tar =
315
+ gzip_tar(|builder| append_bytes_to_tar(builder, "./control", control.as_bytes(), 0o644))?;
316
+ let data_tar = gzip_tar(|builder| append_deb_data_tar(builder, package, source, &deb_package))?;
317
+
318
+ write_ar_archive(
319
+ artifact,
320
+ &[
321
+ ArMember {
322
+ name: "debian-binary",
323
+ mode: 0o100644,
324
+ data: b"2.0\n".to_vec(),
325
+ },
326
+ ArMember {
327
+ name: "control.tar.gz",
328
+ mode: 0o100644,
329
+ data: control_tar,
330
+ },
331
+ ArMember {
332
+ name: "data.tar.gz",
333
+ mode: 0o100644,
334
+ data: data_tar,
335
+ },
336
+ ],
337
+ )
338
+ }
339
+
340
+ fn debian_control_file(
341
+ package: &PackageReport,
342
+ deb_package: &str,
343
+ version: &str,
344
+ arch: &str,
345
+ installed_size: u64,
346
+ ) -> String {
347
+ format!(
348
+ "Package: {deb_package}\n\
349
+ Version: {version}\n\
350
+ Section: utils\n\
351
+ Priority: optional\n\
352
+ Architecture: {arch}\n\
353
+ Maintainer: electron-cli <noreply@example.invalid>\n\
354
+ Installed-Size: {installed_size}\n\
355
+ Description: {description}\n\
356
+ Electron application packaged by electron-cli.\n",
357
+ description = single_line(package.app_name())
358
+ )
359
+ }
360
+
361
+ fn append_deb_data_tar(
362
+ builder: &mut TarBuilder<GzEncoder<Vec<u8>>>,
363
+ package: &PackageReport,
364
+ source: &Path,
365
+ deb_package: &str,
366
+ ) -> Result<()> {
367
+ for directory in [
368
+ "./",
369
+ "./opt",
370
+ "./usr",
371
+ "./usr/bin",
372
+ "./usr/share",
373
+ "./usr/share/applications",
374
+ ] {
375
+ append_directory_to_tar(builder, directory, 0o755)?;
376
+ }
377
+
378
+ let app_root = format!("./opt/{deb_package}");
379
+ append_directory_to_tar(builder, &app_root, 0o755)?;
380
+ append_directory_contents_to_tar(builder, source, Path::new(&app_root))?;
381
+
382
+ let executable = format!("/opt/{deb_package}/{}", package.executable_name());
383
+ append_symlink_to_tar(
384
+ builder,
385
+ format!("./usr/bin/{deb_package}"),
386
+ &executable,
387
+ 0o777,
388
+ )?;
389
+ append_bytes_to_tar(
390
+ builder,
391
+ format!("./usr/share/applications/{deb_package}.desktop"),
392
+ debian_desktop_file(package, deb_package, &executable).as_bytes(),
393
+ 0o644,
394
+ )?;
395
+
396
+ Ok(())
397
+ }
398
+
399
+ fn debian_desktop_file(package: &PackageReport, deb_package: &str, executable: &str) -> String {
400
+ format!(
401
+ "[Desktop Entry]\n\
402
+ Name={name}\n\
403
+ Exec={executable} %U\n\
404
+ Terminal=false\n\
405
+ Type=Application\n\
406
+ StartupWMClass={wm_class}\n\
407
+ Categories=Utility;\n",
408
+ name = single_line(package.app_name()),
409
+ wm_class = deb_package
410
+ )
411
+ }
412
+
413
+ fn gzip_tar(
414
+ write_contents: impl FnOnce(&mut TarBuilder<GzEncoder<Vec<u8>>>) -> Result<()>,
415
+ ) -> Result<Vec<u8>> {
416
+ let encoder = GzEncoder::new(Vec::new(), Compression::default());
417
+ let mut builder = TarBuilder::new(encoder);
418
+ builder.mode(tar::HeaderMode::Deterministic);
419
+ write_contents(&mut builder)?;
420
+ builder.finish().context("Could not finish tar archive")?;
421
+ let encoder = builder
422
+ .into_inner()
423
+ .context("Could not retrieve gzip encoder")?;
424
+ encoder.finish().context("Could not finish gzip archive")
425
+ }
426
+
427
+ fn append_directory_contents_to_tar(
428
+ builder: &mut TarBuilder<GzEncoder<Vec<u8>>>,
429
+ source: &Path,
430
+ destination: &Path,
431
+ ) -> Result<()> {
432
+ let mut entries = fs::read_dir(source)
433
+ .with_context(|| format!("Could not read {}", source.display()))?
434
+ .collect::<Result<Vec<_>, io::Error>>()?;
435
+ entries.sort_by_key(|entry| entry.path());
436
+
437
+ for entry in entries {
438
+ let source_path = entry.path();
439
+ let destination_path = destination.join(entry.file_name());
440
+ append_path_to_tar(builder, &source_path, &destination_path)?;
441
+ }
442
+
443
+ Ok(())
444
+ }
445
+
446
+ fn append_path_to_tar(
447
+ builder: &mut TarBuilder<GzEncoder<Vec<u8>>>,
448
+ source: &Path,
449
+ destination: &Path,
450
+ ) -> Result<()> {
451
+ let metadata = fs::symlink_metadata(source)
452
+ .with_context(|| format!("Could not stat {}", source.display()))?;
453
+
454
+ if metadata.is_dir() {
455
+ append_directory_to_tar(builder, destination, unix_mode(&metadata, 0o755))?;
456
+ append_directory_contents_to_tar(builder, source, destination)?;
457
+ } else if metadata.file_type().is_symlink() {
458
+ let target = fs::read_link(source)
459
+ .with_context(|| format!("Could not read link {}", source.display()))?;
460
+ append_symlink_to_tar(builder, destination, &target, 0o777)?;
461
+ } else if metadata.is_file() {
462
+ append_file_to_tar(builder, source, destination, &metadata)?;
463
+ }
464
+
465
+ Ok(())
466
+ }
467
+
468
+ fn append_directory_to_tar(
469
+ builder: &mut TarBuilder<GzEncoder<Vec<u8>>>,
470
+ path: impl AsRef<Path>,
471
+ mode: u32,
472
+ ) -> Result<()> {
473
+ let mut header = TarHeader::new_gnu();
474
+ header.set_entry_type(tar::EntryType::Directory);
475
+ header.set_size(0);
476
+ header.set_mode(mode);
477
+ header.set_mtime(0);
478
+ header.set_cksum();
479
+ builder
480
+ .append_data(&mut header, path.as_ref(), io::empty())
481
+ .with_context(|| format!("Could not add {} to data tar", path.as_ref().display()))
482
+ }
483
+
484
+ fn append_file_to_tar(
485
+ builder: &mut TarBuilder<GzEncoder<Vec<u8>>>,
486
+ source: &Path,
487
+ destination: &Path,
488
+ metadata: &fs::Metadata,
489
+ ) -> Result<()> {
490
+ let mut header = TarHeader::new_gnu();
491
+ header.set_entry_type(tar::EntryType::Regular);
492
+ header.set_size(metadata.len());
493
+ header.set_mode(unix_mode(metadata, 0o644));
494
+ header.set_mtime(0);
495
+ header.set_cksum();
496
+ let mut file =
497
+ File::open(source).with_context(|| format!("Could not open {}", source.display()))?;
498
+ builder
499
+ .append_data(&mut header, destination, &mut file)
500
+ .with_context(|| format!("Could not add {} to data tar", source.display()))
501
+ }
502
+
503
+ fn append_bytes_to_tar(
504
+ builder: &mut TarBuilder<GzEncoder<Vec<u8>>>,
505
+ path: impl AsRef<Path>,
506
+ contents: &[u8],
507
+ mode: u32,
508
+ ) -> Result<()> {
509
+ let mut header = TarHeader::new_gnu();
510
+ header.set_entry_type(tar::EntryType::Regular);
511
+ header.set_size(contents.len() as u64);
512
+ header.set_mode(mode);
513
+ header.set_mtime(0);
514
+ header.set_cksum();
515
+ builder
516
+ .append_data(&mut header, path.as_ref(), contents)
517
+ .with_context(|| format!("Could not add {} to tar", path.as_ref().display()))
518
+ }
519
+
520
+ fn append_symlink_to_tar(
521
+ builder: &mut TarBuilder<GzEncoder<Vec<u8>>>,
522
+ path: impl AsRef<Path>,
523
+ target: impl AsRef<Path>,
524
+ mode: u32,
525
+ ) -> Result<()> {
526
+ let mut header = TarHeader::new_gnu();
527
+ header.set_entry_type(tar::EntryType::Symlink);
528
+ header.set_size(0);
529
+ header.set_mode(mode);
530
+ header.set_mtime(0);
531
+ header
532
+ .set_link_name(target.as_ref())
533
+ .with_context(|| format!("Could not set link target for {}", path.as_ref().display()))?;
534
+ header.set_cksum();
535
+ builder
536
+ .append_data(&mut header, path.as_ref(), io::empty())
537
+ .with_context(|| format!("Could not add {} to tar", path.as_ref().display()))
538
+ }
539
+
540
+ struct ArMember {
541
+ name: &'static str,
542
+ mode: u32,
543
+ data: Vec<u8>,
544
+ }
545
+
546
+ fn write_ar_archive(artifact: &Path, members: &[ArMember]) -> Result<()> {
547
+ let mut file = BufWriter::new(
548
+ File::create(artifact)
549
+ .with_context(|| format!("Could not create {}", artifact.display()))?,
550
+ );
551
+ file.write_all(b"!<arch>\n")
552
+ .with_context(|| format!("Could not write {}", artifact.display()))?;
553
+
554
+ for member in members {
555
+ write_ar_member(&mut file, member)
556
+ .with_context(|| format!("Could not add {} to {}", member.name, artifact.display()))?;
557
+ }
558
+
559
+ file.flush()
560
+ .with_context(|| format!("Could not finish {}", artifact.display()))
561
+ }
562
+
563
+ fn write_ar_member(writer: &mut impl Write, member: &ArMember) -> Result<()> {
564
+ let name = format!("{}/", member.name);
565
+ if name.len() > 16 {
566
+ bail!("ar member name is too long: {}", member.name);
567
+ }
568
+
569
+ let header = format!(
570
+ "{name:<16}{mtime:<12}{uid:<6}{gid:<6}{mode:<8o}{size:<10}`\n",
571
+ mtime = 0,
572
+ uid = 0,
573
+ gid = 0,
574
+ mode = member.mode,
575
+ size = member.data.len()
576
+ );
577
+ debug_assert_eq!(header.len(), 60);
578
+ writer.write_all(header.as_bytes())?;
579
+ writer.write_all(&member.data)?;
580
+ if member.data.len() % 2 == 1 {
581
+ writer.write_all(b"\n")?;
582
+ }
583
+
584
+ Ok(())
585
+ }
586
+
587
+ fn directory_size(path: &Path) -> Result<u64> {
588
+ let metadata =
589
+ fs::symlink_metadata(path).with_context(|| format!("Could not stat {}", path.display()))?;
590
+ if metadata.is_file() {
591
+ return Ok(metadata.len());
592
+ }
593
+ if !metadata.is_dir() {
594
+ return Ok(0);
595
+ }
596
+
597
+ let mut size = 0;
598
+ for entry in fs::read_dir(path).with_context(|| format!("Could not read {}", path.display()))? {
599
+ let entry = entry?;
600
+ size += directory_size(&entry.path())?;
601
+ }
602
+ Ok(size)
603
+ }
604
+
605
+ fn debian_package_name(name: &str) -> String {
606
+ let mut package = name
607
+ .to_ascii_lowercase()
608
+ .chars()
609
+ .map(|char| {
610
+ if char.is_ascii_alphanumeric() || matches!(char, '+' | '-' | '.') {
611
+ char
612
+ } else {
613
+ '-'
614
+ }
615
+ })
616
+ .collect::<String>()
617
+ .trim_matches(['+', '-', '.'])
618
+ .to_string();
619
+
620
+ if package.len() < 2 {
621
+ package.push_str("app");
622
+ }
623
+
624
+ package
625
+ }
626
+
627
+ fn debian_version(version: Option<&str>) -> String {
628
+ let version = version.unwrap_or("0.1.0");
629
+ let sanitized = version
630
+ .chars()
631
+ .map(|char| {
632
+ if char.is_ascii_alphanumeric() || matches!(char, '.' | '+' | '-' | ':' | '~') {
633
+ char
634
+ } else {
635
+ '~'
636
+ }
637
+ })
638
+ .collect::<String>()
639
+ .trim_matches(['-', '~'])
640
+ .to_string();
641
+
642
+ if sanitized.is_empty() {
643
+ "0.1.0".to_string()
644
+ } else {
645
+ sanitized
646
+ }
647
+ }
648
+
649
+ fn debian_arch(arch: &str) -> String {
650
+ match arch {
651
+ "x64" => "amd64".to_string(),
652
+ "ia32" => "i386".to_string(),
653
+ "armv7l" => "armhf".to_string(),
654
+ arch => arch.to_string(),
655
+ }
656
+ }
657
+
658
+ fn single_line(value: &str) -> String {
659
+ value
660
+ .chars()
661
+ .map(|char| {
662
+ if char == '\n' || char == '\r' {
663
+ ' '
664
+ } else {
665
+ char
666
+ }
667
+ })
668
+ .collect()
669
+ }
670
+
266
671
  #[cfg(unix)]
267
672
  fn unix_mode(metadata: &fs::Metadata, _fallback: u32) -> u32 {
268
673
  use std::os::unix::fs::PermissionsExt;
@@ -324,6 +729,7 @@ impl MakeReport {
324
729
  #[cfg(test)]
325
730
  mod tests {
326
731
  use super::*;
732
+ use std::io::Read;
327
733
  use zip::ZipArchive;
328
734
 
329
735
  #[test]
@@ -363,6 +769,40 @@ mod tests {
363
769
  let _ = fs::remove_dir_all(root);
364
770
  }
365
771
 
772
+ #[test]
773
+ fn builds_make_report_for_deb_target() {
774
+ let root = unique_temp_dir("deb-plan");
775
+ write_package_json(&root);
776
+ write_app_file(&root);
777
+ write_fake_electron_dist(&root);
778
+
779
+ let args = MakeArgs {
780
+ cwd: root.clone(),
781
+ out_dir: PathBuf::from("out"),
782
+ name: None,
783
+ platform: Some("linux".to_string()),
784
+ arch: Some("x64".to_string()),
785
+ target: crate::cli::MakeTarget::Deb,
786
+ skip_package: false,
787
+ force: false,
788
+ dry_run: true,
789
+ json: true,
790
+ };
791
+ let report = build_report(&args).expect("report should build");
792
+
793
+ assert_eq!(report.target, "deb");
794
+ assert!(Path::new(report.artifact.as_str()).ends_with(
795
+ PathBuf::from("out")
796
+ .join("make")
797
+ .join("deb")
798
+ .join("linux")
799
+ .join("x64")
800
+ .join("starter-app_0.1.0_amd64.deb")
801
+ ));
802
+
803
+ let _ = fs::remove_dir_all(root);
804
+ }
805
+
366
806
  #[test]
367
807
  fn makes_zip_artifact_after_packaging() {
368
808
  let root = unique_temp_dir("execute");
@@ -405,6 +845,97 @@ mod tests {
405
845
  let _ = fs::remove_dir_all(root);
406
846
  }
407
847
 
848
+ #[test]
849
+ fn writes_deb_archive_with_control_and_data_members() {
850
+ let root = unique_temp_dir("deb-archive");
851
+ write_package_json(&root);
852
+ write_app_file(&root);
853
+ write_fake_electron_dist(&root);
854
+
855
+ let args = MakeArgs {
856
+ cwd: root.clone(),
857
+ out_dir: PathBuf::from("out"),
858
+ name: None,
859
+ platform: Some("linux".to_string()),
860
+ arch: Some("x64".to_string()),
861
+ target: crate::cli::MakeTarget::Deb,
862
+ skip_package: false,
863
+ force: false,
864
+ dry_run: true,
865
+ json: true,
866
+ };
867
+ let report = build_report(&args).expect("report should build");
868
+ let bundle_dir = Path::new(report.package.bundle_dir().as_str());
869
+ fs::create_dir_all(bundle_dir.join("resources/app"))
870
+ .expect("fake bundle resources should be created");
871
+ fs::write(bundle_dir.join("starter-app"), "").expect("fake binary should be written");
872
+ fs::write(bundle_dir.join("resources/app/package.json"), "{}")
873
+ .expect("fake app package should be written");
874
+
875
+ write_deb_archive(&report.package, Path::new(report.artifact.as_str()))
876
+ .expect("deb should be written");
877
+
878
+ let members = read_ar_members(Path::new(report.artifact.as_str()));
879
+ assert_eq!(
880
+ members.get("debian-binary").map(Vec::as_slice),
881
+ Some(&b"2.0\n"[..])
882
+ );
883
+
884
+ let control = read_tar_file(
885
+ members
886
+ .get("control.tar.gz")
887
+ .expect("control tar should exist"),
888
+ "control",
889
+ );
890
+ assert!(control.contains("Package: starter-app"));
891
+ assert!(control.contains("Architecture: amd64"));
892
+
893
+ let data = members.get("data.tar.gz").expect("data tar should exist");
894
+ assert!(tar_contains(
895
+ data,
896
+ "opt/starter-app/resources/app/package.json"
897
+ ));
898
+ assert!(tar_contains(
899
+ data,
900
+ "usr/share/applications/starter-app.desktop"
901
+ ));
902
+ assert!(tar_contains(data, "usr/bin/starter-app"));
903
+
904
+ let _ = fs::remove_dir_all(root);
905
+ }
906
+
907
+ #[test]
908
+ fn makes_deb_artifact_after_packaging_on_linux() {
909
+ if !cfg!(target_os = "linux") {
910
+ return;
911
+ }
912
+
913
+ let root = unique_temp_dir("deb-execute");
914
+ write_package_json(&root);
915
+ write_app_file(&root);
916
+ write_fake_electron_dist(&root);
917
+
918
+ let args = MakeArgs {
919
+ cwd: root.clone(),
920
+ out_dir: PathBuf::from("out"),
921
+ name: None,
922
+ platform: None,
923
+ arch: None,
924
+ target: crate::cli::MakeTarget::Deb,
925
+ skip_package: false,
926
+ force: false,
927
+ dry_run: false,
928
+ json: false,
929
+ };
930
+ let mut report = build_report(&args).expect("report should build");
931
+
932
+ execute_make(&mut report, &args).expect("make should succeed");
933
+
934
+ assert!(Path::new(report.artifact.as_str()).exists());
935
+
936
+ let _ = fs::remove_dir_all(root);
937
+ }
938
+
408
939
  fn write_package_json(root: &Path) {
409
940
  fs::write(
410
941
  root.join("package.json"),
@@ -446,4 +977,71 @@ mod tests {
446
977
  fs::create_dir_all(&path).expect("temp dir should be created");
447
978
  path
448
979
  }
980
+
981
+ fn read_ar_members(path: &Path) -> std::collections::BTreeMap<String, Vec<u8>> {
982
+ let bytes = fs::read(path).expect("ar archive should be readable");
983
+ assert_eq!(&bytes[..8], b"!<arch>\n");
984
+
985
+ let mut members = std::collections::BTreeMap::new();
986
+ let mut offset = 8;
987
+ while offset < bytes.len() {
988
+ let header = &bytes[offset..offset + 60];
989
+ let name = std::str::from_utf8(&header[0..16])
990
+ .expect("member name should be utf-8")
991
+ .trim()
992
+ .trim_end_matches('/')
993
+ .to_string();
994
+ let size = std::str::from_utf8(&header[48..58])
995
+ .expect("member size should be utf-8")
996
+ .trim()
997
+ .parse::<usize>()
998
+ .expect("member size should parse");
999
+ let data_start = offset + 60;
1000
+ let data_end = data_start + size;
1001
+ members.insert(name, bytes[data_start..data_end].to_vec());
1002
+ offset = data_end + (size % 2);
1003
+ }
1004
+
1005
+ members
1006
+ }
1007
+
1008
+ fn read_tar_file(archive: &[u8], path: &str) -> String {
1009
+ let decoder = flate2::read::GzDecoder::new(archive);
1010
+ let mut archive = tar::Archive::new(decoder);
1011
+ for entry in archive.entries().expect("tar entries should read") {
1012
+ let mut entry = entry.expect("tar entry should read");
1013
+ let entry_path = entry
1014
+ .path()
1015
+ .expect("tar path should read")
1016
+ .to_string_lossy()
1017
+ .trim_start_matches("./")
1018
+ .to_string();
1019
+ if entry_path == path {
1020
+ let mut contents = String::new();
1021
+ entry
1022
+ .read_to_string(&mut contents)
1023
+ .expect("tar file should read");
1024
+ return contents;
1025
+ }
1026
+ }
1027
+
1028
+ panic!("tar file was not found: {path}");
1029
+ }
1030
+
1031
+ fn tar_contains(archive: &[u8], path: &str) -> bool {
1032
+ let decoder = flate2::read::GzDecoder::new(archive);
1033
+ let mut archive = tar::Archive::new(decoder);
1034
+ archive
1035
+ .entries()
1036
+ .expect("tar entries should read")
1037
+ .any(|entry| {
1038
+ entry
1039
+ .expect("tar entry should read")
1040
+ .path()
1041
+ .expect("tar path should read")
1042
+ .to_string_lossy()
1043
+ .trim_start_matches("./")
1044
+ == path
1045
+ })
1046
+ }
449
1047
  }