Skip to content

Commit

Permalink
Add C# *.packages.config support (#1545)
Browse files Browse the repository at this point in the history
This adds support for `packages.config` files for the C# ecosystem under
the new `NugetConfig` lockfile name.

The file format itself seems to be relatively simple, with just a list
of package names and optional versions. The current test fixture is
based on examples of this lockfile found on <https://github.com>.

Both `packages.config` on its own and `packages.*.config` are supported
as valid names. Since this seems to be a lockfile there is no need for
lockfile generation.
  • Loading branch information
cd-work authored Dec 4, 2024
1 parent 44cf9ae commit 1a45ad3
Show file tree
Hide file tree
Showing 8 changed files with 137 additions and 37 deletions.
4 changes: 4 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,10 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/).

## Unreleased

### Added

- Support for C#'s `packages.*.config` lockfile type

## 7.1.5 - 2024-11-26

### Fixed
Expand Down
2 changes: 1 addition & 1 deletion docs/commands/phylum_analyze.md
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ Usage: phylum analyze [OPTIONS] [DEPENDENCY_FILE]...

`-t`, `--type` `<TYPE>`
&emsp; Dependency file type used for all lockfiles (default: auto)
&emsp; Accepted values: `npm`, `yarn`, `pnpm`, `gem`, `pip`, `poetry`, `pipenv`, `mvn`, `gradle`, `msbuild`, `nugetlock`, `gomod`, `go`, `cargo`, `spdx`, `cyclonedx`, `auto`
&emsp; Accepted values: `npm`, `yarn`, `pnpm`, `gem`, `pip`, `poetry`, `pipenv`, `mvn`, `gradle`, `msbuild`, `nugetlock`, `nugetconfig`, `gomod`, `go`, `cargo`, `spdx`, `cyclonedx`, `auto`

`--skip-sandbox`
&emsp; Run lockfile generation without sandbox protection
Expand Down
2 changes: 1 addition & 1 deletion docs/commands/phylum_init.md
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ Usage: phylum init [OPTIONS] [PROJECT_NAME]

`-t`, `--type` `<TYPE>`
&emsp; Dependency file type used for all lockfiles (default: auto)
&emsp; Accepted values: `npm`, `yarn`, `pnpm`, `gem`, `pip`, `poetry`, `pipenv`, `mvn`, `gradle`, `msbuild`, `nugetlock`, `gomod`, `go`, `cargo`, `spdx`, `cyclonedx`, `auto`
&emsp; Accepted values: `npm`, `yarn`, `pnpm`, `gem`, `pip`, `poetry`, `pipenv`, `mvn`, `gradle`, `msbuild`, `nugetlock`, `nugetconfig`, `gomod`, `go`, `cargo`, `spdx`, `cyclonedx`, `auto`

`-f`, `--force`
&emsp; Overwrite existing configurations without confirmation
Expand Down
2 changes: 1 addition & 1 deletion docs/commands/phylum_parse.md
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ Usage: phylum parse [OPTIONS] [DEPENDENCY_FILE]...

`-t`, `--type` `<TYPE>`
&emsp; Dependency file type used for all lockfiles (default: auto)
&emsp; Accepted values: `npm`, `yarn`, `pnpm`, `gem`, `pip`, `poetry`, `pipenv`, `mvn`, `gradle`, `msbuild`, `nugetlock`, `gomod`, `go`, `cargo`, `spdx`, `cyclonedx`, `auto`
&emsp; Accepted values: `npm`, `yarn`, `pnpm`, `gem`, `pip`, `poetry`, `pipenv`, `mvn`, `gradle`, `msbuild`, `nugetlock`, `nugetconfig`, `gomod`, `go`, `cargo`, `spdx`, `cyclonedx`, `auto`

`--skip-sandbox`
&emsp; Run lockfile generation without sandbox protection
Expand Down
1 change: 1 addition & 0 deletions docs/supported_lockfiles.md
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ The Phylum CLI supports processing many different lockfiles:
| `gem` | `Gemfile.lock` |
| `msbuild` | `*.csproj` |
| `nugetlock` | `packages.lock.json` <br /> `packages.*.lock.json` |
| `nugetconfig` | `packages.config` <br /> `packages.*.config` |
| `mvn` | `effective-pom.xml` |
| `gradle` | `gradle.lockfile` <br /> `gradle/dependency-locks/*.lockfile` |
| `go` | `go.sum` |
Expand Down
89 changes: 89 additions & 0 deletions lockfile/src/csharp.rs
Original file line number Diff line number Diff line change
Expand Up @@ -155,6 +155,61 @@ impl Parse for CSProj {
}
}

pub struct PackagesConfig;

impl Parse for PackagesConfig {
/// Parses `packages.*.config` files into a [`Vec`] of packages.
fn parse(&self, data: &str) -> anyhow::Result<Vec<Package>> {
let data = data.trim_start_matches(UTF8_BOM);
let parsed: PackagesConfigXml = quick_xml::de::from_str(data)?;
Ok(parsed.into())
}

fn is_path_lockfile(&self, path: &Path) -> bool {
let file_name = match path.file_name().and_then(|f| f.to_str()) {
Some(file_name) => file_name,
None => return false,
};

// Accept both `packages.config` and `packages.<project_name>.config`.
file_name.starts_with("packages.") && file_name.ends_with(".config")
}

fn is_path_manifest(&self, _path: &Path) -> bool {
false
}
}

/// XML format of the `packages.*.config` file.
#[derive(Deserialize)]
struct PackagesConfigXml {
#[serde(rename = "package", default)]
packages: Vec<PackagesConfigXmlPackage>,
}

impl From<PackagesConfigXml> for Vec<Package> {
fn from(packages_config: PackagesConfigXml) -> Self {
packages_config
.packages
.into_iter()
.map(|package| {
let version =
package.version.map_or(PackageVersion::Unknown, PackageVersion::FirstParty);
Package { version, name: package.id, package_type: PackageType::Nuget }
})
.collect()
}
}

/// Package entry in a `packages.*.config` file.
#[derive(Deserialize)]
struct PackagesConfigXmlPackage {
#[serde(alias = "@id")]
id: String,
#[serde(alias = "@version")]
version: Option<String>,
}

#[cfg(test)]
mod tests {
use super::*;
Expand Down Expand Up @@ -302,4 +357,38 @@ mod tests {
assert_eq!(pkgs[1].name, "NUnit3TestAdapter");
assert_eq!(pkgs[1].version, PackageVersion::FirstParty("3.13.0".into()));
}

#[test]
fn packages_config() {
let pkgs =
PackagesConfig.parse(include_str!("../../tests/fixtures/packages.config")).unwrap();
assert_eq!(pkgs.len(), 4);

let expected_pkgs = [
Package {
name: "AddressParser".into(),
version: PackageVersion::FirstParty("0.0.20".into()),
package_type: PackageType::Nuget,
},
Package {
name: "JetBrains.ReSharper.SDK".into(),
version: PackageVersion::FirstParty("8.2.921-EAP".into()),
package_type: PackageType::Nuget,
},
Package {
name: "boost".into(),
version: PackageVersion::FirstParty("1.78.0".into()),
package_type: PackageType::Nuget,
},
Package {
name: "noversion".into(),
version: PackageVersion::Unknown,
package_type: PackageType::Nuget,
},
];

for expected_pkg in expected_pkgs {
assert!(pkgs.contains(&expected_pkg), "missing package {expected_pkg:?}");
}
}
}
67 changes: 33 additions & 34 deletions lockfile/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ use thiserror::Error;
use walkdir::WalkDir;

pub use crate::cargo::Cargo;
pub use crate::csharp::{CSProj, PackagesLock};
pub use crate::csharp::{CSProj, PackagesConfig, PackagesLock};
pub use crate::cyclonedx::CycloneDX;
pub use crate::golang::{GoMod, GoSum};
pub use crate::java::{GradleLock, Pom};
Expand Down Expand Up @@ -61,6 +61,7 @@ pub enum LockfileFormat {
#[serde(alias = "nuget")]
Msbuild,
NugetLock,
NugetConfig,
GoMod,
Go,
Cargo,
Expand Down Expand Up @@ -103,6 +104,7 @@ impl LockfileFormat {
LockfileFormat::Gradle => "gradle",
LockfileFormat::Msbuild => "msbuild",
LockfileFormat::NugetLock => "nugetlock",
LockfileFormat::NugetConfig => "nugetconfig",
LockfileFormat::GoMod => "gomod",
LockfileFormat::Go => "go",
LockfileFormat::Cargo => "cargo",
Expand All @@ -125,6 +127,7 @@ impl LockfileFormat {
LockfileFormat::Gradle => &GradleLock,
LockfileFormat::Msbuild => &CSProj,
LockfileFormat::NugetLock => &PackagesLock,
LockfileFormat::NugetConfig => &PackagesConfig,
LockfileFormat::GoMod => &GoMod,
LockfileFormat::Go => &GoSum,
LockfileFormat::Cargo => &Cargo,
Expand All @@ -134,44 +137,32 @@ impl LockfileFormat {
}

/// Iterate over all supported lockfile formats.
pub fn iter() -> LockfileFormatIter {
LockfileFormatIter(0)
}
}

/// An iterator of all supported lockfile formats.
pub struct LockfileFormatIter(u8);

impl Iterator for LockfileFormatIter {
type Item = LockfileFormat;

fn next(&mut self) -> Option<Self::Item> {
pub fn iter() -> impl Iterator<Item = LockfileFormat> {
// NOTE: Without explicit override, the lockfile generator will always pick the
// first matching format for the manifest. To ensure best possible support,
// common formats should be returned **before** less common ones (i.e. NPM
// before Yarn).
const FORMATS: &[LockfileFormat] = &[
LockfileFormat::Npm,
LockfileFormat::Yarn,
LockfileFormat::Pnpm,
LockfileFormat::Gem,
LockfileFormat::Pip,
LockfileFormat::Poetry,
LockfileFormat::Pipenv,
LockfileFormat::Maven,
LockfileFormat::Gradle,
LockfileFormat::Msbuild,
LockfileFormat::NugetLock,
LockfileFormat::NugetConfig,
LockfileFormat::GoMod,
LockfileFormat::Go,
LockfileFormat::Cargo,
LockfileFormat::Spdx,
LockfileFormat::CycloneDX,
];

let item = match self.0 {
0 => LockfileFormat::Npm,
1 => LockfileFormat::Yarn,
2 => LockfileFormat::Pnpm,
3 => LockfileFormat::Gem,
4 => LockfileFormat::Pip,
5 => LockfileFormat::Poetry,
6 => LockfileFormat::Pipenv,
7 => LockfileFormat::Maven,
8 => LockfileFormat::Gradle,
9 => LockfileFormat::Msbuild,
10 => LockfileFormat::NugetLock,
11 => LockfileFormat::GoMod,
12 => LockfileFormat::Go,
13 => LockfileFormat::Cargo,
14 => LockfileFormat::Spdx,
15 => LockfileFormat::CycloneDX,
_ => return None,
};
self.0 += 1;
Some(item)
FORMATS.iter().copied()
}
}

Expand Down Expand Up @@ -519,6 +510,9 @@ mod tests {
("pnpm-lock.yaml", LockfileFormat::Pnpm),
("sample.csproj", LockfileFormat::Msbuild),
("packages.lock.json", LockfileFormat::NugetLock),
("packages.project.lock.json", LockfileFormat::NugetLock),
("packages.config", LockfileFormat::NugetConfig),
("packages.project.config", LockfileFormat::NugetConfig),
("gradle.lockfile", LockfileFormat::Gradle),
("default.lockfile", LockfileFormat::Gradle),
("effective-pom.xml", LockfileFormat::Maven),
Expand All @@ -528,7 +522,9 @@ mod tests {
("go.sum", LockfileFormat::Go),
("Cargo.lock", LockfileFormat::Cargo),
(".spdx.json", LockfileFormat::Spdx),
("file.spdx.json", LockfileFormat::Spdx),
(".spdx.yaml", LockfileFormat::Spdx),
("file.spdx.yaml", LockfileFormat::Spdx),
("bom.json", LockfileFormat::CycloneDX),
("bom.xml", LockfileFormat::CycloneDX),
];
Expand All @@ -555,6 +551,7 @@ mod tests {
("nuget", LockfileFormat::Msbuild),
("msbuild", LockfileFormat::Msbuild),
("nugetlock", LockfileFormat::NugetLock),
("nugetconfig", LockfileFormat::NugetConfig),
("gomod", LockfileFormat::GoMod),
("go", LockfileFormat::Go),
("cargo", LockfileFormat::Cargo),
Expand Down Expand Up @@ -585,6 +582,7 @@ mod tests {
("gradle", LockfileFormat::Gradle),
("msbuild", LockfileFormat::Msbuild),
("nugetlock", LockfileFormat::NugetLock),
("nugetconfig", LockfileFormat::NugetConfig),
("gomod", LockfileFormat::GoMod),
("go", LockfileFormat::Go),
("cargo", LockfileFormat::Cargo),
Expand Down Expand Up @@ -629,6 +627,7 @@ mod tests {
(LockfileFormat::Gradle, 1),
(LockfileFormat::Msbuild, 2),
(LockfileFormat::NugetLock, 1),
(LockfileFormat::NugetConfig, 1),
(LockfileFormat::GoMod, 1),
(LockfileFormat::Go, 1),
(LockfileFormat::Cargo, 3),
Expand Down
7 changes: 7 additions & 0 deletions tests/fixtures/packages.config
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
<?xml version="1.0" encoding="utf-8"?>
<packages>
<package id="AddressParser" version="0.0.20" targetFramework="net471" />
<package id="JetBrains.ReSharper.SDK" version="8.2.921-EAP" targetFramework="net35" />
<package id="boost" version="1.78.0" targetFramework="native" developmentDependency="true" />
<package id="noversion" />
</packages>

0 comments on commit 1a45ad3

Please sign in to comment.