crate_index/index/
tree.rs

1use 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
13/// An interface to a crate index directory on the filesystem
14pub struct Tree {
15    root: PathBuf,
16    config: Config,
17    crates: HashSet<String>,
18}
19
20/// Builder for creating a new [`Tree`]
21pub struct Builder {
22    root: PathBuf,
23    config: Config,
24}
25
26impl Builder {
27    /// Set the Url for the registry API.
28    ///
29    /// The API should implement the REST interface as defined in
30    /// [the Cargo book](https://doc.rust-lang.org/cargo/reference/registries.html)
31    pub fn api(mut self, api: Url) -> Self {
32        self.config = self.config.with_api(api);
33        self
34    }
35
36    /// Add an allowed registry.
37    ///
38    /// Crates in this registry are only allowed to have dependencies which are
39    /// also in this registry, or in one of the allowed registries.
40    ///
41    /// Add multiple registries my calling this method multiple times.
42    pub fn allowed_registry(mut self, registry: Url) -> Self {
43        self.config = self.config.with_allowed_registry(registry);
44        self
45    }
46
47    /// Add crates.io as an allowed registry.
48    ///
49    /// You will almost always want this, so this exists as a handy shortcut.
50    pub fn allow_crates_io(mut self) -> Self {
51        self.config = self.config.with_crates_io_registry();
52        self
53    }
54
55    /// Construct the [`Tree`] with the given parameters.
56    ///
57    /// # Errors
58    ///
59    /// This method can fail if the root path doesn't exist, or the filesystem
60    /// cannot be written to.
61    pub async fn build(self) -> io::Result<Tree> {
62        // once 'IntoFuture' is stabilised, this 'build' method should be replaced with
63        // an 'IntoFuture' implementation so that the builder can be awaited directly
64        Tree::new(self.root, self.config).await
65    }
66}
67
68impl Tree {
69    /// Create a new index `Tree`.
70    ///
71    /// The root path, and the URL for downloading .crate files is required.
72    /// Additional options can be set using the builder API (see
73    /// [`Builder`] for options).
74    ///
75    /// # Example
76    ///
77    /// ## Basic Config
78    ///
79    /// ```no_run
80    /// use crate_index::index::Tree;
81    /// # use crate_index::Error;
82    /// # async {
83    /// let root = "/index";
84    /// let download = "https://my-crates-server.com/api/v1/crates/{crate}/{version}/download";
85    ///
86    /// let index_tree = Tree::initialise(root, download).build().await?;
87    /// # Ok::<(), Error>(())
88    /// # };
89    /// ```
90    ///
91    /// ## More Options
92    ///
93    /// ```no_run
94    /// use crate_index::{index::Tree, Url};
95    /// # use crate_index::Error;
96    /// # async {
97    /// let root = "/index";
98    /// let download = "https://my-crates-server.com/api/v1/crates/{crate}/{version}/download";
99    ///
100    ///
101    /// let index_tree = Tree::initialise(root, download)
102    ///     .api(Url::parse("https://my-crates-server.com/").unwrap())
103    ///     .allowed_registry(Url::parse("https://my-intranet:8080/index").unwrap())
104    ///     .allow_crates_io()
105    ///     .build()
106    ///     .await?;
107    /// # Ok::<(), Error>(())
108    /// # };
109    /// ```
110    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    /// Open an existing index tree at the given root path.
131    ///
132    /// # Errors
133    ///
134    /// This method can fail if the given path does not exist, or the config
135    /// file cannot be read.
136    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    /// Insert crate ['Metadata'] into the index.
155    ///
156    /// # Errors
157    ///
158    /// This method can fail if the metadata is deemed to be invalid, or if the
159    /// filesystem cannot be written to.
160    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        // open the index file for editing
166        let mut index_file = self.file(&crate_name).await?;
167
168        // insert the new metadata
169        index_file.insert(crate_metadata).await?;
170
171        self.crates.insert(crate_name);
172
173        Ok(())
174    }
175
176    /// Mark a selected version of a crate as 'yanked'.
177    ///
178    /// # Errors
179    ///
180    /// This function will return [`Error::NotFound`] if the crate or the
181    /// selected version does not exist in the index.
182    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    /// Mark a selected version of a crate as 'unyanked'.
192    ///
193    /// # Errors
194    ///
195    /// This function will return [`Error::NotFound`] if the crate or the
196    /// selected version does not exist in the index.
197    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    /// The location on the filesystem of the root of the index
207    pub fn root(&self) -> &PathBuf {
208        &self.root
209    }
210
211    /// The Url for downloading .crate files
212    pub fn download(&self) -> &String {
213        self.config.download()
214    }
215
216    /// The Url of the API
217    pub fn api(&self) -> &Option<Url> {
218        self.config.api()
219    }
220
221    /// The list of registries which crates in this index are allowed to have
222    /// dependencies on
223    pub fn allowed_registries(&self) -> &Vec<Url> {
224        self.config.allowed_registries()
225    }
226
227    /// Test whether the index contains a particular crate name.
228    ///
229    /// This method is fast, since the crate names are stored in memory.
230    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            // create temporary directory
306            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            // create index file and seed with initial metadata
313            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            // create and insert new metadata
323            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        // create temporary directory
335        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            // create index file and seed with initial metadata
343            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        // reopen the same tree and check crate is there
354        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            // create temporary directory
365            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            // create index file and seed with initial metadata
372            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}