Skip to main content

idt/downloaders/http/
deflate.rs

1use std::fs::File;
2use std::io::BufReader;
3use std::io::Read;
4use std::path::Path;
5use std::path::PathBuf;
6
7use flate2::read::GzDecoder;
8use rootcause::prelude::ResultExt;
9use tar::Archive;
10use xz2::read::XzDecoder;
11
12pub enum HttpDeflateOption<'a> {
13    ExtractTarGz {
14        dest_dir: &'a Path,
15        // Option because not all the downloaded archives has a:
16        // - stable name (i.e. `shellcheck`)
17        // - a usable binary outside the archive (i.e. `elixir_ls` or `lua_ls`)
18        // In these cases `dest_name` is set to None
19        dest_name: Option<&'a str>,
20    },
21    ExtractTarXz {
22        dest_dir: &'a Path,
23        dest_name: Option<&'a str>,
24    },
25    ExtractZip {
26        dest_dir: &'a Path,
27        dest_name: Option<&'a str>,
28    },
29    WriteTo {
30        dest_path: &'a Path,
31    },
32}
33
34impl HttpDeflateOption<'_> {
35    /// Process the downloaded temporary file according to this deflate option.
36    ///
37    /// # Errors
38    /// Returns an error when filesystem, decompression, archive extraction, or zip extraction fails.
39    pub fn process(&self, tmp_file: &Path) -> rootcause::Result<PathBuf> {
40        match self {
41            Self::ExtractTarGz { dest_dir, dest_name } => {
42                let input = File::open(tmp_file)
43                    .context("error opening tmp file for tar.gz extraction")
44                    .attach_with(|| format!("path={}", tmp_file.display()))?;
45                let decoder = GzDecoder::new(input);
46                let archive = Archive::new(decoder);
47
48                Ok(extract_tar(archive, tmp_file, dest_dir, *dest_name)?)
49            }
50            Self::ExtractTarXz { dest_dir, dest_name } => {
51                let input = File::open(tmp_file)
52                    .context("error opening tmp file for tar.xz extraction")
53                    .attach_with(|| format!("path={}", tmp_file.display()))?;
54                let decoder = XzDecoder::new(input);
55                let archive = Archive::new(decoder);
56
57                Ok(extract_tar(archive, tmp_file, dest_dir, *dest_name)?)
58            }
59            Self::ExtractZip { dest_dir, dest_name } => {
60                let input = File::open(tmp_file)
61                    .context("error opening tmp file for zip extraction")
62                    .attach_with(|| format!("path={}", tmp_file.display()))?;
63                let reader = BufReader::new(input);
64                let mut archive = zip::ZipArchive::new(reader)
65                    .context("error reading zip archive")
66                    .attach_with(|| format!("path={}", tmp_file.display()))?;
67
68                if let Some(dest_name) = dest_name {
69                    let mut entry = archive
70                        .by_name(dest_name)
71                        .context("error finding entry in zip archive")
72                        .attach_with(|| format!("path={}", tmp_file.display()))
73                        .attach_with(|| format!("entry={dest_name}"))?;
74                    let dest_path = dest_dir.join(dest_name);
75                    let mut dest = File::create(&dest_path)
76                        .context("error creating dest file for zip entry")
77                        .attach_with(|| format!("path={}", dest_path.display()))?;
78                    std::io::copy(&mut entry, &mut dest)
79                        .context("error extracting zip entry")
80                        .attach_with(|| format!("path={}", tmp_file.display()))
81                        .attach_with(|| format!("entry={dest_name}"))?;
82
83                    Ok(dest_path)
84                } else {
85                    archive
86                        .extract(dest_dir)
87                        .context("error extracting zip archive")
88                        .attach_with(|| format!("path={}", tmp_file.display()))
89                        .attach_with(|| format!("dest_dir={}", dest_dir.display()))?;
90
91                    Ok(dest_dir.into())
92                }
93            }
94            Self::WriteTo { dest_path } => {
95                // Use copy instead of rename to handle cross-filesystem moves (e.g. /tmp -> target).
96                std::fs::copy(tmp_file, dest_path)
97                    .context("error copying tmp file to dest")
98                    .attach_with(|| format!("src={}", tmp_file.display()))
99                    .attach_with(|| format!("dest={}", dest_path.display()))?;
100
101                Ok(dest_path.into())
102            }
103        }
104    }
105}
106
107/// Source for verifying the checksum of a downloaded file.
108pub struct ChecksumSource<'a> {
109    /// URL to a checksums file (e.g., SHA256SUMS).
110    pub checksums_url: &'a str,
111    /// The filename to look up in the checksums file.
112    pub filename: &'a str,
113}
114
115/// Extracts a tar archive to `dest_dir`. When `dest_name` is `Some`, only the matching entry is
116/// extracted; otherwise the entire archive is unpacked.
117fn extract_tar<R: Read>(
118    mut archive: Archive<R>,
119    archive_path: &Path,
120    dest_dir: &Path,
121    dest_name: Option<&str>,
122) -> rootcause::Result<PathBuf> {
123    if let Some(dest_name) = dest_name {
124        for entry in archive
125            .entries()
126            .context("error reading tar entries")
127            .attach_with(|| format!("path={}", archive_path.display()))?
128        {
129            let mut entry = entry
130                .context("error reading tar entry")
131                .attach_with(|| format!("entry={dest_name}"))?;
132            let entry_path = entry
133                .path()
134                .context("error reading tar entry path")
135                .attach_with(|| format!("entry={dest_name}"))?;
136            if entry_path.to_str() == Some(dest_name) {
137                let dest_path = dest_dir.join(dest_name);
138                entry
139                    .unpack(&dest_path)
140                    .context("error extracting tar entry")
141                    .attach_with(|| format!("entry={dest_name}"))?;
142                return Ok(dest_path);
143            }
144        }
145        Err(rootcause::report!("entry not found in tar archive")).attach_with(|| format!("entry={dest_name}"))
146    } else {
147        archive
148            .unpack(dest_dir)
149            .context("error extracting tar archive")
150            .attach_with(|| format!("dest_dir={}", dest_dir.display()))?;
151        Ok(dest_dir.into())
152    }
153}