diff --git a/src/apm_cli/policy/parser.py b/src/apm_cli/policy/parser.py index 95ca9a73d..881369816 100644 --- a/src/apm_cli/policy/parser.py +++ b/src/apm_cli/policy/parser.py @@ -2,6 +2,7 @@ from __future__ import annotations +import errno from pathlib import Path from typing import Any, List, Optional, Tuple, Union @@ -241,18 +242,49 @@ def _build_policy(data: dict) -> ApmPolicy: ) +def _looks_like_yaml_content(source: str) -> bool: + """Return True when a string is more likely inline YAML than a file path. + + This avoids probing the filesystem for large YAML payloads, which can raise + platform-specific path errors such as ENAMETOOLONG on macOS. + """ + stripped = source.lstrip() + + if "\n" in source or "\r" in source: + return True + + if stripped.startswith(("{", "[", "---", "- ")): + return True + + first_line = stripped.splitlines()[0] if stripped else "" + return ": " in first_line or first_line.endswith(":") + + def load_policy(source: Union[str, Path]) -> Tuple[ApmPolicy, List[str]]: """Load and validate an apm-policy.yml from a file path or YAML string. Returns (policy, warnings). Raises PolicyValidationError on invalid input. """ - path = Path(source) if not isinstance(source, Path) else source + raw: str - if path.is_file(): - raw = path.read_text(encoding="utf-8") + if isinstance(source, Path): + raw = source.read_text(encoding="utf-8") if source.is_file() else str(source) + elif _looks_like_yaml_content(source): + raw = source else: - # Treat source as a YAML string - raw = str(source) + path = Path(source) + try: + is_file = path.is_file() + except OSError as exc: + if exc.errno == errno.ENAMETOOLONG: + is_file = False + else: + raise + + if is_file: + raw = path.read_text(encoding="utf-8") + else: + raw = source try: data = yaml.safe_load(raw) diff --git a/tests/unit/policy/test_parser.py b/tests/unit/policy/test_parser.py index 2b5e61d3d..7c085788a 100644 --- a/tests/unit/policy/test_parser.py +++ b/tests/unit/policy/test_parser.py @@ -321,6 +321,26 @@ def test_version_coerced_to_string(self): policy, warnings = load_policy("version: '2.0'") self.assertEqual(policy.version, "2.0") + def test_long_yaml_string_does_not_crash(self): + """Long YAML strings (> PATH_MAX on macOS) must not raise OSError.""" + # Build a YAML payload larger than typical PATH_MAX limits (1024 bytes) + # so that Path.is_file() can raise ENAMETOOLONG on macOS. + long_comment = "# " + "x" * 2048 + "\n" + yaml_str = ( + long_comment + + "name: long-policy\n" + + "version: '1.0'\n" + + "enforcement: off\n" + ) + # Ensure the string is long enough to trigger ENAMETOOLONG on macOS + self.assertGreater(len(yaml_str), 1024) + + # This should parse as inline YAML, not as a file path + policy, warnings = load_policy(yaml_str) + self.assertEqual(policy.name, "long-policy") + self.assertEqual(policy.version, "1.0") + self.assertEqual(policy.enforcement, "off") + class TestLoadPolicyFromFile(unittest.TestCase): """Test load_policy from a file path."""