157157from __future__ import annotations
158158
159159import json
160+ from pathlib import Path
160161from typing import Any , Optional , List , Dict , Union
161162
162163import pandas as pd
169170_loop_mode : bool = False
170171
171172
173+ _YAML_SUFFIXES = {".yaml" , ".yml" }
174+
175+
176+ def _resolve_path_in_cwd (
177+ user_path : str ,
178+ * ,
179+ allowed_suffixes : Optional [set [str ]] = None ,
180+ must_exist : bool = False ,
181+ ) -> Path :
182+ """
183+ Resolve a user-supplied path safely inside the current working directory.
184+
185+ This is used for convenience helpers that read/write local config/result files.
186+ To avoid path traversal / arbitrary file read/write, absolute paths and paths that
187+ escape the current working directory are rejected.
188+ """
189+ if not isinstance (user_path , str ):
190+ raise TypeError ("path must be a string" )
191+
192+ path = Path (user_path )
193+
194+ # Disallow absolute paths and drive-relative paths (Windows: 'C:foo').
195+ if path .is_absolute () or path .drive :
196+ raise ValueError (
197+ "Absolute or drive-relative paths are not allowed. "
198+ "Use a relative path within the current working directory."
199+ )
200+
201+ base_dir = Path .cwd ().resolve ()
202+ resolved = (base_dir / path ).resolve ()
203+
204+ try :
205+ resolved .relative_to (base_dir )
206+ except ValueError as exc :
207+ raise ValueError (
208+ "Path traversal outside the current working directory is not allowed."
209+ ) from exc
210+
211+ if allowed_suffixes is not None :
212+ suffix = resolved .suffix .lower ()
213+ if suffix not in allowed_suffixes :
214+ allowed = ", " .join (sorted (allowed_suffixes ))
215+ raise ValueError (f"Invalid file extension '{ suffix } '. Allowed: { allowed } ." )
216+
217+ if must_exist and not resolved .is_file ():
218+ raise FileNotFoundError (f"File not found: { resolved } " )
219+
220+ return resolved
221+
222+
172223class ProcessContext :
173224 """
174225 Context manager for explicit process simulation management.
@@ -3108,7 +3159,10 @@ def from_json(
31083159 >>> process = ProcessBuilder.from_json('process_config.json',
31093160 ... fluids={'feed': my_fluid}).run()
31103161 """
3111- with open (json_path , "r" ) as f :
3162+ json_file = _resolve_path_in_cwd (
3163+ json_path , allowed_suffixes = {".json" }, must_exist = True
3164+ )
3165+ with json_file .open ("r" , encoding = "utf-8" ) as f :
31123166 config = json .load (f )
31133167 return cls .from_dict (config , fluids )
31143168
@@ -3149,7 +3203,10 @@ def from_yaml(
31493203 "PyYAML is required for YAML support. Install with: pip install pyyaml"
31503204 )
31513205
3152- with open (yaml_path , "r" ) as f :
3206+ yaml_file = _resolve_path_in_cwd (
3207+ yaml_path , allowed_suffixes = _YAML_SUFFIXES , must_exist = True
3208+ )
3209+ with yaml_file .open ("r" , encoding = "utf-8" ) as f :
31533210 config = yaml .safe_load (f )
31543211 return cls .from_dict (config , fluids )
31553212
@@ -3929,18 +3986,28 @@ def save_results(self, filename: str, format: str = "json") -> "ProcessBuilder":
39293986 >>> process.save_results('results.xlsx', format='excel')
39303987 """
39313988 if format == "json" :
3932- with open (filename , "w" ) as f :
3989+ out_file = _resolve_path_in_cwd (
3990+ filename , allowed_suffixes = {".json" }
3991+ )
3992+ out_file .parent .mkdir (parents = True , exist_ok = True )
3993+ with out_file .open ("w" , encoding = "utf-8" ) as f :
39333994 json .dump (self .results_json (), f , indent = 2 )
39343995 elif format == "csv" :
3935- self .results_dataframe ().to_csv (filename , index = False )
3996+ out_file = _resolve_path_in_cwd (filename , allowed_suffixes = {".csv" })
3997+ out_file .parent .mkdir (parents = True , exist_ok = True )
3998+ self .results_dataframe ().to_csv (str (out_file ), index = False )
39363999 elif format == "excel" :
3937- self .results_dataframe ().to_excel (filename , index = False )
4000+ out_file = _resolve_path_in_cwd (
4001+ filename , allowed_suffixes = {".xlsx" , ".xls" }
4002+ )
4003+ out_file .parent .mkdir (parents = True , exist_ok = True )
4004+ self .results_dataframe ().to_excel (str (out_file ), index = False )
39384005 else :
39394006 raise ValueError (
39404007 f"Unknown format: { format } . Use 'json', 'csv', or 'excel'."
39414008 )
39424009
3943- print (f"Results saved to { filename } " )
4010+ print (f"Results saved to { out_file } " )
39444011 return self
39454012
39464013
@@ -4130,7 +4197,8 @@ def glycoldehydrationlmodule(name, teststream):
41304197
41314198
41324199def openprocess (filename ):
4133- processoperations = jneqsim .process .processmodel .ProcessSystem .open (filename )
4200+ file_path = _resolve_path_in_cwd (filename , must_exist = True )
4201+ processoperations = jneqsim .process .processmodel .ProcessSystem .open (str (file_path ))
41344202 return processoperations
41354203
41364204
@@ -4808,9 +4876,13 @@ def results_json(process, filename=None):
48084876
48094877 # Save to file if a filename is provided
48104878 if filename :
4811- with open (filename , "w" ) as json_file :
4879+ out_file = _resolve_path_in_cwd (
4880+ filename , allowed_suffixes = {".json" }
4881+ )
4882+ out_file .parent .mkdir (parents = True , exist_ok = True )
4883+ with out_file .open ("w" , encoding = "utf-8" ) as json_file :
48124884 json .dump (results , json_file , indent = 4 )
4813- print (f"JSON report saved to { filename } " )
4885+ print (f"JSON report saved to { out_file } " )
48144886
48154887 return results
48164888 except Exception as e :
@@ -5566,7 +5638,8 @@ def create_process_from_config(
55665638 or use pre-created fluid objects.
55675639
55685640 Args:
5569- config: Either a path to a YAML file or a configuration dictionary.
5641+ config: Either a path to a YAML file (relative to the current working
5642+ directory) or a configuration dictionary.
55705643 fluids: Optional dictionary mapping fluid names to fluid objects.
55715644 If the config includes a 'fluids' section, fluids are created
55725645 automatically and merged with this dictionary.
@@ -5709,7 +5782,10 @@ def create_process_from_config(
57095782 "PyYAML is required for YAML support. Install with: pip install pyyaml"
57105783 )
57115784
5712- with open (config , "r" ) as f :
5785+ yaml_file = _resolve_path_in_cwd (
5786+ config , allowed_suffixes = _YAML_SUFFIXES , must_exist = True
5787+ )
5788+ with yaml_file .open ("r" , encoding = "utf-8" ) as f :
57135789 config = yaml .safe_load (f )
57145790
57155791 # Initialize fluids dictionary
@@ -5738,7 +5814,7 @@ def load_process_config(yaml_path: str) -> Dict[str, Any]:
57385814 Useful for inspecting or modifying configurations before building.
57395815
57405816 Args:
5741- yaml_path: Path to YAML configuration file.
5817+ yaml_path: Path to YAML configuration file (relative to the current working directory) .
57425818
57435819 Returns:
57445820 Dictionary with the configuration.
@@ -5757,7 +5833,10 @@ def load_process_config(yaml_path: str) -> Dict[str, Any]:
57575833 "PyYAML is required for YAML support. Install with: pip install pyyaml"
57585834 )
57595835
5760- with open (yaml_path , "r" ) as f :
5836+ yaml_file = _resolve_path_in_cwd (
5837+ yaml_path , allowed_suffixes = _YAML_SUFFIXES , must_exist = True
5838+ )
5839+ with yaml_file .open ("r" , encoding = "utf-8" ) as f :
57615840 return yaml .safe_load (f )
57625841
57635842
@@ -5767,7 +5846,7 @@ def save_process_config(config: Dict[str, Any], yaml_path: str) -> None:
57675846
57685847 Args:
57695848 config: Configuration dictionary.
5770- yaml_path: Path to save the YAML file.
5849+ yaml_path: Path to save the YAML file (relative to the current working directory) .
57715850
57725851 Example:
57735852 >>> config = {
@@ -5783,5 +5862,7 @@ def save_process_config(config: Dict[str, Any], yaml_path: str) -> None:
57835862 "PyYAML is required for YAML support. Install with: pip install pyyaml"
57845863 )
57855864
5786- with open (yaml_path , "w" ) as f :
5865+ yaml_file = _resolve_path_in_cwd (yaml_path , allowed_suffixes = _YAML_SUFFIXES )
5866+ yaml_file .parent .mkdir (parents = True , exist_ok = True )
5867+ with yaml_file .open ("w" , encoding = "utf-8" ) as f :
57875868 yaml .dump (config , f , default_flow_style = False , sort_keys = False )
0 commit comments