1use crate::hashing::sha256_prefixed;
2use crate::literal::{
3 find_top_level, is_balanced, parse_literal, parse_literal_map, prepare_parser_lines,
4 split_top_level, LiteralValue,
5};
6use crate::{PrayError, PrayResult};
7use serde::{Deserialize, Serialize};
8use std::borrow::Cow;
9use std::collections::BTreeMap;
10
11#[derive(Debug, Clone, Default, Serialize, Deserialize, PartialEq)]
12pub struct PackageSpec {
13 pub name: String,
14 pub version: String,
15 pub summary: Option<String>,
16 pub description: Option<String>,
17 pub authors: Vec<String>,
18 pub license: Option<String>,
19 pub homepage: Option<String>,
20 pub source_code_uri: Option<String>,
21 pub changelog_uri: Option<String>,
22 pub prayfile_version: Option<String>,
23 pub files: Vec<String>,
24 pub exports: BTreeMap<String, PackageExport>,
25 pub skills: BTreeMap<String, PackageSkill>,
26 pub templates: BTreeMap<String, PackageTemplate>,
27 pub adapters: BTreeMap<String, String>,
28 pub targets: Vec<String>,
29 pub dependencies: Vec<PackageDependency>,
30 pub metadata: BTreeMap<String, LiteralValue>,
31}
32
33#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
34pub struct PackageExport {
35 pub kind: String,
36 pub path: String,
37 pub summary: Option<String>,
38 #[serde(default, skip_serializing_if = "Vec::is_empty")]
39 pub only: Vec<String>,
40 #[serde(default, skip_serializing_if = "Vec::is_empty")]
41 pub except: Vec<String>,
42 #[serde(default, skip_serializing_if = "Option::is_none")]
43 pub default_path: Option<String>,
44}
45
46#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
47pub struct PackageSkill {
48 pub path: String,
49 pub summary: Option<String>,
50}
51
52#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
53pub struct PackageTemplate {
54 pub path: String,
55 pub summary: Option<String>,
56}
57
58#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
59pub struct PackageDependency {
60 pub name: String,
61 pub constraint: String,
62 pub optional: bool,
63}
64
65impl PackageSpec {
66 pub fn canonicalized(&self) -> Self {
67 let mut package = self.clone();
68 package.files.sort();
69 package.authors.sort();
70 package.targets.sort();
71 package.dependencies.sort_by(|left, right| {
72 left.name
73 .cmp(&right.name)
74 .then(left.constraint.cmp(&right.constraint))
75 .then(left.optional.cmp(&right.optional))
76 });
77 package
78 }
79
80 pub fn tree_hash_for_root(&self, root: &std::path::Path) -> PrayResult<String> {
81 let mut file_bytes = BTreeMap::new();
82 for file in &self.files {
83 let path = root.join(file);
84 if !path.exists() {
85 return Err(PrayError::Integrity(format!(
86 "package file missing: {}",
87 file
88 )));
89 }
90 if path.is_dir() {
91 return Err(PrayError::Integrity(format!(
92 "package file is a directory: {}",
93 file
94 )));
95 }
96 file_bytes.insert(file.clone(), std::fs::read(&path)?);
97 }
98 Self::tree_hash_from_file_bytes(&file_bytes)
99 }
100
101 pub fn tree_hash_from_file_bytes(file_bytes: &BTreeMap<String, Vec<u8>>) -> PrayResult<String> {
102 let mut entries = Vec::new();
103 for (path, bytes) in file_bytes {
104 entries.push((path.clone(), sha256_prefixed(bytes)));
105 }
106 entries.sort_by(|left, right| left.0.cmp(&right.0));
107
108 let mut serialized = String::new();
109 for (path, hash) in entries {
110 serialized.push_str("file");
111 serialized.push('\0');
112 serialized.push_str("regular");
113 serialized.push('\0');
114 serialized.push_str(&path);
115 serialized.push('\0');
116 serialized.push_str(&hash);
117 serialized.push('\n');
118 }
119 Ok(sha256_prefixed(serialized.as_bytes()))
120 }
121}
122
123pub fn parse_package_spec(text: &str) -> PrayResult<PackageSpec> {
124 let lines = prepare_parser_lines(text);
125 let mut parser = BlockParser::new(&lines);
126 parser.parse_root()
127}
128
129struct BlockParser<'a> {
130 lines: &'a [Cow<'a, str>],
131 cursor: usize,
132}
133
134impl<'a> BlockParser<'a> {
135 fn new(lines: &'a [Cow<'a, str>]) -> Self {
136 Self { lines, cursor: 0 }
137 }
138
139 fn parse_root(&mut self) -> PrayResult<PackageSpec> {
140 self.expect_start()?;
141 let mut spec = PackageSpec::default();
142 while let Some(statement) = self.next_statement()? {
143 if statement == "end" {
144 return Ok(spec.canonicalized());
145 }
146 self.apply_statement(&mut spec, statement)?;
147 }
148 Err(PrayError::Parse {
149 kind: "prayspec",
150 message: "missing 'end'".to_string(),
151 })
152 }
153
154 fn expect_start(&mut self) -> PrayResult<()> {
155 let statement = self.next_statement()?.ok_or_else(|| PrayError::Parse {
156 kind: "prayspec",
157 message: "empty package spec".to_string(),
158 })?;
159 if !statement.starts_with("Package::Specification.new") {
160 return Err(PrayError::Parse {
161 kind: "prayspec",
162 message: "expected Package::Specification.new".to_string(),
163 });
164 }
165 Ok(())
166 }
167
168 fn apply_statement(&mut self, spec: &mut PackageSpec, statement: String) -> PrayResult<()> {
169 if let Some(rest) = statement.strip_prefix("spec.add_dependency ") {
170 spec.dependencies.push(parse_dependency(rest, false)?);
171 return Ok(());
172 }
173 if let Some(rest) = statement.strip_prefix("spec.add_optional_dependency ") {
174 spec.dependencies.push(parse_dependency(rest, true)?);
175 return Ok(());
176 }
177 if let Some(rest) = statement.strip_prefix("spec.") {
178 if let Some((name, value)) = rest.split_once(" = ") {
179 return self.apply_assignment(spec, name.trim(), value.trim());
180 }
181 }
182 Err(PrayError::Parse {
183 kind: "prayspec",
184 message: format!("unrecognized statement: {statement}"),
185 })
186 }
187
188 fn apply_assignment(&self, spec: &mut PackageSpec, field: &str, value: &str) -> PrayResult<()> {
189 match field {
190 "name" => spec.name = string_from_literal(value)?,
191 "version" => spec.version = string_from_literal(value)?,
192 "summary" => spec.summary = Some(string_from_literal(value)?),
193 "description" => spec.description = Some(string_from_literal(value)?),
194 "authors" => spec.authors = array_of_strings(value)?,
195 "license" => spec.license = Some(string_from_literal(value)?),
196 "homepage" => spec.homepage = Some(string_from_literal(value)?),
197 "source_code_uri" => spec.source_code_uri = Some(string_from_literal(value)?),
198 "changelog_uri" => spec.changelog_uri = Some(string_from_literal(value)?),
199 "prayfile_version" => spec.prayfile_version = Some(string_from_literal(value)?),
200 "files" => spec.files = array_of_strings(value)?,
201 "targets" => spec.targets = array_of_strings(value)?,
202 "exports" => spec.exports = parse_exports(value)?,
203 "skills" => spec.skills = parse_skills(value)?,
204 "templates" => spec.templates = parse_templates(value)?,
205 "adapters" => spec.adapters = parse_string_map(value)?,
206 "metadata" => spec.metadata = parse_metadata(value)?,
207 _ => {
208 return Err(PrayError::Parse {
209 kind: "prayspec",
210 message: format!("unsupported assignment: {field}"),
211 })
212 }
213 }
214 Ok(())
215 }
216
217 fn next_statement(&mut self) -> PrayResult<Option<String>> {
218 while self.cursor < self.lines.len() {
219 let mut statement = self.lines[self.cursor].trim().to_string();
220 self.cursor += 1;
221 if statement.is_empty() {
222 continue;
223 }
224 while !statement.ends_with(" do")
225 && statement != "end"
226 && self.cursor < self.lines.len()
227 && (statement.trim_end().ends_with(',') || !is_balanced(&statement))
228 {
229 let next = self.lines[self.cursor].trim();
230 self.cursor += 1;
231 if next.is_empty() {
232 continue;
233 }
234 statement.push(' ');
235 statement.push_str(next);
236 }
237 return Ok(Some(statement));
238 }
239 Ok(None)
240 }
241}
242
243fn parse_dependency(rest: &str, optional: bool) -> PrayResult<PackageDependency> {
244 let (values, keywords) = parse_call(rest)?;
245 let name = string_from_value(values.first().ok_or_else(|| PrayError::Parse {
246 kind: "prayspec",
247 message: "missing dependency name".to_string(),
248 })?)?;
249 let constraint = values
250 .get(1)
251 .map(string_from_value)
252 .transpose()?
253 .unwrap_or("*".to_string());
254 Ok(PackageDependency {
255 name,
256 constraint,
257 optional: keywords
258 .get("optional")
259 .and_then(|value| value.as_bool())
260 .unwrap_or(optional),
261 })
262}
263
264fn parse_call(rest: &str) -> PrayResult<(Vec<LiteralValue>, BTreeMap<String, LiteralValue>)> {
265 let mut positional = Vec::new();
266 let mut keywords = BTreeMap::new();
267 for segment in split_top_level(rest.trim().trim_end_matches(','), ',') {
268 if let Some((key, value)) = parse_keyword_segment(&segment)? {
269 keywords.insert(key, value);
270 } else if !segment.is_empty() {
271 positional.push(parse_literal(&segment)?);
272 }
273 }
274 Ok((positional, keywords))
275}
276
277fn parse_keyword_segment(segment: &str) -> PrayResult<Option<(String, LiteralValue)>> {
278 if let Some(index) = find_top_level(segment, "=>") {
279 let key = string_from_literal(segment[..index].trim())?;
280 return Ok(Some((key, parse_literal(segment[index + 2..].trim())?)));
281 }
282 if let Some(index) = find_top_level(segment, ":") {
283 let left = segment[..index].trim();
284 let right = segment[index + 1..].trim();
285 if left.is_empty() {
286 return Ok(None);
287 }
288 return Ok(Some((left.to_string(), parse_literal(right)?)));
289 }
290 Ok(None)
291}
292
293fn parse_exports(value: &str) -> PrayResult<BTreeMap<String, PackageExport>> {
294 let map = parse_literal_map(value)?;
295 let mut exports = BTreeMap::new();
296 for (name, literal) in map {
297 let entry = literal.as_map().ok_or_else(|| PrayError::Parse {
298 kind: "prayspec",
299 message: format!("export {name} must be a map"),
300 })?;
301 let missing_path_name = name.clone();
302 exports.insert(
303 name,
304 PackageExport {
305 kind: map_string(entry, "type").unwrap_or_else(|| "fragment".to_string()),
306 path: map_string(entry, "path").ok_or_else(|| PrayError::Parse {
307 kind: "prayspec",
308 message: format!("export {missing_path_name} missing path"),
309 })?,
310 summary: map_string(entry, "summary"),
311 only: map_string_array(entry, "only"),
312 except: map_string_array(entry, "except"),
313 default_path: map_string(entry, "default_path"),
314 },
315 );
316 }
317 Ok(exports)
318}
319
320fn parse_skills(value: &str) -> PrayResult<BTreeMap<String, PackageSkill>> {
321 let map = parse_literal_map(value)?;
322 let mut output = BTreeMap::new();
323 for (name, literal) in map {
324 let entry = literal.as_map().ok_or_else(|| PrayError::Parse {
325 kind: "prayspec",
326 message: format!("skill {name} must be a map"),
327 })?;
328 output.insert(
329 name,
330 PackageSkill {
331 path: map_string(entry, "path").ok_or_else(|| PrayError::Parse {
332 kind: "prayspec",
333 message: "skill missing path".to_string(),
334 })?,
335 summary: map_string(entry, "summary"),
336 },
337 );
338 }
339 Ok(output)
340}
341
342fn parse_templates(value: &str) -> PrayResult<BTreeMap<String, PackageTemplate>> {
343 let map = parse_literal_map(value)?;
344 let mut output = BTreeMap::new();
345 for (name, literal) in map {
346 let entry = literal.as_map().ok_or_else(|| PrayError::Parse {
347 kind: "prayspec",
348 message: format!("template {name} must be a map"),
349 })?;
350 output.insert(
351 name,
352 PackageTemplate {
353 path: map_string(entry, "path").ok_or_else(|| PrayError::Parse {
354 kind: "prayspec",
355 message: "template missing path".to_string(),
356 })?,
357 summary: map_string(entry, "summary"),
358 },
359 );
360 }
361 Ok(output)
362}
363
364fn parse_string_map(value: &str) -> PrayResult<BTreeMap<String, String>> {
365 let map = parse_literal_map(value)?;
366 let mut output = BTreeMap::new();
367 for (key, literal) in map {
368 output.insert(key, string_from_value(&literal)?);
369 }
370 Ok(output)
371}
372
373fn parse_metadata(value: &str) -> PrayResult<BTreeMap<String, LiteralValue>> {
374 parse_literal_map(value)
375}
376
377fn map_string(map: &BTreeMap<String, LiteralValue>, key: &str) -> Option<String> {
378 map.get(key)
379 .and_then(|value| value.as_string().map(str::to_string))
380}
381
382fn map_string_array(map: &BTreeMap<String, LiteralValue>, key: &str) -> Vec<String> {
383 map.get(key)
384 .and_then(|value| value.as_array())
385 .map(|values| {
386 values
387 .iter()
388 .filter_map(|value| value.as_string().map(str::to_string))
389 .collect()
390 })
391 .unwrap_or_default()
392}
393
394fn array_of_strings(value: &str) -> PrayResult<Vec<String>> {
395 let array = parse_literal(value)?;
396 let values = array.as_array().ok_or_else(|| PrayError::Parse {
397 kind: "prayspec",
398 message: "expected array".to_string(),
399 })?;
400 values.iter().map(string_from_value).collect()
401}
402
403fn string_from_value(value: &LiteralValue) -> PrayResult<String> {
404 value
405 .as_string()
406 .map(str::to_string)
407 .ok_or_else(|| PrayError::Parse {
408 kind: "prayspec",
409 message: format!("expected string-like literal, found {:?}", value),
410 })
411}
412
413fn string_from_literal(value: &str) -> PrayResult<String> {
414 string_from_value(&parse_literal(value)?)
415}