Skip to content

Commit 494dbbd

Browse files
committed
updates namespace regex to fit test strings
1 parent 3993da4 commit 494dbbd

File tree

5 files changed

+47
-31
lines changed

5 files changed

+47
-31
lines changed

data/schema/v2/Decision_Point_Value_Selection-2-0-0.schema.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@
1313
],
1414
"maxLength": 1000,
1515
"minLength": 3,
16-
"pattern": "^(?=.{3,1000}$)(x_)?[a-z0-9]{3}([/.-]?[a-z0-9]+){0,997}$$",
16+
"pattern": "^(?=.{3,1000}$)((x_(?!.*[.-]{2,})[a-z][a-z0-9]{2,}(?:[.-][a-z0-9]+)*|(?!.*[.-]{2,})[a-z][a-z0-9]{2,}(?:[.-][a-z0-9]+)*))((/((([A-Za-z]{2,3}(-[A-Za-z]{3}(-[A-Za-z]{3}){0,2})?|[A-Za-z]{4,8})(-[A-Za-z]{4})?(-([A-Za-z]{2}|[0-9]{3}))?(-([A-Za-z0-9]{5,8}|[0-9][A-Za-z0-9]{3}))*(-[A-WY-Za-wy-z0-9](-[A-Za-z0-9]{2,8})+)*(-[Xx](-[A-Za-z0-9]{1,8})+)?|[Xx](-[A-Za-z0-9]{1,8})+|[Ii]-[Dd][Ee][Ff][Aa][Uu][Ll][Tt]|[Ii]-[Mm][Ii][Nn][Gg][Oo]))/|//)(?!.*[.-]{2,})[a-zA-Z][a-zA-Z0-9]*(?:[.-][a-zA-Z0-9]+)*(?:/(?!.*[.-]{2,})[a-zA-Z][a-zA-Z0-9]*(?:[.-][a-zA-Z0-9]+)*)*)?$",
1717
"title": "Namespace",
1818
"type": "string"
1919
},

src/ssvc/namespaces.py

Lines changed: 41 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -29,69 +29,83 @@
2929

3030
from pydantic import Field
3131

32-
X_PFX = "x_"
33-
"""The prefix for extension namespaces. Extension namespaces must start with this prefix."""
34-
3532
MIN_NS_LENGTH = 3
3633
MAX_NS_LENGTH = 1000
3734
NS_LENGTH_INTERVAL = MAX_NS_LENGTH - MIN_NS_LENGTH
3835

39-
4036
# from https://docs.oasis-open.org/csaf/csaf/v2.0/os/csaf-v2.0-os.html
4137
BCP_47_PATTERN = r"(([A-Za-z]{2,3}(-[A-Za-z]{3}(-[A-Za-z]{3}){0,2})?|[A-Za-z]{4,8})(-[A-Za-z]{4})?(-([A-Za-z]{2}|[0-9]{3}))?(-([A-Za-z0-9]{5,8}|[0-9][A-Za-z0-9]{3}))*(-[A-WY-Za-wy-z0-9](-[A-Za-z0-9]{2,8})+)*(-[Xx](-[A-Za-z0-9]{1,8})+)?|[Xx](-[A-Za-z0-9]{1,8})+|[Ii]-[Dd][Ee][Ff][Aa][Uu][Ll][Tt]|[Ii]-[Mm][Ii][Nn][Gg][Oo])"
38+
"""A regular expression pattern for BCP-47 language tags."""
4239

4340
LENGTH_CHECK_PATTERN = rf"(?=.{{{MIN_NS_LENGTH},{MAX_NS_LENGTH}}}$)"
4441
"""Ensures the string is between MIN_NS_LENGTH and MAX_NS_LENGTH characters long."""
4542

4643
# Base namespace part (before any extensions) allows . and - with restrictions
4744
BASE_PATTERN = (
4845
r"(?!.*[.-]{2,})" # no consecutive separators
49-
r"[a-z][a-z0-9]{2,}" # first part starts with a letter, followed by one or more alphanumeric characters
46+
r"[a-z][a-z0-9]{2,}" # first part starts with a letter, followed by three or more alphanumeric characters
5047
r"(?:[.-][a-z0-9]+)*" # remaining parts can have alphanumeric characters and single . or - separators
5148
)
49+
"""The base pattern for namespaces, which must start with a letter and contain at least 3 alphanumeric characters."""
5250

5351
X_PFX = "x_"
52+
"""The prefix for extension namespaces. Extension namespaces must start with this prefix."""
53+
5454
EXPERIMENTAL_BASE = rf"{X_PFX}{BASE_PATTERN}"
55+
f"""The base pattern for experimental namespaces, which must start with the {X_PFX} prefix,
56+
followed by a string matching the base pattern."""
57+
5558
BASE_NS_PATTERN = rf"({EXPERIMENTAL_BASE}|{BASE_PATTERN})"
59+
"""The complete base namespace pattern, which allows for experimental namespaces."""
5660

5761
# Extension segment pattern (alphanumeric + limited punctuation, no consecutive punctuation, ends with alphanumeric)
5862
EXT_SEGMENT_PATTERN = (
5963
r"(?!.*[.-]{2,})" # no consecutive separators
60-
r"[a-zA-Z0-9]+" # first part starts with a letter, followed by one or more alphanumeric characters
64+
r"[a-zA-Z][a-zA-Z0-9]*" # first part starts with a letter, followed by one or more alphanumeric characters
6165
r"(?:[.-][a-zA-Z0-9]+)*" # remaining parts can have alphanumeric characters and single ., -, / separators
6266
)
67+
"""The pattern for extension segments in namespaces, which must start with a letter and contain alphanumeric characters or
68+
limited punctuation characters (., -), with no consecutive punctuation characters allowed."""
6369

6470
# Language extension pattern (BCP-47 or empty for //)
65-
LANG_EXT_PATTERN = rf"(/({BCP_47_PATTERN})|/)"
71+
LANG_EXT_PATTERN = rf"(/({BCP_47_PATTERN})/|//)"
72+
"""The pattern for the first extension segment, which must be either a valid BCP-47 tag or empty (//)."""
6673

6774
# Subsequent extension segments
68-
SUBSEQUENT_EXT_PATTERN = rf"(/{EXT_SEGMENT_PATTERN})*"
75+
SUBSEQUENT_EXT_PATTERN = rf"{EXT_SEGMENT_PATTERN}(?:/{EXT_SEGMENT_PATTERN})*"
76+
"""The pattern for subsequent extension segments, which must follow the rules for extension segments, delimited by slashes (/)."""
6977

7078
# Complete pattern with length validation
7179
NS_PATTERN = re.compile(
72-
rf"^{LENGTH_CHECK_PATTERN}{BASE_NS_PATTERN}({LANG_EXT_PATTERN}{SUBSEQUENT_EXT_PATTERN})?$"
80+
rf"^{LENGTH_CHECK_PATTERN}({BASE_NS_PATTERN})({LANG_EXT_PATTERN}{SUBSEQUENT_EXT_PATTERN})?$"
7381
)
74-
f"""The regular expression pattern for validating namespaces.
82+
f"""The full regular expression pattern for validating namespaces.
83+
84+
!!! note "Length Requirements"
7585
76-
!!! note "Namespace Validation Rules"
86+
- Namespaces must be between {MIN_NS_LENGTH} and {MAX_NS_LENGTH} characters long.
7787
78-
Namespace values must
88+
!!! note "Base Namespace Requirements"
89+
90+
- Must start with a lowercase letter
91+
- Must contain at least 3 total characters in the base part (after the optional experimental/private prefix)
92+
- Must contain only lowercase letters, numbers, dots (`.`), and hyphens (`-`)
93+
- Must not contain consecutive dots or hyphens (no `..`, `--`, `.-`, `-.`, `---`, etc.)
94+
- May optionally start with the experimental/private prefix `{X_PFX}`.
95+
96+
!!! note "Extension Requirements (Optional)"
7997
80-
- be {MIN_NS_LENGTH}-{MAX_NS_LENGTH} characters long
81-
- optionally start with the experimental/private prefix `{X_PFX}`
82-
- after the optional experimental/private prefix, they must:
83-
- start with a letter
84-
- contain at least 3 alphanumeric characters (longer is permitted)
85-
- contain only lowercase alphanumeric characters and limited punctuation characters (`.`, `-`)
86-
- extensions are supported and optional, and are delineated by slashes (`/`)
87-
- more than one extension segment is allowed, however:
88-
- the first extension segment, if present, is reserved for a BCP-47 language tag, otherwise it must be empty
89-
- if no BCP-47 tag is present, the first extension segment must be empty (i.e., `//`)
90-
- double slashes (`//`) are *only* permitted in the *first segment* to indicate no BCP-47 tag
91-
- beyond the first extension segment, subsequent segments must:
92-
- contain only alphanumeric characters and limited punctuation characters (`.`, `-`)
93-
- have only one punctuation character in a row (no double dashes or dots)
94-
- end with an alphanumeric character
98+
- Extensions are optional
99+
- Extensions must be delineated by slashes (`/`)
100+
- If any extension segments are present, the following rules apply:
101+
- The first extension segment, must be a valid BCP-47 language tag or empty (i.e., `//`).
102+
- Subsequent extension segments:
103+
- must start with a letter (upper or lowercase)
104+
- may contain letters, numbers, dots (`.`), and hyphens (`-`)
105+
- must not start or end with a dot or hyphen
106+
- must not contain consecutive dots or hyphens (no `..`, `--`, `.-`, `-.`, `---`, etc.)
107+
- are separated by single forward slashes (`/`)
108+
- multiple extension segments are allowed
95109
96110
"""
97111

src/test/test_mixins.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -114,7 +114,7 @@ def test_namespaced_create(self):
114114
# custom namespaces are allowed as long as they start with x_
115115
for _ in range(100):
116116
# we're just fuzzing some random strings here
117-
ns = f"x_{randint(1000,1000000)}"
117+
ns = f"x_a{randint(1000,1000000)}"
118118
obj = _Namespaced(namespace=ns)
119119
self.assertEqual(obj.namespace, ns)
120120

src/test/test_namespaces.py

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -34,8 +34,9 @@ def test_ns_pattern(self):
3434
"foo",
3535
"foo.bar",
3636
"foo.bar.baz",
37-
"foo/bar/baz/quux",
38-
"foo.bar/baz.quux",
37+
"foo/jp-JP/bar.baz/quux",
38+
"foo//bar/baz/quux",
39+
"foo.bar//baz.quux",
3940
]
4041
should_match.extend([f"x_{ns}" for ns in should_match])
4142

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,7 @@ def setUp(self):
4747
"x_custom//extension", # double slash is okay when it's the first segment
4848
"ssvc/de-DE/reference-arch-1", # valid BCP-47 tag with dashes
4949
"x_test/pl-PL/foo/bar/baz/quux", # valid BCP-47 tag and multiple segments
50+
"foo.bar//baz.quux", # valid namespace with x_ prefix and mixed segments
5051
]
5152
self.expect_fail = [
5253
"999", # invalid namespace, numeric only

0 commit comments

Comments
 (0)