Coverage for /pythoncovmergedfiles/medio/medio/usr/local/lib/python3.11/site-packages/icalendar/parser.py: 80%
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
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
1"""This module parses and generates contentlines as defined in RFC 5545
2(iCalendar), but will probably work for other MIME types with similar syntax.
3Eg. RFC 2426 (vCard)
5It is stupid in the sense that it treats the content purely as strings. No type
6conversion is attempted.
7"""
9from __future__ import annotations
11import functools
12import os
13import re
14from datetime import datetime, time
15from typing import TYPE_CHECKING, Any, Callable
17from icalendar.caselessdict import CaselessDict
18from icalendar.error import JCalParsingError
19from icalendar.parser_tools import (
20 DEFAULT_ENCODING,
21 ICAL_TYPE,
22 SEQUENCE_TYPES,
23 to_unicode,
24)
25from icalendar.timezone.tzid import tzid_from_dt
27if TYPE_CHECKING:
28 from icalendar.enums import VALUE
29 from icalendar.prop import VPROPERTY
32def escape_char(text):
33 """Format value according to iCalendar TEXT escaping rules."""
34 assert isinstance(text, (str, bytes))
35 # NOTE: ORDER MATTERS!
36 return (
37 text.replace(r"\N", "\n")
38 .replace("\\", "\\\\")
39 .replace(";", r"\;")
40 .replace(",", r"\,")
41 .replace("\r\n", r"\n")
42 .replace("\n", r"\n")
43 )
46def unescape_char(text):
47 assert isinstance(text, (str, bytes))
48 # NOTE: ORDER MATTERS!
49 if isinstance(text, str):
50 return (
51 text.replace("\\N", "\\n")
52 .replace("\r\n", "\n")
53 .replace("\\n", "\n")
54 .replace("\\,", ",")
55 .replace("\\;", ";")
56 .replace("\\\\", "\\")
57 )
58 if isinstance(text, bytes):
59 return (
60 text.replace(b"\\N", b"\\n")
61 .replace(b"\r\n", b"\n")
62 .replace(b"\\n", b"\n")
63 .replace(b"\\,", b",")
64 .replace(b"\\;", b";")
65 .replace(b"\\\\", b"\\")
66 )
67 return None
70def foldline(line, limit=75, fold_sep="\r\n "):
71 """Make a string folded as defined in RFC5545
72 Lines of text SHOULD NOT be longer than 75 octets, excluding the line
73 break. Long content lines SHOULD be split into a multiple line
74 representations using a line "folding" technique. That is, a long
75 line can be split between any two characters by inserting a CRLF
76 immediately followed by a single linear white-space character (i.e.,
77 SPACE or HTAB).
78 """
79 assert isinstance(line, str)
80 assert "\n" not in line
82 # Use a fast and simple variant for the common case that line is all ASCII.
83 try:
84 line.encode("ascii")
85 except (UnicodeEncodeError, UnicodeDecodeError):
86 pass
87 else:
88 return fold_sep.join(
89 line[i : i + limit - 1] for i in range(0, len(line), limit - 1)
90 )
92 ret_chars = []
93 byte_count = 0
94 for char in line:
95 char_byte_len = len(char.encode(DEFAULT_ENCODING))
96 byte_count += char_byte_len
97 if byte_count >= limit:
98 ret_chars.append(fold_sep)
99 byte_count = char_byte_len
100 ret_chars.append(char)
102 return "".join(ret_chars)
105#################################################################
106# Property parameter stuff
109def param_value(value, always_quote=False):
110 """Returns a parameter value."""
111 if isinstance(value, SEQUENCE_TYPES):
112 return q_join(map(rfc_6868_escape, value), always_quote=always_quote)
113 if isinstance(value, str):
114 return dquote(rfc_6868_escape(value), always_quote=always_quote)
115 return dquote(rfc_6868_escape(value.to_ical().decode(DEFAULT_ENCODING)))
118# Could be improved
120# [\w-] because of the iCalendar RFC
121# . because of the vCard RFC
122NAME = re.compile(r"[\w.-]+")
124UNSAFE_CHAR = re.compile('[\x00-\x08\x0a-\x1f\x7f",:;]')
125QUNSAFE_CHAR = re.compile('[\x00-\x08\x0a-\x1f\x7f"]')
126FOLD = re.compile(b"(\r?\n)+[ \t]")
127UFOLD = re.compile("(\r?\n)+[ \t]")
128NEWLINE = re.compile(r"\r?\n")
131def validate_token(name):
132 match = NAME.findall(name)
133 if len(match) == 1 and name == match[0]:
134 return
135 raise ValueError(name)
138def validate_param_value(value, quoted=True):
139 validator = QUNSAFE_CHAR if quoted else UNSAFE_CHAR
140 if validator.findall(value):
141 raise ValueError(value)
144# chars presence of which in parameter value will be cause the value
145# to be enclosed in double-quotes
146QUOTABLE = re.compile("[,;:’]") # noqa: RUF001
149def dquote(val, always_quote=False):
150 """Enclose parameter values containing [,;:] in double quotes."""
151 # a double-quote character is forbidden to appear in a parameter value
152 # so replace it with a single-quote character
153 val = val.replace('"', "'")
154 if QUOTABLE.search(val) or always_quote:
155 return f'"{val}"'
156 return val
159# parsing helper
160def q_split(st, sep=",", maxsplit=-1):
161 """Splits a string on char, taking double (q)uotes into considderation."""
162 if maxsplit == 0:
163 return [st]
165 result = []
166 cursor = 0
167 length = len(st)
168 inquote = 0
169 splits = 0
170 for i, ch in enumerate(st):
171 if ch == '"':
172 inquote = not inquote
173 if not inquote and ch == sep:
174 result.append(st[cursor:i])
175 cursor = i + 1
176 splits += 1
177 if i + 1 == length or splits == maxsplit:
178 result.append(st[cursor:])
179 break
180 return result
183def q_join(lst, sep=",", always_quote=False):
184 """Joins a list on sep, quoting strings with QUOTABLE chars."""
185 return sep.join(dquote(itm, always_quote=always_quote) for itm in lst)
188def single_string_parameter(func: Callable | None = None, upper=False):
189 """Create a parameter getter/setter for a single string parameter.
191 Args:
192 upper: Convert the value to uppercase
193 func: The function to decorate.
195 Returns:
196 The property for the parameter or a decorator for the parameter
197 if func is ``None``.
198 """
200 def decorator(func):
201 name = func.__name__
203 @functools.wraps(func)
204 def fget(self: Parameters):
205 """Get the value."""
206 value = self.get(name)
207 if value is not None and upper:
208 value = value.upper()
209 return value
211 def fset(self: Parameters, value: str | None):
212 """Set the value"""
213 if value is None:
214 fdel(self)
215 else:
216 if upper:
217 value = value.upper()
218 self[name] = value
220 def fdel(self: Parameters):
221 """Delete the value."""
222 self.pop(name, None)
224 return property(fget, fset, fdel, doc=func.__doc__)
226 if func is None:
227 return decorator
228 return decorator(func)
231class Parameters(CaselessDict):
232 """Parser and generator of Property parameter strings.
234 It knows nothing of datatypes.
235 Its main concern is textual structure.
237 Examples:
239 Modify parameters:
241 .. code-block:: pycon
243 >>> from icalendar import Parameters
244 >>> params = Parameters()
245 >>> params['VALUE'] = 'TEXT'
246 >>> params.value
247 'TEXT'
248 >>> params
249 Parameters({'VALUE': 'TEXT'})
251 Create new parameters:
253 .. code-block:: pycon
255 >>> params = Parameters(value="BINARY")
256 >>> params.value
257 'BINARY'
259 Set a default:
261 .. code-block:: pycon
263 >>> params = Parameters(value="BINARY", default_value="TEXT")
264 >>> params
265 Parameters({'VALUE': 'BINARY'})
267 """
269 def __init__(self, *args, **kwargs):
270 """Create new parameters."""
271 if args and args[0] is None:
272 # allow passing None
273 args = args[1:]
274 defaults = {
275 key[8:]: kwargs.pop(key)
276 for key in list(kwargs.keys())
277 if key.lower().startswith("default_")
278 }
279 super().__init__(*args, **kwargs)
280 for key, value in defaults.items():
281 self.setdefault(key, value)
283 # The following paremeters must always be enclosed in double quotes
284 always_quoted = (
285 "ALTREP",
286 "DELEGATED-FROM",
287 "DELEGATED-TO",
288 "DIR",
289 "MEMBER",
290 "SENT-BY",
291 # Part of X-APPLE-STRUCTURED-LOCATION
292 "X-ADDRESS",
293 "X-TITLE",
294 # RFC 9253
295 "LINKREL",
296 )
297 # this is quoted should one of the values be present
298 quote_also = {
299 # This is escaped in the RFC
300 "CN": " '",
301 }
303 def params(self):
304 """In RFC 5545 keys are called parameters, so this is to be consitent
305 with the naming conventions.
306 """
307 return self.keys()
309 def to_ical(self, sorted: bool = True): # noqa: A002, FBT001
310 """Returns an :rfc:`5545` representation of the parameters.
312 Args:
313 sorted (bool): Sort the parameters before encoding.
314 exclude_utc (bool): Exclude TZID if it is set to ``"UTC"``
315 """
316 result = []
317 items = list(self.items())
318 if sorted:
319 items.sort()
321 for key, value in items:
322 if key == "TZID" and value == "UTC":
323 # The "TZID" property parameter MUST NOT be applied to DATE-TIME
324 # properties whose time values are specified in UTC.
325 continue
326 upper_key = key.upper()
327 check_quoteable_characters = self.quote_also.get(key.upper())
328 always_quote = upper_key in self.always_quoted or (
329 check_quoteable_characters
330 and any(c in value for c in check_quoteable_characters)
331 )
332 quoted_value = param_value(value, always_quote=always_quote)
333 if isinstance(quoted_value, str):
334 quoted_value = quoted_value.encode(DEFAULT_ENCODING)
335 # CaselessDict keys are always unicode
336 result.append(upper_key.encode(DEFAULT_ENCODING) + b"=" + quoted_value)
337 return b";".join(result)
339 @classmethod
340 def from_ical(cls, st, strict=False):
341 """Parses the parameter format from ical text format."""
343 # parse into strings
344 result = cls()
345 for param in q_split(st, ";"):
346 try:
347 key, val = q_split(param, "=", maxsplit=1)
348 validate_token(key)
349 # Property parameter values that are not in quoted
350 # strings are case insensitive.
351 vals = []
352 for v in q_split(val, ","):
353 if v.startswith('"') and v.endswith('"'):
354 v2 = v.strip('"')
355 validate_param_value(v2, quoted=True)
356 vals.append(rfc_6868_unescape(v2))
357 else:
358 validate_param_value(v, quoted=False)
359 if strict:
360 vals.append(rfc_6868_unescape(v.upper()))
361 else:
362 vals.append(rfc_6868_unescape(v))
363 if not vals:
364 result[key] = val
365 elif len(vals) == 1:
366 result[key] = vals[0]
367 else:
368 result[key] = vals
369 except ValueError as exc: # noqa: PERF203
370 raise ValueError(
371 f"{param!r} is not a valid parameter string: {exc}"
372 ) from exc
373 return result
375 @single_string_parameter(upper=True)
376 def value(self) -> VALUE | str | None:
377 """The VALUE parameter from :rfc:`5545`.
379 Description:
380 This parameter specifies the value type and format of
381 the property value. The property values MUST be of a single value
382 type. For example, a "RDATE" property cannot have a combination
383 of DATE-TIME and TIME value types.
385 If the property's value is the default value type, then this
386 parameter need not be specified. However, if the property's
387 default value type is overridden by some other allowable value
388 type, then this parameter MUST be specified.
390 Applications MUST preserve the value data for x-name and iana-
391 token values that they don't recognize without attempting to
392 interpret or parse the value data.
394 For convenience, using this property, the value will be converted to
395 an uppercase string.
397 .. code-block:: pycon
399 >>> from icalendar import Parameters
400 >>> params = Parameters()
401 >>> params.value = "unknown"
402 >>> params
403 Parameters({'VALUE': 'UNKNOWN'})
405 """
407 def _parameter_value_to_jcal(
408 self, value: str | float | list | VPROPERTY
409 ) -> str | int | float | list[str] | list[int] | list[float]:
410 """Convert a parameter value to jCal format.
412 Args:
413 value: The parameter value
415 Returns:
416 The jCal representation of the parameter value
417 """
418 if isinstance(value, list):
419 return [self._parameter_value_to_jcal(v) for v in value]
420 if hasattr(value, "to_jcal"):
421 # proprty values respond to this
422 jcal = value.to_jcal()
423 # we only need the value part
424 if len(jcal) == 4:
425 return jcal[3]
426 return jcal[3:]
427 for t in (int, float, str):
428 if isinstance(value, t):
429 return t(value)
430 raise TypeError(
431 "Unsupported parameter value type for jCal conversion: "
432 f"{type(value)} {value!r}"
433 )
435 def to_jcal(self, exclude_utc=False) -> dict[str, str]:
436 """Return the jCal representation of the parameters.
438 Args:
439 exclude_utc (bool): Exclude the TZID parameter if it is UTC
440 """
441 jcal = {
442 k.lower(): self._parameter_value_to_jcal(v)
443 for k, v in self.items()
444 if k.lower() != "value"
445 }
446 if exclude_utc and jcal.get("tzid") == "UTC":
447 del jcal["tzid"]
448 return jcal
450 @single_string_parameter
451 def tzid(self) -> str | None:
452 """The TZID parameter from :rfc:`5545`."""
454 def is_utc(self):
455 """Whether the TZID parameter is UTC."""
456 return self.tzid == "UTC"
458 def update_tzid_from(self, dt: datetime | time | Any) -> None:
459 """Update the TZID parameter from a datetime object.
461 This sets the TZID parameter or deletes it according to the datetime.
462 """
463 if isinstance(dt, (datetime, time)):
464 self.tzid = tzid_from_dt(dt)
466 @classmethod
467 def from_jcal(cls, jcal: dict[str : str | list[str]]):
468 """Parse jCal parameters."""
469 if not isinstance(jcal, dict):
470 raise JCalParsingError("The parameters must be a mapping.", cls)
471 for name, value in jcal.items():
472 if not isinstance(name, str):
473 raise JCalParsingError(
474 "All parameter names must be strings.", cls, value=name
475 )
476 if not (
477 (
478 isinstance(value, list)
479 and all(isinstance(v, (str, int, float)) for v in value)
480 and value
481 )
482 or isinstance(value, (str, int, float))
483 ):
484 raise JCalParsingError(
485 "Parameter values must be a string, integer or "
486 "float or a list of those.",
487 cls,
488 name,
489 value=value,
490 )
491 return cls(jcal)
493 @classmethod
494 def from_jcal_property(cls, jcal_property: list):
495 """Create the parameters for a jCal property.
497 Args:
498 jcal_property (list): The jCal property [name, params, value, ...]
499 default_value (str, optional): The default value of the property.
500 If this is given, the default value will not be set.
501 """
502 if not isinstance(jcal_property, list) or len(jcal_property) < 4:
503 raise JCalParsingError(
504 "The property must be a list with at least 4 items.", cls
505 )
506 jcal_params = jcal_property[1]
507 with JCalParsingError.reraise_with_path_added(1):
508 self = cls.from_jcal(jcal_params)
509 if self.is_utc():
510 del self.tzid # we do not want this parameter
511 return self
514def escape_string(val):
515 # f'{i:02X}'
516 return (
517 val.replace(r"\,", "%2C")
518 .replace(r"\:", "%3A")
519 .replace(r"\;", "%3B")
520 .replace(r"\\", "%5C")
521 )
524def unescape_string(val):
525 return (
526 val.replace("%2C", ",")
527 .replace("%3A", ":")
528 .replace("%3B", ";")
529 .replace("%5C", "\\")
530 )
533_unescape_backslash_regex = re.compile(r"\\([\\,;:nN])")
536def unescape_backslash(val: str):
537 r"""Unescape backslash sequences in iCalendar text.
539 Unlike :py:meth:`unescape_string`, this only handles actual backslash escapes
540 per :rfc:`5545`, not URL encoding. This preserves URL-encoded values
541 like ``%3A`` in URLs.
543 Processes backslash escape sequences in a single pass using regex matching.
544 """
545 return _unescape_backslash_regex.sub(
546 lambda m: "\n" if m.group(1) in "nN" else m.group(1), val
547 )
550def split_on_unescaped_semicolon(text: str) -> list[str]:
551 r"""Split text on unescaped semicolons and unescape each part.
553 Splits only on semicolons not preceded by a backslash.
554 After splitting, unescapes backslash sequences in each part.
555 Used by vCard structured properties (ADR, N, ORG) per :rfc:`6350`.
557 Args:
558 text: Text with potential escaped semicolons (e.g., "field1\\;with;field2")
560 Returns:
561 List of unescaped field strings
563 Examples:
564 .. code-block:: pycon
566 >>> from icalendar.parser import split_on_unescaped_semicolon
567 >>> split_on_unescaped_semicolon(r"field1\;with;field2")
568 ['field1;with', 'field2']
569 >>> split_on_unescaped_semicolon("a;b;c")
570 ['a', 'b', 'c']
571 >>> split_on_unescaped_semicolon(r"a\;b\;c")
572 ['a;b;c']
573 >>> split_on_unescaped_semicolon(r"PO Box 123\;Suite 200;City")
574 ['PO Box 123;Suite 200', 'City']
575 """
576 if not text:
577 return [""]
579 result = []
580 current = []
581 i = 0
583 while i < len(text):
584 if text[i] == "\\" and i + 1 < len(text):
585 # Escaped character - keep both backslash and next char
586 current.append(text[i])
587 current.append(text[i + 1])
588 i += 2
589 elif text[i] == ";":
590 # Unescaped semicolon - split point
591 result.append(unescape_backslash("".join(current)))
592 current = []
593 i += 1
594 else:
595 current.append(text[i])
596 i += 1
598 # Add final part
599 result.append(unescape_backslash("".join(current)))
601 return result
604RFC_6868_UNESCAPE_REGEX = re.compile(r"\^\^|\^n|\^'")
607def rfc_6868_unescape(param_value: str) -> str:
608 """Take care of :rfc:`6868` unescaping.
610 - ^^ -> ^
611 - ^n -> system specific newline
612 - ^' -> "
613 - ^ with others stay intact
614 """
615 replacements = {
616 "^^": "^",
617 "^n": os.linesep,
618 "^'": '"',
619 }
620 return RFC_6868_UNESCAPE_REGEX.sub(
621 lambda m: replacements.get(m.group(0), m.group(0)), param_value
622 )
625RFC_6868_ESCAPE_REGEX = re.compile(r'\^|\r\n|\r|\n|"')
628def rfc_6868_escape(param_value: str) -> str:
629 """Take care of :rfc:`6868` escaping.
631 - ^ -> ^^
632 - " -> ^'
633 - newline -> ^n
634 """
635 replacements = {
636 "^": "^^",
637 "\n": "^n",
638 "\r": "^n",
639 "\r\n": "^n",
640 '"': "^'",
641 }
642 return RFC_6868_ESCAPE_REGEX.sub(
643 lambda m: replacements.get(m.group(0), m.group(0)), param_value
644 )
647def unescape_list_or_string(val):
648 if isinstance(val, list):
649 return [unescape_string(s) for s in val]
650 return unescape_string(val)
653#########################################
654# parsing and generation of content lines
657class Contentline(str):
658 """A content line is basically a string that can be folded and parsed into
659 parts.
660 """
662 __slots__ = ("strict",)
664 def __new__(cls, value, strict=False, encoding=DEFAULT_ENCODING):
665 value = to_unicode(value, encoding=encoding)
666 assert "\n" not in value, (
667 "Content line can not contain unescaped new line characters."
668 )
669 self = super().__new__(cls, value)
670 self.strict = strict
671 return self
673 @classmethod
674 def from_parts(
675 cls,
676 name: ICAL_TYPE,
677 params: Parameters,
678 values,
679 sorted: bool = True, # noqa: A002, FBT001
680 ):
681 """Turn a parts into a content line."""
682 assert isinstance(params, Parameters)
683 if hasattr(values, "to_ical"):
684 values = values.to_ical()
685 else:
686 from icalendar.prop import vText
688 values = vText(values).to_ical()
689 # elif isinstance(values, basestring):
690 # values = escape_char(values)
692 # TODO: after unicode only, remove this
693 # Convert back to unicode, after to_ical encoded it.
694 name = to_unicode(name)
695 values = to_unicode(values)
696 if params:
697 params = to_unicode(params.to_ical(sorted=sorted))
698 if params:
699 # some parameter values can be skipped during serialization
700 return cls(f"{name};{params}:{values}")
701 return cls(f"{name}:{values}")
703 def parts(self) -> tuple[str, Parameters, str]:
704 """Split the content line into ``name``, ``parameters``, and ``values`` parts.
706 Properly handles escaping with backslashes and double-quote sections
707 to avoid corrupting URL-encoded characters in values.
709 Example with parameter:
711 .. code-block:: text
713 DESCRIPTION;ALTREP="cid:part1.0001@example.org":The Fall'98 Wild
715 Example without parameters:
717 .. code-block:: text
719 DESCRIPTION:The Fall'98 Wild
720 """
721 try:
722 name_split: int | None = None
723 value_split: int | None = None
724 in_quotes: bool = False
725 escaped: bool = False
727 for i, ch in enumerate(self):
728 if ch == '"' and not escaped:
729 in_quotes = not in_quotes
730 elif ch == "\\" and not in_quotes:
731 escaped = True
732 continue
733 elif not in_quotes and not escaped:
734 # Find first delimiter for name
735 if ch in ":;" and name_split is None:
736 name_split = i
737 # Find value delimiter (first colon)
738 if ch == ":" and value_split is None:
739 value_split = i
741 escaped = False
743 # Validate parsing results
744 if not value_split:
745 # No colon found - value is empty, use end of string
746 value_split = len(self)
748 # Extract name - if no delimiter,
749 # take whole string for validate_token to reject
750 name = self[:name_split] if name_split else self
751 validate_token(name)
753 if not name_split or name_split + 1 == value_split:
754 # No delimiter or empty parameter section
755 raise ValueError("Invalid content line") # noqa: TRY301
756 # Parse parameters - they still need to be escaped/unescaped
757 # for proper handling of commas, semicolons, etc. in parameter values
758 param_str = escape_string(self[name_split + 1 : value_split])
759 params = Parameters.from_ical(param_str, strict=self.strict)
760 params = Parameters(
761 (unescape_string(key), unescape_list_or_string(value))
762 for key, value in iter(params.items())
763 )
764 # Unescape backslash sequences in values but preserve URL encoding
765 values = unescape_backslash(self[value_split + 1 :])
766 except ValueError as exc:
767 raise ValueError(
768 f"Content line could not be parsed into parts: '{self}': {exc}"
769 ) from exc
770 return (name, params, values)
772 @classmethod
773 def from_ical(cls, ical, strict=False):
774 """Unfold the content lines in an iCalendar into long content lines."""
775 ical = to_unicode(ical)
776 # a fold is carriage return followed by either a space or a tab
777 return cls(UFOLD.sub("", ical), strict=strict)
779 def to_ical(self):
780 """Long content lines are folded so they are less than 75 characters
781 wide.
782 """
783 return foldline(self).encode(DEFAULT_ENCODING)
786class Contentlines(list):
787 """I assume that iCalendar files generally are a few kilobytes in size.
788 Then this should be efficient. for Huge files, an iterator should probably
789 be used instead.
790 """
792 def to_ical(self):
793 """Simply join self."""
794 return b"\r\n".join(line.to_ical() for line in self if line) + b"\r\n"
796 @classmethod
797 def from_ical(cls, st):
798 """Parses a string into content lines."""
799 st = to_unicode(st)
800 try:
801 # a fold is carriage return followed by either a space or a tab
802 unfolded = UFOLD.sub("", st)
803 lines = cls(Contentline(line) for line in NEWLINE.split(unfolded) if line)
804 lines.append("") # '\r\n' at the end of every content line
805 except Exception as e:
806 raise ValueError("Expected StringType with content lines") from e
807 return lines
810__all__ = [
811 "FOLD",
812 "NAME",
813 "NEWLINE",
814 "QUNSAFE_CHAR",
815 "QUOTABLE",
816 "UFOLD",
817 "UNSAFE_CHAR",
818 "Contentline",
819 "Contentlines",
820 "Parameters",
821 "dquote",
822 "escape_char",
823 "escape_string",
824 "foldline",
825 "param_value",
826 "q_join",
827 "q_split",
828 "rfc_6868_escape",
829 "rfc_6868_unescape",
830 "split_on_unescaped_semicolon",
831 "unescape_backslash",
832 "unescape_char",
833 "unescape_list_or_string",
834 "unescape_string",
835 "validate_param_value",
836 "validate_token",
837]