aboutsummaryrefslogtreecommitdiffhomepage
path: root/crates/mozart/src/packagist.rs
diff options
context:
space:
mode:
authornsfisis <nsfisis@gmail.com>2026-02-22 12:43:46 +0900
committernsfisis <nsfisis@gmail.com>2026-02-22 12:43:46 +0900
commitf2386320d1934f7e52b4cda36d19c86c239423b0 (patch)
tree68d55589be4be2f6bc423c02fa87778fcbf3740a /crates/mozart/src/packagist.rs
parent1ef1ebdcf50ae1358ec06e3c6a2fb797a8461617 (diff)
downloadphp-mozart-f2386320d1934f7e52b4cda36d19c86c239423b0.tar.gz
php-mozart-f2386320d1934f7e52b4cda36d19c86c239423b0.tar.zst
php-mozart-f2386320d1934f7e52b4cda36d19c86c239423b0.zip
chore: remove unused files
Diffstat (limited to 'crates/mozart/src/packagist.rs')
-rw-r--r--crates/mozart/src/packagist.rs629
1 files changed, 0 insertions, 629 deletions
diff --git a/crates/mozart/src/packagist.rs b/crates/mozart/src/packagist.rs
deleted file mode 100644
index 2503255..0000000
--- a/crates/mozart/src/packagist.rs
+++ /dev/null
@@ -1,629 +0,0 @@
-use mozart_registry::cache::Cache;
-use serde::{Deserialize, Serialize};
-use std::collections::BTreeMap;
-
-#[derive(Debug, Clone, Deserialize)]
-pub struct PackagistDist {
- #[serde(rename = "type")]
- pub dist_type: String,
- pub url: String,
- pub reference: Option<String>,
- pub shasum: Option<String>,
-}
-
-#[derive(Debug, Clone, Deserialize)]
-pub struct PackagistSource {
- #[serde(rename = "type")]
- pub source_type: String,
- pub url: String,
- pub reference: Option<String>,
-}
-
-#[derive(Debug, Clone, Deserialize)]
-pub struct PackagistVersion {
- pub version: String,
- pub version_normalized: String,
- #[serde(default)]
- pub require: BTreeMap<String, String>,
- #[serde(default)]
- pub replace: BTreeMap<String, String>,
- #[serde(default)]
- pub provide: BTreeMap<String, String>,
- #[serde(default)]
- pub conflict: BTreeMap<String, String>,
- pub dist: Option<PackagistDist>,
- pub source: Option<PackagistSource>,
-
- #[serde(rename = "require-dev", default)]
- pub require_dev: BTreeMap<String, String>,
-
- #[serde(default)]
- pub suggest: Option<BTreeMap<String, String>>,
-
- #[serde(rename = "type")]
- pub package_type: Option<String>,
-
- pub autoload: Option<serde_json::Value>,
-
- #[serde(rename = "autoload-dev")]
- pub autoload_dev: Option<serde_json::Value>,
-
- pub license: Option<Vec<String>>,
-
- pub description: Option<String>,
-
- pub homepage: Option<String>,
-
- pub keywords: Option<Vec<String>>,
-
- pub authors: Option<Vec<serde_json::Value>>,
-
- pub support: Option<serde_json::Value>,
-
- pub funding: Option<Vec<serde_json::Value>>,
-
- pub time: Option<String>,
-
- pub extra: Option<serde_json::Value>,
-
- #[serde(rename = "notification-url")]
- pub notification_url: Option<String>,
-}
-
-impl PackagistVersion {
- /// Extract the `extra.branch-alias` map from this version's metadata.
- ///
- /// Composer packages can declare branch aliases in `extra.branch-alias`:
- /// ```json
- /// {
- /// "extra": {
- /// "branch-alias": {
- /// "dev-master": "2.x-dev"
- /// }
- /// }
- /// }
- /// ```
- ///
- /// Returns a map from branch name (e.g. `"dev-master"`) to alias target
- /// (e.g. `"2.x-dev"`). Returns an empty map when no aliases are declared.
- pub fn branch_aliases(&self) -> BTreeMap<String, String> {
- let Some(extra) = &self.extra else {
- return BTreeMap::new();
- };
-
- let Some(branch_alias) = extra.get("branch-alias") else {
- return BTreeMap::new();
- };
-
- let Some(map) = branch_alias.as_object() else {
- return BTreeMap::new();
- };
-
- map.iter()
- .filter_map(|(k, v)| v.as_str().map(|s| (k.clone(), s.to_string())))
- .collect()
- }
-}
-
-/// Parse a Packagist p2 API JSON response.
-///
-/// The response format is: `{"packages": {"vendor/package": [...]}}`.
-pub fn parse_p2_response(json: &str, package_name: &str) -> anyhow::Result<Vec<PackagistVersion>> {
- #[derive(Deserialize)]
- struct P2Response {
- packages: BTreeMap<String, Vec<PackagistVersion>>,
- }
-
- let response: P2Response = serde_json::from_str(json)?;
- response
- .packages
- .into_iter()
- .find(|(key, _)| key == package_name)
- .map(|(_, versions)| versions)
- .ok_or_else(|| anyhow::anyhow!("Package \"{package_name}\" not found in response"))
-}
-
-/// Fetch package version metadata from the Packagist p2 API.
-///
-/// If `repo_cache` is provided, the JSON response is cached on disk under the
-/// key `"provider-{vendor}~{package}.json"`. Subsequent calls for the same
-/// package are served from cache without a network request.
-pub fn fetch_package_versions(
- package_name: &str,
- repo_cache: Option<&Cache>,
-) -> anyhow::Result<Vec<PackagistVersion>> {
- // Build cache key: replace `/` with `~` per cache key convention
- let cache_key = format!("provider-{}.json", package_name.replace('/', "~"));
-
- // Check cache first
- if let Some(cache) = repo_cache
- && let Some(cached) = cache.read(&cache_key)
- {
- return parse_p2_response(&cached, package_name);
- }
-
- // Cache miss — fetch from Packagist
- let url = format!("https://repo.packagist.org/p2/{package_name}.json");
- let response = reqwest::blocking::get(&url)?;
-
- if !response.status().is_success() {
- anyhow::bail!(
- "Failed to fetch package \"{package_name}\" from Packagist (HTTP {})",
- response.status()
- );
- }
-
- let body = response.text()?;
-
- // Write to cache
- if let Some(cache) = repo_cache {
- let _ = cache.write(&cache_key, &body);
- }
-
- parse_p2_response(&body, package_name)
-}
-
-// ─────────────────────────────────────────────────────────────────────────────
-// Packagist search API
-// ─────────────────────────────────────────────────────────────────────────────
-
-/// A single search result from the Packagist search API.
-#[derive(Debug, Deserialize, Serialize, Clone)]
-pub struct SearchResult {
- pub name: String,
- pub description: String,
- pub url: String,
- pub repository: Option<String>,
- pub downloads: u64,
- pub favers: u64,
-}
-
-#[derive(Debug, Deserialize)]
-pub struct SearchResponse {
- pub results: Vec<SearchResult>,
- pub total: u64,
- pub next: Option<String>,
-}
-
-/// Maximum number of pages to fetch from the Packagist search API.
-const SEARCH_MAX_PAGES: usize = 20;
-
-/// Percent-encode a string for use in a URL query parameter value.
-fn url_encode(s: &str) -> String {
- let mut encoded = String::with_capacity(s.len());
- for byte in s.bytes() {
- match byte {
- b'A'..=b'Z' | b'a'..=b'z' | b'0'..=b'9' | b'-' | b'_' | b'.' | b'~' => {
- encoded.push(byte as char);
- }
- b' ' => encoded.push_str("%20"),
- other => {
- encoded.push_str(&format!("%{other:02X}"));
- }
- }
- }
- encoded
-}
-
-/// Search Packagist for packages matching `query`.
-///
-/// Fetches up to `SEARCH_MAX_PAGES` pages of results and returns the full list.
-/// An optional `package_type` filter can narrow results (e.g. `"library"`).
-pub fn search_packages(
- query: &str,
- package_type: Option<&str>,
-) -> anyhow::Result<(Vec<SearchResult>, u64)> {
- let client = reqwest::blocking::Client::builder()
- .user_agent("mozart/0.1.0")
- .build()?;
-
- let mut all_results: Vec<SearchResult> = Vec::new();
- let mut page = 1usize;
- let mut next_url: Option<String> = None;
- let mut total: u64 = 0;
-
- loop {
- let response: SearchResponse = if let Some(ref url) = next_url {
- let resp = client.get(url).send()?;
- if !resp.status().is_success() {
- anyhow::bail!("Packagist search request failed (HTTP {})", resp.status());
- }
- resp.json()?
- } else {
- let encoded_query = url_encode(query);
- let mut url = format!("https://packagist.org/search.json?q={encoded_query}");
- if let Some(t) = package_type {
- url.push_str("&type=");
- url.push_str(&url_encode(t));
- }
-
- let resp = client.get(&url).send()?;
- if !resp.status().is_success() {
- anyhow::bail!("Packagist search request failed (HTTP {})", resp.status());
- }
- resp.json()?
- };
-
- if page == 1 {
- total = response.total;
- }
-
- all_results.extend(response.results);
- next_url = response.next;
- page += 1;
-
- if next_url.is_none() || page > SEARCH_MAX_PAGES {
- break;
- }
- }
-
- Ok((all_results, total))
-}
-
-// ─────────────────────────────────────────────────────────────────────────────
-// Security Advisories API
-// ─────────────────────────────────────────────────────────────────────────────
-
-/// A single security advisory from the Packagist API.
-#[derive(Debug, Clone, Deserialize, Serialize)]
-pub struct SecurityAdvisory {
- #[serde(rename = "advisoryId")]
- pub advisory_id: String,
-
- #[serde(rename = "packageName")]
- pub package_name: String,
-
- #[serde(rename = "remoteId")]
- pub remote_id: String,
-
- pub title: String,
-
- pub link: Option<String>,
-
- pub cve: Option<String>,
-
- /// Composer version constraint string, e.g. ">=1.0,<1.5.1|>=2.0,<2.3"
- #[serde(rename = "affectedVersions")]
- pub affected_versions: String,
-
- pub source: String,
-
- #[serde(rename = "reportedAt")]
- pub reported_at: String,
-
- #[serde(rename = "composerRepository")]
- pub composer_repository: Option<String>,
-
- pub severity: Option<String>,
-
- #[serde(default)]
- pub sources: Vec<AdvisorySource>,
-}
-
-/// A source entry within a security advisory.
-#[derive(Debug, Clone, Deserialize, Serialize)]
-pub struct AdvisorySource {
- pub name: String,
- #[serde(rename = "remoteId")]
- pub remote_id: String,
-}
-
-/// Response from POST `https://packagist.org/api/security-advisories/`.
-#[derive(Debug, Deserialize)]
-pub struct SecurityAdvisoriesResponse {
- pub advisories: BTreeMap<String, Vec<SecurityAdvisory>>,
-}
-
-/// Fetch security advisories for the given package names from the Packagist API.
-///
-/// Sends a POST request to `https://packagist.org/api/security-advisories/`
-/// with form-encoded package names. Returns advisories grouped by package name.
-///
-/// If the package list is very large (500+), requests are batched in chunks of
-/// 500 names per request and the results are merged.
-pub fn fetch_security_advisories(
- package_names: &[&str],
-) -> anyhow::Result<BTreeMap<String, Vec<SecurityAdvisory>>> {
- let client = reqwest::blocking::Client::builder()
- .user_agent("mozart/0.1.0")
- .build()?;
-
- let mut all_advisories: BTreeMap<String, Vec<SecurityAdvisory>> = BTreeMap::new();
-
- for chunk in package_names.chunks(500) {
- // Build an application/x-www-form-urlencoded body manually.
- // Each package is encoded as `packages[]=<name>` and joined with `&`.
- let body: String = chunk
- .iter()
- .map(|name| format!("packages[]={}", url_encode(name)))
- .collect::<Vec<_>>()
- .join("&");
-
- let response = client
- .post("https://packagist.org/api/security-advisories/")
- .header("Content-Type", "application/x-www-form-urlencoded")
- .body(body)
- .send()?;
-
- if !response.status().is_success() {
- anyhow::bail!(
- "Packagist security advisories request failed (HTTP {})",
- response.status()
- );
- }
-
- let parsed: SecurityAdvisoriesResponse = response.json()?;
-
- for (pkg_name, advisories) in parsed.advisories {
- if !advisories.is_empty() {
- all_advisories
- .entry(pkg_name)
- .or_default()
- .extend(advisories);
- }
- }
- }
-
- Ok(all_advisories)
-}
-
-#[cfg(test)]
-mod tests {
- use super::*;
-
- #[test]
- fn parse_p2_response_basic() {
- let json = r#"{
- "packages": {
- "monolog/monolog": [
- {
- "version": "3.8.0",
- "version_normalized": "3.8.0.0",
- "require": {"php": ">=8.1"},
- "dist": {
- "type": "zip",
- "url": "https://example.com/monolog-3.8.0.zip",
- "reference": "abc123",
- "shasum": ""
- },
- "source": {
- "type": "git",
- "url": "https://github.com/Seldaek/monolog.git",
- "reference": "abc123"
- }
- },
- {
- "version": "3.7.0",
- "version_normalized": "3.7.0.0",
- "require": {"php": ">=8.1"}
- }
- ]
- }
- }"#;
-
- let versions = parse_p2_response(json, "monolog/monolog").unwrap();
- assert_eq!(versions.len(), 2);
- assert_eq!(versions[0].version, "3.8.0");
- assert_eq!(versions[0].version_normalized, "3.8.0.0");
- assert_eq!(versions[0].require.get("php").unwrap(), ">=8.1");
- assert!(versions[0].dist.is_some());
- assert!(versions[0].source.is_some());
- assert_eq!(versions[1].version, "3.7.0");
- assert!(versions[1].dist.is_none());
- }
-
- #[test]
- fn parse_p2_response_not_found() {
- let json = r#"{"packages": {"other/pkg": []}}"#;
- let result = parse_p2_response(json, "monolog/monolog");
- assert!(result.is_err());
- }
-
- #[test]
- fn parse_p2_response_with_dev_version() {
- let json = r#"{
- "packages": {
- "test/pkg": [
- {
- "version": "dev-master",
- "version_normalized": "dev-master",
- "require": {}
- },
- {
- "version": "1.0.0",
- "version_normalized": "1.0.0.0",
- "require": {}
- }
- ]
- }
- }"#;
-
- let versions = parse_p2_response(json, "test/pkg").unwrap();
- assert_eq!(versions.len(), 2);
- assert_eq!(versions[0].version, "dev-master");
- assert_eq!(versions[1].version, "1.0.0");
- }
-
- // ──────────── branch_aliases() tests ────────────
-
- #[test]
- fn test_branch_aliases_present() {
- let json = r#"{
- "packages": {
- "test/pkg": [
- {
- "version": "dev-master",
- "version_normalized": "dev-master",
- "require": {},
- "extra": {
- "branch-alias": {
- "dev-master": "2.x-dev"
- }
- }
- }
- ]
- }
- }"#;
-
- let versions = parse_p2_response(json, "test/pkg").unwrap();
- let aliases = versions[0].branch_aliases();
- assert_eq!(aliases.len(), 1);
- assert_eq!(aliases.get("dev-master").unwrap(), "2.x-dev");
- }
-
- #[test]
- fn test_branch_aliases_multiple() {
- let json = r#"{
- "packages": {
- "test/pkg": [
- {
- "version": "dev-master",
- "version_normalized": "dev-master",
- "require": {},
- "extra": {
- "branch-alias": {
- "dev-master": "2.x-dev",
- "dev-1.x": "1.5.x-dev"
- }
- }
- }
- ]
- }
- }"#;
-
- let versions = parse_p2_response(json, "test/pkg").unwrap();
- let aliases = versions[0].branch_aliases();
- assert_eq!(aliases.len(), 2);
- assert_eq!(aliases.get("dev-master").unwrap(), "2.x-dev");
- assert_eq!(aliases.get("dev-1.x").unwrap(), "1.5.x-dev");
- }
-
- #[test]
- fn test_branch_aliases_no_extra() {
- let json = r#"{
- "packages": {
- "test/pkg": [
- {
- "version": "dev-master",
- "version_normalized": "dev-master",
- "require": {}
- }
- ]
- }
- }"#;
-
- let versions = parse_p2_response(json, "test/pkg").unwrap();
- let aliases = versions[0].branch_aliases();
- assert!(aliases.is_empty());
- }
-
- #[test]
- fn test_branch_aliases_extra_without_branch_alias_key() {
- let json = r#"{
- "packages": {
- "test/pkg": [
- {
- "version": "dev-master",
- "version_normalized": "dev-master",
- "require": {},
- "extra": {
- "installer-name": "my-plugin"
- }
- }
- ]
- }
- }"#;
-
- let versions = parse_p2_response(json, "test/pkg").unwrap();
- let aliases = versions[0].branch_aliases();
- assert!(aliases.is_empty());
- }
-
- // ──────────── SecurityAdvisory parsing tests ─────────────────────────────
-
- #[test]
- fn test_parse_security_advisories_response() {
- let json = r#"{
- "advisories": {
- "monolog/monolog": [
- {
- "advisoryId": "PKSA-b2m0-qqf7-qck4",
- "packageName": "monolog/monolog",
- "remoteId": "monolog/monolog/2017-11-13-1.yaml",
- "title": "Header injection in NativeMailerHandler",
- "link": "https://github.com/Seldaek/monolog/pull/683",
- "cve": null,
- "affectedVersions": ">=1.8.0,<1.12.0",
- "source": "FriendsOfPHP/security-advisories",
- "reportedAt": "2017-11-13T00:00:00+00:00",
- "composerRepository": "https://packagist.org",
- "severity": "low",
- "sources": [
- {
- "name": "FriendsOfPHP/security-advisories",
- "remoteId": "monolog/monolog/2017-11-13-1.yaml"
- }
- ]
- }
- ]
- }
- }"#;
-
- let response: SecurityAdvisoriesResponse = serde_json::from_str(json).unwrap();
- assert_eq!(response.advisories.len(), 1);
- let advisories = response.advisories.get("monolog/monolog").unwrap();
- assert_eq!(advisories.len(), 1);
- let adv = &advisories[0];
- assert_eq!(adv.advisory_id, "PKSA-b2m0-qqf7-qck4");
- assert_eq!(adv.package_name, "monolog/monolog");
- assert_eq!(adv.title, "Header injection in NativeMailerHandler");
- assert_eq!(adv.affected_versions, ">=1.8.0,<1.12.0");
- assert_eq!(adv.severity.as_deref(), Some("low"));
- assert!(adv.cve.is_none());
- assert_eq!(adv.sources.len(), 1);
- assert_eq!(adv.sources[0].name, "FriendsOfPHP/security-advisories");
- }
-
- #[test]
- fn test_parse_security_advisories_empty() {
- let json = r#"{"advisories": {"other/package": []}}"#;
- let response: SecurityAdvisoriesResponse = serde_json::from_str(json).unwrap();
- assert_eq!(response.advisories.len(), 1);
- let advisories = response.advisories.get("other/package").unwrap();
- assert!(advisories.is_empty());
- }
-
- #[test]
- fn test_parse_security_advisories_null_fields() {
- let json = r#"{
- "advisories": {
- "vendor/pkg": [
- {
- "advisoryId": "PKSA-0000-0000-0000",
- "packageName": "vendor/pkg",
- "remoteId": "vendor/pkg/2024-01-01.yaml",
- "title": "Some vulnerability",
- "link": null,
- "cve": null,
- "affectedVersions": ">=1.0,<2.0",
- "source": "FriendsOfPHP/security-advisories",
- "reportedAt": "2024-01-01T00:00:00+00:00",
- "composerRepository": null,
- "severity": null,
- "sources": []
- }
- ]
- }
- }"#;
-
- let response: SecurityAdvisoriesResponse = serde_json::from_str(json).unwrap();
- let advisories = response.advisories.get("vendor/pkg").unwrap();
- assert_eq!(advisories.len(), 1);
- let adv = &advisories[0];
- assert!(adv.link.is_none());
- assert!(adv.cve.is_none());
- assert!(adv.severity.is_none());
- assert!(adv.composer_repository.is_none());
- assert!(adv.sources.is_empty());
- }
-}