1use std::ops::Range;
7
8use nova_diagnostics::{Diagnostic, FileId, Span};
9use semver::Version;
10use toml_edit::{ImDocument, Item};
11
12pub const LOCKFILE: &str = "nova.lock";
14
15#[derive(Debug, Clone, PartialEq, Eq)]
17pub struct Lock {
18 pub index: String,
20 pub packages: Vec<LockedPackage>,
22}
23
24#[derive(Debug, Clone, PartialEq, Eq)]
26pub struct LockedPackage {
27 pub name: String,
28 pub version: Version,
29 pub checksum: String,
31 pub dependencies: Vec<String>,
33}
34
35impl Lock {
36 pub fn find(&self, name: &str) -> Option<&LockedPackage> {
37 self.packages.iter().find(|p| p.name == name)
38 }
39
40 pub fn to_text(&self) -> String {
42 let mut out = String::from("# Written by nova. Commit it for a program.\nversion = 1\n");
43 out.push_str(&format!("index = {}\n", quote(&self.index)));
44 for package in &self.packages {
45 out.push_str("\n[[package]]\n");
46 out.push_str(&format!("name = {}\n", quote(&package.name)));
47 out.push_str(&format!(
48 "version = {}\n",
49 quote(&package.version.to_string())
50 ));
51 out.push_str(&format!("checksum = {}\n", quote(&package.checksum)));
52 let names: Vec<String> = package.dependencies.iter().map(|n| quote(n)).collect();
53 out.push_str(&format!("dependencies = [{}]\n", names.join(", ")));
54 }
55 out
56 }
57}
58
59fn quote(text: &str) -> String {
61 let mut out = String::from("\"");
62 for c in text.chars() {
63 match c {
64 '"' => out.push_str("\\\""),
65 '\\' => out.push_str("\\\\"),
66 c if c.is_control() => out.push_str(&format!("\\u{:04X}", c as u32)),
67 c => out.push(c),
68 }
69 }
70 out.push('"');
71 out
72}
73
74pub fn parse_lock(text: &str, file: FileId) -> Result<Lock, Diagnostic> {
77 let bad = |message: &str, at: Option<Range<usize>>| {
78 let at = at.unwrap_or(0..0);
79 Diagnostic::error("M0016", format!("nova.lock cannot be read: {message}"))
80 .with_primary_label(Span::new(at.start as u32, at.end as u32, file), "here")
81 .with_note("delete nova.lock, or run `nova update`")
82 };
83 let document = ImDocument::parse(text).map_err(|e| bad(e.message().trim(), e.span()))?;
84 let table = document.as_table();
85 if table.get("version").and_then(Item::as_integer) != Some(1) {
86 return Err(bad(
87 "its `version` is not 1",
88 table.get("version").and_then(Item::span),
89 ));
90 }
91 let index = table
92 .get("index")
93 .and_then(Item::as_str)
94 .ok_or_else(|| bad("it has no `index`", None))?
95 .to_string();
96 let mut packages = Vec::new();
97 if let Some(item) = table.get("package") {
98 let tables = item
99 .as_array_of_tables()
100 .ok_or_else(|| bad("`package` is not a list of tables", item.span()))?;
101 for package in tables.iter() {
102 let field = |key: &str| package.get(key).and_then(Item::as_str);
103 let (Some(name), Some(version), Some(checksum)) =
104 (field("name"), field("version"), field("checksum"))
105 else {
106 return Err(bad(
107 "a package lacks its name, version or checksum",
108 package.span(),
109 ));
110 };
111 let version = Version::parse(version)
112 .map_err(|e| bad(&format!("version `{version}`: {e}"), package.span()))?;
113 let dependencies = match package.get("dependencies") {
114 None => Vec::new(),
115 Some(item) => item
116 .as_array()
117 .and_then(|array| {
118 array
119 .iter()
120 .map(|v| v.as_str().map(str::to_string))
121 .collect::<Option<Vec<_>>>()
122 })
123 .ok_or_else(|| bad("`dependencies` is not a list of names", item.span()))?,
124 };
125 packages.push(LockedPackage {
126 name: name.to_string(),
127 version,
128 checksum: checksum.to_string(),
129 dependencies,
130 });
131 }
132 }
133 packages.sort_by(|a, b| a.name.cmp(&b.name));
134 Ok(Lock { index, packages })
135}