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

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

58 statements  

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

2 

3import re 

4 

5from icalendar.compatibility import deprecate_for_version_8 

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 Note: 

23 The replacement order is critical: 

24 

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

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

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

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

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

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

31 newline character) 

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

33 newline character) 

34 

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

36 The line-ending normalization is an implementation convenience, 

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

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

39 """ 

40 assert isinstance(text, (str, bytes)) 

41 text = to_unicode(text) 

42 # NOTE: ORDER MATTERS! 

43 return ( 

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

45 .replace("\\", "\\\\") 

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

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

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

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

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

51 ) 

52 

53 

54escape_char = deprecate_for_version_8(_escape_char) 

55"""Format value according to iCalendar TEXT escaping rules. 

56 

57.. deprecated:: 7.0.0 

58 Use the private :func:`_escape_char` internally. For external use, 

59 this function is deprecated. Please use alternative escaping methods 

60 or contact the maintainers. 

61""" 

62 

63 

64def _unescape_char(text: str | bytes) -> str | bytes | None: 

65 r"""Unescape iCalendar TEXT values. 

66 

67 Reverses the escaping applied by :func:`_escape_char` according to 

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

69 

70 Parameters: 

71 text: The escaped text. 

72 

73 Returns: 

74 The unescaped text, or ``None`` if ``text`` is neither ``str`` nor ``bytes``. 

75 

76 Note: 

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

78 

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

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

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

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

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

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

85 """ 

86 assert isinstance(text, (str, bytes)) 

87 # NOTE: ORDER MATTERS! 

88 if isinstance(text, str): 

89 return ( 

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

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

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

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

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

95 .replace("\\\\", "\\") 

96 ) 

97 if isinstance(text, bytes): 

98 return ( 

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

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

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

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

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

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

105 ) 

106 return None 

107 

108 

109unescape_char = deprecate_for_version_8(_unescape_char) 

110"""Unescape iCalendar TEXT values. 

111 

112.. deprecated:: 7.0.0 

113 Use the private :func:`_unescape_char` internally. For external use, 

114 this function is deprecated. Please use alternative unescaping methods 

115 or contact the maintainers. 

116""" 

117 

118 

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

120 """Make a string folded as defined in RFC5545. 

121 

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

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

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

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

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

127 SPACE or HTAB). 

128 """ 

129 assert isinstance(line, str) 

130 assert "\n" not in line 

131 

132 folded_lines: list[str] = [] 

133 current_chars: list[str] = [] 

134 byte_count = 0 

135 for char in line: 

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

137 if current_chars and byte_count + char_byte_len >= limit: 

138 # For compatibility with existing clients, avoid splitting escaped 

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

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

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

142 escaped_prefix = current_chars.pop() 

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

144 current_chars = [escaped_prefix] 

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

146 else: 

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

148 current_chars = [] 

149 byte_count = 0 

150 current_chars.append(char) 

151 byte_count += char_byte_len 

152 

153 if current_chars: 

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

155 

156 return fold_sep.join(folded_lines) 

157 

158 

159foldline = deprecate_for_version_8(_foldline) 

160"""Make a string folded as defined in RFC5545. 

161 

162.. deprecated:: 7.0.0 

163 Use the private :func:`_foldline` internally. 

164""" 

165 

166 

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

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

169 

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

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

172 characters during processing. 

173 

174 Parameters: 

175 val: The string with backslash escapes. 

176 

177 Returns: 

178 The string with backslash escapes converted to percent encoding. 

179 

180 Note: 

181 Conversions: 

182 

183 - ``%`` -> ``%25`` 

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

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

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

187 - ``\\`` -> ``%5C`` 

188 

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

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

191 introduced here. :func:`_unescape_string` reverses it. 

192 """ 

193 # f'{i:02X}' 

194 return ( 

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

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

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

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

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

200 ) 

201 

202 

203escape_string = deprecate_for_version_8(_escape_string) 

204"""Escape backslash sequences to URL-encoded hex values. 

205 

206.. deprecated:: 7.0.0 

207 Use the private :func:`_escape_string` internally. For external use, 

208 this function is deprecated. 

209""" 

210 

211 

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

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

214 

215 Reverses :func:`_escape_string` by converting percent-encoded hex values 

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

217 

218 Parameters: 

219 val: The string with percent-encoded values. 

220 

221 Returns: 

222 The string with percent encoding converted to characters. 

223 

224 Note: 

225 Conversions: 

226 

227 - ``%2C`` -> ``,`` 

228 - ``%3A`` -> ``:`` 

229 - ``%3B`` -> ``;`` 

230 - ``%5C`` -> ``\`` 

231 - ``%25`` -> ``%`` 

232 

233 ``%25`` is restored last so a literal ``%`` that :func:`_escape_string` 

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

235 """ 

236 return ( 

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

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

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

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

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

242 ) 

243 

244 

245unescape_string = deprecate_for_version_8(_unescape_string) 

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

247 

248.. deprecated:: 7.0.0 

249 Use the private :func:`_unescape_string` internally. For external use, 

250 this function is deprecated. 

251""" 

252 

253 

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

255# . because of the vCard RFC 

256NAME = re.compile(r"[\w.-]+") 

257 

258 

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

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

261 

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

263 regex pattern (``[\w.-]+``). 

264 

265 Parameters: 

266 name: The token name to validate. 

267 

268 Raises: 

269 ValueError: If the name is not a valid token. 

270 """ 

271 match = NAME.findall(name) 

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

273 return 

274 raise ValueError(name) 

275 

276 

277__all__ = [ 

278 "_escape_char", 

279 "_escape_string", 

280 "_foldline", 

281 "_unescape_char", 

282 "_unescape_string", 

283 "escape_char", 

284 "escape_string", 

285 "foldline", 

286 "unescape_char", 

287 "unescape_string", 

288 "validate_token", 

289]