Skip to main content

vivacity_core/
lock.rs

1//! Reading composer.lock. Minimal typed view over the raw JSON:
2//! `installed.json`/`installed.php` will have to serve the entries back
3//! **unchanged**, so each package keeps its raw value (`raw`) and only exposes
4//! as typed fields what the installer needs.
5
6use crate::error::{Error, Result};
7use serde_json::{Map, Value};
8use std::path::Path;
9
10#[derive(Debug)]
11pub struct Lock {
12    pub content_hash: Option<String>,
13    pub packages: Vec<LockPackage>,
14    pub packages_dev: Vec<LockPackage>,
15    /// Project platform constraints (php, ext-*, lib-*) -> constraint.
16    pub platform: Vec<(String, String)>,
17    pub platform_dev: Vec<(String, String)>,
18    pub plugin_api_version: Option<String>,
19    pub aliases: Vec<Value>,
20}
21
22#[derive(Debug)]
23pub struct LockPackage {
24    pub raw: Map<String, Value>,
25}
26
27#[derive(Debug, Clone, Copy, PartialEq, Eq)]
28pub enum DistKind {
29    Zip,
30    Other,
31    Missing,
32}
33
34impl LockPackage {
35    fn str_field(&self, key: &str) -> Option<&str> {
36        self.raw.get(key).and_then(Value::as_str)
37    }
38
39    pub fn name(&self) -> &str {
40        self.str_field("name").unwrap_or("")
41    }
42
43    pub fn version(&self) -> &str {
44        self.str_field("version").unwrap_or("")
45    }
46
47    /// Package type, Composer default: "library".
48    pub fn package_type(&self) -> &str {
49        self.str_field("type").unwrap_or("library")
50    }
51
52    pub fn dist_url(&self) -> Option<&str> {
53        self.raw.get("dist")?.get("url")?.as_str()
54    }
55
56    pub fn dist_reference(&self) -> Option<&str> {
57        self.raw.get("dist")?.get("reference")?.as_str()
58    }
59
60    /// sha1 shasum of the dist if non-empty (often empty on Packagist).
61    pub fn dist_shasum(&self) -> Option<&str> {
62        self.raw
63            .get("dist")?
64            .get("shasum")?
65            .as_str()
66            .filter(|s| !s.is_empty())
67    }
68
69    pub fn dist_kind(&self) -> DistKind {
70        match self
71            .raw
72            .get("dist")
73            .and_then(|d| d.get("type"))
74            .and_then(Value::as_str)
75        {
76            Some("zip") => DistKind::Zip,
77            Some(_) => DistKind::Other,
78            None => DistKind::Missing,
79        }
80    }
81
82    /// `target-dir` (legacy PSR-0): the package installs into
83    /// vendor/<name>/<target-dir>.
84    pub fn target_dir(&self) -> Option<&str> {
85        self.str_field("target-dir")
86            .map(|t| t.trim_matches('/'))
87            .filter(|t| !t.is_empty())
88    }
89
90    /// Install path relative to vendor/ (`name` or `name/target-dir`).
91    pub fn install_subpath(&self) -> String {
92        match self.target_dir() {
93            Some(t) => format!("{}/{}", self.name(), t),
94            None => self.name().to_owned(),
95        }
96    }
97
98    pub fn is_metapackage(&self) -> bool {
99        self.package_type() == "metapackage"
100    }
101
102    pub fn bins(&self) -> Vec<&str> {
103        self.raw
104            .get("bin")
105            .and_then(Value::as_array)
106            .map(|a| a.iter().filter_map(Value::as_str).collect())
107            .unwrap_or_default()
108    }
109}
110
111fn parse_packages(v: Option<&Value>) -> Vec<LockPackage> {
112    v.and_then(Value::as_array)
113        .map(|a| {
114            a.iter()
115                .filter_map(|p| p.as_object())
116                .map(|m| LockPackage { raw: m.clone() })
117                .collect()
118        })
119        .unwrap_or_default()
120}
121
122/// `platform` is `{}` or `{"php": ">=8.2", "ext-mbstring": "*"}` and, PHP
123/// encoding quirk, sometimes `[]` (empty array) when there is nothing.
124fn parse_platform(v: Option<&Value>) -> Vec<(String, String)> {
125    v.and_then(Value::as_object)
126        .map(|m| {
127            m.iter()
128                .filter_map(|(k, v)| v.as_str().map(|s| (k.clone(), s.to_owned())))
129                .collect()
130        })
131        .unwrap_or_default()
132}
133
134impl Lock {
135    pub fn parse(text: &str) -> Result<Self> {
136        let v: Value = serde_json::from_str(text).map_err(|source| Error::Json {
137            context: "composer.lock".to_owned(),
138            source,
139        })?;
140        Ok(Lock {
141            content_hash: v
142                .get("content-hash")
143                .and_then(Value::as_str)
144                .map(str::to_owned),
145            packages: parse_packages(v.get("packages")),
146            packages_dev: parse_packages(v.get("packages-dev")),
147            platform: parse_platform(v.get("platform")),
148            platform_dev: parse_platform(v.get("platform-dev")),
149            plugin_api_version: v
150                .get("plugin-api-version")
151                .and_then(Value::as_str)
152                .map(str::to_owned),
153            aliases: v
154                .get("aliases")
155                .and_then(Value::as_array)
156                .cloned()
157                .unwrap_or_default(),
158        })
159    }
160
161    pub fn read(path: &Path) -> Result<Self> {
162        let text = std::fs::read_to_string(path).map_err(|source| Error::ReadFile {
163            path: path.to_path_buf(),
164            source,
165        })?;
166        Self::parse(&text)
167    }
168
169    /// Packages to install according to --no-dev.
170    pub fn wanted_packages(&self, with_dev: bool) -> impl Iterator<Item = &LockPackage> {
171        self.packages
172            .iter()
173            .chain(
174                self.packages_dev
175                    .iter()
176                    .take(if with_dev { usize::MAX } else { 0 }),
177            )
178    }
179}
180
181#[cfg(test)]
182mod tests {
183    use super::*;
184
185    #[test]
186    fn parses_minimal_lock() {
187        let lock = Lock::parse(
188            r#"{"content-hash":"abc","packages":[{"name":"a/b","version":"1.0.0",
189                "dist":{"type":"zip","url":"https://x/y.zip","reference":"deadbeef","shasum":""},
190                "type":"library","bin":["bin/tool"]}],
191               "packages-dev":[],"platform":{"php":">=8.1"},"platform-dev":[]}"#,
192        )
193        .expect("parse");
194        assert_eq!(lock.content_hash.as_deref(), Some("abc"));
195        let p = &lock.packages[0];
196        assert_eq!(p.name(), "a/b");
197        assert_eq!(p.dist_kind(), DistKind::Zip);
198        assert_eq!(p.dist_shasum(), None); // empty -> None
199        assert_eq!(p.bins(), vec!["bin/tool"]);
200        assert_eq!(lock.platform, vec![("php".to_owned(), ">=8.1".to_owned())]);
201        assert_eq!(lock.wanted_packages(false).count(), 1);
202    }
203
204    #[test]
205    fn empty_platform_as_array() {
206        let lock = Lock::parse(r#"{"packages":[],"platform":[]}"#).expect("parse");
207        assert!(lock.platform.is_empty());
208    }
209}