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