Skip to content

Commit 70c7500

Browse files
committed
update
1 parent 644ef50 commit 70c7500

3 files changed

Lines changed: 134 additions & 15 deletions

File tree

README.md

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -144,6 +144,13 @@ NeqSim also includes a `pvtsimulation` package for common PVT experiments (CCE/C
144144
- Documentation: `docs/pvt_simulation.md`
145145
- Direct Java access examples: `examples/pvtsimulation/README.md`
146146

147+
## Transient Multiphase Flow (Two-Fluid Model)
148+
149+
NeqSim includes a mechanistic **Two-Fluid Transient Multiphase Flow Model** for pipeline simulation.
150+
151+
- Jupyter notebook demo (direct Java access): `examples/jupyter/two_fluid_model.ipynb`
152+
- Upstream documentation (NeqSim Java): https://github.com/equinor/neqsim/blob/master/docs/wiki/two_fluid_model.md
153+
147154
### Prerequisites
148155

149156
Java version 8 or higher ([Java JDK](https://adoptium.net/)) needs to be installed. The Python package [JPype](https://github.com/jpype-project/jpype) is used to connect Python and Java. Read the [installation requirements for Jpype](https://jpype.readthedocs.io/en/latest/install.html). Be aware that mixing 64 bit Python with 32 bit Java and vice versa crashes on import of the jpype module. The needed Python packages are listed in the [NeqSim Python dependencies page](https://github.com/equinor/neqsim-python/network/dependencies).

src/neqsim/process/processTools.py

Lines changed: 96 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -157,6 +157,7 @@
157157
from __future__ import annotations
158158

159159
import json
160+
from pathlib import Path
160161
from typing import Any, Optional, List, Dict, Union
161162

162163
import pandas as pd
@@ -169,6 +170,56 @@
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+
172223
class 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

41324199
def 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)
Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
import json
2+
3+
import pytest
4+
5+
6+
def test_resolve_path_rejects_absolute(monkeypatch, tmp_path):
7+
monkeypatch.chdir(tmp_path)
8+
from neqsim.process import processTools
9+
10+
with pytest.raises(ValueError):
11+
processTools._resolve_path_in_cwd(str(tmp_path / "x.yaml"), must_exist=False)
12+
13+
14+
def test_resolve_path_rejects_traversal(monkeypatch, tmp_path):
15+
monkeypatch.chdir(tmp_path)
16+
from neqsim.process import processTools
17+
18+
with pytest.raises(ValueError):
19+
processTools._resolve_path_in_cwd("../secrets.yaml", must_exist=False)
20+
21+
22+
def test_processbuilder_from_json_reads_local_file(monkeypatch, tmp_path):
23+
monkeypatch.chdir(tmp_path)
24+
from neqsim.process.processTools import ProcessBuilder
25+
26+
cfg = {"name": "Test", "equipment": []}
27+
(tmp_path / "process_config.json").write_text(json.dumps(cfg), encoding="utf-8")
28+
29+
builder = ProcessBuilder.from_json("process_config.json")
30+
assert builder.get_process() is not None
31+

0 commit comments

Comments
 (0)