crate_index/index/
tree.rs1use super::Config;
2use crate::{
3 index::{IndexFile, Metadata},
4 utils,
5 validate::Error as ValidationError,
6 Error, Result,
7};
8use async_std::path::PathBuf;
9use semver::Version;
10use std::{collections::HashSet, io};
11use url::Url;
12
13pub struct Tree {
15 root: PathBuf,
16 config: Config,
17 crates: HashSet<String>,
18}
19
20pub struct Builder {
22 root: PathBuf,
23 config: Config,
24}
25
26impl Builder {
27 pub fn api(mut self, api: Url) -> Self {
32 self.config = self.config.with_api(api);
33 self
34 }
35
36 pub fn allowed_registry(mut self, registry: Url) -> Self {
43 self.config = self.config.with_allowed_registry(registry);
44 self
45 }
46
47 pub fn allow_crates_io(mut self) -> Self {
51 self.config = self.config.with_crates_io_registry();
52 self
53 }
54
55 pub async fn build(self) -> io::Result<Tree> {
62 Tree::new(self.root, self.config).await
65 }
66}
67
68impl Tree {
69 pub fn initialise(root: impl Into<PathBuf>, download: impl Into<String>) -> Builder {
111 let root = root.into();
112 let config = Config::new(download);
113 Builder { root, config }
114 }
115
116 pub(crate) async fn new(root: PathBuf, config: Config) -> io::Result<Self> {
117 config.to_file(root.join("config.json")).await?;
118
119 let crates = HashSet::default();
120
121 let tree = Self {
122 root,
123 config,
124 crates,
125 };
126
127 Ok(tree)
128 }
129
130 pub async fn open(root: impl Into<PathBuf>) -> io::Result<Self> {
137 let root = root.into();
138 let config = Config::from_file(root.join("config.json")).await?;
139 let crates = utils::filenames(&root).await?;
140
141 let tree = Self {
142 root,
143 config,
144 crates,
145 };
146
147 Ok(tree)
148 }
149
150 async fn file(&self, crate_name: impl Into<String>) -> Result<IndexFile> {
151 IndexFile::open(self.root(), crate_name).await
152 }
153
154 pub async fn insert(&mut self, crate_metadata: Metadata) -> Result<()> {
161 self.validate_name(crate_metadata.name())?;
162
163 let crate_name = crate_metadata.name().clone();
164
165 let mut index_file = self.file(&crate_name).await?;
167
168 index_file.insert(crate_metadata).await?;
170
171 self.crates.insert(crate_name);
172
173 Ok(())
174 }
175
176 pub async fn yank(&self, crate_name: impl Into<String>, version: &Version) -> Result<()> {
183 let crate_name = crate_name.into();
184 if self.crates.contains(&crate_name) {
185 self.file(crate_name).await?.yank(version).await
186 } else {
187 Err(Error::NotFound)
188 }
189 }
190
191 pub async fn unyank(&self, crate_name: impl Into<String>, version: &Version) -> Result<()> {
198 let crate_name = crate_name.into();
199 if self.crates.contains(&crate_name) {
200 self.file(crate_name).await?.unyank(version).await
201 } else {
202 Err(Error::NotFound)
203 }
204 }
205
206 pub fn root(&self) -> &PathBuf {
208 &self.root
209 }
210
211 pub fn download(&self) -> &String {
213 self.config.download()
214 }
215
216 pub fn api(&self) -> &Option<Url> {
218 self.config.api()
219 }
220
221 pub fn allowed_registries(&self) -> &Vec<Url> {
224 self.config.allowed_registries()
225 }
226
227 pub fn contains_crate(&self, name: impl AsRef<str>) -> bool {
231 self.crates.contains(name.as_ref())
232 }
233
234 fn contains_crate_canonical(&self, name: impl AsRef<str>) -> bool {
235 let name = canonicalise(name);
236 self.crates.iter().map(canonicalise).any(|x| x == name)
237 }
238
239 fn validate_name(&self, name: impl AsRef<str>) -> std::result::Result<(), ValidationError> {
240 let name = name.as_ref();
241 if self.contains_crate_canonical(name) && !self.contains_crate(name) {
242 Err(ValidationError::invalid_name(
243 name,
244 "name is too similar to existing crate",
245 ))
246 } else {
247 Ok(())
248 }
249 }
250}
251
252fn canonicalise(name: impl AsRef<str>) -> String {
253 name.as_ref().to_lowercase().replace('-', "_")
254}
255
256#[cfg(test)]
257mod tests {
258
259 use super::{Metadata, Tree};
260 use crate::{Error, Url};
261 use async_std::path::PathBuf;
262 use semver::Version;
263 use test_case::test_case;
264
265 #[async_std::test]
266 async fn get_and_set() {
267 let temp_dir = tempfile::tempdir().unwrap();
268 let root: PathBuf = temp_dir.path().into();
269 let api = Url::parse("https://my-crates-server.com/").unwrap();
270
271 let download = "https://my-crates-server.com/api/v1/crates/{crate}/{version}/download";
272
273 let index_tree = Tree::initialise(root.clone(), download)
274 .api(api.clone())
275 .allowed_registry(Url::parse("https://my-intranet:8080/index").unwrap())
276 .allow_crates_io()
277 .build()
278 .await
279 .unwrap();
280
281 let expected_allowed_registries = vec![
282 Url::parse("https://my-intranet:8080/index").unwrap(),
283 Url::parse("https://github.com/rust-lang/crates.io-index").unwrap(),
284 ];
285
286 assert_eq!(index_tree.root().as_path(), &root);
287 assert_eq!(index_tree.download(), download);
288 assert_eq!(index_tree.api(), &Some(api));
289 assert_eq!(
290 index_tree.allowed_registries(),
291 &expected_allowed_registries
292 );
293 }
294
295 #[test_case("Some-Name", "0.1.1" ; "when used properly")]
296 #[test_case("Some_Name", "0.1.1" => panics "invalid" ; "when crate names differ only by hypens and underscores")]
297 #[test_case("some_name", "0.1.1" => panics "invalid" ; "when crate names differ only by capitalisation")]
298 #[test_case("other-name", "0.1.1" ; "when inserting a different crate")]
299 #[test_case("Some-Name", "0.1.0" => panics "invalid"; "when version is the same")]
300 #[test_case("Some-Name", "0.0.1" => panics "invalid"; "when version is lower")]
301 #[test_case("nul", "0.0.1" => panics "invalid"; "when name is reserved word")]
302 #[test_case("-start-with-hyphen", "0.0.1" => panics "invalid"; "when name starts with non-alphabetical character")]
303 fn insert(name: &str, version: &str) {
304 async_std::task::block_on(async move {
305 let temp_dir = tempfile::tempdir().unwrap();
307 let root = temp_dir.path();
308 let download = "https://my-crates-server.com/api/v1/crates/{crate}/{version}/download";
309
310 let initial_metadata = metadata("Some-Name", "0.1.0");
311
312 let mut tree = Tree::initialise(root, download)
314 .build()
315 .await
316 .expect("couldn't create index tree");
317
318 tree.insert(initial_metadata)
319 .await
320 .expect("couldn't insert initial metadata");
321
322 let new_metadata = metadata(name, version);
324 tree.insert(new_metadata).await.expect("invalid");
325 });
326 }
327
328 fn metadata(name: &str, version: &str) -> Metadata {
329 Metadata::new(name, Version::parse(version).unwrap(), "checksum")
330 }
331
332 #[async_std::test]
333 async fn open() {
334 let temp_dir = tempfile::tempdir().unwrap();
336 let root = temp_dir.path();
337 let download = "https://my-crates-server.com/api/v1/crates/{crate}/{version}/download";
338
339 let initial_metadata = metadata("Some-Name", "0.1.0");
340
341 {
342 let mut tree = Tree::initialise(root.clone(), download)
344 .build()
345 .await
346 .expect("couldn't create index tree");
347
348 tree.insert(initial_metadata)
349 .await
350 .expect("couldn't insert initial metadata");
351 }
352
353 let tree = Tree::open(root).await.expect("couldn't open index tree");
355 assert!(tree.contains_crate("Some-Name"))
356 }
357
358 #[test_case("Some-Name", "0.1.0"; "when crate exists and version exists")]
359 #[test_case("Some-Name", "0.2.0" => panics "not found"; "when crate exists but version doesn't exist")]
360 #[test_case("Other-Name", "0.2.0" => panics "not found"; "when crate doesn't exist")]
361 fn yank(crate_name: &str, version: &str) {
362 let version = Version::parse(version).unwrap();
363 async_std::task::block_on(async {
364 let temp_dir = tempfile::tempdir().unwrap();
366 let root = temp_dir.path();
367 let download = "https://my-crates-server.com/api/v1/crates/{crate}/{version}/download";
368
369 let initial_metadata = metadata("Some-Name", "0.1.0");
370
371 let mut tree = Tree::initialise(root.clone(), download)
373 .build()
374 .await
375 .expect("couldn't create tree");
376
377 tree.insert(initial_metadata)
378 .await
379 .expect("couldn't insert initial metadata");
380
381 match tree.yank(crate_name, &version).await {
382 Ok(()) => (),
383 Err(Error::NotFound) => panic!("not found"),
384 _ => panic!("something else went wrong"),
385 }
386 })
387 }
388}