1#![deny(missing_docs)]
2
3use crate::error::Error;
4use crate::intern::{
5 InternKey, StringInterner, intern_option, intern_vec, resolve_option, resolve_vec,
6};
7use serde::{Deserialize, Serialize};
8use std::collections::BTreeSet;
9use std::collections::HashMap;
10use std::collections::HashSet;
11use time::OffsetDateTime;
12
13#[derive(Debug, Clone)]
32pub struct PackageIndex {
33 pub(crate) interner: StringInterner,
34 pub(crate) names: Vec<Option<InternKey>>,
35 pub(crate) full_names: Vec<Option<InternKey>>,
36 pub(crate) owners: Vec<Option<InternKey>>,
37 pub(crate) package_urls: Vec<Option<InternKey>>,
38 pub(crate) dates_created: Vec<OffsetDateTime>,
39 pub(crate) dates_updated: Vec<OffsetDateTime>,
40 pub(crate) uuid4s: Vec<Option<InternKey>>,
41 pub(crate) rating_scores: Vec<Option<u32>>,
42 pub(crate) is_pinned: Vec<Option<bool>>,
43 pub(crate) is_deprecated: Vec<Option<bool>>,
44 pub(crate) has_nsfw_content: Vec<Option<bool>>,
45 pub(crate) categories: Vec<Vec<InternKey>>,
46 pub(crate) version_ranges: Vec<(usize, usize)>,
47 pub(crate) versions: VersionTable,
48}
49
50#[derive(Debug, Clone)]
56pub(crate) struct VersionTable {
57 pub package_indices: Vec<usize>,
58 pub names: Vec<Option<InternKey>>,
59 pub full_names: Vec<Option<InternKey>>,
60 pub version_numbers: Vec<Option<InternKey>>,
61 pub download_urls: Vec<Option<InternKey>>,
62 pub dependencies: Vec<Vec<InternKey>>,
63 pub dates_created: Vec<OffsetDateTime>,
64 pub descriptions: Vec<Option<InternKey>>,
65 pub icons: Vec<Option<InternKey>>,
66 pub downloads: Vec<Option<u32>>,
67 pub website_urls: Vec<Option<InternKey>>,
68 pub is_active: Vec<Option<bool>>,
69 pub uuid4s: Vec<Option<InternKey>>,
70 pub file_sizes: Vec<Option<u64>>,
71}
72
73pub(crate) const CACHE_FORMAT_VERSION: u32 = 2;
82
83#[derive(bincode::Encode, bincode::Decode, Debug, Clone)]
103pub(crate) struct SerializedPackageIndex {
104 pub format_version: u32,
107 pub string_table: Vec<String>,
108 pub names: Vec<Option<u32>>,
109 pub full_names: Vec<Option<u32>>,
110 pub owners: Vec<Option<u32>>,
111 pub package_urls: Vec<Option<u32>>,
112 pub dates_created: Vec<i128>,
114 pub dates_updated: Vec<i128>,
115 pub uuid4s: Vec<Option<u32>>,
116 pub rating_scores: Vec<Option<u32>>,
117 pub is_pinned: Vec<Option<bool>>,
118 pub is_deprecated: Vec<Option<bool>>,
119 pub has_nsfw_content: Vec<Option<bool>>,
120 pub categories: Vec<Vec<u32>>,
121 pub version_ranges: Vec<(usize, usize)>,
122 pub versions: SerializedVersionTable,
123}
124
125#[derive(bincode::Encode, bincode::Decode, Debug, Clone)]
131pub(crate) struct SerializedVersionTable {
132 pub package_indices: Vec<usize>,
133 pub names: Vec<Option<u32>>,
134 pub full_names: Vec<Option<u32>>,
135 pub version_numbers: Vec<Option<u32>>,
136 pub download_urls: Vec<Option<u32>>,
137 pub dependencies: Vec<Vec<u32>>,
138 pub dates_created: Vec<i128>,
140 pub descriptions: Vec<Option<u32>>,
141 pub icons: Vec<Option<u32>>,
142 pub downloads: Vec<Option<u32>>,
143 pub website_urls: Vec<Option<u32>>,
144 pub is_active: Vec<Option<bool>>,
145 pub uuid4s: Vec<Option<u32>>,
146 pub file_sizes: Vec<Option<u64>>,
147}
148
149impl PackageIndex {
150 pub fn len(&self) -> usize {
152 self.names.len()
153 }
154
155 #[allow(dead_code)]
157 pub fn is_empty(&self) -> bool {
158 self.names.is_empty()
159 }
160
161 pub fn resolve_name_at(&self, idx: usize) -> Option<String> {
164 resolve_option(&self.interner, *self.names.get(idx)?)
165 }
166
167 pub fn resolve_full_name_at(&self, idx: usize) -> Option<String> {
170 resolve_option(&self.interner, *self.full_names.get(idx)?)
171 }
172
173 pub fn search(&self, term: &str) -> Vec<usize> {
180 let needle = term.to_lowercase();
181
182 (0..self.len())
183 .filter(|&idx| {
184 if let Some(name) = self.resolve_name_at(idx)
185 && name.to_lowercase().contains(&needle)
186 {
187 return true;
188 }
189
190 self
191 .resolve_full_name_at(idx)
192 .is_some_and(|full_name| full_name.to_lowercase().contains(&needle))
193 })
194 .collect()
195 }
196
197 pub fn resolve_owner_at(&self, idx: usize) -> Option<String> {
200 resolve_option(&self.interner, *self.owners.get(idx)?)
201 }
202
203 pub fn name_at(&self, idx: usize) -> Option<&str> {
209 self
210 .names
211 .get(idx)
212 .copied()
213 .flatten()
214 .map(|k| self.interner.resolve(&k))
215 }
216
217 pub fn full_name_at(&self, idx: usize) -> Option<&str> {
223 self
224 .full_names
225 .get(idx)
226 .copied()
227 .flatten()
228 .map(|k| self.interner.resolve(&k))
229 }
230
231 pub fn owner_at(&self, idx: usize) -> Option<&str> {
237 self
238 .owners
239 .get(idx)
240 .copied()
241 .flatten()
242 .map(|k| self.interner.resolve(&k))
243 }
244
245 #[allow(dead_code)]
248 pub fn get_package_by_full_name(&self, full_name: &str) -> Option<Package> {
249 let idx = self.find_index_by_full_name(full_name)?;
250
251 self.get_package_at(idx)
252 }
253
254 #[allow(dead_code)]
257 pub fn find_index_by_full_name(&self, full_name: &str) -> Option<usize> {
258 self.full_names.iter().position(|key| {
259 key
260 .map(|k| self.interner.resolve(&k) == full_name)
261 .unwrap_or(false)
262 })
263 }
264
265 #[allow(dead_code)]
268 pub fn get_package_at(&self, idx: usize) -> Option<Package> {
269 let (version_start, version_end) = *self.version_ranges.get(idx)?;
270 let mut versions = Vec::with_capacity(version_end - version_start);
271
272 for ver_idx in version_start..version_end {
273 versions.push(Version {
274 name: resolve_option(&self.interner, self.versions.names[ver_idx]),
275 full_name: resolve_option(&self.interner, self.versions.full_names[ver_idx]),
276 description: resolve_option(&self.interner, self.versions.descriptions[ver_idx]),
277 icon: resolve_option(&self.interner, self.versions.icons[ver_idx]),
278 version_number: resolve_option(&self.interner, self.versions.version_numbers[ver_idx]),
279 dependencies: resolve_vec(&self.interner, &self.versions.dependencies[ver_idx]),
280 download_url: resolve_option(&self.interner, self.versions.download_urls[ver_idx]),
281 downloads: self.versions.downloads[ver_idx],
282 date_created: self.versions.dates_created[ver_idx],
283 website_url: resolve_option(&self.interner, self.versions.website_urls[ver_idx]),
284 is_active: self.versions.is_active[ver_idx],
285 uuid4: resolve_option(&self.interner, self.versions.uuid4s[ver_idx]),
286 file_size: self.versions.file_sizes[ver_idx],
287 });
288 }
289
290 Some(Package {
291 name: resolve_option(&self.interner, self.names[idx]),
292 full_name: resolve_option(&self.interner, self.full_names[idx]),
293 owner: resolve_option(&self.interner, self.owners[idx]),
294 package_url: resolve_option(&self.interner, self.package_urls[idx]),
295 date_created: self.dates_created[idx],
296 date_updated: self.dates_updated[idx],
297 uuid4: resolve_option(&self.interner, self.uuid4s[idx]),
298 rating_score: self.rating_scores[idx],
299 is_pinned: self.is_pinned[idx],
300 is_deprecated: self.is_deprecated[idx],
301 has_nsfw_content: self.has_nsfw_content[idx],
302 categories: resolve_vec(&self.interner, &self.categories[idx]),
303 versions,
304 })
305 }
306
307 pub(crate) fn get_latest_version_at(&self, idx: usize) -> Option<usize> {
316 let (ver_start, ver_end) = *self.version_ranges.get(idx)?;
317
318 if ver_start >= ver_end {
319 return None;
320 }
321
322 (ver_start..ver_end).max_by_key(|&ver_idx| self.versions.dates_created[ver_idx])
323 }
324
325 pub(crate) fn get_version_at(&self, idx: usize, version_number: &str) -> Option<usize> {
327 let (ver_start, ver_end) = *self.version_ranges.get(idx)?;
328
329 (ver_start..ver_end).find(|&ver_idx| {
330 self.versions.version_numbers[ver_idx].map(|key| self.interner.resolve(&key))
331 == Some(version_number)
332 })
333 }
334
335 pub fn build_name_index(&self) -> HashMap<String, usize> {
338 self
339 .full_names
340 .iter()
341 .enumerate()
342 .filter_map(|(idx, key)| {
343 let name = resolve_option(&self.interner, *key)?;
344 Some((name, idx))
345 })
346 .collect()
347 }
348
349 pub fn validate(&self) -> crate::Result<()> {
356 self.validate_lengths()?;
357 self.validate_version_ranges()?;
358 self.validate_version_ownership()?;
359 self.validate_unique_full_names()?;
360
361 Ok(())
362 }
363
364 fn validate_lengths(&self) -> crate::Result<()> {
368 let pkg_count = self.names.len();
369
370 check_len("full_names", self.full_names.len(), pkg_count)?;
371 check_len("owners", self.owners.len(), pkg_count)?;
372 check_len("package_urls", self.package_urls.len(), pkg_count)?;
373 check_len("dates_created", self.dates_created.len(), pkg_count)?;
374 check_len("dates_updated", self.dates_updated.len(), pkg_count)?;
375 check_len("uuid4s", self.uuid4s.len(), pkg_count)?;
376 check_len("rating_scores", self.rating_scores.len(), pkg_count)?;
377 check_len("is_pinned", self.is_pinned.len(), pkg_count)?;
378 check_len("is_deprecated", self.is_deprecated.len(), pkg_count)?;
379 check_len("has_nsfw_content", self.has_nsfw_content.len(), pkg_count)?;
380 check_len("categories", self.categories.len(), pkg_count)?;
381 check_len("version_ranges", self.version_ranges.len(), pkg_count)?;
382
383 let version_count = self.versions.package_indices.len();
384
385 check_len("versions.names", self.versions.names.len(), version_count)?;
386 check_len(
387 "versions.full_names",
388 self.versions.full_names.len(),
389 version_count,
390 )?;
391 check_len(
392 "versions.version_numbers",
393 self.versions.version_numbers.len(),
394 version_count,
395 )?;
396 check_len(
397 "versions.download_urls",
398 self.versions.download_urls.len(),
399 version_count,
400 )?;
401 check_len(
402 "versions.dependencies",
403 self.versions.dependencies.len(),
404 version_count,
405 )?;
406 check_len(
407 "versions.dates_created",
408 self.versions.dates_created.len(),
409 version_count,
410 )?;
411 check_len(
412 "versions.descriptions",
413 self.versions.descriptions.len(),
414 version_count,
415 )?;
416 check_len("versions.icons", self.versions.icons.len(), version_count)?;
417 check_len(
418 "versions.downloads",
419 self.versions.downloads.len(),
420 version_count,
421 )?;
422 check_len(
423 "versions.website_urls",
424 self.versions.website_urls.len(),
425 version_count,
426 )?;
427 check_len(
428 "versions.is_active",
429 self.versions.is_active.len(),
430 version_count,
431 )?;
432 check_len("versions.uuid4s", self.versions.uuid4s.len(), version_count)?;
433 check_len(
434 "versions.file_sizes",
435 self.versions.file_sizes.len(),
436 version_count,
437 )?;
438
439 Ok(())
440 }
441
442 fn validate_version_ranges(&self) -> crate::Result<()> {
447 let pkg_count = self.names.len();
448 let version_count = self.versions.package_indices.len();
449
450 for (idx, (start, end)) in self.version_ranges.iter().enumerate() {
451 if start > end {
452 return Err(Error::Cache(format!(
453 "Invalid version range at package {}: {} > {}",
454 idx, start, end
455 )));
456 }
457
458 if *end > version_count {
459 return Err(Error::Cache(format!(
460 "Version range at package {} ends at {} but only {} versions exist",
461 idx, end, version_count
462 )));
463 }
464 }
465
466 for (i, &pkg_idx) in self.versions.package_indices.iter().enumerate() {
467 if pkg_idx >= pkg_count {
468 return Err(Error::Cache(format!(
469 "version {} references package index {} but only {} packages exist",
470 i, pkg_idx, pkg_count
471 )));
472 }
473 }
474
475 Ok(())
476 }
477
478 fn validate_version_ownership(&self) -> crate::Result<()> {
485 let version_count = self.versions.package_indices.len();
486
487 let mut owner_of: Vec<Option<usize>> = vec![None; version_count];
488
489 for (pkg_idx, range) in self.version_ranges.iter().enumerate() {
490 let (start, end) = *range;
491
492 for (offset, &owner_pkg) in self.versions.package_indices[start..end].iter().enumerate() {
493 let ver_idx = start + offset;
494
495 if owner_of[ver_idx].is_some() {
496 return Err(Error::Cache(format!(
497 "version {} is claimed by multiple packages (overlapping version ranges)",
498 ver_idx
499 )));
500 }
501
502 owner_of[ver_idx] = Some(pkg_idx);
503
504 if owner_pkg != pkg_idx {
505 return Err(Error::Cache(format!(
506 "version {} lies in package {}'s range but records package_index {}",
507 ver_idx, pkg_idx, owner_pkg
508 )));
509 }
510 }
511 }
512
513 for (ver_idx, owner) in owner_of.iter().enumerate() {
514 if owner.is_none() {
515 return Err(Error::Cache(format!(
516 "version {} is not covered by any package's version range",
517 ver_idx
518 )));
519 }
520 }
521
522 Ok(())
523 }
524
525 fn validate_unique_full_names(&self) -> crate::Result<()> {
531 let mut seen_full_names: HashSet<&str> = HashSet::new();
532
533 for key in self.full_names.iter().flatten() {
534 let full_name = self.interner.resolve(key);
535
536 if !seen_full_names.insert(full_name) {
537 return Err(Error::Cache(format!(
538 "duplicate full name {:?} appears in multiple packages",
539 full_name
540 )));
541 }
542 }
543
544 Ok(())
545 }
546}
547
548fn check_len(field: &str, actual: usize, expected: usize) -> crate::Result<()> {
551 if actual != expected {
552 return Err(Error::Cache(format!(
553 "{field} length {actual} != {expected}"
554 )));
555 }
556
557 Ok(())
558}
559
560#[derive(Serialize, Deserialize, Clone)]
568#[non_exhaustive]
569pub struct Package {
570 pub name: Option<String>,
572 pub full_name: Option<String>,
574 pub owner: Option<String>,
576 pub package_url: Option<String>,
578 #[serde(with = "time::serde::rfc3339")]
580 pub date_created: time::OffsetDateTime,
581 #[serde(with = "time::serde::rfc3339")]
583 pub date_updated: time::OffsetDateTime,
584 pub uuid4: Option<String>,
586 pub rating_score: Option<u32>,
588 pub is_pinned: Option<bool>,
590 pub is_deprecated: Option<bool>,
592 pub has_nsfw_content: Option<bool>,
594 pub categories: Vec<String>,
596 pub versions: Vec<Version>,
598}
599
600#[derive(Serialize, Deserialize, Debug, Clone)]
608#[non_exhaustive]
609pub struct Version {
610 pub name: Option<String>,
612 pub full_name: Option<String>,
614 pub description: Option<String>,
616 pub icon: Option<String>,
618 pub version_number: Option<String>,
620 pub dependencies: Vec<String>,
622 pub download_url: Option<String>,
624 pub downloads: Option<u32>,
626 #[serde(with = "time::serde::rfc3339")]
628 pub date_created: time::OffsetDateTime,
629 pub website_url: Option<String>,
631 pub is_active: Option<bool>,
633 pub uuid4: Option<String>,
635 pub file_size: Option<u64>,
637}
638
639impl Package {
640 pub fn latest_version(&self) -> Option<&Version> {
647 self.versions.iter().max_by_key(|v| v.date_created)
648 }
649
650 #[cfg(test)]
658 pub fn zip_and_url(&self) -> Option<(String, String)> {
659 let pkg = self.latest_version()?;
660 let url: String = pkg.download_url.clone()?;
661 let version_number = pkg.version_number.as_ref()?;
662 let package_name = &self.full_name.as_ref()?;
663 let zip_name = format!("{}-{}.zip", package_name, version_number);
664
665 Some((zip_name, url))
666 }
667}
668
669impl std::fmt::Debug for Package {
672 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
673 f.debug_struct("Package")
674 .field("full_name", &self.full_name)
675 .field(
676 "dependencies",
677 &self.latest_version().map(|v| &v.dependencies),
678 )
679 .field(
680 "download_url",
681 &self.latest_version().map(|v| &v.download_url),
682 )
683 .finish()
684 }
685}
686
687#[derive(Debug, Clone, Default)]
693#[non_exhaustive]
694pub struct Resolution {
695 pub urls: HashMap<String, String>,
698 pub unresolved: Vec<String>,
704}
705
706#[derive(Debug, Deserialize, Serialize)]
711pub struct DependencyGraph {
712 install_packages: Vec<String>,
714 #[serde(default, skip_serializing_if = "Vec::is_empty")]
716 pins: Vec<String>,
717}
718
719impl DependencyGraph {
720 pub fn new(install_packages: Vec<String>) -> Self {
726 Self {
727 install_packages,
728 pins: Vec::new(),
729 }
730 }
731
732 pub fn pins(mut self, pins: impl IntoIterator<Item = String>) -> Self {
735 self.pins.extend(pins);
736
737 self
738 }
739
740 pub fn resolve(&self, index: &PackageIndex) -> HashMap<String, String> {
765 self.resolve_report(index).urls
766 }
767
768 pub fn resolve_report(&self, index: &PackageIndex) -> Resolution {
780 let name_index = index.build_name_index();
781 let mut sorted_set: BTreeSet<String> = self.install_packages.iter().cloned().collect();
782 let mut install_indices = Vec::new();
783 let mut seen: HashSet<usize> = HashSet::new();
784 let mut unresolved: Vec<String> = Vec::new();
785
786 let mut pins: HashMap<&str, BTreeSet<&str>> = HashMap::new();
787
788 for full_name in self.install_packages.iter().chain(&self.pins) {
789 if let Some((name, version)) = crate::util::split_version(full_name) {
790 pins.entry(name).or_default().insert(version);
791 }
792 }
793
794 tracing::debug!("Starting with mod list of: {:#?}", sorted_set);
795
796 while !sorted_set.is_empty() {
797 let Some(item) = sorted_set.pop_first() else {
798 continue;
799 };
800
801 let item = crate::util::full_name_prefix(&item).to_string();
802
803 let Some(&pkg_idx) = name_index.get(&item) else {
804 tracing::warn!(
805 "requested package {:?} was not found in the index; the install will be incomplete",
806 item
807 );
808 unresolved.push(item);
809
810 continue;
811 };
812
813 if !seen.insert(pkg_idx) {
816 continue;
817 }
818
819 let chosen_ver_idx = match pins.get(item.as_str()) {
820 None => index.get_latest_version_at(pkg_idx),
821 Some(versions) if versions.len() > 1 => {
822 tracing::warn!(
823 "package {:?} is pinned to more than one version ({:?}); the install will be incomplete",
824 item,
825 versions
826 );
827 unresolved.extend(versions.iter().map(|version| format!("{item}-{version}")));
828
829 continue;
830 }
831 Some(versions) => {
832 let version = versions.first().copied().unwrap_or_default();
833
834 match index.get_version_at(pkg_idx, version) {
835 Some(ver_idx) => Some(ver_idx),
836 None => {
837 tracing::warn!(
838 "package {:?} has no version {:?}; the install will be incomplete",
839 item,
840 version
841 );
842 unresolved.push(format!("{item}-{version}"));
843
844 continue;
845 }
846 }
847 }
848 };
849
850 if let Some(chosen_ver_idx) = chosen_ver_idx {
851 let deps = resolve_vec(
852 &index.interner,
853 &index.versions.dependencies[chosen_ver_idx],
854 );
855
856 for dep in deps {
857 tracing::debug!(
858 "Found dependency of {:#?} for {:#?} mod",
859 dep,
860 index.resolve_name_at(pkg_idx).unwrap_or_default(),
861 );
862 sorted_set.insert(dep);
863 }
864 }
865
866 install_indices.push((pkg_idx, chosen_ver_idx));
867 }
868
869 let mut urls = HashMap::new();
870
871 for &(idx, chosen_ver_idx) in &install_indices {
872 if let Some((zip_name, url)) = Self::package_zip_and_url(index, idx, chosen_ver_idx) {
873 urls.insert(zip_name, url);
874 } else if let Some(name) = index
875 .resolve_full_name_at(idx)
876 .or_else(|| index.resolve_name_at(idx))
877 {
878 tracing::warn!(
882 "package {:?} has no downloadable version; the install will be incomplete",
883 name
884 );
885 unresolved.push(name);
886 }
887 }
888
889 unresolved.sort();
890 unresolved.dedup();
891
892 Resolution { urls, unresolved }
893 }
894
895 fn package_zip_and_url(
899 index: &PackageIndex,
900 idx: usize,
901 ver_idx: Option<usize>,
902 ) -> Option<(String, String)> {
903 let chosen_ver_idx = ver_idx?;
904 let url = resolve_option(
905 &index.interner,
906 index.versions.download_urls[chosen_ver_idx],
907 )?;
908 let version_number = resolve_option(
909 &index.interner,
910 index.versions.version_numbers[chosen_ver_idx],
911 )?;
912 let package_name = index.resolve_full_name_at(idx)?;
913 let zip_name = format!("{}-{}.zip", package_name, version_number);
914
915 Some((zip_name, url))
916 }
917}
918
919impl From<Vec<Package>> for PackageIndex {
920 fn from(packages: Vec<Package>) -> Self {
921 let num_packages = packages.len();
922 let total_versions: usize = packages.iter().map(|p| p.versions.len()).sum();
923
924 let mut interner = StringInterner::default();
925
926 let mut names = Vec::with_capacity(num_packages);
927 let mut full_names = Vec::with_capacity(num_packages);
928 let mut owners = Vec::with_capacity(num_packages);
929 let mut package_urls = Vec::with_capacity(num_packages);
930 let mut dates_created = Vec::with_capacity(num_packages);
931 let mut dates_updated = Vec::with_capacity(num_packages);
932 let mut uuid4s = Vec::with_capacity(num_packages);
933 let mut rating_scores = Vec::with_capacity(num_packages);
934 let mut is_pinned = Vec::with_capacity(num_packages);
935 let mut is_deprecated = Vec::with_capacity(num_packages);
936 let mut has_nsfw_content = Vec::with_capacity(num_packages);
937 let mut categories = Vec::with_capacity(num_packages);
938 let mut version_ranges = Vec::with_capacity(num_packages);
939
940 let mut package_indices = Vec::with_capacity(total_versions);
941 let mut version_names = Vec::with_capacity(total_versions);
942 let mut version_full_names = Vec::with_capacity(total_versions);
943 let mut version_numbers = Vec::with_capacity(total_versions);
944 let mut download_urls = Vec::with_capacity(total_versions);
945 let mut dependencies = Vec::with_capacity(total_versions);
946 let mut version_dates_created = Vec::with_capacity(total_versions);
947 let mut descriptions = Vec::with_capacity(total_versions);
948 let mut icons = Vec::with_capacity(total_versions);
949 let mut downloads = Vec::with_capacity(total_versions);
950 let mut website_urls = Vec::with_capacity(total_versions);
951 let mut is_active = Vec::with_capacity(total_versions);
952 let mut version_uuid4s = Vec::with_capacity(total_versions);
953 let mut file_sizes = Vec::with_capacity(total_versions);
954
955 let mut version_offset = 0;
956
957 for (pkg_idx, package) in packages.into_iter().enumerate() {
958 names.push(intern_option(&mut interner, package.name.as_deref()));
959 full_names.push(intern_option(&mut interner, package.full_name.as_deref()));
960 owners.push(intern_option(&mut interner, package.owner.as_deref()));
961 package_urls.push(intern_option(&mut interner, package.package_url.as_deref()));
962 dates_created.push(package.date_created);
963 dates_updated.push(package.date_updated);
964 uuid4s.push(intern_option(&mut interner, package.uuid4.as_deref()));
965 rating_scores.push(package.rating_score);
966 is_pinned.push(package.is_pinned);
967 is_deprecated.push(package.is_deprecated);
968 has_nsfw_content.push(package.has_nsfw_content);
969 categories.push(intern_vec(&mut interner, &package.categories));
970
971 let version_count = package.versions.len();
972 version_ranges.push((version_offset, version_offset + version_count));
973
974 for version in package.versions {
975 package_indices.push(pkg_idx);
976 version_names.push(intern_option(&mut interner, version.name.as_deref()));
977 version_full_names.push(intern_option(&mut interner, version.full_name.as_deref()));
978 version_numbers.push(intern_option(
979 &mut interner,
980 version.version_number.as_deref(),
981 ));
982 download_urls.push(intern_option(
983 &mut interner,
984 version.download_url.as_deref(),
985 ));
986 dependencies.push(intern_vec(&mut interner, &version.dependencies));
987 version_dates_created.push(version.date_created);
988 descriptions.push(intern_option(&mut interner, version.description.as_deref()));
989 icons.push(intern_option(&mut interner, version.icon.as_deref()));
990 downloads.push(version.downloads);
991 website_urls.push(intern_option(&mut interner, version.website_url.as_deref()));
992 is_active.push(version.is_active);
993 version_uuid4s.push(intern_option(&mut interner, version.uuid4.as_deref()));
994 file_sizes.push(version.file_size);
995 }
996
997 version_offset += version_count;
998 }
999
1000 PackageIndex {
1001 interner,
1002 names,
1003 full_names,
1004 owners,
1005 package_urls,
1006 dates_created,
1007 dates_updated,
1008 uuid4s,
1009 rating_scores,
1010 is_pinned,
1011 is_deprecated,
1012 has_nsfw_content,
1013 categories,
1014 version_ranges,
1015 versions: VersionTable {
1016 package_indices,
1017 names: version_names,
1018 full_names: version_full_names,
1019 version_numbers,
1020 download_urls,
1021 dependencies,
1022 dates_created: version_dates_created,
1023 descriptions,
1024 icons,
1025 downloads,
1026 website_urls,
1027 is_active,
1028 uuid4s: version_uuid4s,
1029 file_sizes,
1030 },
1031 }
1032 }
1033}
1034
1035fn key_to_index(_interner: &StringInterner, key: InternKey) -> u32 {
1046 let index = key.to_index();
1047
1048 u32::try_from(index)
1052 .unwrap_or_else(|_| panic!("intern key index {index} exceeds u32 range and would truncate"))
1053}
1054
1055fn decode_key(index: u32, table_len: usize) -> Result<InternKey, Error> {
1062 if index as usize >= table_len {
1063 return Err(Error::Cache(format!(
1064 "interned string index {} out of range (string table length {})",
1065 index, table_len
1066 )));
1067 }
1068
1069 InternKey::from_index(index as usize)
1070 .ok_or_else(|| Error::Cache(format!("invalid interned key index {}", index)))
1071}
1072
1073fn encode_dates(dates: &[OffsetDateTime]) -> Vec<i128> {
1080 dates.iter().map(|d| d.unix_timestamp_nanos()).collect()
1081}
1082
1083fn decode_dates(nanos: &[i128]) -> Result<Vec<OffsetDateTime>, Error> {
1087 nanos
1088 .iter()
1089 .map(|&n| {
1090 OffsetDateTime::from_unix_timestamp_nanos(n)
1091 .map_err(|e| Error::Cache(format!("invalid cached timestamp {}: {}", n, e)))
1092 })
1093 .collect()
1094}
1095
1096impl From<&PackageIndex> for SerializedPackageIndex {
1097 fn from(manifest: &PackageIndex) -> Self {
1098 let interner = &manifest.interner;
1099 let string_table: Vec<String> = interner.strings().map(|s| s.to_string()).collect();
1100
1101 let opt = |keys: &[Option<InternKey>]| -> Vec<Option<u32>> {
1104 keys
1105 .iter()
1106 .map(|k| k.map(|key| key_to_index(interner, key)))
1107 .collect()
1108 };
1109
1110 let nested = |lists: &[Vec<InternKey>]| -> Vec<Vec<u32>> {
1111 lists
1112 .iter()
1113 .map(|keys| keys.iter().map(|k| key_to_index(interner, *k)).collect())
1114 .collect()
1115 };
1116
1117 SerializedPackageIndex {
1118 format_version: CACHE_FORMAT_VERSION,
1119 string_table,
1120 names: opt(&manifest.names),
1121 full_names: opt(&manifest.full_names),
1122 owners: opt(&manifest.owners),
1123 package_urls: opt(&manifest.package_urls),
1124 dates_created: encode_dates(&manifest.dates_created),
1125 dates_updated: encode_dates(&manifest.dates_updated),
1126 uuid4s: opt(&manifest.uuid4s),
1127 rating_scores: manifest.rating_scores.clone(),
1128 is_pinned: manifest.is_pinned.clone(),
1129 is_deprecated: manifest.is_deprecated.clone(),
1130 has_nsfw_content: manifest.has_nsfw_content.clone(),
1131 categories: nested(&manifest.categories),
1132 version_ranges: manifest.version_ranges.clone(),
1133 versions: SerializedVersionTable {
1134 package_indices: manifest.versions.package_indices.clone(),
1135 names: opt(&manifest.versions.names),
1136 full_names: opt(&manifest.versions.full_names),
1137 version_numbers: opt(&manifest.versions.version_numbers),
1138 download_urls: opt(&manifest.versions.download_urls),
1139 dependencies: nested(&manifest.versions.dependencies),
1140 dates_created: encode_dates(&manifest.versions.dates_created),
1141 descriptions: opt(&manifest.versions.descriptions),
1142 icons: opt(&manifest.versions.icons),
1143 downloads: manifest.versions.downloads.clone(),
1144 website_urls: opt(&manifest.versions.website_urls),
1145 is_active: manifest.versions.is_active.clone(),
1146 uuid4s: opt(&manifest.versions.uuid4s),
1147 file_sizes: manifest.versions.file_sizes.clone(),
1148 },
1149 }
1150 }
1151}
1152
1153impl TryFrom<SerializedPackageIndex> for PackageIndex {
1154 type Error = Error;
1155
1156 fn try_from(manifest: SerializedPackageIndex) -> Result<Self, Self::Error> {
1160 if manifest.format_version != CACHE_FORMAT_VERSION {
1161 return Err(Error::UnsupportedCacheVersion {
1162 found: manifest.format_version,
1163 expected: CACHE_FORMAT_VERSION,
1164 });
1165 }
1166
1167 let mut interner = StringInterner::default();
1168
1169 for s in &manifest.string_table {
1170 interner.get_or_intern(s);
1171 }
1172
1173 let table_len = manifest.string_table.len();
1174
1175 if interner.len() != table_len {
1183 return Err(Error::Cache(
1184 "cache string table contains duplicate or unresolvable entries".to_string(),
1185 ));
1186 }
1187
1188 let opt = |v: &[Option<u32>]| -> Result<Vec<Option<InternKey>>, Error> {
1189 v.iter()
1190 .map(|idx| idx.map(|i| decode_key(i, table_len)).transpose())
1191 .collect()
1192 };
1193
1194 let nested = |v: &[Vec<u32>]| -> Result<Vec<Vec<InternKey>>, Error> {
1195 v.iter()
1196 .map(|indices| indices.iter().map(|&i| decode_key(i, table_len)).collect())
1197 .collect()
1198 };
1199
1200 let names = opt(&manifest.names)?;
1201 let full_names = opt(&manifest.full_names)?;
1202 let owners = opt(&manifest.owners)?;
1203 let package_urls = opt(&manifest.package_urls)?;
1204 let uuid4s = opt(&manifest.uuid4s)?;
1205 let categories = nested(&manifest.categories)?;
1206
1207 let version_names = opt(&manifest.versions.names)?;
1208 let version_full_names = opt(&manifest.versions.full_names)?;
1209 let version_numbers = opt(&manifest.versions.version_numbers)?;
1210 let download_urls = opt(&manifest.versions.download_urls)?;
1211 let dependencies = nested(&manifest.versions.dependencies)?;
1212 let descriptions = opt(&manifest.versions.descriptions)?;
1213 let icons = opt(&manifest.versions.icons)?;
1214 let website_urls = opt(&manifest.versions.website_urls)?;
1215 let version_uuid4s = opt(&manifest.versions.uuid4s)?;
1216
1217 let dates_created = decode_dates(&manifest.dates_created)?;
1218 let dates_updated = decode_dates(&manifest.dates_updated)?;
1219 let version_dates_created = decode_dates(&manifest.versions.dates_created)?;
1220
1221 let result = PackageIndex {
1222 interner,
1223 names,
1224 full_names,
1225 owners,
1226 package_urls,
1227 dates_created,
1228 dates_updated,
1229 uuid4s,
1230 rating_scores: manifest.rating_scores,
1231 is_pinned: manifest.is_pinned,
1232 is_deprecated: manifest.is_deprecated,
1233 has_nsfw_content: manifest.has_nsfw_content,
1234 categories,
1235 version_ranges: manifest.version_ranges,
1236 versions: VersionTable {
1237 package_indices: manifest.versions.package_indices,
1238 names: version_names,
1239 full_names: version_full_names,
1240 version_numbers,
1241 download_urls,
1242 dependencies,
1243 dates_created: version_dates_created,
1244 descriptions,
1245 icons,
1246 downloads: manifest.versions.downloads,
1247 website_urls,
1248 is_active: manifest.versions.is_active,
1249 uuid4s: version_uuid4s,
1250 file_sizes: manifest.versions.file_sizes,
1251 },
1252 };
1253
1254 result.validate()?;
1255
1256 Ok(result)
1257 }
1258}
1259
1260#[cfg(test)]
1261mod tests {
1262 use super::*;
1263 use time::OffsetDateTime;
1264
1265 fn create_test_package(name: &str, owner: &str, version: &str) -> Package {
1266 let full_name = format!("{}-{}", owner, name);
1267 Package {
1268 name: Some(name.to_string()),
1269 full_name: Some(full_name.clone()),
1270 owner: Some(owner.to_string()),
1271 package_url: Some(format!("https://example.com/{}", name)),
1272 date_created: OffsetDateTime::now_utc(),
1273 date_updated: OffsetDateTime::now_utc(),
1274 uuid4: Some("test-uuid".to_string()),
1275 rating_score: Some(5),
1276 is_pinned: Some(false),
1277 is_deprecated: Some(false),
1278 has_nsfw_content: Some(false),
1279 categories: vec!["category1".to_string()],
1280 versions: vec![Version {
1281 name: Some(name.to_string()),
1282 full_name: Some(full_name.clone()),
1283 description: Some("Test description".to_string()),
1284 icon: Some("icon.png".to_string()),
1285 version_number: Some(version.to_string()),
1286 dependencies: vec![],
1287 download_url: Some(format!("https://example.com/{}/download", name)),
1288 downloads: Some(100),
1289 date_created: OffsetDateTime::now_utc(),
1290 website_url: Some("https://example.com".to_string()),
1291 is_active: Some(true),
1292 uuid4: Some("test-version-uuid".to_string()),
1293 file_size: Some(1024),
1294 }],
1295 }
1296 }
1297
1298 fn create_test_package_with_dependencies(
1299 name: &str,
1300 owner: &str,
1301 version: &str,
1302 dependencies: Vec<String>,
1303 ) -> Package {
1304 let mut pkg = create_test_package(name, owner, version);
1305
1306 if let Some(latest_version) = pkg.versions.first_mut() {
1307 latest_version.dependencies = dependencies;
1308 }
1309
1310 pkg
1311 }
1312
1313 #[test]
1314 fn test_latest_version() {
1315 let mut pkg = create_test_package("TestMod", "TestOwner", "1.0.0");
1316
1317 let older_version = Version {
1318 name: Some("TestMod".to_string()),
1319 full_name: Some("TestOwner-TestMod".to_string()),
1320 description: Some("Older version".to_string()),
1321 icon: Some("icon.png".to_string()),
1322 version_number: Some("0.9.0".to_string()),
1323 dependencies: vec![],
1324 download_url: Some("https://example.com/TestMod/download-old".to_string()),
1325 downloads: Some(50),
1326 date_created: OffsetDateTime::now_utc().saturating_sub(time::Duration::days(30)),
1327 website_url: Some("https://example.com".to_string()),
1328 is_active: Some(true),
1329 uuid4: Some("old-version-uuid".to_string()),
1330 file_size: Some(512),
1331 };
1332 pkg.versions.push(older_version);
1333
1334 let newer_version = Version {
1335 name: Some("TestMod".to_string()),
1336 full_name: Some("TestOwner-TestMod".to_string()),
1337 description: Some("Newer version".to_string()),
1338 icon: Some("icon.png".to_string()),
1339 version_number: Some("1.1.0".to_string()),
1340 dependencies: vec![],
1341 download_url: Some("https://example.com/TestMod/download-new".to_string()),
1342 downloads: Some(150),
1343 date_created: OffsetDateTime::now_utc().saturating_add(time::Duration::days(30)),
1344 website_url: Some("https://example.com".to_string()),
1345 is_active: Some(true),
1346 uuid4: Some("new-version-uuid".to_string()),
1347 file_size: Some(2048),
1348 };
1349 pkg.versions.push(newer_version);
1350
1351 let latest = pkg.latest_version().unwrap();
1352 assert_eq!(latest.version_number, Some("1.1.0".to_string()));
1353 assert_eq!(latest.description, Some("Newer version".to_string()));
1354 }
1355
1356 #[test]
1357 fn test_zip_and_url() {
1358 let pkg = create_test_package("TestMod", "TestOwner", "1.0.0");
1359 let (filename, url) = pkg.zip_and_url().unwrap();
1360
1361 assert_eq!(filename, "TestOwner-TestMod-1.0.0.zip");
1362 assert_eq!(url, "https://example.com/TestMod/download");
1363 }
1364
1365 #[test]
1366 fn test_zip_and_url_missing_data() {
1367 let mut pkg = create_test_package("TestMod", "TestOwner", "1.0.0");
1368
1369 if let Some(version) = pkg.versions.first_mut() {
1370 version.version_number = None;
1371 }
1372
1373 assert!(pkg.zip_and_url().is_none());
1374
1375 let mut pkg = create_test_package("TestMod", "TestOwner", "1.0.0");
1376
1377 if let Some(version) = pkg.versions.first_mut() {
1378 version.download_url = None;
1379 }
1380
1381 assert!(pkg.zip_and_url().is_none());
1382
1383 let mut pkg = create_test_package("TestMod", "TestOwner", "1.0.0");
1384 pkg.full_name = None;
1385
1386 assert!(pkg.zip_and_url().is_none());
1387 }
1388
1389 #[test]
1390 fn test_package_debug() {
1391 let pkg = create_test_package("ModTest", "OwnerTest", "1.2.3");
1392 let debug_output = format!("{:?}", pkg);
1393
1394 assert!(debug_output.contains("Package"));
1395 assert!(debug_output.contains("full_name: Some(\"OwnerTest-ModTest\")"));
1396 assert!(debug_output.contains("dependencies: Some([])"));
1397 assert!(
1398 debug_output.contains("download_url: Some(Some(\"https://example.com/ModTest/download\"))")
1399 );
1400
1401 let mut pkg_with_deps = create_test_package_with_dependencies(
1402 "ModWithDeps",
1403 "OwnerTest",
1404 "2.0.0",
1405 vec!["Dep1-Mod1".to_string(), "Dep2-Mod2".to_string()],
1406 );
1407 let debug_with_deps = format!("{:?}", pkg_with_deps);
1408
1409 assert!(debug_with_deps.contains("dependencies: Some([\"Dep1-Mod1\", \"Dep2-Mod2\"])"));
1410
1411 pkg_with_deps.full_name = None;
1412 let debug_missing_name = format!("{:?}", pkg_with_deps);
1413 assert!(debug_missing_name.contains("full_name: None"));
1414
1415 let mut pkg_no_version = create_test_package("NoVersion", "TestOwner", "1.0.0");
1416 pkg_no_version.versions.clear();
1417 let debug_no_version = format!("{:?}", pkg_no_version);
1418
1419 assert!(debug_no_version.contains("dependencies: None"));
1420 assert!(debug_no_version.contains("download_url: None"));
1421 }
1422
1423 #[test]
1424 fn test_package_index_basic_operations() {
1425 let pkg = create_test_package("TestMod", "TestOwner", "1.0.0");
1426 let packages = vec![pkg];
1427 let interned: PackageIndex = packages.into();
1428
1429 assert_eq!(interned.len(), 1);
1430
1431 assert!(!interned.is_empty());
1432
1433 assert_eq!(interned.resolve_name_at(0), Some("TestMod".to_string()));
1434
1435 assert_eq!(
1436 interned.resolve_full_name_at(0),
1437 Some("TestOwner-TestMod".to_string())
1438 );
1439
1440 assert_eq!(interned.resolve_owner_at(0), Some("TestOwner".to_string()));
1441 }
1442
1443 #[test]
1444 fn search_matches_name_and_full_name_case_insensitively() {
1445 let packages = vec![
1446 create_test_package("CoolMod", "Author", "1.0.0"),
1447 create_test_package("OtherThing", "Someone", "1.0.0"),
1448 ];
1449 let index: PackageIndex = packages.into();
1450
1451 assert_eq!(index.search("coolmod"), vec![0]);
1453 assert_eq!(index.search("COOLMOD"), vec![0]);
1454 assert_eq!(index.search("author-cool"), vec![0]);
1456 assert_eq!(index.search("oolMo"), vec![0]);
1458 assert!(index.search("nothing-like-this").is_empty());
1460 assert_eq!(index.search(""), vec![0, 1]);
1463 }
1464
1465 #[test]
1466 fn test_package_index_borrowing_accessors() {
1467 let pkg = create_test_package("TestMod", "TestOwner", "1.0.0");
1468 let packages = vec![pkg];
1469 let interned: PackageIndex = packages.into();
1470
1471 assert_eq!(interned.name_at(0), Some("TestMod"));
1474
1475 assert_eq!(interned.full_name_at(0), Some("TestOwner-TestMod"));
1476
1477 assert_eq!(interned.owner_at(0), Some("TestOwner"));
1478
1479 assert_eq!(interned.name_at(1), None);
1480
1481 assert_eq!(interned.full_name_at(1), None);
1482
1483 assert_eq!(interned.owner_at(1), None);
1484 }
1485
1486 #[test]
1487 fn test_package_index_get_package_by_full_name() {
1488 let pkg = create_test_package("TestMod", "TestOwner", "1.0.0");
1489 let packages = vec![pkg];
1490 let interned: PackageIndex = packages.into();
1491
1492 let found = interned.get_package_by_full_name("TestOwner-TestMod");
1493
1494 assert!(found.is_some());
1495
1496 let package = found.unwrap();
1497
1498 assert_eq!(package.name, Some("TestMod".to_string()));
1499
1500 assert_eq!(package.owner, Some("TestOwner".to_string()));
1501
1502 assert_eq!(package.versions.len(), 1);
1503 }
1504
1505 #[test]
1506 fn test_package_index_find_index_by_full_name() {
1507 let pkg1 = create_test_package("ModA", "Owner1", "1.0.0");
1508 let pkg2 = create_test_package("ModB", "Owner2", "2.0.0");
1509 let packages = vec![pkg1, pkg2];
1510 let interned: PackageIndex = packages.into();
1511
1512 assert_eq!(interned.find_index_by_full_name("Owner1-ModA"), Some(0));
1513
1514 assert_eq!(interned.find_index_by_full_name("Owner2-ModB"), Some(1));
1515
1516 assert_eq!(interned.find_index_by_full_name("NonExistent"), None);
1517 }
1518
1519 #[test]
1520 fn test_package_index_get_package_at() {
1521 let pkg = create_test_package("TestMod", "TestOwner", "1.0.0");
1522 let packages = vec![pkg];
1523 let interned: PackageIndex = packages.into();
1524
1525 let package = interned.get_package_at(0).unwrap();
1526
1527 assert_eq!(package.name, Some("TestMod".to_string()));
1528
1529 assert_eq!(package.full_name, Some("TestOwner-TestMod".to_string()));
1530
1531 assert_eq!(package.owner, Some("TestOwner".to_string()));
1532
1533 assert_eq!(package.versions.len(), 1);
1534
1535 assert_eq!(
1536 package.versions[0].version_number,
1537 Some("1.0.0".to_string())
1538 );
1539 }
1540
1541 #[test]
1542 fn test_package_index_get_latest_version_at() {
1543 let mut pkg = create_test_package("TestMod", "TestOwner", "1.0.0");
1544
1545 let older_version = Version {
1546 name: Some("TestMod".to_string()),
1547 full_name: Some("TestOwner-TestMod".to_string()),
1548 description: Some("Older version".to_string()),
1549 icon: Some("icon.png".to_string()),
1550 version_number: Some("0.9.0".to_string()),
1551 dependencies: vec![],
1552 download_url: Some("https://example.com/TestMod/download-old".to_string()),
1553 downloads: Some(50),
1554 date_created: OffsetDateTime::now_utc().saturating_sub(time::Duration::days(30)),
1555 website_url: Some("https://example.com".to_string()),
1556 is_active: Some(true),
1557 uuid4: Some("old-version-uuid".to_string()),
1558 file_size: Some(512),
1559 };
1560 pkg.versions.insert(0, older_version);
1561
1562 let packages = vec![pkg];
1563 let interned: PackageIndex = packages.into();
1564
1565 let latest_idx = interned.get_latest_version_at(0);
1566
1567 assert!(latest_idx.is_some());
1568
1569 let version_idx = latest_idx.unwrap();
1570 let version_number = interned.versions.version_numbers[version_idx]
1571 .map(|key| interned.interner.resolve(&key).to_string());
1572
1573 assert_eq!(version_number, Some("1.0.0".to_string()));
1574 }
1575
1576 #[test]
1577 fn test_package_index_get_latest_version_at_empty_range() {
1578 let pkg = create_test_package("TestMod", "TestOwner", "1.0.0");
1579 let packages = vec![pkg];
1580 let mut interned: PackageIndex = packages.into();
1581
1582 interned.version_ranges[0] = (0, 0);
1583
1584 let latest = interned.get_latest_version_at(0);
1585
1586 assert!(latest.is_none());
1587 }
1588
1589 #[test]
1590 fn test_package_index_build_name_index() {
1591 let pkg1 = create_test_package("ModA", "Owner1", "1.0.0");
1592 let pkg2 = create_test_package("ModB", "Owner2", "2.0.0");
1593 let packages = vec![pkg1, pkg2];
1594 let interned: PackageIndex = packages.into();
1595
1596 let index = interned.build_name_index();
1597
1598 assert_eq!(index.len(), 2);
1599
1600 assert_eq!(index.get("Owner1-ModA"), Some(&0));
1601
1602 assert_eq!(index.get("Owner2-ModB"), Some(&1));
1603 }
1604
1605 #[test]
1606 fn test_package_index_validate_valid() {
1607 let pkg = create_test_package("TestMod", "TestOwner", "1.0.0");
1608 let packages = vec![pkg];
1609 let interned: PackageIndex = packages.into();
1610
1611 let result = interned.validate();
1612
1613 assert!(result.is_ok());
1614 }
1615
1616 #[test]
1617 fn validate_split_methods_are_individually_callable() {
1618 let interned: PackageIndex = vec![create_test_package("ModA", "Owner1", "1.0.0")].into();
1619
1620 assert!(interned.validate_lengths().is_ok());
1622 assert!(interned.validate_version_ranges().is_ok());
1623 assert!(interned.validate_version_ownership().is_ok());
1624 assert!(interned.validate_unique_full_names().is_ok());
1625
1626 let mut broken: PackageIndex = vec![create_test_package("ModA", "Owner1", "1.0.0")].into();
1628 broken.owners.pop();
1629 assert!(broken.validate_lengths().is_err());
1630
1631 let dup: PackageIndex = vec![
1633 create_test_package("ModA", "Owner1", "1.0.0"),
1634 create_test_package("ModA", "Owner1", "2.0.0"),
1635 ]
1636 .into();
1637 assert!(dup.validate_unique_full_names().is_err());
1638 }
1639
1640 #[test]
1641 fn encode_decode_dates_roundtrips_and_rejects_out_of_range() {
1642 let dates = vec![
1643 OffsetDateTime::from_unix_timestamp(1_700_000_000).unwrap(),
1644 OffsetDateTime::from_unix_timestamp(0).unwrap(),
1645 ];
1646
1647 let encoded = encode_dates(&dates);
1648 let decoded = decode_dates(&encoded).unwrap();
1649
1650 assert_eq!(decoded, dates);
1651
1652 let err = decode_dates(&[i128::MAX]).unwrap_err();
1655
1656 assert!(matches!(err, Error::Cache(_)));
1657 }
1658
1659 #[test]
1660 fn from_packages_populates_core_fields() {
1661 let interned: PackageIndex = vec![create_test_package("ModA", "Owner1", "1.0.0")].into();
1662
1663 assert_eq!(interned.len(), 1);
1664 assert_eq!(
1665 interned.resolve_full_name_at(0),
1666 Some("Owner1-ModA".to_string())
1667 );
1668
1669 assert_eq!(interned.version_ranges, vec![(0, 1)]);
1672 assert_eq!(interned.versions.package_indices, vec![0]);
1673 }
1674
1675 #[test]
1676 fn test_package_index_validate_mismatched_full_names_length() {
1677 let pkg = create_test_package("TestMod", "TestOwner", "1.0.0");
1678 let packages = vec![pkg];
1679 let mut interned: PackageIndex = packages.into();
1680
1681 interned.full_names.pop();
1682
1683 let result = interned.validate();
1684
1685 assert!(result.is_err());
1686
1687 assert!(
1688 result
1689 .unwrap_err()
1690 .to_string()
1691 .contains("full_names length")
1692 );
1693 }
1694
1695 #[test]
1696 fn test_package_index_validate_mismatched_owners_length() {
1697 let pkg = create_test_package("TestMod", "TestOwner", "1.0.0");
1698 let packages = vec![pkg];
1699 let mut interned: PackageIndex = packages.into();
1700
1701 interned.owners.pop();
1702
1703 let result = interned.validate();
1704
1705 assert!(result.is_err());
1706
1707 assert!(result.unwrap_err().to_string().contains("owners length"));
1708 }
1709
1710 #[test]
1711 fn test_package_index_validate_mismatched_package_urls_length() {
1712 let pkg = create_test_package("TestMod", "TestOwner", "1.0.0");
1713 let packages = vec![pkg];
1714 let mut interned: PackageIndex = packages.into();
1715
1716 interned.package_urls.pop();
1717
1718 let result = interned.validate();
1719
1720 assert!(result.is_err());
1721
1722 assert!(
1723 result
1724 .unwrap_err()
1725 .to_string()
1726 .contains("package_urls length")
1727 );
1728 }
1729
1730 #[test]
1731 fn test_package_index_validate_mismatched_dates_created_length() {
1732 let pkg = create_test_package("TestMod", "TestOwner", "1.0.0");
1733 let packages = vec![pkg];
1734 let mut interned: PackageIndex = packages.into();
1735
1736 interned.dates_created.pop();
1737
1738 let result = interned.validate();
1739
1740 assert!(result.is_err());
1741
1742 assert!(
1743 result
1744 .unwrap_err()
1745 .to_string()
1746 .contains("dates_created length")
1747 );
1748 }
1749
1750 #[test]
1751 fn test_package_index_validate_mismatched_dates_updated_length() {
1752 let pkg = create_test_package("TestMod", "TestOwner", "1.0.0");
1753 let packages = vec![pkg];
1754 let mut interned: PackageIndex = packages.into();
1755
1756 interned.dates_updated.pop();
1757
1758 let result = interned.validate();
1759
1760 assert!(result.is_err());
1761
1762 assert!(
1763 result
1764 .unwrap_err()
1765 .to_string()
1766 .contains("dates_updated length")
1767 );
1768 }
1769
1770 #[test]
1771 fn test_package_index_validate_mismatched_uuid4s_length() {
1772 let pkg = create_test_package("TestMod", "TestOwner", "1.0.0");
1773 let packages = vec![pkg];
1774 let mut interned: PackageIndex = packages.into();
1775
1776 interned.uuid4s.pop();
1777
1778 let result = interned.validate();
1779
1780 assert!(result.is_err());
1781
1782 assert!(result.unwrap_err().to_string().contains("uuid4s length"));
1783 }
1784
1785 #[test]
1786 fn test_package_index_validate_mismatched_rating_scores_length() {
1787 let pkg = create_test_package("TestMod", "TestOwner", "1.0.0");
1788 let packages = vec![pkg];
1789 let mut interned: PackageIndex = packages.into();
1790
1791 interned.rating_scores.pop();
1792
1793 let result = interned.validate();
1794
1795 assert!(result.is_err());
1796
1797 assert!(
1798 result
1799 .unwrap_err()
1800 .to_string()
1801 .contains("rating_scores length")
1802 );
1803 }
1804
1805 #[test]
1806 fn test_package_index_validate_mismatched_is_pinned_length() {
1807 let pkg = create_test_package("TestMod", "TestOwner", "1.0.0");
1808 let packages = vec![pkg];
1809 let mut interned: PackageIndex = packages.into();
1810
1811 interned.is_pinned.pop();
1812
1813 let result = interned.validate();
1814
1815 assert!(result.is_err());
1816
1817 assert!(result.unwrap_err().to_string().contains("is_pinned length"));
1818 }
1819
1820 #[test]
1821 fn test_package_index_validate_mismatched_is_deprecated_length() {
1822 let pkg = create_test_package("TestMod", "TestOwner", "1.0.0");
1823 let packages = vec![pkg];
1824 let mut interned: PackageIndex = packages.into();
1825
1826 interned.is_deprecated.pop();
1827
1828 let result = interned.validate();
1829
1830 assert!(result.is_err());
1831
1832 assert!(
1833 result
1834 .unwrap_err()
1835 .to_string()
1836 .contains("is_deprecated length")
1837 );
1838 }
1839
1840 #[test]
1841 fn test_package_index_validate_mismatched_has_nsfw_content_length() {
1842 let pkg = create_test_package("TestMod", "TestOwner", "1.0.0");
1843 let packages = vec![pkg];
1844 let mut interned: PackageIndex = packages.into();
1845
1846 interned.has_nsfw_content.pop();
1847
1848 let result = interned.validate();
1849
1850 assert!(result.is_err());
1851
1852 assert!(
1853 result
1854 .unwrap_err()
1855 .to_string()
1856 .contains("has_nsfw_content length")
1857 );
1858 }
1859
1860 #[test]
1861 fn test_package_index_validate_mismatched_categories_length() {
1862 let pkg = create_test_package("TestMod", "TestOwner", "1.0.0");
1863 let packages = vec![pkg];
1864 let mut interned: PackageIndex = packages.into();
1865
1866 interned.categories.pop();
1867
1868 let result = interned.validate();
1869
1870 assert!(result.is_err());
1871
1872 assert!(
1873 result
1874 .unwrap_err()
1875 .to_string()
1876 .contains("categories length")
1877 );
1878 }
1879
1880 #[test]
1881 fn test_package_index_validate_mismatched_version_ranges_length() {
1882 let pkg = create_test_package("TestMod", "TestOwner", "1.0.0");
1883 let packages = vec![pkg];
1884 let mut interned: PackageIndex = packages.into();
1885
1886 interned.version_ranges.pop();
1887
1888 let result = interned.validate();
1889
1890 assert!(result.is_err());
1891
1892 assert!(
1893 result
1894 .unwrap_err()
1895 .to_string()
1896 .contains("version_ranges length")
1897 );
1898 }
1899
1900 #[test]
1901 fn test_package_index_validate_mismatched_version_numbers_length() {
1902 let pkg = create_test_package("TestMod", "TestOwner", "1.0.0");
1903 let packages = vec![pkg];
1904 let mut interned: PackageIndex = packages.into();
1905
1906 interned.versions.version_numbers.pop();
1907
1908 let result = interned.validate();
1909
1910 assert!(result.is_err());
1911
1912 assert!(
1913 result
1914 .unwrap_err()
1915 .to_string()
1916 .contains("version_numbers length")
1917 );
1918 }
1919
1920 #[test]
1921 fn test_package_index_validate_mismatched_version_download_urls_length() {
1922 let pkg = create_test_package("TestMod", "TestOwner", "1.0.0");
1923 let packages = vec![pkg];
1924 let mut interned: PackageIndex = packages.into();
1925
1926 interned.versions.download_urls.pop();
1927
1928 let result = interned.validate();
1929
1930 assert!(result.is_err());
1931
1932 assert!(
1933 result
1934 .unwrap_err()
1935 .to_string()
1936 .contains("download_urls length")
1937 );
1938 }
1939
1940 #[test]
1941 fn test_package_index_validate_mismatched_version_dependencies_length() {
1942 let pkg = create_test_package("TestMod", "TestOwner", "1.0.0");
1943 let packages = vec![pkg];
1944 let mut interned: PackageIndex = packages.into();
1945
1946 interned.versions.dependencies.pop();
1947
1948 let result = interned.validate();
1949
1950 assert!(result.is_err());
1951
1952 assert!(
1953 result
1954 .unwrap_err()
1955 .to_string()
1956 .contains("dependencies length")
1957 );
1958 }
1959
1960 #[test]
1961 fn test_package_index_validate_mismatched_version_dates_created_length() {
1962 let pkg = create_test_package("TestMod", "TestOwner", "1.0.0");
1963 let packages = vec![pkg];
1964 let mut interned: PackageIndex = packages.into();
1965
1966 interned.versions.dates_created.pop();
1967
1968 let result = interned.validate();
1969
1970 assert!(result.is_err());
1971
1972 assert!(
1973 result
1974 .unwrap_err()
1975 .to_string()
1976 .contains("dates_created length")
1977 );
1978 }
1979
1980 #[test]
1981 fn test_package_index_validate_mismatched_version_descriptions_length() {
1982 let pkg = create_test_package("TestMod", "TestOwner", "1.0.0");
1983 let packages = vec![pkg];
1984 let mut interned: PackageIndex = packages.into();
1985
1986 interned.versions.descriptions.pop();
1987
1988 let result = interned.validate();
1989
1990 assert!(result.is_err());
1991
1992 assert!(
1993 result
1994 .unwrap_err()
1995 .to_string()
1996 .contains("descriptions length")
1997 );
1998 }
1999
2000 #[test]
2001 fn test_package_index_validate_mismatched_version_icons_length() {
2002 let pkg = create_test_package("TestMod", "TestOwner", "1.0.0");
2003 let packages = vec![pkg];
2004 let mut interned: PackageIndex = packages.into();
2005
2006 interned.versions.icons.pop();
2007
2008 let result = interned.validate();
2009
2010 assert!(result.is_err());
2011
2012 assert!(result.unwrap_err().to_string().contains("icons length"));
2013 }
2014
2015 #[test]
2016 fn test_package_index_validate_mismatched_version_downloads_length() {
2017 let pkg = create_test_package("TestMod", "TestOwner", "1.0.0");
2018 let packages = vec![pkg];
2019 let mut interned: PackageIndex = packages.into();
2020
2021 interned.versions.downloads.pop();
2022
2023 let result = interned.validate();
2024
2025 assert!(result.is_err());
2026
2027 assert!(result.unwrap_err().to_string().contains("downloads length"));
2028 }
2029
2030 #[test]
2031 fn test_package_index_validate_mismatched_version_website_urls_length() {
2032 let pkg = create_test_package("TestMod", "TestOwner", "1.0.0");
2033 let packages = vec![pkg];
2034 let mut interned: PackageIndex = packages.into();
2035
2036 interned.versions.website_urls.pop();
2037
2038 let result = interned.validate();
2039
2040 assert!(result.is_err());
2041
2042 assert!(
2043 result
2044 .unwrap_err()
2045 .to_string()
2046 .contains("website_urls length")
2047 );
2048 }
2049
2050 #[test]
2051 fn test_package_index_validate_mismatched_version_is_active_length() {
2052 let pkg = create_test_package("TestMod", "TestOwner", "1.0.0");
2053 let packages = vec![pkg];
2054 let mut interned: PackageIndex = packages.into();
2055
2056 interned.versions.is_active.pop();
2057
2058 let result = interned.validate();
2059
2060 assert!(result.is_err());
2061
2062 assert!(result.unwrap_err().to_string().contains("is_active length"));
2063 }
2064
2065 #[test]
2066 fn test_package_index_validate_mismatched_version_uuid4s_length() {
2067 let pkg = create_test_package("TestMod", "TestOwner", "1.0.0");
2068 let packages = vec![pkg];
2069 let mut interned: PackageIndex = packages.into();
2070
2071 interned.versions.uuid4s.pop();
2072
2073 let result = interned.validate();
2074
2075 assert!(result.is_err());
2076
2077 assert!(result.unwrap_err().to_string().contains("uuid4s length"));
2078 }
2079
2080 #[test]
2081 fn test_package_index_validate_mismatched_version_file_sizes_length() {
2082 let pkg = create_test_package("TestMod", "TestOwner", "1.0.0");
2083 let packages = vec![pkg];
2084 let mut interned: PackageIndex = packages.into();
2085
2086 interned.versions.file_sizes.pop();
2087
2088 let result = interned.validate();
2089
2090 assert!(result.is_err());
2091
2092 assert!(
2093 result
2094 .unwrap_err()
2095 .to_string()
2096 .contains("file_sizes length")
2097 );
2098 }
2099
2100 #[test]
2101 fn test_package_index_validate_invalid_version_range() {
2102 let pkg = create_test_package("TestMod", "TestOwner", "1.0.0");
2103 let packages = vec![pkg];
2104 let mut interned: PackageIndex = packages.into();
2105
2106 interned.version_ranges[0] = (5, 3);
2107
2108 let result = interned.validate();
2109
2110 assert!(result.is_err());
2111
2112 assert!(
2113 result
2114 .unwrap_err()
2115 .to_string()
2116 .contains("Invalid version range")
2117 );
2118 }
2119
2120 #[test]
2121 fn test_package_index_validate_version_range_out_of_bounds() {
2122 let pkg = create_test_package("TestMod", "TestOwner", "1.0.0");
2123 let packages = vec![pkg];
2124 let mut interned: PackageIndex = packages.into();
2125
2126 interned.version_ranges[0] = (0, 999);
2127
2128 let result = interned.validate();
2129
2130 assert!(result.is_err());
2131
2132 assert!(result.unwrap_err().to_string().contains("ends at"));
2133 }
2134
2135 #[test]
2136 fn test_package_index_serialization_round_trip() {
2137 let pkg = create_test_package("TestMod", "TestOwner", "1.0.0");
2138 let packages = vec![pkg.clone()];
2139 let interned: PackageIndex = packages.into();
2140
2141 let serializable: SerializedPackageIndex = (&interned).into();
2142
2143 let binary = bincode::encode_to_vec(&serializable, bincode::config::standard()).unwrap();
2144
2145 let (deserialized, _): (SerializedPackageIndex, usize) =
2146 bincode::decode_from_slice(&binary, bincode::config::standard()).unwrap();
2147
2148 let recovered: PackageIndex = deserialized.try_into().unwrap();
2149
2150 assert_eq!(recovered.len(), 1);
2151
2152 assert_eq!(recovered.resolve_name_at(0), Some("TestMod".to_string()));
2153
2154 assert_eq!(
2155 recovered.resolve_full_name_at(0),
2156 Some("TestOwner-TestMod".to_string())
2157 );
2158
2159 let recovered_pkg = recovered.get_package_at(0).unwrap();
2160
2161 assert_eq!(recovered_pkg.name, pkg.name);
2162
2163 assert_eq!(recovered_pkg.full_name, pkg.full_name);
2164
2165 assert_eq!(recovered_pkg.versions.len(), pkg.versions.len());
2166 }
2167
2168 #[test]
2169 fn test_version_name_and_full_name_are_version_level_not_package_level() {
2170 let mut pkg = create_test_package("CoolMod", "Author", "1.0.0");
2173 pkg.name = Some("CoolMod".to_string());
2174 pkg.full_name = Some("Author-CoolMod".to_string());
2175 pkg.versions[0].name = Some("Author-CoolMod".to_string());
2176 pkg.versions[0].full_name = Some("Author-CoolMod-1.0.0".to_string());
2177
2178 let interned: PackageIndex = vec![pkg].into();
2179
2180 let materialized = interned.get_package_at(0).unwrap();
2183
2184 assert_eq!(materialized.full_name, Some("Author-CoolMod".to_string()));
2185
2186 assert_eq!(
2187 materialized.versions[0].full_name,
2188 Some("Author-CoolMod-1.0.0".to_string())
2189 );
2190
2191 assert_eq!(
2192 materialized.versions[0].name,
2193 Some("Author-CoolMod".to_string())
2194 );
2195
2196 let serializable: SerializedPackageIndex = (&interned).into();
2198
2199 let binary = bincode::encode_to_vec(&serializable, bincode::config::standard()).unwrap();
2200
2201 let (deserialized, _): (SerializedPackageIndex, usize) =
2202 bincode::decode_from_slice(&binary, bincode::config::standard()).unwrap();
2203
2204 let recovered: PackageIndex = deserialized.try_into().unwrap();
2205 let recovered_pkg = recovered.get_package_at(0).unwrap();
2206
2207 assert_eq!(recovered_pkg.full_name, Some("Author-CoolMod".to_string()));
2208
2209 assert_eq!(
2210 recovered_pkg.versions[0].full_name,
2211 Some("Author-CoolMod-1.0.0".to_string())
2212 );
2213 }
2214
2215 #[test]
2216 fn test_resolve_report_lists_unresolved_packages() {
2217 let interned: PackageIndex = vec![create_test_package("ModA", "Owner1", "1.0.0")].into();
2218
2219 let graph = DependencyGraph::new(vec!["Owner1-ModA".to_string(), "Ghost-Missing".to_string()]);
2220
2221 let report = graph.resolve_report(&interned);
2222
2223 assert_eq!(report.urls.len(), 1);
2226 assert_eq!(report.unresolved, vec!["Ghost-Missing".to_string()]);
2227
2228 assert_eq!(graph.resolve(&interned).len(), 1);
2230 }
2231
2232 #[test]
2233 fn test_resolve_report_flags_package_with_no_downloadable_version() {
2234 let mut pkg = create_test_package("ModA", "Owner1", "1.0.0");
2235
2236 pkg.versions.clear();
2238
2239 let interned: PackageIndex = vec![pkg].into();
2240
2241 let graph = DependencyGraph::new(vec!["Owner1-ModA".to_string()]);
2242 let report = graph.resolve_report(&interned);
2243
2244 assert!(report.urls.is_empty());
2247 assert_eq!(report.unresolved, vec!["Owner1-ModA".to_string()]);
2248 }
2249
2250 #[test]
2251 fn test_duplicate_string_table_returns_error_not_panic() {
2252 let pkg = create_test_package("TestMod", "TestOwner", "1.0.0");
2253 let interned: PackageIndex = vec![pkg].into();
2254 let mut serializable: SerializedPackageIndex = (&interned).into();
2255
2256 let dup = serializable.string_table[0].clone();
2261
2262 serializable.string_table.push(dup);
2263
2264 let last_index = serializable.string_table.len() as u32 - 1;
2265
2266 serializable.names[0] = Some(last_index);
2267
2268 let result = PackageIndex::try_from(serializable);
2269
2270 assert!(result.is_err());
2271 }
2272
2273 #[test]
2274 fn test_corrupt_cache_index_returns_error_not_panic() {
2275 let pkg = create_test_package("TestMod", "TestOwner", "1.0.0");
2276 let interned: PackageIndex = vec![pkg].into();
2277 let mut serializable: SerializedPackageIndex = (&interned).into();
2278
2279 let bad_index = serializable.string_table.len() as u32 + 5;
2282 serializable.names[0] = Some(bad_index);
2283
2284 let result = PackageIndex::try_from(serializable);
2285
2286 assert!(result.is_err());
2287 }
2288
2289 #[test]
2290 fn test_package_index_dependency_graph_resolve() {
2291 let pkg1 = create_test_package("ModA", "Owner1", "1.0.0");
2292 let pkg2 = create_test_package_with_dependencies(
2293 "ModB",
2294 "Owner2",
2295 "2.0.0",
2296 vec!["Owner3-ModC".to_string()],
2297 );
2298 let pkg3 = create_test_package_with_dependencies(
2299 "ModC",
2300 "Owner3",
2301 "1.5.0",
2302 vec!["Owner4-ModD".to_string()],
2303 );
2304 let pkg4 = create_test_package("ModD", "Owner4", "0.9.0");
2305
2306 let packages = vec![pkg1, pkg2, pkg3, pkg4];
2307 let interned: PackageIndex = packages.into();
2308
2309 let dg1 = DependencyGraph::new(vec!["Owner1-ModA".to_string()]);
2310 let result1 = dg1.resolve(&interned);
2311
2312 assert_eq!(result1.len(), 1);
2313
2314 assert!(result1.contains_key("Owner1-ModA-1.0.0.zip"));
2315
2316 let dg2 = DependencyGraph::new(vec!["Owner2-ModB".to_string()]);
2317 let result2 = dg2.resolve(&interned);
2318
2319 assert_eq!(result2.len(), 3);
2320
2321 assert!(result2.contains_key("Owner2-ModB-2.0.0.zip"));
2322
2323 assert!(result2.contains_key("Owner3-ModC-1.5.0.zip"));
2324
2325 assert!(result2.contains_key("Owner4-ModD-0.9.0.zip"));
2326 }
2327
2328 #[test]
2329 fn test_package_index_dependency_graph_resolve_with_missing_dependency() {
2330 let pkg1 = create_test_package("ModA", "Owner1", "1.0.0");
2331 let pkg2 = create_test_package_with_dependencies(
2332 "ModB",
2333 "Owner2",
2334 "2.0.0",
2335 vec!["Owner3-NonExistent".to_string()],
2336 );
2337
2338 let packages = vec![pkg1, pkg2];
2339 let interned: PackageIndex = packages.into();
2340
2341 let dg = DependencyGraph::new(vec!["Owner2-ModB".to_string()]);
2342 let result = dg.resolve(&interned);
2343
2344 assert_eq!(result.len(), 1);
2345
2346 assert!(result.contains_key("Owner2-ModB-2.0.0.zip"));
2347 }
2348
2349 #[test]
2350 fn test_package_index_dependency_graph_resolve_with_version_suffix() {
2351 let pkg1 = create_test_package("ModA", "Owner1", "1.0.0");
2352
2353 let packages = vec![pkg1];
2354 let interned: PackageIndex = packages.into();
2355
2356 let dg = DependencyGraph::new(vec!["Owner1-ModA-1.0.0".to_string()]);
2357 let result = dg.resolve(&interned);
2358
2359 assert_eq!(result.len(), 1);
2360
2361 assert!(result.contains_key("Owner1-ModA-1.0.0.zip"));
2362 }
2363
2364 fn create_test_package_with_versions(
2366 name: &str,
2367 owner: &str,
2368 versions: &[(&str, &[&str])],
2369 ) -> Package {
2370 let mut pkg = create_test_package(name, owner, versions[0].0);
2371 let template = pkg.versions[0].clone();
2372 let start =
2373 OffsetDateTime::now_utc().saturating_sub(time::Duration::days(versions.len() as i64));
2374
2375 pkg.versions = versions
2376 .iter()
2377 .enumerate()
2378 .map(|(day, (version, deps))| Version {
2379 version_number: Some(version.to_string()),
2380 dependencies: deps.iter().map(|d| d.to_string()).collect(),
2381 download_url: Some(format!("https://example.com/{name}/{version}")),
2382 date_created: start.saturating_add(time::Duration::days(day as i64)),
2383 ..template.clone()
2384 })
2385 .collect();
2386
2387 pkg
2388 }
2389
2390 fn pinning_index() -> PackageIndex {
2391 vec![
2392 create_test_package_with_versions(
2393 "ModA",
2394 "Owner1",
2395 &[
2396 ("1.0.0", &["Owner4-ModD-0.9.0"]),
2397 ("2.0.0", &["Owner3-ModC-1.5.0"]),
2398 ],
2399 ),
2400 create_test_package_with_dependencies(
2401 "ModB",
2402 "Owner2",
2403 "1.0.0",
2404 vec!["Owner1-ModA-1.0.0".to_string()],
2405 ),
2406 create_test_package("ModC", "Owner3", "1.5.0"),
2407 create_test_package("ModD", "Owner4", "0.9.0"),
2408 ]
2409 .into()
2410 }
2411
2412 fn sorted_keys(urls: &HashMap<String, String>) -> Vec<&str> {
2413 let mut keys: Vec<&str> = urls.keys().map(String::as_str).collect();
2414
2415 keys.sort();
2416
2417 keys
2418 }
2419
2420 #[test]
2421 fn test_resolve_pinned_version_uses_that_version_and_its_dependencies() {
2422 let report =
2423 DependencyGraph::new(vec!["Owner1-ModA-1.0.0".to_string()]).resolve_report(&pinning_index());
2424
2425 assert!(report.unresolved.is_empty());
2426 assert_eq!(
2427 sorted_keys(&report.urls),
2428 ["Owner1-ModA-1.0.0.zip", "Owner4-ModD-0.9.0.zip"]
2429 );
2430 assert_eq!(
2431 report.urls["Owner1-ModA-1.0.0.zip"],
2432 "https://example.com/ModA/1.0.0"
2433 );
2434 }
2435
2436 #[test]
2437 fn test_resolve_bare_name_still_resolves_latest() {
2438 let report =
2439 DependencyGraph::new(vec!["Owner1-ModA".to_string()]).resolve_report(&pinning_index());
2440
2441 assert!(report.unresolved.is_empty());
2442 assert_eq!(
2443 sorted_keys(&report.urls),
2444 ["Owner1-ModA-2.0.0.zip", "Owner3-ModC-1.5.0.zip"]
2445 );
2446 }
2447
2448 #[test]
2449 fn test_resolve_missing_pinned_version_is_unresolved_not_latest() {
2450 let report =
2451 DependencyGraph::new(vec!["Owner1-ModA-9.9.9".to_string()]).resolve_report(&pinning_index());
2452
2453 assert!(report.urls.is_empty());
2454 assert_eq!(report.unresolved, vec!["Owner1-ModA-9.9.9".to_string()]);
2455 }
2456
2457 #[test]
2458 fn test_resolve_dependency_string_version_is_not_a_pin() {
2459 let report =
2460 DependencyGraph::new(vec!["Owner2-ModB".to_string()]).resolve_report(&pinning_index());
2461
2462 assert!(report.unresolved.is_empty());
2463 assert_eq!(
2464 sorted_keys(&report.urls),
2465 [
2466 "Owner1-ModA-2.0.0.zip",
2467 "Owner2-ModB-1.0.0.zip",
2468 "Owner3-ModC-1.5.0.zip"
2469 ]
2470 );
2471 }
2472
2473 #[test]
2474 fn test_resolve_pin_on_a_dependency_wins_over_latest() {
2475 let requested = DependencyGraph::new(vec![
2476 "Owner2-ModB".to_string(),
2477 "Owner1-ModA-1.0.0".to_string(),
2478 ])
2479 .resolve_report(&pinning_index());
2480 let pinned_only = DependencyGraph::new(vec!["Owner2-ModB".to_string()])
2481 .pins(["Owner1-ModA-1.0.0".to_string()])
2482 .resolve_report(&pinning_index());
2483
2484 for report in [requested, pinned_only] {
2485 assert!(report.unresolved.is_empty());
2486 assert_eq!(
2487 sorted_keys(&report.urls),
2488 [
2489 "Owner1-ModA-1.0.0.zip",
2490 "Owner2-ModB-1.0.0.zip",
2491 "Owner4-ModD-0.9.0.zip"
2492 ]
2493 );
2494 }
2495 }
2496
2497 #[test]
2498 fn test_resolve_pin_wins_over_a_bare_request_for_the_same_package() {
2499 let report = DependencyGraph::new(vec![
2500 "Owner1-ModA".to_string(),
2501 "Owner1-ModA-1.0.0".to_string(),
2502 ])
2503 .resolve_report(&pinning_index());
2504
2505 assert!(report.unresolved.is_empty());
2506 assert_eq!(
2507 sorted_keys(&report.urls),
2508 ["Owner1-ModA-1.0.0.zip", "Owner4-ModD-0.9.0.zip"]
2509 );
2510 }
2511
2512 #[test]
2513 fn test_resolve_conflicting_pins_leave_the_package_unresolved() {
2514 let report = DependencyGraph::new(vec![
2515 "Owner2-ModB".to_string(),
2516 "Owner1-ModA-2.0.0".to_string(),
2517 ])
2518 .pins(["Owner1-ModA-1.0.0".to_string()])
2519 .resolve_report(&pinning_index());
2520
2521 assert_eq!(
2522 report.unresolved,
2523 vec![
2524 "Owner1-ModA-1.0.0".to_string(),
2525 "Owner1-ModA-2.0.0".to_string()
2526 ]
2527 );
2528 assert_eq!(sorted_keys(&report.urls), ["Owner2-ModB-1.0.0.zip"]);
2529 }
2530
2531 #[test]
2532 fn test_resolve_ignores_unversioned_and_unreached_pins() {
2533 let report = DependencyGraph::new(vec!["Owner3-ModC".to_string()])
2534 .pins(["Owner1-ModA".to_string(), "Owner1-ModA-9.9.9".to_string()])
2535 .resolve_report(&pinning_index());
2536
2537 assert!(report.unresolved.is_empty());
2538 assert_eq!(sorted_keys(&report.urls), ["Owner3-ModC-1.5.0.zip"]);
2539 }
2540
2541 #[test]
2542 fn test_cache_rejects_unsupported_format_version() {
2543 let pkg = create_test_package("TestMod", "TestOwner", "1.0.0");
2544 let interned: PackageIndex = vec![pkg].into();
2545 let mut serializable: SerializedPackageIndex = (&interned).into();
2546
2547 serializable.format_version = CACHE_FORMAT_VERSION + 1;
2550
2551 let result = PackageIndex::try_from(serializable);
2552
2553 assert!(matches!(result, Err(Error::UnsupportedCacheVersion { .. })));
2554 }
2555
2556 #[test]
2557 fn test_cache_current_format_version_round_trips() {
2558 let pkg = create_test_package("TestMod", "TestOwner", "1.0.0");
2559 let interned: PackageIndex = vec![pkg].into();
2560 let serializable: SerializedPackageIndex = (&interned).into();
2561
2562 assert_eq!(serializable.format_version, CACHE_FORMAT_VERSION);
2563 assert!(PackageIndex::try_from(serializable).is_ok());
2564 }
2565
2566 #[test]
2567 fn test_validate_rejects_version_owned_by_wrong_package() {
2568 let pkg1 = create_test_package("ModA", "Owner1", "1.0.0");
2569 let pkg2 = create_test_package("ModB", "Owner2", "2.0.0");
2570 let mut interned: PackageIndex = vec![pkg1, pkg2].into();
2571
2572 interned.versions.package_indices[0] = 1;
2574
2575 let result = interned.validate();
2576
2577 assert!(result.is_err());
2578
2579 assert!(result.unwrap_err().to_string().contains("package_index"));
2580 }
2581
2582 #[test]
2583 fn test_validate_rejects_overlapping_version_ranges() {
2584 let pkg1 = create_test_package("ModA", "Owner1", "1.0.0");
2585 let pkg2 = create_test_package("ModB", "Owner2", "2.0.0");
2586 let mut interned: PackageIndex = vec![pkg1, pkg2].into();
2587
2588 interned.version_ranges[1] = (0, 1);
2590
2591 let result = interned.validate();
2592
2593 assert!(result.is_err());
2594
2595 assert!(result.unwrap_err().to_string().contains("overlapping"));
2596 }
2597
2598 #[test]
2599 fn test_package_index_validate_rejects_duplicate_full_names() {
2600 let pkg1 = create_test_package("TestMod", "TestOwner", "1.0.0");
2605 let pkg2 = create_test_package("TestMod", "TestOwner", "2.0.0");
2606 let interned: PackageIndex = vec![pkg1, pkg2].into();
2607
2608 let result = interned.validate();
2609
2610 assert!(result.is_err());
2611
2612 assert!(
2613 result
2614 .unwrap_err()
2615 .to_string()
2616 .contains("duplicate full name")
2617 );
2618 }
2619
2620 #[test]
2621 fn test_package_index_dependency_graph_resolve_handles_cycles() {
2622 let pkg_a = create_test_package_with_dependencies(
2625 "ModA",
2626 "Owner1",
2627 "1.0.0",
2628 vec!["Owner2-ModB".to_string()],
2629 );
2630 let pkg_b = create_test_package_with_dependencies(
2631 "ModB",
2632 "Owner2",
2633 "2.0.0",
2634 vec!["Owner1-ModA".to_string()],
2635 );
2636 let pkg_c = create_test_package_with_dependencies(
2637 "ModC",
2638 "Owner3",
2639 "3.0.0",
2640 vec!["Owner3-ModC".to_string()],
2641 );
2642
2643 let packages = vec![pkg_a, pkg_b, pkg_c];
2644 let interned: PackageIndex = packages.into();
2645
2646 let dg = DependencyGraph::new(vec!["Owner1-ModA".to_string(), "Owner3-ModC".to_string()]);
2647 let result = dg.resolve(&interned);
2648
2649 assert_eq!(result.len(), 3);
2650
2651 assert!(result.contains_key("Owner1-ModA-1.0.0.zip"));
2652
2653 assert!(result.contains_key("Owner2-ModB-2.0.0.zip"));
2654
2655 assert!(result.contains_key("Owner3-ModC-3.0.0.zip"));
2656 }
2657}