Skip to content

Commit

Permalink
restrict suite name character set
Browse files Browse the repository at this point in the history
  • Loading branch information
oliver-sanders committed Aug 7, 2019
1 parent ec4c343 commit e0a3604
Show file tree
Hide file tree
Showing 3 changed files with 150 additions and 1 deletion.
9 changes: 8 additions & 1 deletion cylc/flow/suite_srv_files_mgr.py
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@
import cylc.flow.flags
from cylc.flow.hostuserutil import (
get_host, get_user, is_remote, is_remote_host, is_remote_user)
from cylc.flow.unicode_rules import SuiteNameValidator


class SuiteSrvFilesManager(object):
Expand Down Expand Up @@ -396,7 +397,7 @@ def register(self, reg=None, source=None, redirect=False):
redirect (bool): allow reuse of existing name and run directory.
Return:
The registered suite name (which may be computed here).
str: The registered suite name (which may be computed here).
Raise:
SuiteServiceFileError:
Expand All @@ -407,6 +408,12 @@ def register(self, reg=None, source=None, redirect=False):
if reg is None:
reg = os.path.basename(os.getcwd())

is_valid, message = SuiteNameValidator.validate(reg)
if not is_valid:
raise SuiteServiceFileError(
f'invalid suite name - {message}'
)

if os.path.isabs(reg):
raise SuiteServiceFileError(
"suite name cannot be an absolute path: %s" % reg)
Expand Down
14 changes: 14 additions & 0 deletions cylc/flow/tests/test_suite_srv_files_mgr.py
Original file line number Diff line number Diff line change
Expand Up @@ -165,6 +165,20 @@ def get_register_test_cases():
None, # expected return value
SuiteServiceFileError, # expected exception
"cannot be an absolute path" # expected part of exception message
),
# 10 invalid suite name
("-foo", # reg
None, # source
False, # redirect,
None, # cwd
True, # isabs
True, # isfile
None, # suite_srv_dir
None, # readlink
None, # expected symlink
None, # expected return value
SuiteServiceFileError, # expected exception
"can not start with: ., -" # expected part of exception message
)
]

Expand Down
128 changes: 128 additions & 0 deletions cylc/flow/unicode_rules.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,128 @@
"""Module for unicode restrictions"""

import re


ENGLISH_REGEX_MAP = {
r'\w': 'alphanumeric',
r'\-': '-',
r'\.': '.',
r'\/': '/'
}


def regex_chars_to_text(chars):
r"""Return a string representing a regex component.
Examples:
>>> regex_chars_to_text(['a', 'b', 'c'])
['a', 'b', 'c']
>>> regex_chars_to_text([r'\-', r'\.', r'\/'])
['-', '.', '/']
>>> regex_chars_to_text([r'\w'])
['alphanumeric']
"""
return [
ENGLISH_REGEX_MAP.get(char, char)
for char in chars
]


def length(minimum, maximum):
"""Restrict character length.
Example:
>>> regex, message = length(0, 5)
>>> message
'must be between 0 and 5 characters long'
>>> bool(regex.match('abcde'))
True
>>> bool(regex.match('abcdef'))
False
"""
return (
re.compile(r'^.{%d,%d}$' % (minimum, maximum)),
f'must be between {minimum} and {maximum} characters long'
)


def allowed_characters(*chars):
"""Restrict permitted characters.
Example:
>>> regex, message = allowed_characters('a', 'b', 'c')
>>> message
'can only contain: a, b, c'
>>> bool(regex.match('abc'))
True
>>> bool(regex.match('def'))
False
"""
return (
re.compile(r'^[%s]+$' % ''.join(chars)),
f'can only contain: {", ".join(regex_chars_to_text(chars))}'
)


def not_starts_with(*chars):
"""Restrict first character.
Example:
>>> regex, message = not_starts_with('a', 'b', 'c')
>>> message
'can not start with: a, b, c'
>>> bool(regex.match('def'))
True
>>> bool(regex.match('adef'))
False
"""
return (
re.compile(r'^[^%s]' % ''.join(chars)),
f'can not start with: {", ".join(regex_chars_to_text(chars))}'
)


class UnicodeRuleChecker():

RULES = []

@classmethod
def __init_subclass__(cls):
cls.__doc__ = cls.__doc__ + '\n' if cls.__doc__ else ''
cls.__doc__ += '\n' + '\n'.join([
f'* {message}'
for regex, message in cls.RULES
])

@classmethod
def validate(cls, string):
"""Run this collection of rules against the given string.
Args:
string (str):
String to validate.
Returns:
tuple - (outcome, message)
outcome (bool) - True if all patterns match.
message (str) - User-friendly error message.
"""
for rule, message in cls.RULES:
if not rule.match(string):
return (False, message)
return (True, None)


class SuiteNameValidator(UnicodeRuleChecker):
"""The rules for valid suite names:"""

RULES = [
length(1, 254),
not_starts_with(r'\.', r'\-'),
allowed_characters(r'\w', r'\/', '_', '+', r'\-', r'\.', '@')
]

0 comments on commit e0a3604

Please sign in to comment.