tugger_rust_toolchain/
tar.rs

1// This Source Code Form is subject to the terms of the Mozilla Public
2// License, v. 2.0. If a copy of the MPL was not distributed with this
3// file, You can obtain one at https://mozilla.org/MPL/2.0/.
4
5use {
6    anyhow::{anyhow, Context, Result},
7    sha2::Digest,
8    simple_file_manifest::{FileEntry, FileManifest},
9    std::{
10        io::{BufRead, Read, Write},
11        path::{Path, PathBuf},
12    },
13};
14
15#[derive(Clone, Copy, Debug)]
16pub enum CompressionFormat {
17    Gzip,
18    Xz,
19    Zstd,
20}
21
22fn get_decompression_stream(format: CompressionFormat, data: Vec<u8>) -> Result<Box<dyn Read>> {
23    let reader = std::io::Cursor::new(data);
24
25    match format {
26        CompressionFormat::Zstd => Ok(Box::new(zstd::stream::read::Decoder::new(reader)?)),
27        CompressionFormat::Xz => Ok(Box::new(xz2::read::XzDecoder::new(reader))),
28        CompressionFormat::Gzip => Ok(Box::new(flate2::read::GzDecoder::new(reader))),
29    }
30}
31
32/// Represents an extracted Rust package archive.
33///
34/// File contents exist in memory.
35pub struct PackageArchive {
36    manifest: FileManifest,
37    components: Vec<String>,
38}
39
40impl PackageArchive {
41    /// Construct a new instance with compressed tar data.
42    pub fn new(format: CompressionFormat, data: Vec<u8>) -> Result<Self> {
43        let mut archive = tar::Archive::new(
44            get_decompression_stream(format, data).context("obtaining decompression stream")?,
45        );
46
47        let mut manifest = FileManifest::default();
48
49        for entry in archive.entries().context("obtaining tar archive entries")? {
50            let mut entry = entry.context("resolving tar archive entry")?;
51
52            let path = entry.path().context("resolving entry path")?;
53
54            let first_component = path
55                .components()
56                .next()
57                .ok_or_else(|| anyhow!("unable to get first path component"))?;
58
59            let path = path
60                .strip_prefix(first_component)
61                .context("stripping path prefix")?
62                .to_path_buf();
63
64            let mut entry_data = Vec::new();
65            entry.read_to_end(&mut entry_data)?;
66
67            manifest.add_file_entry(
68                path,
69                FileEntry::new_from_data(entry_data, entry.header().mode()? & 0o111 != 0),
70            )?;
71        }
72
73        if manifest
74            .get("rust-installer-version")
75            .ok_or_else(|| anyhow!("archive does not contain rust-installer-version"))?
76            .resolve_content()?
77            != b"3\n"
78        {
79            return Err(anyhow!("rust-installer-version has unsupported version"));
80        }
81
82        let components = manifest
83            .get("components")
84            .ok_or_else(|| anyhow!("archive does not contain components file"))?
85            .resolve_content()?;
86        let components =
87            String::from_utf8(components).context("converting components file to string")?;
88        let components = components
89            .lines()
90            .map(|l| l.to_string())
91            .collect::<Vec<_>>();
92
93        Ok(Self {
94            manifest,
95            components,
96        })
97    }
98
99    /// Resolve file installs that need to be performed to materialize this package.
100    ///
101    /// Returned Vec has relative destination path and the FileManifest's internal entry
102    /// as members.
103    pub fn resolve_installs(&self) -> Result<Vec<(PathBuf, &FileEntry)>> {
104        let mut res = Vec::new();
105
106        for component in &self.components {
107            let component_path = PathBuf::from(component);
108            let manifest_path = component_path.join("manifest.in");
109
110            let manifest = self
111                .manifest
112                .get(&manifest_path)
113                .ok_or_else(|| anyhow!("{} not found", manifest_path.display()))?;
114
115            let (dirs, files) = Self::parse_manifest(manifest.resolve_content()?)?;
116
117            if !dirs.is_empty() {
118                return Err(anyhow!("support for copying directories not implemented"));
119            }
120
121            for file in files {
122                let manifest_path = component_path.join(&file);
123                let entry = self.manifest.get(&manifest_path).ok_or_else(|| {
124                    anyhow!(
125                        "could not locate file {} in manifest",
126                        manifest_path.display()
127                    )
128                })?;
129
130                res.push((PathBuf::from(file), entry));
131            }
132        }
133
134        Ok(res)
135    }
136
137    /// Write a file containing SHA-256 hashes of file installs to the specified writer.
138    pub fn write_installs_manifest(&self, fh: &mut impl Write) -> Result<()> {
139        for (path, entry) in self.resolve_installs().context("resolving installs")? {
140            let mut hasher = sha2::Sha256::new();
141            hasher.update(entry.resolve_content()?);
142
143            let line = format!(
144                "{}\t{}\n",
145                hex::encode(hasher.finalize().as_slice()),
146                path.display()
147            );
148
149            fh.write_all(line.as_bytes())?;
150        }
151
152        Ok(())
153    }
154
155    /// Materialize files from this manifest into the specified destination directory.
156    pub fn install(&self, dest_dir: &Path) -> Result<()> {
157        for (dest_path, entry) in self.resolve_installs().context("resolving installs")? {
158            let dest_path = dest_dir.join(dest_path);
159
160            entry
161                .write_to_path(&dest_path)
162                .with_context(|| format!("writing {}", dest_path.display(),))?;
163        }
164
165        Ok(())
166    }
167
168    fn parse_manifest(data: Vec<u8>) -> Result<(Vec<String>, Vec<String>)> {
169        let mut files = vec![];
170        let mut dirs = vec![];
171
172        let data = String::from_utf8(data)?;
173
174        for line in data.lines() {
175            if let Some(pos) = line.find(':') {
176                let action = &line[0..pos];
177                let path = &line[pos + 1..];
178
179                match action {
180                    "file" => {
181                        files.push(path.to_string());
182                    }
183                    "dir" => {
184                        dirs.push(path.to_string());
185                    }
186                    _ => return Err(anyhow!("unhandled action in manifest.in: {}", action)),
187                }
188            }
189        }
190
191        Ok((dirs, files))
192    }
193}
194
195/// Read an installs manifest from a given reader.
196///
197/// Returns a mapping of filesystem path to expected SHA-256 digest.
198pub fn read_installs_manifest(fh: &mut impl Read) -> Result<Vec<(PathBuf, String)>> {
199    let mut res = vec![];
200
201    let reader = std::io::BufReader::new(fh);
202
203    for line in reader.lines() {
204        let line = line?;
205
206        if line.is_empty() {
207            break;
208        }
209
210        let mut parts = line.splitn(2, '\t');
211
212        let digest = parts
213            .next()
214            .ok_or_else(|| anyhow!("could not read digest"))?;
215        let filename = parts
216            .next()
217            .ok_or_else(|| anyhow!("could not read filename"))?;
218
219        res.push((PathBuf::from(filename), digest.to_string()));
220    }
221
222    Ok(res)
223}