1use crate::error::{Error, Result};
7use serde_json::{Map, Value};
8use std::path::Path;
9
10#[derive(Debug, Clone)]
11pub struct Lock {
12 pub content_hash: Option<String>,
13 pub packages: Vec<LockPackage>,
14 pub packages_dev: Vec<LockPackage>,
15 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, Clone)]
23pub struct LockPackage {
24 pub raw: Map<String, Value>,
25}
26
27#[derive(Debug, Clone, Copy, PartialEq, Eq)]
28pub enum DistKind {
29 Zip,
30 Tar,
32 Path,
34 Other,
35 Missing,
36}
37
38impl LockPackage {
39 fn str_field(&self, key: &str) -> Option<&str> {
40 self.raw.get(key).and_then(Value::as_str)
41 }
42
43 pub fn name(&self) -> &str {
44 self.str_field("name").unwrap_or("")
45 }
46
47 pub fn version(&self) -> &str {
48 self.str_field("version").unwrap_or("")
49 }
50
51 pub fn package_type(&self) -> &str {
53 self.str_field("type").unwrap_or("library")
54 }
55
56 pub fn dist_url(&self) -> Option<&str> {
57 self.raw.get("dist")?.get("url")?.as_str()
58 }
59
60 pub fn dist_url_expanded(&self) -> Option<String> {
66 let url = self.dist_url()?;
67 if !url.contains('%') {
68 return Some(url.to_owned());
69 }
70 let version = crate::version::normalize_pretty(self.version())
71 .unwrap_or_else(|_| self.version().to_owned());
72 let kind = self
73 .raw
74 .get("dist")
75 .and_then(|d| d.get("type"))
76 .and_then(Value::as_str)
77 .unwrap_or("");
78 Some(
79 url.replace("%package%", self.name())
80 .replace("%version%", &version)
81 .replace("%reference%", self.dist_reference().unwrap_or(""))
82 .replace("%type%", kind)
83 .replace("%prettyVersion%", self.version()),
84 )
85 }
86
87 pub fn dist_reference(&self) -> Option<&str> {
88 self.raw.get("dist")?.get("reference")?.as_str()
89 }
90
91 pub fn dist_shasum(&self) -> Option<&str> {
93 self.raw
94 .get("dist")?
95 .get("shasum")?
96 .as_str()
97 .filter(|s| !s.is_empty())
98 }
99
100 pub fn store_reference(&self) -> Option<String> {
105 match self.dist_reference() {
106 Some(r) => Some(r.to_owned()),
107 None => self.dist_url().map(|u| {
108 use sha1::Digest as _;
109 sha1::Sha1::digest(u.as_bytes())
110 .iter()
111 .map(|b| format!("{b:02x}"))
112 .collect()
113 }),
114 }
115 }
116
117 pub fn dist_kind(&self) -> DistKind {
118 match self
119 .raw
120 .get("dist")
121 .and_then(|d| d.get("type"))
122 .and_then(Value::as_str)
123 {
124 Some("zip") => DistKind::Zip,
125 Some("tar") => DistKind::Tar,
126 Some("path") => DistKind::Path,
127 Some(_) => DistKind::Other,
128 None => DistKind::Missing,
129 }
130 }
131
132 pub fn target_dir(&self) -> Option<&str> {
135 self.str_field("target-dir")
136 .map(|t| t.trim_matches('/'))
137 .filter(|t| !t.is_empty())
138 }
139
140 pub fn install_subpath(&self) -> String {
142 match self.target_dir() {
143 Some(t) => format!("{}/{}", self.name(), t),
144 None => self.name().to_owned(),
145 }
146 }
147
148 pub fn is_metapackage(&self) -> bool {
149 self.package_type() == "metapackage"
150 }
151
152 pub fn is_virtual(&self, flex_packs: bool) -> bool {
155 self.is_metapackage() || (flex_packs && self.package_type() == "symfony-pack")
156 }
157
158 pub fn bins(&self) -> Vec<&str> {
159 self.raw
160 .get("bin")
161 .and_then(Value::as_array)
162 .map(|a| a.iter().filter_map(Value::as_str).collect())
163 .unwrap_or_default()
164 }
165}
166
167fn parse_packages(v: Option<&Value>) -> Vec<LockPackage> {
168 v.and_then(Value::as_array)
169 .map(|a| {
170 a.iter()
171 .filter_map(|p| p.as_object())
172 .map(|m| LockPackage { raw: m.clone() })
173 .collect()
174 })
175 .unwrap_or_default()
176}
177
178fn parse_platform(v: Option<&Value>) -> Vec<(String, String)> {
181 v.and_then(Value::as_object)
182 .map(|m| {
183 m.iter()
184 .filter_map(|(k, v)| v.as_str().map(|s| (k.clone(), s.to_owned())))
185 .collect()
186 })
187 .unwrap_or_default()
188}
189
190impl Lock {
191 pub fn parse(text: &str) -> Result<Self> {
192 let v: Value = serde_json::from_str(text).map_err(|source| Error::Json {
193 context: "composer.lock".to_owned(),
194 source,
195 })?;
196 Ok(Self::from_value(&v))
197 }
198
199 pub fn from_value(v: &Value) -> Self {
202 Lock {
203 content_hash: v
204 .get("content-hash")
205 .and_then(Value::as_str)
206 .map(str::to_owned),
207 packages: parse_packages(v.get("packages")),
208 packages_dev: parse_packages(v.get("packages-dev")),
209 platform: parse_platform(v.get("platform")),
210 platform_dev: parse_platform(v.get("platform-dev")),
211 plugin_api_version: v
212 .get("plugin-api-version")
213 .and_then(Value::as_str)
214 .map(str::to_owned),
215 aliases: v
216 .get("aliases")
217 .and_then(Value::as_array)
218 .cloned()
219 .unwrap_or_default(),
220 }
221 }
222
223 pub fn read(path: &Path) -> Result<Self> {
224 let text = std::fs::read_to_string(path).map_err(|source| Error::ReadFile {
225 path: path.to_path_buf(),
226 source,
227 })?;
228 Self::parse(&text)
229 }
230
231 pub fn flex_packs(&self, root_manifest: &Value, with_dev: bool, plugins_enabled: bool) -> bool {
236 plugins_enabled
237 && self
238 .wanted_packages(with_dev)
239 .any(|p| p.name() == "symfony/flex")
240 && matches!(
241 crate::layout::plugin_allowed(root_manifest, "symfony/flex"),
242 crate::layout::PluginVerdict::Allowed
243 )
244 }
245
246 pub fn wanted_packages(&self, with_dev: bool) -> impl Iterator<Item = &LockPackage> {
247 self.packages
248 .iter()
249 .chain(
250 self.packages_dev
251 .iter()
252 .take(if with_dev { usize::MAX } else { 0 }),
253 )
254 }
255}
256
257#[cfg(test)]
258mod tests {
259 use super::*;
260
261 #[test]
262 fn parses_minimal_lock() {
263 let lock = Lock::parse(
264 r#"{"content-hash":"abc","packages":[{"name":"a/b","version":"1.0.0",
265 "dist":{"type":"zip","url":"https://x/y.zip","reference":"deadbeef","shasum":""},
266 "type":"library","bin":["bin/tool"]}],
267 "packages-dev":[],"platform":{"php":">=8.1"},"platform-dev":[]}"#,
268 )
269 .expect("parse");
270 assert_eq!(lock.content_hash.as_deref(), Some("abc"));
271 let p = &lock.packages[0];
272 assert_eq!(p.name(), "a/b");
273 assert_eq!(p.dist_kind(), DistKind::Zip);
274 assert_eq!(p.dist_shasum(), None); assert_eq!(p.bins(), vec!["bin/tool"]);
276 assert_eq!(lock.platform, vec![("php".to_owned(), ">=8.1".to_owned())]);
277 assert_eq!(lock.wanted_packages(false).count(), 1);
278 }
279
280 #[test]
281 fn empty_platform_as_array() {
282 let lock = Lock::parse(r#"{"packages":[],"platform":[]}"#).expect("parse");
283 assert!(lock.platform.is_empty());
284 }
285}
286
287#[cfg(test)]
288mod placeholder_tests {
289 use super::*;
290
291 #[test]
292 fn dist_url_placeholders_like_composer_mirror() {
293 let p = LockPackage {
294 raw: serde_json::from_str(r#"{"name": "ssddanbrown/asserthtml", "version": "v3.2.0",
295 "dist": {"type": "zip", "url": "https://codeberg.org/api/v1/repos/%package%/archive/%prettyVersion%.zip?r=%reference%&t=%type%&v=%version%", "reference": "0811b5c"}}"#).unwrap(),
296 };
297 assert_eq!(
298 p.dist_url_expanded().unwrap(),
299 "https://codeberg.org/api/v1/repos/ssddanbrown/asserthtml/archive/v3.2.0.zip?r=0811b5c&t=zip&v=3.2.0.0"
300 );
301 }
302}