1use super::FileLock;
4use crate::{Error, IndexKrate, KrateName, Path, PathBuf};
5use smol_str::SmolStr;
6
7#[cfg(feature = "local-builder")]
8pub mod builder;
9
10#[derive(Debug, thiserror::Error)]
12pub enum LocalRegistryError {
13 #[error("missing version {version} for crate {name}")]
15 MissingVersion {
16 name: String,
18 version: SmolStr,
20 },
21 #[error("checksum mismatch for {name}-{version}.crate")]
23 ChecksumMismatch {
24 name: String,
26 version: SmolStr,
28 },
29}
30
31pub struct LocalRegistry {
34 path: PathBuf,
35}
36
37impl LocalRegistry {
38 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 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 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 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 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 #[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 #[inline]
155 pub fn krate_path(&self, name: KrateName<'_>) -> PathBuf {
156 make_path(&self.path, name)
157 }
158}
159
160pub struct LocalRegistryBuilder {
162 path: PathBuf,
163}
164
165impl LocalRegistryBuilder {
166 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 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 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 #[inline]
238 pub fn finalize(self, validate: bool) -> Result<LocalRegistry, Error> {
239 LocalRegistry::open(self.path, validate)
240 }
241}
242
243pub struct ValidKrate<'iv> {
246 buff: bytes::Bytes,
247 iv: &'iv crate::IndexVersion,
248}
249
250impl<'iv> ValidKrate<'iv> {
251 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#[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#[inline]
310pub fn crate_file_components(name: &str) -> Option<(&str, &str)> {
311 let name = name.strip_suffix(".crate")?;
312
313 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}