Skip to content

Commit

Permalink
Fix yarn lockfile with unknown resolvers (#345)
Browse files Browse the repository at this point in the history
The yarn lockfile allows various different resolvers other than the
standard one which pulls from npm. Some of these include the filesystem
(@workspace), http/s (@http/s), and ssh (@ssh).

Instead of complex splitting logic, the parser now makes use of the fact
that dependencies inclued at most one `@` at the beginning of their
name, thus clearly identifying the resolver as anything past the second
`@`.

The supported resolvers are now explicitly handled by checking the
format of the resolution's resolver, an unrecognized resolver will cause
an error.

Closes #344.
  • Loading branch information
cd-work authored May 5, 2022
1 parent ebc6789 commit db3e281
Show file tree
Hide file tree
Showing 2 changed files with 81 additions and 20 deletions.
87 changes: 67 additions & 20 deletions cli/src/lockfiles/javascript.rs
Original file line number Diff line number Diff line change
Expand Up @@ -100,32 +100,65 @@ impl Parseable for YarnLock {
.get(&"resolution".into())
.and_then(YamlValue::as_str)
.filter(|s| !s.is_empty())
.ok_or_else(|| anyhow!("Failed to parse resolution field in yarn lock file"))?;
.ok_or_else(|| anyhow!("Failed to parse yarn resolution field"))?;

let (name, mut resolver) = match resolution[1..].split_once('@') {
Some((name, resolver)) => (&resolution[..name.len() + 1], resolver.to_owned()),
None => {
return Err(anyhow!(
"Failed to parse yarn resolution field for '{}'",
resolution
))
}
};

// Ignore workspace-local dependencies like project itself ("project@workspace:.").
if resolution[1..].contains("@workspace:") {
continue;
// Extract original resolver from patch.
if let Some((_, patch)) = resolver.split_once("patch:") {
// Exctract resolver from `@scope/package@RESOLVER#patch`.
let patch = patch[1..].split_once('@');
let subresolver = patch.and_then(|(_, resolver)| resolver.split_once('#'));
resolver = match subresolver {
Some((resolver, _)) => resolver.to_owned(),
None => {
return Err(anyhow!(
"Failed to parse yarn patch dependency for '{}'",
resolution
))
}
};

// Revert character replacements.
resolver = resolver.replace("%3A", ":");
resolver = resolver.replace("%23", "#");
resolver = resolver.replace("%25", "%");
}

let (name, _version) = match resolution[1..].split_once("@patch:") {
// Extract npm version from patched dependencies.
Some((_, patch)) => patch
.rsplit_once("@npm")
.ok_or_else(|| anyhow!("Failed to parse patch in yarn lock file"))?,
None => resolution
.rsplit_once("@npm")
.ok_or_else(|| anyhow!("Failed to parse name in yarn lock file"))?,
let (name, version) = if resolver.starts_with("workspace:") {
// Ignore filesystem dependencies like the project ("project@workspace:.").
continue;
} else if resolver.starts_with("npm:") {
let version = package
.get(&"version".into())
.and_then(YamlValue::as_str)
.ok_or_else(|| anyhow!("Failed to parse yarn version for '{}'", resolution))?;

(name, version.to_owned())
} else if resolver.starts_with("http:")
|| resolver.starts_with("https:")
|| resolver.starts_with("ssh:")
{
(name, resolver)
} else {
return Err(anyhow!(
"Failed to parse yarn dependency resolver for '{}'",
resolution
));
};

let version = package
.get(&"version".into())
.and_then(YamlValue::as_str)
.ok_or_else(|| anyhow!("Failed to parse version in yarn lock file"))?;

packages.push(PackageDescriptor {
name: name.to_owned(),
version: version.to_owned(),
package_type: PackageType::Npm,
name: name.to_owned(),
version,
});
}

Expand Down Expand Up @@ -213,7 +246,7 @@ mod tests {

let pkgs = parser.parse().unwrap();

assert_eq!(pkgs.len(), 51);
assert_eq!(pkgs.len(), 53);

let expected_pkgs = [
PackageDescriptor {
Expand All @@ -236,6 +269,20 @@ mod tests {
version: "1.2.3".into(),
package_type: PackageType::Npm,
},
PackageDescriptor {
name: "ethereumjs-abi".into(),
version: "https://github.com/ethereumjs/ethereumjs-abi.git\
#commit=ee3994657fa7a427238e6ba92a84d0b529bbcde0"
.into(),
package_type: PackageType::Npm,
},
PackageDescriptor {
name: "@me/remote-patch".into(),
version: "ssh://git@github.com:phylum/remote-patch\
#commit=d854c43ea177d1faeea56189249fff8c24a764bd"
.into(),
package_type: PackageType::Npm,
},
];

for expected_pkg in expected_pkgs {
Expand Down
14 changes: 14 additions & 0 deletions cli/tests/fixtures/yarn.lock
Original file line number Diff line number Diff line change
Expand Up @@ -470,3 +470,17 @@ __metadata:
checksum: ae0123222c6df65b437669d63dfa8c36cee20a504101b2fcd97b8bf76f91259c17f9f2b4d70a1e3c6bbcee7f51b28392833adb6b2770b23b01abec84e369660b
languageName: node
linkType: hard

"ethereumjs-abi@git+https://github.com/ethereumjs/ethereumjs-abi.git":
version: 0.6.8
resolution: "ethereumjs-abi@https://github.com/ethereumjs/ethereumjs-abi.git#commit=ee3994657fa7a427238e6ba92a84d0b529bbcde0"
checksum: ae074be0bb012857ab5d3ae644d1163b908a48dd724b7d2567cfde309dc72222d460438f2411936a70dc949dc604ce1ef7118f7273bd525815579143c907e336
languageName: node
linkType: hard

"@me/remote-patch@patch:@me/remote-patch@ssh://git@github.com:phylum/remote-patch#commit=d854c43ea177d1faeea56189249fff8c24a764bd#.yarn/patches/@me-remote-patch-0deadbeef0::locator=root%40workspace%3A.":
version: 1.2.3
resolution: "@me/remote-patch@patch:@me/remote-patch@ssh%3A//git@github.com%3Aphylum/remote-patch%23commit=d854c43ea177d1faeea56189249fff8c24a764bd#.yarn/patches/@me-remote-patch-0deadbeef0::version=1.2.3&hash=ff00ff&locator=root%40workspace%3A."
checksum: ae0123222c6df65b437669d63dfa8c36cee20a504101b2fcd97b8bf76f91259c17f9f2b4d70a1e3c6bbcee7f51b28392833adb6b2770b23b01abec84e369660b
languageName: node
linkType: hard

0 comments on commit db3e281

Please sign in to comment.