Skip to main content

lovely/
lockfile.rs

1use crate::fsutil;
2use crate::{LovelyError, Result};
3use std::collections::BTreeMap;
4use std::path::Path;
5
6pub const LOCK_FILE: &str = "lovely.lock";
7
8#[derive(Debug, Clone, PartialEq, Eq)]
9pub struct LockFile {
10    pub schema: u32,
11    pub runtime_channel: String,
12    pub love: LockedComponent,
13    pub emscripten: LockedComponent,
14}
15
16#[derive(Debug, Clone, PartialEq, Eq)]
17pub struct LockedComponent {
18    pub source: String,
19    pub revision: String,
20    pub sha256: String,
21}
22
23impl LockFile {
24    pub fn preview_default() -> Self {
25        Self {
26            schema: 1,
27            runtime_channel: "love-11-plus".to_string(),
28            love: LockedComponent {
29                source: "https://github.com/love2d/love".to_string(),
30                revision: "main".to_string(),
31                sha256: "unresolved".to_string(),
32            },
33            emscripten: LockedComponent {
34                source: "emsdk".to_string(),
35                revision: "pinned-by-runtime-manifest".to_string(),
36                sha256: "unresolved".to_string(),
37            },
38        }
39    }
40
41    pub fn load_from(path: &Path) -> Result<Self> {
42        let text = fsutil::read_to_string(path)?;
43        Self::parse(&text)
44    }
45
46    pub fn parse(text: &str) -> Result<Self> {
47        let mut values = BTreeMap::<String, String>::new();
48        for (index, line) in text.lines().enumerate() {
49            let line = line.trim();
50            if line.is_empty() || line.starts_with('#') {
51                continue;
52            }
53            let Some((key, value)) = line.split_once('=') else {
54                return Err(LovelyError::Lock(format!(
55                    "line {} is not a key/value pair",
56                    index + 1
57                )));
58            };
59            values.insert(key.trim().to_string(), unquote(value.trim()));
60        }
61
62        let schema = values
63            .get("schema")
64            .and_then(|value| value.parse::<u32>().ok())
65            .unwrap_or(1);
66        Ok(Self {
67            schema,
68            runtime_channel: take(&values, "runtime_channel")?,
69            love: LockedComponent {
70                source: take(&values, "love.source")?,
71                revision: take(&values, "love.revision")?,
72                sha256: take(&values, "love.sha256")?,
73            },
74            emscripten: LockedComponent {
75                source: take(&values, "emscripten.source")?,
76                revision: take(&values, "emscripten.revision")?,
77                sha256: take(&values, "emscripten.sha256")?,
78            },
79        })
80    }
81
82    pub fn to_text(&self) -> String {
83        format!(
84            r#"# Generated by Lovely. Commit this file for reproducible builds.
85schema = {schema}
86runtime_channel = "{runtime_channel}"
87
88love.source = "{love_source}"
89love.revision = "{love_revision}"
90love.sha256 = "{love_sha}"
91
92emscripten.source = "{emscripten_source}"
93emscripten.revision = "{emscripten_revision}"
94emscripten.sha256 = "{emscripten_sha}"
95"#,
96            schema = self.schema,
97            runtime_channel = escape(&self.runtime_channel),
98            love_source = escape(&self.love.source),
99            love_revision = escape(&self.love.revision),
100            love_sha = escape(&self.love.sha256),
101            emscripten_source = escape(&self.emscripten.source),
102            emscripten_revision = escape(&self.emscripten.revision),
103            emscripten_sha = escape(&self.emscripten.sha256),
104        )
105    }
106
107    pub fn has_unresolved_checksums(&self) -> bool {
108        [&self.love, &self.emscripten]
109            .iter()
110            .any(|component| component.sha256 == "unresolved")
111    }
112}
113
114fn take(values: &BTreeMap<String, String>, key: &str) -> Result<String> {
115    values
116        .get(key)
117        .cloned()
118        .ok_or_else(|| LovelyError::Lock(format!("missing {key}")))
119}
120
121fn unquote(value: &str) -> String {
122    if value.starts_with('"') && value.ends_with('"') && value.len() >= 2 {
123        value[1..value.len() - 1].replace("\\\"", "\"")
124    } else {
125        value.to_string()
126    }
127}
128
129fn escape(input: &str) -> String {
130    input.replace('\\', "\\\\").replace('"', "\\\"")
131}