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

1""" 

2ViUR utility functions regarding string processing. 

3""" 

4import re 

5import secrets 

6import string 

7import unicodedata 

8import warnings 

9 

10 

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 

16 

17 :param length: The desired length of the generated string. 

18 

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)) 

22 

23 

24# String base mapping 

25__STRING_ESCAPE_MAPPING = { 

26 "<": "&lt;", 

27 ">": "&gt;", 

28 "\"": "&quot;", 

29 "'": "&#39;", 

30 "(": "&#40;", 

31 ")": "&#41;", 

32 "=": "&#61;", 

33 "\n": " ", 

34 "\0": "", 

35} 

36 

37# Translation table for string escaping 

38__STRING_ESCAPE_TRANSTAB = str.maketrans(__STRING_ESCAPE_MAPPING) 

39 

40# Lookup-table for string unescaping 

41__STRING_UNESCAPE_MAPPING = {v: k for k, v in __STRING_ESCAPE_MAPPING.items() if v} 

42 

43 

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. 

48 

49 :param val: The value to be escaped. 

50 :param max_length: Cut-off after max_length characters. None or 0 means "unlimited". 

51 

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 

58 

59 res = str(val).strip().translate(__STRING_ESCAPE_TRANSTAB) 

60 

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] 

63 

64 return res 

65 

66 

67def unescape(val: str) -> str: 

68 """ 

69 Unquotes characters formerly escaped by `escape`. 

70 

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". 

74 

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) 

83 

84 return __STRING_UNESCAPE_MAPPING.get(find) or re_match.group(0) 

85 

86 return re.sub(r"&(\w{2,4}|#0*(\d{2}));", __escape_replace, str(val).strip()) 

87 

88 

89def normalize_ascii(text: str) -> str: 

90 """ 

91 Normalize a Unicode string to an ASCII-only version. 

92 

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. 

95 

96 For example: 

97 "Änderung" -> "Anderung" 

98 "Café" -> "Cafe" 

99 

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 ) 

109 

110 

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`. 

115 

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. 

121 

122 Example: 

123 

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)