CoolProp 8.0.0
An open-source fluid property and humid air property database
SVDSurfaceSerializer.cpp
Go to the documentation of this file.
2
3#include <cmath>
4#include <cstdio>
5#include <cstring>
6#include <filesystem>
7#include <fstream>
8#include <stdexcept>
9#include <utility>
10#include <vector>
11
13
14#include <msgpack.hpp>
15
28#include "miniz.h"
29
30namespace CoolProp {
31namespace sbtl {
32
33namespace {
34
35// Discriminator for the BoundaryCurve subclass stored in a packed
36// region blob. Adding a new subclass: bump revision, give it a new
37// kind id, write/read its State.
38enum class CurveKind : std::uint8_t
39{
40 CONSTANT = 0,
41 CUBIC_SPLINE = 1,
42 PIECEWISE_CHEBYSHEV = 2,
43 // SuperancillaryBoundaryCurve — stores only the State POD scalars
44 // (no piecewise tables); the SuperAncillary handle itself is
45 // re-acquired at load time from the source HEOS for the fluid.
46 SUPERANCILLARY = 3,
47 // SuperancillaryTemperatureBoundaryCurve — T-parameterized sibling
48 // of SUPERANCILLARY used by the DT-indexed preset for its
49 // rho_sat,L / rho_sat,V dome boundaries. Same POD-only scheme
50 // (T_min/T_max in place of p_min/p_max); SA handle re-acquired at
51 // load time.
52 SUPERANCILLARY_T = 4,
53};
54
55// ---------- packing helpers -------------------------------------------
56
57template <typename Packer>
58void pack_curve(Packer& pk, const region::BoundaryCurve& curve) {
59 // dynamic_cast probes for each known concrete type. Order is
60 // chosen so the cheapest cast (ConstantCurve) is tried first.
61 if (const auto* c = dynamic_cast<const region::ConstantCurve*>(&curve)) {
62 const auto s = c->state();
63 pk.pack_array(4);
64 pk.pack(static_cast<std::uint8_t>(CurveKind::CONSTANT));
65 pk.pack(s.a_lo);
66 pk.pack(s.a_hi);
67 pk.pack(s.b);
68 return;
69 }
70 if (const auto* c = dynamic_cast<const region::CubicSplineCurve*>(&curve)) {
71 const auto s = c->state();
72 pk.pack_array(6);
73 pk.pack(static_cast<std::uint8_t>(CurveKind::CUBIC_SPLINE));
74 pk.pack(s.a);
75 pk.pack(s.b);
76 pk.pack(s.M);
77 pk.pack(s.b_min);
78 pk.pack(s.b_max);
79 return;
80 }
81 if (const auto* c = dynamic_cast<const region::PiecewiseChebyshevCurve*>(&curve)) {
82 const auto s = c->state();
83 pk.pack_array(7);
84 pk.pack(static_cast<std::uint8_t>(CurveKind::PIECEWISE_CHEBYSHEV));
85 pk.pack(s.a_lo);
86 pk.pack(s.a_hi);
87 pk.pack(static_cast<std::uint8_t>(s.scale));
88 pk.pack_array(s.pieces.size());
89 for (const auto& p : s.pieces) {
90 pk.pack_array(6);
91 pk.pack(p.t_lo);
92 pk.pack(p.t_hi);
93 pk.pack(p.inv_half_span);
94 pk.pack(p.t_mid);
95 pk.pack(p.coeffs);
96 pk.pack(p.deriv_coeffs);
97 }
98 pk.pack(s.b_min);
99 pk.pack(s.b_max);
100 return;
101 }
102 if (const auto* c = dynamic_cast<const region::SuperancillaryBoundaryCurve*>(&curve)) {
103 const auto s = c->state();
104 // Pack only the State POD scalars; the SuperAncillary handle
105 // itself isn't serialised (it's a per-fluid singleton lazily
106 // built by HelmholtzEOSMixtureBackend). On load, the handle
107 // is re-acquired from the fluid HEOS at unpack_surface entry.
108 pk.pack_array(8);
109 pk.pack(static_cast<std::uint8_t>(CurveKind::SUPERANCILLARY));
110 pk.pack(s.p_min);
111 pk.pack(s.p_max);
112 pk.pack(static_cast<std::int8_t>(s.prop_key));
113 pk.pack(static_cast<std::int16_t>(s.Q));
114 pk.pack(s.output_scale);
115 pk.pack(s.b_min);
116 pk.pack(s.b_max);
117 return;
118 }
119 if (const auto* c = dynamic_cast<const region::SuperancillaryTemperatureBoundaryCurve*>(&curve)) {
120 const auto s = c->state();
121 // T-parameterized sibling of SUPERANCILLARY (DT-preset dome
122 // boundaries). Same POD-only scheme; the SuperAncillary handle
123 // is re-acquired from the fluid HEOS at load time.
124 pk.pack_array(8);
125 pk.pack(static_cast<std::uint8_t>(CurveKind::SUPERANCILLARY_T));
126 pk.pack(s.T_min);
127 pk.pack(s.T_max);
128 pk.pack(static_cast<std::int8_t>(s.prop_key));
129 pk.pack(static_cast<std::int16_t>(s.Q));
130 pk.pack(s.output_scale);
131 pk.pack(s.b_min);
132 pk.pack(s.b_max);
133 return;
134 }
135 throw std::runtime_error("SVDSurfaceSerializer: unknown BoundaryCurve subclass");
136}
137
138template <typename Packer>
139void pack_decomp(Packer& pk, std::size_t region_idx, ::CoolProp::parameters prop_key, const svd::SVDDecomposition& d) {
140 pk.pack_array(14);
141 pk.pack(static_cast<std::uint32_t>(region_idx));
142 pk.pack(static_cast<std::int32_t>(prop_key));
143 pk.pack(d.NX);
144 pk.pack(d.NY);
145 pk.pack(d.rank);
146 pk.pack(static_cast<std::uint8_t>(d.out_transform));
147 pk.pack(static_cast<std::uint8_t>(d.slope_source));
148 pk.pack(d.x_grid);
149 pk.pack(d.y_grid);
150 pk.pack(d.U);
151 pk.pack(d.dU_dx);
152 pk.pack(d.V_S);
153 pk.pack(d.dV_S_dy);
154 pk.pack(d.S);
155}
156
157template <typename Packer>
158void pack_region(Packer& pk, const region::Region& r) {
159 const auto& axis = r.primary();
160 pk.pack_array(9);
161 pk.pack(static_cast<std::uint8_t>(axis.scale));
162 pk.pack(axis.a_lo);
163 pk.pack(axis.a_hi);
164 pk.pack(axis.a_lo_t);
165 pk.pack(axis.a_hi_t);
166 pk.pack(axis.inv_span_t);
167 pack_curve(pk, r.b_lo());
168 pack_curve(pk, r.b_hi());
169 // Secondary-axis (eta) scale — element [8], appended in rev 16.
170 // LINEAR (0) for every pre-rev-16 surface; DmassT VAPOR/SUPER use LOG.
171 pk.pack(static_cast<std::uint8_t>(r.secondary_scale()));
172}
173
174template <typename Packer>
175void pack_surface(Packer& pk, const SVDSurface& s) {
176 pk.pack_array(4);
177 pk.pack(static_cast<std::int32_t>(s.input_pair()));
178
179 const auto& props = s.properties();
180 pk.pack_array(props.size());
181 for (const auto& p : props) {
182 // Property entry is currently a length-1 array `[key]`. The
183 // OutputTransform lives inside the per-property SVDDecomposition
184 // blob (packed by pack_decomp below), so the surface-level
185 // record only needs the property key here. Leaving it as a
186 // length-1 array (rather than packing the bare int) lets a
187 // future revision extend it to `[key, transform_override]` or
188 // similar without bumping the on-wire revision number.
189 pk.pack_array(1);
190 pk.pack(static_cast<std::int32_t>(p));
191 }
192
193 const std::size_t n_regions = s.region_count();
194 pk.pack_array(n_regions);
195 for (std::size_t r = 0; r < n_regions; ++r) {
196 pack_region(pk, s.atlas().region(r));
197 }
198
199 // Decomps are flattened: region_idx ranges first, then properties.
200 pk.pack_array(n_regions * props.size());
201 for (std::size_t r = 0; r < n_regions; ++r) {
202 for (const auto& p : props) {
203 pack_decomp(pk, r, p, s.decomposition(r, p));
204 }
205 }
206}
207
208// ---------- unpacking helpers ----------------------------------------
209//
210// The msgpack-c API is intrinsically pointer-arithmetic-on-unions:
211// every array access goes through `obj.via.array.ptr[i]` where `via`
212// is a tagged union and `ptr` is a raw msgpack_object*. Both patterns
213// trip cppcoreguidelines-pro-{type-union-access,bounds-pointer-arithmetic}
214// on every line of the unpacker. These are msgpack-c idioms, not
215// real bugs, so we silence them en bloc rather than per-line.
216// NOLINTBEGIN(cppcoreguidelines-pro-type-union-access,cppcoreguidelines-pro-bounds-pointer-arithmetic)
217
218// Convenience wrappers around msgpack::object → typed conversion.
219template <typename T>
220T as(const msgpack::object& o) {
221 return o.as<T>();
222}
223
224void check_array(const msgpack::object& o, std::size_t expected_min, const char* what) {
225 if (o.type != msgpack::type::ARRAY) {
226 throw std::runtime_error(std::string("SVDSurfaceSerializer: expected array for ") + what);
227 }
228 if (o.via.array.size < expected_min) {
229 throw std::runtime_error(std::string("SVDSurfaceSerializer: array for ") + what + " too short");
230 }
231}
232
233std::unique_ptr<region::BoundaryCurve> unpack_curve(const msgpack::object& o,
234 const std::shared_ptr<region::SuperancillaryBoundaryCurve::SuperAncillary_t>& sa) {
235 check_array(o, 1, "curve");
236 const auto kind = static_cast<CurveKind>(as<std::uint8_t>(o.via.array.ptr[0]));
237 switch (kind) {
238 case CurveKind::CONSTANT: {
239 check_array(o, 4, "constant curve");
240 const auto a_lo = as<double>(o.via.array.ptr[1]);
241 const auto a_hi = as<double>(o.via.array.ptr[2]);
242 const auto b = as<double>(o.via.array.ptr[3]);
243 return std::make_unique<region::ConstantCurve>(a_lo, a_hi, b);
244 }
245 case CurveKind::CUBIC_SPLINE: {
246 check_array(o, 6, "cubic spline curve");
247 region::CubicSplineCurve::State s;
248 s.a = as<std::vector<double>>(o.via.array.ptr[1]);
249 s.b = as<std::vector<double>>(o.via.array.ptr[2]);
250 s.M = as<std::vector<double>>(o.via.array.ptr[3]);
251 s.b_min = as<double>(o.via.array.ptr[4]);
252 s.b_max = as<double>(o.via.array.ptr[5]);
253 return region::CubicSplineCurve::from_state(std::move(s));
254 }
255 case CurveKind::PIECEWISE_CHEBYSHEV: {
256 check_array(o, 7, "piecewise chebyshev curve");
257 region::PiecewiseChebyshevCurve::State s;
258 s.a_lo = as<double>(o.via.array.ptr[1]);
259 s.a_hi = as<double>(o.via.array.ptr[2]);
260 s.scale = static_cast<region::PiecewiseChebyshevCurve::ParamScale>(as<std::uint8_t>(o.via.array.ptr[3]));
261 const auto& pieces_obj = o.via.array.ptr[4];
262 check_array(pieces_obj, 1, "chebyshev pieces");
263 s.pieces.reserve(pieces_obj.via.array.size);
264 for (std::uint32_t i = 0; i < pieces_obj.via.array.size; ++i) {
265 const auto& po = pieces_obj.via.array.ptr[i];
266 check_array(po, 6, "chebyshev piece");
267 region::PiecewiseChebyshevCurve::PieceState ps;
268 ps.t_lo = as<double>(po.via.array.ptr[0]);
269 ps.t_hi = as<double>(po.via.array.ptr[1]);
270 ps.inv_half_span = as<double>(po.via.array.ptr[2]);
271 ps.t_mid = as<double>(po.via.array.ptr[3]);
272 ps.coeffs = as<std::vector<double>>(po.via.array.ptr[4]);
273 ps.deriv_coeffs = as<std::vector<double>>(po.via.array.ptr[5]);
274 s.pieces.push_back(std::move(ps));
275 }
276 s.b_min = as<double>(o.via.array.ptr[5]);
277 s.b_max = as<double>(o.via.array.ptr[6]);
279 }
280 case CurveKind::SUPERANCILLARY: {
281 check_array(o, 8, "superancillary curve");
282 if (!sa) {
283 throw std::runtime_error("SVDSurfaceSerializer: stream contains a SuperAncillary-backed curve but no SuperAncillary handle is "
284 "available for the fluid (source backend must be HEOS for re-hydration)");
285 }
286 region::SuperancillaryBoundaryCurve::State s{};
287 s.p_min = as<double>(o.via.array.ptr[1]);
288 s.p_max = as<double>(o.via.array.ptr[2]);
289 s.prop_key = static_cast<char>(as<std::int8_t>(o.via.array.ptr[3]));
290 s.Q = static_cast<short>(as<std::int16_t>(o.via.array.ptr[4]));
291 s.output_scale = as<double>(o.via.array.ptr[5]);
292 s.b_min = as<double>(o.via.array.ptr[6]);
293 s.b_max = as<double>(o.via.array.ptr[7]);
295 }
296 case CurveKind::SUPERANCILLARY_T: {
297 check_array(o, 8, "superancillary-T curve");
298 if (!sa) {
299 throw std::runtime_error("SVDSurfaceSerializer: stream contains a SuperAncillary-backed curve but no SuperAncillary handle is "
300 "available for the fluid (source backend must be HEOS for re-hydration)");
301 }
302 region::SuperancillaryTemperatureBoundaryCurve::State s{};
303 s.T_min = as<double>(o.via.array.ptr[1]);
304 s.T_max = as<double>(o.via.array.ptr[2]);
305 s.prop_key = static_cast<char>(as<std::int8_t>(o.via.array.ptr[3]));
306 s.Q = static_cast<short>(as<std::int16_t>(o.via.array.ptr[4]));
307 s.output_scale = as<double>(o.via.array.ptr[5]);
308 s.b_min = as<double>(o.via.array.ptr[6]);
309 s.b_max = as<double>(o.via.array.ptr[7]);
311 }
312 }
313 throw std::runtime_error("SVDSurfaceSerializer: unknown curve kind");
314}
315
316region::Region unpack_region(const msgpack::object& o, const std::shared_ptr<region::SuperancillaryBoundaryCurve::SuperAncillary_t>& sa) {
317 // 9 elements since rev 16 (secondary-axis scale at [8]). The load()
318 // revision gate rejects pre-rev-16 streams before they reach here, so
319 // the 9th element is always present.
320 check_array(o, 9, "region");
321 const auto scale = static_cast<region::AxisScale>(as<std::uint8_t>(o.via.array.ptr[0]));
322 const auto a_lo = as<double>(o.via.array.ptr[1]);
323 const auto a_hi = as<double>(o.via.array.ptr[2]);
324 auto axis = region::AxisTransform::make(scale, a_lo, a_hi);
325 // a_lo_t/a_hi_t/inv_span_t are derived; we re-derive via make()
326 // and don't read the stream values — they're stored for round-
327 // trip diagnostics only.
328 auto b_lo = unpack_curve(o.via.array.ptr[6], sa);
329 auto b_hi = unpack_curve(o.via.array.ptr[7], sa);
330 const auto secondary = static_cast<region::AxisScale>(as<std::uint8_t>(o.via.array.ptr[8]));
331 return {axis, std::move(b_lo), std::move(b_hi), secondary};
332}
333
334svd::SVDDecomposition unpack_decomp(const msgpack::object& o, std::size_t* out_region_idx, ::CoolProp::parameters* out_prop) {
335 check_array(o, 14, "decomp");
336 *out_region_idx = static_cast<std::size_t>(as<std::uint32_t>(o.via.array.ptr[0]));
337 *out_prop = static_cast<::CoolProp::parameters>(as<std::int32_t>(o.via.array.ptr[1]));
338 svd::SVDDecomposition d;
339 d.NX = as<std::int32_t>(o.via.array.ptr[2]);
340 d.NY = as<std::int32_t>(o.via.array.ptr[3]);
341 d.rank = as<std::int32_t>(o.via.array.ptr[4]);
342 d.out_transform = static_cast<svd::OutputTransform>(as<std::uint8_t>(o.via.array.ptr[5]));
343 d.slope_source = static_cast<svd::SlopeSource>(as<std::uint8_t>(o.via.array.ptr[6]));
344 d.x_grid = as<std::vector<double>>(o.via.array.ptr[7]);
345 d.y_grid = as<std::vector<double>>(o.via.array.ptr[8]);
346 d.U = as<std::vector<double>>(o.via.array.ptr[9]);
347 d.dU_dx = as<std::vector<double>>(o.via.array.ptr[10]);
348 d.V_S = as<std::vector<double>>(o.via.array.ptr[11]);
349 d.dV_S_dy = as<std::vector<double>>(o.via.array.ptr[12]);
350 d.S = as<std::vector<double>>(o.via.array.ptr[13]);
351 return d;
352}
353
354// Best-effort SuperAncillary handle acquisition for `fluid_name`. Returns
355// null when the fluid isn't a HEOS pure fluid (mixtures / unknown names);
356// the unpack_curve SUPERANCILLARY arm will throw with a clear message if
357// it actually needs the handle. We don't differentiate the source-backend
358// here (HEOS vs REFPROP vs IF97) because only HEOS surfaces ever emit a
359// SuperancillaryBoundaryCurve into the stream — REFPROP / IF97 fall
360// through to the cubic-spline path at sat-boundary build time.
361std::shared_ptr<region::SuperancillaryBoundaryCurve::SuperAncillary_t> acquire_superanc_for(const std::string& fluid_name) noexcept {
362 try {
363 std::unique_ptr<::CoolProp::AbstractState> as(::CoolProp::AbstractState::factory("HEOS", fluid_name));
364 auto* heos = dynamic_cast<::CoolProp::HelmholtzEOSMixtureBackend*>(as.get());
365 if (heos == nullptr) return nullptr;
366 auto sa = heos->get_superanc();
367 if (sa) {
368 // Eagerly build the caloric expansions so the deserialized
369 // curve's first eval() doesn't pay the lazy-build cost on
370 // the hot path.
371 try {
372 heos->ensure_caloric_superancillaries();
373 } catch (...) { // NOLINT(bugprone-empty-catch)
374 }
375 }
376 return sa;
377 } catch (...) { // NOLINT(bugprone-empty-catch)
378 return nullptr;
379 }
380}
381
382SVDSurface unpack_surface(const std::string& fluid_name, const msgpack::object& o) {
383 check_array(o, 4, "surface");
384 const auto input_pair = static_cast<::CoolProp::input_pairs>(as<std::int32_t>(o.via.array.ptr[0]));
385
386 const auto& props_obj = o.via.array.ptr[1];
387 check_array(props_obj, 0, "properties");
388 std::vector<::CoolProp::parameters> properties;
389 properties.reserve(props_obj.via.array.size);
390 for (std::uint32_t i = 0; i < props_obj.via.array.size; ++i) {
391 const auto& pe = props_obj.via.array.ptr[i];
392 check_array(pe, 1, "property entry");
393 properties.push_back(static_cast<::CoolProp::parameters>(as<std::int32_t>(pe.via.array.ptr[0])));
394 }
395
396 SVDSurface surface(fluid_name, input_pair, properties);
397
398 // Acquire SA handle once per surface load — passed into unpack_region
399 // → unpack_curve so the SUPERANCILLARY arm can rehydrate without
400 // re-constructing the HEOS state for every region.
401 const auto sa = acquire_superanc_for(fluid_name);
402
403 const auto& regions_obj = o.via.array.ptr[2];
404 check_array(regions_obj, 0, "regions");
405 for (std::uint32_t i = 0; i < regions_obj.via.array.size; ++i) {
406 surface.add_region(unpack_region(regions_obj.via.array.ptr[i], sa));
407 }
408
409 const auto& decomps_obj = o.via.array.ptr[3];
410 check_array(decomps_obj, 0, "decomps");
411 for (std::uint32_t i = 0; i < decomps_obj.via.array.size; ++i) {
412 std::size_t r_idx = 0;
414 auto decomp = unpack_decomp(decomps_obj.via.array.ptr[i], &r_idx, &prop);
415 surface.add_region_property_svd(r_idx, prop, std::move(decomp));
416 }
417
418 surface.seal();
419 return surface;
420}
421
422// ---------- compression / decompression -------------------------------
423
424std::vector<char> zlib_compress(const msgpack::sbuffer& sbuf) {
425 // Match TabularBackends.cpp's allocation strategy: start with the
426 // raw size and let zlib write into that buffer. miniz returns
427 // Z_OK when it fits.
428 std::vector<char> out(sbuf.size() + (sbuf.size() / 1000) + 128);
429 auto out_size = static_cast<mz_ulong>(out.size());
430 // NOLINTBEGIN(cppcoreguidelines-pro-type-reinterpret-cast) — zlib's C API takes unsigned char*; our buffers are char (pre-existing glue).
431 const int code = compress(reinterpret_cast<unsigned char*>(out.data()), &out_size, reinterpret_cast<const unsigned char*>(sbuf.data()),
432 static_cast<mz_ulong>(sbuf.size()));
433 // NOLINTEND(cppcoreguidelines-pro-type-reinterpret-cast)
434 if (code != Z_OK) {
435 throw std::runtime_error(std::string("SVDSurfaceSerializer: zlib compress failed (code ") + std::to_string(code) + ")");
436 }
437 out.resize(out_size);
438 return out;
439}
440
441std::vector<char> zlib_uncompress(const std::vector<char>& compressed) {
442 // Mirrors TabularBackends.cpp:52-69: start at 5x the compressed
443 // size, double on Z_BUF_ERROR until it fits or we exhaust patience.
444 std::vector<char> out(compressed.size() * 5);
445 auto out_size = static_cast<mz_ulong>(out.size());
446 auto in_size = static_cast<mz_ulong>(compressed.size());
447 int code = 0;
448 int retries = 0;
449 do {
450 // NOLINTBEGIN(cppcoreguidelines-pro-type-reinterpret-cast) — zlib C API takes unsigned char* (pre-existing glue).
451 code =
452 uncompress(reinterpret_cast<unsigned char*>(out.data()), &out_size, reinterpret_cast<const unsigned char*>(compressed.data()), in_size);
453 // NOLINTEND(cppcoreguidelines-pro-type-reinterpret-cast)
454 if (code == Z_BUF_ERROR) {
455 if (++retries > 8) {
456 throw std::runtime_error("SVDSurfaceSerializer: zlib uncompress would not fit after 8 retries");
457 }
458 out.resize(out.size() * 2);
459 out_size = static_cast<mz_ulong>(out.size());
460 } else if (code != Z_OK) {
461 throw std::runtime_error(std::string("SVDSurfaceSerializer: zlib uncompress failed (code ") + std::to_string(code) + ")");
462 }
463 } while (code != Z_OK);
464 out.resize(out_size);
465 return out;
466}
467
468} // namespace
469
470// ---------- public API ------------------------------------------------
471
472std::vector<char> SVDSurfaceSerializer::save(const SVDSurface& surface) {
473 if (!surface.sealed()) {
474 throw std::logic_error("SVDSurfaceSerializer::save: surface must be sealed");
475 }
476 msgpack::sbuffer sbuf;
477 msgpack::packer<msgpack::sbuffer> pk(sbuf);
478 pk.pack_array(4);
479 pk.pack(std::string("SVDS"));
480 pk.pack(static_cast<std::int32_t>(kRevision));
481 pk.pack(surface.fluid_name());
482 // Single-surface file for now — but the format puts surfaces in
483 // an array so a single fluid's PH + PT surfaces could share one
484 // file in the future without a revision bump.
485 pk.pack_array(1);
486 pack_surface(pk, surface);
487 return zlib_compress(sbuf);
488}
489
490SVDSurface SVDSurfaceSerializer::load(const std::vector<char>& compressed) {
491 const auto plain = zlib_uncompress(compressed);
492
493 msgpack::object_handle oh;
494 msgpack::unpack(oh, plain.data(), plain.size());
495 const msgpack::object& root = oh.get();
496
497 check_array(root, 4, "root");
498 const auto magic = as<std::string>(root.via.array.ptr[0]);
499 if (magic != "SVDS") {
500 throw std::runtime_error("SVDSurfaceSerializer::load: bad magic (expected 'SVDS', got '" + magic + "')");
501 }
502 const auto revision = as<std::int32_t>(root.via.array.ptr[1]);
503 if (revision != kRevision) {
504 throw std::runtime_error("SVDSurfaceSerializer::load: revision mismatch (expected " + std::to_string(kRevision) + ", got "
505 + std::to_string(revision) + ")");
506 }
507 const auto fluid_name = as<std::string>(root.via.array.ptr[2]);
508
509 const auto& surfaces_obj = root.via.array.ptr[3];
510 check_array(surfaces_obj, 1, "surfaces");
511 if (surfaces_obj.via.array.size != 1) {
512 throw std::runtime_error("SVDSurfaceSerializer::load: expected exactly 1 surface (got " + std::to_string(surfaces_obj.via.array.size) + ")");
513 }
514 return unpack_surface(fluid_name, surfaces_obj.via.array.ptr[0]);
515}
516
517// NOLINTEND(cppcoreguidelines-pro-type-union-access,cppcoreguidelines-pro-bounds-pointer-arithmetic)
518
519void SVDSurfaceSerializer::save_to_file(const SVDSurface& surface, const std::string& path) {
520 const auto compressed = save(surface);
521 ::write_bytes_atomic(std::filesystem::path(path), compressed.data(), compressed.size(), /*restrict_perms=*/false);
522}
523
525 std::ifstream in(path, std::ios::binary | std::ios::ate);
526 if (!in) {
527 throw std::runtime_error("SVDSurfaceSerializer::load_from_file: cannot open " + path);
528 }
529 const auto size = in.tellg();
530 in.seekg(0, std::ios::beg);
531 std::vector<char> buf(static_cast<std::size_t>(size));
532 if (!in.read(buf.data(), size)) {
533 throw std::runtime_error("SVDSurfaceSerializer::load_from_file: read failed for " + path);
534 }
535 return load(buf);
536}
537
539 // ALTERNATIVE_SVDTABLES_DIRECTORY (when non-empty) overrides the
540 // default $HOME/.CoolProp/SVDTables location. Both the .svd.bin.z
541 // surfaces and the .critpatch.bin sidecars route through this
542 // helper, so a single config key redirects both. Motivation:
543 // read-only $HOME (CI containers), shared workstations, centrally-
544 // managed cache directories.
545 const std::string alt = ::CoolProp::get_config_string(ALTERNATIVE_SVDTABLES_DIRECTORY);
546 std::string dir = alt.empty() ? (::get_home_dir() + "/.CoolProp/SVDTables") : alt;
547 std::error_code ec;
548 std::filesystem::create_directories(dir, ec);
549 if (ec) {
550 // Non-fatal: tables can still be built and used in-memory.
551 // Surface the error to stderr so a user with an unwritable
552 // home dir (CI sandboxes, read-only mounts) sees *why* every
553 // SVDSBTL session pays the full build cost.
554 // NOLINTNEXTLINE(cppcoreguidelines-pro-type-vararg,cert-err33-c) — deliberate diagnostic-only stderr write (pre-existing)
555 std::fprintf(stderr, "SVDSurfaceSerializer: could not create cache dir %s: %s\n", dir.c_str(), ec.message().c_str());
556 }
557 // Normalize: callers naively concatenate dir + filename, so the
558 // returned path must end with a separator regardless of whether
559 // the user supplied a trailing slash in their override.
560 if (!dir.empty() && dir.back() != '/' && dir.back() != '\\') {
561 dir.push_back('/');
562 }
563 return dir;
564}
565
566std::string SVDSurfaceSerializer::default_cache_path(const std::string& fluid_name, const std::string& source_backend,
567 ::CoolProp::input_pairs input_pair, const std::string& opthash) {
568 // Defense-in-depth against path traversal. CoolProp fluid names
569 // come from the JSON catalog so this is unlikely to be hit in
570 // practice, but a caller passing an attacker-controlled string
571 // (e.g. through PropsSI("...", "SVDSBTL&HEOS::<weird name>"))
572 // would otherwise be able to read or write outside the cache dir.
573 auto unsafe = [](const std::string& s) {
574 return s.empty() || s.find('/') != std::string::npos || s.find('\\') != std::string::npos || s.find("..") != std::string::npos;
575 };
576 if (unsafe(fluid_name)) {
577 throw std::invalid_argument("SVDSurfaceSerializer::default_cache_path: invalid fluid_name (must be a bare component name)");
578 }
579 if (unsafe(source_backend)) {
580 throw std::invalid_argument("SVDSurfaceSerializer::default_cache_path: invalid source_backend");
581 }
582 // opthash is filename-restricted to [0-9a-z_] — covers the hex
583 // FNV-1a 64 output (lowercase) and the literal "no_opts" default
584 // sentinel for callers that don't carry an options blob. Uppercase
585 // and path-separator characters are rejected to keep the cache
586 // directory layout deterministic across filesystems with
587 // case-insensitive matching.
588 for (char c : opthash) {
589 const bool ok = (c >= '0' && c <= '9') || (c >= 'a' && c <= 'z') || c == '_';
590 if (!ok) {
591 throw std::invalid_argument("SVDSurfaceSerializer::default_cache_path: opthash must match [0-9a-z_]+");
592 }
593 }
594 if (opthash.empty()) {
595 throw std::invalid_argument("SVDSurfaceSerializer::default_cache_path: opthash must be non-empty");
596 }
597 // Format: <fluid>.<source>.<input_pair_name>.<opthash>.svd.bin.z
598 // so HEOS-built and REFPROP-built (and IF97-built) tables for the
599 // same fluid never collide on disk, two different option sets for
600 // the same (fluid, source, input_pair) get distinct files, and
601 // reordering the input_pairs enum in DataStructures.h can never
602 // silently misalign caches (the symbolic name is the key).
603 const std::string& pair_name = ::CoolProp::get_input_pair_short_desc(input_pair);
604 return default_cache_dir() + fluid_name + "." + source_backend + "." + pair_name + "." + opthash + ".svd.bin.z";
605}
606
607} // namespace sbtl
608} // namespace CoolProp