1use crate::types::{ComposerPackage, ComposerVersion};
12use deps_core::{
13 Deprecation, DepsError, HttpCache, Result, is_dot_segment, lsp_helpers::warn_rejected_value,
14};
15use serde::Deserialize;
16use std::any::Any;
17use std::sync::Arc;
18
19const PACKAGIST_BASE: &str = "https://repo.packagist.org";
20const PACKAGIST_SEARCH: &str = "https://packagist.org/search.json";
21const PACKAGIST_WEB: &str = "https://packagist.org/packages";
22
23pub fn package_url(name: &str) -> String {
33 if let Some((vendor, package)) = name.split_once('/') {
34 format!(
35 "{PACKAGIST_WEB}/{}/{}",
36 urlencoding::encode(vendor),
37 urlencoding::encode(package)
38 )
39 } else {
40 format!("{PACKAGIST_WEB}/{}", urlencoding::encode(name))
41 }
42}
43
44pub const REGISTRY: &str = "Packagist";
46
47fn p2_url(base: &str, name: &str) -> String {
58 if let Some((vendor, package)) = name.split_once('/') {
59 format!(
60 "{base}/p2/{}/{}.json",
61 urlencoding::encode(vendor),
62 urlencoding::encode(package)
63 )
64 } else {
65 format!("{base}/p2/{}.json", urlencoding::encode(name))
66 }
67}
68
69fn has_dot_segment(name: &str) -> bool {
73 if let Some((vendor, package)) = name.split_once('/') {
74 return is_dot_segment(vendor) || is_dot_segment(package);
75 }
76 is_dot_segment(name)
77}
78
79fn reject_dot_segment(name: &str) -> Result<()> {
82 if has_dot_segment(name) {
83 warn_rejected_value("is_dot_segment", "Packagist p2 metadata request URL", name);
84 return Err(DepsError::PackageNotFound {
85 package: name.to_string(),
86 registry: REGISTRY,
87 });
88 }
89 Ok(())
90}
91
92fn select_latest_for_existence_composer<T>(
120 versions: &[T],
121 as_version: impl Fn(&T) -> &dyn deps_core::Version,
122 minimum_rank: u8,
123) -> Option<usize> {
124 if versions.is_empty() {
125 return None;
126 }
127 Some(
128 versions
129 .iter()
130 .position(|v| {
131 crate::formatter::composer_version_stability_rank(
132 as_version(v).version_string().as_str(),
133 ) >= minimum_rank
134 })
135 .or_else(|| {
136 versions
137 .iter()
138 .position(|v| !as_version(v).removal_status().blocks_resolution())
139 })
140 .unwrap_or(0),
141 )
142}
143
144fn compound_stability_flag_rank(req_str: &str) -> Option<u8> {
161 req_str
162 .split("||")
163 .flat_map(str::split_whitespace)
164 .filter_map(|token| {
165 let (_, flag) = crate::formatter::strip_stability_flag(token.trim());
166 flag.map(crate::formatter::composer_stability_rank)
167 })
168 .min()
169}
170
171pub(crate) fn effective_minimum_stability_rank(
191 req_str: &str,
192 manifest_minimum: Option<&str>,
193) -> u8 {
194 let trimmed = req_str.trim();
195 if let Some(rank) = compound_stability_flag_rank(trimmed) {
196 return rank;
197 }
198 if crate::types::is_prerelease_marker(trimmed) {
199 return 0;
200 }
201 manifest_minimum.map_or(crate::formatter::COMPOSER_STABLE_RANK, |s| {
202 crate::formatter::composer_stability_rank(s)
203 })
204}
205
206#[derive(Clone)]
211pub struct PackagistRegistry {
212 cache: Arc<HttpCache>,
213 base: String,
214}
215
216impl PackagistRegistry {
217 pub fn new(cache: Arc<HttpCache>) -> Self {
219 Self::with_registry_base(cache, PACKAGIST_BASE.to_string())
220 }
221
222 fn with_registry_base(cache: Arc<HttpCache>, base: String) -> Self {
225 Self { cache, base }
226 }
227
228 pub async fn get_versions(&self, name: &str) -> Result<Vec<ComposerVersion>> {
237 reject_dot_segment(name)?;
238 let url = p2_url(&self.base, name);
239 let data = self.cache.get_cached(&url).await?;
240 parse_package_metadata(name, &data)
241 }
242
243 pub async fn get_latest_matching(
263 &self,
264 name: &str,
265 req_str: &str,
266 ) -> Result<Option<ComposerVersion>> {
267 self.get_latest_matching_impl(name, req_str, None).await
268 }
269
270 pub async fn get_latest_matching_for_manifest(
283 &self,
284 name: &str,
285 req_str: &str,
286 minimum_stability: Option<&str>,
287 ) -> Result<Option<ComposerVersion>> {
288 self.get_latest_matching_impl(name, req_str, minimum_stability)
289 .await
290 }
291
292 async fn get_latest_matching_impl(
293 &self,
294 name: &str,
295 req_str: &str,
296 manifest_minimum: Option<&str>,
297 ) -> Result<Option<ComposerVersion>> {
298 let versions = self.get_versions(name).await?;
299
300 let minimum_rank = effective_minimum_stability_rank(req_str, manifest_minimum);
301
302 if deps_core::is_existence_wildcard_str(req_str) {
303 let idx = select_latest_for_existence_composer(
304 &versions,
305 |v| v as &dyn deps_core::Version,
306 minimum_rank,
307 );
308 return Ok(idx.and_then(|idx| versions.into_iter().nth(idx)));
309 }
310
311 let formatter = crate::formatter::ComposerFormatter;
312 use deps_core::lsp_helpers::RequirementResolution;
313
314 Ok(versions.into_iter().find(|v| {
315 crate::formatter::composer_version_stability_rank(v.version.as_str()) >= minimum_rank
316 && formatter.version_satisfies_requirement(&v.version, req_str)
317 }))
318 }
319
320 #[must_use]
329 pub fn select_latest_matching_for_manifest(
330 &self,
331 versions: &[Box<dyn deps_core::Version>],
332 req: &deps_core::VersionReq,
333 minimum_stability: Option<&str>,
334 ) -> Option<usize> {
335 self.select_latest_matching_impl(versions, req, minimum_stability)
336 }
337
338 fn select_latest_matching_impl(
339 &self,
340 versions: &[Box<dyn deps_core::Version>],
341 req: &deps_core::VersionReq,
342 manifest_minimum: Option<&str>,
343 ) -> Option<usize> {
344 let minimum_rank = effective_minimum_stability_rank(req.as_str(), manifest_minimum);
345
346 if deps_core::is_existence_wildcard(req) {
347 return select_latest_for_existence_composer(versions, |v| v.as_ref(), minimum_rank);
348 }
349
350 let formatter = crate::formatter::ComposerFormatter;
351 use deps_core::lsp_helpers::RequirementResolution;
352
353 versions.iter().position(|v| {
354 !v.removal_status().blocks_resolution()
357 && crate::formatter::composer_version_stability_rank(v.version_string().as_str())
358 >= minimum_rank
359 && formatter.version_satisfies_requirement(v.version_string(), req.as_str())
360 })
361 }
362
363 pub async fn search(&self, query: &str, limit: usize) -> Result<Vec<ComposerPackage>> {
371 let url = format!(
372 "{}?q={}&per_page={}",
373 PACKAGIST_SEARCH,
374 urlencoding::encode(query),
375 limit
376 );
377
378 let data = self.cache.get_cached(&url).await?;
379 parse_search_response(&data)
380 }
381}
382
383#[derive(Deserialize)]
385struct PackagistResponse {
386 packages: std::collections::HashMap<String, Vec<MinifiedVersion>>,
387}
388
389#[derive(Deserialize, Clone, Default)]
399struct MinifiedVersion {
400 version: Option<String>,
401 version_normalized: Option<String>,
402 abandoned: Option<serde_json::Value>,
403 #[serde(default)]
405 time: Option<String>,
406}
407
408fn expand_minified_versions(entries: Vec<MinifiedVersion>) -> Vec<ComposerVersion> {
419 let mut result = Vec::new();
420 let mut current = MinifiedVersion::default();
421
422 for entry in entries {
423 let published_at = entry
426 .time
427 .as_deref()
428 .and_then(deps_core::PublishTime::parse_rfc3339);
429
430 if entry.version.is_some() {
432 current.version = entry.version;
433 }
434 if entry.version_normalized.is_some() {
435 current.version_normalized = entry.version_normalized;
436 }
437 if entry.abandoned.is_some() {
438 current.abandoned = entry.abandoned;
439 }
440
441 let Some(ref version) = current.version else {
442 continue;
443 };
444
445 if version.starts_with("dev-") || version.ends_with("-dev") {
447 continue;
448 }
449
450 let abandoned = current
451 .abandoned
452 .as_ref()
453 .is_some_and(|v| v.as_bool() == Some(true) || v.is_string());
454 let deprecation = deprecation_from_abandoned(current.abandoned.as_ref());
455
456 result.push(ComposerVersion {
457 version: version.clone().into(),
458 version_normalized: current
459 .version_normalized
460 .clone()
461 .unwrap_or_else(|| version.clone()),
462 abandoned,
463 deprecation,
464 published_at,
465 });
466 }
467
468 result
469}
470
471fn deprecation_from_abandoned(abandoned: Option<&serde_json::Value>) -> Option<Deprecation> {
486 let value = abandoned?;
487 if value.as_bool() == Some(true) {
488 return Some(Deprecation {
489 reason: None,
490 replacement: None,
491 });
492 }
493 let replacement = value.as_str()?;
494 Some(Deprecation {
495 reason: None,
496 replacement: (!replacement.trim().is_empty()).then(|| replacement.trim().to_string()),
497 })
498}
499
500fn parse_package_metadata(name: &str, data: &[u8]) -> Result<Vec<ComposerVersion>> {
502 let response: PackagistResponse =
503 deps_core::parse_json_checked(data).map_err(DepsError::Json)?;
504
505 let key = name.to_lowercase();
507 let entries = response.packages.get(&key).cloned().unwrap_or_default();
508
509 Ok(expand_minified_versions(entries))
510}
511
512#[derive(Deserialize)]
514struct SearchResponse {
515 results: Vec<SearchResult>,
516}
517
518#[derive(Deserialize)]
520struct SearchResult {
521 name: String,
522 #[serde(default)]
523 description: Option<String>,
524 #[serde(default)]
525 repository: Option<String>,
526 #[serde(default)]
527 url: Option<String>,
528 #[serde(default)]
529 version: Option<String>,
530}
531
532fn parse_search_response(data: &[u8]) -> Result<Vec<ComposerPackage>> {
534 let response: SearchResponse = deps_core::parse_json_checked(data).map_err(DepsError::Json)?;
535
536 Ok(response
537 .results
538 .into_iter()
539 .map(|r| ComposerPackage {
540 name: r.name.into(),
541 description: r.description,
542 repository: r.repository,
543 homepage: r.url,
544 latest_version: r.version.unwrap_or_default().into(),
545 })
546 .collect())
547}
548
549impl deps_core::Registry for PackagistRegistry {
550 fn get_versions<'a>(
551 &'a self,
552 name: &'a deps_core::PackageName,
553 ) -> deps_core::ecosystem::BoxFuture<'a, Result<Vec<Box<dyn deps_core::Version>>>> {
554 Box::pin(async move {
555 let versions = self.get_versions(name.as_str()).await?;
556 Ok(versions
557 .into_iter()
558 .map(|v| Box::new(v) as Box<dyn deps_core::Version>)
559 .collect())
560 })
561 }
562
563 fn get_latest_matching<'a>(
564 &'a self,
565 name: &'a deps_core::PackageName,
566 req: &'a deps_core::VersionReq,
567 ) -> deps_core::ecosystem::BoxFuture<'a, Result<Option<Box<dyn deps_core::Version>>>> {
568 Box::pin(async move {
569 let version = self
570 .get_latest_matching(name.as_str(), req.as_str())
571 .await?;
572 Ok(version.map(|v| Box::new(v) as Box<dyn deps_core::Version>))
573 })
574 }
575
576 fn get_latest_matching_with_context<'a>(
581 &'a self,
582 name: &'a deps_core::PackageName,
583 req: &'a deps_core::VersionReq,
584 minimum_stability: Option<&'a str>,
585 ) -> deps_core::ecosystem::BoxFuture<'a, Result<Option<Box<dyn deps_core::Version>>>> {
586 Box::pin(async move {
587 let version = self
588 .get_latest_matching_for_manifest(name.as_str(), req.as_str(), minimum_stability)
589 .await?;
590 Ok(version.map(|v| Box::new(v) as Box<dyn deps_core::Version>))
591 })
592 }
593
594 fn search<'a>(
595 &'a self,
596 query: &'a str,
597 limit: usize,
598 ) -> deps_core::ecosystem::BoxFuture<'a, Result<Vec<Box<dyn deps_core::Metadata>>>> {
599 Box::pin(async move {
600 let packages = self.search(query, limit).await?;
601 Ok(packages
602 .into_iter()
603 .map(|p| Box::new(p) as Box<dyn deps_core::Metadata>)
604 .collect())
605 })
606 }
607
608 fn select_latest_matching(
629 &self,
630 versions: &[Box<dyn deps_core::Version>],
631 req: &deps_core::VersionReq,
632 ) -> Option<usize> {
633 self.select_latest_matching_impl(versions, req, None)
634 }
635
636 fn select_latest_matching_with_context(
640 &self,
641 versions: &[Box<dyn deps_core::Version>],
642 req: &deps_core::VersionReq,
643 minimum_stability: Option<&str>,
644 ) -> Option<usize> {
645 self.select_latest_matching_for_manifest(versions, req, minimum_stability)
646 }
647
648 fn reports_yanked(&self) -> bool {
654 false
655 }
656
657 fn as_any(&self) -> &dyn Any {
658 self
659 }
660}
661
662#[cfg(test)]
663mod tests {
664 use super::*;
665
666 use std::assert_matches;
667
668 #[test]
669 fn test_package_url_preserves_vendor_package() {
670 assert_eq!(
671 package_url("symfony/console"),
672 "https://packagist.org/packages/symfony/console"
673 );
674 }
675
676 #[test]
677 fn test_package_url_encodes_malicious_segments() {
678 let url = package_url("evil)[/pkg](x");
679 assert!(!url.contains('('));
680 assert!(!url.contains(')'));
681 assert!(!url.contains('['));
682 assert!(!url.contains(']'));
683 }
684
685 #[test]
686 fn test_package_url_encodes_newline_autolink_and_percent() {
687 let url = package_url("evil\n<%>/pkg");
688 assert!(!url.contains('\n'));
689 assert!(!url.contains('<'));
690 assert!(!url.contains('>'));
691 assert!(url.contains("%25"));
692 }
693
694 #[test]
695 fn test_package_url_empty_name() {
696 assert_eq!(package_url(""), "https://packagist.org/packages/");
697 }
698
699 #[test]
700 fn test_reject_dot_segment_rejects_bare_dot_dot() {
701 assert!(reject_dot_segment("..").is_err());
702 }
703
704 #[test]
705 fn test_reject_dot_segment_rejects_vendor_dot_dot() {
706 assert!(reject_dot_segment("../evil").is_err());
707 }
708
709 #[test]
710 fn test_reject_dot_segment_rejects_package_dot_dot() {
711 assert!(reject_dot_segment("vendor/..").is_err());
712 }
713
714 #[test]
715 fn test_reject_dot_segment_accepts_normal_names() {
716 assert!(reject_dot_segment("monolog/monolog").is_ok());
717 assert!(reject_dot_segment("symfony").is_ok());
718 }
719
720 #[test]
724 fn test_p2_url_bare_dot_dot_vendor_normalizes_above_p2_prefix() {
725 let url = p2_url("https://repo.packagist.org", "../evil");
726 let parsed = url::Url::parse(&url).unwrap();
727 assert_eq!(
728 parsed.path(),
729 "/evil.json",
730 "parsed path: {}",
731 parsed.path()
732 );
733 }
734
735 #[test]
740 fn test_p2_url_dot_segment_sweep() {
741 deps_core::test_util::assert_dot_segment_gated_or_contained(
742 |seg| {
743 let name = format!("{seg}/package");
744 reject_dot_segment(&name)
745 .ok()
746 .map(|()| p2_url("https://repo.packagist.org", &name))
747 },
748 "repo.packagist.org",
749 "/p2/",
750 );
751 deps_core::test_util::assert_dot_segment_gated_or_contained(
752 |seg| {
753 let name = format!("vendor/{seg}");
754 reject_dot_segment(&name)
755 .ok()
756 .map(|()| p2_url("https://repo.packagist.org", &name))
757 },
758 "repo.packagist.org",
759 "/p2/",
760 );
761 }
762
763 #[tokio::test]
773 async fn test_get_versions_rejects_vendor_dot_dot_as_not_found() {
774 let registry = PackagistRegistry::new(Arc::new(HttpCache::new()));
775 let err = registry.get_versions("../evil").await.unwrap_err();
776 assert_matches!(err, DepsError::PackageNotFound { .. });
777 }
778
779 #[test]
780 fn test_expand_minified_versions_basic() {
781 let entries = vec![
782 MinifiedVersion {
783 version: Some("3.0.0".into()),
784 version_normalized: Some("3.0.0.0".into()),
785 abandoned: None,
786 time: None,
787 },
788 MinifiedVersion {
789 version: Some("2.0.0".into()),
790 version_normalized: Some("2.0.0.0".into()),
791 abandoned: None,
792 time: None,
793 },
794 ];
795
796 let versions = expand_minified_versions(entries);
797 assert_eq!(versions.len(), 2);
798 assert_eq!(versions[0].version, "3.0.0");
799 assert_eq!(versions[1].version, "2.0.0");
800 assert!(!versions[0].abandoned);
801 }
802
803 #[test]
804 fn test_expand_minified_versions_field_inheritance() {
805 let entries = vec![
807 MinifiedVersion {
808 version: Some("3.0.0".into()),
809 version_normalized: Some("3.0.0.0".into()),
810 abandoned: None,
811 time: None,
812 },
813 MinifiedVersion {
814 version: Some("2.9.0".into()),
815 version_normalized: None, abandoned: None,
817 time: None,
818 },
819 ];
820
821 let versions = expand_minified_versions(entries);
822 assert_eq!(versions.len(), 2);
823 assert_eq!(versions[1].version, "2.9.0");
824 assert_eq!(versions[1].version_normalized, "3.0.0.0"); }
826
827 #[test]
828 fn test_expand_minified_versions_filters_dev() {
829 let entries = vec![
830 MinifiedVersion {
831 version: Some("3.0.0".into()),
832 version_normalized: Some("3.0.0.0".into()),
833 abandoned: None,
834 time: None,
835 },
836 MinifiedVersion {
837 version: Some("dev-main".into()),
838 version_normalized: None,
839 abandoned: None,
840 time: None,
841 },
842 MinifiedVersion {
843 version: Some("2.0.0-dev".into()),
844 version_normalized: None,
845 abandoned: None,
846 time: None,
847 },
848 ];
849
850 let versions = expand_minified_versions(entries);
851 assert_eq!(versions.len(), 1);
852 assert_eq!(versions[0].version, "3.0.0");
853 }
854
855 #[test]
856 fn test_expand_minified_versions_abandoned() {
857 let entries = vec![MinifiedVersion {
858 version: Some("3.0.0".into()),
859 version_normalized: Some("3.0.0.0".into()),
860 abandoned: Some(serde_json::Value::String("Use other/package".into())),
861 time: None,
862 }];
863
864 let versions = expand_minified_versions(entries);
865 assert_eq!(versions.len(), 1);
866 assert!(versions[0].abandoned);
867 assert_eq!(
868 versions[0].deprecation,
869 Some(Deprecation {
870 reason: None,
871 replacement: Some("Use other/package".to_string()),
872 })
873 );
874 }
875
876 #[test]
879 fn test_expand_minified_versions_abandoned_true_has_no_replacement() {
880 let entries = vec![MinifiedVersion {
881 version: Some("3.0.0".into()),
882 version_normalized: Some("3.0.0.0".into()),
883 abandoned: Some(serde_json::Value::Bool(true)),
884 time: None,
885 }];
886
887 let versions = expand_minified_versions(entries);
888 assert_eq!(
889 versions[0].deprecation,
890 Some(Deprecation {
891 reason: None,
892 replacement: None,
893 })
894 );
895 }
896
897 #[test]
900 fn test_expand_minified_versions_abandoned_whitespace_replacement_is_none() {
901 let entries = vec![MinifiedVersion {
902 version: Some("3.0.0".into()),
903 version_normalized: Some("3.0.0.0".into()),
904 abandoned: Some(serde_json::Value::String(" ".into())),
905 time: None,
906 }];
907
908 let versions = expand_minified_versions(entries);
909 assert_eq!(
910 versions[0].deprecation,
911 Some(Deprecation {
912 reason: None,
913 replacement: None,
914 })
915 );
916 }
917
918 #[test]
920 fn test_expand_minified_versions_not_abandoned_has_no_deprecation() {
921 let entries = vec![MinifiedVersion {
922 version: Some("3.0.0".into()),
923 version_normalized: Some("3.0.0.0".into()),
924 abandoned: None,
925 time: None,
926 }];
927
928 let versions = expand_minified_versions(entries);
929 assert_eq!(versions[0].deprecation, None);
930 }
931
932 #[test]
933 fn test_expand_minified_versions_with_time() {
934 let entries = vec![MinifiedVersion {
935 version: Some("3.0.0".into()),
936 version_normalized: Some("3.0.0.0".into()),
937 abandoned: None,
938 time: Some("2026-01-02T08:56:05+00:00".into()),
939 }];
940
941 let versions = expand_minified_versions(entries);
942 assert_eq!(versions.len(), 1);
943 assert_eq!(
944 versions[0].published_at,
945 deps_core::PublishTime::parse_rfc3339("2026-01-02T08:56:05+00:00")
946 );
947 }
948
949 #[test]
950 fn test_expand_minified_versions_without_time() {
951 let entries = vec![MinifiedVersion {
952 version: Some("3.0.0".into()),
953 version_normalized: Some("3.0.0.0".into()),
954 abandoned: None,
955 time: None,
956 }];
957
958 let versions = expand_minified_versions(entries);
959 assert_eq!(versions.len(), 1);
960 assert!(versions[0].published_at.is_none());
961 }
962
963 #[test]
964 fn test_expand_minified_versions_with_malformed_time() {
965 let entries = vec![MinifiedVersion {
966 version: Some("3.0.0".into()),
967 version_normalized: Some("3.0.0.0".into()),
968 abandoned: None,
969 time: Some("not-a-timestamp".into()),
970 }];
971
972 let versions = expand_minified_versions(entries);
973 assert_eq!(versions.len(), 1);
974 assert!(
975 versions[0].published_at.is_none(),
976 "malformed time degrades to None, not an error"
977 );
978 }
979
980 #[test]
981 fn test_expand_minified_versions_time_is_not_inherited() {
982 let entries = vec![
986 MinifiedVersion {
987 version: Some("3.0.0".into()),
988 version_normalized: Some("3.0.0.0".into()),
989 abandoned: None,
990 time: Some("2026-01-02T08:56:05+00:00".into()),
991 },
992 MinifiedVersion {
993 version: Some("2.9.0".into()),
994 version_normalized: None, abandoned: None,
996 time: None, },
998 ];
999
1000 let versions = expand_minified_versions(entries);
1001 assert_eq!(versions.len(), 2);
1002 assert!(versions[0].published_at.is_some());
1003 assert!(
1004 versions[1].published_at.is_none(),
1005 "time must not be inherited from the previous entry"
1006 );
1007 }
1008
1009 #[test]
1010 fn test_parse_search_response() {
1011 let json = r#"{
1012 "results": [
1013 {
1014 "name": "symfony/console",
1015 "description": "Symfony Console Component",
1016 "version": "6.0.0",
1017 "url": "https://packagist.org/packages/symfony/console",
1018 "repository": "https://github.com/symfony/console"
1019 }
1020 ],
1021 "total": 1
1022}"#;
1023
1024 let packages = parse_search_response(json.as_bytes()).unwrap();
1025 assert_eq!(packages.len(), 1);
1026
1027 let pkg = &packages[0];
1028 assert_eq!(pkg.name, "symfony/console");
1029 assert_eq!(pkg.description, Some("Symfony Console Component".into()));
1030 assert_eq!(pkg.latest_version, "6.0.0");
1031 }
1032
1033 #[test]
1034 fn test_parse_search_response_nesting_at_max_depth_accepted() {
1035 let depth = deps_core::MAX_JSON_NESTING_DEPTH;
1036 let json = format!(
1037 r#"{{"results": [], "extra": {}1{}}}"#,
1038 "[".repeat(depth - 1),
1039 "]".repeat(depth - 1)
1040 );
1041 assert!(parse_search_response(json.as_bytes()).is_ok());
1042 }
1043
1044 #[test]
1045 fn test_parse_search_response_nesting_over_max_depth_rejected() {
1046 let depth = deps_core::MAX_JSON_NESTING_DEPTH + 1;
1047 let json = format!(
1048 r#"{{"results": [], "extra": {}1{}}}"#,
1049 "[".repeat(depth),
1050 "]".repeat(depth)
1051 );
1052 assert!(parse_search_response(json.as_bytes()).is_err());
1053 }
1054
1055 #[test]
1056 fn test_parse_package_metadata() {
1057 let json = r#"{
1058 "packages": {
1059 "monolog/monolog": [
1060 {
1061 "version": "3.0.0",
1062 "version_normalized": "3.0.0.0",
1063 "abandoned": null
1064 },
1065 {
1066 "version": "2.0.0",
1067 "version_normalized": "2.0.0.0"
1068 }
1069 ]
1070 }
1071}"#;
1072
1073 let versions = parse_package_metadata("monolog/monolog", json.as_bytes()).unwrap();
1074 assert_eq!(versions.len(), 2);
1075 assert_eq!(versions[0].version, "3.0.0");
1076 }
1077
1078 #[test]
1079 fn test_parse_package_metadata_deeply_nested_json_rejected_before_parse() {
1080 let depth = deps_core::MAX_JSON_NESTING_DEPTH + 1;
1083 let deeply_nested = format!(
1084 r#"{{"packages":{{"monolog/monolog":[{{"version":"3.0.0","abandoned":{}1{}}}]}}}}"#,
1085 "[".repeat(depth),
1086 "]".repeat(depth)
1087 );
1088 assert!(parse_package_metadata("monolog/monolog", deeply_nested.as_bytes()).is_err());
1089 }
1090
1091 #[test]
1092 fn test_select_latest_matching_not_default_none() {
1093 use deps_core::{Registry, VersionReq};
1099
1100 let cache = Arc::new(HttpCache::new());
1101 let registry = PackagistRegistry::new(cache);
1102 let versions: Vec<Box<dyn deps_core::Version>> = vec![
1103 Box::new(ComposerVersion {
1104 version: "2.0.0".into(),
1105 version_normalized: "2.0.0.0".into(),
1106 abandoned: true,
1107 deprecation: None,
1108 published_at: None,
1109 }),
1110 Box::new(ComposerVersion {
1111 version: "1.0.0".into(),
1112 version_normalized: "1.0.0.0".into(),
1113 abandoned: false,
1114 deprecation: None,
1115 published_at: None,
1116 }),
1117 ];
1118 let req = VersionReq::new("*");
1119 assert_eq!(registry.select_latest_matching(&versions, &req), Some(0));
1120 }
1121
1122 #[test]
1123 fn test_select_latest_matching_all_abandoned_still_resolves() {
1124 use deps_core::{Registry, VersionReq};
1128
1129 let cache = Arc::new(HttpCache::new());
1130 let registry = PackagistRegistry::new(cache);
1131 let versions: Vec<Box<dyn deps_core::Version>> = vec![
1132 Box::new(ComposerVersion {
1133 version: "2.0.0".into(),
1134 version_normalized: "2.0.0.0".into(),
1135 abandoned: true,
1136 deprecation: None,
1137 published_at: None,
1138 }),
1139 Box::new(ComposerVersion {
1140 version: "1.0.0".into(),
1141 version_normalized: "1.0.0.0".into(),
1142 abandoned: true,
1143 deprecation: None,
1144 published_at: None,
1145 }),
1146 ];
1147 let req = VersionReq::new("*");
1148 assert_eq!(registry.select_latest_matching(&versions, &req), Some(0));
1149 }
1150
1151 #[tokio::test]
1158 async fn test_get_latest_matching_wildcard_all_abandoned_still_resolves() {
1159 let mut server = mockito::Server::new_async().await;
1160 let base = server.url();
1161 let registry = PackagistRegistry::with_registry_base(Arc::new(HttpCache::new()), base);
1162
1163 server
1164 .mock("GET", "/p2/vendor/abandoned-pkg.json")
1165 .with_status(200)
1166 .with_body(
1167 r#"{"packages": {"vendor/abandoned-pkg": [
1168 {"version": "2.0.0", "version_normalized": "2.0.0.0", "abandoned": true},
1169 {"version": "1.0.0", "version_normalized": "1.0.0.0", "abandoned": true}
1170 ]}}"#,
1171 )
1172 .create_async()
1173 .await;
1174
1175 let latest = registry
1176 .get_latest_matching("vendor/abandoned-pkg", "*")
1177 .await
1178 .unwrap();
1179
1180 let version = latest.expect("an all-abandoned package still exists and resolves");
1181 assert_eq!(version.version, "2.0.0");
1182 }
1183
1184 #[test]
1188 fn test_select_latest_matching_excludes_prerelease_for_loose_requirement() {
1189 use deps_core::{Registry, VersionReq};
1190
1191 let cache = Arc::new(HttpCache::new());
1192 let registry = PackagistRegistry::new(cache);
1193 let versions: Vec<Box<dyn deps_core::Version>> = vec![
1194 Box::new(ComposerVersion {
1195 version: "2.0.0-beta1".into(),
1196 version_normalized: "2.0.0.0-beta1".into(),
1197 abandoned: false,
1198 deprecation: None,
1199 published_at: None,
1200 }),
1201 Box::new(ComposerVersion {
1202 version: "1.5.0".into(),
1203 version_normalized: "1.5.0.0".into(),
1204 abandoned: false,
1205 deprecation: None,
1206 published_at: None,
1207 }),
1208 ];
1209 let req = VersionReq::new(">=1.0");
1210 assert_eq!(registry.select_latest_matching(&versions, &req), Some(1));
1211 }
1212
1213 #[test]
1217 fn test_select_latest_matching_allows_prerelease_when_requirement_names_it() {
1218 use deps_core::{Registry, VersionReq};
1219
1220 let cache = Arc::new(HttpCache::new());
1221 let registry = PackagistRegistry::new(cache);
1222 let versions: Vec<Box<dyn deps_core::Version>> = vec![
1223 Box::new(ComposerVersion {
1224 version: "2.0.0-beta1".into(),
1225 version_normalized: "2.0.0.0-beta1".into(),
1226 abandoned: false,
1227 deprecation: None,
1228 published_at: None,
1229 }),
1230 Box::new(ComposerVersion {
1231 version: "1.5.0".into(),
1232 version_normalized: "1.5.0.0".into(),
1233 abandoned: false,
1234 deprecation: None,
1235 published_at: None,
1236 }),
1237 ];
1238 let req = VersionReq::new("2.0.0-beta1");
1239 assert_eq!(registry.select_latest_matching(&versions, &req), Some(0));
1240 }
1241
1242 #[tokio::test]
1245 async fn test_get_latest_matching_wildcard_excludes_prerelease() {
1246 let mut server = mockito::Server::new_async().await;
1247 let base = server.url();
1248 let registry = PackagistRegistry::with_registry_base(Arc::new(HttpCache::new()), base);
1249
1250 server
1251 .mock("GET", "/p2/vendor/pkg.json")
1252 .with_status(200)
1253 .with_body(
1254 r#"{"packages": {"vendor/pkg": [
1255 {"version": "2.0.0-beta1", "version_normalized": "2.0.0.0-beta1"},
1256 {"version": "1.5.0", "version_normalized": "1.5.0.0"}
1257 ]}}"#,
1258 )
1259 .create_async()
1260 .await;
1261
1262 let latest = registry
1263 .get_latest_matching("vendor/pkg", "*")
1264 .await
1265 .unwrap();
1266
1267 let version = latest.expect("a package with a stable release still resolves");
1268 assert_eq!(version.version, "1.5.0");
1269 }
1270
1271 #[test]
1278 fn test_select_latest_matching_allows_short_alias_prerelease_when_requirement_names_it() {
1279 use deps_core::{Registry, VersionReq};
1280
1281 let cache = Arc::new(HttpCache::new());
1282 let registry = PackagistRegistry::new(cache);
1283 let versions: Vec<Box<dyn deps_core::Version>> = vec![
1284 Box::new(ComposerVersion {
1285 version: "2.0.0-a1".into(),
1286 version_normalized: "2.0.0.0-alpha1".into(),
1287 abandoned: false,
1288 deprecation: None,
1289 published_at: None,
1290 }),
1291 Box::new(ComposerVersion {
1292 version: "1.5.0".into(),
1293 version_normalized: "1.5.0.0".into(),
1294 abandoned: false,
1295 deprecation: None,
1296 published_at: None,
1297 }),
1298 ];
1299 let req = VersionReq::new("2.0.0-a1");
1300 assert_eq!(registry.select_latest_matching(&versions, &req), Some(0));
1301 }
1302
1303 #[test]
1307 fn test_select_latest_matching_allows_short_alias_prerelease_with_caret_requirement() {
1308 use deps_core::{Registry, VersionReq};
1309
1310 let cache = Arc::new(HttpCache::new());
1311 let registry = PackagistRegistry::new(cache);
1312 let versions: Vec<Box<dyn deps_core::Version>> = vec![Box::new(ComposerVersion {
1313 version: "1.0.0-a1".into(),
1314 version_normalized: "1.0.0.0-alpha1".into(),
1315 abandoned: false,
1316 deprecation: None,
1317 published_at: None,
1318 })];
1319 let req = VersionReq::new("^1.0.0-a1");
1320 assert_eq!(registry.select_latest_matching(&versions, &req), Some(0));
1321 }
1322
1323 #[tokio::test]
1327 async fn test_get_latest_matching_allows_prerelease_when_requirement_names_it() {
1328 let mut server = mockito::Server::new_async().await;
1329 let base = server.url();
1330 let registry = PackagistRegistry::with_registry_base(Arc::new(HttpCache::new()), base);
1331
1332 server
1333 .mock("GET", "/p2/vendor/pkg.json")
1334 .with_status(200)
1335 .with_body(
1336 r#"{"packages": {"vendor/pkg": [
1337 {"version": "2.0.0-beta1", "version_normalized": "2.0.0.0-beta1"},
1338 {"version": "1.5.0", "version_normalized": "1.5.0.0"}
1339 ]}}"#,
1340 )
1341 .create_async()
1342 .await;
1343
1344 let latest = registry
1345 .get_latest_matching("vendor/pkg", "2.0.0-beta1")
1346 .await
1347 .unwrap();
1348
1349 let version = latest.expect("an explicit prerelease pin resolves to that prerelease");
1350 assert_eq!(version.version, "2.0.0-beta1");
1351 }
1352
1353 #[test]
1358 fn test_select_latest_matching_wildcard_prerelease_only_still_resolves() {
1359 use deps_core::{Registry, VersionReq};
1360
1361 let cache = Arc::new(HttpCache::new());
1362 let registry = PackagistRegistry::new(cache);
1363 let versions: Vec<Box<dyn deps_core::Version>> = vec![
1364 Box::new(ComposerVersion {
1365 version: "2.0.0-beta2".into(),
1366 version_normalized: "2.0.0.0-beta2".into(),
1367 abandoned: false,
1368 deprecation: None,
1369 published_at: None,
1370 }),
1371 Box::new(ComposerVersion {
1372 version: "2.0.0-beta1".into(),
1373 version_normalized: "2.0.0.0-beta1".into(),
1374 abandoned: false,
1375 deprecation: None,
1376 published_at: None,
1377 }),
1378 ];
1379 let req = VersionReq::new("*");
1380 assert_eq!(registry.select_latest_matching(&versions, &req), Some(0));
1381 }
1382
1383 #[tokio::test]
1387 async fn test_get_latest_matching_wildcard_prerelease_only_still_resolves() {
1388 let mut server = mockito::Server::new_async().await;
1389 let base = server.url();
1390 let registry = PackagistRegistry::with_registry_base(Arc::new(HttpCache::new()), base);
1391
1392 server
1393 .mock("GET", "/p2/vendor/prerelease-only.json")
1394 .with_status(200)
1395 .with_body(
1396 r#"{"packages": {"vendor/prerelease-only": [
1397 {"version": "2.0.0-beta2", "version_normalized": "2.0.0.0-beta2"},
1398 {"version": "2.0.0-beta1", "version_normalized": "2.0.0.0-beta1"}
1399 ]}}"#,
1400 )
1401 .create_async()
1402 .await;
1403
1404 let latest = registry
1405 .get_latest_matching("vendor/prerelease-only", "*")
1406 .await
1407 .unwrap();
1408
1409 let version = latest.expect("a prerelease-only package still exists and resolves");
1410 assert_eq!(version.version, "2.0.0-beta2");
1411 }
1412
1413 fn stability_fixture() -> Vec<Box<dyn deps_core::Version>> {
1416 vec![
1417 Box::new(ComposerVersion {
1418 version: "2.0.0-alpha1".into(),
1419 version_normalized: "2.0.0.0-alpha1".into(),
1420 abandoned: false,
1421 deprecation: None,
1422 published_at: None,
1423 }),
1424 Box::new(ComposerVersion {
1425 version: "2.0.0-beta1".into(),
1426 version_normalized: "2.0.0.0-beta1".into(),
1427 abandoned: false,
1428 deprecation: None,
1429 published_at: None,
1430 }),
1431 Box::new(ComposerVersion {
1432 version: "1.5.0".into(),
1433 version_normalized: "1.5.0.0".into(),
1434 abandoned: false,
1435 deprecation: None,
1436 published_at: None,
1437 }),
1438 ]
1439 }
1440
1441 #[test]
1445 fn test_select_latest_matching_for_manifest_honors_looser_minimum_stability() {
1446 let cache = Arc::new(HttpCache::new());
1447 let registry = PackagistRegistry::new(cache);
1448 let versions = stability_fixture();
1449 let req = deps_core::VersionReq::new("*");
1450
1451 assert_eq!(
1452 registry.select_latest_matching_for_manifest(&versions, &req, Some("beta")),
1453 Some(1),
1454 "beta release should be latest under minimum-stability: beta"
1455 );
1456 }
1457
1458 #[test]
1461 fn test_select_latest_matching_for_manifest_honors_alpha_minimum_stability() {
1462 let cache = Arc::new(HttpCache::new());
1463 let registry = PackagistRegistry::new(cache);
1464 let versions = stability_fixture();
1465 let req = deps_core::VersionReq::new("*");
1466
1467 assert_eq!(
1468 registry.select_latest_matching_for_manifest(&versions, &req, Some("alpha")),
1469 Some(0),
1470 "alpha release should be latest under minimum-stability: alpha"
1471 );
1472 }
1473
1474 #[test]
1477 fn test_select_latest_matching_for_manifest_none_matches_default() {
1478 use deps_core::Registry;
1479
1480 let cache = Arc::new(HttpCache::new());
1481 let registry = PackagistRegistry::new(cache);
1482 let versions = stability_fixture();
1483 let req = deps_core::VersionReq::new("*");
1484
1485 assert_eq!(
1486 registry.select_latest_matching_for_manifest(&versions, &req, None),
1487 registry.select_latest_matching(&versions, &req),
1488 );
1489 }
1490
1491 #[test]
1494 fn test_select_latest_matching_for_manifest_explicit_stable_excludes_prerelease() {
1495 let cache = Arc::new(HttpCache::new());
1496 let registry = PackagistRegistry::new(cache);
1497 let versions = stability_fixture();
1498 let req = deps_core::VersionReq::new("*");
1499
1500 assert_eq!(
1501 registry.select_latest_matching_for_manifest(&versions, &req, Some("stable")),
1502 Some(2),
1503 );
1504 }
1505
1506 #[tokio::test]
1510 async fn test_get_latest_matching_for_manifest_honors_looser_minimum_stability() {
1511 let mut server = mockito::Server::new_async().await;
1512 let base = server.url();
1513 let registry = PackagistRegistry::with_registry_base(Arc::new(HttpCache::new()), base);
1514
1515 server
1516 .mock("GET", "/p2/vendor/pkg.json")
1517 .with_status(200)
1518 .with_body(
1519 r#"{"packages": {"vendor/pkg": [
1520 {"version": "2.0.0-beta1", "version_normalized": "2.0.0.0-beta1"},
1521 {"version": "1.5.0", "version_normalized": "1.5.0.0"}
1522 ]}}"#,
1523 )
1524 .create_async()
1525 .await;
1526
1527 let latest = registry
1528 .get_latest_matching_for_manifest("vendor/pkg", "*", Some("beta"))
1529 .await
1530 .unwrap();
1531
1532 assert_eq!(
1533 latest.expect("beta release resolves").version,
1534 "2.0.0-beta1"
1535 );
1536 }
1537
1538 #[test]
1544 fn test_select_latest_matching_at_beta_flag_allows_beta() {
1545 use deps_core::{Registry, VersionReq};
1546
1547 let cache = Arc::new(HttpCache::new());
1548 let registry = PackagistRegistry::new(cache);
1549 let versions: Vec<Box<dyn deps_core::Version>> = vec![
1550 Box::new(ComposerVersion {
1551 version: "1.5.0-beta1".into(),
1552 version_normalized: "1.5.0.0-beta1".into(),
1553 abandoned: false,
1554 deprecation: None,
1555 published_at: None,
1556 }),
1557 Box::new(ComposerVersion {
1558 version: "1.0.0".into(),
1559 version_normalized: "1.0.0.0".into(),
1560 abandoned: false,
1561 deprecation: None,
1562 published_at: None,
1563 }),
1564 ];
1565 let req = VersionReq::new("^1.0@beta");
1566 assert_eq!(
1567 registry.select_latest_matching(&versions, &req),
1568 Some(0),
1569 "beta release matching the range must resolve under an @beta opt-in"
1570 );
1571 }
1572
1573 #[test]
1576 fn test_select_latest_matching_at_beta_flag_excludes_alpha() {
1577 use deps_core::{Registry, VersionReq};
1578
1579 let cache = Arc::new(HttpCache::new());
1580 let registry = PackagistRegistry::new(cache);
1581 let versions: Vec<Box<dyn deps_core::Version>> = vec![
1582 Box::new(ComposerVersion {
1583 version: "1.5.0-alpha1".into(),
1584 version_normalized: "1.5.0.0-alpha1".into(),
1585 abandoned: false,
1586 deprecation: None,
1587 published_at: None,
1588 }),
1589 Box::new(ComposerVersion {
1590 version: "1.0.0".into(),
1591 version_normalized: "1.0.0.0".into(),
1592 abandoned: false,
1593 deprecation: None,
1594 published_at: None,
1595 }),
1596 ];
1597 let req = VersionReq::new("^1.0@beta");
1598 assert_eq!(
1599 registry.select_latest_matching(&versions, &req),
1600 Some(1),
1601 "alpha release must still be excluded under an @beta opt-in"
1602 );
1603 }
1604
1605 #[test]
1609 fn test_select_latest_matching_at_stable_flag_still_excludes_prerelease() {
1610 use deps_core::{Registry, VersionReq};
1611
1612 let cache = Arc::new(HttpCache::new());
1613 let registry = PackagistRegistry::new(cache);
1614 let versions: Vec<Box<dyn deps_core::Version>> = vec![
1615 Box::new(ComposerVersion {
1616 version: "1.5.0-beta1".into(),
1617 version_normalized: "1.5.0.0-beta1".into(),
1618 abandoned: false,
1619 deprecation: None,
1620 published_at: None,
1621 }),
1622 Box::new(ComposerVersion {
1623 version: "1.0.0".into(),
1624 version_normalized: "1.0.0.0".into(),
1625 abandoned: false,
1626 deprecation: None,
1627 published_at: None,
1628 }),
1629 ];
1630 let req = VersionReq::new("^1.0@stable");
1631 assert_eq!(registry.select_latest_matching(&versions, &req), Some(1));
1632 }
1633
1634 #[test]
1638 fn test_select_latest_matching_at_rc_flag_allows_rc_excludes_beta() {
1639 use deps_core::{Registry, VersionReq};
1640
1641 let cache = Arc::new(HttpCache::new());
1642 let registry = PackagistRegistry::new(cache);
1643 let versions: Vec<Box<dyn deps_core::Version>> = vec![
1644 Box::new(ComposerVersion {
1645 version: "1.5.0-beta1".into(),
1646 version_normalized: "1.5.0.0-beta1".into(),
1647 abandoned: false,
1648 deprecation: None,
1649 published_at: None,
1650 }),
1651 Box::new(ComposerVersion {
1652 version: "1.4.0-RC1".into(),
1653 version_normalized: "1.4.0.0-RC1".into(),
1654 abandoned: false,
1655 deprecation: None,
1656 published_at: None,
1657 }),
1658 Box::new(ComposerVersion {
1659 version: "1.0.0".into(),
1660 version_normalized: "1.0.0.0".into(),
1661 abandoned: false,
1662 deprecation: None,
1663 published_at: None,
1664 }),
1665 ];
1666 let req = VersionReq::new("^1.0@RC");
1667 assert_eq!(
1668 registry.select_latest_matching(&versions, &req),
1669 Some(1),
1670 "RC release must resolve, but a looser beta release must still be excluded"
1671 );
1672 }
1673
1674 #[test]
1678 fn test_select_latest_matching_at_alpha_flag_allows_alpha() {
1679 use deps_core::{Registry, VersionReq};
1680
1681 let cache = Arc::new(HttpCache::new());
1682 let registry = PackagistRegistry::new(cache);
1683 let versions: Vec<Box<dyn deps_core::Version>> = vec![
1684 Box::new(ComposerVersion {
1685 version: "1.5.0-alpha1".into(),
1686 version_normalized: "1.5.0.0-alpha1".into(),
1687 abandoned: false,
1688 deprecation: None,
1689 published_at: None,
1690 }),
1691 Box::new(ComposerVersion {
1692 version: "1.0.0".into(),
1693 version_normalized: "1.0.0.0".into(),
1694 abandoned: false,
1695 deprecation: None,
1696 published_at: None,
1697 }),
1698 ];
1699 let req = VersionReq::new("^1.0@alpha");
1700 assert_eq!(registry.select_latest_matching(&versions, &req), Some(0));
1701 }
1702
1703 #[test]
1706 fn test_select_latest_matching_at_dev_flag_allows_alpha() {
1707 use deps_core::{Registry, VersionReq};
1708
1709 let cache = Arc::new(HttpCache::new());
1710 let registry = PackagistRegistry::new(cache);
1711 let versions: Vec<Box<dyn deps_core::Version>> = vec![
1712 Box::new(ComposerVersion {
1713 version: "1.5.0-alpha1".into(),
1714 version_normalized: "1.5.0.0-alpha1".into(),
1715 abandoned: false,
1716 deprecation: None,
1717 published_at: None,
1718 }),
1719 Box::new(ComposerVersion {
1720 version: "1.0.0".into(),
1721 version_normalized: "1.0.0.0".into(),
1722 abandoned: false,
1723 deprecation: None,
1724 published_at: None,
1725 }),
1726 ];
1727 let req = VersionReq::new("^1.0@dev");
1728 assert_eq!(registry.select_latest_matching(&versions, &req), Some(0));
1729 }
1730
1731 #[test]
1737 fn test_select_latest_matching_at_flag_in_or_branch() {
1738 use deps_core::{Registry, VersionReq};
1739
1740 let cache = Arc::new(HttpCache::new());
1741 let registry = PackagistRegistry::new(cache);
1742 let versions: Vec<Box<dyn deps_core::Version>> = vec![Box::new(ComposerVersion {
1743 version: "1.5.0-beta1".into(),
1744 version_normalized: "1.5.0.0-beta1".into(),
1745 abandoned: false,
1746 deprecation: None,
1747 published_at: None,
1748 })];
1749 let req = VersionReq::new("^1.0@beta || ^2.0");
1750 assert_eq!(registry.select_latest_matching(&versions, &req), Some(0));
1751 }
1752
1753 #[test]
1756 fn test_select_latest_matching_at_flag_in_and_range() {
1757 use deps_core::{Registry, VersionReq};
1758
1759 let cache = Arc::new(HttpCache::new());
1760 let registry = PackagistRegistry::new(cache);
1761 let versions: Vec<Box<dyn deps_core::Version>> = vec![Box::new(ComposerVersion {
1762 version: "1.5.0-alpha1".into(),
1763 version_normalized: "1.5.0.0-alpha1".into(),
1764 abandoned: false,
1765 deprecation: None,
1766 published_at: None,
1767 })];
1768 let req = VersionReq::new(">=1.0@dev <2.0");
1769 assert_eq!(registry.select_latest_matching(&versions, &req), Some(0));
1770 }
1771
1772 #[test]
1776 fn test_compound_stability_flag_rank() {
1777 assert_eq!(
1778 compound_stability_flag_rank("^1.0@beta || ^2.0"),
1779 Some(crate::formatter::composer_stability_rank("beta"))
1780 );
1781 assert_eq!(
1782 compound_stability_flag_rank(">=1.0@dev <2.0"),
1783 Some(crate::formatter::composer_stability_rank("dev"))
1784 );
1785 assert_eq!(
1786 compound_stability_flag_rank("^1.0@alpha || ^2.0@RC"),
1787 Some(crate::formatter::composer_stability_rank("alpha")),
1788 "the loosest flag among branches must win"
1789 );
1790 assert_eq!(compound_stability_flag_rank("^1.0 || ^2.0"), None);
1791 }
1792
1793 #[test]
1798 fn test_select_latest_matching_excludes_separatorless_short_alias() {
1799 use deps_core::{Registry, VersionReq};
1800
1801 let cache = Arc::new(HttpCache::new());
1802 let registry = PackagistRegistry::new(cache);
1803 let versions: Vec<Box<dyn deps_core::Version>> = vec![
1804 Box::new(ComposerVersion {
1805 version: "2.0.0a1".into(),
1806 version_normalized: "2.0.0a1".into(),
1807 abandoned: false,
1808 deprecation: None,
1809 published_at: None,
1810 }),
1811 Box::new(ComposerVersion {
1812 version: "1.5.0".into(),
1813 version_normalized: "1.5.0.0".into(),
1814 abandoned: false,
1815 deprecation: None,
1816 published_at: None,
1817 }),
1818 ];
1819 let req = VersionReq::new(">=1.0");
1820 assert_eq!(registry.select_latest_matching(&versions, &req), Some(1));
1821 }
1822
1823 #[test]
1826 fn test_select_latest_matching_allows_separatorless_short_alias_pin_when_requirement_names_it()
1827 {
1828 use deps_core::{Registry, VersionReq};
1829
1830 let cache = Arc::new(HttpCache::new());
1831 let registry = PackagistRegistry::new(cache);
1832 let versions: Vec<Box<dyn deps_core::Version>> = vec![
1833 Box::new(ComposerVersion {
1834 version: "2.0.0a1".into(),
1835 version_normalized: "2.0.0a1".into(),
1836 abandoned: false,
1837 deprecation: None,
1838 published_at: None,
1839 }),
1840 Box::new(ComposerVersion {
1841 version: "1.5.0".into(),
1842 version_normalized: "1.5.0.0".into(),
1843 abandoned: false,
1844 deprecation: None,
1845 published_at: None,
1846 }),
1847 ];
1848 let req = VersionReq::new("2.0.0a1");
1849 assert_eq!(registry.select_latest_matching(&versions, &req), Some(0));
1850 }
1851
1852 #[test]
1854 fn test_select_latest_matching_excludes_separatorless_dev_suffix() {
1855 use deps_core::{Registry, VersionReq};
1856
1857 let cache = Arc::new(HttpCache::new());
1858 let registry = PackagistRegistry::new(cache);
1859 let versions: Vec<Box<dyn deps_core::Version>> = vec![
1860 Box::new(ComposerVersion {
1861 version: "2.0.0dev".into(),
1862 version_normalized: "2.0.0dev".into(),
1863 abandoned: false,
1864 deprecation: None,
1865 published_at: None,
1866 }),
1867 Box::new(ComposerVersion {
1868 version: "1.5.0".into(),
1869 version_normalized: "1.5.0.0".into(),
1870 abandoned: false,
1871 deprecation: None,
1872 published_at: None,
1873 }),
1874 ];
1875 let req = VersionReq::new(">=1.0");
1876 assert_eq!(registry.select_latest_matching(&versions, &req), Some(1));
1877 }
1878
1879 #[test]
1886 fn test_select_latest_matching_v_prefixed_alpha_excluded_by_default() {
1887 use deps_core::{Registry, VersionReq};
1888
1889 let cache = Arc::new(HttpCache::new());
1890 let registry = PackagistRegistry::new(cache);
1891 let versions: Vec<Box<dyn deps_core::Version>> = vec![
1892 Box::new(ComposerVersion {
1893 version: "v2.3.0-alpha.1".into(),
1894 version_normalized: "2.3.0.0-alpha1".into(),
1895 abandoned: false,
1896 deprecation: None,
1897 published_at: None,
1898 }),
1899 Box::new(ComposerVersion {
1900 version: "v2.2.8".into(),
1901 version_normalized: "2.2.8.0".into(),
1902 abandoned: false,
1903 deprecation: None,
1904 published_at: None,
1905 }),
1906 ];
1907 let req = VersionReq::new("*");
1908 assert_eq!(
1909 registry.select_latest_matching(&versions, &req),
1910 Some(1),
1911 "v2.2.8 (stable) must resolve as latest, not the v-prefixed alpha ahead of it"
1912 );
1913 }
1914
1915 #[test]
1919 fn test_select_latest_matching_v_prefixed_rc_excluded_for_concrete_requirement() {
1920 use deps_core::{Registry, VersionReq};
1921
1922 let cache = Arc::new(HttpCache::new());
1923 let registry = PackagistRegistry::new(cache);
1924 let versions: Vec<Box<dyn deps_core::Version>> = vec![
1925 Box::new(ComposerVersion {
1926 version: "V3.0.0-RC1".into(),
1927 version_normalized: "3.0.0.0-RC1".into(),
1928 abandoned: false,
1929 deprecation: None,
1930 published_at: None,
1931 }),
1932 Box::new(ComposerVersion {
1933 version: "v2.9.0".into(),
1934 version_normalized: "2.9.0.0".into(),
1935 abandoned: false,
1936 deprecation: None,
1937 published_at: None,
1938 }),
1939 ];
1940 let req = VersionReq::new(">=2.0");
1941 assert_eq!(registry.select_latest_matching(&versions, &req), Some(1));
1942 }
1943
1944 #[test]
1954 fn test_select_latest_matching_uppercase_v_prefixed_version_satisfies_requirement() {
1955 use deps_core::{Registry, VersionReq};
1956
1957 let cache = Arc::new(HttpCache::new());
1958 let registry = PackagistRegistry::new(cache);
1959 let versions: Vec<Box<dyn deps_core::Version>> = vec![Box::new(ComposerVersion {
1960 version: "V3.1.0".into(),
1961 version_normalized: "3.1.0.0".into(),
1962 abandoned: false,
1963 deprecation: None,
1964 published_at: None,
1965 })];
1966 let req = VersionReq::new(">=3.0");
1967 assert_eq!(
1968 registry.select_latest_matching(&versions, &req),
1969 Some(0),
1970 "V3.1.0 must satisfy >=3.0 through the real select_latest_matching path"
1971 );
1972 }
1973
1974 #[test]
1980 fn test_select_latest_matching_excludes_separatorless_rc_suffix() {
1981 use deps_core::{Registry, VersionReq};
1982
1983 let cache = Arc::new(HttpCache::new());
1984 let registry = PackagistRegistry::new(cache);
1985 let versions: Vec<Box<dyn deps_core::Version>> = vec![
1986 Box::new(ComposerVersion {
1987 version: "2.0.0RC1".into(),
1988 version_normalized: "2.0.0RC1".into(),
1991 abandoned: false,
1992 deprecation: None,
1993 published_at: None,
1994 }),
1995 Box::new(ComposerVersion {
1996 version: "1.5.0".into(),
1997 version_normalized: "1.5.0.0".into(),
1998 abandoned: false,
1999 deprecation: None,
2000 published_at: None,
2001 }),
2002 ];
2003 let req = VersionReq::new(">=1.0");
2004 assert_eq!(registry.select_latest_matching(&versions, &req), Some(1));
2005 }
2006
2007 #[test]
2011 fn test_select_latest_matching_allows_separatorless_rc_pin_when_requirement_names_it() {
2012 use deps_core::{Registry, VersionReq};
2013
2014 let cache = Arc::new(HttpCache::new());
2015 let registry = PackagistRegistry::new(cache);
2016 let versions: Vec<Box<dyn deps_core::Version>> = vec![
2017 Box::new(ComposerVersion {
2018 version: "2.0.0RC1".into(),
2019 version_normalized: "2.0.0RC1".into(),
2020 abandoned: false,
2021 deprecation: None,
2022 published_at: None,
2023 }),
2024 Box::new(ComposerVersion {
2025 version: "1.5.0".into(),
2026 version_normalized: "1.5.0.0".into(),
2027 abandoned: false,
2028 deprecation: None,
2029 published_at: None,
2030 }),
2031 ];
2032 let req = VersionReq::new("2.0.0RC1");
2033 assert_eq!(registry.select_latest_matching(&versions, &req), Some(0));
2034 }
2035
2036 #[test]
2040 fn test_select_latest_matching_excludes_dot_separated_alpha_suffix() {
2041 use deps_core::{Registry, VersionReq};
2042
2043 let cache = Arc::new(HttpCache::new());
2044 let registry = PackagistRegistry::new(cache);
2045 let versions: Vec<Box<dyn deps_core::Version>> = vec![
2046 Box::new(ComposerVersion {
2047 version: "2.6.3.alpha".into(),
2048 version_normalized: "2.6.3.0-alpha".into(),
2049 abandoned: false,
2050 deprecation: None,
2051 published_at: None,
2052 }),
2053 Box::new(ComposerVersion {
2054 version: "2.6.2".into(),
2055 version_normalized: "2.6.2.0".into(),
2056 abandoned: false,
2057 deprecation: None,
2058 published_at: None,
2059 }),
2060 ];
2061 let req = VersionReq::new(">=2.0");
2062 assert_eq!(registry.select_latest_matching(&versions, &req), Some(1));
2063 }
2064
2065 #[test]
2070 fn test_select_latest_matching_allows_dot_separated_alpha_pin_when_requirement_names_it() {
2071 use deps_core::{Registry, VersionReq};
2072
2073 let cache = Arc::new(HttpCache::new());
2074 let registry = PackagistRegistry::new(cache);
2075 let versions: Vec<Box<dyn deps_core::Version>> = vec![
2076 Box::new(ComposerVersion {
2077 version: "2.6.3.alpha".into(),
2078 version_normalized: "2.6.3.0-alpha".into(),
2079 abandoned: false,
2080 deprecation: None,
2081 published_at: None,
2082 }),
2083 Box::new(ComposerVersion {
2084 version: "2.6.2".into(),
2085 version_normalized: "2.6.2.0".into(),
2086 abandoned: false,
2087 deprecation: None,
2088 published_at: None,
2089 }),
2090 ];
2091 let req = VersionReq::new("2.6.3.alpha");
2092 assert_eq!(registry.select_latest_matching(&versions, &req), Some(0));
2093 }
2094
2095 #[tokio::test]
2096 #[ignore]
2097 async fn test_fetch_real_monolog_versions() {
2098 let cache = Arc::new(HttpCache::new());
2099 let registry = PackagistRegistry::new(cache);
2100 let versions = registry.get_versions("monolog/monolog").await.unwrap();
2101
2102 assert!(!versions.is_empty());
2103 assert!(
2104 versions
2105 .iter()
2106 .any(|v| v.version.as_str().starts_with("3."))
2107 );
2108 }
2109
2110 #[tokio::test]
2111 #[ignore]
2112 async fn test_search_real() {
2113 let cache = Arc::new(HttpCache::new());
2114 let registry = PackagistRegistry::new(cache);
2115 let results = registry.search("symfony", 5).await.unwrap();
2116
2117 assert!(!results.is_empty());
2118 }
2119}