1 // Copyright (C) 2024 The Android Open Source Project
2 //
3 // Licensed under the Apache License, Version 2.0 (the "License");
4 // you may not use this file except in compliance with the License.
5 // You may obtain a copy of the License at
6 //
7 // http://www.apache.org/licenses/LICENSE-2.0
8 //
9 // Unless required by applicable law or agreed to in writing, software
10 // distributed under the License is distributed on an "AS IS" BASIS,
11 // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12 // See the License for the specific language governing permissions and
13 // limitations under the License.
14
15 use anyhow::{anyhow, Result};
16 use cfg_expr::{
17 targets::{Arch, Family, Os},
18 Predicate, TargetPredicate,
19 };
20 use crates_index::{http, Crate, Dependency, DependencyKind, SparseIndex, Version};
21 use reqwest::blocking::Client;
22 use semver::VersionReq;
23 use std::{
24 cell::RefCell,
25 collections::{HashMap, HashSet},
26 };
27
28 pub struct CratesIoIndex {
29 fetcher: Box<dyn CratesIoFetcher>,
30 }
31
32 impl CratesIoIndex {
new() -> Result<CratesIoIndex>33 pub fn new() -> Result<CratesIoIndex> {
34 Ok(CratesIoIndex {
35 fetcher: Box::new(OnlineFetcher {
36 index: crates_index::SparseIndex::new_cargo_default()?,
37 client: reqwest::blocking::ClientBuilder::new().gzip(true).build()?,
38 fetched: RefCell::new(HashSet::new()),
39 }),
40 })
41 }
new_offline() -> Result<CratesIoIndex>42 pub fn new_offline() -> Result<CratesIoIndex> {
43 Ok(CratesIoIndex {
44 fetcher: Box::new(OfflineFetcher {
45 index: crates_index::SparseIndex::new_cargo_default()?,
46 }),
47 })
48 }
get_crate(&self, crate_name: impl AsRef<str>) -> Result<Crate>49 pub fn get_crate(&self, crate_name: impl AsRef<str>) -> Result<Crate> {
50 self.fetcher.fetch(crate_name.as_ref())
51 }
52 }
53
54 pub trait CratesIoFetcher {
fetch(&self, crate_name: &str) -> Result<Crate>55 fn fetch(&self, crate_name: &str) -> Result<Crate>;
56 }
57
58 pub struct OnlineFetcher {
59 index: SparseIndex,
60 client: Client,
61 // Keep track of crates we have fetched, to avoid fetching them multiple times.
62 fetched: RefCell<HashSet<String>>,
63 }
64
65 pub struct OfflineFetcher {
66 index: SparseIndex,
67 }
68
69 impl CratesIoFetcher for OnlineFetcher {
fetch(&self, crate_name: &str) -> Result<Crate>70 fn fetch(&self, crate_name: &str) -> Result<Crate> {
71 // Adapted from https://github.com/frewsxcv/rust-crates-index/blob/master/examples/sparse_http_reqwest.rs
72
73 let mut fetched = self.fetched.borrow_mut();
74 if fetched.contains(crate_name) {
75 return Ok(self.index.crate_from_cache(crate_name.as_ref())?);
76 }
77 let req = self.index.make_cache_request(crate_name)?.body(())?;
78 let req = http::Request::from_parts(req.into_parts().0, vec![]);
79 let req: reqwest::blocking::Request = req.try_into()?;
80 let res = self.client.execute(req)?;
81 let mut builder = http::Response::builder().status(res.status()).version(res.version());
82 builder
83 .headers_mut()
84 .ok_or(anyhow!("Failed to get headers"))?
85 .extend(res.headers().iter().map(|(k, v)| (k.clone(), v.clone())));
86 let body = res.bytes()?;
87 let res = builder.body(body.to_vec())?;
88 let res = self
89 .index
90 .parse_cache_response(crate_name, res, true)?
91 .ok_or(anyhow!("Crate not found"))?;
92 fetched.insert(crate_name.to_string());
93 Ok(res)
94 }
95 }
96 impl CratesIoFetcher for OfflineFetcher {
fetch(&self, crate_name: &str) -> Result<Crate>97 fn fetch(&self, crate_name: &str) -> Result<Crate> {
98 Ok(self.index.crate_from_cache(crate_name.as_ref())?)
99 }
100 }
101
102 /// Filter versions by those that are "safe", meaning not yanked or pre-release.
103 pub trait SafeVersions {
104 // Versions of the crate that aren't yanked or pre-release.
safe_versions(&self) -> impl DoubleEndedIterator<Item = &Version>105 fn safe_versions(&self) -> impl DoubleEndedIterator<Item = &Version>;
106 // Versions of the crate greater than 'version'.
versions_gt(&self, version: &semver::Version) -> impl DoubleEndedIterator<Item = &Version>107 fn versions_gt(&self, version: &semver::Version) -> impl DoubleEndedIterator<Item = &Version> {
108 self.safe_versions().filter(|v| {
109 semver::Version::parse(v.version()).map_or(false, |parsed| parsed.gt(version))
110 })
111 }
112 // Get a specific version of a crate.
get_version(&self, version: &semver::Version) -> Option<&Version>113 fn get_version(&self, version: &semver::Version) -> Option<&Version> {
114 self.safe_versions().find(|v| {
115 semver::Version::parse(v.version()).map_or(false, |parsed| parsed.eq(version))
116 })
117 }
118 }
119 impl SafeVersions for Crate {
safe_versions(&self) -> impl DoubleEndedIterator<Item = &Version>120 fn safe_versions(&self) -> impl DoubleEndedIterator<Item = &Version> {
121 self.versions().iter().filter(|v| {
122 !v.is_yanked()
123 && semver::Version::parse(v.version()).map_or(false, |parsed| parsed.pre.is_empty())
124 })
125 }
126 }
127
128 /// Filter dependencies for those likely to be relevant to Android.
129 pub trait AndroidDependencies {
android_deps(&self) -> impl DoubleEndedIterator<Item = &Dependency>130 fn android_deps(&self) -> impl DoubleEndedIterator<Item = &Dependency>;
android_version_reqs_by_name(&self) -> HashMap<&str, &str>131 fn android_version_reqs_by_name(&self) -> HashMap<&str, &str> {
132 self.android_deps().map(|dep| (dep.crate_name(), dep.requirement())).collect()
133 }
android_deps_with_version_reqs( &self, ) -> impl DoubleEndedIterator<Item = (&Dependency, VersionReq)>134 fn android_deps_with_version_reqs(
135 &self,
136 ) -> impl DoubleEndedIterator<Item = (&Dependency, VersionReq)> {
137 self.android_deps().filter_map(|dep| {
138 VersionReq::parse(dep.requirement()).map_or(None, |req| Some((dep, req)))
139 })
140 }
141 }
142 impl AndroidDependencies for Version {
android_deps(&self) -> impl DoubleEndedIterator<Item = &Dependency>143 fn android_deps(&self) -> impl DoubleEndedIterator<Item = &Dependency> {
144 self.dependencies().iter().filter(|dep| {
145 dep.kind() == DependencyKind::Normal && !dep.is_optional() && dep.is_android()
146 })
147 }
148 }
149
150 /// Dependencies that are likely to be relevant to Android.
151 /// Unconditional dependencies (without a target cfg string) are always relevant.
152 /// Conditional dependencies are relevant if they are for Unix, Android, or Linux, and for an architecture we care about (Arm, RISC-V, or X86)
153 pub trait IsAndroid {
154 /// Returns true if this dependency is likely to be relevant to Android.
is_android(&self) -> bool155 fn is_android(&self) -> bool;
156 }
157 impl IsAndroid for Dependency {
is_android(&self) -> bool158 fn is_android(&self) -> bool {
159 self.target().map_or(true, is_android)
160 }
161 }
is_android(target: &str) -> bool162 fn is_android(target: &str) -> bool {
163 let expr = cfg_expr::Expression::parse(target);
164 if expr.is_err() {
165 return false;
166 }
167 let expr = expr.unwrap();
168 expr.eval(|pred| match pred {
169 Predicate::Target(target_predicate) => match target_predicate {
170 TargetPredicate::Family(family) => *family == Family::unix,
171 TargetPredicate::Os(os) => *os == Os::android || *os == Os::linux,
172 TargetPredicate::Arch(arch) => {
173 [Arch::arm, Arch::aarch64, Arch::riscv32, Arch::riscv64, Arch::x86, Arch::x86_64]
174 .contains(arch)
175 }
176 _ => true,
177 },
178 _ => true,
179 })
180 }
181
182 pub trait DependencyChanges {
is_new_dep(&self, base_deps: &HashMap<&str, &str>) -> bool183 fn is_new_dep(&self, base_deps: &HashMap<&str, &str>) -> bool;
is_changed_dep(&self, base_deps: &HashMap<&str, &str>) -> bool184 fn is_changed_dep(&self, base_deps: &HashMap<&str, &str>) -> bool;
185 }
186
187 impl DependencyChanges for Dependency {
is_new_dep(&self, base_deps: &HashMap<&str, &str>) -> bool188 fn is_new_dep(&self, base_deps: &HashMap<&str, &str>) -> bool {
189 !base_deps.contains_key(self.crate_name())
190 }
191
is_changed_dep(&self, base_deps: &HashMap<&str, &str>) -> bool192 fn is_changed_dep(&self, base_deps: &HashMap<&str, &str>) -> bool {
193 let base_dep = base_deps.get(self.crate_name());
194 base_dep.is_none() || base_dep.is_some_and(|base_req| *base_req != self.requirement())
195 }
196 }
197
198 #[cfg(test)]
199 mod tests {
200 use super::*;
201 #[test]
test_android_cfgs()202 fn test_android_cfgs() {
203 assert!(!is_android("asmjs-unknown-emscripten"), "Parse error");
204 assert!(!is_android("cfg(windows)"));
205 assert!(is_android("cfg(unix)"));
206 assert!(!is_android(r#"cfg(target_os = "redox")"#));
207 assert!(!is_android(r#"cfg(target_arch = "wasm32")"#));
208 assert!(is_android(r#"cfg(any(target_os = "linux", target_os = "android"))"#));
209 assert!(is_android(
210 r#"cfg(any(all(target_arch = "arm", target_pointer_width = "32"), target_arch = "mips", target_arch = "powerpc"))"#
211 ));
212 assert!(!is_android(
213 r#"cfg(all(target_arch = "wasm32", target_vendor = "unknown", target_os = "unknown"))"#
214 ));
215 assert!(is_android("cfg(tracing_unstable)"));
216 assert!(is_android(r#"cfg(any(unix, target_os = "wasi"))"#));
217 assert!(is_android(r#"cfg(not(all(target_arch = "arm", target_os = "none")))"#))
218 }
219 }
220