use super::raw::{
RawCatalogEntry, RawDepSpec, RawImporter, RawPackageInfo, RawPatchedDependency,
RawPnpmLockfile, RawSettings, RawSnapshot,
};
use serde::Deserialize;
use std::collections::BTreeMap;
pub(super) fn try_parse(content: &str) -> Option<RawPnpmLockfile> {
if has_document_separator(content) {
return None;
}
let mut p = Parser::new(content.as_bytes());
p.parse()
}
fn has_document_separator(content: &str) -> bool {
content.as_bytes().split(|&b| b == b'\n').any(|line| {
let line = line.strip_suffix(b"\r").unwrap_or(line);
match line.strip_prefix(b"---") {
None => false,
Some(rest) => rest.is_empty() || matches!(rest[0], b' ' | b'\t' | b'#'),
}
})
}
struct Parser<'a> {
bytes: &'a [u8],
pos: usize,
}
struct Line<'a> {
indent: usize,
body: &'a [u8],
start: usize,
}
impl<'a> Parser<'a> {
fn new(bytes: &'a [u8]) -> Self {
Parser { bytes, pos: 0 }
}
fn peek_line(&self) -> Option<Line<'a>> {
let mut pos = self.pos;
while pos < self.bytes.len() {
let nl = memchr::memchr(b'\n', &self.bytes[pos..])
.map(|i| pos + i)
.unwrap_or(self.bytes.len());
let mut raw = &self.bytes[pos..nl];
if raw.last() == Some(&b'\r') {
raw = &raw[..raw.len() - 1];
}
let indent = raw.iter().take_while(|&&b| b == b' ').count();
let body = &raw[indent..];
if body.is_empty() || body[0] == b'#' {
pos = nl + 1;
continue;
}
return Some(Line {
indent,
body,
start: pos,
});
}
None
}
fn consume_line(&mut self, line: &Line<'a>) {
let nl = memchr::memchr(b'\n', &self.bytes[line.start..])
.map(|i| line.start + i + 1)
.unwrap_or(self.bytes.len());
self.pos = nl;
}
fn parse(&mut self) -> Option<RawPnpmLockfile> {
let mut lockfile_version: Option<yaml_serde::Value> = None;
let mut settings = None;
let mut overrides = None;
let mut package_extensions_checksum = None;
let mut pnpmfile_checksum = None;
let mut catalogs = None;
let mut patched_dependencies = None;
let mut ignored_optional_dependencies = None;
let mut importers = BTreeMap::new();
let mut packages = BTreeMap::new();
let mut snapshots = BTreeMap::new();
let mut time = None;
while let Some(line) = self.peek_line() {
if line.indent != 0 {
return None;
}
let (key, inline) = split_key(line.body)?;
self.consume_line(&line);
match key {
b"lockfileVersion" => {
lockfile_version = Some(parse_scalar_value(inline?)?);
}
b"packageExtensionsChecksum" => {
package_extensions_checksum = Some(scalar_string(inline?)?);
}
b"pnpmfileChecksum" => {
pnpmfile_checksum = Some(scalar_string(inline?)?);
}
b"settings" => {
if inline.is_some() {
return None;
}
settings = Some(self.parse_via_serde::<RawSettings>(2)?);
}
b"overrides" => {
overrides = Some(self.parse_string_map(inline, 2)?);
}
b"catalogs" => {
if inline.is_some() {
return None;
}
catalogs = Some(self.parse_catalogs()?);
}
b"patchedDependencies" => {
if inline.is_some() {
return None;
}
patched_dependencies =
Some(self.parse_via_serde::<BTreeMap<String, RawPatchedDependency>>(2)?);
}
b"ignoredOptionalDependencies" => {
ignored_optional_dependencies = Some(self.parse_seq_value(inline, 2)?);
}
b"importers" => {
if inline.is_some() {
return None;
}
importers = self.parse_importers()?;
}
b"packages" => {
if inline.is_some() {
return None;
}
packages = self.parse_packages()?;
}
b"snapshots" => {
if inline.is_some() {
return None;
}
snapshots = self.parse_snapshots()?;
}
b"time" => {
time = Some(self.parse_string_map(inline, 2)?);
}
_ => return None,
}
}
Some(RawPnpmLockfile {
lockfile_version: lockfile_version?,
settings,
overrides,
package_extensions_checksum,
pnpmfile_checksum,
catalogs,
patched_dependencies,
ignored_optional_dependencies,
importers,
packages,
snapshots,
time,
})
}
fn parse_via_serde<T: for<'de> Deserialize<'de>>(&mut self, min_indent: usize) -> Option<T> {
let block = self.take_block(min_indent)?;
let dedented = dedent(&block, min_indent)?;
yaml_serde::from_str::<T>(&dedented).ok()
}
fn take_block(&mut self, min_indent: usize) -> Option<String> {
let start = self.pos;
let mut end = self.pos;
while let Some(line) = self.peek_line() {
if line.indent < min_indent {
break;
}
self.consume_line(&line);
end = self.pos;
}
std::str::from_utf8(&self.bytes[start..end])
.ok()
.map(|s| s.to_string())
}
fn parse_string_map(
&mut self,
inline: Option<&[u8]>,
min_indent: usize,
) -> Option<BTreeMap<String, String>> {
if inline.is_some() {
return None;
}
let mut map = BTreeMap::new();
while let Some(line) = self.peek_line() {
if line.indent < min_indent {
break;
}
if line.indent != min_indent {
return None;
}
let (k, v) = split_key(line.body)?;
let v = v?; self.consume_line(&line);
map.insert(scalar_key_string(k)?, scalar_string(v)?);
}
if map.is_empty() {
return None;
}
Some(map)
}
fn parse_seq_value(&mut self, inline: Option<&[u8]>, min_indent: usize) -> Option<Vec<String>> {
if let Some(v) = inline {
return serde_value_from_fragment(v);
}
let block = self.take_block(min_indent)?;
let dedented = dedent(&block, min_indent)?;
yaml_serde::from_str::<Vec<String>>(&dedented).ok()
}
fn parse_catalogs(&mut self) -> Option<BTreeMap<String, BTreeMap<String, RawCatalogEntry>>> {
let block = self.take_block(2)?;
let dedented = dedent(&block, 2)?;
yaml_serde::from_str(&dedented).ok()
}
fn parse_importers(&mut self) -> Option<BTreeMap<String, RawImporter>> {
let mut importers = BTreeMap::new();
while let Some(line) = self.peek_line() {
if line.indent < 2 {
break;
}
if line.indent != 2 {
return None;
}
let (name, rest) = split_key(line.body)?;
let name = scalar_key_string(name)?;
self.consume_line(&line);
let imp = if let Some(inline) = rest {
if inline == b"{}" {
default_importer()
} else {
return None;
}
} else {
self.parse_importer_body()?
};
importers.insert(name, imp);
}
Some(importers)
}
fn parse_importer_body(&mut self) -> Option<RawImporter> {
let mut dependencies = None;
let mut dev_dependencies = None;
let mut optional_dependencies = None;
let mut skipped_optional_dependencies = None;
while let Some(line) = self.peek_line() {
if line.indent < 4 {
break;
}
if line.indent != 4 {
return None;
}
let (key, rest) = split_key(line.body)?;
if rest.is_some() {
return None;
}
self.consume_line(&line);
let specs = self.parse_dep_specs(6)?;
match key {
b"dependencies" => dependencies = Some(specs),
b"devDependencies" => dev_dependencies = Some(specs),
b"optionalDependencies" => optional_dependencies = Some(specs),
b"skippedOptionalDependencies" => skipped_optional_dependencies = Some(specs),
_ => return None,
}
}
Some(RawImporter {
dependencies,
dev_dependencies,
optional_dependencies,
skipped_optional_dependencies,
})
}
fn parse_dep_specs(&mut self, entry_indent: usize) -> Option<BTreeMap<String, RawDepSpec>> {
let mut map = BTreeMap::new();
while let Some(line) = self.peek_line() {
if line.indent < entry_indent {
break;
}
if line.indent != entry_indent {
return None;
}
let (name, rest) = split_key(line.body)?;
if rest.is_some() {
return None;
}
let name = scalar_key_string(name)?;
self.consume_line(&line);
let mut specifier = None;
let mut version = None;
let child_indent = entry_indent + 2;
while let Some(c) = self.peek_line() {
if c.indent < child_indent {
break;
}
if c.indent != child_indent {
return None;
}
let (ck, cv) = split_key(c.body)?;
let cv = cv?;
self.consume_line(&c);
match ck {
b"specifier" => specifier = Some(scalar_string(cv)?),
b"version" => version = Some(scalar_string(cv)?),
_ => return None,
}
}
map.insert(
name,
RawDepSpec {
specifier: specifier?,
version: version?,
},
);
}
if map.is_empty() {
return None;
}
Some(map)
}
fn parse_packages(&mut self) -> Option<BTreeMap<String, RawPackageInfo>> {
let mut map = BTreeMap::new();
while let Some(line) = self.peek_line() {
if line.indent < 2 {
break;
}
if line.indent != 2 {
return None;
}
let (key, rest) = split_key(line.body)?;
let name = scalar_key_string(key)?;
self.consume_line(&line);
let info = if let Some(inline) = rest {
if inline == b"{}" {
default_package_info()
} else {
return None;
}
} else {
self.parse_package_body(4)?
};
map.insert(name, info);
}
Some(map)
}
fn parse_package_body(&mut self, indent: usize) -> Option<RawPackageInfo> {
let block = self.take_block(indent)?;
if block.is_empty() {
return Some(default_package_info());
}
let dedented = dedent(&block, indent)?;
yaml_serde::from_str::<RawPackageInfo>(&dedented).ok()
}
fn parse_snapshots(&mut self) -> Option<BTreeMap<String, RawSnapshot>> {
let mut map = BTreeMap::new();
while let Some(line) = self.peek_line() {
if line.indent < 2 {
break;
}
if line.indent != 2 {
return None;
}
let (key, rest) = split_key(line.body)?;
let name = scalar_key_string(key)?;
self.consume_line(&line);
let snap = if let Some(inline) = rest {
if inline == b"{}" {
default_snapshot()
} else {
return None;
}
} else {
self.parse_snapshot_body(4)?
};
map.insert(name, snap);
}
Some(map)
}
fn parse_snapshot_body(&mut self, indent: usize) -> Option<RawSnapshot> {
let mut dependencies = None;
let mut optional_dependencies = None;
let mut bundled_dependencies = None;
let mut optional = None;
let mut transitive_peer_dependencies = None;
while let Some(line) = self.peek_line() {
if line.indent < indent {
break;
}
if line.indent != indent {
return None;
}
let (key, rest) = split_key(line.body)?;
self.consume_line(&line);
match key {
b"dependencies" => {
dependencies = Some(self.parse_string_map(rest, indent + 2)?);
}
b"optionalDependencies" => {
optional_dependencies = Some(self.parse_string_map(rest, indent + 2)?);
}
b"optional" => {
optional = Some(parse_bool(rest?)?);
}
b"bundledDependencies" => {
bundled_dependencies = Some(self.parse_seq_value(rest, indent + 2)?);
}
b"transitivePeerDependencies" => {
transitive_peer_dependencies = Some(self.parse_seq_value(rest, indent + 2)?);
}
_ => return None,
}
}
Some(RawSnapshot {
dependencies,
optional_dependencies,
bundled_dependencies,
optional,
transitive_peer_dependencies,
})
}
}
fn default_package_info() -> RawPackageInfo {
RawPackageInfo::default()
}
fn default_importer() -> RawImporter {
RawImporter {
dependencies: None,
dev_dependencies: None,
optional_dependencies: None,
skipped_optional_dependencies: None,
}
}
fn default_snapshot() -> RawSnapshot {
RawSnapshot {
dependencies: None,
optional_dependencies: None,
bundled_dependencies: None,
optional: None,
transitive_peer_dependencies: None,
}
}
fn split_key(body: &[u8]) -> Option<(&[u8], Option<&[u8]>)> {
let mut i = 0;
let mut in_single = false;
let mut in_double = false;
while i < body.len() {
let b = body[i];
match b {
b'\'' if !in_double => in_single = !in_single,
b'"' if !in_single => in_double = !in_double,
b':' if !in_single && !in_double => {
let next = body.get(i + 1);
if next.is_none() {
return Some((&body[..i], None));
}
if next == Some(&b' ') {
let val = &body[i + 2..];
let val = trim_trailing_ws(val);
return Some((&body[..i], Some(val)));
}
}
_ => {}
}
i += 1;
}
None
}
fn trim_trailing_ws(b: &[u8]) -> &[u8] {
let mut end = b.len();
while end > 0 && (b[end - 1] == b' ' || b[end - 1] == b'\t') {
end -= 1;
}
&b[..end]
}
fn scalar_key_string(raw: &[u8]) -> Option<String> {
scalar_string(raw)
}
fn scalar_string(raw: &[u8]) -> Option<String> {
if raw.is_empty() {
return Some(String::new());
}
if raw[0] == b'\'' {
if raw.len() < 2 || raw[raw.len() - 1] != b'\'' {
return None;
}
let inner = &raw[1..raw.len() - 1];
let s = std::str::from_utf8(inner).ok()?;
return Some(s.replace("''", "'"));
}
if raw[0] == b'"' {
if raw.len() < 2 || raw[raw.len() - 1] != b'"' {
return None;
}
let inner = &raw[1..raw.len() - 1];
if inner.contains(&b'\\') {
return None;
}
return std::str::from_utf8(inner).ok().map(|s| s.to_string());
}
if raw[0] == b'{' || raw[0] == b'[' || raw[0] == b'&' || raw[0] == b'*' {
return None;
}
let end = strip_inline_comment(raw);
if end == 0 {
return None;
}
std::str::from_utf8(&raw[..end]).ok().map(|s| s.to_string())
}
fn strip_inline_comment(raw: &[u8]) -> usize {
let mut i = 0;
while i < raw.len() {
if raw[i] == b'#' && (i == 0 || raw[i - 1] == b' ' || raw[i - 1] == b'\t') {
let mut end = i;
while end > 0 && (raw[end - 1] == b' ' || raw[end - 1] == b'\t') {
end -= 1;
}
return end;
}
i += 1;
}
raw.len()
}
fn parse_scalar_value(raw: &[u8]) -> Option<yaml_serde::Value> {
let s = std::str::from_utf8(raw).ok()?;
yaml_serde::from_str::<yaml_serde::Value>(s).ok()
}
fn parse_bool(raw: &[u8]) -> Option<bool> {
match raw {
b"true" => Some(true),
b"false" => Some(false),
_ => None,
}
}
fn serde_value_from_fragment(raw: &[u8]) -> Option<Vec<String>> {
let s = std::str::from_utf8(raw).ok()?;
yaml_serde::from_str::<Vec<String>>(s).ok()
}
fn dedent(block: &str, n: usize) -> Option<String> {
let mut out = String::with_capacity(block.len());
for line in block.split_inclusive('\n') {
let trimmed = line.trim_end_matches(['\n', '\r']);
if trimmed.is_empty() {
out.push('\n');
continue;
}
let spaces = trimmed.bytes().take_while(|&b| b == b' ').count();
if spaces < n {
return None;
}
out.push_str(&line[n..]);
}
Some(out)
}
#[cfg(test)]
mod subset_tests {
use super::super::raw::{SubsetDiff, diff_subset_vs_serde};
use super::*;
const NATIVE: &str = include_str!("../../tests/fixtures/pnpm-native.yaml");
const KITCHEN_SINK: &str = include_str!("../../tests/fixtures/pnpm-kitchen-sink.yaml");
const V6: &str = include_str!("../../tests/fixtures/pnpm-v6.yaml");
const GIT_RESOLUTION: &str = include_str!("../../tests/fixtures/pnpm-git-resolution.yaml");
const V5_3_PLAIN: &str = include_str!("../../tests/fixtures/pnpm-v5_3-plain.yaml");
const V5_4_PLAIN: &str = include_str!("../../tests/fixtures/pnpm-v5_4-plain.yaml");
const V6_TOPLEVEL: &str = include_str!("../../tests/fixtures/pnpm-v6-toplevel-deps.yaml");
const V6_WORKSPACE: &str = include_str!("../../tests/fixtures/pnpm-v6-workspace.yaml");
const V9_EXOTIC: &str = include_str!("../../tests/fixtures/pnpm-v9-exotic.yaml");
const V9_WS_CATALOG: &str = include_str!("../../tests/fixtures/pnpm-v9-workspace-catalog.yaml");
fn assert_match_or_declines(name: &str, content: &str) {
match diff_subset_vs_serde(content) {
SubsetDiff::Match | SubsetDiff::Declined => {}
SubsetDiff::Divergence { subset, serde } => {
panic!(
"SILENT WRONG PARSE in `{name}`: subset fast path diverged from serde.\n\
--- subset ---\n{subset}\n--- serde ---\n{serde}"
);
}
}
}
fn assert_fires_and_matches(name: &str, content: &str) {
match diff_subset_vs_serde(content) {
SubsetDiff::Match => {}
SubsetDiff::Declined => {
panic!("`{name}`: expected the subset fast path to fire, but it declined")
}
SubsetDiff::Divergence { subset, serde } => {
panic!(
"SILENT WRONG PARSE in `{name}`: subset diverged from serde.\n\
--- subset ---\n{subset}\n--- serde ---\n{serde}"
);
}
}
}
#[test]
fn differential_committed_fixtures() {
assert_fires_and_matches("pnpm-native", NATIVE);
assert_fires_and_matches("pnpm-kitchen-sink", KITCHEN_SINK);
assert_match_or_declines("pnpm-v6", V6);
assert_fires_and_matches("pnpm-git-resolution", GIT_RESOLUTION);
}
#[test]
fn cross_version_corpus_matches_or_declines() {
assert_match_or_declines("pnpm-v5_3-plain", V5_3_PLAIN);
assert_match_or_declines("pnpm-v5_4-plain", V5_4_PLAIN);
assert_match_or_declines("pnpm-v6-toplevel-deps", V6_TOPLEVEL);
assert_match_or_declines("pnpm-v6-workspace", V6_WORKSPACE);
assert_match_or_declines("pnpm-v9-exotic", V9_EXOTIC);
assert_match_or_declines("pnpm-v9-workspace-catalog", V9_WS_CATALOG);
}
#[test]
fn pre_v9_top_level_dep_formats_decline_never_misparse() {
assert!(try_parse(V5_3_PLAIN).is_none(), "lv5.3 must decline");
assert!(try_parse(V5_4_PLAIN).is_none(), "lv5.4 must decline");
assert!(
try_parse(V6_TOPLEVEL).is_none(),
"lv6 top-level deps must decline"
);
}
#[test]
fn v9_git_tarball_alias_keys_fire_and_match() {
assert_fires_and_matches("pnpm-v9-exotic", V9_EXOTIC);
let raw = try_parse(V9_EXOTIC).unwrap();
assert!(
raw.packages
.keys()
.any(|k| k.contains("codeload.github.com"))
);
}
#[test]
fn fast_path_fires_on_native_fixture() {
let raw = try_parse(NATIVE).expect("subset parser should accept native pnpm fixture");
assert_eq!(raw.packages.len(), 8);
assert_eq!(raw.snapshots.len(), 8);
assert_eq!(raw.importers.len(), 1);
}
#[test]
fn kitchen_sink_fires_and_models_every_section() {
let raw = try_parse(KITCHEN_SINK).expect("kitchen-sink should be accepted");
assert_eq!(raw.importers.len(), 3);
assert!(raw.catalogs.is_some());
assert!(raw.overrides.is_some());
assert!(raw.patched_dependencies.is_some());
assert_eq!(raw.packages.len(), 9);
assert_eq!(raw.snapshots.len(), 9);
}
#[test]
fn empty_importer_inline_map_fires_and_matches() {
let lock = "lockfileVersion: '9.0'\nimporters:\n .:\n dependencies:\n a:\n specifier: ^1\n version: 1.0.0\n packages/empty: {}\n packages/also-empty: {}\n";
assert_fires_and_matches("empty-importer", lock);
let raw = try_parse(lock).unwrap();
assert_eq!(raw.importers.len(), 3);
let empty = &raw.importers["packages/empty"];
assert!(empty.dependencies.is_none());
assert!(empty.dev_dependencies.is_none());
}
#[test]
fn inline_flow_map_fields_decline_not_misparse() {
for content in [
"lockfileVersion: '9.0'\noverrides: {foo: bar}\n",
"lockfileVersion: '9.0'\ntime: {foo@1.0.0: '2020-01-01'}\n",
"lockfileVersion: '9.0'\nsettings: {autoInstallPeers: true}\n",
"lockfileVersion: '9.0'\npatchedDependencies: {foo@1.0.0: {path: a, hash: b}}\n",
"lockfileVersion: '9.0'\ncatalogs: {default: {react: {specifier: ^18, version: 18.0.0}}}\n",
"lockfileVersion: '9.0'\nimporters: {.: {dependencies: {a: {specifier: ^1, version: 1.0.0}}}}\n",
] {
assert!(
try_parse(content).is_none(),
"inline flow map must decline, not misparse: {content:?}"
);
assert_match_or_declines("inline-flow-map", content);
}
}
#[test]
fn empty_block_map_declines_to_distinguish_null_from_empty() {
let null_overrides = "lockfileVersion: '9.0'\noverrides:\n";
assert!(try_parse(null_overrides).is_none());
assert_match_or_declines("null-overrides", null_overrides);
let snap_empty_deps = "lockfileVersion: '9.0'\nsnapshots:\n foo@1.0.0:\n dependencies:\n optional: true\n";
assert!(try_parse(snap_empty_deps).is_none());
assert_match_or_declines("snap-empty-deps", snap_empty_deps);
}
#[test]
fn empty_importer_dependencies_block_declines_not_misparse() {
let lock = "lockfileVersion: '9.0'\nimporters:\n .:\n dependencies:\n devDependencies:\n a:\n specifier: ^1\n version: 1.0.0\n";
assert!(try_parse(lock).is_none());
assert_match_or_declines("empty-importer-dependencies", lock);
}
#[test]
fn default_package_info_matches_serde_empty_map() {
let by_default = format!("{:#?}", default_package_info());
let by_serde = format!(
"{:#?}",
yaml_serde::from_str::<RawPackageInfo>("{}").unwrap()
);
assert_eq!(by_default, by_serde);
}
#[test]
fn block_string_map_still_fires() {
let block = "lockfileVersion: '9.0'\noverrides:\n foo: bar\n baz: '^1.0.0'\n";
assert_fires_and_matches("block-overrides", block);
}
#[test]
fn populated_block_forms_still_fire_per_field() {
let cases: &[(&str, &str)] = &[
(
"overrides-block",
"lockfileVersion: '9.0'\noverrides:\n foo: '1'\n",
),
(
"time-block",
"lockfileVersion: '9.0'\ntime:\n foo@1.0.0: '2020-01-01T00:00:00.000Z'\n",
),
(
"settings-block",
"lockfileVersion: '9.0'\nsettings:\n autoInstallPeers: true\n excludeLinksFromLockfile: false\n",
),
(
"patchedDependencies-block",
"lockfileVersion: '9.0'\npatchedDependencies:\n foo@1.0.0:\n path: patches/foo.patch\n hash: abc123\n",
),
(
"catalogs-block",
"lockfileVersion: '9.0'\ncatalogs:\n default:\n react:\n specifier: ^18\n version: 18.0.0\n",
),
(
"importers-block",
"lockfileVersion: '9.0'\nimporters:\n .:\n dependencies:\n a:\n specifier: ^1\n version: 1.0.0\n",
),
(
"snapshot-optionalDeps-block",
"lockfileVersion: '9.0'\nsnapshots:\n foo@1.0.0:\n optionalDependencies:\n bar: 2.0.0\n optional: true\n",
),
];
for (name, content) in cases {
assert_fires_and_matches(name, content);
}
}
#[test]
fn map_field_null_empty_populated_track_serde() {
let populated = "lockfileVersion: '9.0'\noverrides:\n foo: bar\n";
assert_fires_and_matches("overrides-populated", populated);
for empty in [
"lockfileVersion: '9.0'\noverrides:\n",
"lockfileVersion: '9.0'\noverrides: {}\n",
] {
assert!(
try_parse(empty).is_none(),
"empty/inline overrides must decline: {empty:?}"
);
assert_match_or_declines("overrides-empty", empty);
}
}
#[test]
fn snapshot_dep_value_with_peer_parens_keeps_full_value() {
let lock = "lockfileVersion: '9.0'\nsnapshots:\n foo@1.0.0:\n dependencies:\n bar: 2.0.0(baz@3.0.0)\n";
assert_fires_and_matches("dep-value-peer-parens", lock);
}
#[test]
fn scoped_peer_key_fires_and_matches() {
let lock = "lockfileVersion: '9.0'\nsnapshots:\n '@foo/bar@1.0.0(@types/node@20.0.0)':\n dependencies:\n '@types/node': 20.0.0\n";
assert_fires_and_matches("scoped-peer-key", lock);
}
#[test]
fn numeric_and_bool_looking_string_values_match_serde() {
let lock = "lockfileVersion: '9.0'\noverrides:\n a: 1.5\n b: 10\n c: true\n d: null\n";
assert_fires_and_matches("numeric-string-values", lock);
}
#[test]
fn quoted_bool_for_optional_declines_not_misparses() {
let lock = "lockfileVersion: '9.0'\nsnapshots:\n foo@1.0.0:\n optional: 'true'\n";
assert!(try_parse(lock).is_none());
assert_match_or_declines("quoted-optional", lock);
}
#[test]
fn multiline_block_scalar_declines() {
let lock = "lockfileVersion: '9.0'\noverrides:\n foo: |\n multi\n line\n";
assert!(try_parse(lock).is_none());
assert_match_or_declines("literal-scalar", lock);
}
#[test]
fn declines_multi_document_stream() {
let two_docs = "lockfileVersion: '9.0'\npackages: {}\n---\nlockfileVersion: '9.0'\nimporters:\n .:\n dependencies: {}\n";
assert!(try_parse(two_docs).is_none());
}
#[test]
fn declines_unknown_top_level_key() {
let with_unknown = "lockfileVersion: '9.0'\nfutureField: 1\n";
assert!(try_parse(with_unknown).is_none());
}
#[test]
fn declines_tab_indentation_rather_than_misparsing() {
let tabbed = "lockfileVersion: '9.0'\nimporters:\n\t.:\n\t\tdependencies: {}\n";
assert!(try_parse(tabbed).is_none());
assert_match_or_declines("tab-indent", tabbed);
}
#[test]
fn declines_empty_input() {
assert!(try_parse("").is_none());
assert!(try_parse("\n\n").is_none());
assert!(try_parse("# just a comment\n").is_none());
}
#[test]
fn importer_only_no_packages_fires() {
let lock = "lockfileVersion: '9.0'\nimporters:\n .:\n dependencies:\n a:\n specifier: ^1\n version: 1.0.0\n";
assert_fires_and_matches("importer-only", lock);
let raw = try_parse(lock).unwrap();
assert!(raw.packages.is_empty());
assert!(raw.snapshots.is_empty());
}
#[test]
fn crlf_line_endings_parse_identically() {
let lf = "lockfileVersion: '9.0'\nimporters:\n .:\n dependencies:\n a:\n specifier: ^1\n version: 1.0.0\n";
let crlf = lf.replace('\n', "\r\n");
assert_fires_and_matches("crlf", &crlf);
let a = format!("{:#?}", try_parse(lf).unwrap());
let b = format!("{:#?}", try_parse(&crlf).unwrap());
assert_eq!(a, b, "CRLF and LF must parse to the same structure");
}
#[test]
fn comments_and_blank_lines_are_skipped() {
let lock = "lockfileVersion: '9.0'\n\n# a comment\nimporters:\n\n # importer comment\n .:\n dependencies:\n a:\n specifier: ^1\n version: 1.0.0\n";
assert_fires_and_matches("comments", lock);
}
#[test]
fn values_containing_colons_keep_the_full_value() {
let lock = "lockfileVersion: '9.0'\noverrides:\n pkg: 'git+ssh://git@github.com/foo/bar.git#v1'\n url: 'https://example.com/path:8080/x'\n";
assert_fires_and_matches("colon-values", lock);
let raw = try_parse(lock).unwrap();
let ov = raw.overrides.unwrap();
assert_eq!(ov["pkg"], "git+ssh://git@github.com/foo/bar.git#v1");
assert_eq!(ov["url"], "https://example.com/path:8080/x");
}
#[test]
fn split_key_respects_quoted_colons() {
let (k, v) = split_key(b"'a:b': value").unwrap();
assert_eq!(k, b"'a:b'");
assert_eq!(v, Some(&b"value"[..]));
}
#[test]
fn split_key_trailing_colon_has_no_value() {
let (k, v) = split_key(b"importers:").unwrap();
assert_eq!(k, b"importers");
assert_eq!(v, None);
}
#[test]
fn split_key_bails_without_structural_colon() {
assert!(split_key(b"justtext").is_none());
assert!(split_key(b"a:b").is_none());
}
#[test]
fn scalar_string_handles_quote_styles() {
assert_eq!(scalar_string(b"plain").unwrap(), "plain");
assert_eq!(scalar_string(b"'quoted'").unwrap(), "quoted");
assert_eq!(scalar_string(b"\"double\"").unwrap(), "double");
assert_eq!(scalar_string(b"'it''s'").unwrap(), "it's");
assert_eq!(scalar_string(b"").unwrap(), "");
}
#[test]
fn scalar_string_bails_on_backslash_escapes_and_flow() {
assert!(scalar_string(b"\"a\\tb\"").is_none());
assert!(scalar_string(b"'open").is_none());
assert!(scalar_string(b"\"open").is_none());
assert!(scalar_string(b"{inline: map}").is_none());
assert!(scalar_string(b"[a, b]").is_none());
assert!(scalar_string(b"&anchor").is_none());
assert!(scalar_string(b"*alias").is_none());
}
#[test]
fn plain_scalar_strips_inline_comment_quoted_keeps_hash() {
assert_eq!(scalar_string(b"bar # keep this").unwrap(), "bar");
assert_eq!(scalar_string(b"bar\t# c").unwrap(), "bar");
assert_eq!(scalar_string(b"bar baz # c").unwrap(), "bar baz");
assert_eq!(scalar_string(b"bar#baz").unwrap(), "bar#baz");
assert!(scalar_string(b"# x").is_none());
assert!(scalar_string(b" # x").is_none());
assert_eq!(
scalar_string(b"\"bar # keep this\"").unwrap(),
"bar # keep this"
);
assert_eq!(
scalar_string(b"'bar # keep this'").unwrap(),
"bar # keep this"
);
let lock = "lockfileVersion: '9.0'\n\nimporters:\n\n .:\n dependencies:\n a:\n specifier: ^1 # range\n version: 1.0.0 # pin\n";
assert!(matches!(
diff_subset_vs_serde(lock),
SubsetDiff::Match | SubsetDiff::Declined
));
let parsed = try_parse(lock).expect("subset should accept");
let importer = parsed.importers.get(".").expect("root importer");
let deps = importer.dependencies.as_ref().expect("dependencies block");
let dep = deps.get("a").expect("dep a");
assert_eq!(dep.version, "1.0.0");
assert_eq!(dep.specifier, "^1");
}
#[test]
fn trailing_whitespace_on_a_value_is_trimmed_consistently() {
let lock = "lockfileVersion: '9.0'\noverrides:\n pkg: ^1.0.0 \n";
assert_match_or_declines("trailing-ws", lock);
}
use proptest::prelude::*;
#[derive(Debug, Clone)]
enum FieldVal {
Null,
Empty,
Populated(Vec<(String, String)>),
}
fn plain_token() -> impl Strategy<Value = String> {
"[a-z][a-z0-9]{0,7}"
}
fn field_val() -> impl Strategy<Value = FieldVal> {
prop_oneof![
1 => Just(FieldVal::Null),
1 => Just(FieldVal::Empty),
3 => prop::collection::vec((plain_token(), plain_token()), 1..4)
.prop_map(FieldVal::Populated),
]
}
fn logical_lock() -> impl Strategy<Value = (FieldVal, FieldVal)> {
(field_val(), field_val())
}
fn render_block(key: &str, val: &FieldVal, out: &mut String) {
match val {
FieldVal::Null | FieldVal::Empty => {}
FieldVal::Populated(entries) => {
out.push_str(key);
out.push_str(":\n");
for (k, v) in entries {
out.push_str(" ");
out.push_str(k);
out.push_str(": ");
out.push_str(v);
out.push('\n');
}
}
}
}
fn render_inline(key: &str, val: &FieldVal, out: &mut String) {
match val {
FieldVal::Null => {
out.push_str(key);
out.push_str(":\n");
}
FieldVal::Empty => {
out.push_str(key);
out.push_str(": {}\n");
}
FieldVal::Populated(entries) => {
out.push_str(key);
out.push_str(": {");
let parts: Vec<String> = entries.iter().map(|(k, v)| format!("{k}: {v}")).collect();
out.push_str(&parts.join(", "));
out.push_str("}\n");
}
}
}
proptest! {
#![proptest_config(ProptestConfig {
cases: 256,
// Fixed seed: deterministic CI, reproducible failures.
rng_algorithm: proptest::test_runner::RngAlgorithm::ChaCha,
..ProptestConfig::default()
})]
#[test]
fn inline_vs_block_never_diverges((overrides, time) in logical_lock()) {
let mut block = String::from("lockfileVersion: '9.0'\n");
render_block("overrides", &overrides, &mut block);
render_block("time", &time, &mut block);
assert_match_or_declines("fuzz-block", &block);
let mut inline = String::from("lockfileVersion: '9.0'\n");
render_inline("overrides", &overrides, &mut inline);
render_inline("time", &time, &mut inline);
assert_match_or_declines("fuzz-inline", &inline);
}
}
#[test]
#[ignore]
fn differential_real_corpus() {
let dir = match std::env::var("AUBE_PNPM_CORPUS") {
Ok(d) if !d.is_empty() => d,
_ => {
eprintln!("AUBE_PNPM_CORPUS not set; skipping");
return;
}
};
let (mut total, mut matched, mut declined) = (0usize, 0usize, 0usize);
let mut diverged = Vec::new();
for entry in std::fs::read_dir(&dir).expect("corpus dir readable") {
let p = entry.unwrap().path();
if !p.is_file() {
continue;
}
let Ok(content) = std::fs::read_to_string(&p) else {
continue;
};
total += 1;
let name = p.file_name().and_then(|n| n.to_str()).unwrap_or("?");
match diff_subset_vs_serde(&content) {
SubsetDiff::Match => {
matched += 1;
eprintln!("MATCH {name}");
}
SubsetDiff::Declined => {
declined += 1;
eprintln!("DECLINE {name}");
}
SubsetDiff::Divergence { subset, serde } => {
eprintln!("DIVERGE {name}");
diverged.push((p.clone(), subset, serde))
}
}
}
eprintln!(
"corpus={total} match={matched} declined={declined} diverge={}",
diverged.len()
);
for (p, s, d) in &diverged {
eprintln!("=== DIVERGENCE {} ===", p.display());
for (i, (a, b)) in s.lines().zip(d.lines()).enumerate() {
if a != b {
eprintln!(" first diff at line {i}:\n subset: {a}\n serde : {b}");
break;
}
}
}
assert!(
diverged.is_empty(),
"{} divergence(s) found",
diverged.len()
);
}
}