Skip to content

Commit deafa94

Browse files
committed
fix(interpolate): never abort() on non-increasing x in Interpolator1D
gsl_spline_init() requires strictly increasing x. When it isn't (e.g. duplicate colour-gradient stops, presets, the colorize reverse mapping, or float rounding), GSL invokes its default error handler which calls abort() and takes down the whole host process (SIGABRT, "gsl: interp.c: x values must be strictly increasing"). Make Interpolator1D robust instead: - Merge coincident x (keeping the first y) so valid-but-sloppy data still interpolates and renders rather than crashing. - Throw the catchable std::invalid_argument it already uses for other preconditions when x is decreasing or fewer than two distinct points remain after merging. - Disable GSL's abort-on-error handler and check gsl_spline_init()'s return code, converting any residual GSL error into an exception so the library can never abort() the host. Add gtest coverage for coincident, all-coincident, decreasing, and mismatched/too-few inputs, and document the new contract on the constructor.
1 parent 9577cfa commit deafa94

3 files changed

Lines changed: 119 additions & 5 deletions

File tree

HighMap/include/highmap/interpolate/interpolate1d.hpp

Lines changed: 11 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -71,13 +71,20 @@ class Interpolator1D
7171
* This constructor initializes the interpolation object with the provided x
7272
* and y data points and the specified interpolation method.
7373
*
74-
* @param x A vector of x coordinates (independent variable).
74+
* Coincident x coordinates are merged (keeping the first y) so that
75+
* valid-but-sloppy data still interpolates instead of crashing; GSL's
76+
* abort-on-error handler is disabled so a precondition violation throws a
77+
* catchable exception rather than terminating the process.
78+
*
79+
* @param x A vector of x coordinates (independent variable). Must be
80+
* sorted in increasing order.
7581
* @param y A vector of y coordinates (dependent variable).
7682
* @param method The interpolation method to use (default is linear).
7783
*
78-
* @throws std::invalid_argumentifxandyhavedifferentsizesoriftherearefewerthan
79-
* two points. An exception is also thrown if the method
80-
* requires monotonic data and the provided data is not monotonic.
84+
* @throws std::invalid_argument if x and y have different sizes, if there
85+
* are fewer than two points with distinct x values, if x is
86+
* not sorted in increasing order, or if the method requires
87+
* monotonic data and the provided data is not monotonic.
8188
*/
8289
Interpolator1D(const std::vector<float> &x,
8390
const std::vector<float> &y,

HighMap/src/interpolate/interpolate1d.cpp

Lines changed: 53 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77
#include <stdexcept>
88
#include <vector>
99

10+
#include <gsl/gsl_errno.h>
1011
#include <gsl/gsl_interp.h>
1112
#include <gsl/gsl_spline.h>
1213

@@ -41,6 +42,40 @@ Interpolator1D::Interpolator1D(const std::vector<float> &x,
4142
this->x_data = std::vector<double>(x.begin(), x.end());
4243
this->y_data = std::vector<double>(y.begin(), y.end());
4344

45+
// GSL requires strictly increasing x: gsl_spline_init() with coincident or
46+
// out-of-order x invokes GSL's default error handler, which abort()s the
47+
// whole process (SIGABRT) instead of letting the caller recover. Merge
48+
// coincident x (keeping the first y) so valid-but-sloppy data - duplicate
49+
// colour-gradient stops, presets, float rounding - still renders, and throw
50+
// a catchable exception only when the data cannot be repaired.
51+
{
52+
std::vector<double> xs, ys;
53+
xs.reserve(this->x_data.size());
54+
ys.reserve(this->y_data.size());
55+
56+
for (size_t i = 0; i < this->x_data.size(); ++i)
57+
{
58+
if (!xs.empty() && this->x_data[i] < xs.back())
59+
throw std::invalid_argument(
60+
"x values must be sorted in increasing order.");
61+
62+
if (!xs.empty() && this->x_data[i] == xs.back())
63+
continue; // drop coincident point, keep the first y
64+
65+
xs.push_back(this->x_data[i]);
66+
ys.push_back(this->y_data[i]);
67+
}
68+
69+
this->x_data = std::move(xs);
70+
this->y_data = std::move(ys);
71+
}
72+
73+
if (this->x_data.size() < 2)
74+
{
75+
throw std::invalid_argument(
76+
"at least two points with distinct x values are required.");
77+
}
78+
4479
const size_t size = this->x_data.size();
4580

4681
// store min and max values after double conversion to clamp input
@@ -86,7 +121,24 @@ Interpolator1D::Interpolator1D(const std::vector<float> &x,
86121
default: throw std::invalid_argument("Unsupported interpolation method.");
87122
}
88123

89-
gsl_spline_init(this->interp, this->x_data.data(), this->y_data.data(), size);
124+
// Belt-and-braces: never let GSL abort() the host process. Disable the
125+
// default (abort-on-error) handler process-wide and turn any residual GSL
126+
// error into a catchable exception instead of a SIGABRT.
127+
gsl_set_error_handler_off();
128+
129+
const int status = gsl_spline_init(this->interp,
130+
this->x_data.data(),
131+
this->y_data.data(),
132+
size);
133+
134+
if (status != GSL_SUCCESS)
135+
{
136+
gsl_spline_free(this->interp);
137+
gsl_interp_accel_free(this->accel_);
138+
throw std::invalid_argument(
139+
std::string("GSL spline initialization failed: ") +
140+
gsl_strerror(status));
141+
}
90142
}
91143

92144
Interpolator1D::~Interpolator1D()

tests/src/test_interpolate1d.cpp

Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
1+
#include "highmap.hpp"
2+
3+
#include <gtest/gtest.h>
4+
5+
using namespace hmap;
6+
7+
// Coincident x used to reach gsl_spline_init() and abort() the whole process
8+
// (gsl: interp.c: "x values must be strictly increasing"). They must now be
9+
// merged so the interpolator still builds and evaluates.
10+
TEST(Interpolator1D, CoincidentXDoesNotCrash)
11+
{
12+
std::vector<float> x = {0.f, 0.f, 0.5f, 1.f, 1.f};
13+
std::vector<float> y = {0.f, 0.f, 0.5f, 1.f, 1.f};
14+
15+
EXPECT_NO_THROW({
16+
Interpolator1D itp(x, y, InterpolationMethod1D::LINEAR);
17+
EXPECT_NEAR(itp(0.f), 0.f, 1e-5f);
18+
EXPECT_NEAR(itp(0.5f), 0.5f, 1e-5f);
19+
EXPECT_NEAR(itp(1.f), 1.f, 1e-5f);
20+
});
21+
}
22+
23+
// After merging coincident x there must still be at least two distinct points.
24+
TEST(Interpolator1D, AllCoincidentXThrows)
25+
{
26+
std::vector<float> x = {0.5f, 0.5f, 0.5f};
27+
std::vector<float> y = {0.f, 1.f, 2.f};
28+
29+
EXPECT_THROW(Interpolator1D(x, y, InterpolationMethod1D::LINEAR),
30+
std::invalid_argument);
31+
}
32+
33+
// Out-of-order (decreasing) x is unrepairable and must throw, not abort.
34+
TEST(Interpolator1D, DecreasingXThrows)
35+
{
36+
std::vector<float> x = {0.f, 1.f, 0.5f};
37+
std::vector<float> y = {0.f, 1.f, 0.5f};
38+
39+
EXPECT_THROW(Interpolator1D(x, y, InterpolationMethod1D::LINEAR),
40+
std::invalid_argument);
41+
}
42+
43+
// Existing preconditions remain enforced.
44+
TEST(Interpolator1D, MismatchedOrTooFewPointsThrows)
45+
{
46+
EXPECT_THROW(Interpolator1D(std::vector<float>{0.f, 1.f},
47+
std::vector<float>{0.f},
48+
InterpolationMethod1D::LINEAR),
49+
std::invalid_argument);
50+
51+
EXPECT_THROW(Interpolator1D(std::vector<float>{0.f},
52+
std::vector<float>{0.f},
53+
InterpolationMethod1D::LINEAR),
54+
std::invalid_argument);
55+
}

0 commit comments

Comments
 (0)