diff options
| author | nsfisis <nsfisis@gmail.com> | 2026-02-22 12:43:46 +0900 |
|---|---|---|
| committer | nsfisis <nsfisis@gmail.com> | 2026-02-22 12:43:46 +0900 |
| commit | f2386320d1934f7e52b4cda36d19c86c239423b0 (patch) | |
| tree | 68d55589be4be2f6bc423c02fa87778fcbf3740a /crates/mozart/src/packagist.rs | |
| parent | 1ef1ebdcf50ae1358ec06e3c6a2fb797a8461617 (diff) | |
| download | php-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.rs | 629 |
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()); - } -} |
