1use std::collections::BTreeMap;
5use std::path::Path;
6
7use semver::Version;
8use serde::{Deserialize, Serialize};
9
10use dre_protocol::PluginId;
11
12pub const LOCK_FILE: &str = "dre.lock";
13
14#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
15pub struct Locked {
16 pub version: Version,
17 pub sha256: Checksums,
18 #[serde(default, skip_serializing_if = "Option::is_none")]
21 pub from: Option<String>,
22 #[serde(default, skip_serializing_if = "Vec::is_empty")]
24 pub provides: Vec<PluginId>,
25}
26
27#[derive(Debug, Clone, PartialEq, Default)]
31pub struct Checksums(BTreeMap<String, String>);
32
33const LEGACY: &str = "";
35
36impl Checksums {
37 pub fn new() -> Checksums {
38 Checksums::default()
39 }
40
41 pub fn legacy_only(sha: &str) -> Checksums {
43 let mut c = Checksums::new();
44 c.0.insert(LEGACY.into(), sha.to_lowercase());
45 c
46 }
47
48 pub fn get(&self, platform: &str) -> Option<&str> {
49 self.0.get(platform).map(String::as_str)
50 }
51
52 pub fn is_empty(&self) -> bool {
53 self.0.is_empty()
54 }
55
56 pub fn contains(&self, sha: &str) -> bool {
58 self.0
59 .iter()
60 .any(|(p, s)| p != LEGACY && s.eq_ignore_ascii_case(sha))
61 }
62
63 pub fn legacy(&self) -> Option<&str> {
64 self.get(LEGACY)
65 }
66
67 pub fn insert(&mut self, platform: &str, sha: &str) {
68 self.0.remove(LEGACY);
69 self.0.insert(platform.into(), sha.to_lowercase());
70 }
71
72 pub fn merge(&mut self, other: &Checksums) {
74 for (p, s) in &other.0 {
75 if p != LEGACY {
76 self.0.entry(p.clone()).or_insert_with(|| s.clone());
77 }
78 }
79 }
80}
81
82impl Serialize for Checksums {
83 fn serialize<S: serde::Serializer>(&self, s: S) -> Result<S::Ok, S::Error> {
84 match self.legacy() {
85 Some(sha) if self.0.len() == 1 => s.serialize_str(sha),
86 _ => self.0.serialize(s),
87 }
88 }
89}
90
91impl<'de> Deserialize<'de> for Checksums {
92 fn deserialize<D: serde::Deserializer<'de>>(d: D) -> Result<Self, D::Error> {
93 struct V;
94 impl<'de> serde::de::Visitor<'de> for V {
95 type Value = Checksums;
96 fn expecting(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
97 f.write_str("checksums by platform, or one checksum")
98 }
99 fn visit_str<E>(self, sha: &str) -> Result<Checksums, E> {
100 Ok(Checksums::legacy_only(sha))
101 }
102 fn visit_u64<E>(self, n: u64) -> Result<Checksums, E> {
104 Ok(Checksums::legacy_only(&n.to_string()))
105 }
106 fn visit_i64<E>(self, n: i64) -> Result<Checksums, E> {
107 Ok(Checksums::legacy_only(&n.to_string()))
108 }
109 fn visit_f64<E>(self, n: f64) -> Result<Checksums, E> {
110 Ok(Checksums::legacy_only(&n.to_string()))
111 }
112 fn visit_map<A: serde::de::MapAccess<'de>>(self, mut m: A) -> Result<Checksums, A::Error> {
113 let mut out = BTreeMap::new();
114 while let Some((k, v)) = m.next_entry::<String, String>()? {
115 out.insert(k, v.to_lowercase());
116 }
117 Ok(Checksums(out))
118 }
119 }
120 d.deserialize_any(V)
121 }
122}
123
124#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
126pub struct LockedPackage {
127 pub git: String,
128 pub revision: String,
129 pub commit: String,
130}
131
132#[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize)]
133pub struct Lock {
134 #[serde(default, skip_serializing_if = "BTreeMap::is_empty")]
136 pub plugins: BTreeMap<String, Locked>,
137 #[serde(default, skip_serializing_if = "BTreeMap::is_empty")]
139 pub packages: BTreeMap<String, LockedPackage>,
140 #[serde(default, skip_serializing_if = "BTreeMap::is_empty")]
142 pub local: BTreeMap<String, String>,
143}
144
145impl Lock {
146 pub fn load(root: &Path) -> Result<Lock, String> {
148 let p = root.join(LOCK_FILE);
149 match std::fs::read_to_string(&p) {
150 Ok(t) => serde_saphyr::from_str(&t).map_err(|e| format!("{LOCK_FILE}: {e}")),
151 Err(e) if e.kind() == std::io::ErrorKind::NotFound => Ok(Lock::default()),
152 Err(e) => Err(format!("{LOCK_FILE}: {e}")),
153 }
154 }
155
156 pub fn save(&self, root: &Path) -> Result<(), String> {
157 let body = crate::config::to_yaml(self)?;
158 let text = format!(
159 "# Generated by DRE: the exact plugin versions and package commits this project uses. Commit this file.\n{body}"
160 );
161 crate::fs::write_atomic(&root.join(LOCK_FILE), text.as_bytes())
162 .map_err(|e| format!("{LOCK_FILE}: {e}"))
163 }
164
165 pub fn get(&self, package: &str) -> Option<&Locked> {
167 self.plugins.get(package)
168 }
169
170 pub fn version(&self, package: &str) -> Option<Version> {
171 self.get(package).map(|l| l.version.clone())
172 }
173}