diff --git a/snapcraft/models/project.py b/snapcraft/models/project.py index 258b5aba23..5db1c0c66e 100644 --- a/snapcraft/models/project.py +++ b/snapcraft/models/project.py @@ -238,26 +238,44 @@ def _validate_version_name(version: str, model_name: str) -> None: ) -def _validate_component_name(name: str) -> None: - """Validate a component name.""" - if not re.fullmatch(r"[a-z-]*[a-z][a-z-]*", name): - raise ValueError( - "Component names can only use ASCII lowercase letters and hyphens" - ) +def _validate_name(*, name: str, field_name: str) -> str: + """Validate a name. - if name.startswith("snap-"): + :param name: The name to validate. + :param field_name: The name of the field being validated. + + :returns: The validated name. + """ + if not re.match(r"^[a-z0-9-]*[a-z][a-z0-9-]*$", name): raise ValueError( - "Component names cannot start with the reserved namespace 'snap-'" + f"{field_name} names can only use lowercase alphanumeric " + "and hyphens and must have at least one letter" ) if name.startswith("-"): - raise ValueError("Component names cannot start with a hyphen") + raise ValueError(f"{field_name} names cannot start with a hyphen") if name.endswith("-"): - raise ValueError("Component names cannot end with a hyphen") + raise ValueError(f"{field_name} names cannot end with a hyphen") if "--" in name: - raise ValueError("Component names cannot have two hyphens in a row") + raise ValueError(f"{field_name} names cannot have two hyphens in a row") + + return name + + +def _validate_component(name: str) -> str: + """Validate a component name. + + :param name: The component name to validate. + + :returns: The validated component name. + """ + if name.startswith("snap-"): + raise ValueError( + "component names cannot start with the reserved prefix 'snap-'" + ) + return _validate_name(name=name, field_name="component") def _get_partitions_from_components( @@ -731,23 +749,8 @@ def _validate_mandatory_base(cls, values): @pydantic.validator("name") @classmethod - def _validate_name(cls, name): - if not re.match(r"^[a-z0-9-]*[a-z][a-z0-9-]*$", name): - raise ValueError( - "Snap names can only use ASCII lowercase letters, numbers, and hyphens, " - "and must have at least one letter" - ) - - if name.startswith("-"): - raise ValueError("Snap names cannot start with a hyphen") - - if name.endswith("-"): - raise ValueError("Snap names cannot end with a hyphen") - - if "--" in name: - raise ValueError("Snap names cannot have two hyphens in a row") - - return name + def _validate_snap_name(cls, name): + return _validate_name(name=name, field_name="snap") @pydantic.validator("version") @classmethod @@ -764,7 +767,7 @@ def _validate_version(cls, version, values): def _validate_components(cls, components): """Validate component names.""" for component_name in components.keys(): - _validate_component_name(component_name) + _validate_component(name=component_name) return components @@ -1077,7 +1080,7 @@ class ComponentProject(models.CraftBaseModel, extra=pydantic.Extra.ignore): def _validate_components(cls, components): """Validate component names.""" for component_name in components.keys(): - _validate_component_name(component_name) + _validate_component(name=component_name) return components diff --git a/tests/unit/models/test_projects.py b/tests/unit/models/test_projects.py index a8d1305021..136b4bd137 100644 --- a/tests/unit/models/test_projects.py +++ b/tests/unit/models/test_projects.py @@ -235,13 +235,13 @@ def test_project_name_valid(self, name, project_yaml_data): @pytest.mark.parametrize( "name,error", [ - ("name_with_underscores", "Snap names can only use"), - ("name-with-UPPERCASE", "Snap names can only use"), - ("name with spaces", "Snap names can only use"), - ("-name-starts-with-hyphen", "Snap names cannot start with a hyphen"), - ("name-ends-with-hyphen-", "Snap names cannot end with a hyphen"), - ("name-has--two-hyphens", "Snap names cannot have two hyphens in a row"), - ("123456", "Snap names can only use"), + ("name_with_underscores", "snap names can only use"), + ("name-with-UPPERCASE", "snap names can only use"), + ("name with spaces", "snap names can only use"), + ("-name-starts-with-hyphen", "snap names cannot start with a hyphen"), + ("name-ends-with-hyphen-", "snap names cannot end with a hyphen"), + ("name-has--two-hyphens", "snap names cannot have two hyphens in a row"), + ("123456", "snap names can only use"), ( "a2345678901234567890123456789012345678901", "ensure this value has at most 40 characters", @@ -2490,7 +2490,15 @@ def test_component_type_invalid( project.unmarshal(project_yaml_data(components=component)) @pytest.mark.parametrize( - "name", ["name", "name-with-dashes", "x" * 40, "foo-snap-bar"] + "name", + [ + "name", + "name-with-dashes", + "name-with-numbers-0123", + "0123-name-with-numbers", + "x" * 40, + "foo-snap-bar", + ], ) def test_component_name_valid( self, project, name, project_yaml_data, stub_component_data @@ -2505,26 +2513,21 @@ def test_component_name_valid( @pytest.mark.parametrize( "name,error", [ - ( - "snap-", - "Component names cannot start with the reserved namespace 'snap-'", - ), - ( + pytest.param( "snap-foo", - "Component names cannot start with the reserved namespace 'snap-'", + "component names cannot start with the reserved prefix 'snap-'", + id="reserved prefix", ), - ("123456", "Component names can only use"), - ("name-ends-with-digits-0123", "Component names can only use"), - ("456-name-starts-with-digits", "Component names can only use"), - ("name-789-contains-digits", "Component names can only use"), - ("name_with_underscores", "Component names can only use"), - ("name-with-UPPERCASE", "Component names can only use"), - ("name with spaces", "Component names can only use"), - ("-name-starts-with-hyphen", "Component names cannot start with a hyphen"), - ("name-ends-with-hyphen-", "Component names cannot end with a hyphen"), + pytest.param("123456", "component names can only use", id="no letters"), + ("name_with_underscores", "component names can only use"), + ("name-with-UPPERCASE", "component names can only use"), + ("name with spaces", "component names can only use"), + ("name-with-$symbols", "component names can only use"), + ("-name-starts-with-hyphen", "component names cannot start with a hyphen"), + ("name-ends-with-hyphen-", "component names cannot end with a hyphen"), ( "name-has--two-hyphens", - "Component names cannot have two hyphens in a row", + "component names cannot have two hyphens in a row", ), ("x" * 41, "ensure this value has at most 40 characters"), ],