Skip to main content

xwin/
download.rs

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
82/// Each SDK MSI has 1 or more cab files associated with it containing the actual
83/// data we need that must be downloaded separately and indexed from the MSI
84fn 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    // The `Media` table contains the list of cabs by name, which we then need
98    // to lookup in the list of payloads.
99    // Columns: [DiskId, LastSequence, DiskPrompt, Cabinet, VolumeLabel, Source]
100    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            // Columns:
105            // 0 - DiskId
106            // 1 - LastSequence
107            // 2 - DiskPrompt
108            // 3 - Cabinet name
109            // ...
110            if row.len() >= 3 {
111                // For some reason most/all of the msi files contain a NULL cabinet
112                // in the first position which is useless
113                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}