1    	import uuid
2    	from dataclasses import astuple, dataclass
3    	from typing import Generator, MutableSet, Optional, TypeVar, Union
4    	
5    	from lxml import etree
6    	from lxml.etree import _Element
7    	
8    	from pcs.common.types import StringCollection
9    	from pcs.common.validate import is_integer
10   	
11   	T = TypeVar("T", bound=type)
12   	
13   	
14   	def bin_to_str(binary: bytes) -> str:
15   	    return "".join(map(chr, binary))
16   	
17   	
18   	def get_all_subclasses(cls: T) -> MutableSet[T]:
19   	    subclasses = set(cls.__subclasses__())
20   	    return subclasses.union(
21   	        {s for c in subclasses for s in get_all_subclasses(c)}
22   	    )
23   	
24   	
25   	def get_unique_uuid(already_used: StringCollection) -> str:
26   	    is_duplicate = True
27   	    while is_duplicate:
28   	        candidate = str(uuid.uuid4())
29   	        is_duplicate = candidate in already_used
30   	    return candidate
31   	
32   	
33   	def format_os_error(e: OSError) -> str:
34   	    if e.filename:
35   	        return f"{e.strerror}: '{e.filename}'"
36   	    if e.strerror:
37   	        return e.strerror
38   	    return (f"{e.__class__.__name__} {e}").strip()
39   	
40   	
41   	def xml_fromstring(xml: str) -> _Element:
42   	    # If the xml contains encoding declaration such as:
43   	    # <?xml version="1.0" encoding="UTF-8"?>
44   	    # we get an exception in python3:
45   	    # ValueError: Unicode strings with encoding declaration are not supported.
46   	    # Please use bytes input or XML fragments without declaration.
47   	    # So we encode the string to bytes.
48   	    return etree.fromstring(
49   	        xml.encode("utf-8"),
50   	        # it raises on a huge xml without the flag huge_tree=True
51   	        # see https://bugzilla.redhat.com/show_bug.cgi?id=1506864
(1) Event Sigma main event: The Python application enables entity expansion by setting the `lxml.etree.XMLParser` value `resolve_entities` to `true` or `internal` (the default value). If untrusted XML is parsed with entity expansion enabled, a malicious attacker could submit a document that contains very deeply nested entity definitions (known as a Billion Laughs Attack), causing the parser to use large amounts of memory and processing power resulting in a denial of service (DoS) condition.
(2) Event remediation: Explicitly set `resolve_entities` argument to `False`.
52   	        etree.XMLParser(huge_tree=True),
53   	    )
54   	
55   	
56   	def timeout_to_seconds(timeout: Union[int, str]) -> Optional[int]:
57   	    """
58   	    Transform pacemaker style timeout to number of seconds. If `timeout` is not
59   	    a valid timeout, `None` is returned.
60   	
61   	    timeout -- timeout string
62   	    """
63   	    try:
64   	        candidate = int(timeout)
65   	        if candidate >= 0:
66   	            return candidate
67   	        return None
68   	    except ValueError:
69   	        pass
70   	    # Now we know the timeout is not an integer nor an integer string.
71   	    # Let's make sure mypy knows the timeout is a string as well.
72   	    timeout = str(timeout)
73   	    suffix_multiplier = {
74   	        "s": 1,
75   	        "sec": 1,
76   	        "m": 60,
77   	        "min": 60,
78   	        "h": 3600,
79   	        "hr": 3600,
80   	    }
81   	    for suffix, multiplier in suffix_multiplier.items():
82   	        if timeout.endswith(suffix):
83   	            candidate2 = timeout[: -len(suffix)]
84   	            if is_integer(candidate2, at_least=0):
85   	                return int(candidate2) * multiplier
86   	    return None
87   	
88   	
89   	@dataclass(frozen=True)
90   	class Version:
91   	    major: int
92   	    minor: Optional[int] = None
93   	    revision: Optional[int] = None
94   	
95   	    @property
96   	    def as_full_tuple(self) -> tuple[int, int, int]:
97   	        return (
98   	            self.major,
99   	            self.minor if self.minor is not None else 0,
100  	            self.revision if self.revision is not None else 0,
101  	        )
102  	
103  	    def normalize(self) -> "Version":
104  	        return self.__class__(*self.as_full_tuple)
105  	
106  	    def __iter__(self) -> Generator[Optional[int], None, None]:
107  	        yield from astuple(self)
108  	
109  	    def __getitem__(self, index: int) -> Optional[int]:
110  	        return astuple(self)[index]
111  	
112  	    def __str__(self) -> str:
113  	        return ".".join([str(x) for x in self if x is not None])
114  	
115  	    def __lt__(self, other: "Version") -> bool:
116  	        return self.as_full_tuple < other.as_full_tuple
117  	
118  	    def __le__(self, other: "Version") -> bool:
119  	        return self.as_full_tuple <= other.as_full_tuple
120  	
121  	    def __hash__(self) -> int:
122  	        # https://docs.astral.sh/ruff/rules/eq-without-hash/
123  	        # used self.as_full_tuple because __eq__ and __hash__ should be in sync,
124  	        # objects which compare equal must have the same hash value
125  	        return hash(self.as_full_tuple)
126  	
127  	    # See, https://stackoverflow.com/questions/37557411/why-does-defining-the-argument-types-for-eq-throw-a-mypy-type-error
128  	    def __eq__(self, other: object) -> bool:
129  	        if not isinstance(other, Version):
130  	            return NotImplemented
131  	        return self.as_full_tuple == other.as_full_tuple
132  	
133  	    def __ne__(self, other: object) -> bool:
134  	        if not isinstance(other, Version):
135  	            return NotImplemented
136  	        return self.as_full_tuple != other.as_full_tuple
137  	
138  	    def __gt__(self, other: "Version") -> bool:
139  	        return self.as_full_tuple > other.as_full_tuple
140  	
141  	    def __ge__(self, other: "Version") -> bool:
142  	        return self.as_full_tuple >= other.as_full_tuple
143