Skip to main content

idt/downloaders/http/
deflate.rs

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