tame_index/index/
local.rs

1//! Contains code for reading and writing [local registries](https://doc.rust-lang.org/cargo/reference/source-replacement.html#local-registry-sources)
2
3use super::FileLock;
4use crate::{Error, IndexKrate, KrateName, Path, PathBuf};
5use smol_str::SmolStr;
6
7#[cfg(feature = "local-builder")]
8pub mod builder;
9
10/// An error that can occur when validating or creating a [`LocalRegistry`]
11#[derive(Debug, thiserror::Error)]
12pub enum LocalRegistryError {
13    /// A .crate file has a version that isnot in the index
14    #[error("missing version {version} for crate {name}")]
15    MissingVersion {
16        /// The name of the crate
17        name: String,
18        /// The specific crate version
19        version: SmolStr,
20    },
21    /// A .crate file's checksum did not match the checksum in the index for that version
22    #[error("checksum mismatch for {name}-{version}.crate")]
23    ChecksumMismatch {
24        /// The name of the crate
25        name: String,
26        /// The specific crate version
27        version: SmolStr,
28    },
29}
30
31/// A [local registry](https://doc.rust-lang.org/cargo/reference/source-replacement.html#local-registry-sources)
32/// implementation
33pub struct LocalRegistry {
34    path: PathBuf,
35}
36
37impl LocalRegistry {
38    /// Opens an existing local registry, optionally validating it
39    pub fn open(path: PathBuf, validate: bool) -> Result<Self, Error> {
40        if validate {
41            Self::validate(&path)?;
42        }
43
44        Ok(Self { path })
45    }
46
47    /// Validates the specified path contains a local registry
48    ///
49    /// Validation ensures every crate file matches the expected according to
50    /// the index entry for the crate
51    pub fn validate(path: &Path) -> Result<(), Error> {
52        let rd = std::fs::read_dir(path).map_err(|err| Error::IoPath(err, path.to_owned()))?;
53
54        // There _should_ only be one directory in the root path, `index`
55        let index_root = path.join("index");
56        if !index_root.exists() {
57            return Err(Error::IoPath(
58                std::io::Error::new(
59                    std::io::ErrorKind::NotFound,
60                    "unable to find index directory",
61                ),
62                index_root,
63            ));
64        }
65
66        // Don't bother deserializing multiple times if there are multiple versions
67        // of the same crate
68        let mut indexed = std::collections::BTreeMap::new();
69
70        for entry in rd {
71            let Ok(entry) = entry else {
72                continue;
73            };
74            if entry.file_type().map_or(true, |ft| !ft.is_file()) {
75                continue;
76            }
77            let Ok(path) = PathBuf::from_path_buf(entry.path()) else {
78                continue;
79            };
80
81            let Some(fname) = path.file_name() else {
82                continue;
83            };
84            let Some((crate_name, version)) = crate_file_components(fname) else {
85                continue;
86            };
87
88            let index_entry = if let Some(ie) = indexed.get(crate_name) {
89                ie
90            } else {
91                let krate_name: crate::KrateName<'_> = crate_name.try_into()?;
92                let path = index_root.join(krate_name.relative_path(None));
93
94                let index_contents =
95                    std::fs::read(&path).map_err(|err| Error::IoPath(err, path.clone()))?;
96                let ik = IndexKrate::from_slice(&index_contents)?;
97
98                indexed.insert(crate_name.to_owned(), ik);
99
100                indexed.get(crate_name).unwrap()
101            };
102
103            let index_vers = index_entry
104                .versions
105                .iter()
106                .find(|kv| kv.version == version)
107                .ok_or_else(|| LocalRegistryError::MissingVersion {
108                    name: crate_name.to_owned(),
109                    version: version.into(),
110                })?;
111
112            // Read the crate file from disk and verify its checksum matches
113            let file =
114                std::fs::File::open(&path).map_err(|err| Error::IoPath(err, path.clone()))?;
115            if !validate_checksum::<{ 8 * 1024 }>(&file, &index_vers.checksum)
116                .map_err(|err| Error::IoPath(err, path.clone()))?
117            {
118                return Err(LocalRegistryError::ChecksumMismatch {
119                    name: crate_name.to_owned(),
120                    version: version.into(),
121                }
122                .into());
123            }
124        }
125
126        Ok(())
127    }
128
129    /// Gets the index information for the crate
130    ///
131    /// Note this naming is just to be consistent with [`crate::SparseIndex`] and
132    /// [`crate::GitIndex`], local registries do not have a .cache in the index
133    #[inline]
134    pub fn cached_krate(
135        &self,
136        name: KrateName<'_>,
137        _lock: &FileLock,
138    ) -> Result<Option<IndexKrate>, Error> {
139        let index_path = self.krate_path(name);
140
141        let buf = match std::fs::read(&index_path) {
142            Ok(buf) => buf,
143            Err(err) if err.kind() == std::io::ErrorKind::NotFound => return Ok(None),
144            Err(err) => return Err(Error::IoPath(err, index_path)),
145        };
146
147        Ok(Some(IndexKrate::from_slice(&buf)?))
148    }
149
150    /// Gets the path to the index entry for the krate.
151    ///
152    /// Note that unlike .cache entries for git and sparse indices, these are not
153    /// binary files, they are just the JSON line format
154    #[inline]
155    pub fn krate_path(&self, name: KrateName<'_>) -> PathBuf {
156        make_path(&self.path, name)
157    }
158}
159
160/// Allows the building of a local registry from a [`RemoteGitIndex`] or [`RemoteSparseIndex`]
161pub struct LocalRegistryBuilder {
162    path: PathBuf,
163}
164
165impl LocalRegistryBuilder {
166    /// Creates a builder for the specified directory.
167    ///
168    /// The directory is required to be empty, but it will
169    /// be created if it doesn't exist
170    pub fn create(path: PathBuf) -> Result<Self, Error> {
171        if path.exists() {
172            let count = std::fs::read_dir(&path)?.count();
173            if count != 0 {
174                return Err(Error::IoPath(
175                    std::io::Error::new(
176                        std::io::ErrorKind::AlreadyExists,
177                        format!("{count} entries already exist at the specified path"),
178                    ),
179                    path,
180                ));
181            }
182        } else {
183            std::fs::create_dir_all(&path)?;
184        }
185
186        std::fs::create_dir_all(path.join("index"))?;
187
188        Ok(Self { path })
189    }
190
191    /// Inserts the specified crate index entry and one or more crates files
192    /// into the registry
193    ///
194    /// This will fail if the specified crate is already located in the index, it
195    /// is your responsibility to insert the crate and all the versions you want
196    /// only once
197    pub fn insert(&self, krate: &IndexKrate, krates: &[ValidKrate<'_>]) -> Result<u64, Error> {
198        let index_path = make_path(&self.path, krate.name().try_into()?);
199
200        if index_path.exists() {
201            return Err(Error::IoPath(
202                std::io::Error::new(
203                    std::io::ErrorKind::AlreadyExists,
204                    "crate has already been inserted",
205                ),
206                index_path,
207            ));
208        }
209
210        let mut written = {
211            if let Err(err) = std::fs::create_dir_all(index_path.parent().unwrap()) {
212                return Err(Error::IoPath(err, index_path));
213            }
214
215            let mut index_entry =
216                std::fs::File::create(&index_path).map_err(|err| Error::IoPath(err, index_path))?;
217            krate.write_json_lines(&mut index_entry)?;
218            // This _should_ never fail, but even if it does, just ignore it
219            use std::io::Seek;
220            index_entry.stream_position().unwrap_or_default()
221        };
222
223        for krate in krates {
224            let krate_fname = format!("{}-{}.crate", krate.iv.name, krate.iv.version);
225            let krate_path = self.path.join(krate_fname);
226
227            std::fs::write(&krate_path, &krate.buff)
228                .map_err(|err| Error::IoPath(err, krate_path))?;
229
230            written += krate.buff.len() as u64;
231        }
232
233        Ok(written)
234    }
235
236    /// Consumes the builder and opens a [`LocalRegistry`]
237    #[inline]
238    pub fn finalize(self, validate: bool) -> Result<LocalRegistry, Error> {
239        LocalRegistry::open(self.path, validate)
240    }
241}
242
243/// A wrapper around the raw byte buffer for a .crate response from a remote
244/// index
245pub struct ValidKrate<'iv> {
246    buff: bytes::Bytes,
247    iv: &'iv crate::IndexVersion,
248}
249
250impl<'iv> ValidKrate<'iv> {
251    /// Given a buffer, validates its checksum matches the specified version
252    pub fn validate(
253        buff: impl Into<bytes::Bytes>,
254        expected: &'iv crate::IndexVersion,
255    ) -> Result<Self, Error> {
256        let buff = buff.into();
257
258        let computed = {
259            use sha2::{Digest, Sha256};
260            let mut hasher = Sha256::new();
261            hasher.update(&buff);
262            hasher.finalize()
263        };
264
265        if computed.as_slice() != expected.checksum.0 {
266            return Err(LocalRegistryError::ChecksumMismatch {
267                name: expected.name.to_string(),
268                version: expected.version.clone(),
269            }
270            .into());
271        }
272
273        Ok(Self { buff, iv: expected })
274    }
275}
276
277/// Ensures the specified stream's sha-256 matches the specified checksum
278#[inline]
279pub fn validate_checksum<const N: usize>(
280    mut stream: impl std::io::Read,
281    chksum: &crate::krate::Chksum,
282) -> Result<bool, std::io::Error> {
283    use sha2::{Digest, Sha256};
284
285    let mut buffer = [0u8; N];
286    let mut hasher = Sha256::new();
287
288    loop {
289        let read = stream.read(&mut buffer)?;
290        if read == 0 {
291            break;
292        }
293
294        hasher.update(&buffer[..read]);
295    }
296
297    let computed = hasher.finalize();
298
299    Ok(computed.as_slice() == chksum.0)
300}
301
302/// Splits a crate package name into its component parts
303///
304/// `<crate-name>-<semver>.crate`
305///
306/// The naming is a bit annoying for these since the separator between
307/// the crate name and version is a `-` which can be present in both
308/// the crate name as well as the semver, so we have to take those into account
309#[inline]
310pub fn crate_file_components(name: &str) -> Option<(&str, &str)> {
311    let name = name.strip_suffix(".crate")?;
312
313    // The first `.` should be after the major version
314    let dot = name.find('.')?;
315    let dash_sep = name[..dot].rfind('-')?;
316
317    Some((&name[..dash_sep], &name[dash_sep + 1..]))
318}
319
320#[inline]
321fn make_path(root: &Path, name: KrateName<'_>) -> PathBuf {
322    let rel_path = name.relative_path(None);
323
324    let mut index_path = PathBuf::with_capacity(root.as_str().len() + 7 + rel_path.len());
325    index_path.push(root);
326    index_path.push("index");
327    index_path.push(rel_path);
328
329    index_path
330}
331
332#[cfg(test)]
333mod test {
334    #[test]
335    fn gets_components() {
336        use super::crate_file_components as cfc;
337
338        assert_eq!(cfc("cc-1.0.75.crate").unwrap(), ("cc", "1.0.75"));
339        assert_eq!(
340            cfc("cfg-expr-0.1.0-dev+1234.crate").unwrap(),
341            ("cfg-expr", "0.1.0-dev+1234")
342        );
343        assert_eq!(
344            cfc("android_system_properties-0.1.5.crate").unwrap(),
345            ("android_system_properties", "0.1.5")
346        );
347    }
348}