1use crate::{Ctx, Error, manifest, util::Sha256};
2use anyhow::Context as _;
3use camino::Utf8PathBuf as PathBuf;
4use std::sync::Arc;
5
6#[derive(Debug)]
7struct Cab {
8 filename: PathBuf,
9 sha256: Sha256,
10 url: String,
11 #[allow(dead_code)]
12 size: u64,
13}
14
15pub(crate) struct CabContents {
16 pub(crate) path: PathBuf,
17 pub(crate) content: bytes::Bytes,
18 pub(crate) sequence: u32,
19}
20
21pub(crate) enum PayloadContents {
22 Vsix(bytes::Bytes),
23 Msi {
24 msi: bytes::Bytes,
25 cabs: Vec<CabContents>,
26 },
27}
28
29pub(crate) fn download(
30 ctx: Arc<Ctx>,
31 pkgs: Arc<std::collections::BTreeMap<String, manifest::ManifestItem>>,
32 item: &crate::WorkItem,
33) -> Result<Option<PayloadContents>, Error> {
34 item.progress.set_message("📥 downloading..");
35
36 let contents = ctx.get_and_validate(
37 &item.payload.url,
38 &item.payload.filename,
39 Some(item.payload.sha256.clone()),
40 item.progress.clone(),
41 )?;
42
43 let pc = match item.payload.filename.extension() {
44 Some("msi") => {
45 let cabs: Vec<_> = match pkgs.values().find(|mi| {
46 mi.payloads
47 .iter()
48 .any(|mi_payload| mi_payload.sha256 == item.payload.sha256)
49 }) {
50 Some(mi) => mi
51 .payloads
52 .iter()
53 .filter(|pay| pay.file_name.ends_with(".cab"))
54 .map(|pay| Cab {
55 filename: pay
56 .file_name
57 .strip_prefix("Installers\\")
58 .unwrap_or(&pay.file_name)
59 .into(),
60 sha256: pay.sha256.clone(),
61 url: pay.url.clone(),
62 size: pay.size,
63 })
64 .collect(),
65 None => anyhow::bail!(
66 "unable to find manifest parent for {}",
67 item.payload.filename
68 ),
69 };
70
71 download_cabs(ctx, &cabs, item, contents)
72 }
73 Some("vsix") => Ok(Some(PayloadContents::Vsix(contents))),
74 ext => anyhow::bail!("unknown extension {ext:?}"),
75 };
76
77 item.progress.finish_with_message("downloaded");
78
79 pc
80}
81
82fn download_cabs(
85 ctx: Arc<Ctx>,
86 cabs: &[Cab],
87 msi: &crate::WorkItem,
88 msi_content: bytes::Bytes,
89) -> Result<Option<PayloadContents>, Error> {
90 use rayon::prelude::*;
91
92 let msi_filename = &msi.payload.filename;
93
94 let mut msi_pkg = msi::Package::open(std::io::Cursor::new(msi_content.clone()))
95 .with_context(|| format!("invalid MSI for {msi_filename}"))?;
96
97 let cab_files: Vec<_> = msi_pkg
101 .select_rows(msi::Select::table("Media"))
102 .with_context(|| format!("{msi_filename} does not contain a list of CAB files"))?
103 .filter_map(|row| {
104 if row.len() >= 3 {
111 row[3]
114 .as_str()
115 .and_then(|s| row[1].as_int().map(|seq| (s, seq as u32)))
116 .and_then(|(name, seq)| {
117 let cab_name = name.trim_matches('"');
118
119 cabs.iter().find_map(|payload| {
120 (payload.filename == cab_name).then(|| {
121 (
122 PathBuf::from(format!(
123 "{}/{cab_name}",
124 msi_filename.file_stem().unwrap(),
125 )),
126 payload.sha256.clone(),
127 payload.url.clone(),
128 seq,
129 )
130 })
131 })
132 })
133 } else {
134 None
135 }
136 })
137 .collect();
138
139 if cab_files.is_empty() {
140 return Ok(None);
141 }
142
143 let cabs = cab_files
144 .into_par_iter()
145 .map(
146 |(cab_name, chksum, url, sequence)| -> Result<CabContents, Error> {
147 let cab_contents =
148 ctx.get_and_validate(url, &cab_name, Some(chksum), msi.progress.clone())?;
149 Ok(CabContents {
150 path: cab_name,
151 content: cab_contents,
152 sequence,
153 })
154 },
155 )
156 .collect::<Result<Vec<_>, _>>()?;
157
158 Ok(Some(PayloadContents::Msi {
159 msi: msi_content,
160 cabs,
161 }))
162}