diff --git a/code/include/gauss_bonnet.hpp b/code/include/gauss_bonnet.hpp index 8ac316f..32b4e0a 100644 --- a/code/include/gauss_bonnet.hpp +++ b/code/include/gauss_bonnet.hpp @@ -44,7 +44,7 @@ // double gauss_bonnet_rhs(mesh) — 2π · χ(M) // double gauss_bonnet_deficit(mesh, maps) — lhs − rhs (0 = satisfied) // void check_gauss_bonnet(mesh, maps [, tol]) — throws if violated -// void enforce_gauss_bonnet(mesh, maps) — shifts θ_v by uniform Δ +// double enforce_gauss_bonnet(mesh, maps) — shifts θ_v by uniform Δ; returns |deficit| // (HyperIdealMaps overloads are deleted — see box above) #include "conformal_mesh.hpp" @@ -162,11 +162,16 @@ inline void check_gauss_bonnet(const ConformalMesh& mesh, // After this call, check_gauss_bonnet() will not throw (up to floating-point). // Modifies ALL vertices' θ_v (no v_idx filtering) — the shift is a property // of the target angles, independent of which vertices are free DOFs. +// +// H3 (test-coverage audit, 2026-06-01): both overloads now return the total +// absolute correction applied: |Σ(2π−Θ_v) − 2π·χ|. A large value signals +// that the input angles were far from satisfying Gauss–Bonnet. /// Distribute the Gauss-Bonnet deficit uniformly across all `Θ_v`: /// add `δ = (lhs − rhs) / V` to every entry so that the identity holds /// exactly afterwards. Overload for a raw property map. -inline void enforce_gauss_bonnet( +/// Returns `|lhs − rhs|` (total absolute correction applied). +inline double enforce_gauss_bonnet( ConformalMesh& mesh, ConformalMesh::Property_map& theta) { @@ -177,15 +182,17 @@ inline void enforce_gauss_bonnet( double delta = (lhs - rhs) / static_cast(mesh.number_of_vertices()); for (auto v : mesh.vertices()) theta[v] += delta; + return std::abs(lhs - rhs); } /// Distribute the Gauss-Bonnet deficit uniformly across `maps.theta_v`. /// Supported for EuclideanMaps and SphericalMaps only. /// HyperIdealMaps overload is deleted — see header comment for why. +/// Returns `|lhs − rhs|` (total absolute correction applied; see raw-map overload). template -inline void enforce_gauss_bonnet(ConformalMesh& mesh, Maps& maps) +inline double enforce_gauss_bonnet(ConformalMesh& mesh, Maps& maps) { - enforce_gauss_bonnet(mesh, maps.theta_v); + return enforce_gauss_bonnet(mesh, maps.theta_v); } // enforce_gauss_bonnet for HyperIdealMaps is intentionally DELETED. diff --git a/code/include/serialization.hpp b/code/include/serialization.hpp index 4265deb..c5b5988 100644 --- a/code/include/serialization.hpp +++ b/code/include/serialization.hpp @@ -264,6 +264,27 @@ inline void save_result_xml( /// Load a DOF vector from an XML result file written by /// `save_result_xml`. If `res`, `geom`, `layout2d` are non-null they /// are filled as well. +/// +/// V5 (input-validation audit, 2026-06-01): this reader implements a +/// **strict internal-only XML subset** — not a general XML parser. It +/// expects the exact one-element-per-line layout written by +/// `save_result_xml`. Files that are semantically equivalent XML but +/// formatted differently (attributes split across lines, extra +/// whitespace, XML declaration on its own line, etc.) are explicitly +/// *rejected* with `std::runtime_error` rather than silently mis-read +/// into zeros. Interoperability with other XML producers is out of +/// scope; use the JSON format for that. +/// +/// Strict-subset requirements that are validated: +/// 1. A line containing `` character (tag +/// open) on the same line. +/// 4. The ` load_result_xml( const std::string& path, NewtonResult* res = nullptr, @@ -275,13 +296,25 @@ inline std::vector load_result_xml( std::vector x; std::string line; + bool found_root = false; while (std::getline(ifs, line)) { - // Root element + // Root element — V5: geometry attribute must be on the same line. if (line.find(" attribute not found on its" + " opening line. Only the format written by save_result_xml is" + " supported — reformatted XML is rejected to prevent silent" + " misreads. Use the JSON format for interoperability."); + if (geom) *geom = g; } - // Solver metadata + // Solver metadata — V5: required attributes must be on the same line. else if (line.find("converged = (detail_xml::xml_get_attr(line, "converged") == "true"); @@ -303,10 +336,16 @@ inline std::vector load_result_xml( } } } - // DOF vector + // DOF vector — V5: the '>' tag-open must be on the same line. else if (line.find("0 1 2... + // V5: require the tag to be closed ('>') on the same line so the + // content-extraction below works correctly. auto open_end = line.find('>'); + if (open_end == std::string::npos) + throw std::runtime_error( + "conformallab: XML strict-subset violation in " + path + + ": opening '>' not on same line as tag." + " Only the format written by save_result_xml is supported."); auto close = line.find(""); std::string text; if (close != std::string::npos) { @@ -330,7 +369,46 @@ inline std::vector load_result_xml( layout2d->success = true; } } + + // V5: if the file was non-empty but never produced a root + // element, the file is likely reformatted or not a ConformalResult XML at all. + if (!found_root) { + // Distinguish "empty file" (ifs.peek() == EOF at open) from wrong format. + // We re-open to check file size — if it had content but no root element + // was found on a single line, it was reformatted. + std::ifstream probe(path, std::ios::ate); + if (probe && probe.tellg() > 0) + throw std::runtime_error( + "conformallab: XML strict-subset violation in " + path + + ": root element not found on its own line." + " Only the format written by save_result_xml is supported."); + } + return x; } +/// Validate that a loaded DOF vector has the expected number of DOFs. +/// +/// V6 (input-validation audit, 2026-06-01): a result file from a *different* +/// mesh loads happily; the size mismatch only surfaces later (out-of-bounds +/// or wrong-answer) when `x` is indexed against the new mesh. This helper +/// provides a clear early check at the call-site where the loaded vector is +/// paired with the mesh. +/// +/// Throws `std::runtime_error` if `x.size() != expected_dofs`. +inline void check_dof_vector_size( + const std::vector& x, + int expected_dofs, + const std::string& context = "") +{ + if (static_cast(x.size()) != expected_dofs) { + std::ostringstream msg; + msg << "conformallab: DOF-vector size mismatch"; + if (!context.empty()) msg << " in " << context; + msg << ": loaded " << x.size() + << " values but mesh has " << expected_dofs << " DOFs."; + throw std::runtime_error(msg.str()); + } +} + } // namespace conformallab