1use crate::constraint::parse_constraints;
7use crate::package::{Link, LinkType, Links, Origin, Package, SourceRef};
8use crate::version::{
9 normalize, normalize_branch, parse_numeric_alias_prefix, parse_stability, VersionError,
10 DEFAULT_BRANCH_ALIAS,
11};
12use serde_json::{Map, Value};
13
14#[derive(Debug, thiserror::Error)]
15#[error("{0}")]
16pub struct LoadError(pub String);
17
18pub fn expand_minified(versions: &[Value]) -> Vec<Value> {
20 expand_minified_owned(versions.to_vec())
21}
22
23pub fn expand_minified_owned(versions: Vec<Value>) -> Vec<Value> {
25 let mut expanded: Vec<Value> = Vec::with_capacity(versions.len());
26 let mut current: Option<Map<String, Value>> = None;
27 for v in versions {
28 let data = match v {
29 Value::Object(o) => o,
30 _ => Map::new(),
31 };
32 if current.as_ref().is_some_and(Map::is_empty) {
34 current = None;
35 }
36 match &mut current {
37 None => {
38 current = Some(data.clone());
39 expanded.push(Value::Object(data));
40 }
41 Some(cur) => {
42 for (k, val) in data {
43 if val == Value::String("__unset".into()) {
44 cur.shift_remove(&k);
45 } else {
46 cur.insert(k, val);
47 }
48 }
49 expanded.push(Value::Object(cur.clone()));
50 }
51 }
52 }
53 expanded
54}
55
56pub fn branch_alias(config: &Map<String, Value>) -> Option<String> {
58 let version = config.get("version")?.as_str()?;
59 if !version.starts_with("dev-") && !version.ends_with("-dev") {
60 return None;
61 }
62 if let Some(map) = config
63 .get("extra")
64 .and_then(|e| e.get("branch-alias"))
65 .and_then(Value::as_object)
66 {
67 for (source, target) in map {
68 let Some(target) = target.as_str() else {
69 continue;
70 };
71 if !target.ends_with("-dev") {
72 continue;
73 }
74 let validated = if target == DEFAULT_BRANCH_ALIAS {
75 target.to_owned()
76 } else {
77 normalize_branch(&target[..target.len() - 4])
78 };
79 if !validated.ends_with("-dev") {
80 continue;
81 }
82 if version.to_lowercase() != source.to_lowercase() {
83 continue;
84 }
85 if let (Some(sp), Some(tp)) = (
86 parse_numeric_alias_prefix(source),
87 parse_numeric_alias_prefix(target),
88 ) {
89 if !tp.to_lowercase().starts_with(&sp.to_lowercase()) {
90 continue;
91 }
92 }
93 return Some(validated);
94 }
95 }
96 if config.get("default-branch") == Some(&Value::Bool(true)) {
97 let v = version.strip_prefix('v').unwrap_or(version);
98 if parse_numeric_alias_prefix(v).is_none() {
99 return Some(DEFAULT_BRANCH_ALIAS.to_owned());
100 }
101 }
102 None
103}
104
105pub fn pretty_alias(normalized: &str) -> String {
107 const X: &str = ".9999999";
108 let mut out = String::new();
109 let mut rest = normalized;
110 while let Some(i) = rest.find(X) {
111 out.push_str(&rest[..i]);
112 out.push_str(".x");
113 rest = &rest[i + X.len()..];
114 while let Some(r) = rest.strip_prefix(X) {
115 rest = r;
116 }
117 }
118 out.push_str(rest);
119 out
120}
121
122fn php_string(v: &Value) -> String {
124 match v {
125 Value::String(s) => s.clone(),
126 Value::Bool(true) => "1".to_owned(),
127 Value::Bool(false) | Value::Null => String::new(),
128 Value::Number(n) => match n.as_f64() {
129 Some(f) if n.is_f64() && f.fract() == 0.0 && f.abs() < 1e15 => format!("{}", f as i64),
130 _ => n.to_string(),
131 },
132 other => other.to_string(),
133 }
134}
135
136fn source_ref(name: &str, key: &str, v: Option<&Value>) -> Result<Option<SourceRef>, LoadError> {
139 let Some(v) = v.filter(|v| !v.is_null()) else {
141 return Ok(None);
142 };
143 let o = v.as_object();
144 let has = |k: &str| o.is_some_and(|o| o.get(k).is_some_and(|x| !x.is_null()));
145 let ok = if key == "source" {
146 has("type") && has("url") && has("reference")
147 } else {
148 has("type") && has("url")
149 };
150 if !ok {
151 let shape = if key == "source" {
152 "{\"type\": ..., \"url\": ..., \"reference\": ...}"
153 } else {
154 "{\"type\": ..., \"url\": ..., \"reference\": ..., \"shasum\": ...}"
155 };
156 return Err(LoadError(format!(
157 "Package {name}'s {key} key should be specified as {shape},\n{v} given."
158 )));
159 }
160 let o = o.expect("object checked above");
161 Ok(Some(SourceRef {
162 kind: php_string(&o["type"]),
163 url: php_string(&o["url"]),
164 reference: o.get("reference").filter(|r| !r.is_null()).map(php_string),
165 }))
166}
167
168fn parse_links(
173 source: &str,
174 source_version: &str,
175 kind: LinkType,
176 map: Option<&Value>,
177 batch: bool,
178) -> Result<Links, LoadError> {
179 let mut out = Links::default();
180 let Some(obj) = map.and_then(Value::as_object) else {
181 return Ok(out);
182 };
183 for (target, constraint) in obj {
184 let target = target.to_lowercase();
185 if batch && target == source {
186 continue;
187 }
188 let Some(pretty) = constraint.as_str() else {
189 if batch {
190 return Err(LoadError(format!(
191 "Link constraint in {source} {} > {target} should be a string, got {}",
192 kind.description(),
193 match constraint {
194 Value::Null => "null".to_owned(),
195 Value::Bool(_) => "bool".to_owned(),
196 Value::Number(n) =>
197 if n.is_f64() {
198 "float".to_owned()
199 } else {
200 "int".to_owned()
201 },
202 Value::Array(_) | Value::Object(_) => "array".to_owned(),
203 Value::String(_) => unreachable!(),
204 }
205 )));
206 }
207 continue;
208 };
209 let text = if pretty == "self.version" {
210 source_version
211 } else {
212 pretty
213 };
214 let parsed = parse_constraints(text).map_err(|e| {
215 LoadError(format!(
216 "Link constraint in {source} {} > {target} should be a valid version constraint, got \"{text}\": {}",
217 kind.description(),
218 e.0
219 ))
220 })?;
221 out.insert(Link::new(source, &target, parsed.constraint, pretty, kind));
222 }
223 Ok(out)
224}
225
226pub fn load(
230 config: &Value,
231 origin: Origin,
232 batch: bool,
233) -> Result<(Package, Option<(String, String)>), LoadError> {
234 let obj = config
235 .as_object()
236 .ok_or_else(|| LoadError("package config is not an object".into()))?;
237 let name = obj
238 .get("name")
239 .and_then(Value::as_str)
240 .ok_or_else(|| LoadError(format!("Unknown package has no name defined ({config}).")))?;
241 let pretty_version = match obj.get("version") {
242 Some(v @ (Value::String(_) | Value::Number(_) | Value::Bool(_))) => php_string(v),
243 _ => return Err(LoadError(format!("Package {name} has no version defined."))),
244 };
245 let version = match obj.get("version_normalized").and_then(Value::as_str) {
246 Some(v) if v == DEFAULT_BRANCH_ALIAS => {
247 normalize(&pretty_version, None).map_err(|e: VersionError| {
248 LoadError(format!(
249 "Failed to normalize version for package \"{name}\": {}",
250 e.0
251 ))
252 })?
253 }
254 Some(v) => v.to_owned(),
255 None => normalize(&pretty_version, None).map_err(|e: VersionError| {
256 LoadError(format!(
257 "Failed to normalize version for package \"{name}\": {}",
258 e.0
259 ))
260 })?,
261 };
262 let mut p = Package::new(name, &version, &pretty_version, origin);
263 p.package_type = obj
264 .get("type")
265 .and_then(Value::as_str)
266 .map(str::to_lowercase)
267 .unwrap_or_else(|| "library".to_owned());
268 p.is_default_branch = obj.get("default-branch") == Some(&Value::Bool(true));
269 p.source = source_ref(name, "source", obj.get("source"))?;
270 p.dist = source_ref(name, "dist", obj.get("dist"))?;
271 p.stability = parse_stability(&version);
272 let src = p.name.clone();
273 p.requires = parse_links(
274 &src,
275 &pretty_version,
276 LinkType::Require,
277 obj.get("require"),
278 batch,
279 )?;
280 p.conflicts = parse_links(
281 &src,
282 &pretty_version,
283 LinkType::Conflict,
284 obj.get("conflict"),
285 batch,
286 )?;
287 p.provides = parse_links(
288 &src,
289 &pretty_version,
290 LinkType::Provide,
291 obj.get("provide"),
292 batch,
293 )?;
294 p.replaces = parse_links(
295 &src,
296 &pretty_version,
297 LinkType::Replace,
298 obj.get("replace"),
299 batch,
300 )?;
301 p.dev_requires = parse_links(
302 &src,
303 &pretty_version,
304 LinkType::DevRequire,
305 obj.get("require-dev"),
306 batch,
307 )?;
308 p.raw = config.clone();
309 let alias = branch_alias(obj).map(|normalized| {
310 let pretty = pretty_alias(&normalized);
311 (normalized, pretty)
312 });
313 Ok((p, alias))
314}
315
316pub fn load_packages(
322 configs: &[Value],
323 origin: Origin,
324 arena: &mut Vec<Package>,
325 batch: bool,
326) -> Result<Vec<usize>, LoadError> {
327 let mut out = Vec::new();
328 for c in configs {
329 let (p, alias) = load(c, origin, batch)?;
330 let idx = arena.len();
331 arena.push(p);
332 if let Some((normalized, pretty)) = alias {
333 let a = arena[idx].alias(idx, &normalized, &pretty);
334 let aidx = arena.len();
335 arena.push(a);
336 out.push(aidx);
337 }
338 out.push(idx);
339 }
340 Ok(out)
341}
342
343#[cfg(test)]
344mod tests {
345 use super::*;
346 use serde_json::json;
347
348 #[test]
349 fn expands_minified_metadata() {
350 let v = vec![
351 json!({"name": "a/b", "version": "1.1.0", "require": {"php": ">=8"}, "time": "t1"}),
352 json!({"version": "1.0.0", "time": "__unset"}),
353 ];
354 let e = expand_minified(&v);
355 assert_eq!(e[1]["version"], "1.0.0");
356 assert_eq!(e[1]["require"]["php"], ">=8");
357 assert!(e[1].get("time").is_none());
358 assert_eq!(e[0]["time"], "t1");
359 }
360
361 #[test]
362 fn loads_package_with_alias_and_self_version() {
363 let cfg = json!({"name": "Acme/Lib", "version": "dev-main", "default-branch": true,
364 "replace": {"acme/old": "self.version"}, "require": {"php": "^8.1"}});
365 let mut arena = Vec::new();
366 let ids = load_packages(&[cfg], Origin::Repository(0), &mut arena, false).unwrap();
367 assert_eq!(
368 ids,
369 vec![1, 0],
370 "alias first, like ArrayRepository::addPackage"
371 );
372 let p = &arena[0];
373 assert_eq!(p.name, "acme/lib");
374 assert_eq!(p.version, "dev-main");
375 assert_eq!(
376 p.replaces.get("acme/old").unwrap().constraint.to_string(),
377 "== dev-main"
378 );
379 let a = &arena[1];
380 assert_eq!(a.version, "9999999-dev");
381 assert_eq!(a.pretty_version, "9999999-dev");
382 assert_eq!(a.alias_of, Some(0));
383 assert_eq!(a.replaces.0.len(), 2);
385 assert_eq!(a.replaces.0[1].constraint.to_string(), "== 9999999-dev");
386 assert_eq!(a.replaces.0[1].pretty_constraint, "dev-main");
387 let cfg = json!({"name": "acme/lib", "version": "1.0.0", "replace": {"acme/lib": "self.version", "acme/old": "1.0"}});
390 let mut arena = Vec::new();
391 load_packages(
392 std::slice::from_ref(&cfg),
393 Origin::Repository(0),
394 &mut arena,
395 true,
396 )
397 .unwrap();
398 assert_eq!(
399 arena[0]
400 .replaces
401 .0
402 .iter()
403 .map(|l| l.target.as_str())
404 .collect::<Vec<_>>(),
405 vec!["acme/old"]
406 );
407 load_packages(&[cfg], Origin::Locked, &mut arena, false).unwrap();
408 assert_eq!(arena[1].replaces.len(), 2);
409 assert_eq!(pretty_alias("1.9999999.9999999.9999999-dev"), "1.x-dev");
410 assert_eq!(pretty_alias("2.0.9999999.9999999-dev"), "2.0.x-dev");
411 }
412}