Skip to content

Commit de6b657

Browse files
authored
Merge pull request #205 from package-url/add_validation_function
Add validation function
2 parents 62d1f73 + ceb97bc commit de6b657

6 files changed

Lines changed: 1004 additions & 18 deletions

File tree

etc/scripts/generate_validators.py

Lines changed: 267 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,267 @@
1+
# Generate a simple script based on provided list for package types
2+
3+
"""
4+
{
5+
"$schema": "https://packageurl.org/schemas/purl-type-definition.schema-1.0.json",
6+
"$id": "https://packageurl.org/types/pypi-definition.json",
7+
"type": "pypi",
8+
"type_name": "PyPI",
9+
"description": "Python packages",
10+
"repository": {
11+
"use_repository": true,
12+
"default_repository_url": "https://pypi.org",
13+
"note": "Previously https://pypi.python.org"
14+
},
15+
"namespace_definition": {
16+
"requirement": "prohibited",
17+
"note": "there is no namespace"
18+
},
19+
"name_definition": {
20+
"native_name": "name",
21+
"case_sensitive": false,
22+
"normalization_rules": [
23+
"Replace underscore _ with dash -",
24+
"Replace dot . with underscore _ when used in distribution (sdist, wheel) names"
25+
],
26+
"note": "PyPI treats - and _ as the same character and is not case sensitive. Therefore a PyPI package name must be lowercased and underscore _ replaced with a dash -. Note that PyPI itself is preserving the case of package names. When used in distribution and wheel names, the dot . is replaced with an underscore _"
27+
},
28+
"version_definition": {
29+
"case_sensitive": false,
30+
"native_name": "version"
31+
},
32+
"qualifiers_definition": [
33+
{
34+
"key": "file_name",
35+
"requirement": "optional",
36+
"description": "The file_name qualifier selects a particular distribution file (case-sensitive). For naming convention, see the Python Packaging User Guide on source distributions https://packaging.python.org/en/latest/specifications/source-distribution-format/#source-distribution-file-name and on binary distributions https://packaging.python.org/en/latest/specifications/binary-distribution-format/#file-name-convention and the rules for platform compatibility tags https://packaging.python.org/en/latest/specifications/platform-compatibility-tags/"
37+
}
38+
],
39+
"examples": [
40+
"pkg:pypi/django@1.11.1",
41+
"pkg:pypi/django@1.11.1?filename=Django-1.11.1.tar.gz",
42+
"pkg:pypi/django@1.11.1?filename=Django-1.11.1-py2.py3-none-any.whl",
43+
"pkg:pypi/django-allauth@12.23"
44+
]
45+
}
46+
"""
47+
from packageurl import PackageURL
48+
from pathlib import Path
49+
import json
50+
51+
HEADER = '''# Copyright (c) the purl authors
52+
# SPDX-License-Identifier: MIT
53+
#
54+
# Permission is hereby granted, free of charge, to any person obtaining a copy
55+
# of this software and associated documentation files (the "Software"), to deal
56+
# in the Software without restriction, including without limitation the rights
57+
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
58+
# copies of the Software, and to permit persons to whom the Software is
59+
# furnished to do so, subject to the following conditions:
60+
#
61+
# The above copyright notice and this permission notice shall be included in all
62+
# copies or substantial portions of the Software.
63+
#
64+
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
65+
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
66+
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
67+
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
68+
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
69+
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
70+
# SOFTWARE.
71+
72+
# Visit https://github.com/package-url/packageurl-python for support and
73+
# download.
74+
75+
"""
76+
Validate each type according to the PURL spec type definitions
77+
"""
78+
79+
class TypeValidator:
80+
@classmethod
81+
def validate(cls, purl, strict=False):
82+
if not strict:
83+
purl = cls.normalize(purl)
84+
85+
if cls.namespace_requirement == "prohibited" and purl.namespace:
86+
yield f"Namespace is prohibited for purl type: {cls.type!r}"
87+
88+
elif cls.namespace_requirement == "required" and not purl.namespace:
89+
yield f"Namespace is required for purl type: {cls.type!r}"
90+
91+
if (
92+
not cls.namespace_case_sensitive
93+
and purl.namespace
94+
and purl.namespace.lower() != purl.namespace
95+
):
96+
yield f"Namespace is not lowercased for purl type: {cls.type!r}"
97+
98+
if not cls.name_case_sensitive and purl.name and purl.name.lower() != purl.name:
99+
yield f"Name is not lowercased for purl type: {cls.type!r}"
100+
101+
if not cls.version_case_sensitive and purl.version and purl.version.lower() != purl.version:
102+
yield f"Version is not lowercased for purl type: {cls.type!r}"
103+
104+
yield from cls.validate_type(purl, strict=strict)
105+
106+
@classmethod
107+
def normalize(cls, purl):
108+
from packageurl import PackageURL
109+
from packageurl import normalize
110+
111+
type_norm, namespace_norm, name_norm, version_norm, qualifiers_norm, subpath_norm = (
112+
normalize(
113+
purl.type,
114+
purl.namespace,
115+
purl.name,
116+
purl.version,
117+
purl.qualifiers,
118+
purl.subpath,
119+
encode=False,
120+
)
121+
)
122+
123+
return PackageURL(
124+
type=type_norm,
125+
namespace=namespace_norm,
126+
name=name_norm,
127+
version=version_norm,
128+
qualifiers=qualifiers_norm,
129+
subpath=subpath_norm,
130+
)
131+
132+
@classmethod
133+
def validate_type(cls, purl, strict=False):
134+
if strict:
135+
yield from cls.validate_qualifiers(purl=purl)
136+
137+
@classmethod
138+
def validate_qualifiers(cls, purl):
139+
if not purl.qualifiers:
140+
return
141+
142+
purl_qualifiers_keys = set(purl.qualifiers.keys())
143+
allowed_qualifiers_set = cls.allowed_qualifiers
144+
145+
disallowed = purl_qualifiers_keys - allowed_qualifiers_set
146+
147+
if disallowed:
148+
yield (
149+
f"Invalid qualifiers found: {', '.join(sorted(disallowed))}. "
150+
f"Allowed qualifiers are: {', '.join(sorted(allowed_qualifiers_set))}"
151+
)
152+
'''
153+
154+
155+
TEMPLATE = """
156+
class {class_name}({validator_class}):
157+
type = "{type}"
158+
type_name = "{type_name}"
159+
description = '''{description}'''
160+
use_repository = {use_repository}
161+
default_repository_url = "{default_repository_url}"
162+
namespace_requirement = "{namespace_requirement}"
163+
allowed_qualifiers = {allowed_qualifiers}
164+
namespace_case_sensitive = {namespace_case_sensitive}
165+
name_case_sensitive = {name_case_sensitive}
166+
version_case_sensitive = {version_case_sensitive}
167+
purl_pattern = "{purl_pattern}"
168+
"""
169+
170+
171+
def generate_validators():
172+
"""
173+
Generate validators for all package types defined in the packageurl specification.
174+
"""
175+
176+
base_dir = Path(__file__).parent.parent.parent
177+
178+
types_dir = base_dir / "spec" / "types"
179+
180+
script_parts = [HEADER]
181+
182+
validators_by_type = {}
183+
184+
for type in sorted(types_dir.glob("*.json")):
185+
type_def = json.loads(type.read_text())
186+
187+
_type = type_def["type"]
188+
standard_validator_class = "TypeValidator"
189+
190+
class_prefix = _type.capitalize()
191+
class_name = f"{class_prefix}{standard_validator_class}"
192+
validators_by_type[_type] = class_name
193+
name_normalization_rules=type_def["name_definition"].get("normalization_rules") or []
194+
allowed_qualifiers = [defintion.get("key") for defintion in type_def.get("qualifiers_definition") or []]
195+
namespace_case_sensitive = type_def["namespace_definition"].get("case_sensitive") or False
196+
name_case_sensitive = type_def["name_definition"].get("case_sensitive") or False
197+
version_definition = type_def.get("version_definition") or {}
198+
version_case_sensitive = version_definition.get("case_sensitive") or True
199+
repository = type_def.get("repository")
200+
use_repository_url = repository.get("use_repository") or False
201+
202+
if use_repository_url and "repsitory_url" not in allowed_qualifiers:
203+
allowed_qualifiers.append("repository_url")
204+
205+
allowed_qualifiers = set(allowed_qualifiers)
206+
207+
type_validator = TEMPLATE.format(**dict(
208+
class_name=class_name,
209+
validator_class=standard_validator_class,
210+
type=_type,
211+
type_name=type_def["type_name"],
212+
description=type_def["description"],
213+
use_repository=type_def["repository"]["use_repository"],
214+
default_repository_url=type_def["repository"].get("default_repository_url") or "",
215+
namespace_requirement=type_def["namespace_definition"]["requirement"],
216+
name_normalization_rules=name_normalization_rules,
217+
allowed_qualifiers=allowed_qualifiers or [],
218+
namespace_case_sensitive=namespace_case_sensitive,
219+
name_case_sensitive=name_case_sensitive,
220+
version_case_sensitive=version_case_sensitive,
221+
purl_pattern=f"pkg:{_type}/.*"
222+
))
223+
224+
script_parts.append(type_validator)
225+
226+
script_parts.append(generate_validators_by_type(validators_by_type=validators_by_type))
227+
# script_parts.append(attach_router(validators_by_type.values()))
228+
229+
validate_script = base_dir / "src" / "packageurl" / "validate.py"
230+
231+
validate_script.write_text("\n".join(script_parts))
232+
233+
234+
def generate_validators_by_type(validators_by_type):
235+
"""
236+
Return a python snippet that maps a type to it's TypeValidator class
237+
"""
238+
snippets = []
239+
for type, class_name in validators_by_type.items():
240+
snippet = f" {type!r} : {class_name},"
241+
snippets.append(snippet)
242+
243+
snippets = "\n".join(snippets)
244+
start = "VALIDATORS_BY_TYPE = {"
245+
end = "}"
246+
return f"{start}\n{snippets}\n{end}"
247+
248+
def attach_router(classes):
249+
snippets = []
250+
for class_name in classes:
251+
snippet = f" {class_name},"
252+
snippets.append(snippet)
253+
snippets = "\n".join(snippets)
254+
start = "PACKAGE_REGISTRY = [ \n"
255+
end = "\n ]"
256+
classes = f"{start}{snippets}{end}"
257+
router_code = '''
258+
validate_router = Router()
259+
260+
for pkg_class in PACKAGE_REGISTRY:
261+
validate_router.append(pattern=pkg_class.purl_pattern, endpoint=pkg_class.validate)
262+
'''
263+
return f"{classes}{router_code}"
264+
265+
266+
if __name__ == "__main__":
267+
generate_validators()

spec

Submodule spec updated 59 files

0 commit comments

Comments
 (0)