diff --git a/Cargo.toml b/Cargo.toml index e842f8e..d84c1c7 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,7 +1,7 @@ [package] name = "packageurl" -version = "0.5.0" -edition = "2021" +version = "0.6.0" +edition = "2024" authors = [ "Martin Larralde ", "Jens Reimann ", @@ -9,27 +9,26 @@ authors = [ license = "MIT" description = "Rust implementation of the package url specification" documentation = "https://docs.rs/packageurl" -repository = "https://github.com/scm-rs/packageurl-rs" +repository = "https://github.com/scm-rs/packageurl.rs" readme = "README.md" keywords = ["purl", "package-url"] categories = ["parser-implementations", "encoding", "development-tools"] -rust-version = "1.82.0" +rust-version = "1.85.0" [dependencies] -percent-encoding = "2.1.0" -thiserror = "2.0.12" +percent-encoding = "2" +thiserror = "2" -memchr = { version = "2.4.0", optional = true } -serde = { version = "1.0.0", optional = true, features = ["derive"] } +memchr = { version = "2", optional = true } +serde = { version = "1", optional = true, features = ["derive"] } [features] default = [] [dev-dependencies] -criterion = "0.5.1" -rstest = "0.25.0" -serde = { version = "1.0.0", features = ["derive"] } -serde_json = "1.0.13" +criterion = "0.7" +serde = { version = "1", features = ["derive"] } +serde_json = "1" url = "2" [[bench]] diff --git a/README.md b/README.md index adaf34b..600ad4b 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,6 @@ # -`packageurl-rs` [![Star me](https://img.shields.io/github/stars/scm-rs/packageurl.rs.svg?style=social&label=Star)](https://github.com/scm-rs/packageurl.rs/stargazers) +`packageurl.rs` [![Star me](https://img.shields.io/github/stars/scm-rs/packageurl.rs.svg?style=social&label=Star)](https://github.com/scm-rs/packageurl.rs/stargazers) *Read and generate Package URLs in Rust.* @@ -74,7 +74,7 @@ a changelog as part of the [GitHub releases](https://github.com/scm-rs/packageur ## 💭 Feedback Found a bug? Have an enhancement request? Head over to the -[GitHub issue tracker](https://github.com/scm-rs/packageurl-rs/issues) of the project if +[GitHub issue tracker](https://github.com/scm-rs/packageurl.rs/issues) of the project if you need to report or ask something. If you are filling in on a bug, please include as much information as you can about the issue, and try to recreate the same bug in a simple, easily reproducible situation. diff --git a/benches/bench.rs b/benches/bench.rs index a992727..2a38484 100644 --- a/benches/bench.rs +++ b/benches/bench.rs @@ -1,4 +1,4 @@ -use criterion::{criterion_group, criterion_main, Criterion}; +use criterion::{Criterion, criterion_group, criterion_main}; use packageurl::PackageUrl; use std::str::FromStr; diff --git a/src/errors.rs b/src/errors.rs index 8a19151..3c9d8d6 100644 --- a/src/errors.rs +++ b/src/errors.rs @@ -12,6 +12,8 @@ pub enum Error { InvalidKey(String), #[error("missing name")] MissingName, + #[error("no namespace allowed for type {0:?}")] + TypeProhibitsNamespace(String), #[error("invalid namespace component: {0:?}")] InvalidNamespaceComponent(String), #[error("missing scheme")] diff --git a/src/lib.rs b/src/lib.rs index ad4997b..f7f3f3c 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -33,7 +33,7 @@ //! [`FromStr`]: https://doc.rust-lang.org/std/str/trait.FromStr.html //! [`PackageUrl`]: example_generated/struct.PackageUrl.html //! [`'static`]: https://doc.rust-lang.org/reference/items/static-items.html#static-lifetime-elision -#![doc(issue_tracker_base_url = "https://github.com/althonos/packageurl-rs/issues/")] +#![doc(issue_tracker_base_url = "https://github.com/althonos/packageurl.rs/issues/")] mod errors; mod parser; diff --git a/src/purl.rs b/src/purl.rs index 9678b15..f3bff4f 100644 --- a/src/purl.rs +++ b/src/purl.rs @@ -12,7 +12,7 @@ use serde::{Deserialize, Serialize}; use super::errors::Error; use super::errors::Result; use super::parser; -use super::utils::{to_lowercase, PercentCodec}; +use super::utils::{PercentCodec, to_lowercase}; use super::validation; const ENCODE_SET: &AsciiSet = &percent_encoding::CONTROLS @@ -26,8 +26,6 @@ const ENCODE_SET: &AsciiSet = &percent_encoding::CONTROLS .add(b'?') .add(b'{') .add(b'}') - // .add(b'/') - // .add(b':') .add(b';') .add(b'=') .add(b'+') @@ -36,7 +34,8 @@ const ENCODE_SET: &AsciiSet = &percent_encoding::CONTROLS .add(b'[') .add(b']') .add(b'^') - .add(b'|'); + .add(b'|') + .add(b'/'); /// A Package URL. #[derive(Debug, Clone, PartialEq, Eq)] @@ -66,7 +65,7 @@ impl<'a> PackageUrl<'a> { /// cannot contain spaces. /// /// # Name - /// The package name will be canonicalize depending on the type: for instance, + /// The package name will be canonicalized depending on the type: for instance, /// 'bitbucket' packages have a case-insensitive name, so the name will be /// lowercased if needed. /// @@ -87,7 +86,7 @@ impl<'a> PackageUrl<'a> { t = to_lowercase(t); // lowercase name if required by type and needed match t.as_ref() { - "bitbucket" | "deb" | "github" | "hex" | "npm" => { + "bitbucket" | "deb" | "github" | "hex" | "npm" | "composer" | "mlflow" => { n = to_lowercase(n); } "pypi" => { @@ -152,20 +151,31 @@ impl<'a> PackageUrl<'a> { } /// Assign a namespace to the package. - pub fn with_namespace(&mut self, namespace: N) -> &mut Self + pub fn with_namespace(&mut self, namespace: N) -> Result<&mut Self> where N: Into>, { + // Fail if namespace is prohibited for this type + match self.ty.as_ref() { + "bitnami" | "cargo" | "cocoapods" | "conda" | "cran" | "gem" | "hackage" | "mlflow" + | "nuget" | "oci" | "pub" | "pypi" => { + return Err(Error::TypeProhibitsNamespace(self.ty.to_string())); + } + _ => {} + } + + // Lowercase namespace if needed for this type let mut n = namespace.into(); match self.ty.as_ref() { - "bitbucket" | "deb" | "github" | "golang" | "hex" | "rpm" => { + "apk" | "bitbucket" | "composer" | "deb" | "github" | "golang" | "hex" | "qpkg" + | "rpm" => { n = to_lowercase(n); } _ => {} } self.namespace = Some(n); - self + Ok(self) } /// Clear the namespace @@ -175,12 +185,17 @@ impl<'a> PackageUrl<'a> { } /// Assign a version to the package. - pub fn with_version(&mut self, version: V) -> &mut Self + pub fn with_version(&mut self, version: V) -> Result<&mut Self> where V: Into>, { - self.version = Some(version.into()); - self + let mut v = version.into(); + if self.ty.as_ref() == "huggingface" { + v = to_lowercase(v); + } + + self.version = Some(v); + Ok(self) } /// Clear the version @@ -263,10 +278,10 @@ impl FromStr for PackageUrl<'static> { let mut purl = Self::new(ty, name)?; if let Some(ns) = namespace { - purl.with_namespace(ns); + purl.with_namespace(ns)?; } if let Some(v) = version { - purl.with_version(v); + purl.with_version(v)?; } if let Some(sp) = subpath { purl.with_subpath(sp)?; @@ -367,7 +382,9 @@ mod tests { let purl_string = PackageUrl::new("type", "name") .unwrap() .with_namespace("name/space") + .unwrap() .with_version("version") + .unwrap() .with_subpath("sub/path") .unwrap() .add_qualifier("k1", "v1") @@ -380,11 +397,20 @@ mod tests { #[test] fn test_percent_encoding_idempotent() { - let orig = "pkg:brew/openssl%25401.1@1.1.1w"; + let orig = "pkg:brew/open%2Fssl%25401.1@1.1.1w"; let round_trip = orig.parse::().unwrap().to_string(); assert_eq!(orig, round_trip); } + #[test] + fn test_percent_encoded_name() { + let raw_purl = "pkg:type/name/space/first%2Fname"; + let purl = PackageUrl::from_str(raw_purl).unwrap(); + assert_eq!(purl.ty(), "type"); + assert_eq!(purl.namespace(), Some("name/space")); + assert_eq!(purl.name(), "first/name"); + } + #[test] fn test_percent_encoding_qualifier() { let mut purl = "pkg:deb/ubuntu/gnome-calculator@1:41.1-2ubuntu2" @@ -396,7 +422,10 @@ mod tests { ) .unwrap(); let encoded = purl.to_string(); - assert_eq!(encoded, "pkg:deb/ubuntu/gnome-calculator@1:41.1-2ubuntu2?vcs_url=git%2Bhttps://salsa.debian.org/gnome-team/gnome-calculator.git%40debian/1%2541.1-2"); + assert_eq!( + encoded, + "pkg:deb/ubuntu/gnome-calculator@1:41.1-2ubuntu2?vcs_url=git%2Bhttps:%2F%2Fsalsa.debian.org%2Fgnome-team%2Fgnome-calculator.git%40debian%2F1%2541.1-2" + ); } #[cfg(feature = "serde")] diff --git a/tests/spec/macros.rs b/tests/spec/macros.rs index 467c551..d2b1513 100644 --- a/tests/spec/macros.rs +++ b/tests/spec/macros.rs @@ -57,11 +57,11 @@ pub fn run_build_test(case: &SpecTestCase) { let mut purl = purl_result.unwrap(); if let Some(ref ns) = input.namespace { - purl.with_namespace(ns.as_ref()); + let _ = purl.with_namespace(ns.as_ref()); } if let Some(ref v) = input.version { - purl.with_version(v.as_ref()); + let _ = purl.with_version(v.as_ref()); } if let Some(ref sp) = input.subpath { @@ -125,7 +125,6 @@ pub fn run_tests_from_spec(path: &Path) { } } - macro_rules! generate_json_tests { ($($test_name:ident => $file_path:expr),* $(,)?) => { $( diff --git a/tests/spec/mod.rs b/tests/spec/mod.rs index 13d7a17..1a6d0b0 100644 --- a/tests/spec/mod.rs +++ b/tests/spec/mod.rs @@ -8,16 +8,15 @@ mod testcase; generate_json_tests! { alpm_test => "tests/spec/purl-spec/tests/types/alpm-test.json", apk_test => "tests/spec/purl-spec/tests/types/apk-test.json", - bintray_test => "tests/spec/purl-spec/tests/types/bintray-test.json", bitbucket_test => "tests/spec/purl-spec/tests/types/bitbucket-test.json", bitnami_test => "tests/spec/purl-spec/tests/types/bitnami-test.json", cargo_test => "tests/spec/purl-spec/tests/types/cargo-test.json", cocoapods_test => "tests/spec/purl-spec/tests/types/cocoapods-test.json", - // composer_test => "tests/spec/purl-spec/tests/types/composer-test.json", + composer_test => "tests/spec/purl-spec/tests/types/composer-test.json", // conan_test => "tests/spec/purl-spec/tests/types/conan-test.json", conda_test => "tests/spec/purl-spec/tests/types/conda-test.json", // cpan_test => "tests/spec/purl-spec/tests/types/cpan-test.json", - // cran_test => "tests/spec/purl-spec/tests/types/cran-test.json", + cran_test => "tests/spec/purl-spec/tests/types/cran-test.json", deb_test => "tests/spec/purl-spec/tests/types/deb-test.json", docker_test => "tests/spec/purl-spec/tests/types/docker-test.json", gem_test => "tests/spec/purl-spec/tests/types/gem-test.json", @@ -26,9 +25,9 @@ generate_json_tests! { golang_test => "tests/spec/purl-spec/tests/types/golang-test.json", hackage_test => "tests/spec/purl-spec/tests/types/hackage-test.json", hex_test => "tests/spec/purl-spec/tests/types/hex-test.json", - // huggingface_test => "tests/spec/purl-spec/tests/types/huggingface-test.json", + huggingface_test => "tests/spec/purl-spec/tests/types/huggingface-test.json", luarocks_test => "tests/spec/purl-spec/tests/types/luarocks-test.json", - // maven_test => "tests/spec/purl-spec/tests/types/maven-test.json", + maven_test => "tests/spec/purl-spec/tests/types/maven-test.json", // mlflow_test => "tests/spec/purl-spec/tests/types/mlflow-test.json", // npm_test => "tests/spec/purl-spec/tests/types/npm-test.json", nuget_test => "tests/spec/purl-spec/tests/types/nuget-test.json", diff --git a/tests/spec/purl-spec b/tests/spec/purl-spec index 96d6f8c..52055b1 160000 --- a/tests/spec/purl-spec +++ b/tests/spec/purl-spec @@ -1 +1 @@ -Subproject commit 96d6f8c4123d4a6a5c2bbc90cf61fca834f421d9 +Subproject commit 52055b1547819f4044b4451623452af263d8f3be