Coverage for /pythoncovmergedfiles/medio/medio/usr/local/lib/python3.11/site-packages/icalendar/parser/string.py: 88%

Shortcuts on this page

r m x   toggle line displays

j k   next/prev highlighted chunk

0   (zero) top of page

1   (one) first highlighted chunk

50 statements  

1"""Functions for manipulating strings and bytes.""" 

2 

3import re 

4 

5from icalendar.error import InvalidToken 

6from icalendar.parser_tools import DEFAULT_ENCODING, to_unicode 

7 

8 

9def _escape_char(text: str | bytes) -> str: 

10 r"""Format value according to iCalendar TEXT escaping rules. 

11 

12 Escapes special characters in text values according to :rfc:`5545#section-3.3.11` 

13 rules. 

14 The order of replacements matters to avoid double-escaping. 

15 

16 Parameters: 

17 text: The text to escape. 

18 

19 Returns: 

20 The escaped text with special characters escaped. 

21 

22 Raises: 

23 TypeError: If ``text`` is neither ``str`` nor ``bytes``. 

24 

25 Note: 

26 The replacement order is critical: 

27 

28 1. ``\N`` -> ``\n`` (normalize newlines to lowercase) 

29 2. ``\`` -> ``\\`` (escape backslashes) 

30 3. ``;`` -> ``\;`` (escape semicolons) 

31 4. ``,`` -> ``\,`` (escape commas) 

32 5. ``\r\n`` -> ``\n`` (normalize line endings) 

33 6. ``"\n"`` -> ``r"\n"`` (transform a newline character to a literal, or raw, 

34 newline character) 

35 7. ``"\r"`` -> ``r"\n"`` (transform a lone carriage return to a literal 

36 newline character) 

37 

38 Steps 5 to 7 normalize ``\r\n``, ``\n``, or a lone ``\r`` to ``\n``. 

39 The line-ending normalization is an implementation convenience, 

40 not part of :rfc:`5545`, which only defines ``\n`` or ``\N`` for an 

41 intentional line break, and doesn't give an escape form for a lone ``\r``. 

42 """ 

43 if not isinstance(text, (str, bytes)): 

44 raise TypeError(f"Expected str or bytes, got {type(text).__name__}") 

45 text = to_unicode(text) 

46 # NOTE: ORDER MATTERS! 

47 return ( 

48 text.replace(r"\N", "\n") 

49 .replace("\\", "\\\\") 

50 .replace(";", r"\;") 

51 .replace(",", r"\,") 

52 .replace("\r\n", r"\n") 

53 .replace("\n", r"\n") 

54 .replace("\r", r"\n") 

55 ) 

56 

57 

58def _unescape_char(text: str | bytes) -> str | bytes: 

59 r"""Unescape iCalendar TEXT values. 

60 

61 Reverses the escaping applied by ``_escape_char`` according to 

62 :rfc:`5545#section-3.3.11` TEXT escaping rules. 

63 

64 Parameters: 

65 text: The escaped text. 

66 

67 Returns: 

68 The unescaped text. 

69 

70 Raises: 

71 TypeError: If ``text`` is neither ``str`` nor ``bytes``. 

72 

73 Note: 

74 The replacement order is critical to avoid double-unescaping: 

75 

76 1. ``\N`` -> ``\n`` (intermediate step) 

77 2. ``\r\n`` -> ``\n`` (normalize line endings) 

78 3. ``\n`` -> newline (unescape newlines) 

79 4. ``\,`` -> ``,`` (unescape commas) 

80 5. ``\;`` -> ``;`` (unescape semicolons) 

81 6. ``\\`` -> ``\`` (unescape backslashes last) 

82 """ 

83 if not isinstance(text, (str, bytes)): 

84 raise TypeError(f"Expected str or bytes, got {type(text).__name__}") 

85 # NOTE: ORDER MATTERS! 

86 if isinstance(text, str): 

87 return ( 

88 text.replace("\\N", "\\n") 

89 .replace("\r\n", "\n") 

90 .replace("\\n", "\n") 

91 .replace("\\,", ",") 

92 .replace("\\;", ";") 

93 .replace("\\\\", "\\") 

94 ) 

95 return ( 

96 text.replace(b"\\N", b"\\n") 

97 .replace(b"\r\n", b"\n") 

98 .replace(b"\\n", b"\n") 

99 .replace(b"\\,", b",") 

100 .replace(b"\\;", b";") 

101 .replace(b"\\\\", b"\\") 

102 ) 

103 

104 

105def _foldline(line: str, limit: int = 75, fold_sep: str = "\r\n ") -> str: 

106 r"""Make a string folded as defined in :rfc:`5545#section-3.1`. 

107 

108 Lines of text SHOULD NOT be longer than 75 octets, excluding the line 

109 break. Long content lines SHOULD be split into a multiple line 

110 representations using a line "folding" technique. That is, a long 

111 line can be split between any two characters by inserting a CRLF 

112 immediately followed by a single linear white-space character (i.e., 

113 SPACE or HTAB). 

114 

115 Raises: 

116 TypeError: If ``line`` is not a ``str``. 

117 ValueError: If ``line`` contains ``\n``. 

118 

119 """ 

120 if not isinstance(line, str): 

121 raise TypeError(f"Expected str, got {type(line).__name__}") 

122 if "\n" in line: 

123 raise ValueError("Line must not contain unescaped new line characters.") 

124 

125 folded_lines: list[str] = [] 

126 current_chars: list[str] = [] 

127 byte_count = 0 

128 for char in line: 

129 char_byte_len = len(char.encode(DEFAULT_ENCODING)) 

130 if current_chars and byte_count + char_byte_len >= limit: 

131 # For compatibility with existing clients, avoid splitting escaped 

132 # values such as TEXT backslash escapes or RFC 6868 parameter 

133 # escapes across a folded line boundary. See issue #1501. 

134 if len(current_chars) > 1 and current_chars[-1] in r"\^": 

135 escaped_prefix = current_chars.pop() 

136 folded_lines.append("".join(current_chars)) 

137 current_chars = [escaped_prefix] 

138 byte_count = len(escaped_prefix.encode(DEFAULT_ENCODING)) 

139 else: 

140 folded_lines.append("".join(current_chars)) 

141 current_chars = [] 

142 byte_count = 0 

143 current_chars.append(char) 

144 byte_count += char_byte_len 

145 

146 if current_chars: 

147 folded_lines.append("".join(current_chars)) 

148 

149 return fold_sep.join(folded_lines) 

150 

151 

152def _escape_string(val: str) -> str: 

153 r"""Escape backslash sequences to URL-encoded hex values. 

154 

155 Converts backslash-escaped characters to their percent-encoded hex 

156 equivalents. This is used for parameter parsing to preserve escaped 

157 characters during processing. 

158 

159 Parameters: 

160 val: The string with backslash escapes. 

161 

162 Returns: 

163 The string with backslash escapes converted to percent encoding. 

164 

165 Note: 

166 Conversions: 

167 

168 - ``%`` -> ``%25`` 

169 - ``\,`` -> ``%2C`` 

170 - ``\:`` -> ``%3A`` 

171 - ``\;`` -> ``%3B`` 

172 - ``\\`` -> ``%5C`` 

173 

174 A literal ``%`` is escaped first so that percent sequences already in 

175 the value (e.g. ``%2C`` in a URI) are not confused with the markers 

176 introduced here. ``_unescape_string`` reverses it. 

177 """ 

178 # f'{i:02X}' 

179 return ( 

180 val.replace("%", "%25") 

181 .replace(r"\,", "%2C") 

182 .replace(r"\:", "%3A") 

183 .replace(r"\;", "%3B") 

184 .replace(r"\\", "%5C") 

185 ) 

186 

187 

188def _unescape_string(val: str) -> str: 

189 r"""Unescape URL-encoded hex values to their original characters. 

190 

191 Reverses ``_escape_string`` by converting percent-encoded hex values 

192 back to their original characters. This is used for parameter parsing. 

193 

194 Parameters: 

195 val: The string with percent-encoded values. 

196 

197 Returns: 

198 The string with percent encoding converted to characters. 

199 

200 Note: 

201 Conversions: 

202 

203 - ``%2C`` -> ``,`` 

204 - ``%3A`` -> ``:`` 

205 - ``%3B`` -> ``;`` 

206 - ``%5C`` -> ``\`` 

207 - ``%25`` -> ``%`` 

208 

209 ``%25`` is restored last so a literal ``%`` that ``_escape_string`` 

210 protected does not re-trigger the marker replacements above. 

211 """ 

212 return ( 

213 val.replace("%2C", ",") 

214 .replace("%3A", ":") 

215 .replace("%3B", ";") 

216 .replace("%5C", "\\") 

217 .replace("%25", "%") 

218 ) 

219 

220 

221# [\w-] because of the iCalendar RFC 

222# . because of the vCard RFC 

223NAME = re.compile(r"[\w.-]+", re.ASCII) 

224 

225 

226def validate_token(name: str) -> None: 

227 r"""Validate that a name is a valid iCalendar token. 

228 

229 Checks if the name matches the :rfc:`5545` token syntax using the NAME 

230 regex pattern (``[\w.-]+``), with ``\w`` matching ASCII characters only. 

231 

232 Parameters: 

233 name: Required. The token name to validate. 

234 

235 Raises: 

236 ~icalendar.error.InvalidToken: If the name is not a valid token. 

237 

238 .. versionchanged:: 0.0.0 

239 Raise :exc:`~icalendar.error.InvalidToken`, a subclass of 

240 :exc:`ValueError`, and reject non-ASCII characters. 

241 """ 

242 match = NAME.findall(name) 

243 if len(match) == 1 and name == match[0]: 

244 return 

245 raise InvalidToken(name) 

246 

247 

248__all__ = [ 

249 "_escape_char", 

250 "_escape_string", 

251 "_foldline", 

252 "_unescape_char", 

253 "_unescape_string", 

254 "validate_token", 

255]