1use crate::{pkg, source, DepKind, Edge};
2use anyhow::{anyhow, Result};
3use forc_tracing::{println_action_green, println_action_red};
4use petgraph::{visit::EdgeRef, Direction};
5use serde::{Deserialize, Serialize};
6use std::{
7 borrow::Cow,
8 collections::{BTreeSet, HashMap, HashSet},
9 fs,
10 path::Path,
11 str::FromStr,
12};
13use sway_core::fuel_prelude::fuel_tx;
14
15#[derive(Debug, Default, Deserialize, Serialize)]
17pub struct Lock {
18 pub(crate) package: BTreeSet<PkgLock>,
20}
21
22pub struct Diff<'a> {
26 pub removed: BTreeSet<&'a PkgLock>,
27 pub added: BTreeSet<&'a PkgLock>,
28}
29
30#[derive(Clone, Debug, Eq, Hash, Ord, PartialEq, PartialOrd, Deserialize, Serialize)]
31#[serde(rename_all = "kebab-case")]
32pub struct PkgLock {
33 pub(crate) name: String,
34 version: Option<semver::Version>,
38 source: String,
40 dependencies: Option<Vec<PkgDepLine>>,
41 contract_dependencies: Option<Vec<PkgDepLine>>,
42}
43
44pub type PkgDepLine = String;
57
58impl PkgLock {
59 pub fn from_node(graph: &pkg::Graph, node: pkg::NodeIx, disambiguate: &HashSet<&str>) -> Self {
61 let pinned = &graph[node];
62 let name = pinned.name.clone();
63 let version = pinned.source.semver();
64 let source = pinned.source.to_string();
65 let all_dependencies: Vec<(String, DepKind)> = graph
68 .edges_directed(node, Direction::Outgoing)
69 .map(|edge| {
70 let dep_edge = edge.weight();
71 let dep_node = edge.target();
72 let dep_pkg = &graph[dep_node];
73 let dep_name = if *dep_edge.name != dep_pkg.name {
74 Some(&dep_edge.name[..])
75 } else {
76 None
77 };
78 let dep_kind = &dep_edge.kind;
79 let disambiguate = disambiguate.contains(&dep_pkg.name[..]);
80 (
81 pkg_dep_line(
82 dep_name,
83 &dep_pkg.name,
84 &dep_pkg.source,
85 dep_kind,
86 disambiguate,
87 ),
88 dep_kind.clone(),
89 )
90 })
91 .collect();
92 let mut dependencies: Vec<String> = all_dependencies
93 .iter()
94 .filter_map(|(dep_pkg, dep_kind)| {
95 (*dep_kind == DepKind::Library).then_some(dep_pkg.clone())
96 })
97 .collect();
98 let mut contract_dependencies: Vec<String> = all_dependencies
99 .iter()
100 .filter_map(|(dep_pkg, dep_kind)| {
101 matches!(*dep_kind, DepKind::Contract { .. }).then_some(dep_pkg.clone())
102 })
103 .collect();
104 dependencies.sort();
105 contract_dependencies.sort();
106
107 let dependencies = if !dependencies.is_empty() {
108 Some(dependencies)
109 } else {
110 None
111 };
112
113 let contract_dependencies = if !contract_dependencies.is_empty() {
114 Some(contract_dependencies)
115 } else {
116 None
117 };
118
119 Self {
120 name,
121 version,
122 source,
123 dependencies,
124 contract_dependencies,
125 }
126 }
127
128 pub fn unique_string(&self) -> String {
132 pkg_unique_string(&self.name, &self.source)
133 }
134
135 pub fn name_disambiguated(&self, disambiguate: &HashSet<&str>) -> Cow<str> {
140 let disambiguate = disambiguate.contains(&self.name[..]);
141 pkg_name_disambiguated(&self.name, &self.source, disambiguate)
142 }
143}
144
145enum UnparsedDepKind {
149 Library,
150 Contract,
151}
152
153impl Lock {
154 pub fn from_path(path: &Path) -> Result<Self> {
156 let string = fs::read_to_string(path)
157 .map_err(|e| anyhow!("failed to read {}: {}", path.display(), e))?;
158 toml::de::from_str(&string).map_err(|e| anyhow!("failed to parse lock file: {}", e))
159 }
160
161 pub fn from_graph(graph: &pkg::Graph) -> Self {
164 let names = graph.node_indices().map(|n| &graph[n].name[..]);
165 let disambiguate: HashSet<_> = names_requiring_disambiguation(names).collect();
166 let package: BTreeSet<_> = graph
168 .node_indices()
169 .map(|node| PkgLock::from_node(graph, node, &disambiguate))
170 .collect();
171 Self { package }
172 }
173
174 pub fn to_graph(&self) -> Result<pkg::Graph> {
176 let mut graph = pkg::Graph::new();
177
178 let names = self.package.iter().map(|pkg| &pkg.name[..]);
180 let disambiguate: HashSet<_> = names_requiring_disambiguation(names).collect();
181
182 let mut pkg_to_node: HashMap<String, pkg::NodeIx> = HashMap::new();
185 for pkg in &self.package {
186 let key = pkg.name_disambiguated(&disambiguate).into_owned();
189 let name = pkg.name.clone();
190 let source: source::Pinned = pkg.source.parse().map_err(|e| {
191 anyhow!("invalid 'source' entry for package {} lock: {:?}", name, e)
192 })?;
193 let pkg = pkg::Pinned { name, source };
194 let node = graph.add_node(pkg);
195 pkg_to_node.insert(key, node);
196 }
197
198 for pkg in &self.package {
200 let key = pkg.name_disambiguated(&disambiguate);
201 let node = pkg_to_node[&key[..]];
202 let contract_deps = pkg
205 .contract_dependencies
206 .as_ref()
207 .into_iter()
208 .flatten()
209 .map(|contract_dep| (contract_dep, UnparsedDepKind::Contract));
210 let lib_deps = pkg
213 .dependencies
214 .as_ref()
215 .into_iter()
216 .flatten()
217 .map(|lib_dep| (lib_dep, UnparsedDepKind::Library));
218 for (dep_line, dep_kind) in lib_deps.chain(contract_deps) {
219 let (dep_name, dep_key, dep_salt) = parse_pkg_dep_line(dep_line)
220 .map_err(|e| anyhow!("failed to parse dependency \"{}\": {}", dep_line, e))?;
221 let dep_node = pkg_to_node
222 .get(dep_key)
223 .copied()
224 .ok_or_else(|| anyhow!("found dep {} without node entry in graph", dep_key))?;
225 let dep_name = dep_name.unwrap_or(&graph[dep_node].name).to_string();
226 let dep_kind = match dep_kind {
227 UnparsedDepKind::Library => DepKind::Library,
228 UnparsedDepKind::Contract => {
229 let dep_salt = dep_salt.unwrap_or_default();
230 DepKind::Contract { salt: dep_salt }
231 }
232 };
233 let dep_edge = Edge::new(dep_name, dep_kind);
234 graph.update_edge(node, dep_node, dep_edge);
235 }
236 }
237
238 Ok(graph)
239 }
240
241 pub fn diff<'a>(&'a self, old: &'a Self) -> Diff<'a> {
245 let added = self.package.difference(&old.package).collect();
246 let removed = old.package.difference(&self.package).collect();
247 Diff { added, removed }
248 }
249}
250
251fn names_requiring_disambiguation<'a, I>(names: I) -> impl Iterator<Item = &'a str>
253where
254 I: IntoIterator<Item = &'a str>,
255{
256 let mut visited = BTreeSet::default();
257 names.into_iter().filter(move |&name| !visited.insert(name))
258}
259
260fn pkg_name_disambiguated<'a>(name: &'a str, source: &'a str, disambiguate: bool) -> Cow<'a, str> {
261 match disambiguate {
262 true => Cow::Owned(pkg_unique_string(name, source)),
263 false => Cow::Borrowed(name),
264 }
265}
266
267fn pkg_unique_string(name: &str, source: &str) -> String {
268 format!("{name} {source}")
269}
270
271fn pkg_dep_line(
272 dep_name: Option<&str>,
273 name: &str,
274 source: &source::Pinned,
275 dep_kind: &DepKind,
276 disambiguate: bool,
277) -> PkgDepLine {
278 let source_string = source.to_string();
280 let pkg_string = pkg_name_disambiguated(name, &source_string, disambiguate);
281 let pkg_string = match dep_name {
283 None => pkg_string.into_owned(),
284 Some(dep_name) => format!("({dep_name}) {pkg_string}"),
285 };
286 match dep_kind {
288 DepKind::Library => pkg_string,
289 DepKind::Contract { salt } => {
290 if *salt == fuel_tx::Salt::zeroed() {
291 pkg_string
292 } else {
293 format!("{pkg_string} ({salt})")
294 }
295 }
296 }
297}
298
299type ParsedPkgLine<'a> = (Option<&'a str>, &'a str, Option<fuel_tx::Salt>);
300fn parse_pkg_dep_line(pkg_dep_line: &str) -> anyhow::Result<ParsedPkgLine> {
306 let s = pkg_dep_line.trim();
307 let (dep_name, s) = match s.starts_with('(') {
308 false => (None, s),
309 true => {
310 let s = &s["(".len()..];
312 let mut iter = s.split(')');
313 let dep_name = iter
314 .next()
315 .ok_or_else(|| anyhow!("missing closing parenthesis"))?;
316 let s = &s[dep_name.len() + ")".len()..];
318 (Some(dep_name), s)
319 }
320 };
321
322 let mut iter = s.split('(');
324 let pkg_str = iter
325 .next()
326 .ok_or_else(|| anyhow!("missing pkg string"))?
327 .trim();
328 let salt_str = iter.next().map(|s| s.trim()).map(|s| &s[..s.len() - 1]);
329 let salt = match salt_str {
330 Some(salt_str) => Some(
331 fuel_tx::Salt::from_str(salt_str)
332 .map_err(|e| anyhow!("invalid salt in lock file: {e}"))?,
333 ),
334 None => None,
335 };
336
337 Ok((dep_name, pkg_str, salt))
338}
339
340pub fn print_diff(member_names: &HashSet<String>, diff: &Diff) {
341 print_removed_pkgs(member_names, diff.removed.iter().copied());
342 print_added_pkgs(member_names, diff.added.iter().copied());
343}
344
345pub fn print_removed_pkgs<'a, I>(member_names: &HashSet<String>, removed: I)
346where
347 I: IntoIterator<Item = &'a PkgLock>,
348{
349 for pkg in removed {
350 if !member_names.contains(&pkg.name) {
351 let src = match pkg.source.starts_with(source::git::Pinned::PREFIX) {
352 true => format!(" {}", pkg.source),
353 false => String::new(),
354 };
355 println_action_red(
356 "Removing",
357 &format!("{}{src}", ansiterm::Style::new().bold().paint(&pkg.name)),
358 );
359 }
360 }
361}
362
363pub fn print_added_pkgs<'a, I>(member_names: &HashSet<String>, removed: I)
364where
365 I: IntoIterator<Item = &'a PkgLock>,
366{
367 for pkg in removed {
368 if !member_names.contains(&pkg.name) {
369 let src = match pkg.source.starts_with(source::git::Pinned::PREFIX) {
370 true => format!(" {}", pkg.source),
371 false => "".to_string(),
372 };
373 println_action_green(
374 "Adding",
375 &format!("{}{src}", ansiterm::Style::new().bold().paint(&pkg.name)),
376 );
377 }
378 }
379}
380
381#[cfg(test)]
382mod tests {
383 use sway_core::fuel_prelude::fuel_tx;
384
385 use super::parse_pkg_dep_line;
386
387 #[test]
388 fn test_parse_pkg_line_with_salt_with_dep_name() {
389 let pkg_dep_line = "(std2) std path+from-root (0000000000000000000000000000000000000000000000000000000000000000)";
390 let (dep_name, pkg_string, salt) = parse_pkg_dep_line(pkg_dep_line).unwrap();
391 assert_eq!(salt, Some(fuel_tx::Salt::zeroed()));
392 assert_eq!(dep_name, Some("std2"));
393 assert_eq!(pkg_string, "std path+from-root");
394 }
395
396 #[test]
397 fn test_parse_pkg_line_with_salt_without_dep_name() {
398 let pkg_dep_line =
399 "std path+from-root (0000000000000000000000000000000000000000000000000000000000000000)";
400 let (dep_name, pkg_string, salt) = parse_pkg_dep_line(pkg_dep_line).unwrap();
401 assert_eq!(salt, Some(fuel_tx::Salt::zeroed()));
402 assert_eq!(dep_name, None);
403 assert_eq!(pkg_string, "std path+from-root");
404 }
405
406 #[test]
407 fn test_parse_pkg_line_without_salt_with_dep_name() {
408 let pkg_dep_line = "(std2) std path+from-root";
409 let (dep_name, pkg_string, salt) = parse_pkg_dep_line(pkg_dep_line).unwrap();
410 assert_eq!(salt, None);
411 assert_eq!(dep_name, Some("std2"));
412 assert_eq!(pkg_string, "std path+from-root");
413 }
414
415 #[test]
416 fn test_parse_pkg_line_without_salt_without_dep_name() {
417 let pkg_dep_line = "std path+from-root";
418 let (dep_name, pkg_string, salt) = parse_pkg_dep_line(pkg_dep_line).unwrap();
419 assert_eq!(salt, None);
420 assert_eq!(dep_name, None);
421 assert_eq!(pkg_string, "std path+from-root");
422 }
423
424 #[test]
425 #[should_panic]
426 fn test_parse_pkg_line_invalid_salt() {
427 let pkg_dep_line = "std path+from-root (1)";
428 parse_pkg_dep_line(pkg_dep_line).unwrap();
429 }
430}