1 use anyhow::Error;
2 use flate2::read::GzDecoder;
3 use std::collections::HashMap;
4 use std::fs::File;
5 use std::io::Read;
6 use std::path::{Path, PathBuf};
7 use tar::Archive;
8 
9 const DEFAULT_TARGET: &str = "x86_64-unknown-linux-gnu";
10 
11 #[derive(Debug, Hash, Eq, PartialEq, Clone)]
12 pub(crate) enum PkgType {
13     Rust,
14     RustSrc,
15     Rustc,
16     Cargo,
17     Rls,
18     RustAnalyzer,
19     Clippy,
20     Rustfmt,
21     LlvmTools,
22     Miri,
23     Other(String),
24 }
25 
26 impl PkgType {
from_component(component: &str) -> Self27     pub(crate) fn from_component(component: &str) -> Self {
28         match component {
29             "rust" => PkgType::Rust,
30             "rust-src" => PkgType::RustSrc,
31             "rustc" => PkgType::Rustc,
32             "cargo" => PkgType::Cargo,
33             "rls" | "rls-preview" => PkgType::Rls,
34             "rust-analyzer" | "rust-analyzer-preview" => PkgType::RustAnalyzer,
35             "clippy" | "clippy-preview" => PkgType::Clippy,
36             "rustfmt" | "rustfmt-preview" => PkgType::Rustfmt,
37             "llvm-tools" | "llvm-tools-preview" => PkgType::LlvmTools,
38             "miri" | "miri-preview" => PkgType::Miri,
39             other => PkgType::Other(other.into()),
40         }
41     }
42 
43     /// First part of the tarball name.
tarball_component_name(&self) -> &str44     fn tarball_component_name(&self) -> &str {
45         match self {
46             PkgType::Rust => "rust",
47             PkgType::RustSrc => "rust-src",
48             PkgType::Rustc => "rustc",
49             PkgType::Cargo => "cargo",
50             PkgType::Rls => "rls",
51             PkgType::RustAnalyzer => "rust-analyzer",
52             PkgType::Clippy => "clippy",
53             PkgType::Rustfmt => "rustfmt",
54             PkgType::LlvmTools => "llvm-tools",
55             PkgType::Miri => "miri",
56             PkgType::Other(component) => component,
57         }
58     }
59 
60     /// Whether this package has the same version as Rust itself, or has its own `version` and
61     /// `git-commit-hash` files inside the tarball.
should_use_rust_version(&self) -> bool62     fn should_use_rust_version(&self) -> bool {
63         match self {
64             PkgType::Cargo => false,
65             PkgType::Rls => false,
66             PkgType::RustAnalyzer => false,
67             PkgType::Clippy => false,
68             PkgType::Rustfmt => false,
69             PkgType::LlvmTools => false,
70             PkgType::Miri => false,
71 
72             PkgType::Rust => true,
73             PkgType::RustSrc => true,
74             PkgType::Rustc => true,
75             PkgType::Other(_) => true,
76         }
77     }
78 
79     /// Whether this package is target-independent or not.
target_independent(&self) -> bool80     fn target_independent(&self) -> bool {
81         *self == PkgType::RustSrc
82     }
83 }
84 
85 #[derive(Debug, Default, Clone)]
86 pub(crate) struct VersionInfo {
87     pub(crate) version: Option<String>,
88     pub(crate) git_commit: Option<String>,
89     pub(crate) present: bool,
90 }
91 
92 pub(crate) struct Versions {
93     channel: String,
94     dist_path: PathBuf,
95     versions: HashMap<PkgType, VersionInfo>,
96 }
97 
98 impl Versions {
new(channel: &str, dist_path: &Path) -> Result<Self, Error>99     pub(crate) fn new(channel: &str, dist_path: &Path) -> Result<Self, Error> {
100         Ok(Self { channel: channel.into(), dist_path: dist_path.into(), versions: HashMap::new() })
101     }
102 
channel(&self) -> &str103     pub(crate) fn channel(&self) -> &str {
104         &self.channel
105     }
106 
version(&mut self, mut package: &PkgType) -> Result<VersionInfo, Error>107     pub(crate) fn version(&mut self, mut package: &PkgType) -> Result<VersionInfo, Error> {
108         if package.should_use_rust_version() {
109             package = &PkgType::Rust;
110         }
111 
112         match self.versions.get(package) {
113             Some(version) => Ok(version.clone()),
114             None => {
115                 let version_info = self.load_version_from_tarball(package)?;
116                 self.versions.insert(package.clone(), version_info.clone());
117                 Ok(version_info)
118             }
119         }
120     }
121 
load_version_from_tarball(&mut self, package: &PkgType) -> Result<VersionInfo, Error>122     fn load_version_from_tarball(&mut self, package: &PkgType) -> Result<VersionInfo, Error> {
123         let tarball_name = self.tarball_name(package, DEFAULT_TARGET)?;
124         let tarball = self.dist_path.join(tarball_name);
125 
126         let file = match File::open(&tarball) {
127             Ok(file) => file,
128             Err(err) if err.kind() == std::io::ErrorKind::NotFound => {
129                 // Missing tarballs do not return an error, but return empty data.
130                 return Ok(VersionInfo::default());
131             }
132             Err(err) => return Err(err.into()),
133         };
134         let mut tar = Archive::new(GzDecoder::new(file));
135 
136         let mut version = None;
137         let mut git_commit = None;
138         for entry in tar.entries()? {
139             let mut entry = entry?;
140 
141             let dest;
142             match entry.path()?.components().nth(1).and_then(|c| c.as_os_str().to_str()) {
143                 Some("version") => dest = &mut version,
144                 Some("git-commit-hash") => dest = &mut git_commit,
145                 _ => continue,
146             }
147             let mut buf = String::new();
148             entry.read_to_string(&mut buf)?;
149             *dest = Some(buf);
150 
151             // Short circuit to avoid reading the whole tar file if not necessary.
152             if version.is_some() && git_commit.is_some() {
153                 break;
154             }
155         }
156 
157         Ok(VersionInfo { version, git_commit, present: true })
158     }
159 
disable_version(&mut self, package: &PkgType)160     pub(crate) fn disable_version(&mut self, package: &PkgType) {
161         match self.versions.get_mut(package) {
162             Some(version) => {
163                 *version = VersionInfo::default();
164             }
165             None => {
166                 self.versions.insert(package.clone(), VersionInfo::default());
167             }
168         }
169     }
170 
archive_name( &mut self, package: &PkgType, target: &str, extension: &str, ) -> Result<String, Error>171     pub(crate) fn archive_name(
172         &mut self,
173         package: &PkgType,
174         target: &str,
175         extension: &str,
176     ) -> Result<String, Error> {
177         let component_name = package.tarball_component_name();
178         let version = match self.channel.as_str() {
179             "stable" => self.rustc_version().into(),
180             "beta" => "beta".into(),
181             "nightly" => "nightly".into(),
182             _ => format!("{}-dev", self.rustc_version()),
183         };
184 
185         if package.target_independent() {
186             Ok(format!("{}-{}.{}", component_name, version, extension))
187         } else {
188             Ok(format!("{}-{}-{}.{}", component_name, version, target, extension))
189         }
190     }
191 
tarball_name( &mut self, package: &PkgType, target: &str, ) -> Result<String, Error>192     pub(crate) fn tarball_name(
193         &mut self,
194         package: &PkgType,
195         target: &str,
196     ) -> Result<String, Error> {
197         self.archive_name(package, target, "tar.gz")
198     }
199 
rustc_version(&self) -> &str200     pub(crate) fn rustc_version(&self) -> &str {
201         const RUSTC_VERSION: &str = include_str!("../../../version");
202         RUSTC_VERSION.trim()
203     }
204 }
205