use serde::de::{IgnoredAny, MapAccess, SeqAccess, Visitor};
use serde::{Deserialize, Deserializer, Serialize};
use std::collections::BTreeMap;
use std::fmt;
#[cfg(not(feature = "rustls"))]
compile_error!("aube-registry requires the `rustls` feature");
macro_rules! visit_primitives_to {
($de:lifetime, $default:expr) => {
fn visit_unit<E: serde::de::Error>(self) -> Result<Self::Value, E> {
Ok($default)
}
fn visit_none<E: serde::de::Error>(self) -> Result<Self::Value, E> {
Ok($default)
}
fn visit_some<D2: Deserializer<$de>>(self, d: D2) -> Result<Self::Value, D2::Error> {
d.deserialize_any(self)
}
fn visit_bool<E: serde::de::Error>(self, _: bool) -> Result<Self::Value, E> {
Ok($default)
}
fn visit_i64<E: serde::de::Error>(self, _: i64) -> Result<Self::Value, E> {
Ok($default)
}
fn visit_u64<E: serde::de::Error>(self, _: u64) -> Result<Self::Value, E> {
Ok($default)
}
fn visit_f64<E: serde::de::Error>(self, _: f64) -> Result<Self::Value, E> {
Ok($default)
}
};
}
macro_rules! visit_seq_to {
($de:lifetime, $default:expr) => {
fn visit_seq<A: SeqAccess<$de>>(self, mut access: A) -> Result<Self::Value, A::Error> {
while access.next_element::<IgnoredAny>()?.is_some() {}
Ok($default)
}
};
}
macro_rules! visit_map_to {
($de:lifetime, $default:expr) => {
fn visit_map<A: MapAccess<$de>>(self, mut access: A) -> Result<Self::Value, A::Error> {
while access.next_entry::<IgnoredAny, IgnoredAny>()?.is_some() {}
Ok($default)
}
};
}
macro_rules! visit_strings_to {
($de:lifetime, $default:expr) => {
fn visit_str<E: serde::de::Error>(self, _: &str) -> Result<Self::Value, E> {
Ok($default)
}
fn visit_string<E: serde::de::Error>(self, _: String) -> Result<Self::Value, E> {
Ok($default)
}
};
}
fn non_string_tolerant_map<'de, D>(de: D) -> Result<BTreeMap<String, String>, D::Error>
where
D: Deserializer<'de>,
{
struct V;
impl<'de> Visitor<'de> for V {
type Value = BTreeMap<String, String>;
fn expecting(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.write_str("null or an object mapping strings to strings")
}
fn visit_unit<E: serde::de::Error>(self) -> Result<Self::Value, E> {
Ok(BTreeMap::new())
}
fn visit_none<E: serde::de::Error>(self) -> Result<Self::Value, E> {
Ok(BTreeMap::new())
}
fn visit_some<D2: Deserializer<'de>>(self, d: D2) -> Result<Self::Value, D2::Error> {
d.deserialize_any(self)
}
fn visit_map<A: MapAccess<'de>>(self, mut access: A) -> Result<Self::Value, A::Error> {
let mut out = BTreeMap::new();
while let Some(key) = access.next_key::<String>()? {
let MaybeString(maybe) = access.next_value()?;
if let Some(s) = maybe {
out.insert(key, s);
}
}
Ok(out)
}
}
de.deserialize_any(V)
}
pub mod client;
pub mod config;
pub mod jsr;
pub mod osv_bloom_client;
pub mod osv_mirror;
pub mod slow_metadata;
pub mod supply_chain;
pub use aube_manifest::BundledDependencies;
#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
pub enum NetworkMode {
#[default]
Online,
PreferOffline,
Offline,
}
#[derive(Debug, Clone, Deserialize, Serialize)]
pub struct Packument {
pub name: String,
#[serde(default)]
pub modified: Option<String>,
#[serde(default)]
pub versions: BTreeMap<String, VersionMetadata>,
#[serde(
rename = "dist-tags",
default,
deserialize_with = "non_string_tolerant_map"
)]
pub dist_tags: BTreeMap<String, String>,
#[serde(default, deserialize_with = "non_string_tolerant_map")]
pub time: BTreeMap<String, String>,
}
#[derive(Debug, Clone, Deserialize, Serialize)]
#[serde(rename_all = "camelCase", from = "VersionMetadataRaw")]
pub struct VersionMetadata {
pub name: String,
pub version: String,
#[serde(default, deserialize_with = "non_string_tolerant_map")]
pub dependencies: BTreeMap<String, String>,
#[serde(default, deserialize_with = "non_string_tolerant_map")]
pub dev_dependencies: BTreeMap<String, String>,
#[serde(default, deserialize_with = "non_string_tolerant_map")]
pub peer_dependencies: BTreeMap<String, String>,
#[serde(default)]
pub peer_dependencies_meta: BTreeMap<String, PeerDepMeta>,
#[serde(default, deserialize_with = "non_string_tolerant_map")]
pub optional_dependencies: BTreeMap<String, String>,
pub bundled_dependencies: Option<BundledDependencies>,
pub dist: Option<Dist>,
#[serde(default, deserialize_with = "aube_util::string_or_seq")]
pub os: Vec<String>,
#[serde(default, deserialize_with = "aube_util::string_or_seq")]
pub cpu: Vec<String>,
#[serde(default, deserialize_with = "aube_util::string_or_seq")]
pub libc: Vec<String>,
#[serde(default, deserialize_with = "aube_manifest::engines_tolerant")]
pub engines: BTreeMap<String, String>,
#[serde(default, deserialize_with = "license_string")]
pub license: Option<String>,
#[serde(default, rename = "funding", deserialize_with = "funding_url")]
pub funding_url: Option<String>,
#[serde(default, rename = "bin", deserialize_with = "bin_map")]
pub bin: BTreeMap<String, String>,
#[serde(default)]
pub has_install_script: bool,
#[serde(default, deserialize_with = "deprecated_string")]
pub deprecated: Option<String>,
#[serde(default)]
pub approver: Option<serde_json::Value>,
#[serde(default, rename = "_npmUser", deserialize_with = "npm_user_tolerant")]
pub npm_user: Option<NpmUser>,
}
#[derive(Debug, Deserialize)]
#[serde(rename_all = "camelCase")]
struct VersionMetadataRaw {
name: String,
version: String,
#[serde(default, deserialize_with = "non_string_tolerant_map")]
dependencies: BTreeMap<String, String>,
#[serde(default, deserialize_with = "non_string_tolerant_map")]
dev_dependencies: BTreeMap<String, String>,
#[serde(default, deserialize_with = "non_string_tolerant_map")]
peer_dependencies: BTreeMap<String, String>,
#[serde(default)]
peer_dependencies_meta: BTreeMap<String, PeerDepMeta>,
#[serde(default, deserialize_with = "non_string_tolerant_map")]
optional_dependencies: BTreeMap<String, String>,
#[serde(default, rename = "bundledDependencies")]
bundled_dependencies: Option<BundledDependencies>,
#[serde(default, rename = "bundleDependencies")]
bundle_dependencies_alias: Option<BundledDependencies>,
dist: Option<Dist>,
#[serde(default, deserialize_with = "aube_util::string_or_seq")]
os: Vec<String>,
#[serde(default, deserialize_with = "aube_util::string_or_seq")]
cpu: Vec<String>,
#[serde(default, deserialize_with = "aube_util::string_or_seq")]
libc: Vec<String>,
#[serde(default, deserialize_with = "aube_manifest::engines_tolerant")]
engines: BTreeMap<String, String>,
#[serde(default, deserialize_with = "license_string")]
license: Option<String>,
#[serde(default, rename = "funding", deserialize_with = "funding_url")]
funding_url: Option<String>,
#[serde(default, rename = "bin", deserialize_with = "bin_map")]
bin: BTreeMap<String, String>,
#[serde(default)]
has_install_script: bool,
#[serde(default, deserialize_with = "deprecated_string")]
deprecated: Option<String>,
#[serde(default)]
approver: Option<serde_json::Value>,
#[serde(default, rename = "_npmUser", deserialize_with = "npm_user_tolerant")]
npm_user: Option<NpmUser>,
}
impl From<VersionMetadataRaw> for VersionMetadata {
fn from(raw: VersionMetadataRaw) -> Self {
Self {
name: raw.name,
version: raw.version,
dependencies: raw.dependencies,
dev_dependencies: raw.dev_dependencies,
peer_dependencies: raw.peer_dependencies,
peer_dependencies_meta: raw.peer_dependencies_meta,
optional_dependencies: raw.optional_dependencies,
bundled_dependencies: raw.bundled_dependencies.or(raw.bundle_dependencies_alias),
dist: raw.dist,
os: raw.os,
cpu: raw.cpu,
libc: raw.libc,
engines: raw.engines,
license: raw.license,
funding_url: raw.funding_url,
bin: raw.bin,
has_install_script: raw.has_install_script,
deprecated: raw.deprecated,
approver: raw.approver,
npm_user: raw.npm_user,
}
}
}
#[derive(Debug, Clone, Deserialize, Serialize, PartialEq, Eq)]
pub struct PeerDepMeta {
#[serde(default)]
pub optional: bool,
}
#[derive(Debug, Clone, Deserialize, Serialize, Default)]
pub struct NpmUser {
#[serde(default, rename = "trustedPublisher")]
pub trusted_publisher: Option<serde_json::Value>,
}
#[derive(Debug, Clone, Deserialize, Serialize)]
pub struct Dist {
pub tarball: String,
pub integrity: Option<String>,
pub shasum: Option<String>,
#[serde(default, rename = "unpackedSize")]
pub unpacked_size: Option<u64>,
#[serde(default)]
pub attestations: Option<Attestations>,
}
#[derive(Debug, Clone, Deserialize, Serialize, Default)]
pub struct Attestations {
#[serde(default)]
pub provenance: Option<serde_json::Value>,
}
fn deprecated_string<'de, D>(de: D) -> Result<Option<String>, D::Error>
where
D: Deserializer<'de>,
{
let MaybeString(maybe) = MaybeString::deserialize(de)?;
Ok(maybe.filter(|s| !s.is_empty()))
}
fn license_string<'de, D>(de: D) -> Result<Option<String>, D::Error>
where
D: Deserializer<'de>,
{
struct V;
impl<'de> Visitor<'de> for V {
type Value = Option<String>;
fn expecting(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.write_str("a license string, a {type, url} object, or null")
}
fn visit_str<E: serde::de::Error>(self, s: &str) -> Result<Self::Value, E> {
Ok((!s.is_empty()).then(|| s.to_owned()))
}
fn visit_string<E: serde::de::Error>(self, s: String) -> Result<Self::Value, E> {
Ok((!s.is_empty()).then_some(s))
}
fn visit_map<A: MapAccess<'de>>(self, mut access: A) -> Result<Self::Value, A::Error> {
let mut found: Option<String> = None;
while let Some(key) = access.next_key::<String>()? {
if key == "type" && found.is_none() {
let MaybeString(maybe) = access.next_value()?;
found = maybe.filter(|s| !s.is_empty());
} else {
let _: IgnoredAny = access.next_value()?;
}
}
Ok(found)
}
visit_primitives_to!('de, None);
visit_seq_to!('de, None);
}
de.deserialize_any(V)
}
fn funding_url<'de, D>(de: D) -> Result<Option<String>, D::Error>
where
D: Deserializer<'de>,
{
struct V;
impl<'de> Visitor<'de> for V {
type Value = Option<String>;
fn expecting(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.write_str("a funding URL string, a {url} object, or an array of either")
}
fn visit_str<E: serde::de::Error>(self, s: &str) -> Result<Self::Value, E> {
Ok((!s.is_empty()).then(|| s.to_owned()))
}
fn visit_string<E: serde::de::Error>(self, s: String) -> Result<Self::Value, E> {
Ok((!s.is_empty()).then_some(s))
}
fn visit_map<A: MapAccess<'de>>(self, access: A) -> Result<Self::Value, A::Error> {
extract_url_from_map(access)
}
fn visit_seq<A: SeqAccess<'de>>(self, mut access: A) -> Result<Self::Value, A::Error> {
while let Some(FundingArrayEntry(maybe)) = access.next_element()? {
if let Some(s) = maybe {
while access.next_element::<IgnoredAny>()?.is_some() {}
return Ok(Some(s));
}
}
Ok(None)
}
visit_primitives_to!('de, None);
}
de.deserialize_any(V)
}
fn extract_url_from_map<'de, A: MapAccess<'de>>(mut access: A) -> Result<Option<String>, A::Error> {
let mut found: Option<String> = None;
while let Some(key) = access.next_key::<String>()? {
if key == "url" && found.is_none() {
let MaybeString(maybe) = access.next_value()?;
found = maybe.filter(|s| !s.is_empty());
} else {
let _: IgnoredAny = access.next_value()?;
}
}
Ok(found)
}
fn npm_user_tolerant<'de, D>(de: D) -> Result<Option<NpmUser>, D::Error>
where
D: Deserializer<'de>,
{
struct V;
impl<'de> Visitor<'de> for V {
type Value = Option<NpmUser>;
fn expecting(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.write_str("an _npmUser object or any other JSON value")
}
fn visit_map<A: MapAccess<'de>>(self, mut access: A) -> Result<Self::Value, A::Error> {
let mut trusted_publisher: Option<serde_json::Value> = None;
while let Some(key) = access.next_key::<String>()? {
if key == "trustedPublisher" && trusted_publisher.is_none() {
trusted_publisher = access.next_value::<Option<serde_json::Value>>()?;
} else {
let _: IgnoredAny = access.next_value()?;
}
}
Ok(Some(NpmUser { trusted_publisher }))
}
visit_primitives_to!('de, None);
visit_seq_to!('de, None);
visit_strings_to!('de, None);
}
de.deserialize_any(V)
}
fn bin_map<'de, D>(de: D) -> Result<BTreeMap<String, String>, D::Error>
where
D: Deserializer<'de>,
{
struct V;
impl<'de> Visitor<'de> for V {
type Value = BTreeMap<String, String>;
fn expecting(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.write_str("a bin string, a bin map, or null")
}
fn visit_str<E: serde::de::Error>(self, s: &str) -> Result<Self::Value, E> {
if s.is_empty() {
return Ok(BTreeMap::new());
}
let mut m = BTreeMap::new();
m.insert(String::new(), s.to_owned());
Ok(m)
}
fn visit_string<E: serde::de::Error>(self, s: String) -> Result<Self::Value, E> {
if s.is_empty() {
return Ok(BTreeMap::new());
}
let mut m = BTreeMap::new();
m.insert(String::new(), s);
Ok(m)
}
fn visit_map<A: MapAccess<'de>>(self, mut access: A) -> Result<Self::Value, A::Error> {
let mut out = BTreeMap::new();
while let Some(key) = access.next_key::<String>()? {
let MaybeString(maybe) = access.next_value()?;
if let Some(s) = maybe {
out.insert(key, s);
}
}
Ok(out)
}
visit_primitives_to!('de, BTreeMap::new());
visit_seq_to!('de, BTreeMap::new());
}
de.deserialize_any(V)
}
struct MaybeString(Option<String>);
impl<'de> Deserialize<'de> for MaybeString {
fn deserialize<D: Deserializer<'de>>(de: D) -> Result<Self, D::Error> {
struct V;
impl<'de> Visitor<'de> for V {
type Value = MaybeString;
fn expecting(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.write_str("any JSON value")
}
fn visit_str<E: serde::de::Error>(self, s: &str) -> Result<MaybeString, E> {
Ok(MaybeString(Some(s.to_owned())))
}
fn visit_string<E: serde::de::Error>(self, s: String) -> Result<MaybeString, E> {
Ok(MaybeString(Some(s)))
}
visit_primitives_to!('de, MaybeString(None));
visit_seq_to!('de, MaybeString(None));
visit_map_to!('de, MaybeString(None));
}
de.deserialize_any(V)
}
}
struct FundingArrayEntry(Option<String>);
impl<'de> Deserialize<'de> for FundingArrayEntry {
fn deserialize<D: Deserializer<'de>>(de: D) -> Result<Self, D::Error> {
struct V;
impl<'de> Visitor<'de> for V {
type Value = FundingArrayEntry;
fn expecting(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.write_str("a funding URL string or a {url} object")
}
fn visit_str<E: serde::de::Error>(self, s: &str) -> Result<FundingArrayEntry, E> {
Ok(FundingArrayEntry((!s.is_empty()).then(|| s.to_owned())))
}
fn visit_string<E: serde::de::Error>(self, s: String) -> Result<FundingArrayEntry, E> {
Ok(FundingArrayEntry((!s.is_empty()).then_some(s)))
}
fn visit_map<A: MapAccess<'de>>(
self,
access: A,
) -> Result<FundingArrayEntry, A::Error> {
Ok(FundingArrayEntry(extract_url_from_map(access)?))
}
visit_primitives_to!('de, FundingArrayEntry(None));
visit_seq_to!('de, FundingArrayEntry(None));
}
de.deserialize_any(V)
}
}
#[derive(Debug, thiserror::Error, miette::Diagnostic)]
pub enum Error {
#[error("HTTP error: {0}")]
Http(#[from] reqwest::Error),
#[error("package not found: {0}")]
#[diagnostic(code(ERR_AUBE_PACKAGE_NOT_FOUND))]
NotFound(String),
#[error("access entity not found: {0}")]
#[diagnostic(code(ERR_AUBE_ACCESS_ENTITY_NOT_FOUND))]
AccessEntityNotFound(String),
#[error("version not found: {0}@{1}")]
#[diagnostic(code(ERR_AUBE_VERSION_NOT_FOUND))]
VersionNotFound(String, String),
#[error("authentication required")]
#[diagnostic(code(ERR_AUBE_UNAUTHORIZED))]
Unauthorized,
#[error("I/O error: {0}")]
Io(#[from] std::io::Error),
#[error("registry rejected write: HTTP {status}: {body}")]
#[diagnostic(code(ERR_AUBE_REGISTRY_WRITE_REJECTED))]
RegistryWrite { status: u16, body: String },
#[error("offline: {0} is not available in the local cache")]
#[diagnostic(code(ERR_AUBE_OFFLINE))]
Offline(String),
#[error("invalid package name: {0:?}")]
#[diagnostic(code(ERR_AUBE_INVALID_PACKAGE_NAME))]
InvalidName(String),
}
impl Error {
pub fn is_throttle(&self) -> bool {
match self {
Error::Http(e) => {
if e.is_timeout() {
return true;
}
matches!(
e.status().map(|s| s.as_u16()),
Some(429) | Some(502) | Some(503) | Some(504)
)
}
Error::RegistryWrite { status, .. } => matches!(*status, 429 | 502 | 503 | 504),
_ => false,
}
}
}
#[cfg(test)]
mod tests {
use super::*;
fn parse(json: &str) -> VersionMetadata {
serde_json::from_str(json).unwrap()
}
#[test]
fn libc_accepts_string() {
let v = parse(r#"{"name":"x","version":"1.0.0","libc":"glibc"}"#);
assert_eq!(v.libc, vec!["glibc"]);
}
#[test]
fn libc_accepts_array() {
let v = parse(r#"{"name":"x","version":"1.0.0","libc":["glibc","musl"]}"#);
assert_eq!(v.libc, vec!["glibc", "musl"]);
}
#[test]
fn os_and_cpu_accept_string() {
let v = parse(r#"{"name":"x","version":"1.0.0","os":"linux","cpu":"x64"}"#);
assert_eq!(v.os, vec!["linux"]);
assert_eq!(v.cpu, vec!["x64"]);
}
#[test]
fn null_is_treated_as_empty() {
let v = parse(r#"{"name":"x","version":"1.0.0","os":null,"cpu":null,"libc":null}"#);
assert!(v.os.is_empty());
assert!(v.cpu.is_empty());
assert!(v.libc.is_empty());
}
#[test]
fn libc_array_containing_null_drops_null() {
let v = parse(r#"{"name":"x","version":"1.0.0","libc":[null]}"#);
assert!(v.libc.is_empty());
}
#[test]
fn os_cpu_libc_arrays_drop_non_string_entries() {
let v = parse(
r#"{
"name":"x","version":"1.0.0",
"os":["linux",null,42],
"cpu":["x64",null],
"libc":["glibc",{"x":1}]
}"#,
);
assert_eq!(v.os, vec!["linux"]);
assert_eq!(v.cpu, vec!["x64"]);
assert_eq!(v.libc, vec!["glibc"]);
}
#[test]
fn approver_metadata_is_extracted() {
let v = parse(
r#"{
"name":"x",
"version":"1.0.0",
"approver":{"name":"release-manager"}
}"#,
);
assert_eq!(
v.approver
.as_ref()
.and_then(|a| a.get("name"))
.and_then(serde_json::Value::as_str),
Some("release-manager")
);
}
#[test]
fn bin_normalizes_packument_shapes() {
let missing = parse(r#"{"name":"x","version":"1.0.0"}"#);
assert!(missing.bin.is_empty(), "missing bin → empty map");
let empty_string = parse(r#"{"name":"x","version":"1.0.0","bin":""}"#);
assert!(empty_string.bin.is_empty(), "empty string bin → empty map");
let null_bin = parse(r#"{"name":"x","version":"1.0.0","bin":null}"#);
assert!(null_bin.bin.is_empty(), "null bin → empty map");
let empty_map = parse(r#"{"name":"x","version":"1.0.0","bin":{}}"#);
assert!(empty_map.bin.is_empty(), "empty map bin → empty map");
let string_bin = parse(r#"{"name":"x","version":"1.0.0","bin":"cli.js"}"#);
assert_eq!(string_bin.bin.get(""), Some(&"cli.js".to_string()));
let map_bin = parse(r#"{"name":"x","version":"1.0.0","bin":{"foo":"cli.js"}}"#);
assert_eq!(map_bin.bin.get("foo"), Some(&"cli.js".to_string()));
}
#[test]
fn bin_map_roundtrips_through_cache_serialization() {
let mut bin = BTreeMap::new();
bin.insert("semver".to_string(), "bin/semver.js".to_string());
let v = VersionMetadata {
name: "semver".to_string(),
version: "7.7.4".to_string(),
dependencies: BTreeMap::new(),
dev_dependencies: BTreeMap::new(),
peer_dependencies: BTreeMap::new(),
peer_dependencies_meta: BTreeMap::new(),
optional_dependencies: BTreeMap::new(),
bundled_dependencies: None,
dist: None,
os: Vec::new(),
cpu: Vec::new(),
libc: Vec::new(),
engines: BTreeMap::new(),
license: None,
funding_url: None,
bin,
has_install_script: false,
deprecated: None,
approver: None,
npm_user: None,
};
let json = serde_json::to_string(&v).unwrap();
let back: VersionMetadata = serde_json::from_str(&json).unwrap();
assert_eq!(
back.bin.get("semver"),
Some(&"bin/semver.js".to_string()),
"bin map must round-trip through cache serialization"
);
}
#[test]
fn missing_fields_default_to_empty() {
let v = parse(r#"{"name":"x","version":"1.0.0"}"#);
assert!(v.os.is_empty());
assert!(v.cpu.is_empty());
assert!(v.libc.is_empty());
}
#[test]
fn attestations_provenance_is_extracted() {
let v = parse(
r#"{"name":"x","version":"1.0.0",
"dist":{"tarball":"t","attestations":{"provenance":{"predicateType":"slsa"}}}}"#,
);
let dist = v.dist.expect("dist present");
let att = dist.attestations.expect("attestations present");
assert!(att.provenance.is_some(), "provenance present");
}
#[test]
fn attestations_missing_is_none() {
let v = parse(r#"{"name":"x","version":"1.0.0","dist":{"tarball":"t"}}"#);
let dist = v.dist.expect("dist present");
assert!(dist.attestations.is_none());
}
#[test]
fn npm_user_object_with_trusted_publisher_is_parsed() {
let v = parse(
r#"{"name":"x","version":"1.0.0",
"_npmUser":{"name":"u","email":"u@x","trustedPublisher":{"id":"gh"}}}"#,
);
let user = v.npm_user.expect("_npmUser present");
assert!(user.trusted_publisher.is_some());
}
#[test]
fn npm_user_object_without_trusted_publisher_is_parsed() {
let v = parse(r#"{"name":"x","version":"1.0.0","_npmUser":{"name":"u","email":"u@x"}}"#);
let user = v.npm_user.expect("_npmUser present");
assert!(user.trusted_publisher.is_none());
}
#[test]
fn npm_user_garbage_sibling_fields_still_extract_trusted_publisher() {
let v = parse(
r#"{"name":"x","version":"1.0.0",
"_npmUser":{
"trustedPublisher":{"id":"gh"},
"name":42,
"email":[null,{"deep":{"nested":"garbage"}}],
"future_field_with_strict_type":"this would fail strict deserialize"
}}"#,
);
let user = v
.npm_user
.expect("_npmUser present despite garbage siblings");
assert!(user.trusted_publisher.is_some());
}
#[test]
fn npm_user_string_form_degrades_to_none() {
let v = parse(r#"{"name":"x","version":"1.0.0","_npmUser":"isaacs <i@npmjs.com>"}"#);
assert!(v.npm_user.is_none());
}
#[test]
fn npm_user_null_or_missing_is_none() {
let v_null = parse(r#"{"name":"x","version":"1.0.0","_npmUser":null}"#);
assert!(v_null.npm_user.is_none());
let v_missing = parse(r#"{"name":"x","version":"1.0.0"}"#);
assert!(v_missing.npm_user.is_none());
}
#[test]
fn deprecated_string_is_preserved_and_false_is_empty() {
let v = parse(r#"{"name":"x","version":"1.0.0","deprecated":"use y"}"#);
assert_eq!(v.deprecated.as_deref(), Some("use y"));
let v = parse(r#"{"name":"x","version":"1.0.1","deprecated":false}"#);
assert!(v.deprecated.is_none());
}
#[test]
fn dependency_maps_drop_null_entries() {
let v = parse(
r#"{
"name": "x",
"version": "1.0.0",
"dependencies": {"kept": "^1", "stripped": null},
"devDependencies": {"dkept": "^2", "dstripped": null},
"peerDependencies": {"pkept": "^3", "pstripped": null},
"optionalDependencies": {"okept": "^4", "ostripped": null}
}"#,
);
assert_eq!(v.dependencies.len(), 1);
assert_eq!(v.dependencies["kept"], "^1");
assert_eq!(v.dev_dependencies.len(), 1);
assert_eq!(v.peer_dependencies.len(), 1);
assert_eq!(v.optional_dependencies.len(), 1);
}
#[test]
fn dependency_maps_drop_object_valued_entries() {
let v = parse(
r#"{
"name": "deep-diff",
"version": "0.1.0",
"devDependencies": {
"vows": {"version": "0.6.4", "dependencies": {"diff": {"version": "1.0.4"}}},
"extend": {"version": "1.1.1"},
"lodash": "0.9.2"
}
}"#,
);
assert_eq!(v.dev_dependencies.len(), 1);
assert_eq!(v.dev_dependencies["lodash"], "0.9.2");
}
#[test]
fn dependency_maps_null_whole_field_is_empty() {
let v = parse(
r#"{
"name": "x",
"version": "1.0.0",
"dependencies": null,
"devDependencies": null,
"peerDependencies": null,
"optionalDependencies": null
}"#,
);
assert!(v.dependencies.is_empty());
assert!(v.dev_dependencies.is_empty());
assert!(v.peer_dependencies.is_empty());
assert!(v.optional_dependencies.is_empty());
}
fn parse_packument(json: &str) -> Packument {
serde_json::from_str(json).unwrap()
}
#[test]
fn packument_dist_tags_drops_null_tag() {
let p = parse_packument(
r#"{
"name": "pkg",
"dist-tags": {"latest": "1.2.3", "beta": null}
}"#,
);
assert_eq!(p.dist_tags.len(), 1);
assert_eq!(p.dist_tags["latest"], "1.2.3");
}
#[test]
fn packument_dist_tags_null_whole_field_is_empty() {
let p = parse_packument(r#"{"name":"pkg","dist-tags":null}"#);
assert!(p.dist_tags.is_empty());
}
#[test]
fn packument_preserves_modified_timestamp() {
let p = parse_packument(
r#"{
"name": "pkg",
"modified": "2026-04-14T14:26:11.557Z"
}"#,
);
assert_eq!(p.modified.as_deref(), Some("2026-04-14T14:26:11.557Z"));
}
#[test]
fn packument_time_drops_null_entries() {
let p = parse_packument(
r#"{
"name": "pkg",
"time": {"1.0.0": "2024-01-01T00:00:00.000Z", "0.9.0": null}
}"#,
);
assert_eq!(p.time.len(), 1);
assert!(p.time.contains_key("1.0.0"));
}
#[test]
fn packument_time_null_whole_field_is_empty() {
let p = parse_packument(r#"{"name":"pkg","time":null}"#);
assert!(p.time.is_empty());
}
#[test]
fn engines_accepts_legacy_array_shape() {
let v = parse(r#"{"name":"madge","version":"0.0.1","engines":["node >= 0.8.0"]}"#);
assert!(v.engines.is_empty());
}
#[test]
fn engines_accepts_legacy_string_shape() {
let v = parse(r#"{"name":"qs","version":"0.6.0","engines":"node >= 0.4.0"}"#);
assert!(v.engines.is_empty());
}
#[test]
fn engines_accepts_map_shape() {
let v = parse(r#"{"name":"x","version":"1.0.0","engines":{"node":">=18"}}"#);
assert_eq!(v.engines.get("node"), Some(&">=18".to_string()));
}
#[test]
fn engines_null_is_empty() {
let v = parse(r#"{"name":"x","version":"1.0.0","engines":null}"#);
assert!(v.engines.is_empty());
}
#[test]
fn bundled_deps_accepts_both_canonical_and_alias() {
let v = parse(
r#"{
"name":"x","version":"1.0.0",
"bundledDependencies":["canonical"],
"bundleDependencies":["legacy"]
}"#,
);
let deps = BTreeMap::new();
let names = v.bundled_dependencies.as_ref().unwrap().names(&deps);
assert_eq!(names, vec!["canonical"]);
}
#[test]
fn bundled_deps_falls_back_to_alias_only() {
let v = parse(r#"{"name":"x","version":"1.0.0","bundleDependencies":["legacy"]}"#);
let deps = BTreeMap::new();
let names = v.bundled_dependencies.as_ref().unwrap().names(&deps);
assert_eq!(names, vec!["legacy"]);
}
}