purl 1.1.1 → 1.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/.gitmodules +3 -0
- data/CHANGELOG.md +75 -0
- data/README.md +23 -0
- data/Rakefile +237 -0
- data/lib/purl/package_url.rb +16 -15
- data/lib/purl/registry_url.rb +6 -24
- data/lib/purl/version.rb +1 -1
- data/purl-types.json +155 -117
- metadata +3 -4
- data/schemas/purl-types.schema.json +0 -154
- data/schemas/test-suite-data.schema.json +0 -134
- data/test-suite-data.json +0 -710
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 519631a904ead8452084959edb20711e684c0fc9c0b0a848ff6775f86bc31675
|
|
4
|
+
data.tar.gz: cb82893be40a50ae28651b78ed293dd0303b6cc926b81985fe256d32f0b0fea7
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 5ef9defb2759c124020aa5d6ba88eb281e0bd1fbf256cafdd3ba7c4e17bca09aec0286fe2851b74e12313062a5d3e38a76db85376b60d3f645e9f4720e0cb2b8
|
|
7
|
+
data.tar.gz: 6399a94cf6bd5bd92fa6e5da30f7f9c6e056982728bc5828ead26e9d8c9c17b5beedbe4bdababebc0bee701bdeaf27f4ffa7b84e54a451d45e7dea7c9575c9b7
|
data/.gitmodules
ADDED
data/CHANGELOG.md
CHANGED
|
@@ -7,6 +7,81 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|
|
7
7
|
|
|
8
8
|
## [Unreleased]
|
|
9
9
|
|
|
10
|
+
## [1.2.0] - 2025-07-27
|
|
11
|
+
|
|
12
|
+
### Added
|
|
13
|
+
- Default registry URLs for 10 additional package types:
|
|
14
|
+
- `golang`: https://pkg.go.dev (Go package discovery site)
|
|
15
|
+
- `luarocks`: https://luarocks.org (Lua package repository)
|
|
16
|
+
- `clojars`: https://clojars.org (Clojure package repository)
|
|
17
|
+
- `elm`: https://package.elm-lang.org (Elm package catalog)
|
|
18
|
+
- `deno`: https://deno.land (Deno module registry)
|
|
19
|
+
- `homebrew`: https://formulae.brew.sh (Homebrew package browser)
|
|
20
|
+
- `bioconductor`: https://bioconductor.org (R bioinformatics packages)
|
|
21
|
+
- `huggingface`: https://huggingface.co (Machine learning models)
|
|
22
|
+
- `swift`: https://swiftpackageindex.com (Swift package index)
|
|
23
|
+
- `conan`: https://conan.io/center (C/C++ package center)
|
|
24
|
+
|
|
25
|
+
### Enhanced
|
|
26
|
+
- Registry configuration support for newly added package types
|
|
27
|
+
- Updated test suite to validate all new default registries
|
|
28
|
+
- Improved package type coverage with comprehensive registry URL mapping
|
|
29
|
+
|
|
30
|
+
### Configuration
|
|
31
|
+
- Updated `purl-types.json` to version 1.2.0 with enhanced registry configurations
|
|
32
|
+
- Added specialized registry handling for Go's unique import path structure
|
|
33
|
+
|
|
34
|
+
## [1.1.2] - 2025-07-25
|
|
35
|
+
|
|
36
|
+
### Added
|
|
37
|
+
- Comprehensive benchmarking rake tasks for performance analysis
|
|
38
|
+
- `rake benchmark:parse` - PURL parsing performance benchmarks
|
|
39
|
+
- `rake benchmark:types` - Package type parsing comparison
|
|
40
|
+
- `rake benchmark:registry` - Registry URL generation benchmarks
|
|
41
|
+
- `rake benchmark:all` - Run all benchmarks
|
|
42
|
+
|
|
43
|
+
### Improved
|
|
44
|
+
- **26% improvement in parsing throughput** (~175K PURLs/second)
|
|
45
|
+
- **8% improvement in string conversion performance** (~315K conversions/second)
|
|
46
|
+
- **7% improvement in object creation** (~280K objects/second)
|
|
47
|
+
- Optimized string operations in parse method with conditional regex application
|
|
48
|
+
- Reduced string allocations in `to_s` method using array joining
|
|
49
|
+
- Cached compiled regexes with `.freeze` for better performance
|
|
50
|
+
- Lower memory allocation pressure in high-throughput scenarios
|
|
51
|
+
|
|
52
|
+
## [1.1.1] - 2025-07-25
|
|
53
|
+
|
|
54
|
+
### Added
|
|
55
|
+
- Comprehensive RDoc documentation for all classes and methods
|
|
56
|
+
- RDoc task in Rakefile with proper configuration
|
|
57
|
+
- API documentation link in README
|
|
58
|
+
|
|
59
|
+
### Fixed
|
|
60
|
+
- Add bigdecimal gem dependency to resolve potential loading issues
|
|
61
|
+
- Improve JSON schema loading error handling
|
|
62
|
+
|
|
63
|
+
## [1.1.0] - 2025-07-25
|
|
64
|
+
|
|
65
|
+
### Added
|
|
66
|
+
- JSON schema validation for configuration files (`purl-types.json` and `test-suite-data.json`)
|
|
67
|
+
- New rake tasks: `spec:validate_schemas` and `spec:validate_examples`
|
|
68
|
+
- Examples for all package types in configuration
|
|
69
|
+
- Default registry URLs for package types
|
|
70
|
+
- Enhanced reverse parsing support for additional package types
|
|
71
|
+
- `with` method for creating modified PURL objects (immutable pattern)
|
|
72
|
+
- FUNDING.yml for project sponsorship support
|
|
73
|
+
|
|
74
|
+
### Enhanced
|
|
75
|
+
- Improved README documentation with custom registry examples
|
|
76
|
+
- Enhanced test coverage for new functionality
|
|
77
|
+
- Better compliance test output formatting
|
|
78
|
+
- Comprehensive package type examples and validation
|
|
79
|
+
|
|
80
|
+
### Documentation
|
|
81
|
+
- Updated documentation to remove emoji and enhance readability
|
|
82
|
+
- Added comprehensive examples for custom registry usage
|
|
83
|
+
- Enhanced API documentation throughout
|
|
84
|
+
|
|
10
85
|
## [1.0.0] - 2025-01-24
|
|
11
86
|
|
|
12
87
|
### Added
|
data/README.md
CHANGED
|
@@ -10,6 +10,10 @@ This library features comprehensive error handling with namespaced error types,
|
|
|
10
10
|
|
|
11
11
|
**[Available on RubyGems](https://rubygems.org/gems/purl)** | **[API Documentation](https://rdoc.info/github/andrew/purl)**
|
|
12
12
|
|
|
13
|
+
## Related Libraries
|
|
14
|
+
|
|
15
|
+
- **[Vers](https://github.com/andrew/vers)** - A Ruby library for working with version ranges that supports the VERS specification
|
|
16
|
+
|
|
13
17
|
## Features
|
|
14
18
|
|
|
15
19
|
- **Comprehensive PURL parsing and validation** with 37 package types (32 official + 5 additional ecosystems)
|
|
@@ -381,6 +385,25 @@ rake spec:types
|
|
|
381
385
|
rake spec:verify_types
|
|
382
386
|
```
|
|
383
387
|
|
|
388
|
+
### Testing Against Official Specification
|
|
389
|
+
|
|
390
|
+
This library includes the official [purl-spec](https://github.com/package-url/purl-spec) repository as a git submodule for testing and validation:
|
|
391
|
+
|
|
392
|
+
```bash
|
|
393
|
+
# Initialize submodule (first time only)
|
|
394
|
+
git submodule update --init --recursive
|
|
395
|
+
|
|
396
|
+
# Update submodule to latest spec
|
|
397
|
+
git submodule update --remote purl-spec
|
|
398
|
+
```
|
|
399
|
+
|
|
400
|
+
The tests use files from the submodule to:
|
|
401
|
+
- **Schema validation**: Validate our `purl-types.json` against the official schema in `purl-spec/schemas/`
|
|
402
|
+
- **Type compliance**: Ensure our supported types match the official types in `purl-spec/types/`
|
|
403
|
+
- **Test data**: Access official test cases and examples from `purl-spec/tests/`
|
|
404
|
+
|
|
405
|
+
The submodule is automatically updated weekly via Dependabot, ensuring tests stay current with the latest specification changes. When the submodule updates, you can review and merge the PR to adopt new spec requirements.
|
|
406
|
+
|
|
384
407
|
### Rake Tasks
|
|
385
408
|
|
|
386
409
|
- `rake spec:update` - Fetch latest test cases from official PURL spec repository
|
data/Rakefile
CHANGED
|
@@ -562,3 +562,240 @@ namespace :spec do
|
|
|
562
562
|
end
|
|
563
563
|
end
|
|
564
564
|
end
|
|
565
|
+
|
|
566
|
+
namespace :benchmark do
|
|
567
|
+
desc "Run PURL parsing benchmarks"
|
|
568
|
+
task :parse do
|
|
569
|
+
require "benchmark"
|
|
570
|
+
require "json"
|
|
571
|
+
require_relative "lib/purl"
|
|
572
|
+
|
|
573
|
+
puts "🚀 PURL Parsing Benchmarks"
|
|
574
|
+
puts "=" * 50
|
|
575
|
+
|
|
576
|
+
# Load sample PURLs from purl-types.json
|
|
577
|
+
purl_types_data = JSON.parse(File.read(File.join(__dir__, "purl-types.json")))
|
|
578
|
+
sample_purls = []
|
|
579
|
+
|
|
580
|
+
purl_types_data["types"].each do |type_name, type_config|
|
|
581
|
+
examples = type_config["examples"]
|
|
582
|
+
sample_purls.concat(examples) if examples&.is_a?(Array)
|
|
583
|
+
end
|
|
584
|
+
|
|
585
|
+
# Add some complex PURLs for stress testing
|
|
586
|
+
complex_purls = [
|
|
587
|
+
"pkg:npm/@babel/core@7.20.0?arch=x64&dev=true#lib/index.js",
|
|
588
|
+
"pkg:maven/org.apache.commons/commons-lang3@3.12.0?classifier=sources",
|
|
589
|
+
"pkg:composer/symfony/console@5.4.0?extra=test&dev=true#src/Application.php",
|
|
590
|
+
"pkg:gem/rails@7.0.0?platform=ruby&env=production#app/controllers/application_controller.rb"
|
|
591
|
+
]
|
|
592
|
+
sample_purls.concat(complex_purls)
|
|
593
|
+
|
|
594
|
+
puts "📊 Sample size: #{sample_purls.length} PURLs"
|
|
595
|
+
puts "📦 Package types: #{purl_types_data['types'].keys.length}"
|
|
596
|
+
puts
|
|
597
|
+
|
|
598
|
+
# Benchmark parsing
|
|
599
|
+
puts "🔍 Parsing Performance:"
|
|
600
|
+
parsing_time = Benchmark.realtime do
|
|
601
|
+
sample_purls.each { |purl| Purl.parse(purl) }
|
|
602
|
+
end
|
|
603
|
+
|
|
604
|
+
puts " Total time: #{(parsing_time * 1000).round(2)}ms"
|
|
605
|
+
puts " Average per PURL: #{(parsing_time * 1000 / sample_purls.length).round(3)}ms"
|
|
606
|
+
puts " PURLs per second: #{(sample_purls.length / parsing_time).round(0)}"
|
|
607
|
+
puts
|
|
608
|
+
|
|
609
|
+
# Benchmark creation
|
|
610
|
+
puts "🔧 Object Creation Performance:"
|
|
611
|
+
creation_time = Benchmark.realtime do
|
|
612
|
+
1000.times do
|
|
613
|
+
Purl::PackageURL.new(
|
|
614
|
+
type: "gem",
|
|
615
|
+
namespace: "rails",
|
|
616
|
+
name: "rails",
|
|
617
|
+
version: "7.0.0",
|
|
618
|
+
qualifiers: {"arch" => "x64"},
|
|
619
|
+
subpath: "app/models/user.rb"
|
|
620
|
+
)
|
|
621
|
+
end
|
|
622
|
+
end
|
|
623
|
+
|
|
624
|
+
puts " 1000 objects: #{(creation_time * 1000).round(2)}ms"
|
|
625
|
+
puts " Average per object: #{(creation_time * 1000 / 1000).round(3)}ms"
|
|
626
|
+
puts " Objects per second: #{(1000 / creation_time).round(0)}"
|
|
627
|
+
puts
|
|
628
|
+
|
|
629
|
+
# Benchmark to_s conversion
|
|
630
|
+
puts "🔤 String Conversion Performance:"
|
|
631
|
+
test_purl = Purl.parse("pkg:npm/@babel/core@7.20.0?arch=x64#lib/index.js")
|
|
632
|
+
|
|
633
|
+
string_time = Benchmark.realtime do
|
|
634
|
+
10000.times { test_purl.to_s }
|
|
635
|
+
end
|
|
636
|
+
|
|
637
|
+
puts " 10,000 conversions: #{(string_time * 1000).round(2)}ms"
|
|
638
|
+
puts " Average per conversion: #{(string_time * 1000 / 10000).round(4)}ms"
|
|
639
|
+
puts " Conversions per second: #{(10000 / string_time).round(0)}"
|
|
640
|
+
puts
|
|
641
|
+
|
|
642
|
+
# Memory usage estimation
|
|
643
|
+
puts "💾 Memory Usage Estimation:"
|
|
644
|
+
purl_objects = sample_purls.map { |purl| Purl.parse(purl) }
|
|
645
|
+
|
|
646
|
+
# Rough estimation based on object count and typical Ruby object overhead
|
|
647
|
+
estimated_memory = purl_objects.length * 200 # ~200 bytes per PURL object estimate
|
|
648
|
+
puts " #{purl_objects.length} PURL objects: ~#{estimated_memory} bytes"
|
|
649
|
+
puts " Average per object: ~200 bytes"
|
|
650
|
+
puts
|
|
651
|
+
|
|
652
|
+
# Test different complexity levels
|
|
653
|
+
puts "🎯 Complexity Benchmarks:"
|
|
654
|
+
|
|
655
|
+
complexity_tests = {
|
|
656
|
+
"Simple" => "pkg:gem/rails@7.0.0",
|
|
657
|
+
"With namespace" => "pkg:npm/@babel/core@7.0.0",
|
|
658
|
+
"With qualifiers" => "pkg:cargo/rand@0.7.2?arch=x86_64&os=linux",
|
|
659
|
+
"With subpath" => "pkg:maven/org.springframework/spring-core@5.3.0#org/springframework/core/SpringVersion.class",
|
|
660
|
+
"Full complexity" => "pkg:npm/@babel/core@7.20.0?arch=x64&dev=true&os=linux#lib/parser/index.js"
|
|
661
|
+
}
|
|
662
|
+
|
|
663
|
+
complexity_tests.each do |level, purl_string|
|
|
664
|
+
time = Benchmark.realtime do
|
|
665
|
+
1000.times { Purl.parse(purl_string) }
|
|
666
|
+
end
|
|
667
|
+
puts " #{level.ljust(15)}: #{(time * 1000 / 1000).round(4)}ms per parse"
|
|
668
|
+
end
|
|
669
|
+
|
|
670
|
+
puts
|
|
671
|
+
puts "✅ Benchmark completed!"
|
|
672
|
+
end
|
|
673
|
+
|
|
674
|
+
desc "Compare parsing performance across package types"
|
|
675
|
+
task :types do
|
|
676
|
+
require "benchmark"
|
|
677
|
+
require "json"
|
|
678
|
+
require_relative "lib/purl"
|
|
679
|
+
|
|
680
|
+
puts "📊 Package Type Parsing Comparison"
|
|
681
|
+
puts "=" * 50
|
|
682
|
+
|
|
683
|
+
purl_types_data = JSON.parse(File.read(File.join(__dir__, "purl-types.json")))
|
|
684
|
+
|
|
685
|
+
# Benchmark each type with its examples
|
|
686
|
+
type_benchmarks = {}
|
|
687
|
+
|
|
688
|
+
purl_types_data["types"].each do |type_name, type_config|
|
|
689
|
+
examples = type_config["examples"]
|
|
690
|
+
next unless examples&.is_a?(Array) && examples.any?
|
|
691
|
+
|
|
692
|
+
time = Benchmark.realtime do
|
|
693
|
+
100.times do
|
|
694
|
+
examples.each { |purl| Purl.parse(purl) }
|
|
695
|
+
end
|
|
696
|
+
end
|
|
697
|
+
|
|
698
|
+
avg_time_per_purl = time / (100 * examples.length)
|
|
699
|
+
type_benchmarks[type_name] = {
|
|
700
|
+
time: avg_time_per_purl,
|
|
701
|
+
examples_count: examples.length
|
|
702
|
+
}
|
|
703
|
+
end
|
|
704
|
+
|
|
705
|
+
# Sort by performance (fastest first)
|
|
706
|
+
sorted_benchmarks = type_benchmarks.sort_by { |_, data| data[:time] }
|
|
707
|
+
|
|
708
|
+
puts "🏆 Performance Rankings (fastest to slowest):"
|
|
709
|
+
puts " Rank Type Avg Time/Parse Examples"
|
|
710
|
+
puts " " + "-" * 45
|
|
711
|
+
|
|
712
|
+
sorted_benchmarks.each_with_index do |(type, data), index|
|
|
713
|
+
rank = (index + 1).to_s.rjust(2)
|
|
714
|
+
time_str = "#{(data[:time] * 1000).round(4)}ms".rjust(10)
|
|
715
|
+
examples_str = data[:examples_count].to_s.rjust(8)
|
|
716
|
+
|
|
717
|
+
puts " #{rank}. #{type.ljust(12)} #{time_str} #{examples_str}"
|
|
718
|
+
end
|
|
719
|
+
|
|
720
|
+
fastest = sorted_benchmarks.first
|
|
721
|
+
slowest = sorted_benchmarks.last
|
|
722
|
+
|
|
723
|
+
puts
|
|
724
|
+
puts "📈 Performance Summary:"
|
|
725
|
+
puts " Fastest: #{fastest[0]} (#{(fastest[1][:time] * 1000).round(4)}ms)"
|
|
726
|
+
puts " Slowest: #{slowest[0]} (#{(slowest[1][:time] * 1000).round(4)}ms)"
|
|
727
|
+
puts " Ratio: #{(slowest[1][:time] / fastest[1][:time]).round(1)}x difference"
|
|
728
|
+
puts
|
|
729
|
+
puts "✅ Type comparison completed!"
|
|
730
|
+
end
|
|
731
|
+
|
|
732
|
+
desc "Benchmark registry URL generation"
|
|
733
|
+
task :registry do
|
|
734
|
+
require "benchmark"
|
|
735
|
+
require "json"
|
|
736
|
+
require_relative "lib/purl"
|
|
737
|
+
|
|
738
|
+
puts "🌐 Registry URL Generation Benchmarks"
|
|
739
|
+
puts "=" * 50
|
|
740
|
+
|
|
741
|
+
# Get PURLs that support registry URL generation
|
|
742
|
+
registry_purls = []
|
|
743
|
+
Purl.registry_supported_types.each do |type|
|
|
744
|
+
examples = Purl.type_examples(type)
|
|
745
|
+
registry_purls.concat(examples) if examples.any?
|
|
746
|
+
end
|
|
747
|
+
|
|
748
|
+
puts "📊 Testing with #{registry_purls.length} registry-supported PURLs"
|
|
749
|
+
puts
|
|
750
|
+
|
|
751
|
+
# Parse all PURLs first
|
|
752
|
+
parsed_purls = registry_purls.map { |purl| Purl.parse(purl) }
|
|
753
|
+
|
|
754
|
+
# Benchmark registry URL generation
|
|
755
|
+
puts "🔗 URL Generation Performance:"
|
|
756
|
+
url_time = Benchmark.realtime do
|
|
757
|
+
parsed_purls.each { |purl| purl.registry_url }
|
|
758
|
+
end
|
|
759
|
+
|
|
760
|
+
puts " Total time: #{(url_time * 1000).round(2)}ms"
|
|
761
|
+
puts " Average per URL: #{(url_time * 1000 / parsed_purls.length).round(3)}ms"
|
|
762
|
+
puts " URLs per second: #{(parsed_purls.length / url_time).round(0)}"
|
|
763
|
+
puts
|
|
764
|
+
|
|
765
|
+
# Benchmark versioned URL generation
|
|
766
|
+
puts "🏷️ Versioned URL Performance:"
|
|
767
|
+
versioned_time = Benchmark.realtime do
|
|
768
|
+
parsed_purls.each { |purl| purl.registry_url_with_version }
|
|
769
|
+
end
|
|
770
|
+
|
|
771
|
+
puts " Total time: #{(versioned_time * 1000).round(2)}ms"
|
|
772
|
+
puts " Average per URL: #{(versioned_time * 1000 / parsed_purls.length).round(3)}ms"
|
|
773
|
+
puts " URLs per second: #{(parsed_purls.length / versioned_time).round(0)}"
|
|
774
|
+
puts
|
|
775
|
+
|
|
776
|
+
# Compare parsing vs URL generation
|
|
777
|
+
parsing_time = Benchmark.realtime do
|
|
778
|
+
registry_purls.each { |purl| Purl.parse(purl) }
|
|
779
|
+
end
|
|
780
|
+
|
|
781
|
+
puts "⚖️ Performance Comparison:"
|
|
782
|
+
puts " Parsing: #{(parsing_time * 1000 / registry_purls.length).round(3)}ms per PURL"
|
|
783
|
+
puts " URL generation: #{(url_time * 1000 / parsed_purls.length).round(3)}ms per PURL"
|
|
784
|
+
puts " Versioned URLs: #{(versioned_time * 1000 / parsed_purls.length).round(3)}ms per PURL"
|
|
785
|
+
|
|
786
|
+
ratio = url_time / parsing_time
|
|
787
|
+
puts " URL gen vs parsing: #{ratio.round(2)}x #{ratio > 1 ? 'slower' : 'faster'}"
|
|
788
|
+
|
|
789
|
+
puts
|
|
790
|
+
puts "✅ Registry URL benchmarks completed!"
|
|
791
|
+
end
|
|
792
|
+
|
|
793
|
+
desc "Run all benchmarks"
|
|
794
|
+
task all: [:parse, :types, :registry] do
|
|
795
|
+
puts
|
|
796
|
+
puts "🎉 All benchmarks completed!"
|
|
797
|
+
puts " Use 'rake benchmark:parse' for parsing performance"
|
|
798
|
+
puts " Use 'rake benchmark:types' for type comparison"
|
|
799
|
+
puts " Use 'rake benchmark:registry' for URL generation"
|
|
800
|
+
end
|
|
801
|
+
end
|
data/lib/purl/package_url.rb
CHANGED
|
@@ -48,8 +48,8 @@ module Purl
|
|
|
48
48
|
# @return [String, nil] subpath within the package
|
|
49
49
|
attr_reader :subpath
|
|
50
50
|
|
|
51
|
-
VALID_TYPE_CHARS = /\A[a-zA-Z0-9\.\+\-]+\z
|
|
52
|
-
VALID_QUALIFIER_KEY_CHARS = /\A[a-zA-Z0-9\.\-_]+\z
|
|
51
|
+
VALID_TYPE_CHARS = /\A[a-zA-Z0-9\.\+\-]+\z/.freeze
|
|
52
|
+
VALID_QUALIFIER_KEY_CHARS = /\A[a-zA-Z0-9\.\-_]+\z/.freeze
|
|
53
53
|
|
|
54
54
|
# Create a new PackageURL instance
|
|
55
55
|
#
|
|
@@ -107,7 +107,7 @@ module Purl
|
|
|
107
107
|
|
|
108
108
|
# Remove the pkg: prefix and any leading slashes (they're not significant)
|
|
109
109
|
remainder = purl_string[4..-1]
|
|
110
|
-
remainder = remainder.sub(/\A\/+/, "")
|
|
110
|
+
remainder = remainder.sub(/\A\/+/, "") if remainder.start_with?("/")
|
|
111
111
|
|
|
112
112
|
# Split off qualifiers (query string) first
|
|
113
113
|
if remainder.include?("?")
|
|
@@ -223,17 +223,17 @@ module Purl
|
|
|
223
223
|
# purl = PackageURL.new(type: "gem", name: "rails", version: "7.0.0")
|
|
224
224
|
# puts purl.to_s # "pkg:gem/rails@7.0.0"
|
|
225
225
|
def to_s
|
|
226
|
-
|
|
226
|
+
parts = ["pkg:", type.downcase]
|
|
227
227
|
|
|
228
228
|
if namespace
|
|
229
229
|
# Encode namespace parts, but preserve the structure
|
|
230
230
|
namespace_parts = namespace.split("/").map do |part|
|
|
231
231
|
URI.encode_www_form_component(part)
|
|
232
232
|
end
|
|
233
|
-
|
|
233
|
+
parts << "/" << namespace_parts.join("/")
|
|
234
234
|
end
|
|
235
235
|
|
|
236
|
-
|
|
236
|
+
parts << "/" << URI.encode_www_form_component(name)
|
|
237
237
|
|
|
238
238
|
if version
|
|
239
239
|
# Special handling for version encoding - don't encode colon in certain contexts
|
|
@@ -244,7 +244,7 @@ module Purl
|
|
|
244
244
|
else
|
|
245
245
|
URI.encode_www_form_component(version)
|
|
246
246
|
end
|
|
247
|
-
|
|
247
|
+
parts << "@" << encoded_version
|
|
248
248
|
end
|
|
249
249
|
|
|
250
250
|
if subpath
|
|
@@ -253,7 +253,7 @@ module Purl
|
|
|
253
253
|
normalized_subpath = self.class.normalize_subpath(subpath)
|
|
254
254
|
if normalized_subpath
|
|
255
255
|
subpath_parts = normalized_subpath.split("/").map { |part| URI.encode_www_form_component(part) }
|
|
256
|
-
|
|
256
|
+
parts << "#" << subpath_parts.join("/")
|
|
257
257
|
end
|
|
258
258
|
end
|
|
259
259
|
|
|
@@ -265,10 +265,10 @@ module Purl
|
|
|
265
265
|
encoded_value = value.to_s # Don't encode values to match canonical form
|
|
266
266
|
"#{encoded_key}=#{encoded_value}"
|
|
267
267
|
end
|
|
268
|
-
|
|
268
|
+
parts << "?" << query_parts.join("&")
|
|
269
269
|
end
|
|
270
270
|
|
|
271
|
-
|
|
271
|
+
parts.join
|
|
272
272
|
end
|
|
273
273
|
|
|
274
274
|
# Convert the PackageURL to a hash representation
|
|
@@ -532,14 +532,15 @@ module Purl
|
|
|
532
532
|
end
|
|
533
533
|
|
|
534
534
|
def validate_conan_specific_rules
|
|
535
|
-
# For conan packages, if a namespace is present WITHOUT any qualifiers,
|
|
536
|
-
# it's ambiguous (
|
|
537
|
-
|
|
535
|
+
# For conan packages, if a namespace is present WITHOUT any qualifiers at all,
|
|
536
|
+
# it's ambiguous. However, any qualifiers (including build settings) make it unambiguous.
|
|
537
|
+
# According to the official spec, user/channel are only required if the package was published with them.
|
|
538
|
+
if @namespace && (@qualifiers.nil? || @qualifiers.empty?)
|
|
538
539
|
raise ValidationError.new(
|
|
539
|
-
"Conan PURLs with namespace require
|
|
540
|
+
"Conan PURLs with namespace require qualifiers to be unambiguous",
|
|
540
541
|
component: :qualifiers,
|
|
541
542
|
value: @qualifiers,
|
|
542
|
-
rule: "conan packages with namespace need
|
|
543
|
+
rule: "conan packages with namespace need qualifiers for disambiguation"
|
|
543
544
|
)
|
|
544
545
|
end
|
|
545
546
|
|
data/lib/purl/registry_url.rb
CHANGED
|
@@ -5,7 +5,7 @@ module Purl
|
|
|
5
5
|
# Load registry patterns from JSON configuration
|
|
6
6
|
def self.load_registry_patterns
|
|
7
7
|
@registry_patterns ||= begin
|
|
8
|
-
# Load
|
|
8
|
+
# Load extended registry configs
|
|
9
9
|
config_path = File.join(__dir__, "..", "..", "purl-types.json")
|
|
10
10
|
require "json"
|
|
11
11
|
config = JSON.parse(File.read(config_path))
|
|
@@ -16,37 +16,19 @@ module Purl
|
|
|
16
16
|
next unless type_config["registry_config"]
|
|
17
17
|
|
|
18
18
|
registry_config = type_config["registry_config"]
|
|
19
|
-
patterns[type] = build_pattern_config(type, registry_config)
|
|
19
|
+
patterns[type] = build_pattern_config(type, registry_config, type_config)
|
|
20
20
|
end
|
|
21
21
|
|
|
22
22
|
patterns
|
|
23
23
|
end
|
|
24
24
|
end
|
|
25
25
|
|
|
26
|
-
def self.build_pattern_config(type, config)
|
|
27
|
-
# Get the default registry for this type from
|
|
28
|
-
type_config = load_types_config["types"][type]
|
|
26
|
+
def self.build_pattern_config(type, config, type_config)
|
|
27
|
+
# Get the default registry for this type from the extended config
|
|
29
28
|
default_registry = type_config["default_registry"]
|
|
30
29
|
|
|
31
|
-
#
|
|
32
|
-
route_patterns = []
|
|
33
|
-
if default_registry
|
|
34
|
-
# Add all template variations
|
|
35
|
-
if config["path_template"]
|
|
36
|
-
route_patterns << default_registry + config["path_template"]
|
|
37
|
-
end
|
|
38
|
-
if config["namespace_path_template"]
|
|
39
|
-
route_patterns << default_registry + config["namespace_path_template"]
|
|
40
|
-
end
|
|
41
|
-
if config["version_path_template"]
|
|
42
|
-
route_patterns << default_registry + config["version_path_template"]
|
|
43
|
-
end
|
|
44
|
-
if config["namespace_version_path_template"]
|
|
45
|
-
route_patterns << default_registry + config["namespace_version_path_template"]
|
|
46
|
-
end
|
|
47
|
-
end
|
|
48
|
-
# Fall back to legacy route_patterns if available
|
|
49
|
-
route_patterns = config["route_patterns"] if route_patterns.empty? && config["route_patterns"]
|
|
30
|
+
# Use route_patterns directly from our extended registry config
|
|
31
|
+
route_patterns = config["route_patterns"] || []
|
|
50
32
|
|
|
51
33
|
# Build reverse regex from template or use legacy format
|
|
52
34
|
reverse_regex = nil
|
data/lib/purl/version.rb
CHANGED