purl 1.1.1 → 1.1.2
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/CHANGELOG.md +51 -0
- data/README.md +4 -0
- data/Rakefile +237 -0
- data/lib/purl/package_url.rb +10 -10
- data/lib/purl/version.rb +1 -1
- metadata +2 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: f2e3a83fa55008ae8b31be8d3e5c7306556e4b7d4371e6db46927deae1edee3c
|
|
4
|
+
data.tar.gz: 5a092dfa8e630db8ca28064b1e101a62320d601708b38351a92b2f5431d692dc
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: a8db461d6066ce37a6fb9c50c373e2dd6739fddb123ac0627c14f645dadc0b610b24e30a6d0dbcda28148f103870ba8cbc28e34efb65ab026b463752753fcd6d
|
|
7
|
+
data.tar.gz: 5420d53af149d4c1ef1fd8337f232bf4f03f420fcaa3a2de32a38751505167c0eb3d431b382ad803f29ed8b2230f80789cbcdb9c15b4e43a0af5b69fcb554031
|
data/CHANGELOG.md
CHANGED
|
@@ -7,6 +7,57 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|
|
7
7
|
|
|
8
8
|
## [Unreleased]
|
|
9
9
|
|
|
10
|
+
## [1.1.2] - 2025-07-25
|
|
11
|
+
|
|
12
|
+
### Added
|
|
13
|
+
- Comprehensive benchmarking rake tasks for performance analysis
|
|
14
|
+
- `rake benchmark:parse` - PURL parsing performance benchmarks
|
|
15
|
+
- `rake benchmark:types` - Package type parsing comparison
|
|
16
|
+
- `rake benchmark:registry` - Registry URL generation benchmarks
|
|
17
|
+
- `rake benchmark:all` - Run all benchmarks
|
|
18
|
+
|
|
19
|
+
### Improved
|
|
20
|
+
- **26% improvement in parsing throughput** (~175K PURLs/second)
|
|
21
|
+
- **8% improvement in string conversion performance** (~315K conversions/second)
|
|
22
|
+
- **7% improvement in object creation** (~280K objects/second)
|
|
23
|
+
- Optimized string operations in parse method with conditional regex application
|
|
24
|
+
- Reduced string allocations in `to_s` method using array joining
|
|
25
|
+
- Cached compiled regexes with `.freeze` for better performance
|
|
26
|
+
- Lower memory allocation pressure in high-throughput scenarios
|
|
27
|
+
|
|
28
|
+
## [1.1.1] - 2025-07-25
|
|
29
|
+
|
|
30
|
+
### Added
|
|
31
|
+
- Comprehensive RDoc documentation for all classes and methods
|
|
32
|
+
- RDoc task in Rakefile with proper configuration
|
|
33
|
+
- API documentation link in README
|
|
34
|
+
|
|
35
|
+
### Fixed
|
|
36
|
+
- Add bigdecimal gem dependency to resolve potential loading issues
|
|
37
|
+
- Improve JSON schema loading error handling
|
|
38
|
+
|
|
39
|
+
## [1.1.0] - 2025-07-25
|
|
40
|
+
|
|
41
|
+
### Added
|
|
42
|
+
- JSON schema validation for configuration files (`purl-types.json` and `test-suite-data.json`)
|
|
43
|
+
- New rake tasks: `spec:validate_schemas` and `spec:validate_examples`
|
|
44
|
+
- Examples for all package types in configuration
|
|
45
|
+
- Default registry URLs for package types
|
|
46
|
+
- Enhanced reverse parsing support for additional package types
|
|
47
|
+
- `with` method for creating modified PURL objects (immutable pattern)
|
|
48
|
+
- FUNDING.yml for project sponsorship support
|
|
49
|
+
|
|
50
|
+
### Enhanced
|
|
51
|
+
- Improved README documentation with custom registry examples
|
|
52
|
+
- Enhanced test coverage for new functionality
|
|
53
|
+
- Better compliance test output formatting
|
|
54
|
+
- Comprehensive package type examples and validation
|
|
55
|
+
|
|
56
|
+
### Documentation
|
|
57
|
+
- Updated documentation to remove emoji and enhance readability
|
|
58
|
+
- Added comprehensive examples for custom registry usage
|
|
59
|
+
- Enhanced API documentation throughout
|
|
60
|
+
|
|
10
61
|
## [1.0.0] - 2025-01-24
|
|
11
62
|
|
|
12
63
|
### 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)
|
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
|
data/lib/purl/version.rb
CHANGED
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: purl
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 1.1.
|
|
4
|
+
version: 1.1.2
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Andrew Nesbitt
|
|
@@ -41,6 +41,7 @@ metadata:
|
|
|
41
41
|
allowed_push_host: https://rubygems.org
|
|
42
42
|
homepage_uri: https://github.com/andrew/purl
|
|
43
43
|
source_code_uri: https://github.com/andrew/purl
|
|
44
|
+
changelog_uri: https://github.com/andrew/purl/blob/main/CHANGELOG.md
|
|
44
45
|
rdoc_options: []
|
|
45
46
|
require_paths:
|
|
46
47
|
- lib
|