From 94ae52fbbc4e9043dfc8433b718d9b29e4586888 Mon Sep 17 00:00:00 2001 From: Keshav Priyadarshi Date: Tue, 10 Mar 2026 17:36:24 +0530 Subject: [PATCH 1/4] Remove memmap2 dependency Signed-off-by: Keshav Priyadarshi --- Cargo.lock | 41 +++++++++++++++++++++++++++++++---------- Cargo.toml | 2 +- 2 files changed, 32 insertions(+), 11 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 2598d67..63e9dca 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -468,15 +468,6 @@ version = "2.7.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f52b00d39961fc5b2736ea853c9cc86238e165017a493d1d5c8eac6bdc4cc273" -[[package]] -name = "memmap2" -version = "0.9.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "744133e4a0e0a658e1374cf3bf8e415c4052a15a111acd372764c55b4177d490" -dependencies = [ - "libc", -] - [[package]] name = "mime" version = "0.3.17" @@ -561,6 +552,16 @@ dependencies = [ "vcpkg", ] +[[package]] +name = "packageurl" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "35da99768af1ae8830ccf30d295db0e09c24bcfda5a67515191dd4b773f6d82a" +dependencies = [ + "percent-encoding", + "thiserror", +] + [[package]] name = "percent-encoding" version = "2.3.2" @@ -608,8 +609,8 @@ name = "purl_validator" version = "0.58.0" dependencies = [ "fst", - "memmap2", "once_cell", + "packageurl", "reqwest", ] @@ -895,6 +896,26 @@ dependencies = [ "windows-sys 0.61.2", ] +[[package]] +name = "thiserror" +version = "2.0.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4288b5bcbc7920c07a1149a35cf9590a2aa808e0bc1eafaade0b80947865fbc4" +dependencies = [ + "thiserror-impl", +] + +[[package]] +name = "thiserror-impl" +version = "2.0.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ebc4ee7f67670e9b64d05fa4253e753e016c6c95ff35b89b7941d6b856dec1d5" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + [[package]] name = "tinystr" version = "0.8.2" diff --git a/Cargo.toml b/Cargo.toml index 0a21147..d1a38cb 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -24,7 +24,7 @@ include = [ [dependencies] fst = "0.4.7" -memmap2 = "0.9.9" +packageurl = "0.6.0" once_cell = "1.21" [build-dependencies] From 582ae5684c558d2cdd19342435830371bbfcafc3 Mon Sep 17 00:00:00 2001 From: Keshav Priyadarshi Date: Tue, 10 Mar 2026 17:39:39 +0530 Subject: [PATCH 2/4] Return error for invalid or unsupported PURLs Signed-off-by: Keshav Priyadarshi --- src/lib.rs | 34 +++++++++++++++++++++++++++------- 1 file changed, 27 insertions(+), 7 deletions(-) diff --git a/src/lib.rs b/src/lib.rs index 20ae634..adec306 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -24,35 +24,55 @@ See https://aboutcode.org for more information about nexB OSS projects. //! ``` //! use purl_validator::validate; //! -//! let result: bool = validate("pkg:nuget/FluentValidation"); +//! let result: bool = validate("pkg:nuget/FluentValidation") +//! .expect("only fails if PURL is invalid or contains version, qualifier, or subpath"); //! ``` //! use fst::Set; - use once_cell::sync::Lazy; +use packageurl::PackageUrl; use std::env; +use std::str::FromStr; static FST_DATA: &[u8] = include_bytes!(concat!(env!("OUT_DIR"), "/purls.fst")); static VALIDATOR: Lazy> = Lazy::new(|| Set::new(FST_DATA).expect("Failed to load FST from embedded bytes")); -fn strip_and_check_purl(packageurl: &str, fst_map: &Set<&[u8]>) -> bool { +fn strip_and_check_purl(packageurl: &str, fst_map: &Set<&[u8]>) -> Result { + let purl = PackageUrl::from_str(packageurl).map_err(ValidateError::InvalidPurl)?; + if purl.version().is_some() || !purl.qualifiers().is_empty() || purl.subpath().is_some() { + return Err(ValidateError::UnsupportedPurl( + "only base PURL is supported (no version, qualifiers, or subpath)", + )); + } + let trimmed_packageurl = packageurl.trim_end_matches("/"); - fst_map.contains(trimmed_packageurl) + Ok(fst_map.contains(trimmed_packageurl)) } /// Validate a Package URL (PURL) /// -/// Returns `true` if the given base PURL represents an existing package, -/// otherwise returns `false`. +/// Return `Ok(true)` if given **base PURL** represents an existing package, +/// `Ok(false)` if it does not, or `Err` if the PURL is invalid or contains +/// unsupported fields (version, qualifiers, or subpath). +/// +/// A **base PURL** is a PURL without a version, qualifiers, or subpath. +/// PURLs containing a version, qualifiers, or subpath are **not supported** +/// and will cause the validator to return an error. /// /// Use pre-built FST (Finite State Transducer) to perform lookups and confirm whether /// the **base PURL** exists. -pub fn validate(packageurl: &str) -> bool { +pub fn validate(packageurl: &str) -> Result { strip_and_check_purl(packageurl, &VALIDATOR) } +#[derive(Debug)] +pub enum ValidateError { + InvalidPurl(packageurl::Error), + UnsupportedPurl(&'static str), +} + #[cfg(test)] mod validate_tests; From 7b061e690ce40b34993a68e1c180844f8e3ad1e4 Mon Sep 17 00:00:00 2001 From: Keshav Priyadarshi Date: Tue, 10 Mar 2026 17:40:51 +0530 Subject: [PATCH 3/4] Add test for unsupported and invalid PURLs Signed-off-by: Keshav Priyadarshi --- src/validate_tests.rs | 39 ++++++++++++++++++++++++++++++--------- 1 file changed, 30 insertions(+), 9 deletions(-) diff --git a/src/validate_tests.rs b/src/validate_tests.rs index 141658b..e01c164 100644 --- a/src/validate_tests.rs +++ b/src/validate_tests.rs @@ -20,11 +20,12 @@ fn test_validate_with_custom_file() { let data: Vec = fs::read(test_path).unwrap(); let data_slice: &[u8] = &data; let validator = Set::new(data_slice).unwrap(); - assert!(strip_and_check_purl( - "pkg:nuget/FluentUtils.EnumExtensions", - &validator - )); - assert!(!strip_and_check_purl("pkg:example/nonexistent", &validator)); + + let result = strip_and_check_purl("pkg:nuget/FluentUtils.EnumExtensions", &validator).unwrap(); + assert!(result); + + let result = strip_and_check_purl("pkg:example/nonexistent", &validator).unwrap(); + assert!(!result); } #[test] @@ -35,8 +36,28 @@ fn test_validate_with_packageurl_trailing_slash() { let validator = Set::new(data_slice).unwrap(); assert!(validator.contains("pkg:nuget/FluentUtils.EnumExtensions")); - assert!(strip_and_check_purl( - "pkg:nuget/FluentUtils.EnumExtensions/", - &validator - )); + let result = strip_and_check_purl("pkg:nuget/FluentUtils.EnumExtensions/", &validator).unwrap(); + assert!(result); +} + +#[test] +fn test_validate_with_packageurl_invalid_purl() { + let test_path = Path::new(env!("CARGO_MANIFEST_DIR")).join("tests/data/test_purls.fst"); + let data: Vec = fs::read(test_path).unwrap(); + let data_slice: &[u8] = &data; + let validator = Set::new(data_slice).unwrap(); + + let result = strip_and_check_purl("nuget/foobar", &validator); + assert!(matches!(result, Err(ValidateError::InvalidPurl(_)))); +} + +#[test] +fn test_validate_with_packageurl_unsupported_purl() { + let test_path = Path::new(env!("CARGO_MANIFEST_DIR")).join("tests/data/test_purls.fst"); + let data: Vec = fs::read(test_path).unwrap(); + let data_slice: &[u8] = &data; + let validator = Set::new(data_slice).unwrap(); + + let result = strip_and_check_purl("pkg:nuget/FluentUtils.EnumExtensions@1.0.0", &validator); + assert!(matches!(result, Err(ValidateError::UnsupportedPurl(_)))); } From 9b124067f9a0429f645c9cc4f9885fc2df256127 Mon Sep 17 00:00:00 2001 From: Keshav Priyadarshi Date: Tue, 10 Mar 2026 17:52:16 +0530 Subject: [PATCH 4/4] Add examples for invalid input Signed-off-by: Keshav Priyadarshi --- README.md | 28 +++++++++++++++++++++++++--- 1 file changed, 25 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index 3a385a9..a5a77da 100644 --- a/README.md +++ b/README.md @@ -33,12 +33,34 @@ Add `purl-validator` to your Rust dependency cargo add purl-validator ``` -Use it in your code like this +Use it in your code like this: ```rust use purl_validator::validate; -let result: bool = validate("pkg:nuget/FluentValidation"); +fn main() { + let result: bool = validate("pkg:nuget/FluentValidation") + .expect("only fails if PURL is invalid or contains version, qualifier, or subpath"); +} +``` + +Examples and errors: + +```rust +fn example() { + // This will return: Ok(true) + validate("pkg:nuget/FluentValidation"); + + // This will return: Ok(false) + validate("pkg:nuget/non-existent-foo-bar"); + + + // This will return an error: Err(UnsupportedPurl("only base PURL is supported (no version, qualifiers, or subpath)")) + validate("pkg:nuget/FluentValidation@10.2.3"); + + // This will return an error: Err(InvalidPurl("")) + validate("nuget/FluentValidation"); +} ``` ## Contribution @@ -90,4 +112,4 @@ limitations under the License. ``` [^1]: MineCode continuously collects package metadata from various package ecosystems to maintain an up-to-date catalog of known packages. -[^2]: A Base Package URL is a Package URL without a version or subpath. \ No newline at end of file +[^2]: A Base Package URL is a Package URL without a version, qualifiers or subpath. \ No newline at end of file