Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
42 changes: 37 additions & 5 deletions src/apm_cli/policy/parser.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

from __future__ import annotations

import errno
from pathlib import Path
from typing import Any, List, Optional, Tuple, Union

Expand Down Expand Up @@ -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)
Expand Down
20 changes: 20 additions & 0 deletions tests/unit/policy/test_parser.py
Original file line number Diff line number Diff line change
Expand Up @@ -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."""
Expand Down
Loading