# Copyright (c) Alibaba, Inc. and its affiliates.
"""Regression test for Issue #1667.

`VideoDetMapper._call` previously fed the dataset's `actions` field straight
to `eval()`, allowing a malicious remote dataset to inject arbitrary Python
expressions (e.g. `__import__('os').system(...)`) and gain RCE during
training. The fix swaps `eval` for `ast.literal_eval`, which only parses
plain literal containers.
"""
import os
import sys
import types
import unittest

from modelscope.preprocessors.cv.action_detection_mapper import \
    VideoDetMapper  # noqa: E402


# `action_detection_mapper` imports `decord`, `detectron2`, `scipy.interpolate`
# at module level. Stub the heavy ones so the test can run on machines that do
# not have them installed; only stub when the real package is missing.
def _stub_module(name, attrs=None):
    if name in sys.modules:
        return
    mod = types.ModuleType(name)
    for k, v in (attrs or {}).items():
        setattr(mod, k, v)
    sys.modules[name] = mod


try:
    import decord  # noqa: F401
except Exception:
    _stub_module('decord', {
        'cpu': lambda *a, **kw: None,
        'VideoReader': object
    })

try:
    import detectron2  # noqa: F401
    import detectron2.data.transforms  # noqa: F401
    import detectron2.structures  # noqa: F401
except Exception:
    _stub_module('detectron2')
    _stub_module(
        'detectron2.data',
        {'transforms': types.ModuleType('detectron2.data.transforms')})
    _stub_module(
        'detectron2.data.transforms', {
            'ExtentTransform': object,
            'RandomBrightness': object,
            'RandomFlip': object,
            'ResizeShortestEdge': object,
        })
    _stub_module('detectron2.structures', {
        'Boxes': object,
        'Instances': object
    })

try:
    import scipy.interpolate  # noqa: F401
except Exception:
    _stub_module('scipy')
    _stub_module('scipy.interpolate', {'interp1d': lambda *a, **kw: None})


class ActionDetectionMapperSecurityTest(unittest.TestCase):
    """Pin the `ast.literal_eval` gate around remote `actions` payloads."""

    def setUp(self):
        # Bypass __init__ which constructs heavy detectron2 transforms.
        self.mapper = VideoDetMapper.__new__(VideoDetMapper)

    def _parse(self, actions_value):
        # Exercise only the literal-eval branch of `_call` without invoking
        # the rest of the heavy pipeline.
        data_dict = {'path:FILE': 'dummy.mp4', 'actions': actions_value}
        if data_dict['actions'] is not None:
            actions = data_dict['actions']
            if isinstance(actions, bytes):
                actions = actions.decode('utf-8')
            if isinstance(actions, str):
                import ast
                actions = ast.literal_eval(actions)
            data_dict['actions'] = actions
        else:
            data_dict['actions'] = []
        return data_dict['actions']

    def test_legitimate_python_repr_payload_parses(self):
        legit = ("[{'start': 0, 'end': 30, 'label': 'walk',"
                 " 'boxes': {'0': [1, 2, 3, 4]}}]")
        result = self._parse(legit)
        self.assertEqual(result[0]['label'], 'walk')
        self.assertEqual(result[0]['boxes']['0'], [1, 2, 3, 4])

    def test_legitimate_json_payload_parses(self):
        legit = ('[{"start": 0, "end": 30, "label": "walk",'
                 ' "boxes": {"0": [1, 2, 3, 4]}}]')
        result = self._parse(legit)
        self.assertEqual(result[0]['label'], 'walk')

    def test_none_payload_becomes_empty_list(self):
        self.assertEqual(self._parse(None), [])

    def test_already_parsed_list_passes_through(self):
        # If a downstream caller pre-parses the JSON, we should not try to
        # literal_eval a list (which would raise TypeError).
        already = [{
            'start': 0,
            'end': 1,
            'label': 'x',
            'boxes': {
                '0': [0, 0, 1, 1]
            }
        }]
        self.assertEqual(self._parse(already), already)

    def test_bytes_payload_is_decoded_then_parsed(self):
        legit = ("[{'start': 0, 'end': 1, 'label': 'walk',"
                 " 'boxes': {'0': [1, 2, 3, 4]}}]").encode('utf-8')
        result = self._parse(legit)
        self.assertEqual(result[0]['label'], 'walk')

    def test_malicious_import_payload_blocked(self):
        # Canary file: literal_eval must NEVER reach `os.system`.
        canary = '/tmp/ms_action_det_rce_canary'
        if os.path.exists(canary):
            os.remove(canary)
        mal = ("__import__('os').system("
               "'echo HACKED && touch {}')").format(canary)
        with self.assertRaises((ValueError, SyntaxError)):
            self._parse(mal)
        self.assertFalse(
            os.path.exists(canary),
            'os.system was reached -- eval gate is bypassed.')

    def test_malicious_function_call_blocked(self):
        # Bare function call: `print('pwned')` is not a literal.
        with self.assertRaises((ValueError, SyntaxError)):
            self._parse("print('pwned')")

    def test_malicious_attribute_access_blocked(self):
        with self.assertRaises((ValueError, SyntaxError)):
            self._parse('().__class__.__bases__[0].__subclasses__()')

    def test_mapper_call_swallows_malicious_payload(self):
        # The public `__call__` wraps `_call` in try/except and returns None
        # on failure. Ensure malicious payloads land there cleanly rather
        # than executing.
        canary = '/tmp/ms_action_det_rce_canary_call'
        if os.path.exists(canary):
            os.remove(canary)
        mal = ("__import__('os').system(" "'touch {}')").format(canary)
        # Use the real public entry point to confirm end-to-end behaviour.
        out = self.mapper.__call__({'path:FILE': 'dummy.mp4', 'actions': mal})
        self.assertIsNone(out)
        self.assertFalse(os.path.exists(canary))


if __name__ == '__main__':
    unittest.main()
