aboutsummaryrefslogtreecommitdiffhomepage
path: root/crates/mozart-core/src
diff options
context:
space:
mode:
Diffstat (limited to 'crates/mozart-core/src')
-rw-r--r--crates/mozart-core/src/package.rs98
-rw-r--r--crates/mozart-core/src/platform.rs43
2 files changed, 131 insertions, 10 deletions
diff --git a/crates/mozart-core/src/package.rs b/crates/mozart-core/src/package.rs
index 18714ec..ccbda1f 100644
--- a/crates/mozart-core/src/package.rs
+++ b/crates/mozart-core/src/package.rs
@@ -1,5 +1,7 @@
+use serde::de::{Deserializer, MapAccess, SeqAccess, Visitor};
use serde::{Deserialize, Serialize};
use std::collections::BTreeMap;
+use std::fmt;
use std::fs;
use std::path::Path;
@@ -505,7 +507,15 @@ pub struct RawPackageData {
#[serde(default, skip_serializing_if = "BTreeMap::is_empty")]
pub replace: BTreeMap<String, String>,
- #[serde(default, skip_serializing_if = "Vec::is_empty")]
+ /// `repositories` accepts either a JSON array or a JSON object keyed by
+ /// repository name. Composer iterates `foreach ($repoConfigs as ...)` in
+ /// `RepositoryFactory::createRepos`, so PHP transparently handles either
+ /// shape; mirror that here.
+ #[serde(
+ default,
+ deserialize_with = "deserialize_repositories",
+ skip_serializing_if = "Vec::is_empty"
+ )]
pub repositories: Vec<RawRepository>,
#[serde(skip_serializing_if = "Option::is_none")]
@@ -587,6 +597,51 @@ fn default_root_package_name() -> String {
"__root__".to_string()
}
+/// Deserialize `repositories` from either a JSON array or a JSON object.
+/// PHP's `json_decode($x, true)` produces an associative array in either
+/// case, and `RepositoryFactory::createRepos` iterates the values without
+/// caring whether the key was an int (array) or a string (object). The map
+/// keys are dropped — `RawRepository` doesn't carry a name field, and
+/// downstream code doesn't depend on the original keying.
+fn deserialize_repositories<'de, D>(deserializer: D) -> Result<Vec<RawRepository>, D::Error>
+where
+ D: Deserializer<'de>,
+{
+ struct RepositoriesVisitor;
+
+ impl<'de> Visitor<'de> for RepositoriesVisitor {
+ type Value = Vec<RawRepository>;
+
+ fn expecting(&self, f: &mut fmt::Formatter) -> fmt::Result {
+ f.write_str("a sequence or map of repository definitions")
+ }
+
+ fn visit_seq<A>(self, mut seq: A) -> Result<Self::Value, A::Error>
+ where
+ A: SeqAccess<'de>,
+ {
+ let mut out = Vec::with_capacity(seq.size_hint().unwrap_or(0));
+ while let Some(repo) = seq.next_element::<RawRepository>()? {
+ out.push(repo);
+ }
+ Ok(out)
+ }
+
+ fn visit_map<A>(self, mut map: A) -> Result<Self::Value, A::Error>
+ where
+ A: MapAccess<'de>,
+ {
+ let mut out = Vec::with_capacity(map.size_hint().unwrap_or(0));
+ while let Some((_key, repo)) = map.next_entry::<String, RawRepository>()? {
+ out.push(repo);
+ }
+ Ok(out)
+ }
+ }
+
+ deserializer.deserialize_any(RepositoriesVisitor)
+}
+
impl RawPackageData {
pub fn new(name: String) -> Self {
Self {
@@ -828,4 +883,45 @@ mod tests {
let raw = RawPackageData::new("foo/bar".to_string());
assert!(raw.validate_root_does_not_self_require().is_ok());
}
+
+ #[test]
+ fn raw_repositories_array_form() {
+ let json = r#"{
+ "name": "test/array",
+ "repositories": [
+ {"type": "vcs", "url": "https://example.com/a"},
+ {"type": "vcs", "url": "https://example.com/b"}
+ ]
+ }"#;
+ let raw: RawPackageData = serde_json::from_str(json).unwrap();
+ assert_eq!(raw.repositories.len(), 2);
+ assert_eq!(
+ raw.repositories[0].url.as_deref(),
+ Some("https://example.com/a")
+ );
+ assert_eq!(
+ raw.repositories[1].url.as_deref(),
+ Some("https://example.com/b")
+ );
+ }
+
+ #[test]
+ fn raw_repositories_object_form() {
+ let json = r#"{
+ "name": "test/object",
+ "repositories": {
+ "first": {"type": "vcs", "url": "https://example.com/a"},
+ "second": {"type": "package", "package": {"name": "x/y", "version": "1.0.0"}}
+ }
+ }"#;
+ let raw: RawPackageData = serde_json::from_str(json).unwrap();
+ assert_eq!(raw.repositories.len(), 2);
+ assert_eq!(raw.repositories[0].repo_type, "vcs");
+ assert_eq!(
+ raw.repositories[0].url.as_deref(),
+ Some("https://example.com/a")
+ );
+ assert_eq!(raw.repositories[1].repo_type, "package");
+ assert!(raw.repositories[1].package.is_some());
+ }
}
diff --git a/crates/mozart-core/src/platform.rs b/crates/mozart-core/src/platform.rs
index 819d8c9..47ce2a0 100644
--- a/crates/mozart-core/src/platform.rs
+++ b/crates/mozart-core/src/platform.rs
@@ -16,17 +16,32 @@ pub struct PlatformPackage {
/// Returns true if the package name is a Composer platform package.
///
-/// Platform packages include: php, php-*, ext-*, lib-*, composer,
-/// composer-plugin-api, composer-runtime-api.
+/// Mirrors `Composer\Repository\PlatformRepository::PLATFORM_PACKAGE_REGEX`:
+/// `php`, `php-64bit`, `php-ipv6`, `php-zts`, `php-debug`, `hhvm`,
+/// `ext-<name>`, `lib-<name>`, `composer`, `composer-plugin-api`,
+/// `composer-runtime-api`. The `php-` family is a closed set, NOT a wildcard
+/// — `php-http/client-common` is a regular vendor package, not a platform
+/// requirement.
pub fn is_platform_package(name: &str) -> bool {
let lower = name.to_lowercase();
- lower == "php"
- || lower.starts_with("php-")
- || lower.starts_with("ext-")
- || lower.starts_with("lib-")
- || lower == "composer"
- || lower == "composer-plugin-api"
- || lower == "composer-runtime-api"
+ match lower.as_str() {
+ "php"
+ | "php-64bit"
+ | "php-ipv6"
+ | "php-zts"
+ | "php-debug"
+ | "hhvm"
+ | "composer"
+ | "composer-plugin-api"
+ | "composer-runtime-api" => true,
+ _ => {
+ (lower.starts_with("ext-") || lower.starts_with("lib-"))
+ && lower[4..]
+ .chars()
+ .next()
+ .is_some_and(|c| c.is_ascii_alphanumeric())
+ }
+ }
}
// ─── Detection ───────────────────────────────────────────────────────────────
@@ -378,6 +393,16 @@ mod tests {
}
#[test]
+ fn test_is_platform_package_php_prefix_vendor() {
+ // Vendor packages whose names happen to start with "php-" are NOT
+ // platform packages. Composer's PLATFORM_PACKAGE_REGEX limits the
+ // `php-` family to a closed set of suffixes (64bit/ipv6/zts/debug).
+ assert!(!is_platform_package("php-http/client-common"));
+ assert!(!is_platform_package("php-http/httplug"));
+ assert!(!is_platform_package("php-amqplib/php-amqplib"));
+ }
+
+ #[test]
fn test_parse_platform_info_basic() {
let output = "PHP_VERSION:8.2.1\nPHP_INT_SIZE:8\nPHP_DEBUG:0\nPHP_ZTS:0\nIPV6:1\nEXTENSIONS:\njson:8.2.1\nctype:8.2.1\n";
let packages = parse_platform_info(output);