Skip to main content

deps_dart/
registry.rs

1//! pub.dev registry client.
2
3use crate::types::{DartVersion, PackageInfo};
4use crate::version::compare_versions;
5use deps_core::{DepsError, HttpCache, Result, is_dot_segment, lsp_helpers::warn_rejected_value};
6use serde::Deserialize;
7use std::any::Any;
8use std::sync::Arc;
9
10const PUB_DEV_API_BASE: &str = "https://pub.dev/api";
11
12/// Display name for pub.dev used in not-found and API-response error messages.
13pub const REGISTRY: &str = "pub.dev";
14
15/// Returns the URL for a package's page on pub.dev.
16///
17/// Display link only, never fetched by this process — unlike `package_metadata_url`
18/// (a fetch sink), so it is deliberately not gated against a `.`/`..` name (see
19/// [`deps_core::is_dot_segment`]'s doc for the fetch-sink-vs-display-link scope split, #379).
20pub fn package_url(name: &str) -> String {
21    format!("https://pub.dev/packages/{}", urlencoding::encode(name))
22}
23
24/// Builds the pub.dev registry request URL for a package's metadata (versions or info —
25/// both live at the same `/api/packages/{name}` endpoint).
26///
27/// Unlike `package_url` (a display link, never fetched), this is a fetch sink: `name` is
28/// percent-encoded, but a `name` of exactly `.`/`..` survives encoding unchanged (`.` is an
29/// unreserved character) and is only rejected by the caller's [`is_dot_segment`] guard, run
30/// *before* this function — see that predicate's doc for why encoding alone is insufficient
31/// (#349). Takes `base` (rather than reading [`PUB_DEV_API_BASE`] directly) so tests can
32/// point it at a mockito server.
33fn package_metadata_url(base: &str, name: &str) -> String {
34    format!("{base}/packages/{}", urlencoding::encode(name))
35}
36
37/// Rejects a dot-segment `name` before it would reach [`package_metadata_url`], as
38/// `DepsError::PackageNotFound` — mirroring `deps-npm`'s identical guard for the same
39/// vulnerability class (#341/#349): percent-encoding a pub.dev package name does not stop
40/// the URL parser's dot-segment normalization from retargeting the request (`..` escapes
41/// the `/api/packages/` prefix entirely; `.` collapses it to `/api/packages/`).
42fn reject_dot_segment(name: &str) -> Result<()> {
43    if is_dot_segment(name) {
44        warn_rejected_value(
45            "is_dot_segment",
46            "pub.dev package metadata request URL",
47            name,
48        );
49        return Err(DepsError::PackageNotFound {
50            package: name.to_string(),
51            registry: REGISTRY,
52        });
53    }
54    Ok(())
55}
56
57#[derive(Clone)]
58pub struct PubDevRegistry {
59    cache: Arc<HttpCache>,
60    /// API base URL — [`PUB_DEV_API_BASE`] in production, overridden to a mockito server
61    /// URL in tests via [`Self::with_base`] (mirrors `deps-npm`'s
62    /// `NpmRegistry::with_registry_base`).
63    base: String,
64}
65
66impl PubDevRegistry {
67    pub fn new(cache: Arc<HttpCache>) -> Self {
68        Self {
69            cache,
70            base: PUB_DEV_API_BASE.to_string(),
71        }
72    }
73
74    /// Creates a registry client pointed at a custom API base URL, for pointing at a
75    /// mockito server in tests.
76    #[cfg(test)]
77    fn with_base(cache: Arc<HttpCache>, base: String) -> Self {
78        Self { cache, base }
79    }
80
81    pub async fn get_versions(&self, name: &str) -> Result<Vec<DartVersion>> {
82        reject_dot_segment(name)?;
83        let url = package_metadata_url(&self.base, name);
84        let data = self.cache.get_cached(&url).await?;
85        parse_versions_response(&data)
86    }
87
88    pub async fn get_latest_matching(
89        &self,
90        name: &str,
91        req_str: &str,
92    ) -> Result<Option<DartVersion>> {
93        let versions = self.get_versions(name).await?;
94        Ok(versions.into_iter().find(|v| {
95            crate::version::version_matches_constraint(v.version.as_str(), req_str) && !v.retracted
96        }))
97    }
98
99    pub async fn search(&self, query: &str, limit: usize) -> Result<Vec<PackageInfo>> {
100        let url = format!("{}/search?q={}", self.base, urlencoding::encode(query));
101        let data = self.cache.get_cached(&url).await?;
102        let search_result: SearchResponse = deps_core::parse_json_checked(&data)?;
103
104        let mut results = Vec::new();
105        for entry in search_result.packages.into_iter().take(limit) {
106            // Fetch metadata for each package. `entry.package` is registry-derived (the
107            // search response), exactly as untrusted as a manifest-declared name — routed
108            // through the same guard + encoding as `get_versions`/`get_package_info` rather
109            // than interpolated directly (#349).
110            if reject_dot_segment(&entry.package).is_err() {
111                continue;
112            }
113            let pkg_url = package_metadata_url(&self.base, &entry.package);
114            if let Ok(pkg_data) = self.cache.get_cached(&pkg_url).await
115                && let Ok(info) = parse_package_info(&pkg_data)
116            {
117                results.push(info);
118            }
119        }
120
121        Ok(results)
122    }
123
124    pub async fn get_package_info(&self, name: &str) -> Result<PackageInfo> {
125        reject_dot_segment(name)?;
126        let url = package_metadata_url(&self.base, name);
127        let data = self.cache.get_cached(&url).await?;
128        parse_package_info(&data)
129    }
130}
131
132#[derive(Deserialize)]
133struct PackageResponse {
134    name: String,
135    latest: VersionDetail,
136    versions: Vec<VersionEntry>,
137}
138
139#[derive(Deserialize)]
140struct VersionEntry {
141    version: String,
142    #[serde(default)]
143    retracted: bool,
144    published: Option<String>,
145}
146
147#[derive(Deserialize)]
148struct VersionDetail {
149    version: String,
150    pubspec: Option<PubspecMeta>,
151}
152
153#[derive(Deserialize)]
154struct PubspecMeta {
155    name: Option<String>,
156    description: Option<String>,
157    homepage: Option<String>,
158    repository: Option<String>,
159    documentation: Option<String>,
160}
161
162#[derive(Deserialize)]
163struct SearchResponse {
164    #[serde(default)]
165    packages: Vec<SearchEntry>,
166}
167
168#[derive(Deserialize)]
169struct SearchEntry {
170    package: String,
171}
172
173fn parse_versions_response(data: &[u8]) -> Result<Vec<DartVersion>> {
174    let response: PackageResponse = deps_core::parse_json_checked(data)?;
175
176    let mut versions: Vec<DartVersion> = response
177        .versions
178        .into_iter()
179        .map(|e| DartVersion {
180            version: e.version.into(),
181            retracted: e.retracted,
182            published_at: e
183                .published
184                .as_deref()
185                .and_then(deps_core::PublishTime::parse_rfc3339),
186        })
187        .collect();
188
189    versions.sort_by(|a, b| compare_versions(b.version.as_str(), a.version.as_str()));
190
191    Ok(versions)
192}
193
194fn parse_package_info(data: &[u8]) -> Result<PackageInfo> {
195    let response: PackageResponse = deps_core::parse_json_checked(data)?;
196    let pubspec = response.latest.pubspec.unwrap_or(PubspecMeta {
197        name: Some(response.name.clone()),
198        description: None,
199        homepage: None,
200        repository: None,
201        documentation: None,
202    });
203
204    Ok(PackageInfo {
205        name: pubspec.name.unwrap_or(response.name).into(),
206        description: pubspec.description,
207        homepage: pubspec.homepage,
208        repository: pubspec.repository,
209        documentation: pubspec.documentation,
210        version: response.latest.version.into(),
211        license: None,
212    })
213}
214
215impl deps_core::Version for DartVersion {
216    fn version_string(&self) -> &deps_core::ConcreteVersion {
217        &self.version
218    }
219
220    fn removal_status(&self) -> deps_core::RemovalStatus {
221        deps_core::RemovalStatus::from_yanked(self.retracted)
222    }
223
224    fn is_prerelease(&self) -> bool {
225        crate::version::is_prerelease(self.version.as_str())
226    }
227
228    fn published_at(&self) -> Option<deps_core::PublishTime> {
229        self.published_at
230    }
231
232    fn as_any(&self) -> &dyn std::any::Any {
233        self
234    }
235}
236
237impl deps_core::Metadata for PackageInfo {
238    fn name(&self) -> &deps_core::PackageName {
239        &self.name
240    }
241
242    fn description(&self) -> Option<&str> {
243        self.description.as_deref()
244    }
245
246    fn repository(&self) -> Option<&str> {
247        self.repository.as_deref()
248    }
249
250    fn documentation(&self) -> Option<&str> {
251        self.documentation.as_deref()
252    }
253
254    fn latest_version(&self) -> &deps_core::ConcreteVersion {
255        &self.version
256    }
257
258    fn as_any(&self) -> &dyn std::any::Any {
259        self
260    }
261}
262
263// Registry trait (trait object support)
264impl deps_core::Registry for PubDevRegistry {
265    fn get_versions<'a>(
266        &'a self,
267        name: &'a deps_core::PackageName,
268    ) -> deps_core::ecosystem::BoxFuture<'a, Result<Vec<Box<dyn deps_core::Version>>>> {
269        Box::pin(async move {
270            let versions = self.get_versions(name.as_str()).await?;
271            Ok(versions
272                .into_iter()
273                .map(|v| Box::new(v) as Box<dyn deps_core::Version>)
274                .collect())
275        })
276    }
277
278    fn get_latest_matching<'a>(
279        &'a self,
280        name: &'a deps_core::PackageName,
281        req: &'a deps_core::VersionReq,
282    ) -> deps_core::ecosystem::BoxFuture<'a, Result<Option<Box<dyn deps_core::Version>>>> {
283        Box::pin(async move {
284            let version = self
285                .get_latest_matching(name.as_str(), req.as_str())
286                .await?;
287            Ok(version.map(|v| Box::new(v) as Box<dyn deps_core::Version>))
288        })
289    }
290
291    fn search<'a>(
292        &'a self,
293        query: &'a str,
294        limit: usize,
295    ) -> deps_core::ecosystem::BoxFuture<'a, Result<Vec<Box<dyn deps_core::Metadata>>>> {
296        Box::pin(async move {
297            let results = self.search(query, limit).await?;
298            Ok(results
299                .into_iter()
300                .map(|m| Box::new(m) as Box<dyn deps_core::Metadata>)
301                .collect())
302        })
303    }
304
305    fn select_latest_matching(
306        &self,
307        versions: &[Box<dyn deps_core::Version>],
308        req: &deps_core::VersionReq,
309    ) -> Option<usize> {
310        if deps_core::is_existence_wildcard(req) {
311            return deps_core::select_latest_for_existence(versions, |v| v.as_ref());
312        }
313        versions.iter().position(|v| {
314            crate::version::version_matches_constraint(v.version_string().as_str(), req.as_str())
315                && !v.removal_status().blocks_resolution()
316        })
317    }
318
319    fn as_any(&self) -> &dyn Any {
320        self
321    }
322}
323
324#[cfg(test)]
325mod tests {
326    use super::*;
327
328    use std::assert_matches;
329
330    #[test]
331    fn test_package_url() {
332        assert_eq!(package_url("provider"), "https://pub.dev/packages/provider");
333        assert_eq!(package_url("http"), "https://pub.dev/packages/http");
334    }
335
336    #[test]
337    fn test_package_url_encodes_malicious_name() {
338        let url = package_url("evil](https://evil.example)[pkg");
339        assert!(!url.contains('('));
340        assert!(!url.contains(')'));
341        assert!(!url.contains('['));
342        assert!(!url.contains(']'));
343    }
344
345    #[test]
346    fn test_package_url_encodes_newline_autolink_and_percent() {
347        let url = package_url("evil\n<https://evil%zz.example>");
348        assert!(!url.contains('\n'));
349        assert!(!url.contains('<'));
350        assert!(!url.contains('>'));
351        assert!(url.contains("%25"));
352    }
353
354    #[test]
355    fn test_package_url_empty_name() {
356        assert_eq!(package_url(""), "https://pub.dev/packages/");
357    }
358
359    #[test]
360    fn test_parse_versions_response() {
361        let json = r#"{
362            "name": "http",
363            "latest": {"version": "1.2.0", "pubspec": {"name": "http"}},
364            "versions": [
365                {"version": "1.0.0", "retracted": false},
366                {"version": "1.2.0", "retracted": false},
367                {"version": "1.1.0", "retracted": false},
368                {"version": "0.9.0", "retracted": true}
369            ]
370        }"#;
371
372        let versions = parse_versions_response(json.as_bytes()).unwrap();
373        assert_eq!(versions.len(), 4);
374        assert_eq!(versions[0].version, "1.2.0");
375        assert_eq!(versions[1].version, "1.1.0");
376        assert_eq!(versions[2].version, "1.0.0");
377        assert!(versions[3].retracted);
378    }
379
380    #[test]
381    fn test_parse_versions_response_with_published() {
382        let json = r#"{
383            "name": "http",
384            "latest": {"version": "1.2.0"},
385            "versions": [
386                {"version": "1.2.0", "retracted": false, "published": "2025-03-10T14:22:05.123Z"}
387            ]
388        }"#;
389
390        let versions = parse_versions_response(json.as_bytes()).unwrap();
391        assert_eq!(versions.len(), 1);
392        assert_eq!(
393            versions[0].published_at,
394            deps_core::PublishTime::parse_rfc3339("2025-03-10T14:22:05.123Z")
395        );
396    }
397
398    #[test]
399    fn test_parse_versions_response_without_published() {
400        let json = r#"{
401            "name": "http",
402            "latest": {"version": "1.2.0"},
403            "versions": [
404                {"version": "1.2.0", "retracted": false}
405            ]
406        }"#;
407
408        let versions = parse_versions_response(json.as_bytes()).unwrap();
409        assert_eq!(versions.len(), 1);
410        assert!(versions[0].published_at.is_none());
411    }
412
413    #[test]
414    fn test_parse_versions_response_with_malformed_published() {
415        let json = r#"{
416            "name": "http",
417            "latest": {"version": "1.2.0"},
418            "versions": [
419                {"version": "1.2.0", "retracted": false, "published": "not-a-timestamp"}
420            ]
421        }"#;
422
423        let versions = parse_versions_response(json.as_bytes()).unwrap();
424        assert_eq!(versions.len(), 1);
425        assert!(
426            versions[0].published_at.is_none(),
427            "malformed published degrades to None, not an error"
428        );
429    }
430
431    #[test]
432    fn test_parse_versions_response_empty() {
433        let json = r#"{
434            "name": "test",
435            "latest": {"version": "1.0.0"},
436            "versions": []
437        }"#;
438        let versions = parse_versions_response(json.as_bytes()).unwrap();
439        assert!(versions.is_empty());
440    }
441
442    #[test]
443    fn test_parse_package_info() {
444        let json = r#"{
445            "name": "provider",
446            "latest": {
447                "version": "6.1.2",
448                "pubspec": {
449                    "name": "provider",
450                    "description": "A wrapper around InheritedWidget",
451                    "homepage": "https://pub.dev/packages/provider",
452                    "repository": "https://github.com/rrousselGit/provider",
453                    "documentation": "https://pub.dev/documentation/provider"
454                }
455            },
456            "versions": []
457        }"#;
458
459        let info = parse_package_info(json.as_bytes()).unwrap();
460        assert_eq!(info.name, "provider");
461        assert_eq!(
462            info.description,
463            Some("A wrapper around InheritedWidget".into())
464        );
465        assert_eq!(info.version, "6.1.2");
466    }
467
468    #[test]
469    fn test_parse_package_info_minimal() {
470        let json = r#"{
471            "name": "minimal",
472            "latest": {"version": "0.1.0"},
473            "versions": []
474        }"#;
475
476        let info = parse_package_info(json.as_bytes()).unwrap();
477        assert_eq!(info.name, "minimal");
478        assert_eq!(info.version, "0.1.0");
479        assert!(info.description.is_none());
480    }
481
482    #[test]
483    fn test_parse_search_response() {
484        let json = r#"{
485            "packages": [
486                {"package": "provider"},
487                {"package": "riverpod"}
488            ]
489        }"#;
490        let response: SearchResponse = serde_json::from_slice(json.as_bytes()).unwrap();
491        assert_eq!(response.packages.len(), 2);
492        assert_eq!(response.packages[0].package, "provider");
493    }
494
495    #[test]
496    fn test_registry_creation() {
497        let cache = Arc::new(HttpCache::new());
498        let _registry = PubDevRegistry::new(cache);
499    }
500
501    /// #349: `../../search` must not escape the `/api/packages/` prefix via dot-segment
502    /// normalization — the raw `.`/`/` characters have to be percent-encoded. Asserted on
503    /// the *parsed* URL's path (`url::Url::parse`), not the raw format-string output: a
504    /// `!url.contains("../")` check alone would pass even while the unencoded segments
505    /// still normalized away the `packages` path component at the transport layer. Calls
506    /// the actual production `package_metadata_url` helper, not a re-implemented copy of
507    /// its `format!`, so a future encoding regression here fails this test.
508    #[test]
509    fn test_package_metadata_url_encodes_path_traversal() {
510        let name = "../../search";
511        let url = package_metadata_url(PUB_DEV_API_BASE, name);
512        let parsed = url::Url::parse(&url).unwrap();
513        let segments: Vec<&str> = parsed.path_segments().unwrap().collect();
514        assert_eq!(segments.len(), 3, "segments: {segments:?}");
515        assert_eq!(segments[0], "api");
516        assert_eq!(segments[1], "packages");
517        assert_eq!(urlencoding::decode(segments[2]).unwrap(), name);
518    }
519
520    // --- S1 (impl-critic): a name of exactly `.`/`..` survives percent-encoding (`.` is
521    // an unreserved RFC 3986 character) and is collapsed by the URL parser's dot-segment
522    // normalization — identical to #341's npm bug, reachable here via `get_versions`,
523    // `get_package_info`, and search's inner per-result fetch. `reject_dot_segment` must
524    // catch it before `package_metadata_url` is ever called. ---
525
526    /// Demonstrates the vulnerability `reject_dot_segment` exists to prevent:
527    /// `package_metadata_url` alone (with no caller-side guard) builds a URL that, once
528    /// parsed, has already lost the `packages` path component — `..` normalizes two
529    /// levels up to the bare `/api/` root instead of a 404 for a literal package named
530    /// `..`.
531    #[test]
532    fn test_package_metadata_url_dot_dot_normalizes_above_packages_prefix() {
533        let url = package_metadata_url(PUB_DEV_API_BASE, "..");
534        let parsed = url::Url::parse(&url).unwrap();
535        assert_eq!(parsed.path(), "/api/", "parsed path: {}", parsed.path());
536    }
537
538    #[test]
539    fn test_reject_dot_segment_rejects_bare_dot_dot() {
540        assert!(reject_dot_segment("..").is_err());
541    }
542
543    #[test]
544    fn test_reject_dot_segment_rejects_bare_dot() {
545        assert!(reject_dot_segment(".").is_err());
546    }
547
548    #[test]
549    fn test_reject_dot_segment_accepts_normal_names() {
550        assert!(reject_dot_segment("provider").is_ok());
551        assert!(reject_dot_segment("../../search").is_ok());
552    }
553
554    /// #365 regression sweep: exercises the real production pair (`is_dot_segment` gate +
555    /// `package_metadata_url` sink) against the shared adversarial input set, guarding
556    /// against a 6th recurrence of #349's defect class.
557    #[test]
558    fn test_package_metadata_url_dot_segment_sweep() {
559        deps_core::test_util::assert_dot_segment_gated_or_contained(
560            |seg| (!is_dot_segment(seg)).then(|| package_metadata_url(PUB_DEV_API_BASE, seg)),
561            "pub.dev",
562            "/api/packages/",
563        );
564    }
565
566    #[tokio::test]
567    async fn test_get_versions_rejects_bare_dot_dot_as_not_found() {
568        // #365 R1: asserts the exact `PackageNotFound` variant (gate rejected before any
569        // request), not the broader `is_not_found()` (also true for a live 404 `HttpStatus`)
570        // — pub.dev 404ing for this path today would make a deleted gate go undetected by
571        // this test.
572        let registry = PubDevRegistry::new(Arc::new(HttpCache::new()));
573        let err = registry.get_versions("..").await.unwrap_err();
574        assert_matches!(err, DepsError::PackageNotFound { .. });
575    }
576
577    #[tokio::test]
578    async fn test_get_package_info_rejects_bare_dot_as_not_found() {
579        let registry = PubDevRegistry::new(Arc::new(HttpCache::new()));
580        let err = registry.get_package_info(".").await.unwrap_err();
581        assert_matches!(err, DepsError::PackageNotFound { .. });
582    }
583
584    // --- S2 (impl-critic): `search`'s inner per-result fetch (`entry.package`) was
585    // fully unencoded and unguarded, 12 lines below the encoded `get_versions` sink — a
586    // missed #349 call site. Covered end-to-end via mockito, matching the guard/encoding
587    // now shared with `get_versions`/`get_package_info` through `package_metadata_url` and
588    // `reject_dot_segment`. ---
589
590    #[tokio::test]
591    async fn test_search_inner_fetch_encodes_malicious_package_name() {
592        let mut server = mockito::Server::new_async().await;
593        let search_mock = server
594            .mock("GET", "/search?q=widget")
595            .with_status(200)
596            .with_body(r#"{"packages": [{"package": "../../search"}]}"#)
597            .create_async()
598            .await;
599        // The traversal-shaped name must be percent-encoded into a single opaque path
600        // segment under `/api/packages/`, never resolved as a literal traversal against
601        // the mock server's own routes.
602        let pkg_mock = server
603            .mock("GET", "/packages/..%2F..%2Fsearch")
604            .with_status(200)
605            .with_body(r#"{"name": "search", "latest": {"version": "1.0.0"}, "versions": []}"#)
606            .create_async()
607            .await;
608
609        let registry = PubDevRegistry::with_base(Arc::new(HttpCache::new()), server.url());
610        let results = registry.search("widget", 10).await.unwrap();
611
612        assert_eq!(results.len(), 1);
613        search_mock.assert_async().await;
614        pkg_mock.assert_async().await;
615    }
616
617    #[tokio::test]
618    async fn test_search_inner_fetch_skips_dot_segment_package_name() {
619        let mut server = mockito::Server::new_async().await;
620        let search_mock = server
621            .mock("GET", "/search?q=widget")
622            .with_status(200)
623            .with_body(r#"{"packages": [{"package": ".."}, {"package": "provider"}]}"#)
624            .create_async()
625            .await;
626        let pkg_mock = server
627            .mock("GET", "/packages/provider")
628            .with_status(200)
629            .with_body(r#"{"name": "provider", "latest": {"version": "1.0.0"}, "versions": []}"#)
630            .create_async()
631            .await;
632        // No mock registered for a request resolving to the bare `/api/` root — if the
633        // dot-segment guard regressed, that unexpected request would fail the test.
634
635        let registry = PubDevRegistry::with_base(Arc::new(HttpCache::new()), server.url());
636        let results = registry.search("widget", 10).await.unwrap();
637
638        assert_eq!(
639            results.len(),
640            1,
641            "the dot-segment entry must be skipped, not fetched"
642        );
643        search_mock.assert_async().await;
644        pkg_mock.assert_async().await;
645    }
646
647    #[test]
648    fn test_version_trait() {
649        use deps_core::Version;
650        let ver = DartVersion {
651            version: "1.0.0".into(),
652            retracted: true,
653            published_at: None,
654        };
655        assert_eq!(ver.version_string(), "1.0.0");
656        assert!(ver.removal_status().blocks_resolution());
657        assert!(ver.features().is_empty());
658    }
659
660    #[test]
661    fn test_metadata_trait() {
662        use deps_core::Metadata;
663        let info = PackageInfo {
664            name: "test".into(),
665            description: Some("A test package".into()),
666            homepage: None,
667            repository: Some("https://github.com/test/test".into()),
668            documentation: None,
669            version: "1.0.0".into(),
670            license: None,
671        };
672        assert_eq!(info.name(), "test");
673        assert_eq!(info.description(), Some("A test package"));
674        assert_eq!(info.repository(), Some("https://github.com/test/test"));
675        assert!(info.documentation().is_none());
676    }
677
678    #[test]
679    fn test_registry_as_any() {
680        use deps_core::Registry;
681        let cache = Arc::new(HttpCache::new());
682        let registry = PubDevRegistry::new(cache);
683        assert!(registry.as_any().is::<PubDevRegistry>());
684    }
685
686    #[test]
687    fn test_select_latest_matching_not_default_none() {
688        use deps_core::{Registry, VersionReq};
689
690        let cache = Arc::new(HttpCache::new());
691        let registry = PubDevRegistry::new(cache);
692        let versions: Vec<Box<dyn deps_core::Version>> = vec![
693            Box::new(DartVersion {
694                version: "2.0.0".into(),
695                retracted: true,
696                published_at: None,
697            }),
698            Box::new(DartVersion {
699                version: "1.0.0".into(),
700                retracted: false,
701                published_at: None,
702            }),
703        ];
704        let req = VersionReq::new("*");
705        assert_eq!(registry.select_latest_matching(&versions, &req), Some(1));
706    }
707
708    #[test]
709    fn test_select_latest_matching_all_retracted_returns_newest_retracted() {
710        use deps_core::{Registry, VersionReq};
711
712        let cache = Arc::new(HttpCache::new());
713        let registry = PubDevRegistry::new(cache);
714        let versions: Vec<Box<dyn deps_core::Version>> = vec![
715            Box::new(DartVersion {
716                version: "2.0.0".into(),
717                retracted: true,
718                published_at: None,
719            }),
720            Box::new(DartVersion {
721                version: "1.0.0".into(),
722                retracted: true,
723                published_at: None,
724            }),
725        ];
726        let req = VersionReq::new("*");
727        assert_eq!(registry.select_latest_matching(&versions, &req), Some(0));
728    }
729
730    #[test]
731    fn test_select_latest_matching_all_prerelease_returns_newest_prerelease() {
732        use deps_core::{Registry, VersionReq};
733
734        let cache = Arc::new(HttpCache::new());
735        let registry = PubDevRegistry::new(cache);
736        let versions: Vec<Box<dyn deps_core::Version>> = vec![
737            Box::new(DartVersion {
738                version: "2.0.0-beta.1".into(),
739                retracted: false,
740                published_at: None,
741            }),
742            Box::new(DartVersion {
743                version: "1.0.0-alpha.1".into(),
744                retracted: false,
745                published_at: None,
746            }),
747        ];
748        let req = VersionReq::new("*");
749        assert_eq!(registry.select_latest_matching(&versions, &req), Some(0));
750    }
751}