# Copyright 2026 SigmaSRC, Inc. All Rights Reserved.

"""Reading the output of OS commands as text.

Windows console programs emit the console (OEM) code page -- cp850 on a
French host, cp437 on a US one -- never UTF-8.  Since 1.6.2 the agent runs
in UTF-8 mode (PYTHONUTF8=1, see sigma.util.utf8), which makes os.popen()
decode its pipe as *strict* UTF-8.  A bare os.popen(cmd, "r").read() then
blows up on the first accented byte:

    UnicodeDecodeError: 'utf-8' codec can't decode byte 0x82 in position 14

0x82 is 'e' with an acute accent in cp850.  Raised inside a Fuel rule this
surfaces to the server as

    Run-time exception (builtins.UnicodeDecodeError: ...)

and the rule reports nothing at all -- so a single accented character in
'net localgroup' or 'qwinsta' output loses the whole check on that host.

popen() below is a drop-in replacement for os.popen(cmd, "r") that decodes
with the console code page and replaces undecodable bytes instead of
raising.  On non-Windows platforms it *is* os.popen, so nothing about the
POSIX path changes -- in particular the wait-status encoding that
rtlib.runscript() unpacks as (sts >> 8, sts & 0xff).

This generalises the fix 1.6.3 made in gather/gather_command.py, which
carries its own copy of this logic for the command gatherer.
"""

import os
import subprocess
import sys

# Windows console output is the OEM code page; Python spells that "oem".
# Everywhere else UTF-8 is both the truth and the default.
CONSOLE_ENCODING = "oem" if sys.platform == "win32" else "utf-8"


class _ConsolePipe(object):

    """The file-like object popen() hands back.

    Supports the operations call sites actually use on an os.popen()
    result -- iteration, read(), readline(), readlines(), close() -- and
    keeps os.popen's close() contract: None when the child exited
    cleanly, otherwise its exit status.

    """

    def __init__(self, proc):
        self._proc = proc
        self._stream = proc.stdout

    def __iter__(self):
        return iter(self._stream)

    def __enter__(self):
        return self

    def __exit__(self, *exc_info):
        self.close()

    def read(self, *args):
        return self._stream.read(*args)

    def readline(self, *args):
        return self._stream.readline(*args)

    def readlines(self, *args):
        return self._stream.readlines(*args)

    def close(self):
        self._stream.close()
        sts = self._proc.wait()
        # os.popen() reports a clean exit as None, not 0.
        return sts or None


def _popen_encoded(command, encoding, errors="replace"):
    """popen() with the decoding spelled out; the seam the tests drive.

    Kept platform-independent on purpose: a test can ask for cp850 on
    Linux and exercise exactly what a French Windows host does.

    """
    proc = subprocess.Popen(command, shell=True, stdout=subprocess.PIPE,
                            encoding=encoding, errors=errors)
    return _ConsolePipe(proc)


if sys.platform == "win32":

    def popen(command, mode="r"):
        """os.popen(command, "r"), decoded with the console code page."""
        if "w" in mode:
            raise ValueError("console.popen() supports read mode only")
        return _popen_encoded(command, CONSOLE_ENCODING)

else:
    # No OEM code page in play -- keep os.popen and its exact semantics.
    popen = os.popen
