Source code for icalendar_anonymizer.formats.jcal

# SPDX-FileCopyrightText: 2025 icalendar-anonymizer contributors
# SPDX-License-Identifier: AGPL-3.0-or-later

"""jCal (RFC 7265) anonymization support.

jCal is a lossless JSON encoding of iCalendar's own property model, so
anonymization is delegated entirely to the existing `anonymize()` engine:
parse jCal into a `Calendar`, anonymize unchanged, serialize back to jCal.
No jCal-specific anonymization logic exists or is needed.
"""

from icalendar import Calendar
from icalendar.error import JCalParsingError

from icalendar_anonymizer.anonymizer import anonymize


[docs] def anonymize_jcal( data: list, salt: bytes | None = None, field_modes: dict[str, str] | None = None, ) -> list: """Anonymize a jCal (RFC 7265) document. jCal is a direct JSON encoding of iCalendar's property model, so this reuses the same field names and modes as `anonymize()` for .ics input (SUMMARY, DESCRIPTION, ATTENDEE, etc.). jCal property names are lowercase per RFC 7265, but `anonymize()` already treats field names case-insensitively. Args: data: Parsed jCal document, e.g. `["vcalendar", [...], [...]]` as produced by `json.loads()` of jCal JSON text, or an equivalent Python list structure. salt: Optional salt for hashing. If None, generates random salt. field_modes: Optional dict mapping field names to anonymization modes. Same shape and vocabulary as `anonymize()`'s field_modes. Returns: New anonymized jCal document (same list-of-lists shape). Raises: TypeError: If data is not a list, or salt is not bytes. ValueError: If data is not a valid jCal document, or field_modes contains invalid fields/modes. """ if not isinstance(data, list): raise TypeError(f"data must be a list (jCal document), got {type(data).__name__}") try: cal = Calendar.from_jcal(data) except JCalParsingError as e: raise ValueError(f"Invalid jCal document: {e}") from e # Calendar.from_jcal() is typed to return the base Component (it can in # principle parse a bare subcomponent), but a well-formed jCal document # for anonymize_jcal's own documented input shape always starts with # "vcalendar" and comes back as a Calendar; anonymize() itself already # enforces this with a runtime isinstance check. if not isinstance(cal, Calendar): raise ValueError( # noqa: TRY004 f"Invalid jCal document: expected a vcalendar, got {cal.name}" ) anonymized_cal = anonymize(cal, salt=salt, field_modes=field_modes) return anonymized_cal.to_jcal()