cargo/sources/registry/
local.rs

1//! Access to a regstiry on the local filesystem. See [`LocalRegistry`] for more.
2
3use crate::core::PackageId;
4use crate::sources::registry::{LoadResponse, MaybeLock, RegistryConfig, RegistryData};
5use crate::util::errors::CargoResult;
6use crate::util::{Filesystem, GlobalContext};
7use cargo_util::{paths, Sha256};
8use std::fs::File;
9use std::io::SeekFrom;
10use std::io::{self, prelude::*};
11use std::path::Path;
12use std::task::Poll;
13
14/// A local registry is a registry that lives on the filesystem as a set of
15/// `.crate` files with an `index` directory in the [same format] as a remote
16/// registry.
17///
18/// This type is primarily accessed through the [`RegistryData`] trait.
19///
20/// When a local registry is requested for a package, it simply looks into what
21/// its index has under the `index` directory. When [`LocalRegistry::download`]
22/// is called, a local registry verifies the checksum of the requested `.crate`
23/// tarball and then unpacks it to `$CARGO_HOME/.registry/src`.
24///
25/// > Note that there is a third-party subcommand [`cargo-local-registry`],
26/// > which happened to be developed by a former Cargo team member when local
27/// > registry was introduced. The tool is to ease the burden of maintaining
28/// > local registries. However, in general the Cargo team avoids recommending
29/// > any specific third-party crate. Just FYI.
30///
31/// [same format]: super#the-format-of-the-index
32/// [`cargo-local-registry`]: https://crates.io/crates/cargo-local-registry
33///
34/// # Filesystem hierarchy
35///
36/// Here is an example layout of a local registry on a local filesystem:
37///
38/// ```text
39/// [registry root]/
40/// ├── index/                      # registry index
41/// │  ├── an/
42/// │  │  └── yh/
43/// │  │     └── anyhow
44/// │  ├── ru/
45/// │  │  └── st/
46/// │  │     ├── rustls
47/// │  │     └── rustls-ffi
48/// │  └── se/
49/// │     └── mv/
50/// │        └── semver
51/// ├── anyhow-1.0.71.crate         # pre-downloaded crate tarballs
52/// ├── rustls-0.20.8.crate
53/// ├── rustls-ffi-0.8.2.crate
54/// └── semver-1.0.17.crate
55/// ```
56///
57/// For general concepts of registries, see the [module-level documentation](crate::sources::registry).
58pub struct LocalRegistry<'gctx> {
59    /// Path to the registry index.
60    index_path: Filesystem,
61    /// Root path of this local registry.
62    root: Filesystem,
63    /// Path where this local registry extract `.crate` tarballs to.
64    src_path: Filesystem,
65    gctx: &'gctx GlobalContext,
66    /// Whether this source has updated all package information it may contain.
67    updated: bool,
68    /// Disables status messages.
69    quiet: bool,
70}
71
72impl<'gctx> LocalRegistry<'gctx> {
73    /// Creates a local registry at `root`.
74    ///
75    /// * `name` --- Name of a path segment where `.crate` tarballs are stored.
76    ///   Expect to be unique.
77    pub fn new(root: &Path, gctx: &'gctx GlobalContext, name: &str) -> LocalRegistry<'gctx> {
78        LocalRegistry {
79            src_path: gctx.registry_source_path().join(name),
80            index_path: Filesystem::new(root.join("index")),
81            root: Filesystem::new(root.to_path_buf()),
82            gctx,
83            updated: false,
84            quiet: false,
85        }
86    }
87}
88
89impl<'gctx> RegistryData for LocalRegistry<'gctx> {
90    fn prepare(&self) -> CargoResult<()> {
91        Ok(())
92    }
93
94    fn index_path(&self) -> &Filesystem {
95        &self.index_path
96    }
97
98    fn assert_index_locked<'a>(&self, path: &'a Filesystem) -> &'a Path {
99        // Note that the `*_unlocked` variant is used here since we're not
100        // modifying the index and it's required to be externally synchronized.
101        path.as_path_unlocked()
102    }
103
104    fn load(
105        &mut self,
106        root: &Path,
107        path: &Path,
108        _index_version: Option<&str>,
109    ) -> Poll<CargoResult<LoadResponse>> {
110        if self.updated {
111            let raw_data = match paths::read_bytes(&root.join(path)) {
112                Err(e)
113                    if e.downcast_ref::<io::Error>()
114                        .map_or(false, |ioe| ioe.kind() == io::ErrorKind::NotFound) =>
115                {
116                    return Poll::Ready(Ok(LoadResponse::NotFound));
117                }
118                r => r,
119            }?;
120            Poll::Ready(Ok(LoadResponse::Data {
121                raw_data,
122                index_version: None,
123            }))
124        } else {
125            Poll::Pending
126        }
127    }
128
129    fn config(&mut self) -> Poll<CargoResult<Option<RegistryConfig>>> {
130        // Local registries don't have configuration for remote APIs or anything
131        // like that
132        Poll::Ready(Ok(None))
133    }
134
135    fn block_until_ready(&mut self) -> CargoResult<()> {
136        if self.updated {
137            return Ok(());
138        }
139        // Nothing to update, we just use what's on disk. Verify it actually
140        // exists though. We don't use any locks as we're just checking whether
141        // these directories exist.
142        let root = self.root.clone().into_path_unlocked();
143        if !root.is_dir() {
144            anyhow::bail!("local registry path is not a directory: {}", root.display());
145        }
146        let index_path = self.index_path.clone().into_path_unlocked();
147        if !index_path.is_dir() {
148            anyhow::bail!(
149                "local registry index path is not a directory: {}",
150                index_path.display()
151            );
152        }
153        self.updated = true;
154        Ok(())
155    }
156
157    fn invalidate_cache(&mut self) {
158        // Local registry has no cache - just reads from disk.
159    }
160
161    fn set_quiet(&mut self, _quiet: bool) {
162        self.quiet = true;
163    }
164
165    fn is_updated(&self) -> bool {
166        self.updated
167    }
168
169    fn download(&mut self, pkg: PackageId, checksum: &str) -> CargoResult<MaybeLock> {
170        // Note that the usage of `into_path_unlocked` here is because the local
171        // crate files here never change in that we're not the one writing them,
172        // so it's not our responsibility to synchronize access to them.
173        let path = self.root.join(&pkg.tarball_name()).into_path_unlocked();
174        let mut crate_file = paths::open(&path)?;
175
176        // If we've already got an unpacked version of this crate, then skip the
177        // checksum below as it is in theory already verified.
178        let dst = path.file_stem().unwrap();
179        if self.src_path.join(dst).into_path_unlocked().exists() {
180            return Ok(MaybeLock::Ready(crate_file));
181        }
182
183        if !self.quiet {
184            self.gctx.shell().status("Unpacking", pkg)?;
185        }
186
187        // We don't actually need to download anything per-se, we just need to
188        // verify the checksum matches the .crate file itself.
189        let actual = Sha256::new().update_file(&crate_file)?.finish_hex();
190        if actual != checksum {
191            anyhow::bail!("failed to verify the checksum of `{}`", pkg)
192        }
193
194        crate_file.seek(SeekFrom::Start(0))?;
195
196        Ok(MaybeLock::Ready(crate_file))
197    }
198
199    fn finish_download(
200        &mut self,
201        _pkg: PackageId,
202        _checksum: &str,
203        _data: &[u8],
204    ) -> CargoResult<File> {
205        panic!("this source doesn't download")
206    }
207}