1use serde::de::{DeserializeSeed, IgnoredAny, MapAccess, SeqAccess, Visitor};
2use serde::{Deserialize, Deserializer, Serialize};
3use std::collections::BTreeMap;
4use std::fmt;
5
6#[cfg(not(feature = "rustls"))]
11compile_error!("aube-registry requires the `rustls` feature");
12
13macro_rules! visit_primitives_to {
23 ($de:lifetime, $default:expr) => {
24 fn visit_unit<E: serde::de::Error>(self) -> Result<Self::Value, E> {
25 Ok($default)
26 }
27 fn visit_none<E: serde::de::Error>(self) -> Result<Self::Value, E> {
28 Ok($default)
29 }
30 fn visit_some<D2: Deserializer<$de>>(self, d: D2) -> Result<Self::Value, D2::Error> {
31 d.deserialize_any(self)
32 }
33 fn visit_bool<E: serde::de::Error>(self, _: bool) -> Result<Self::Value, E> {
34 Ok($default)
35 }
36 fn visit_i64<E: serde::de::Error>(self, _: i64) -> Result<Self::Value, E> {
37 Ok($default)
38 }
39 fn visit_u64<E: serde::de::Error>(self, _: u64) -> Result<Self::Value, E> {
40 Ok($default)
41 }
42 fn visit_f64<E: serde::de::Error>(self, _: f64) -> Result<Self::Value, E> {
43 Ok($default)
44 }
45 };
46}
47
48macro_rules! visit_seq_to {
52 ($de:lifetime, $default:expr) => {
53 fn visit_seq<A: SeqAccess<$de>>(self, mut access: A) -> Result<Self::Value, A::Error> {
54 while access.next_element::<IgnoredAny>()?.is_some() {}
55 Ok($default)
56 }
57 };
58}
59
60macro_rules! visit_map_to {
62 ($de:lifetime, $default:expr) => {
63 fn visit_map<A: MapAccess<$de>>(self, mut access: A) -> Result<Self::Value, A::Error> {
64 while access.next_entry::<IgnoredAny, IgnoredAny>()?.is_some() {}
65 Ok($default)
66 }
67 };
68}
69
70macro_rules! visit_strings_to {
74 ($de:lifetime, $default:expr) => {
75 fn visit_str<E: serde::de::Error>(self, _: &str) -> Result<Self::Value, E> {
76 Ok($default)
77 }
78 fn visit_string<E: serde::de::Error>(self, _: String) -> Result<Self::Value, E> {
79 Ok($default)
80 }
81 };
82}
83
84fn non_string_tolerant_map<'de, D>(de: D) -> Result<BTreeMap<String, String>, D::Error>
115where
116 D: Deserializer<'de>,
117{
118 struct V;
119
120 impl<'de> Visitor<'de> for V {
121 type Value = BTreeMap<String, String>;
122
123 fn expecting(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
124 f.write_str("null or an object mapping strings to strings")
125 }
126
127 fn visit_unit<E: serde::de::Error>(self) -> Result<Self::Value, E> {
128 Ok(BTreeMap::new())
129 }
130
131 fn visit_none<E: serde::de::Error>(self) -> Result<Self::Value, E> {
132 Ok(BTreeMap::new())
133 }
134
135 fn visit_some<D2: Deserializer<'de>>(self, d: D2) -> Result<Self::Value, D2::Error> {
136 d.deserialize_any(self)
137 }
138
139 fn visit_map<A: MapAccess<'de>>(self, mut access: A) -> Result<Self::Value, A::Error> {
140 let mut out = BTreeMap::new();
141 while let Some(key) = access.next_key::<String>()? {
142 let MaybeString(maybe) = access.next_value()?;
143 if let Some(s) = maybe {
144 out.insert(key, s);
145 }
146 }
147 Ok(out)
148 }
149 }
150
151 de.deserialize_any(V)
152}
153
154pub mod client;
155pub mod config;
156pub mod jsr;
157pub mod osv_bloom_client;
158pub mod osv_mirror;
159pub mod slow_metadata;
160pub mod supply_chain;
161
162pub use aube_manifest::BundledDependencies;
167
168#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
172pub enum NetworkMode {
173 #[default]
176 Online,
177 PreferOffline,
181 Offline,
184}
185
186#[derive(Debug, Clone, Deserialize, Serialize)]
189pub struct Packument {
190 pub name: String,
191 #[serde(default)]
192 pub modified: Option<String>,
193 #[serde(default)]
194 pub versions: BTreeMap<String, VersionMetadata>,
195 #[serde(
196 rename = "dist-tags",
197 default,
198 deserialize_with = "non_string_tolerant_map"
199 )]
200 pub dist_tags: BTreeMap<String, String>,
201 #[serde(default, deserialize_with = "non_string_tolerant_map")]
210 pub time: BTreeMap<String, String>,
211}
212
213#[derive(Debug, Clone, Deserialize)]
218pub struct PackumentTrustHistory {
219 #[serde(default, deserialize_with = "non_string_tolerant_map")]
220 pub time: BTreeMap<String, String>,
221 #[serde(default)]
222 pub versions: BTreeMap<String, VersionTrustMetadata>,
223}
224
225#[derive(Debug)]
230pub struct ExactVersionPackument {
231 pub metadata: VersionMetadata,
232 pub history: PackumentTrustHistory,
233}
234
235pub(crate) struct ExactVersionPackumentSeed<'a> {
236 pub version: &'a str,
237}
238
239impl<'de> DeserializeSeed<'de> for ExactVersionPackumentSeed<'_> {
240 type Value = ExactVersionPackument;
241
242 fn deserialize<D>(self, deserializer: D) -> Result<Self::Value, D::Error>
243 where
244 D: Deserializer<'de>,
245 {
246 deserializer.deserialize_map(ExactVersionPackumentVisitor {
247 version: self.version,
248 })
249 }
250}
251
252struct ExactVersionPackumentVisitor<'a> {
253 version: &'a str,
254}
255
256impl<'de> Visitor<'de> for ExactVersionPackumentVisitor<'_> {
257 type Value = ExactVersionPackument;
258
259 fn expecting(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
260 formatter.write_str("an npm packument containing the requested exact version")
261 }
262
263 fn visit_map<A>(self, mut access: A) -> Result<Self::Value, A::Error>
264 where
265 A: MapAccess<'de>,
266 {
267 let mut metadata = None;
268 let mut history = BTreeMap::new();
269 let mut time = BTreeMap::new();
270
271 while let Some(field) = access.next_key::<String>()? {
272 match field.as_str() {
273 "versions" => {
274 let decoded = access.next_value_seed(ExactVersionMapSeed {
275 version: self.version,
276 })?;
277 metadata = decoded.0;
278 history = decoded.1;
279 }
280 "time" => {
281 time = access.next_value::<TolerantStringMap>()?.0;
282 }
283 _ => {
284 access.next_value::<IgnoredAny>()?;
285 }
286 }
287 }
288
289 let metadata = metadata.ok_or_else(|| {
290 serde::de::Error::custom(format!(
291 "packument does not contain requested version {}",
292 self.version
293 ))
294 })?;
295 Ok(ExactVersionPackument {
296 metadata,
297 history: PackumentTrustHistory {
298 time,
299 versions: history,
300 },
301 })
302 }
303}
304
305struct ExactVersionMapSeed<'a> {
306 version: &'a str,
307}
308
309impl<'de> DeserializeSeed<'de> for ExactVersionMapSeed<'_> {
310 type Value = (
311 Option<VersionMetadata>,
312 BTreeMap<String, VersionTrustMetadata>,
313 );
314
315 fn deserialize<D>(self, deserializer: D) -> Result<Self::Value, D::Error>
316 where
317 D: Deserializer<'de>,
318 {
319 deserializer.deserialize_map(ExactVersionMapVisitor {
320 version: self.version,
321 })
322 }
323}
324
325struct ExactVersionMapVisitor<'a> {
326 version: &'a str,
327}
328
329impl<'de> Visitor<'de> for ExactVersionMapVisitor<'_> {
330 type Value = (
331 Option<VersionMetadata>,
332 BTreeMap<String, VersionTrustMetadata>,
333 );
334
335 fn expecting(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
336 formatter.write_str("an npm packument versions map")
337 }
338
339 fn visit_map<A>(self, mut access: A) -> Result<Self::Value, A::Error>
340 where
341 A: MapAccess<'de>,
342 {
343 let mut metadata = None;
344 let mut history = BTreeMap::new();
345 while let Some(version) = access.next_key::<String>()? {
346 if version == self.version {
347 metadata = Some(access.next_value::<VersionMetadata>()?);
348 } else {
349 history.insert(version, access.next_value::<VersionTrustMetadata>()?);
350 }
351 }
352 Ok((metadata, history))
353 }
354}
355
356#[derive(Deserialize)]
357#[serde(transparent)]
358struct TolerantStringMap(
359 #[serde(deserialize_with = "non_string_tolerant_map")] BTreeMap<String, String>,
360);
361
362#[derive(Debug, Clone, Deserialize)]
363#[serde(rename_all = "camelCase")]
364pub struct VersionTrustMetadata {
365 #[serde(default)]
366 pub approver: Option<serde_json::Value>,
367 #[serde(default, rename = "_npmUser", deserialize_with = "npm_user_tolerant")]
368 pub npm_user: Option<NpmUser>,
369 #[serde(default)]
370 pub dist: Option<VersionTrustDist>,
371}
372
373#[derive(Debug, Clone, Deserialize)]
374pub struct VersionTrustDist {
375 #[serde(default)]
376 pub attestations: Option<Attestations>,
377}
378
379#[derive(Debug, Clone, Deserialize, Serialize)]
388#[serde(rename_all = "camelCase", from = "VersionMetadataRaw")]
389pub struct VersionMetadata {
390 pub name: String,
391 pub version: String,
392 #[serde(default, deserialize_with = "non_string_tolerant_map")]
393 pub dependencies: BTreeMap<String, String>,
394 #[serde(default, deserialize_with = "non_string_tolerant_map")]
395 pub dev_dependencies: BTreeMap<String, String>,
396 #[serde(default, deserialize_with = "non_string_tolerant_map")]
397 pub peer_dependencies: BTreeMap<String, String>,
398 #[serde(default)]
399 pub peer_dependencies_meta: BTreeMap<String, PeerDepMeta>,
400 #[serde(default, deserialize_with = "non_string_tolerant_map")]
401 pub optional_dependencies: BTreeMap<String, String>,
402 pub bundled_dependencies: Option<BundledDependencies>,
410 pub dist: Option<Dist>,
411 #[serde(default, deserialize_with = "aube_util::string_or_seq")]
412 pub os: Vec<String>,
413 #[serde(default, deserialize_with = "aube_util::string_or_seq")]
414 pub cpu: Vec<String>,
415 #[serde(default, deserialize_with = "aube_util::string_or_seq")]
416 pub libc: Vec<String>,
417 #[serde(default, deserialize_with = "aube_manifest::engines_tolerant")]
430 pub engines: BTreeMap<String, String>,
431 #[serde(default, deserialize_with = "license_string")]
438 pub license: Option<String>,
439 #[serde(default, rename = "funding", deserialize_with = "funding_url")]
447 pub funding_url: Option<String>,
448 #[serde(default, rename = "bin", deserialize_with = "bin_map")]
461 pub bin: BTreeMap<String, String>,
462 #[serde(default)]
463 pub has_install_script: bool,
464 #[serde(default, deserialize_with = "deprecated_string")]
466 pub deprecated: Option<String>,
467 #[serde(default)]
471 pub approver: Option<serde_json::Value>,
472 #[serde(default, rename = "_npmUser", deserialize_with = "npm_user_tolerant")]
479 pub npm_user: Option<NpmUser>,
480}
481
482#[derive(Debug, Deserialize)]
499#[serde(rename_all = "camelCase")]
500struct VersionMetadataRaw {
501 name: String,
502 version: String,
503 #[serde(default, deserialize_with = "non_string_tolerant_map")]
504 dependencies: BTreeMap<String, String>,
505 #[serde(default, deserialize_with = "non_string_tolerant_map")]
506 dev_dependencies: BTreeMap<String, String>,
507 #[serde(default, deserialize_with = "non_string_tolerant_map")]
508 peer_dependencies: BTreeMap<String, String>,
509 #[serde(default)]
510 peer_dependencies_meta: BTreeMap<String, PeerDepMeta>,
511 #[serde(default, deserialize_with = "non_string_tolerant_map")]
512 optional_dependencies: BTreeMap<String, String>,
513 #[serde(default, rename = "bundledDependencies")]
514 bundled_dependencies: Option<BundledDependencies>,
515 #[serde(default, rename = "bundleDependencies")]
516 bundle_dependencies_alias: Option<BundledDependencies>,
517 dist: Option<Dist>,
518 #[serde(default, deserialize_with = "aube_util::string_or_seq")]
519 os: Vec<String>,
520 #[serde(default, deserialize_with = "aube_util::string_or_seq")]
521 cpu: Vec<String>,
522 #[serde(default, deserialize_with = "aube_util::string_or_seq")]
523 libc: Vec<String>,
524 #[serde(default, deserialize_with = "aube_manifest::engines_tolerant")]
525 engines: BTreeMap<String, String>,
526 #[serde(default, deserialize_with = "license_string")]
527 license: Option<String>,
528 #[serde(default, rename = "funding", deserialize_with = "funding_url")]
529 funding_url: Option<String>,
530 #[serde(default, rename = "bin", deserialize_with = "bin_map")]
531 bin: BTreeMap<String, String>,
532 #[serde(default)]
533 has_install_script: bool,
534 #[serde(default, deserialize_with = "deprecated_string")]
535 deprecated: Option<String>,
536 #[serde(default)]
537 approver: Option<serde_json::Value>,
538 #[serde(default, rename = "_npmUser", deserialize_with = "npm_user_tolerant")]
539 npm_user: Option<NpmUser>,
540}
541
542impl From<VersionMetadataRaw> for VersionMetadata {
543 fn from(raw: VersionMetadataRaw) -> Self {
544 Self {
545 name: raw.name,
546 version: raw.version,
547 dependencies: raw.dependencies,
548 dev_dependencies: raw.dev_dependencies,
549 peer_dependencies: raw.peer_dependencies,
550 peer_dependencies_meta: raw.peer_dependencies_meta,
551 optional_dependencies: raw.optional_dependencies,
552 bundled_dependencies: raw.bundled_dependencies.or(raw.bundle_dependencies_alias),
553 dist: raw.dist,
554 os: raw.os,
555 cpu: raw.cpu,
556 libc: raw.libc,
557 engines: raw.engines,
558 license: raw.license,
559 funding_url: raw.funding_url,
560 bin: raw.bin,
561 has_install_script: raw.has_install_script,
562 deprecated: raw.deprecated,
563 approver: raw.approver,
564 npm_user: raw.npm_user,
565 }
566 }
567}
568
569#[derive(Debug, Clone, Deserialize, Serialize, PartialEq, Eq)]
570pub struct PeerDepMeta {
571 #[serde(default)]
572 pub optional: bool,
573}
574
575#[derive(Debug, Clone, Deserialize, Serialize, Default)]
576pub struct NpmUser {
577 #[serde(default, rename = "trustedPublisher")]
581 pub trusted_publisher: Option<serde_json::Value>,
582}
583
584#[derive(Debug, Clone, Deserialize, Serialize)]
585pub struct Dist {
586 pub tarball: String,
587 pub integrity: Option<String>,
588 pub shasum: Option<String>,
589 #[serde(default, rename = "unpackedSize")]
594 pub unpacked_size: Option<u64>,
595 #[serde(default)]
601 pub attestations: Option<Attestations>,
602}
603
604#[derive(Debug, Clone, Deserialize, Serialize, Default)]
605pub struct Attestations {
606 #[serde(default)]
607 pub provenance: Option<serde_json::Value>,
608}
609
610fn deprecated_string<'de, D>(de: D) -> Result<Option<String>, D::Error>
611where
612 D: Deserializer<'de>,
613{
614 let MaybeString(maybe) = MaybeString::deserialize(de)?;
615 Ok(maybe.filter(|s| !s.is_empty()))
616}
617
618fn license_string<'de, D>(de: D) -> Result<Option<String>, D::Error>
624where
625 D: Deserializer<'de>,
626{
627 struct V;
628 impl<'de> Visitor<'de> for V {
629 type Value = Option<String>;
630
631 fn expecting(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
632 f.write_str("a license string, a {type, url} object, or null")
633 }
634
635 fn visit_str<E: serde::de::Error>(self, s: &str) -> Result<Self::Value, E> {
636 Ok((!s.is_empty()).then(|| s.to_owned()))
637 }
638
639 fn visit_string<E: serde::de::Error>(self, s: String) -> Result<Self::Value, E> {
640 Ok((!s.is_empty()).then_some(s))
641 }
642
643 fn visit_map<A: MapAccess<'de>>(self, mut access: A) -> Result<Self::Value, A::Error> {
644 let mut found: Option<String> = None;
645 while let Some(key) = access.next_key::<String>()? {
646 if key == "type" && found.is_none() {
647 let MaybeString(maybe) = access.next_value()?;
648 found = maybe.filter(|s| !s.is_empty());
649 } else {
650 let _: IgnoredAny = access.next_value()?;
651 }
652 }
653 Ok(found)
654 }
655
656 visit_primitives_to!('de, None);
657 visit_seq_to!('de, None);
658 }
659 de.deserialize_any(V)
660}
661
662fn funding_url<'de, D>(de: D) -> Result<Option<String>, D::Error>
668where
669 D: Deserializer<'de>,
670{
671 struct V;
672 impl<'de> Visitor<'de> for V {
673 type Value = Option<String>;
674
675 fn expecting(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
676 f.write_str("a funding URL string, a {url} object, or an array of either")
677 }
678
679 fn visit_str<E: serde::de::Error>(self, s: &str) -> Result<Self::Value, E> {
680 Ok((!s.is_empty()).then(|| s.to_owned()))
681 }
682
683 fn visit_string<E: serde::de::Error>(self, s: String) -> Result<Self::Value, E> {
684 Ok((!s.is_empty()).then_some(s))
685 }
686
687 fn visit_map<A: MapAccess<'de>>(self, access: A) -> Result<Self::Value, A::Error> {
688 extract_url_from_map(access)
689 }
690
691 fn visit_seq<A: SeqAccess<'de>>(self, mut access: A) -> Result<Self::Value, A::Error> {
692 while let Some(FundingArrayEntry(maybe)) = access.next_element()? {
697 if let Some(s) = maybe {
698 while access.next_element::<IgnoredAny>()?.is_some() {}
699 return Ok(Some(s));
700 }
701 }
702 Ok(None)
703 }
704
705 visit_primitives_to!('de, None);
706 }
707 de.deserialize_any(V)
708}
709
710fn extract_url_from_map<'de, A: MapAccess<'de>>(mut access: A) -> Result<Option<String>, A::Error> {
713 let mut found: Option<String> = None;
714 while let Some(key) = access.next_key::<String>()? {
715 if key == "url" && found.is_none() {
716 let MaybeString(maybe) = access.next_value()?;
717 found = maybe.filter(|s| !s.is_empty());
718 } else {
719 let _: IgnoredAny = access.next_value()?;
720 }
721 }
722 Ok(found)
723}
724
725fn npm_user_tolerant<'de, D>(de: D) -> Result<Option<NpmUser>, D::Error>
732where
733 D: Deserializer<'de>,
734{
735 struct V;
736 impl<'de> Visitor<'de> for V {
737 type Value = Option<NpmUser>;
738
739 fn expecting(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
740 f.write_str("an _npmUser object or any other JSON value")
741 }
742
743 fn visit_map<A: MapAccess<'de>>(self, mut access: A) -> Result<Self::Value, A::Error> {
752 let mut trusted_publisher: Option<serde_json::Value> = None;
753 while let Some(key) = access.next_key::<String>()? {
754 if key == "trustedPublisher" && trusted_publisher.is_none() {
755 trusted_publisher = access.next_value::<Option<serde_json::Value>>()?;
756 } else {
757 let _: IgnoredAny = access.next_value()?;
758 }
759 }
760 Ok(Some(NpmUser { trusted_publisher }))
761 }
762
763 visit_primitives_to!('de, None);
764 visit_seq_to!('de, None);
765 visit_strings_to!('de, None);
766 }
767 de.deserialize_any(V)
768}
769
770fn bin_map<'de, D>(de: D) -> Result<BTreeMap<String, String>, D::Error>
784where
785 D: Deserializer<'de>,
786{
787 struct V;
788 impl<'de> Visitor<'de> for V {
789 type Value = BTreeMap<String, String>;
790
791 fn expecting(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
792 f.write_str("a bin string, a bin map, or null")
793 }
794
795 fn visit_str<E: serde::de::Error>(self, s: &str) -> Result<Self::Value, E> {
796 if s.is_empty() {
797 return Ok(BTreeMap::new());
798 }
799 let mut m = BTreeMap::new();
800 m.insert(String::new(), s.to_owned());
801 Ok(m)
802 }
803
804 fn visit_string<E: serde::de::Error>(self, s: String) -> Result<Self::Value, E> {
805 if s.is_empty() {
806 return Ok(BTreeMap::new());
807 }
808 let mut m = BTreeMap::new();
809 m.insert(String::new(), s);
810 Ok(m)
811 }
812
813 fn visit_map<A: MapAccess<'de>>(self, mut access: A) -> Result<Self::Value, A::Error> {
814 let mut out = BTreeMap::new();
815 while let Some(key) = access.next_key::<String>()? {
816 let MaybeString(maybe) = access.next_value()?;
817 if let Some(s) = maybe {
818 out.insert(key, s);
819 }
820 }
821 Ok(out)
822 }
823
824 visit_primitives_to!('de, BTreeMap::new());
825 visit_seq_to!('de, BTreeMap::new());
826 }
827 de.deserialize_any(V)
828}
829
830struct MaybeString(Option<String>);
835
836impl<'de> Deserialize<'de> for MaybeString {
837 fn deserialize<D: Deserializer<'de>>(de: D) -> Result<Self, D::Error> {
838 struct V;
839 impl<'de> Visitor<'de> for V {
840 type Value = MaybeString;
841
842 fn expecting(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
843 f.write_str("any JSON value")
844 }
845
846 fn visit_str<E: serde::de::Error>(self, s: &str) -> Result<MaybeString, E> {
847 Ok(MaybeString(Some(s.to_owned())))
848 }
849
850 fn visit_string<E: serde::de::Error>(self, s: String) -> Result<MaybeString, E> {
851 Ok(MaybeString(Some(s)))
852 }
853
854 visit_primitives_to!('de, MaybeString(None));
855 visit_seq_to!('de, MaybeString(None));
856 visit_map_to!('de, MaybeString(None));
857 }
858 de.deserialize_any(V)
859 }
860}
861
862struct FundingArrayEntry(Option<String>);
866
867impl<'de> Deserialize<'de> for FundingArrayEntry {
868 fn deserialize<D: Deserializer<'de>>(de: D) -> Result<Self, D::Error> {
869 struct V;
870 impl<'de> Visitor<'de> for V {
871 type Value = FundingArrayEntry;
872
873 fn expecting(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
874 f.write_str("a funding URL string or a {url} object")
875 }
876
877 fn visit_str<E: serde::de::Error>(self, s: &str) -> Result<FundingArrayEntry, E> {
878 Ok(FundingArrayEntry((!s.is_empty()).then(|| s.to_owned())))
879 }
880
881 fn visit_string<E: serde::de::Error>(self, s: String) -> Result<FundingArrayEntry, E> {
882 Ok(FundingArrayEntry((!s.is_empty()).then_some(s)))
883 }
884
885 fn visit_map<A: MapAccess<'de>>(
886 self,
887 access: A,
888 ) -> Result<FundingArrayEntry, A::Error> {
889 Ok(FundingArrayEntry(extract_url_from_map(access)?))
890 }
891
892 visit_primitives_to!('de, FundingArrayEntry(None));
893 visit_seq_to!('de, FundingArrayEntry(None));
894 }
895 de.deserialize_any(V)
896 }
897}
898
899#[derive(Debug, thiserror::Error, miette::Diagnostic)]
900pub enum Error {
901 #[error("HTTP error: {0}")]
902 Http(#[from] reqwest::Error),
903 #[error("package not found: {0}")]
904 #[diagnostic(code(ERR_AUBE_PACKAGE_NOT_FOUND))]
905 NotFound(String),
906 #[error("access entity not found: {0}")]
907 #[diagnostic(code(ERR_AUBE_ACCESS_ENTITY_NOT_FOUND))]
908 AccessEntityNotFound(String),
909 #[error("registry identity endpoint is unavailable")]
910 #[diagnostic(code(ERR_AUBE_REGISTRY_ERROR))]
911 AccessIdentityUnavailable,
912 #[error("version not found: {0}@{1}")]
913 #[diagnostic(code(ERR_AUBE_VERSION_NOT_FOUND))]
914 VersionNotFound(String, String),
915 #[error("authentication required")]
920 #[diagnostic(code(ERR_AUBE_UNAUTHORIZED))]
921 Unauthorized,
922 #[error("I/O error: {0}")]
923 Io(#[from] std::io::Error),
924 #[error("registry rejected write: HTTP {status}: {body}")]
925 #[diagnostic(code(ERR_AUBE_REGISTRY_WRITE_REJECTED))]
926 RegistryWrite { status: u16, body: String },
927 #[error("offline: {0} is not available in the local cache")]
928 #[diagnostic(code(ERR_AUBE_OFFLINE))]
929 Offline(String),
930 #[error("invalid package name: {0:?}")]
935 #[diagnostic(code(ERR_AUBE_INVALID_PACKAGE_NAME))]
936 InvalidName(String),
937}
938
939impl Error {
940 pub fn is_throttle(&self) -> bool {
948 match self {
949 Error::Http(e) => {
950 if e.is_timeout() {
951 return true;
952 }
953 matches!(
954 e.status().map(|s| s.as_u16()),
955 Some(429) | Some(502) | Some(503) | Some(504)
956 )
957 }
958 Error::RegistryWrite { status, .. } => matches!(*status, 429 | 502 | 503 | 504),
959 _ => false,
960 }
961 }
962}
963
964#[cfg(test)]
965mod tests {
966 use super::*;
967
968 fn parse(json: &str) -> VersionMetadata {
969 serde_json::from_str(json).unwrap()
970 }
971
972 #[test]
973 fn exact_version_packument_seed_keeps_only_selected_full_metadata() {
974 let json = br#"{
975 "name":"platform-package",
976 "versions":{
977 "1.0.0":{
978 "name":"platform-package",
979 "version":"1.0.0",
980 "dependencies":{"discarded":"1"},
981 "approver":{"name":"release-manager"}
982 },
983 "2.0.0":{
984 "name":"platform-package",
985 "version":"2.0.0",
986 "dependencies":{"retained":"2"},
987 "dist":{"tarball":"https://registry.example/pkg.tgz"}
988 }
989 },
990 "time":{"1.0.0":"2025-01-01T00:00:00.000Z","2.0.0":"2025-02-01T00:00:00.000Z"}
991 }"#;
992 let mut deserializer = sonic_rs::Deserializer::from_slice(json);
993 let decoded = serde::de::DeserializeSeed::deserialize(
994 ExactVersionPackumentSeed { version: "2.0.0" },
995 &mut deserializer,
996 )
997 .unwrap();
998
999 assert_eq!(decoded.metadata.version, "2.0.0");
1000 assert_eq!(
1001 decoded.metadata.dependencies.get("retained"),
1002 Some(&"2".to_string())
1003 );
1004 assert!(!decoded.history.versions.contains_key("2.0.0"));
1005 assert!(decoded.history.versions.contains_key("1.0.0"));
1006 assert_eq!(decoded.history.time.len(), 2);
1007 }
1008
1009 #[test]
1010 fn exact_version_packument_seed_rejects_missing_selected_version() {
1011 let json = br#"{"versions":{"1.0.0":{"name":"x","version":"1.0.0"}}}"#;
1012 let mut deserializer = sonic_rs::Deserializer::from_slice(json);
1013 let error = serde::de::DeserializeSeed::deserialize(
1014 ExactVersionPackumentSeed { version: "2.0.0" },
1015 &mut deserializer,
1016 )
1017 .unwrap_err();
1018
1019 assert!(error.to_string().contains("requested version 2.0.0"));
1020 }
1021
1022 #[test]
1023 fn libc_accepts_string() {
1024 let v = parse(r#"{"name":"x","version":"1.0.0","libc":"glibc"}"#);
1025 assert_eq!(v.libc, vec!["glibc"]);
1026 }
1027
1028 #[test]
1029 fn libc_accepts_array() {
1030 let v = parse(r#"{"name":"x","version":"1.0.0","libc":["glibc","musl"]}"#);
1031 assert_eq!(v.libc, vec!["glibc", "musl"]);
1032 }
1033
1034 #[test]
1035 fn os_and_cpu_accept_string() {
1036 let v = parse(r#"{"name":"x","version":"1.0.0","os":"linux","cpu":"x64"}"#);
1037 assert_eq!(v.os, vec!["linux"]);
1038 assert_eq!(v.cpu, vec!["x64"]);
1039 }
1040
1041 #[test]
1042 fn null_is_treated_as_empty() {
1043 let v = parse(r#"{"name":"x","version":"1.0.0","os":null,"cpu":null,"libc":null}"#);
1044 assert!(v.os.is_empty());
1045 assert!(v.cpu.is_empty());
1046 assert!(v.libc.is_empty());
1047 }
1048
1049 #[test]
1054 fn libc_array_containing_null_drops_null() {
1055 let v = parse(r#"{"name":"x","version":"1.0.0","libc":[null]}"#);
1056 assert!(v.libc.is_empty());
1057 }
1058
1059 #[test]
1060 fn os_cpu_libc_arrays_drop_non_string_entries() {
1061 let v = parse(
1062 r#"{
1063 "name":"x","version":"1.0.0",
1064 "os":["linux",null,42],
1065 "cpu":["x64",null],
1066 "libc":["glibc",{"x":1}]
1067 }"#,
1068 );
1069 assert_eq!(v.os, vec!["linux"]);
1070 assert_eq!(v.cpu, vec!["x64"]);
1071 assert_eq!(v.libc, vec!["glibc"]);
1072 }
1073
1074 #[test]
1075 fn approver_metadata_is_extracted() {
1076 let v = parse(
1077 r#"{
1078 "name":"x",
1079 "version":"1.0.0",
1080 "approver":{"name":"release-manager"}
1081 }"#,
1082 );
1083 assert_eq!(
1084 v.approver
1085 .as_ref()
1086 .and_then(|a| a.get("name"))
1087 .and_then(serde_json::Value::as_str),
1088 Some("release-manager")
1089 );
1090 }
1091
1092 #[test]
1093 fn bin_normalizes_packument_shapes() {
1094 let missing = parse(r#"{"name":"x","version":"1.0.0"}"#);
1095 assert!(missing.bin.is_empty(), "missing bin → empty map");
1096 let empty_string = parse(r#"{"name":"x","version":"1.0.0","bin":""}"#);
1097 assert!(empty_string.bin.is_empty(), "empty string bin → empty map");
1098 let null_bin = parse(r#"{"name":"x","version":"1.0.0","bin":null}"#);
1099 assert!(null_bin.bin.is_empty(), "null bin → empty map");
1100 let empty_map = parse(r#"{"name":"x","version":"1.0.0","bin":{}}"#);
1101 assert!(empty_map.bin.is_empty(), "empty map bin → empty map");
1102 let string_bin = parse(r#"{"name":"x","version":"1.0.0","bin":"cli.js"}"#);
1106 assert_eq!(string_bin.bin.get(""), Some(&"cli.js".to_string()));
1107 let map_bin = parse(r#"{"name":"x","version":"1.0.0","bin":{"foo":"cli.js"}}"#);
1108 assert_eq!(map_bin.bin.get("foo"), Some(&"cli.js".to_string()));
1109 }
1110
1111 #[test]
1117 fn bin_map_roundtrips_through_cache_serialization() {
1118 let mut bin = BTreeMap::new();
1119 bin.insert("semver".to_string(), "bin/semver.js".to_string());
1120 let v = VersionMetadata {
1121 name: "semver".to_string(),
1122 version: "7.7.4".to_string(),
1123 dependencies: BTreeMap::new(),
1124 dev_dependencies: BTreeMap::new(),
1125 peer_dependencies: BTreeMap::new(),
1126 peer_dependencies_meta: BTreeMap::new(),
1127 optional_dependencies: BTreeMap::new(),
1128 bundled_dependencies: None,
1129 dist: None,
1130 os: Vec::new(),
1131 cpu: Vec::new(),
1132 libc: Vec::new(),
1133 engines: BTreeMap::new(),
1134 license: None,
1135 funding_url: None,
1136 bin,
1137 has_install_script: false,
1138 deprecated: None,
1139 approver: None,
1140 npm_user: None,
1141 };
1142 let json = serde_json::to_string(&v).unwrap();
1143 let back: VersionMetadata = serde_json::from_str(&json).unwrap();
1144 assert_eq!(
1145 back.bin.get("semver"),
1146 Some(&"bin/semver.js".to_string()),
1147 "bin map must round-trip through cache serialization"
1148 );
1149 }
1150
1151 #[test]
1152 fn missing_fields_default_to_empty() {
1153 let v = parse(r#"{"name":"x","version":"1.0.0"}"#);
1154 assert!(v.os.is_empty());
1155 assert!(v.cpu.is_empty());
1156 assert!(v.libc.is_empty());
1157 }
1158
1159 #[test]
1160 fn attestations_provenance_is_extracted() {
1161 let v = parse(
1162 r#"{"name":"x","version":"1.0.0",
1163 "dist":{"tarball":"t","attestations":{"provenance":{"predicateType":"slsa"}}}}"#,
1164 );
1165 let dist = v.dist.expect("dist present");
1166 let att = dist.attestations.expect("attestations present");
1167 assert!(att.provenance.is_some(), "provenance present");
1168 }
1169
1170 #[test]
1171 fn attestations_missing_is_none() {
1172 let v = parse(r#"{"name":"x","version":"1.0.0","dist":{"tarball":"t"}}"#);
1173 let dist = v.dist.expect("dist present");
1174 assert!(dist.attestations.is_none());
1175 }
1176
1177 #[test]
1178 fn npm_user_object_with_trusted_publisher_is_parsed() {
1179 let v = parse(
1180 r#"{"name":"x","version":"1.0.0",
1181 "_npmUser":{"name":"u","email":"u@x","trustedPublisher":{"id":"gh"}}}"#,
1182 );
1183 let user = v.npm_user.expect("_npmUser present");
1184 assert!(user.trusted_publisher.is_some());
1185 }
1186
1187 #[test]
1188 fn npm_user_object_without_trusted_publisher_is_parsed() {
1189 let v = parse(r#"{"name":"x","version":"1.0.0","_npmUser":{"name":"u","email":"u@x"}}"#);
1190 let user = v.npm_user.expect("_npmUser present");
1191 assert!(user.trusted_publisher.is_none());
1192 }
1193
1194 #[test]
1203 fn npm_user_garbage_sibling_fields_still_extract_trusted_publisher() {
1204 let v = parse(
1205 r#"{"name":"x","version":"1.0.0",
1206 "_npmUser":{
1207 "trustedPublisher":{"id":"gh"},
1208 "name":42,
1209 "email":[null,{"deep":{"nested":"garbage"}}],
1210 "future_field_with_strict_type":"this would fail strict deserialize"
1211 }}"#,
1212 );
1213 let user = v
1214 .npm_user
1215 .expect("_npmUser present despite garbage siblings");
1216 assert!(user.trusted_publisher.is_some());
1217 }
1218
1219 #[test]
1222 fn npm_user_string_form_degrades_to_none() {
1223 let v = parse(r#"{"name":"x","version":"1.0.0","_npmUser":"isaacs <i@npmjs.com>"}"#);
1224 assert!(v.npm_user.is_none());
1225 }
1226
1227 #[test]
1228 fn npm_user_null_or_missing_is_none() {
1229 let v_null = parse(r#"{"name":"x","version":"1.0.0","_npmUser":null}"#);
1230 assert!(v_null.npm_user.is_none());
1231 let v_missing = parse(r#"{"name":"x","version":"1.0.0"}"#);
1232 assert!(v_missing.npm_user.is_none());
1233 }
1234
1235 #[test]
1236 fn deprecated_string_is_preserved_and_false_is_empty() {
1237 let v = parse(r#"{"name":"x","version":"1.0.0","deprecated":"use y"}"#);
1238 assert_eq!(v.deprecated.as_deref(), Some("use y"));
1239
1240 let v = parse(r#"{"name":"x","version":"1.0.1","deprecated":false}"#);
1241 assert!(v.deprecated.is_none());
1242 }
1243
1244 #[test]
1249 fn dependency_maps_drop_null_entries() {
1250 let v = parse(
1251 r#"{
1252 "name": "x",
1253 "version": "1.0.0",
1254 "dependencies": {"kept": "^1", "stripped": null},
1255 "devDependencies": {"dkept": "^2", "dstripped": null},
1256 "peerDependencies": {"pkept": "^3", "pstripped": null},
1257 "optionalDependencies": {"okept": "^4", "ostripped": null}
1258 }"#,
1259 );
1260 assert_eq!(v.dependencies.len(), 1);
1261 assert_eq!(v.dependencies["kept"], "^1");
1262 assert_eq!(v.dev_dependencies.len(), 1);
1263 assert_eq!(v.peer_dependencies.len(), 1);
1264 assert_eq!(v.optional_dependencies.len(), 1);
1265 }
1266
1267 #[test]
1274 fn dependency_maps_drop_object_valued_entries() {
1275 let v = parse(
1276 r#"{
1277 "name": "deep-diff",
1278 "version": "0.1.0",
1279 "devDependencies": {
1280 "vows": {"version": "0.6.4", "dependencies": {"diff": {"version": "1.0.4"}}},
1281 "extend": {"version": "1.1.1"},
1282 "lodash": "0.9.2"
1283 }
1284 }"#,
1285 );
1286 assert_eq!(v.dev_dependencies.len(), 1);
1287 assert_eq!(v.dev_dependencies["lodash"], "0.9.2");
1288 }
1289
1290 #[test]
1291 fn dependency_maps_null_whole_field_is_empty() {
1292 let v = parse(
1293 r#"{
1294 "name": "x",
1295 "version": "1.0.0",
1296 "dependencies": null,
1297 "devDependencies": null,
1298 "peerDependencies": null,
1299 "optionalDependencies": null
1300 }"#,
1301 );
1302 assert!(v.dependencies.is_empty());
1303 assert!(v.dev_dependencies.is_empty());
1304 assert!(v.peer_dependencies.is_empty());
1305 assert!(v.optional_dependencies.is_empty());
1306 }
1307
1308 fn parse_packument(json: &str) -> Packument {
1309 serde_json::from_str(json).unwrap()
1310 }
1311
1312 #[test]
1313 fn packument_dist_tags_drops_null_tag() {
1314 let p = parse_packument(
1315 r#"{
1316 "name": "pkg",
1317 "dist-tags": {"latest": "1.2.3", "beta": null}
1318 }"#,
1319 );
1320 assert_eq!(p.dist_tags.len(), 1);
1321 assert_eq!(p.dist_tags["latest"], "1.2.3");
1322 }
1323
1324 #[test]
1325 fn packument_dist_tags_null_whole_field_is_empty() {
1326 let p = parse_packument(r#"{"name":"pkg","dist-tags":null}"#);
1327 assert!(p.dist_tags.is_empty());
1328 }
1329
1330 #[test]
1331 fn packument_preserves_modified_timestamp() {
1332 let p = parse_packument(
1333 r#"{
1334 "name": "pkg",
1335 "modified": "2026-04-14T14:26:11.557Z"
1336 }"#,
1337 );
1338 assert_eq!(p.modified.as_deref(), Some("2026-04-14T14:26:11.557Z"));
1339 }
1340
1341 #[test]
1342 fn packument_time_drops_null_entries() {
1343 let p = parse_packument(
1344 r#"{
1345 "name": "pkg",
1346 "time": {"1.0.0": "2024-01-01T00:00:00.000Z", "0.9.0": null}
1347 }"#,
1348 );
1349 assert_eq!(p.time.len(), 1);
1350 assert!(p.time.contains_key("1.0.0"));
1351 }
1352
1353 #[test]
1354 fn packument_time_null_whole_field_is_empty() {
1355 let p = parse_packument(r#"{"name":"pkg","time":null}"#);
1356 assert!(p.time.is_empty());
1357 }
1358
1359 #[test]
1368 fn engines_accepts_legacy_array_shape() {
1369 let v = parse(r#"{"name":"madge","version":"0.0.1","engines":["node >= 0.8.0"]}"#);
1370 assert!(v.engines.is_empty());
1371 }
1372
1373 #[test]
1374 fn engines_accepts_legacy_string_shape() {
1375 let v = parse(r#"{"name":"qs","version":"0.6.0","engines":"node >= 0.4.0"}"#);
1376 assert!(v.engines.is_empty());
1377 }
1378
1379 #[test]
1380 fn engines_accepts_map_shape() {
1381 let v = parse(r#"{"name":"x","version":"1.0.0","engines":{"node":">=18"}}"#);
1382 assert_eq!(v.engines.get("node"), Some(&">=18".to_string()));
1383 }
1384
1385 #[test]
1386 fn engines_null_is_empty() {
1387 let v = parse(r#"{"name":"x","version":"1.0.0","engines":null}"#);
1388 assert!(v.engines.is_empty());
1389 }
1390
1391 #[test]
1397 fn bundled_deps_accepts_both_canonical_and_alias() {
1398 let v = parse(
1399 r#"{
1400 "name":"x","version":"1.0.0",
1401 "bundledDependencies":["canonical"],
1402 "bundleDependencies":["legacy"]
1403 }"#,
1404 );
1405 let deps = BTreeMap::new();
1406 let names = v.bundled_dependencies.as_ref().unwrap().names(&deps);
1407 assert_eq!(names, vec!["canonical"]);
1408 }
1409
1410 #[test]
1411 fn bundled_deps_falls_back_to_alias_only() {
1412 let v = parse(r#"{"name":"x","version":"1.0.0","bundleDependencies":["legacy"]}"#);
1413 let deps = BTreeMap::new();
1414 let names = v.bundled_dependencies.as_ref().unwrap().names(&deps);
1415 assert_eq!(names, vec!["legacy"]);
1416 }
1417}