Skip to content

Commit 924b539

Browse files
authored
Merge pull request #256 from Titas-Ghosh/fix-zmq-numpy-write-serialization
Fix ZMQ write JSON serialization for NumPy types, add regression test
2 parents f791ab0 + c732f60 commit 924b539

3 files changed

Lines changed: 56 additions & 16 deletions

File tree

concore.py

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -373,7 +373,9 @@ def write(port_identifier, name, val, delta=0):
373373
if isinstance(port_identifier, str) and port_identifier in zmq_ports:
374374
zmq_p = zmq_ports[port_identifier]
375375
try:
376-
zmq_p.send_json_with_retry(val)
376+
# Keep ZMQ payloads JSON-serializable by normalizing numpy types.
377+
zmq_val = convert_numpy_to_python(val)
378+
zmq_p.send_json_with_retry(zmq_val)
377379
except zmq.error.ZMQError as e:
378380
logging.error(f"ZMQ write error on port {port_identifier} (name: {name}): {e}")
379381
except Exception as e:
@@ -430,4 +432,4 @@ def initval(simtime_val_str):
430432

431433
except Exception as e:
432434
logging.error(f"Error parsing simtime_val_str '{simtime_val_str}': {e}. Returning empty list.")
433-
return []
435+
return []

mkconcore.py

Lines changed: 20 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -75,17 +75,23 @@
7575
import shlex # Added for POSIX shell escaping
7676

7777
# input validation helper
78-
def safe_name(value, context):
79-
"""
80-
Validates that the input string does not contain characters dangerous
81-
for filesystem paths or shell command injection.
82-
"""
83-
if not value:
84-
raise ValueError(f"{context} cannot be empty")
85-
# blocks path traversal (/, \), control characters, and shell metacharacters (*, ?, <, >, |, ;, &, $, `, ', ", (, ))
86-
if re.search(r'[\x00-\x1F\x7F\\/:*?"<>|;&`$\'()]', value):
87-
raise ValueError(f"Unsafe {context}: '{value}' contains illegal characters.")
88-
return value
78+
def safe_name(value, context, allow_path=False):
79+
"""
80+
Validates that the input string does not contain characters dangerous
81+
for filesystem paths or shell command injection.
82+
"""
83+
if not value:
84+
raise ValueError(f"{context} cannot be empty")
85+
# blocks control characters and shell metacharacters
86+
# allow path separators and drive colons for full paths when needed
87+
if allow_path:
88+
pattern = r'[\x00-\x1F\x7F*?"<>|;&`$\'()]'
89+
else:
90+
# blocks path traversal (/, \, :) in addition to shell metacharacters
91+
pattern = r'[\x00-\x1F\x7F\\/:*?"<>|;&`$\'()]'
92+
if re.search(pattern, value):
93+
raise ValueError(f"Unsafe {context}: '{value}' contains illegal characters.")
94+
return value
8995

9096
MKCONCORE_VER = "22-09-18"
9197

@@ -146,8 +152,8 @@ def _resolve_concore_path():
146152
sourcedir = sys.argv[2]
147153
outdir = sys.argv[3]
148154

149-
# Validate outdir argument
150-
safe_name(outdir, "Output directory argument")
155+
# Validate outdir argument (allow full paths)
156+
safe_name(outdir, "Output directory argument", allow_path=True)
151157

152158
if not os.path.isdir(sourcedir):
153159
logging.error(f"{sourcedir} does not exist")
@@ -1221,4 +1227,4 @@ def cleanup_script_files():
12211227
os.chmod(outdir+"/clear",stat.S_IRWXU)
12221228
os.chmod(outdir+"/maxtime",stat.S_IRWXU)
12231229
os.chmod(outdir+"/params",stat.S_IRWXU)
1224-
os.chmod(outdir+"/unlock",stat.S_IRWXU)
1230+
os.chmod(outdir+"/unlock",stat.S_IRWXU)

tests/test_concore.py

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -212,3 +212,35 @@ def test_windows_quoted_input(self):
212212
s = s[1:-1] # simulate quote stripping before parse_params
213213
params = parse_params(s)
214214
assert params == {"a": 1, "b": 2}
215+
216+
217+
class TestWriteZMQ:
218+
@pytest.fixture(autouse=True)
219+
def reset_zmq_ports(self):
220+
import concore
221+
original_ports = concore.zmq_ports.copy()
222+
yield
223+
concore.zmq_ports.clear()
224+
concore.zmq_ports.update(original_ports)
225+
226+
def test_write_converts_numpy_types_for_zmq(self):
227+
import concore
228+
229+
class DummyPort:
230+
def __init__(self):
231+
self.sent = None
232+
233+
def send_json_with_retry(self, message):
234+
self.sent = message
235+
236+
dummy = DummyPort()
237+
concore.zmq_ports["test_zmq"] = dummy
238+
239+
payload = [np.int64(7), np.float64(3.5), {"x": np.float32(1.25)}]
240+
concore.write("test_zmq", "data", payload)
241+
242+
assert dummy.sent is not None
243+
assert dummy.sent == [7, 3.5, {"x": 1.25}]
244+
assert not isinstance(dummy.sent[0], np.generic)
245+
assert not isinstance(dummy.sent[1], np.generic)
246+
assert not isinstance(dummy.sent[2]["x"], np.generic)

0 commit comments

Comments
 (0)