Coverage for /home/runner/work/viur-core/viur-core/viur/src/viur/core/utils/string.py: 77%
29 statements
« prev ^ index » next coverage.py v7.9.1, created at 2025-06-26 11:31 +0000
« prev ^ index » next coverage.py v7.9.1, created at 2025-06-26 11:31 +0000
1"""
2ViUR utility functions regarding string processing.
3"""
4import re
5import secrets
6import string
7import unicodedata
8import warnings
11def random(length: int = 13) -> str:
12 """
13 Return a string containing random characters of given *length*.
14 It's safe to use this string in URLs or HTML.
15 Because we use the secrets module it could be used for security purposes as well
17 :param length: The desired length of the generated string.
19 :returns: A string with random characters of the given length.
20 """
21 return "".join(secrets.choice(string.ascii_letters + string.digits) for _ in range(length))
24# String base mapping
25__STRING_ESCAPE_MAPPING = {
26 "<": "<",
27 ">": ">",
28 "\"": """,
29 "'": "'",
30 "(": "(",
31 ")": ")",
32 "=": "=",
33 "\n": " ",
34 "\0": "",
35}
37# Translation table for string escaping
38__STRING_ESCAPE_TRANSTAB = str.maketrans(__STRING_ESCAPE_MAPPING)
40# Lookup-table for string unescaping
41__STRING_UNESCAPE_MAPPING = {v: k for k, v in __STRING_ESCAPE_MAPPING.items() if v}
44def escape(val: str, max_length: int | None = 254, maxLength: int | None = None) -> str:
45 """
46 Quotes special characters from a string and removes "\\\\0".
47 It shall be used to prevent XSS injections in data.
49 :param val: The value to be escaped.
50 :param max_length: Cut-off after max_length characters. None or 0 means "unlimited".
52 :returns: The quoted string.
53 """
54 # fixme: Remove in viur-core >= 4
55 if maxLength is not None and max_length == 254: 55 ↛ 56line 55 didn't jump to line 56 because the condition on line 55 was never true
56 warnings.warn("'maxLength' is deprecated, please use 'max_length'", DeprecationWarning)
57 max_length = maxLength
59 res = str(val).strip().translate(__STRING_ESCAPE_TRANSTAB)
61 if max_length: 61 ↛ 64line 61 didn't jump to line 64 because the condition on line 61 was always true
62 return res[:max_length]
64 return res
67def unescape(val: str) -> str:
68 """
69 Unquotes characters formerly escaped by `escape`.
71 :param val: The value to be unescaped.
72 :param max_length: Optional cut-off after max_length characters. \
73 A value of None or 0 means "unlimited".
75 :returns: The unquoted string.
76 """
77 def __escape_replace(re_match):
78 # In case group 2 is matched, search for its escape sequence
79 if find := re_match.group(2):
80 find = f"&#{find};"
81 else:
82 find = re_match.group(0)
84 return __STRING_UNESCAPE_MAPPING.get(find) or re_match.group(0)
86 return re.sub(r"&(\w{2,4}|#0*(\d{2}));", __escape_replace, str(val).strip())
89def normalize_ascii(text: str) -> str:
90 """
91 Normalize a Unicode string to an ASCII-only version.
93 This function uses NFKD normalization to decompose accented and special characters,
94 then encodes the result to ASCII, ignoring any characters that can't be represented.
96 For example:
97 "Änderung" -> "Anderung"
98 "Café" -> "Cafe"
100 :param text: The input Unicode string to normalize.
101 :return: A normalized ASCII-only version of the input string.
102 """
103 return (
104 unicodedata
105 .normalize("NFKD", text)
106 .encode("ascii", "ignore")
107 .decode("ascii")
108 )
111def is_prefix(name: str, prefix: str, delimiter: str = ".") -> bool:
112 """
113 Utility function to check if a given name matches a prefix,
114 which defines a specialization, delimited by `delimiter`.
116 In ViUR, modules, bones, renders, etc. provide a kind or handler
117 to classify or subclassify the specific object. To securitly
118 check for a specific type, it is either required to ask for the
119 exact type or if its prefixed by a path delimited normally by
120 dots.
122 Example:
124 .. code-block:: python
125 handler = "tree.file.special"
126 utils.string.is_prefix(handler, "tree") # True
127 utils.string.is_prefix(handler, "tree.node") # False
128 utils.string.is_prefix(handler, "tree.file") # True
129 utils.string.is_prefix(handler, "tree.file.special") # True
130 """
131 return name == prefix or name.startswith(prefix + delimiter)