Skip to content

Commit 977369d

Browse files
authored
Merge pull request #327 from Titas-Ghosh/allow-subdir-node-sources-clean
2 parents cc83e4d + 2e10f48 commit 977369d

2 files changed

Lines changed: 104 additions & 43 deletions

File tree

mkconcore.py

Lines changed: 80 additions & 43 deletions
Original file line numberDiff line numberDiff line change
@@ -92,6 +92,24 @@ def safe_name(value, context, allow_path=False):
9292
if re.search(pattern, value):
9393
raise ValueError(f"Unsafe {context}: '{value}' contains illegal characters.")
9494
return value
95+
96+
def safe_relpath(value, context):
97+
"""
98+
Allow relative subpaths while blocking traversal and absolute/drive paths.
99+
"""
100+
if not value:
101+
raise ValueError(f"{context} cannot be empty")
102+
normalized = value.replace("\\", "/")
103+
safe_name(normalized, context, allow_path=True)
104+
if normalized.startswith("/") or normalized.startswith("~"):
105+
raise ValueError(f"Unsafe {context}: absolute paths are not allowed.")
106+
if re.match(r"^[A-Za-z]:", normalized):
107+
raise ValueError(f"Unsafe {context}: drive paths are not allowed.")
108+
if ":" in normalized:
109+
raise ValueError(f"Unsafe {context}: ':' is not allowed in relative paths.")
110+
if any(part in ("", "..") for part in normalized.split("/")):
111+
raise ValueError(f"Unsafe {context}: invalid path segment.")
112+
return normalized
95113

96114
MKCONCORE_VER = "22-09-18"
97115

@@ -273,14 +291,15 @@ def cleanup_script_files():
273291
node_label = re.sub(r'(\s+|\n)', ' ', node_label)
274292

275293
#Validate node labels
276-
if ':' in node_label:
277-
container_part, source_part = node_label.split(':', 1)
278-
safe_name(container_part, f"Node container name '{container_part}'")
279-
safe_name(source_part, f"Node source file '{source_part}'")
280-
else:
281-
safe_name(node_label, f"Node label '{node_label}'")
282-
# Explicitly reject incorrect format to prevent later crashes and ambiguity
283-
raise ValueError(f"Invalid node label '{node_label}': expected format 'container:source' with a ':' separator.")
294+
if ':' in node_label:
295+
container_part, source_part = node_label.split(':', 1)
296+
safe_name(container_part, f"Node container name '{container_part}'")
297+
source_part = safe_relpath(source_part, f"Node source file '{source_part}'")
298+
node_label = f"{container_part}:{source_part}"
299+
else:
300+
safe_name(node_label, f"Node label '{node_label}'")
301+
# Explicitly reject incorrect format to prevent later crashes and ambiguity
302+
raise ValueError(f"Invalid node label '{node_label}': expected format 'container:source' with a ':' separator.")
284303

285304
nodes_dict[node['id']] = node_label
286305
node_id_to_label_map[node['id']] = node_label.split(':')[0]
@@ -466,12 +485,15 @@ def cleanup_script_files():
466485
if not sourcecode:
467486
continue
468487

469-
if "." in sourcecode:
470-
dockername, langext = os.path.splitext(sourcecode)
471-
else:
472-
dockername, langext = sourcecode, ""
473-
474-
script_target_path = os.path.join(outdir, "src", sourcecode)
488+
if "." in sourcecode:
489+
dockername, langext = os.path.splitext(sourcecode)
490+
else:
491+
dockername, langext = sourcecode, ""
492+
493+
script_target_path = os.path.join(outdir, "src", sourcecode)
494+
script_target_parent = os.path.dirname(script_target_path)
495+
if script_target_parent:
496+
os.makedirs(script_target_parent, exist_ok=True)
475497

476498
# If the script was specialized, it's already in outdir/src. If not, copy from sourcedir.
477499
if node_id_key not in node_edge_params:
@@ -661,27 +683,33 @@ def cleanup_script_files():
661683

662684
# 4. Write final iport/oport files
663685
logging.info("Writing .iport and .oport files...")
664-
for node_label, ports in node_port_mappings.items():
686+
for node_label, ports in node_port_mappings.items():
665687
try:
666688
containername, sourcecode = node_label.split(':', 1)
667-
if not sourcecode or "." not in sourcecode: continue
668-
dockername = os.path.splitext(sourcecode)[0]
669-
with open(os.path.join(outdir, "src", f"{dockername}.iport"), "w") as fport:
670-
fport.write(str(ports['iport']).replace("'" + prefixedgenode, "'"))
671-
with open(os.path.join(outdir, "src", f"{dockername}.oport"), "w") as fport:
672-
fport.write(str(ports['oport']).replace("'" + prefixedgenode, "'"))
689+
if not sourcecode or "." not in sourcecode: continue
690+
dockername = os.path.splitext(sourcecode)[0]
691+
iport_path = os.path.join(outdir, "src", f"{dockername}.iport")
692+
oport_path = os.path.join(outdir, "src", f"{dockername}.oport")
693+
iport_parent = os.path.dirname(iport_path)
694+
if iport_parent:
695+
os.makedirs(iport_parent, exist_ok=True)
696+
with open(iport_path, "w") as fport:
697+
fport.write(str(ports['iport']).replace("'" + prefixedgenode, "'"))
698+
with open(oport_path, "w") as fport:
699+
fport.write(str(ports['oport']).replace("'" + prefixedgenode, "'"))
673700
except ValueError:
674701
continue
675702

676703

677704
#if docker, make docker-dirs, generate build, run, stop, clear scripts and quit
678-
if (concoretype=="docker"):
679-
for node in nodes_dict:
680-
containername,sourcecode = nodes_dict[node].split(':')
681-
if len(sourcecode)!=0 and sourcecode.find(".")!=-1: #3/28/21
682-
dockername,langext = sourcecode.split(".")
683-
if not os.path.exists(outdir+"/src/Dockerfile."+dockername): # 3/30/21
684-
try:
705+
if (concoretype=="docker"):
706+
for node in nodes_dict:
707+
containername,sourcecode = nodes_dict[node].split(':')
708+
if len(sourcecode)!=0 and sourcecode.find(".")!=-1: #3/28/21
709+
dockername,langext = sourcecode.split(".")
710+
dockerfile_path = os.path.join(outdir, "src", f"Dockerfile.{dockername}")
711+
if not os.path.exists(dockerfile_path): # 3/30/21
712+
try:
685713
if langext=="py":
686714
src_path = CONCOREPATH+"/Dockerfile.py"
687715
logging.info("assuming .py extension for Dockerfile")
@@ -699,11 +727,14 @@ def cleanup_script_files():
699727
logging.info("assuming .m extension for Dockerfile")
700728
with open(src_path) as fsource:
701729
source_content = fsource.read()
702-
except:
703-
logging.error(f"{CONCOREPATH} is not correct path to concore")
704-
quit()
705-
with open(outdir+"/src/Dockerfile."+dockername,"w") as fcopy:
706-
fcopy.write(source_content)
730+
except:
731+
logging.error(f"{CONCOREPATH} is not correct path to concore")
732+
quit()
733+
dockerfile_parent = os.path.dirname(dockerfile_path)
734+
if dockerfile_parent:
735+
os.makedirs(dockerfile_parent, exist_ok=True)
736+
with open(dockerfile_path,"w") as fcopy:
737+
fcopy.write(source_content)
707738
if langext=="py":
708739
fcopy.write('CMD ["python", "-i", "'+sourcecode+'"]\n')
709740
if langext=="m":
@@ -947,16 +978,22 @@ def cleanup_script_files():
947978
if concoretype=="posix":
948979
fbuild.write('#!/bin/bash' + "\n")
949980

950-
for node in nodes_dict:
951-
containername,sourcecode = nodes_dict[node].split(':')
952-
if len(sourcecode)!=0:
953-
if sourcecode.find(".")==-1:
954-
logging.error("cannot pull container "+sourcecode+" with control core type "+concoretype) #3/28/21
955-
quit()
956-
dockername,langext = sourcecode.split(".")
957-
fbuild.write('mkdir '+containername+"\n")
958-
if concoretype == "windows":
959-
fbuild.write("copy .\\src\\"+sourcecode+" .\\"+containername+"\\"+sourcecode+"\n")
981+
for node in nodes_dict:
982+
containername,sourcecode = nodes_dict[node].split(':')
983+
if len(sourcecode)!=0:
984+
if sourcecode.find(".")==-1:
985+
logging.error("cannot pull container "+sourcecode+" with control core type "+concoretype) #3/28/21
986+
quit()
987+
dockername,langext = sourcecode.split(".")
988+
fbuild.write('mkdir '+containername+"\n")
989+
source_subdir = os.path.dirname(sourcecode).replace("\\", "/")
990+
if source_subdir:
991+
if concoretype == "windows":
992+
fbuild.write("mkdir .\\"+containername+"\\"+source_subdir.replace("/", "\\")+"\n")
993+
else:
994+
fbuild.write("mkdir -p ./"+containername+"/"+source_subdir+"\n")
995+
if concoretype == "windows":
996+
fbuild.write("copy .\\src\\"+sourcecode+" .\\"+containername+"\\"+sourcecode+"\n")
960997
if langext == "py":
961998
fbuild.write("copy .\\src\\concore.py .\\" + containername + "\\concore.py\n")
962999
elif langext == "cpp":

tests/test_cli.py

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -114,6 +114,30 @@ def test_run_command_default_type(self):
114114
else:
115115
self.assertTrue(Path('out/build').exists())
116116

117+
def test_run_command_subdir_source(self):
118+
with self.runner.isolated_filesystem(temp_dir=self.temp_dir):
119+
result = self.runner.invoke(cli, ['init', 'test-project'])
120+
self.assertEqual(result.exit_code, 0)
121+
122+
subdir = Path('test-project/src/subdir')
123+
subdir.mkdir(parents=True, exist_ok=True)
124+
shutil.move('test-project/src/script.py', subdir / 'script.py')
125+
126+
workflow_path = Path('test-project/workflow.graphml')
127+
content = workflow_path.read_text()
128+
content = content.replace('N1:script.py', 'N1:subdir/script.py')
129+
workflow_path.write_text(content)
130+
131+
result = self.runner.invoke(cli, [
132+
'run',
133+
'test-project/workflow.graphml',
134+
'--source', 'test-project/src',
135+
'--output', 'out',
136+
'--type', 'posix'
137+
])
138+
self.assertEqual(result.exit_code, 0)
139+
self.assertTrue(Path('out/src/subdir/script.py').exists())
140+
117141
def test_run_command_existing_output(self):
118142
with self.runner.isolated_filesystem(temp_dir=self.temp_dir):
119143
result = self.runner.invoke(cli, ['init', 'test-project'])

0 commit comments

Comments
 (0)