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 fn build_name_index(&self) -> HashMap<String, usize> {
328 self
329 .full_names
330 .iter()
331 .enumerate()
332 .filter_map(|(idx, key)| {
333 let name = resolve_option(&self.interner, *key)?;
334 Some((name, idx))
335 })
336 .collect()
337 }
338
339 pub fn validate(&self) -> crate::Result<()> {
346 self.validate_lengths()?;
347 self.validate_version_ranges()?;
348 self.validate_version_ownership()?;
349 self.validate_unique_full_names()?;
350
351 Ok(())
352 }
353
354 fn validate_lengths(&self) -> crate::Result<()> {
358 let pkg_count = self.names.len();
359
360 check_len("full_names", self.full_names.len(), pkg_count)?;
361 check_len("owners", self.owners.len(), pkg_count)?;
362 check_len("package_urls", self.package_urls.len(), pkg_count)?;
363 check_len("dates_created", self.dates_created.len(), pkg_count)?;
364 check_len("dates_updated", self.dates_updated.len(), pkg_count)?;
365 check_len("uuid4s", self.uuid4s.len(), pkg_count)?;
366 check_len("rating_scores", self.rating_scores.len(), pkg_count)?;
367 check_len("is_pinned", self.is_pinned.len(), pkg_count)?;
368 check_len("is_deprecated", self.is_deprecated.len(), pkg_count)?;
369 check_len("has_nsfw_content", self.has_nsfw_content.len(), pkg_count)?;
370 check_len("categories", self.categories.len(), pkg_count)?;
371 check_len("version_ranges", self.version_ranges.len(), pkg_count)?;
372
373 let version_count = self.versions.package_indices.len();
374
375 check_len("versions.names", self.versions.names.len(), version_count)?;
376 check_len(
377 "versions.full_names",
378 self.versions.full_names.len(),
379 version_count,
380 )?;
381 check_len(
382 "versions.version_numbers",
383 self.versions.version_numbers.len(),
384 version_count,
385 )?;
386 check_len(
387 "versions.download_urls",
388 self.versions.download_urls.len(),
389 version_count,
390 )?;
391 check_len(
392 "versions.dependencies",
393 self.versions.dependencies.len(),
394 version_count,
395 )?;
396 check_len(
397 "versions.dates_created",
398 self.versions.dates_created.len(),
399 version_count,
400 )?;
401 check_len(
402 "versions.descriptions",
403 self.versions.descriptions.len(),
404 version_count,
405 )?;
406 check_len("versions.icons", self.versions.icons.len(), version_count)?;
407 check_len(
408 "versions.downloads",
409 self.versions.downloads.len(),
410 version_count,
411 )?;
412 check_len(
413 "versions.website_urls",
414 self.versions.website_urls.len(),
415 version_count,
416 )?;
417 check_len(
418 "versions.is_active",
419 self.versions.is_active.len(),
420 version_count,
421 )?;
422 check_len("versions.uuid4s", self.versions.uuid4s.len(), version_count)?;
423 check_len(
424 "versions.file_sizes",
425 self.versions.file_sizes.len(),
426 version_count,
427 )?;
428
429 Ok(())
430 }
431
432 fn validate_version_ranges(&self) -> crate::Result<()> {
437 let pkg_count = self.names.len();
438 let version_count = self.versions.package_indices.len();
439
440 for (idx, (start, end)) in self.version_ranges.iter().enumerate() {
441 if start > end {
442 return Err(Error::Cache(format!(
443 "Invalid version range at package {}: {} > {}",
444 idx, start, end
445 )));
446 }
447
448 if *end > version_count {
449 return Err(Error::Cache(format!(
450 "Version range at package {} ends at {} but only {} versions exist",
451 idx, end, version_count
452 )));
453 }
454 }
455
456 for (i, &pkg_idx) in self.versions.package_indices.iter().enumerate() {
457 if pkg_idx >= pkg_count {
458 return Err(Error::Cache(format!(
459 "version {} references package index {} but only {} packages exist",
460 i, pkg_idx, pkg_count
461 )));
462 }
463 }
464
465 Ok(())
466 }
467
468 fn validate_version_ownership(&self) -> crate::Result<()> {
475 let version_count = self.versions.package_indices.len();
476
477 let mut owner_of: Vec<Option<usize>> = vec![None; version_count];
478
479 for (pkg_idx, range) in self.version_ranges.iter().enumerate() {
480 let (start, end) = *range;
481
482 for (offset, &owner_pkg) in self.versions.package_indices[start..end].iter().enumerate() {
483 let ver_idx = start + offset;
484
485 if owner_of[ver_idx].is_some() {
486 return Err(Error::Cache(format!(
487 "version {} is claimed by multiple packages (overlapping version ranges)",
488 ver_idx
489 )));
490 }
491
492 owner_of[ver_idx] = Some(pkg_idx);
493
494 if owner_pkg != pkg_idx {
495 return Err(Error::Cache(format!(
496 "version {} lies in package {}'s range but records package_index {}",
497 ver_idx, pkg_idx, owner_pkg
498 )));
499 }
500 }
501 }
502
503 for (ver_idx, owner) in owner_of.iter().enumerate() {
504 if owner.is_none() {
505 return Err(Error::Cache(format!(
506 "version {} is not covered by any package's version range",
507 ver_idx
508 )));
509 }
510 }
511
512 Ok(())
513 }
514
515 fn validate_unique_full_names(&self) -> crate::Result<()> {
521 let mut seen_full_names: HashSet<&str> = HashSet::new();
522
523 for key in self.full_names.iter().flatten() {
524 let full_name = self.interner.resolve(key);
525
526 if !seen_full_names.insert(full_name) {
527 return Err(Error::Cache(format!(
528 "duplicate full name {:?} appears in multiple packages",
529 full_name
530 )));
531 }
532 }
533
534 Ok(())
535 }
536}
537
538fn check_len(field: &str, actual: usize, expected: usize) -> crate::Result<()> {
541 if actual != expected {
542 return Err(Error::Cache(format!(
543 "{field} length {actual} != {expected}"
544 )));
545 }
546
547 Ok(())
548}
549
550#[derive(Serialize, Deserialize, Clone)]
558#[non_exhaustive]
559pub struct Package {
560 pub name: Option<String>,
562 pub full_name: Option<String>,
564 pub owner: Option<String>,
566 pub package_url: Option<String>,
568 #[serde(with = "time::serde::rfc3339")]
570 pub date_created: time::OffsetDateTime,
571 #[serde(with = "time::serde::rfc3339")]
573 pub date_updated: time::OffsetDateTime,
574 pub uuid4: Option<String>,
576 pub rating_score: Option<u32>,
578 pub is_pinned: Option<bool>,
580 pub is_deprecated: Option<bool>,
582 pub has_nsfw_content: Option<bool>,
584 pub categories: Vec<String>,
586 pub versions: Vec<Version>,
588}
589
590#[derive(Serialize, Deserialize, Debug, Clone)]
598#[non_exhaustive]
599pub struct Version {
600 pub name: Option<String>,
602 pub full_name: Option<String>,
604 pub description: Option<String>,
606 pub icon: Option<String>,
608 pub version_number: Option<String>,
610 pub dependencies: Vec<String>,
612 pub download_url: Option<String>,
614 pub downloads: Option<u32>,
616 #[serde(with = "time::serde::rfc3339")]
618 pub date_created: time::OffsetDateTime,
619 pub website_url: Option<String>,
621 pub is_active: Option<bool>,
623 pub uuid4: Option<String>,
625 pub file_size: Option<u64>,
627}
628
629impl Package {
630 pub fn latest_version(&self) -> Option<&Version> {
637 self.versions.iter().max_by_key(|v| v.date_created)
638 }
639
640 #[cfg(test)]
648 pub fn zip_and_url(&self) -> Option<(String, String)> {
649 let pkg = self.latest_version()?;
650 let url: String = pkg.download_url.clone()?;
651 let version_number = pkg.version_number.as_ref()?;
652 let package_name = &self.full_name.as_ref()?;
653 let zip_name = format!("{}-{}.zip", package_name, version_number);
654
655 Some((zip_name, url))
656 }
657}
658
659impl std::fmt::Debug for Package {
662 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
663 f.debug_struct("Package")
664 .field("full_name", &self.full_name)
665 .field(
666 "dependencies",
667 &self.latest_version().map(|v| &v.dependencies),
668 )
669 .field(
670 "download_url",
671 &self.latest_version().map(|v| &v.download_url),
672 )
673 .finish()
674 }
675}
676
677#[derive(Debug, Clone, Default)]
683#[non_exhaustive]
684pub struct Resolution {
685 pub urls: HashMap<String, String>,
688 pub unresolved: Vec<String>,
692}
693
694#[derive(Debug, Deserialize, Serialize)]
699pub struct DependencyGraph {
700 install_packages: Vec<String>,
702}
703
704impl DependencyGraph {
705 pub fn new(install_packages: Vec<String>) -> Self {
711 Self { install_packages }
712 }
713
714 pub fn resolve(&self, index: &PackageIndex) -> HashMap<String, String> {
738 self.resolve_report(index).urls
739 }
740
741 pub fn resolve_report(&self, index: &PackageIndex) -> Resolution {
753 let name_index = index.build_name_index();
754 let mut sorted_set: BTreeSet<String> = self.install_packages.iter().cloned().collect();
755 let mut install_indices = Vec::new();
756 let mut seen: HashSet<usize> = HashSet::new();
757 let mut unresolved: Vec<String> = Vec::new();
758
759 tracing::debug!("Starting with mod list of: {:#?}", sorted_set);
760
761 while !sorted_set.is_empty() {
762 let Some(item) = sorted_set.pop_first() else {
763 continue;
764 };
765
766 let item = crate::util::full_name_prefix(&item).to_string();
767
768 let Some(&pkg_idx) = name_index.get(&item) else {
769 tracing::warn!(
770 "requested package {:?} was not found in the index; the install will be incomplete",
771 item
772 );
773 unresolved.push(item);
774
775 continue;
776 };
777
778 if !seen.insert(pkg_idx) {
781 continue;
782 }
783
784 if let Some(latest_ver_idx) = index.get_latest_version_at(pkg_idx) {
785 let deps = resolve_vec(
786 &index.interner,
787 &index.versions.dependencies[latest_ver_idx],
788 );
789
790 for dep in deps {
791 tracing::debug!(
792 "Found dependency of {:#?} for {:#?} mod",
793 dep,
794 index.resolve_name_at(pkg_idx).unwrap_or_default(),
795 );
796 sorted_set.insert(dep);
797 }
798 }
799
800 install_indices.push(pkg_idx);
801 }
802
803 let mut urls = HashMap::new();
804
805 for &idx in &install_indices {
806 if let Some((zip_name, url)) = Self::package_zip_and_url(index, idx) {
807 urls.insert(zip_name, url);
808 } else if let Some(name) = index
809 .resolve_full_name_at(idx)
810 .or_else(|| index.resolve_name_at(idx))
811 {
812 tracing::warn!(
816 "package {:?} has no downloadable version; the install will be incomplete",
817 name
818 );
819 unresolved.push(name);
820 }
821 }
822
823 unresolved.sort();
824 unresolved.dedup();
825
826 Resolution { urls, unresolved }
827 }
828
829 fn package_zip_and_url(index: &PackageIndex, idx: usize) -> Option<(String, String)> {
833 let latest_ver_idx = index.get_latest_version_at(idx)?;
834 let url = resolve_option(
835 &index.interner,
836 index.versions.download_urls[latest_ver_idx],
837 )?;
838 let version_number = resolve_option(
839 &index.interner,
840 index.versions.version_numbers[latest_ver_idx],
841 )?;
842 let package_name = index.resolve_full_name_at(idx)?;
843 let zip_name = format!("{}-{}.zip", package_name, version_number);
844
845 Some((zip_name, url))
846 }
847}
848
849impl From<Vec<Package>> for PackageIndex {
850 fn from(packages: Vec<Package>) -> Self {
851 let num_packages = packages.len();
852 let total_versions: usize = packages.iter().map(|p| p.versions.len()).sum();
853
854 let mut interner = StringInterner::default();
855
856 let mut names = Vec::with_capacity(num_packages);
857 let mut full_names = Vec::with_capacity(num_packages);
858 let mut owners = Vec::with_capacity(num_packages);
859 let mut package_urls = Vec::with_capacity(num_packages);
860 let mut dates_created = Vec::with_capacity(num_packages);
861 let mut dates_updated = Vec::with_capacity(num_packages);
862 let mut uuid4s = Vec::with_capacity(num_packages);
863 let mut rating_scores = Vec::with_capacity(num_packages);
864 let mut is_pinned = Vec::with_capacity(num_packages);
865 let mut is_deprecated = Vec::with_capacity(num_packages);
866 let mut has_nsfw_content = Vec::with_capacity(num_packages);
867 let mut categories = Vec::with_capacity(num_packages);
868 let mut version_ranges = Vec::with_capacity(num_packages);
869
870 let mut package_indices = Vec::with_capacity(total_versions);
871 let mut version_names = Vec::with_capacity(total_versions);
872 let mut version_full_names = Vec::with_capacity(total_versions);
873 let mut version_numbers = Vec::with_capacity(total_versions);
874 let mut download_urls = Vec::with_capacity(total_versions);
875 let mut dependencies = Vec::with_capacity(total_versions);
876 let mut version_dates_created = Vec::with_capacity(total_versions);
877 let mut descriptions = Vec::with_capacity(total_versions);
878 let mut icons = Vec::with_capacity(total_versions);
879 let mut downloads = Vec::with_capacity(total_versions);
880 let mut website_urls = Vec::with_capacity(total_versions);
881 let mut is_active = Vec::with_capacity(total_versions);
882 let mut version_uuid4s = Vec::with_capacity(total_versions);
883 let mut file_sizes = Vec::with_capacity(total_versions);
884
885 let mut version_offset = 0;
886
887 for (pkg_idx, package) in packages.into_iter().enumerate() {
888 names.push(intern_option(&mut interner, package.name.as_deref()));
889 full_names.push(intern_option(&mut interner, package.full_name.as_deref()));
890 owners.push(intern_option(&mut interner, package.owner.as_deref()));
891 package_urls.push(intern_option(&mut interner, package.package_url.as_deref()));
892 dates_created.push(package.date_created);
893 dates_updated.push(package.date_updated);
894 uuid4s.push(intern_option(&mut interner, package.uuid4.as_deref()));
895 rating_scores.push(package.rating_score);
896 is_pinned.push(package.is_pinned);
897 is_deprecated.push(package.is_deprecated);
898 has_nsfw_content.push(package.has_nsfw_content);
899 categories.push(intern_vec(&mut interner, &package.categories));
900
901 let version_count = package.versions.len();
902 version_ranges.push((version_offset, version_offset + version_count));
903
904 for version in package.versions {
905 package_indices.push(pkg_idx);
906 version_names.push(intern_option(&mut interner, version.name.as_deref()));
907 version_full_names.push(intern_option(&mut interner, version.full_name.as_deref()));
908 version_numbers.push(intern_option(
909 &mut interner,
910 version.version_number.as_deref(),
911 ));
912 download_urls.push(intern_option(
913 &mut interner,
914 version.download_url.as_deref(),
915 ));
916 dependencies.push(intern_vec(&mut interner, &version.dependencies));
917 version_dates_created.push(version.date_created);
918 descriptions.push(intern_option(&mut interner, version.description.as_deref()));
919 icons.push(intern_option(&mut interner, version.icon.as_deref()));
920 downloads.push(version.downloads);
921 website_urls.push(intern_option(&mut interner, version.website_url.as_deref()));
922 is_active.push(version.is_active);
923 version_uuid4s.push(intern_option(&mut interner, version.uuid4.as_deref()));
924 file_sizes.push(version.file_size);
925 }
926
927 version_offset += version_count;
928 }
929
930 PackageIndex {
931 interner,
932 names,
933 full_names,
934 owners,
935 package_urls,
936 dates_created,
937 dates_updated,
938 uuid4s,
939 rating_scores,
940 is_pinned,
941 is_deprecated,
942 has_nsfw_content,
943 categories,
944 version_ranges,
945 versions: VersionTable {
946 package_indices,
947 names: version_names,
948 full_names: version_full_names,
949 version_numbers,
950 download_urls,
951 dependencies,
952 dates_created: version_dates_created,
953 descriptions,
954 icons,
955 downloads,
956 website_urls,
957 is_active,
958 uuid4s: version_uuid4s,
959 file_sizes,
960 },
961 }
962 }
963}
964
965fn key_to_index(_interner: &StringInterner, key: InternKey) -> u32 {
976 let index = key.to_index();
977
978 u32::try_from(index)
982 .unwrap_or_else(|_| panic!("intern key index {index} exceeds u32 range and would truncate"))
983}
984
985fn decode_key(index: u32, table_len: usize) -> Result<InternKey, Error> {
992 if index as usize >= table_len {
993 return Err(Error::Cache(format!(
994 "interned string index {} out of range (string table length {})",
995 index, table_len
996 )));
997 }
998
999 InternKey::from_index(index as usize)
1000 .ok_or_else(|| Error::Cache(format!("invalid interned key index {}", index)))
1001}
1002
1003fn encode_dates(dates: &[OffsetDateTime]) -> Vec<i128> {
1010 dates.iter().map(|d| d.unix_timestamp_nanos()).collect()
1011}
1012
1013fn decode_dates(nanos: &[i128]) -> Result<Vec<OffsetDateTime>, Error> {
1017 nanos
1018 .iter()
1019 .map(|&n| {
1020 OffsetDateTime::from_unix_timestamp_nanos(n)
1021 .map_err(|e| Error::Cache(format!("invalid cached timestamp {}: {}", n, e)))
1022 })
1023 .collect()
1024}
1025
1026impl From<&PackageIndex> for SerializedPackageIndex {
1027 fn from(manifest: &PackageIndex) -> Self {
1028 let interner = &manifest.interner;
1029 let string_table: Vec<String> = interner.strings().map(|s| s.to_string()).collect();
1030
1031 let opt = |keys: &[Option<InternKey>]| -> Vec<Option<u32>> {
1034 keys
1035 .iter()
1036 .map(|k| k.map(|key| key_to_index(interner, key)))
1037 .collect()
1038 };
1039
1040 let nested = |lists: &[Vec<InternKey>]| -> Vec<Vec<u32>> {
1041 lists
1042 .iter()
1043 .map(|keys| keys.iter().map(|k| key_to_index(interner, *k)).collect())
1044 .collect()
1045 };
1046
1047 SerializedPackageIndex {
1048 format_version: CACHE_FORMAT_VERSION,
1049 string_table,
1050 names: opt(&manifest.names),
1051 full_names: opt(&manifest.full_names),
1052 owners: opt(&manifest.owners),
1053 package_urls: opt(&manifest.package_urls),
1054 dates_created: encode_dates(&manifest.dates_created),
1055 dates_updated: encode_dates(&manifest.dates_updated),
1056 uuid4s: opt(&manifest.uuid4s),
1057 rating_scores: manifest.rating_scores.clone(),
1058 is_pinned: manifest.is_pinned.clone(),
1059 is_deprecated: manifest.is_deprecated.clone(),
1060 has_nsfw_content: manifest.has_nsfw_content.clone(),
1061 categories: nested(&manifest.categories),
1062 version_ranges: manifest.version_ranges.clone(),
1063 versions: SerializedVersionTable {
1064 package_indices: manifest.versions.package_indices.clone(),
1065 names: opt(&manifest.versions.names),
1066 full_names: opt(&manifest.versions.full_names),
1067 version_numbers: opt(&manifest.versions.version_numbers),
1068 download_urls: opt(&manifest.versions.download_urls),
1069 dependencies: nested(&manifest.versions.dependencies),
1070 dates_created: encode_dates(&manifest.versions.dates_created),
1071 descriptions: opt(&manifest.versions.descriptions),
1072 icons: opt(&manifest.versions.icons),
1073 downloads: manifest.versions.downloads.clone(),
1074 website_urls: opt(&manifest.versions.website_urls),
1075 is_active: manifest.versions.is_active.clone(),
1076 uuid4s: opt(&manifest.versions.uuid4s),
1077 file_sizes: manifest.versions.file_sizes.clone(),
1078 },
1079 }
1080 }
1081}
1082
1083impl TryFrom<SerializedPackageIndex> for PackageIndex {
1084 type Error = Error;
1085
1086 fn try_from(manifest: SerializedPackageIndex) -> Result<Self, Self::Error> {
1090 if manifest.format_version != CACHE_FORMAT_VERSION {
1091 return Err(Error::UnsupportedCacheVersion {
1092 found: manifest.format_version,
1093 expected: CACHE_FORMAT_VERSION,
1094 });
1095 }
1096
1097 let mut interner = StringInterner::default();
1098
1099 for s in &manifest.string_table {
1100 interner.get_or_intern(s);
1101 }
1102
1103 let table_len = manifest.string_table.len();
1104
1105 if interner.len() != table_len {
1113 return Err(Error::Cache(
1114 "cache string table contains duplicate or unresolvable entries".to_string(),
1115 ));
1116 }
1117
1118 let opt = |v: &[Option<u32>]| -> Result<Vec<Option<InternKey>>, Error> {
1119 v.iter()
1120 .map(|idx| idx.map(|i| decode_key(i, table_len)).transpose())
1121 .collect()
1122 };
1123
1124 let nested = |v: &[Vec<u32>]| -> Result<Vec<Vec<InternKey>>, Error> {
1125 v.iter()
1126 .map(|indices| indices.iter().map(|&i| decode_key(i, table_len)).collect())
1127 .collect()
1128 };
1129
1130 let names = opt(&manifest.names)?;
1131 let full_names = opt(&manifest.full_names)?;
1132 let owners = opt(&manifest.owners)?;
1133 let package_urls = opt(&manifest.package_urls)?;
1134 let uuid4s = opt(&manifest.uuid4s)?;
1135 let categories = nested(&manifest.categories)?;
1136
1137 let version_names = opt(&manifest.versions.names)?;
1138 let version_full_names = opt(&manifest.versions.full_names)?;
1139 let version_numbers = opt(&manifest.versions.version_numbers)?;
1140 let download_urls = opt(&manifest.versions.download_urls)?;
1141 let dependencies = nested(&manifest.versions.dependencies)?;
1142 let descriptions = opt(&manifest.versions.descriptions)?;
1143 let icons = opt(&manifest.versions.icons)?;
1144 let website_urls = opt(&manifest.versions.website_urls)?;
1145 let version_uuid4s = opt(&manifest.versions.uuid4s)?;
1146
1147 let dates_created = decode_dates(&manifest.dates_created)?;
1148 let dates_updated = decode_dates(&manifest.dates_updated)?;
1149 let version_dates_created = decode_dates(&manifest.versions.dates_created)?;
1150
1151 let result = PackageIndex {
1152 interner,
1153 names,
1154 full_names,
1155 owners,
1156 package_urls,
1157 dates_created,
1158 dates_updated,
1159 uuid4s,
1160 rating_scores: manifest.rating_scores,
1161 is_pinned: manifest.is_pinned,
1162 is_deprecated: manifest.is_deprecated,
1163 has_nsfw_content: manifest.has_nsfw_content,
1164 categories,
1165 version_ranges: manifest.version_ranges,
1166 versions: VersionTable {
1167 package_indices: manifest.versions.package_indices,
1168 names: version_names,
1169 full_names: version_full_names,
1170 version_numbers,
1171 download_urls,
1172 dependencies,
1173 dates_created: version_dates_created,
1174 descriptions,
1175 icons,
1176 downloads: manifest.versions.downloads,
1177 website_urls,
1178 is_active: manifest.versions.is_active,
1179 uuid4s: version_uuid4s,
1180 file_sizes: manifest.versions.file_sizes,
1181 },
1182 };
1183
1184 result.validate()?;
1185
1186 Ok(result)
1187 }
1188}
1189
1190#[cfg(test)]
1191mod tests {
1192 use super::*;
1193 use time::OffsetDateTime;
1194
1195 fn create_test_package(name: &str, owner: &str, version: &str) -> Package {
1196 let full_name = format!("{}-{}", owner, name);
1197 Package {
1198 name: Some(name.to_string()),
1199 full_name: Some(full_name.clone()),
1200 owner: Some(owner.to_string()),
1201 package_url: Some(format!("https://example.com/{}", name)),
1202 date_created: OffsetDateTime::now_utc(),
1203 date_updated: OffsetDateTime::now_utc(),
1204 uuid4: Some("test-uuid".to_string()),
1205 rating_score: Some(5),
1206 is_pinned: Some(false),
1207 is_deprecated: Some(false),
1208 has_nsfw_content: Some(false),
1209 categories: vec!["category1".to_string()],
1210 versions: vec![Version {
1211 name: Some(name.to_string()),
1212 full_name: Some(full_name.clone()),
1213 description: Some("Test description".to_string()),
1214 icon: Some("icon.png".to_string()),
1215 version_number: Some(version.to_string()),
1216 dependencies: vec![],
1217 download_url: Some(format!("https://example.com/{}/download", name)),
1218 downloads: Some(100),
1219 date_created: OffsetDateTime::now_utc(),
1220 website_url: Some("https://example.com".to_string()),
1221 is_active: Some(true),
1222 uuid4: Some("test-version-uuid".to_string()),
1223 file_size: Some(1024),
1224 }],
1225 }
1226 }
1227
1228 fn create_test_package_with_dependencies(
1229 name: &str,
1230 owner: &str,
1231 version: &str,
1232 dependencies: Vec<String>,
1233 ) -> Package {
1234 let mut pkg = create_test_package(name, owner, version);
1235 if let Some(latest_version) = pkg.versions.first_mut() {
1236 latest_version.dependencies = dependencies;
1237 }
1238 pkg
1239 }
1240
1241 #[test]
1242 fn test_latest_version() {
1243 let mut pkg = create_test_package("TestMod", "TestOwner", "1.0.0");
1244
1245 let older_version = Version {
1246 name: Some("TestMod".to_string()),
1247 full_name: Some("TestOwner-TestMod".to_string()),
1248 description: Some("Older version".to_string()),
1249 icon: Some("icon.png".to_string()),
1250 version_number: Some("0.9.0".to_string()),
1251 dependencies: vec![],
1252 download_url: Some("https://example.com/TestMod/download-old".to_string()),
1253 downloads: Some(50),
1254 date_created: OffsetDateTime::now_utc().saturating_sub(time::Duration::days(30)),
1255 website_url: Some("https://example.com".to_string()),
1256 is_active: Some(true),
1257 uuid4: Some("old-version-uuid".to_string()),
1258 file_size: Some(512),
1259 };
1260 pkg.versions.push(older_version);
1261
1262 let newer_version = Version {
1263 name: Some("TestMod".to_string()),
1264 full_name: Some("TestOwner-TestMod".to_string()),
1265 description: Some("Newer version".to_string()),
1266 icon: Some("icon.png".to_string()),
1267 version_number: Some("1.1.0".to_string()),
1268 dependencies: vec![],
1269 download_url: Some("https://example.com/TestMod/download-new".to_string()),
1270 downloads: Some(150),
1271 date_created: OffsetDateTime::now_utc().saturating_add(time::Duration::days(30)),
1272 website_url: Some("https://example.com".to_string()),
1273 is_active: Some(true),
1274 uuid4: Some("new-version-uuid".to_string()),
1275 file_size: Some(2048),
1276 };
1277 pkg.versions.push(newer_version);
1278
1279 let latest = pkg.latest_version().unwrap();
1280 assert_eq!(latest.version_number, Some("1.1.0".to_string()));
1281 assert_eq!(latest.description, Some("Newer version".to_string()));
1282 }
1283
1284 #[test]
1285 fn test_zip_and_url() {
1286 let pkg = create_test_package("TestMod", "TestOwner", "1.0.0");
1287 let (filename, url) = pkg.zip_and_url().unwrap();
1288
1289 assert_eq!(filename, "TestOwner-TestMod-1.0.0.zip");
1290 assert_eq!(url, "https://example.com/TestMod/download");
1291 }
1292
1293 #[test]
1294 fn test_zip_and_url_missing_data() {
1295 let mut pkg = create_test_package("TestMod", "TestOwner", "1.0.0");
1296
1297 if let Some(version) = pkg.versions.first_mut() {
1298 version.version_number = None;
1299 }
1300
1301 assert!(pkg.zip_and_url().is_none());
1302
1303 let mut pkg = create_test_package("TestMod", "TestOwner", "1.0.0");
1304
1305 if let Some(version) = pkg.versions.first_mut() {
1306 version.download_url = None;
1307 }
1308
1309 assert!(pkg.zip_and_url().is_none());
1310
1311 let mut pkg = create_test_package("TestMod", "TestOwner", "1.0.0");
1312 pkg.full_name = None;
1313
1314 assert!(pkg.zip_and_url().is_none());
1315 }
1316
1317 #[test]
1318 fn test_package_debug() {
1319 let pkg = create_test_package("ModTest", "OwnerTest", "1.2.3");
1320 let debug_output = format!("{:?}", pkg);
1321
1322 assert!(debug_output.contains("Package"));
1323 assert!(debug_output.contains("full_name: Some(\"OwnerTest-ModTest\")"));
1324 assert!(debug_output.contains("dependencies: Some([])"));
1325 assert!(
1326 debug_output.contains("download_url: Some(Some(\"https://example.com/ModTest/download\"))")
1327 );
1328
1329 let mut pkg_with_deps = create_test_package_with_dependencies(
1330 "ModWithDeps",
1331 "OwnerTest",
1332 "2.0.0",
1333 vec!["Dep1-Mod1".to_string(), "Dep2-Mod2".to_string()],
1334 );
1335 let debug_with_deps = format!("{:?}", pkg_with_deps);
1336
1337 assert!(debug_with_deps.contains("dependencies: Some([\"Dep1-Mod1\", \"Dep2-Mod2\"])"));
1338
1339 pkg_with_deps.full_name = None;
1340 let debug_missing_name = format!("{:?}", pkg_with_deps);
1341 assert!(debug_missing_name.contains("full_name: None"));
1342
1343 let mut pkg_no_version = create_test_package("NoVersion", "TestOwner", "1.0.0");
1344 pkg_no_version.versions.clear();
1345 let debug_no_version = format!("{:?}", pkg_no_version);
1346
1347 assert!(debug_no_version.contains("dependencies: None"));
1348 assert!(debug_no_version.contains("download_url: None"));
1349 }
1350
1351 #[test]
1352 fn test_package_index_basic_operations() {
1353 let pkg = create_test_package("TestMod", "TestOwner", "1.0.0");
1354 let packages = vec![pkg];
1355 let interned: PackageIndex = packages.into();
1356
1357 assert_eq!(interned.len(), 1);
1358
1359 assert!(!interned.is_empty());
1360
1361 assert_eq!(interned.resolve_name_at(0), Some("TestMod".to_string()));
1362
1363 assert_eq!(
1364 interned.resolve_full_name_at(0),
1365 Some("TestOwner-TestMod".to_string())
1366 );
1367
1368 assert_eq!(interned.resolve_owner_at(0), Some("TestOwner".to_string()));
1369 }
1370
1371 #[test]
1372 fn search_matches_name_and_full_name_case_insensitively() {
1373 let packages = vec![
1374 create_test_package("CoolMod", "Author", "1.0.0"),
1375 create_test_package("OtherThing", "Someone", "1.0.0"),
1376 ];
1377 let index: PackageIndex = packages.into();
1378
1379 assert_eq!(index.search("coolmod"), vec![0]);
1381 assert_eq!(index.search("COOLMOD"), vec![0]);
1382 assert_eq!(index.search("author-cool"), vec![0]);
1384 assert_eq!(index.search("oolMo"), vec![0]);
1386 assert!(index.search("nothing-like-this").is_empty());
1388 assert_eq!(index.search(""), vec![0, 1]);
1391 }
1392
1393 #[test]
1394 fn test_package_index_borrowing_accessors() {
1395 let pkg = create_test_package("TestMod", "TestOwner", "1.0.0");
1396 let packages = vec![pkg];
1397 let interned: PackageIndex = packages.into();
1398
1399 assert_eq!(interned.name_at(0), Some("TestMod"));
1402
1403 assert_eq!(interned.full_name_at(0), Some("TestOwner-TestMod"));
1404
1405 assert_eq!(interned.owner_at(0), Some("TestOwner"));
1406
1407 assert_eq!(interned.name_at(1), None);
1408
1409 assert_eq!(interned.full_name_at(1), None);
1410
1411 assert_eq!(interned.owner_at(1), None);
1412 }
1413
1414 #[test]
1415 fn test_package_index_get_package_by_full_name() {
1416 let pkg = create_test_package("TestMod", "TestOwner", "1.0.0");
1417 let packages = vec![pkg];
1418 let interned: PackageIndex = packages.into();
1419
1420 let found = interned.get_package_by_full_name("TestOwner-TestMod");
1421
1422 assert!(found.is_some());
1423
1424 let package = found.unwrap();
1425
1426 assert_eq!(package.name, Some("TestMod".to_string()));
1427
1428 assert_eq!(package.owner, Some("TestOwner".to_string()));
1429
1430 assert_eq!(package.versions.len(), 1);
1431 }
1432
1433 #[test]
1434 fn test_package_index_find_index_by_full_name() {
1435 let pkg1 = create_test_package("ModA", "Owner1", "1.0.0");
1436 let pkg2 = create_test_package("ModB", "Owner2", "2.0.0");
1437 let packages = vec![pkg1, pkg2];
1438 let interned: PackageIndex = packages.into();
1439
1440 assert_eq!(interned.find_index_by_full_name("Owner1-ModA"), Some(0));
1441
1442 assert_eq!(interned.find_index_by_full_name("Owner2-ModB"), Some(1));
1443
1444 assert_eq!(interned.find_index_by_full_name("NonExistent"), None);
1445 }
1446
1447 #[test]
1448 fn test_package_index_get_package_at() {
1449 let pkg = create_test_package("TestMod", "TestOwner", "1.0.0");
1450 let packages = vec![pkg];
1451 let interned: PackageIndex = packages.into();
1452
1453 let package = interned.get_package_at(0).unwrap();
1454
1455 assert_eq!(package.name, Some("TestMod".to_string()));
1456
1457 assert_eq!(package.full_name, Some("TestOwner-TestMod".to_string()));
1458
1459 assert_eq!(package.owner, Some("TestOwner".to_string()));
1460
1461 assert_eq!(package.versions.len(), 1);
1462
1463 assert_eq!(
1464 package.versions[0].version_number,
1465 Some("1.0.0".to_string())
1466 );
1467 }
1468
1469 #[test]
1470 fn test_package_index_get_latest_version_at() {
1471 let mut pkg = create_test_package("TestMod", "TestOwner", "1.0.0");
1472
1473 let older_version = Version {
1474 name: Some("TestMod".to_string()),
1475 full_name: Some("TestOwner-TestMod".to_string()),
1476 description: Some("Older version".to_string()),
1477 icon: Some("icon.png".to_string()),
1478 version_number: Some("0.9.0".to_string()),
1479 dependencies: vec![],
1480 download_url: Some("https://example.com/TestMod/download-old".to_string()),
1481 downloads: Some(50),
1482 date_created: OffsetDateTime::now_utc().saturating_sub(time::Duration::days(30)),
1483 website_url: Some("https://example.com".to_string()),
1484 is_active: Some(true),
1485 uuid4: Some("old-version-uuid".to_string()),
1486 file_size: Some(512),
1487 };
1488 pkg.versions.insert(0, older_version);
1489
1490 let packages = vec![pkg];
1491 let interned: PackageIndex = packages.into();
1492
1493 let latest_idx = interned.get_latest_version_at(0);
1494
1495 assert!(latest_idx.is_some());
1496
1497 let version_idx = latest_idx.unwrap();
1498 let version_number = interned.versions.version_numbers[version_idx]
1499 .map(|key| interned.interner.resolve(&key).to_string());
1500
1501 assert_eq!(version_number, Some("1.0.0".to_string()));
1502 }
1503
1504 #[test]
1505 fn test_package_index_get_latest_version_at_empty_range() {
1506 let pkg = create_test_package("TestMod", "TestOwner", "1.0.0");
1507 let packages = vec![pkg];
1508 let mut interned: PackageIndex = packages.into();
1509
1510 interned.version_ranges[0] = (0, 0);
1511
1512 let latest = interned.get_latest_version_at(0);
1513
1514 assert!(latest.is_none());
1515 }
1516
1517 #[test]
1518 fn test_package_index_build_name_index() {
1519 let pkg1 = create_test_package("ModA", "Owner1", "1.0.0");
1520 let pkg2 = create_test_package("ModB", "Owner2", "2.0.0");
1521 let packages = vec![pkg1, pkg2];
1522 let interned: PackageIndex = packages.into();
1523
1524 let index = interned.build_name_index();
1525
1526 assert_eq!(index.len(), 2);
1527
1528 assert_eq!(index.get("Owner1-ModA"), Some(&0));
1529
1530 assert_eq!(index.get("Owner2-ModB"), Some(&1));
1531 }
1532
1533 #[test]
1534 fn test_package_index_validate_valid() {
1535 let pkg = create_test_package("TestMod", "TestOwner", "1.0.0");
1536 let packages = vec![pkg];
1537 let interned: PackageIndex = packages.into();
1538
1539 let result = interned.validate();
1540
1541 assert!(result.is_ok());
1542 }
1543
1544 #[test]
1545 fn validate_split_methods_are_individually_callable() {
1546 let interned: PackageIndex = vec![create_test_package("ModA", "Owner1", "1.0.0")].into();
1547
1548 assert!(interned.validate_lengths().is_ok());
1550 assert!(interned.validate_version_ranges().is_ok());
1551 assert!(interned.validate_version_ownership().is_ok());
1552 assert!(interned.validate_unique_full_names().is_ok());
1553
1554 let mut broken: PackageIndex = vec![create_test_package("ModA", "Owner1", "1.0.0")].into();
1556 broken.owners.pop();
1557 assert!(broken.validate_lengths().is_err());
1558
1559 let dup: PackageIndex = vec![
1561 create_test_package("ModA", "Owner1", "1.0.0"),
1562 create_test_package("ModA", "Owner1", "2.0.0"),
1563 ]
1564 .into();
1565 assert!(dup.validate_unique_full_names().is_err());
1566 }
1567
1568 #[test]
1569 fn encode_decode_dates_roundtrips_and_rejects_out_of_range() {
1570 let dates = vec![
1571 OffsetDateTime::from_unix_timestamp(1_700_000_000).unwrap(),
1572 OffsetDateTime::from_unix_timestamp(0).unwrap(),
1573 ];
1574
1575 let encoded = encode_dates(&dates);
1576 let decoded = decode_dates(&encoded).unwrap();
1577
1578 assert_eq!(decoded, dates);
1579
1580 let err = decode_dates(&[i128::MAX]).unwrap_err();
1583
1584 assert!(matches!(err, Error::Cache(_)));
1585 }
1586
1587 #[test]
1588 fn from_packages_populates_core_fields() {
1589 let interned: PackageIndex = vec![create_test_package("ModA", "Owner1", "1.0.0")].into();
1590
1591 assert_eq!(interned.len(), 1);
1592 assert_eq!(
1593 interned.resolve_full_name_at(0),
1594 Some("Owner1-ModA".to_string())
1595 );
1596
1597 assert_eq!(interned.version_ranges, vec![(0, 1)]);
1600 assert_eq!(interned.versions.package_indices, vec![0]);
1601 }
1602
1603 #[test]
1604 fn test_package_index_validate_mismatched_full_names_length() {
1605 let pkg = create_test_package("TestMod", "TestOwner", "1.0.0");
1606 let packages = vec![pkg];
1607 let mut interned: PackageIndex = packages.into();
1608
1609 interned.full_names.pop();
1610
1611 let result = interned.validate();
1612
1613 assert!(result.is_err());
1614
1615 assert!(
1616 result
1617 .unwrap_err()
1618 .to_string()
1619 .contains("full_names length")
1620 );
1621 }
1622
1623 #[test]
1624 fn test_package_index_validate_mismatched_owners_length() {
1625 let pkg = create_test_package("TestMod", "TestOwner", "1.0.0");
1626 let packages = vec![pkg];
1627 let mut interned: PackageIndex = packages.into();
1628
1629 interned.owners.pop();
1630
1631 let result = interned.validate();
1632
1633 assert!(result.is_err());
1634
1635 assert!(result.unwrap_err().to_string().contains("owners length"));
1636 }
1637
1638 #[test]
1639 fn test_package_index_validate_mismatched_package_urls_length() {
1640 let pkg = create_test_package("TestMod", "TestOwner", "1.0.0");
1641 let packages = vec![pkg];
1642 let mut interned: PackageIndex = packages.into();
1643
1644 interned.package_urls.pop();
1645
1646 let result = interned.validate();
1647
1648 assert!(result.is_err());
1649
1650 assert!(
1651 result
1652 .unwrap_err()
1653 .to_string()
1654 .contains("package_urls length")
1655 );
1656 }
1657
1658 #[test]
1659 fn test_package_index_validate_mismatched_dates_created_length() {
1660 let pkg = create_test_package("TestMod", "TestOwner", "1.0.0");
1661 let packages = vec![pkg];
1662 let mut interned: PackageIndex = packages.into();
1663
1664 interned.dates_created.pop();
1665
1666 let result = interned.validate();
1667
1668 assert!(result.is_err());
1669
1670 assert!(
1671 result
1672 .unwrap_err()
1673 .to_string()
1674 .contains("dates_created length")
1675 );
1676 }
1677
1678 #[test]
1679 fn test_package_index_validate_mismatched_dates_updated_length() {
1680 let pkg = create_test_package("TestMod", "TestOwner", "1.0.0");
1681 let packages = vec![pkg];
1682 let mut interned: PackageIndex = packages.into();
1683
1684 interned.dates_updated.pop();
1685
1686 let result = interned.validate();
1687
1688 assert!(result.is_err());
1689
1690 assert!(
1691 result
1692 .unwrap_err()
1693 .to_string()
1694 .contains("dates_updated length")
1695 );
1696 }
1697
1698 #[test]
1699 fn test_package_index_validate_mismatched_uuid4s_length() {
1700 let pkg = create_test_package("TestMod", "TestOwner", "1.0.0");
1701 let packages = vec![pkg];
1702 let mut interned: PackageIndex = packages.into();
1703
1704 interned.uuid4s.pop();
1705
1706 let result = interned.validate();
1707
1708 assert!(result.is_err());
1709
1710 assert!(result.unwrap_err().to_string().contains("uuid4s length"));
1711 }
1712
1713 #[test]
1714 fn test_package_index_validate_mismatched_rating_scores_length() {
1715 let pkg = create_test_package("TestMod", "TestOwner", "1.0.0");
1716 let packages = vec![pkg];
1717 let mut interned: PackageIndex = packages.into();
1718
1719 interned.rating_scores.pop();
1720
1721 let result = interned.validate();
1722
1723 assert!(result.is_err());
1724
1725 assert!(
1726 result
1727 .unwrap_err()
1728 .to_string()
1729 .contains("rating_scores length")
1730 );
1731 }
1732
1733 #[test]
1734 fn test_package_index_validate_mismatched_is_pinned_length() {
1735 let pkg = create_test_package("TestMod", "TestOwner", "1.0.0");
1736 let packages = vec![pkg];
1737 let mut interned: PackageIndex = packages.into();
1738
1739 interned.is_pinned.pop();
1740
1741 let result = interned.validate();
1742
1743 assert!(result.is_err());
1744
1745 assert!(result.unwrap_err().to_string().contains("is_pinned length"));
1746 }
1747
1748 #[test]
1749 fn test_package_index_validate_mismatched_is_deprecated_length() {
1750 let pkg = create_test_package("TestMod", "TestOwner", "1.0.0");
1751 let packages = vec![pkg];
1752 let mut interned: PackageIndex = packages.into();
1753
1754 interned.is_deprecated.pop();
1755
1756 let result = interned.validate();
1757
1758 assert!(result.is_err());
1759
1760 assert!(
1761 result
1762 .unwrap_err()
1763 .to_string()
1764 .contains("is_deprecated length")
1765 );
1766 }
1767
1768 #[test]
1769 fn test_package_index_validate_mismatched_has_nsfw_content_length() {
1770 let pkg = create_test_package("TestMod", "TestOwner", "1.0.0");
1771 let packages = vec![pkg];
1772 let mut interned: PackageIndex = packages.into();
1773
1774 interned.has_nsfw_content.pop();
1775
1776 let result = interned.validate();
1777
1778 assert!(result.is_err());
1779
1780 assert!(
1781 result
1782 .unwrap_err()
1783 .to_string()
1784 .contains("has_nsfw_content length")
1785 );
1786 }
1787
1788 #[test]
1789 fn test_package_index_validate_mismatched_categories_length() {
1790 let pkg = create_test_package("TestMod", "TestOwner", "1.0.0");
1791 let packages = vec![pkg];
1792 let mut interned: PackageIndex = packages.into();
1793
1794 interned.categories.pop();
1795
1796 let result = interned.validate();
1797
1798 assert!(result.is_err());
1799
1800 assert!(
1801 result
1802 .unwrap_err()
1803 .to_string()
1804 .contains("categories length")
1805 );
1806 }
1807
1808 #[test]
1809 fn test_package_index_validate_mismatched_version_ranges_length() {
1810 let pkg = create_test_package("TestMod", "TestOwner", "1.0.0");
1811 let packages = vec![pkg];
1812 let mut interned: PackageIndex = packages.into();
1813
1814 interned.version_ranges.pop();
1815
1816 let result = interned.validate();
1817
1818 assert!(result.is_err());
1819
1820 assert!(
1821 result
1822 .unwrap_err()
1823 .to_string()
1824 .contains("version_ranges length")
1825 );
1826 }
1827
1828 #[test]
1829 fn test_package_index_validate_mismatched_version_numbers_length() {
1830 let pkg = create_test_package("TestMod", "TestOwner", "1.0.0");
1831 let packages = vec![pkg];
1832 let mut interned: PackageIndex = packages.into();
1833
1834 interned.versions.version_numbers.pop();
1835
1836 let result = interned.validate();
1837
1838 assert!(result.is_err());
1839
1840 assert!(
1841 result
1842 .unwrap_err()
1843 .to_string()
1844 .contains("version_numbers length")
1845 );
1846 }
1847
1848 #[test]
1849 fn test_package_index_validate_mismatched_version_download_urls_length() {
1850 let pkg = create_test_package("TestMod", "TestOwner", "1.0.0");
1851 let packages = vec![pkg];
1852 let mut interned: PackageIndex = packages.into();
1853
1854 interned.versions.download_urls.pop();
1855
1856 let result = interned.validate();
1857
1858 assert!(result.is_err());
1859
1860 assert!(
1861 result
1862 .unwrap_err()
1863 .to_string()
1864 .contains("download_urls length")
1865 );
1866 }
1867
1868 #[test]
1869 fn test_package_index_validate_mismatched_version_dependencies_length() {
1870 let pkg = create_test_package("TestMod", "TestOwner", "1.0.0");
1871 let packages = vec![pkg];
1872 let mut interned: PackageIndex = packages.into();
1873
1874 interned.versions.dependencies.pop();
1875
1876 let result = interned.validate();
1877
1878 assert!(result.is_err());
1879
1880 assert!(
1881 result
1882 .unwrap_err()
1883 .to_string()
1884 .contains("dependencies length")
1885 );
1886 }
1887
1888 #[test]
1889 fn test_package_index_validate_mismatched_version_dates_created_length() {
1890 let pkg = create_test_package("TestMod", "TestOwner", "1.0.0");
1891 let packages = vec![pkg];
1892 let mut interned: PackageIndex = packages.into();
1893
1894 interned.versions.dates_created.pop();
1895
1896 let result = interned.validate();
1897
1898 assert!(result.is_err());
1899
1900 assert!(
1901 result
1902 .unwrap_err()
1903 .to_string()
1904 .contains("dates_created length")
1905 );
1906 }
1907
1908 #[test]
1909 fn test_package_index_validate_mismatched_version_descriptions_length() {
1910 let pkg = create_test_package("TestMod", "TestOwner", "1.0.0");
1911 let packages = vec![pkg];
1912 let mut interned: PackageIndex = packages.into();
1913
1914 interned.versions.descriptions.pop();
1915
1916 let result = interned.validate();
1917
1918 assert!(result.is_err());
1919
1920 assert!(
1921 result
1922 .unwrap_err()
1923 .to_string()
1924 .contains("descriptions length")
1925 );
1926 }
1927
1928 #[test]
1929 fn test_package_index_validate_mismatched_version_icons_length() {
1930 let pkg = create_test_package("TestMod", "TestOwner", "1.0.0");
1931 let packages = vec![pkg];
1932 let mut interned: PackageIndex = packages.into();
1933
1934 interned.versions.icons.pop();
1935
1936 let result = interned.validate();
1937
1938 assert!(result.is_err());
1939
1940 assert!(result.unwrap_err().to_string().contains("icons length"));
1941 }
1942
1943 #[test]
1944 fn test_package_index_validate_mismatched_version_downloads_length() {
1945 let pkg = create_test_package("TestMod", "TestOwner", "1.0.0");
1946 let packages = vec![pkg];
1947 let mut interned: PackageIndex = packages.into();
1948
1949 interned.versions.downloads.pop();
1950
1951 let result = interned.validate();
1952
1953 assert!(result.is_err());
1954
1955 assert!(result.unwrap_err().to_string().contains("downloads length"));
1956 }
1957
1958 #[test]
1959 fn test_package_index_validate_mismatched_version_website_urls_length() {
1960 let pkg = create_test_package("TestMod", "TestOwner", "1.0.0");
1961 let packages = vec![pkg];
1962 let mut interned: PackageIndex = packages.into();
1963
1964 interned.versions.website_urls.pop();
1965
1966 let result = interned.validate();
1967
1968 assert!(result.is_err());
1969
1970 assert!(
1971 result
1972 .unwrap_err()
1973 .to_string()
1974 .contains("website_urls length")
1975 );
1976 }
1977
1978 #[test]
1979 fn test_package_index_validate_mismatched_version_is_active_length() {
1980 let pkg = create_test_package("TestMod", "TestOwner", "1.0.0");
1981 let packages = vec![pkg];
1982 let mut interned: PackageIndex = packages.into();
1983
1984 interned.versions.is_active.pop();
1985
1986 let result = interned.validate();
1987
1988 assert!(result.is_err());
1989
1990 assert!(result.unwrap_err().to_string().contains("is_active length"));
1991 }
1992
1993 #[test]
1994 fn test_package_index_validate_mismatched_version_uuid4s_length() {
1995 let pkg = create_test_package("TestMod", "TestOwner", "1.0.0");
1996 let packages = vec![pkg];
1997 let mut interned: PackageIndex = packages.into();
1998
1999 interned.versions.uuid4s.pop();
2000
2001 let result = interned.validate();
2002
2003 assert!(result.is_err());
2004
2005 assert!(result.unwrap_err().to_string().contains("uuid4s length"));
2006 }
2007
2008 #[test]
2009 fn test_package_index_validate_mismatched_version_file_sizes_length() {
2010 let pkg = create_test_package("TestMod", "TestOwner", "1.0.0");
2011 let packages = vec![pkg];
2012 let mut interned: PackageIndex = packages.into();
2013
2014 interned.versions.file_sizes.pop();
2015
2016 let result = interned.validate();
2017
2018 assert!(result.is_err());
2019
2020 assert!(
2021 result
2022 .unwrap_err()
2023 .to_string()
2024 .contains("file_sizes length")
2025 );
2026 }
2027
2028 #[test]
2029 fn test_package_index_validate_invalid_version_range() {
2030 let pkg = create_test_package("TestMod", "TestOwner", "1.0.0");
2031 let packages = vec![pkg];
2032 let mut interned: PackageIndex = packages.into();
2033
2034 interned.version_ranges[0] = (5, 3);
2035
2036 let result = interned.validate();
2037
2038 assert!(result.is_err());
2039
2040 assert!(
2041 result
2042 .unwrap_err()
2043 .to_string()
2044 .contains("Invalid version range")
2045 );
2046 }
2047
2048 #[test]
2049 fn test_package_index_validate_version_range_out_of_bounds() {
2050 let pkg = create_test_package("TestMod", "TestOwner", "1.0.0");
2051 let packages = vec![pkg];
2052 let mut interned: PackageIndex = packages.into();
2053
2054 interned.version_ranges[0] = (0, 999);
2055
2056 let result = interned.validate();
2057
2058 assert!(result.is_err());
2059
2060 assert!(result.unwrap_err().to_string().contains("ends at"));
2061 }
2062
2063 #[test]
2064 fn test_package_index_serialization_round_trip() {
2065 let pkg = create_test_package("TestMod", "TestOwner", "1.0.0");
2066 let packages = vec![pkg.clone()];
2067 let interned: PackageIndex = packages.into();
2068
2069 let serializable: SerializedPackageIndex = (&interned).into();
2070
2071 let binary = bincode::encode_to_vec(&serializable, bincode::config::standard()).unwrap();
2072
2073 let (deserialized, _): (SerializedPackageIndex, usize) =
2074 bincode::decode_from_slice(&binary, bincode::config::standard()).unwrap();
2075
2076 let recovered: PackageIndex = deserialized.try_into().unwrap();
2077
2078 assert_eq!(recovered.len(), 1);
2079
2080 assert_eq!(recovered.resolve_name_at(0), Some("TestMod".to_string()));
2081
2082 assert_eq!(
2083 recovered.resolve_full_name_at(0),
2084 Some("TestOwner-TestMod".to_string())
2085 );
2086
2087 let recovered_pkg = recovered.get_package_at(0).unwrap();
2088
2089 assert_eq!(recovered_pkg.name, pkg.name);
2090
2091 assert_eq!(recovered_pkg.full_name, pkg.full_name);
2092
2093 assert_eq!(recovered_pkg.versions.len(), pkg.versions.len());
2094 }
2095
2096 #[test]
2097 fn test_version_name_and_full_name_are_version_level_not_package_level() {
2098 let mut pkg = create_test_package("CoolMod", "Author", "1.0.0");
2101 pkg.name = Some("CoolMod".to_string());
2102 pkg.full_name = Some("Author-CoolMod".to_string());
2103 pkg.versions[0].name = Some("Author-CoolMod".to_string());
2104 pkg.versions[0].full_name = Some("Author-CoolMod-1.0.0".to_string());
2105
2106 let interned: PackageIndex = vec![pkg].into();
2107
2108 let materialized = interned.get_package_at(0).unwrap();
2111
2112 assert_eq!(materialized.full_name, Some("Author-CoolMod".to_string()));
2113
2114 assert_eq!(
2115 materialized.versions[0].full_name,
2116 Some("Author-CoolMod-1.0.0".to_string())
2117 );
2118
2119 assert_eq!(
2120 materialized.versions[0].name,
2121 Some("Author-CoolMod".to_string())
2122 );
2123
2124 let serializable: SerializedPackageIndex = (&interned).into();
2126
2127 let binary = bincode::encode_to_vec(&serializable, bincode::config::standard()).unwrap();
2128
2129 let (deserialized, _): (SerializedPackageIndex, usize) =
2130 bincode::decode_from_slice(&binary, bincode::config::standard()).unwrap();
2131
2132 let recovered: PackageIndex = deserialized.try_into().unwrap();
2133 let recovered_pkg = recovered.get_package_at(0).unwrap();
2134
2135 assert_eq!(recovered_pkg.full_name, Some("Author-CoolMod".to_string()));
2136
2137 assert_eq!(
2138 recovered_pkg.versions[0].full_name,
2139 Some("Author-CoolMod-1.0.0".to_string())
2140 );
2141 }
2142
2143 #[test]
2144 fn test_resolve_report_lists_unresolved_packages() {
2145 let interned: PackageIndex = vec![create_test_package("ModA", "Owner1", "1.0.0")].into();
2146
2147 let graph = DependencyGraph::new(vec!["Owner1-ModA".to_string(), "Ghost-Missing".to_string()]);
2148
2149 let report = graph.resolve_report(&interned);
2150
2151 assert_eq!(report.urls.len(), 1);
2154 assert_eq!(report.unresolved, vec!["Ghost-Missing".to_string()]);
2155
2156 assert_eq!(graph.resolve(&interned).len(), 1);
2158 }
2159
2160 #[test]
2161 fn test_resolve_report_flags_package_with_no_downloadable_version() {
2162 let mut pkg = create_test_package("ModA", "Owner1", "1.0.0");
2163
2164 pkg.versions.clear();
2166
2167 let interned: PackageIndex = vec![pkg].into();
2168
2169 let graph = DependencyGraph::new(vec!["Owner1-ModA".to_string()]);
2170 let report = graph.resolve_report(&interned);
2171
2172 assert!(report.urls.is_empty());
2175 assert_eq!(report.unresolved, vec!["Owner1-ModA".to_string()]);
2176 }
2177
2178 #[test]
2179 fn test_duplicate_string_table_returns_error_not_panic() {
2180 let pkg = create_test_package("TestMod", "TestOwner", "1.0.0");
2181 let interned: PackageIndex = vec![pkg].into();
2182 let mut serializable: SerializedPackageIndex = (&interned).into();
2183
2184 let dup = serializable.string_table[0].clone();
2189
2190 serializable.string_table.push(dup);
2191
2192 let last_index = serializable.string_table.len() as u32 - 1;
2193
2194 serializable.names[0] = Some(last_index);
2195
2196 let result = PackageIndex::try_from(serializable);
2197
2198 assert!(result.is_err());
2199 }
2200
2201 #[test]
2202 fn test_corrupt_cache_index_returns_error_not_panic() {
2203 let pkg = create_test_package("TestMod", "TestOwner", "1.0.0");
2204 let interned: PackageIndex = vec![pkg].into();
2205 let mut serializable: SerializedPackageIndex = (&interned).into();
2206
2207 let bad_index = serializable.string_table.len() as u32 + 5;
2210 serializable.names[0] = Some(bad_index);
2211
2212 let result = PackageIndex::try_from(serializable);
2213
2214 assert!(result.is_err());
2215 }
2216
2217 #[test]
2218 fn test_package_index_dependency_graph_resolve() {
2219 let pkg1 = create_test_package("ModA", "Owner1", "1.0.0");
2220 let pkg2 = create_test_package_with_dependencies(
2221 "ModB",
2222 "Owner2",
2223 "2.0.0",
2224 vec!["Owner3-ModC".to_string()],
2225 );
2226 let pkg3 = create_test_package_with_dependencies(
2227 "ModC",
2228 "Owner3",
2229 "1.5.0",
2230 vec!["Owner4-ModD".to_string()],
2231 );
2232 let pkg4 = create_test_package("ModD", "Owner4", "0.9.0");
2233
2234 let packages = vec![pkg1, pkg2, pkg3, pkg4];
2235 let interned: PackageIndex = packages.into();
2236
2237 let dg1 = DependencyGraph::new(vec!["Owner1-ModA".to_string()]);
2238 let result1 = dg1.resolve(&interned);
2239
2240 assert_eq!(result1.len(), 1);
2241
2242 assert!(result1.contains_key("Owner1-ModA-1.0.0.zip"));
2243
2244 let dg2 = DependencyGraph::new(vec!["Owner2-ModB".to_string()]);
2245 let result2 = dg2.resolve(&interned);
2246
2247 assert_eq!(result2.len(), 3);
2248
2249 assert!(result2.contains_key("Owner2-ModB-2.0.0.zip"));
2250
2251 assert!(result2.contains_key("Owner3-ModC-1.5.0.zip"));
2252
2253 assert!(result2.contains_key("Owner4-ModD-0.9.0.zip"));
2254 }
2255
2256 #[test]
2257 fn test_package_index_dependency_graph_resolve_with_missing_dependency() {
2258 let pkg1 = create_test_package("ModA", "Owner1", "1.0.0");
2259 let pkg2 = create_test_package_with_dependencies(
2260 "ModB",
2261 "Owner2",
2262 "2.0.0",
2263 vec!["Owner3-NonExistent".to_string()],
2264 );
2265
2266 let packages = vec![pkg1, pkg2];
2267 let interned: PackageIndex = packages.into();
2268
2269 let dg = DependencyGraph::new(vec!["Owner2-ModB".to_string()]);
2270 let result = dg.resolve(&interned);
2271
2272 assert_eq!(result.len(), 1);
2273
2274 assert!(result.contains_key("Owner2-ModB-2.0.0.zip"));
2275 }
2276
2277 #[test]
2278 fn test_package_index_dependency_graph_resolve_with_version_suffix() {
2279 let pkg1 = create_test_package("ModA", "Owner1", "1.0.0");
2280
2281 let packages = vec![pkg1];
2282 let interned: PackageIndex = packages.into();
2283
2284 let dg = DependencyGraph::new(vec!["Owner1-ModA-1.0.0".to_string()]);
2285 let result = dg.resolve(&interned);
2286
2287 assert_eq!(result.len(), 1);
2288
2289 assert!(result.contains_key("Owner1-ModA-1.0.0.zip"));
2290 }
2291
2292 #[test]
2293 fn test_cache_rejects_unsupported_format_version() {
2294 let pkg = create_test_package("TestMod", "TestOwner", "1.0.0");
2295 let interned: PackageIndex = vec![pkg].into();
2296 let mut serializable: SerializedPackageIndex = (&interned).into();
2297
2298 serializable.format_version = CACHE_FORMAT_VERSION + 1;
2301
2302 let result = PackageIndex::try_from(serializable);
2303
2304 assert!(matches!(result, Err(Error::UnsupportedCacheVersion { .. })));
2305 }
2306
2307 #[test]
2308 fn test_cache_current_format_version_round_trips() {
2309 let pkg = create_test_package("TestMod", "TestOwner", "1.0.0");
2310 let interned: PackageIndex = vec![pkg].into();
2311 let serializable: SerializedPackageIndex = (&interned).into();
2312
2313 assert_eq!(serializable.format_version, CACHE_FORMAT_VERSION);
2314 assert!(PackageIndex::try_from(serializable).is_ok());
2315 }
2316
2317 #[test]
2318 fn test_validate_rejects_version_owned_by_wrong_package() {
2319 let pkg1 = create_test_package("ModA", "Owner1", "1.0.0");
2320 let pkg2 = create_test_package("ModB", "Owner2", "2.0.0");
2321 let mut interned: PackageIndex = vec![pkg1, pkg2].into();
2322
2323 interned.versions.package_indices[0] = 1;
2325
2326 let result = interned.validate();
2327
2328 assert!(result.is_err());
2329
2330 assert!(result.unwrap_err().to_string().contains("package_index"));
2331 }
2332
2333 #[test]
2334 fn test_validate_rejects_overlapping_version_ranges() {
2335 let pkg1 = create_test_package("ModA", "Owner1", "1.0.0");
2336 let pkg2 = create_test_package("ModB", "Owner2", "2.0.0");
2337 let mut interned: PackageIndex = vec![pkg1, pkg2].into();
2338
2339 interned.version_ranges[1] = (0, 1);
2341
2342 let result = interned.validate();
2343
2344 assert!(result.is_err());
2345
2346 assert!(result.unwrap_err().to_string().contains("overlapping"));
2347 }
2348
2349 #[test]
2350 fn test_package_index_validate_rejects_duplicate_full_names() {
2351 let pkg1 = create_test_package("TestMod", "TestOwner", "1.0.0");
2356 let pkg2 = create_test_package("TestMod", "TestOwner", "2.0.0");
2357 let interned: PackageIndex = vec![pkg1, pkg2].into();
2358
2359 let result = interned.validate();
2360
2361 assert!(result.is_err());
2362
2363 assert!(
2364 result
2365 .unwrap_err()
2366 .to_string()
2367 .contains("duplicate full name")
2368 );
2369 }
2370
2371 #[test]
2372 fn test_package_index_dependency_graph_resolve_handles_cycles() {
2373 let pkg_a = create_test_package_with_dependencies(
2376 "ModA",
2377 "Owner1",
2378 "1.0.0",
2379 vec!["Owner2-ModB".to_string()],
2380 );
2381 let pkg_b = create_test_package_with_dependencies(
2382 "ModB",
2383 "Owner2",
2384 "2.0.0",
2385 vec!["Owner1-ModA".to_string()],
2386 );
2387 let pkg_c = create_test_package_with_dependencies(
2388 "ModC",
2389 "Owner3",
2390 "3.0.0",
2391 vec!["Owner3-ModC".to_string()],
2392 );
2393
2394 let packages = vec![pkg_a, pkg_b, pkg_c];
2395 let interned: PackageIndex = packages.into();
2396
2397 let dg = DependencyGraph::new(vec!["Owner1-ModA".to_string(), "Owner3-ModC".to_string()]);
2398 let result = dg.resolve(&interned);
2399
2400 assert_eq!(result.len(), 3);
2401
2402 assert!(result.contains_key("Owner1-ModA-1.0.0.zip"));
2403
2404 assert!(result.contains_key("Owner2-ModB-2.0.0.zip"));
2405
2406 assert!(result.contains_key("Owner3-ModC-3.0.0.zip"));
2407 }
2408}