1use std::collections::BTreeMap;
16
17use miette::{Context, IntoDiagnostic, ensure};
18use semver::Version;
19use serde::{Deserialize, Serialize};
20use thiserror::Error;
21use tokio::fs;
22use url::Url;
23
24use crate::{
25 ManagedFile,
26 errors::{DeserializationError, FileExistsError, FileNotFound, SerializationError, WriteError},
27 package::{Package, PackageName},
28 registry::RegistryUri,
29};
30
31mod digest;
32pub use digest::{Digest, DigestAlgorithm};
33
34pub const LOCKFILE: &str = "Proto.lock";
36
37#[derive(Debug, Serialize, Deserialize, Clone, PartialEq, Eq, PartialOrd, Ord)]
41pub struct LockedPackage {
42 pub name: PackageName,
44 pub digest: Digest,
46 pub registry: RegistryUri,
48 pub repository: String,
50 pub version: Version,
52 pub dependencies: Vec<PackageName>,
54 pub dependants: usize,
58}
59
60impl LockedPackage {
61 pub fn lock(
63 package: &Package,
64 registry: RegistryUri,
65 repository: String,
66 dependants: usize,
67 ) -> Self {
68 Self {
69 name: package.name().to_owned(),
70 registry,
71 repository,
72 digest: package.digest(DigestAlgorithm::SHA256).to_owned(),
73 version: package.version().to_owned(),
74 dependencies: package
75 .manifest
76 .dependencies
77 .iter()
78 .map(|d| d.package.clone())
79 .collect(),
80 dependants,
81 }
82 }
83
84 pub fn validate(&self, package: &Package) -> miette::Result<()> {
86 let digest: Digest = DigestAlgorithm::SHA256.digest(&package.tgz);
87
88 #[derive(Error, Debug)]
89 #[error("{property} mismatch - expected {expected}, actual {actual}")]
90 struct ValidationError {
91 property: &'static str,
92 expected: String,
93 actual: String,
94 }
95
96 ensure!(
97 &self.name == package.name(),
98 ValidationError {
99 property: "name",
100 expected: self.name.to_string(),
101 actual: package.name().to_string(),
102 }
103 );
104
105 ensure!(
106 &self.version == package.version(),
107 ValidationError {
108 property: "version",
109 expected: self.version.to_string(),
110 actual: package.version().to_string(),
111 }
112 );
113
114 ensure!(
115 self.digest == digest,
116 ValidationError {
117 property: "digest",
118 expected: self.digest.to_string(),
119 actual: digest.to_string(),
120 }
121 );
122
123 Ok(())
124 }
125}
126
127#[derive(Serialize, Deserialize)]
128struct RawLockfile {
129 version: u16,
130 packages: Vec<LockedPackage>,
131}
132
133#[derive(Default)]
137pub struct Lockfile {
138 packages: BTreeMap<PackageName, LockedPackage>,
139}
140
141impl Lockfile {
142 pub async fn exists() -> miette::Result<bool> {
144 fs::try_exists(LOCKFILE)
145 .await
146 .into_diagnostic()
147 .wrap_err(FileExistsError(LOCKFILE))
148 }
149
150 pub async fn read() -> miette::Result<Self> {
152 match fs::read_to_string(LOCKFILE).await {
153 Ok(contents) => {
154 let raw: RawLockfile = toml::from_str(&contents)
155 .into_diagnostic()
156 .wrap_err(DeserializationError(ManagedFile::Lock))?;
157 Ok(Self::from_iter(raw.packages.into_iter()))
158 }
159 Err(err) if matches!(err.kind(), std::io::ErrorKind::NotFound) => {
160 Err(FileNotFound(LOCKFILE.into()).into())
161 }
162 Err(err) => Err(err).into_diagnostic(),
163 }
164 }
165
166 pub async fn read_or_default() -> miette::Result<Self> {
168 if Lockfile::exists().await? {
169 Lockfile::read().await
170 } else {
171 Ok(Lockfile::default())
172 }
173 }
174
175 pub async fn write(&self) -> miette::Result<()> {
177 let mut packages: Vec<_> = self
178 .packages
179 .values()
180 .map(|pkg| {
181 let mut locked = pkg.clone();
182 locked.dependencies.sort();
183 locked
184 })
185 .collect();
186
187 packages.sort();
188
189 let raw = RawLockfile {
190 version: 1,
191 packages,
192 };
193
194 fs::write(
195 LOCKFILE,
196 toml::to_string(&raw)
197 .into_diagnostic()
198 .wrap_err(SerializationError(ManagedFile::Lock))?
199 .into_bytes(),
200 )
201 .await
202 .into_diagnostic()
203 .wrap_err(WriteError(LOCKFILE))
204 }
205
206 pub fn get(&self, name: &PackageName) -> Option<&LockedPackage> {
208 self.packages.get(name)
209 }
210}
211
212impl FromIterator<LockedPackage> for Lockfile {
213 fn from_iter<I: IntoIterator<Item = LockedPackage>>(iter: I) -> Self {
214 Self {
215 packages: iter
216 .into_iter()
217 .map(|locked| (locked.name.clone(), locked))
218 .collect(),
219 }
220 }
221}
222
223impl From<Lockfile> for Vec<FileRequirement> {
224 fn from(lock: Lockfile) -> Self {
229 lock.packages.values().map(FileRequirement::from).collect()
230 }
231}
232
233#[derive(Serialize, Clone, PartialEq, Eq)]
237pub struct FileRequirement {
238 pub(crate) package: PackageName,
239 pub(crate) url: Url,
240 pub(crate) digest: Digest,
241}
242
243impl FileRequirement {
244 pub fn url(&self) -> &Url {
246 &self.url
247 }
248
249 pub fn new(
251 url: &RegistryUri,
252 repository: &String,
253 name: &PackageName,
254 version: &Version,
255 digest: &Digest,
256 ) -> Self {
257 let mut url = url.clone();
258 let new_path = format!(
259 "{}/{}/{}/{}-{}.tgz",
260 url.path(),
261 repository,
262 name,
263 name,
264 version
265 );
266
267 url.set_path(&new_path);
268
269 Self {
270 package: name.to_owned(),
271 url: url.into(),
272 digest: digest.clone(),
273 }
274 }
275}
276
277impl From<LockedPackage> for FileRequirement {
278 fn from(package: LockedPackage) -> Self {
279 Self::new(
280 &package.registry,
281 &package.repository,
282 &package.name,
283 &package.version,
284 &package.digest,
285 )
286 }
287}
288
289impl From<&LockedPackage> for FileRequirement {
290 fn from(package: &LockedPackage) -> Self {
291 Self::new(
292 &package.registry,
293 &package.repository,
294 &package.name,
295 &package.version,
296 &package.digest,
297 )
298 }
299}
300
301#[cfg(test)]
302mod tests {
303 use std::{collections::BTreeMap, str::FromStr};
304
305 use semver::Version;
306
307 use crate::{package::PackageName, registry::RegistryUri};
308
309 use super::{Digest, DigestAlgorithm, FileRequirement, LockedPackage, Lockfile};
310
311 fn simple_lockfile() -> Lockfile {
312 Lockfile {
313 packages: BTreeMap::from([
314 (
315 PackageName::new("package1").unwrap(),
316 LockedPackage {
317 name: PackageName::new("package1").unwrap(),
318 digest: Digest::from_parts(
319 DigestAlgorithm::SHA256,
320 "c109c6b120c525e6ea7b2db98335d39a3272f572ac86ba7b2d65c765c353c122",
321 )
322 .unwrap(),
323 registry: RegistryUri::from_str("http://my-registry.com").unwrap(),
324 repository: "my-repo".to_owned(),
325 version: Version::new(0, 1, 0),
326 dependencies: vec![],
327 dependants: 1,
328 },
329 ),
330 (
331 PackageName::new("package2").unwrap(),
332 LockedPackage {
333 name: PackageName::new("package2").unwrap(),
334 digest: Digest::from_parts(
335 DigestAlgorithm::SHA256,
336 "c109c6b120c525e6ea7b2db98335d39a3272f572ac86ba7b2d65c765c353bce3",
337 )
338 .unwrap(),
339 registry: RegistryUri::from_str("http://my-registry.com").unwrap(),
340 repository: "my-other-repo".to_owned(),
341 version: Version::new(0, 2, 0),
342 dependencies: vec![],
343 dependants: 1,
344 },
345 ),
346 (
347 PackageName::new("package3").unwrap(),
348 LockedPackage {
349 name: PackageName::new("package3").unwrap(),
350 digest: Digest::from_parts(
351 DigestAlgorithm::SHA256,
352 "c109c6b120c525e6ea7b2db98335d39a3272f572ac86ba7b2d65c765c353bce3",
353 )
354 .unwrap(),
355 registry: RegistryUri::from_str("http://your-registry.com").unwrap(),
356 repository: "your-repo".to_owned(),
357 version: Version::new(0, 2, 0),
358 dependencies: vec![],
359 dependants: 1,
360 },
361 ),
362 (
363 PackageName::new("package4").unwrap(),
364 LockedPackage {
365 name: PackageName::new("package4").unwrap(),
366 digest: Digest::from_parts(
367 DigestAlgorithm::SHA256,
368 "c109c6b120c525e6ea7b2db98335d39a3272f572ac86ba7b2d65c765c353bce3",
369 )
370 .unwrap(),
371 registry: RegistryUri::from_str("http://your-registry.com").unwrap(),
372 repository: "your-other-repo".to_owned(),
373 version: Version::new(0, 2, 0),
374 dependencies: vec![],
375 dependants: 1,
376 },
377 ),
378 ]),
379 }
380 }
381
382 #[test]
383 fn stable_file_requirement_order() {
384 let lock = simple_lockfile();
385 let files: Vec<FileRequirement> = lock.into();
386 for _ in 0..30 {
387 let other_files: Vec<FileRequirement> = simple_lockfile().into();
388 assert!(other_files == files)
389 }
390 }
391}