From c732f606549689cdbd77553591be6913d70fb8fd Mon Sep 17 00:00:00 2001 From: Titas-Ghosh Date: Thu, 12 Feb 2026 01:00:35 +0530 Subject: [PATCH] Fix ZMQ write JSON serialization for NumPy types --- concore.py | 6 ++++-- mkconcore.py | 34 ++++++++++++++++++++-------------- tests/test_concore.py | 32 ++++++++++++++++++++++++++++++++ 3 files changed, 56 insertions(+), 16 deletions(-) diff --git a/concore.py b/concore.py index 3742412a..088cc5df 100644 --- a/concore.py +++ b/concore.py @@ -373,7 +373,9 @@ def write(port_identifier, name, val, delta=0): if isinstance(port_identifier, str) and port_identifier in zmq_ports: zmq_p = zmq_ports[port_identifier] try: - zmq_p.send_json_with_retry(val) + # Keep ZMQ payloads JSON-serializable by normalizing numpy types. + zmq_val = convert_numpy_to_python(val) + zmq_p.send_json_with_retry(zmq_val) except zmq.error.ZMQError as e: logging.error(f"ZMQ write error on port {port_identifier} (name: {name}): {e}") except Exception as e: @@ -430,4 +432,4 @@ def initval(simtime_val_str): except Exception as e: logging.error(f"Error parsing simtime_val_str '{simtime_val_str}': {e}. Returning empty list.") - return [] \ No newline at end of file + return [] diff --git a/mkconcore.py b/mkconcore.py index c3995df0..3b20f8e4 100644 --- a/mkconcore.py +++ b/mkconcore.py @@ -75,17 +75,23 @@ import shlex # Added for POSIX shell escaping # input validation helper -def safe_name(value, context): - """ - Validates that the input string does not contain characters dangerous - for filesystem paths or shell command injection. - """ - if not value: - raise ValueError(f"{context} cannot be empty") - # blocks path traversal (/, \), control characters, and shell metacharacters (*, ?, <, >, |, ;, &, $, `, ', ", (, )) - if re.search(r'[\x00-\x1F\x7F\\/:*?"<>|;&`$\'()]', value): - raise ValueError(f"Unsafe {context}: '{value}' contains illegal characters.") - return value +def safe_name(value, context, allow_path=False): + """ + Validates that the input string does not contain characters dangerous + for filesystem paths or shell command injection. + """ + if not value: + raise ValueError(f"{context} cannot be empty") + # blocks control characters and shell metacharacters + # allow path separators and drive colons for full paths when needed + if allow_path: + pattern = r'[\x00-\x1F\x7F*?"<>|;&`$\'()]' + else: + # blocks path traversal (/, \, :) in addition to shell metacharacters + pattern = r'[\x00-\x1F\x7F\\/:*?"<>|;&`$\'()]' + if re.search(pattern, value): + raise ValueError(f"Unsafe {context}: '{value}' contains illegal characters.") + return value MKCONCORE_VER = "22-09-18" @@ -146,8 +152,8 @@ def _resolve_concore_path(): sourcedir = sys.argv[2] outdir = sys.argv[3] -# Validate outdir argument -safe_name(outdir, "Output directory argument") +# Validate outdir argument (allow full paths) +safe_name(outdir, "Output directory argument", allow_path=True) if not os.path.isdir(sourcedir): logging.error(f"{sourcedir} does not exist") @@ -1221,4 +1227,4 @@ def cleanup_script_files(): os.chmod(outdir+"/clear",stat.S_IRWXU) os.chmod(outdir+"/maxtime",stat.S_IRWXU) os.chmod(outdir+"/params",stat.S_IRWXU) - os.chmod(outdir+"/unlock",stat.S_IRWXU) \ No newline at end of file + os.chmod(outdir+"/unlock",stat.S_IRWXU) diff --git a/tests/test_concore.py b/tests/test_concore.py index e7db9637..1fb0182d 100644 --- a/tests/test_concore.py +++ b/tests/test_concore.py @@ -212,3 +212,35 @@ def test_windows_quoted_input(self): s = s[1:-1] # simulate quote stripping before parse_params params = parse_params(s) assert params == {"a": 1, "b": 2} + + +class TestWriteZMQ: + @pytest.fixture(autouse=True) + def reset_zmq_ports(self): + import concore + original_ports = concore.zmq_ports.copy() + yield + concore.zmq_ports.clear() + concore.zmq_ports.update(original_ports) + + def test_write_converts_numpy_types_for_zmq(self): + import concore + + class DummyPort: + def __init__(self): + self.sent = None + + def send_json_with_retry(self, message): + self.sent = message + + dummy = DummyPort() + concore.zmq_ports["test_zmq"] = dummy + + payload = [np.int64(7), np.float64(3.5), {"x": np.float32(1.25)}] + concore.write("test_zmq", "data", payload) + + assert dummy.sent is not None + assert dummy.sent == [7, 3.5, {"x": 1.25}] + assert not isinstance(dummy.sent[0], np.generic) + assert not isinstance(dummy.sent[1], np.generic) + assert not isinstance(dummy.sent[2]["x"], np.generic)