From b9bf8558f0cc105cfd54eaa64594f4531275bca6 Mon Sep 17 00:00:00 2001 From: Loic Gouarin Date: Thu, 29 Oct 2026 18:17:54 +0100 Subject: [PATCH] Fix out-of-bounds accesses on out-of-range axes Functions taking an axis used it to index shape, strides or slice containers without checking it. xt::argmax(a, 2) on a 2-D xtensor wrote past the std::array holding the result shape (stack-buffer-overflow under AddressSanitizer). diff, trapz, unwrap, cumsum, average, sort, partition, median, quantile, fft, expand_dims, flip, repeat, squeeze, concatenate, stack, the axis iterators and the blockwise reducers read or wrote out of bounds the same way. normalize_axis did no validation, and its container overloads only had an XTENSOR_ASSERT, which is off in release builds. It also took the axis as std::ptrdiff_t, so an unsigned std::size_t(-1) became the last axis instead of an error. normalize_axis now throws std::runtime_error when the axis is not in [-dim, dim). That is the exception type reducers, roll, split and squeeze already throw for a bad axis. It is a template over the axis type, and negative values count from the end only for signed types. The container overloads and forward_normalize run the same check. expand_dims and stack insert a new axis, so they validate against dim + 1. Functions that indexed with the raw axis now go through normalize_axis: expand_dims, split, flip, repeat, squeeze, concatenate, stack, axis_begin, axis_end, axis_slice_begin, axis_slice_end and the blockwise reducers. sort, partition and argpartition check the axis before their 1-D shortcut, which used to ignore it. roll and split drop their own bounds check in favor of the shared one. argmin and argmax validate the axis inside arg_func_impl, next to the code that builds the result shape. That code now uses a loop that skips the axis instead of two std::copy calls, which removes the GCC -Warray-bounds warnings on alt_shape. squeeze and the blockwise reducers also reject repeated axes, because they size the result shape from the number of axes. squeeze accepts negative axes. average throws when 1-D weights come with anything other than exactly one axis, instead of reading ax[0]. --- include/xtensor/core/xmath.hpp | 4 + include/xtensor/generators/xbuilder.hpp | 3 + include/xtensor/misc/xmanipulation.hpp | 66 ++++++----- include/xtensor/misc/xsort.hpp | 30 ++--- .../xtensor/reducers/xblockwise_reducer.hpp | 15 ++- include/xtensor/utils/xutils.hpp | 110 ++++++++++-------- include/xtensor/views/xaxis_iterator.hpp | 2 + .../xtensor/views/xaxis_slice_iterator.hpp | 2 + test/test_xaxis_iterator.cpp | 7 ++ test/test_xaxis_slice_iterator.cpp | 7 ++ test/test_xblockwise_reducer.cpp | 24 ++++ test/test_xbuilder.cpp | 18 +++ test/test_xfft.cpp | 8 ++ test/test_xmanipulation.cpp | 28 +++++ test/test_xmath.cpp | 19 +++ test/test_xsort.cpp | 26 +++++ test/test_xutils.cpp | 33 ++++++ 17 files changed, 309 insertions(+), 93 deletions(-) diff --git a/include/xtensor/core/xmath.hpp b/include/xtensor/core/xmath.hpp index 6b86c4b74..feb3f1f1a 100644 --- a/include/xtensor/core/xmath.hpp +++ b/include/xtensor/core/xmath.hpp @@ -1942,6 +1942,10 @@ namespace xt auto ax = normalize_axis(e, axes); if (weights.dimension() == 1) { + if (ax.size() != 1) + { + XTENSOR_THROW(std::runtime_error, "1-D weights need exactly one axis."); + } if (weights.size() != e.shape()[ax[0]]) { XTENSOR_THROW(std::runtime_error, "Weights need to have the same shape as expression at axes."); diff --git a/include/xtensor/generators/xbuilder.hpp b/include/xtensor/generators/xbuilder.hpp index 1026b21c7..5ea28e3e6 100644 --- a/include/xtensor/generators/xbuilder.hpp +++ b/include/xtensor/generators/xbuilder.hpp @@ -779,6 +779,7 @@ namespace xt shape_type new_shape = xtl::forward_sequence( std::get<0>(t).shape() ); + axis = normalize_axis(new_shape.size(), axis); auto check_shape = [&axis, &new_shape](auto& arr) { @@ -884,6 +885,8 @@ namespace xt { using shape_type = promote_shape_t::shape_type...>; using source_shape_type = decltype(std::get<0>(t).shape()); + // The stacking axis is a new one, it can be inserted up to and including dimension(). + axis = normalize_axis(std::get<0>(t).dimension() + 1, axis); auto new_shape = detail::add_axis( xtl::forward_sequence(std::get<0>(t).shape()), axis, diff --git a/include/xtensor/misc/xmanipulation.hpp b/include/xtensor/misc/xmanipulation.hpp index db4125e01..b97d7f178 100644 --- a/include/xtensor/misc/xmanipulation.hpp +++ b/include/xtensor/misc/xmanipulation.hpp @@ -547,10 +547,29 @@ namespace xt namespace detail { + // Each squeezed axis removes one dimension from the result, so the axes + // must be valid and distinct for the result shape to be filled exactly. + template + inline dynamic_shape normalize_squeeze_axes(std::size_t dim, const S& axis) + { + dynamic_shape res; + for (auto ax : axis) + { + const std::size_t nax = normalize_axis(dim, ax); + if (std::find(res.cbegin(), res.cend(), nax) != res.cend()) + { + XTENSOR_THROW(std::runtime_error, "Axis " + std::to_string(nax) + " is repeated in squeeze."); + } + res.push_back(nax); + } + return res; + } + template inline auto squeeze_impl(E&& e, S&& axis, check_policy::none) { - std::size_t new_dim = e.dimension() - axis.size(); + const auto axes = normalize_squeeze_axes(e.dimension(), axis); + std::size_t new_dim = e.dimension() - axes.size(); dynamic_shape new_shape(new_dim); dynamic_shape new_strides(new_dim); @@ -558,7 +577,7 @@ namespace xt for (std::size_t i = 0, ix = 0; i < e.dimension(); ++i) { - if (axis.cend() == std::find(axis.cbegin(), axis.cend(), i)) + if (axes.cend() == std::find(axes.cbegin(), axes.cend(), i)) { new_shape[ix] = e.shape()[i]; new_strides[ix++] = old_strides[i]; @@ -577,18 +596,15 @@ namespace xt template inline auto squeeze_impl(E&& e, S&& axis, check_policy::full) { - for (auto ix : axis) + auto axes = normalize_squeeze_axes(e.dimension(), axis); + for (auto ix : axes) { - if (static_cast(ix) > e.dimension()) - { - XTENSOR_THROW(std::runtime_error, "Axis argument to squeeze > dimension of expression"); - } - if (e.shape()[static_cast(ix)] != 1) + if (e.shape()[ix] != 1) { XTENSOR_THROW(std::runtime_error, "Trying to squeeze axis != 1"); } } - return squeeze_impl(std::forward(e), std::forward(axis), check_policy::none()); + return squeeze_impl(std::forward(e), std::move(axes), check_policy::none()); } } @@ -646,8 +662,10 @@ namespace xt template inline auto expand_dims(E&& e, std::size_t axis) { - xstrided_slice_vector sv(e.dimension() + 1, all()); - sv[axis] = newaxis(); + // The new axis can be inserted at any position up to and including dimension(). + const std::size_t new_dim = e.dimension() + 1; + xstrided_slice_vector sv(new_dim, all()); + sv[normalize_axis(new_dim, axis)] = newaxis(); return strided_view(std::forward(e), std::move(sv)); } @@ -734,8 +752,9 @@ namespace xt * * This splits an xexpression along the axis in `n` equal parts and * returns a vector of ``strided_view``. - * Calling split with axis > dimension of e or a `n` that does not result in - * an equal division of the xexpression will throw a runtime_error. + * Calling split with an axis that is not smaller than the dimension of e or + * a `n` that does not result in an equal division of the xexpression will + * throw a runtime_error. * * @ingroup xt_xmanipulation * @param e input xexpression @@ -745,11 +764,7 @@ namespace xt template inline auto split(E& e, std::size_t n, std::size_t axis) { - if (axis >= e.dimension()) - { - XTENSOR_THROW(std::runtime_error, "Split along axis > dimension."); - } - + axis = normalize_axis(e.dimension(), axis); std::size_t ax_sz = e.shape()[axis]; xstrided_slice_vector sv(e.dimension(), all()); std::size_t step = ax_sz / n; @@ -838,6 +853,7 @@ namespace xt { using shape_type = xindex_type_t::shape_type>; + axis = normalize_axis(e.dimension(), axis); shape_type shape; resize_container(shape, e.shape().size()); std::copy(e.shape().cbegin(), e.shape().cend(), shape.begin()); @@ -1067,17 +1083,13 @@ namespace xt const auto& shape = cpy.shape(); const auto dim = cpy.dimension(); - if (axis < -static_cast(dim) || axis >= static_cast(dim)) - { - XTENSOR_THROW(std::runtime_error, "axis is not within shape dimension."); - } + const std::size_t saxis = normalize_axis(dim, axis); if (cpy.size() == 0) { return cpy; } - std::size_t saxis = normalize_axis(dim, axis); const auto axis_dim = static_cast(shape[saxis]); while (shift < 0) @@ -1098,8 +1110,8 @@ namespace xt template inline auto make_xrepeat(E&& e, R&& r, typename std::decay_t::size_type axis) { - const auto casted_axis = static_cast::size_type>(axis); - if (r.size() != e.shape(casted_axis)) + axis = normalize_axis(e.dimension(), axis); + if (r.size() != e.shape(axis)) { XTENSOR_THROW(std::invalid_argument, "repeats must have the same size as the specified axis"); } @@ -1120,8 +1132,8 @@ namespace xt template inline auto repeat(E&& e, std::size_t repeats, std::size_t axis) { - const auto casted_axis = static_cast::size_type>(axis); - std::vector broadcasted_repeats(e.shape(casted_axis)); + axis = normalize_axis(e.dimension(), axis); + std::vector broadcasted_repeats(e.shape(axis)); std::fill(broadcasted_repeats.begin(), broadcasted_repeats.end(), repeats); return repeat(std::forward(e), std::move(broadcasted_repeats), axis); } diff --git a/include/xtensor/misc/xsort.hpp b/include/xtensor/misc/xsort.hpp index 196ec45cd..03bf0b05b 100644 --- a/include/xtensor/misc/xsort.hpp +++ b/include/xtensor/misc/xsort.hpp @@ -156,6 +156,7 @@ namespace xt template inline R map_axis(const E& e, std::ptrdiff_t axis, F&& lambda) { + const std::size_t ax = normalize_axis(e.dimension(), axis); if (e.dimension() == 1) { R res = e; @@ -163,7 +164,6 @@ namespace xt return res; } - const std::size_t ax = normalize_axis(e.dimension(), axis); if (ax == detail::leading_axis(e)) { R res = e; @@ -706,6 +706,7 @@ namespace xt using result_type = typename detail::argsort_result_type::type; const auto& de = e.derived_cast(); + const std::size_t ax = normalize_axis(de.dimension(), axis); if (de.dimension() == 1) { @@ -729,7 +730,6 @@ namespace xt ); }; - const std::size_t ax = normalize_axis(de.dimension(), axis); if (ax == detail::leading_axis(de)) { result_type res = result_type::from_shape(de.shape()); @@ -1157,13 +1157,16 @@ namespace xt }; template - inline typename argfunc_result_type::type arg_func_impl(const E& e, std::size_t axis, F&& cmp) + inline typename argfunc_result_type::type arg_func_impl(const E& e, std::ptrdiff_t ax, F&& cmp) { using eval_type = typename detail::sort_eval_type::type; using value_type = typename E::value_type; using result_type = typename argfunc_result_type::type; using result_shape_type = typename result_type::shape_type; + // Checked here because the result shape below relies on axis < dimension(). + const std::size_t axis = normalize_axis(e.dimension(), ax); + if (e.dimension() == 1) { auto begin = e.template begin(); @@ -1184,13 +1187,14 @@ namespace xt result_shape_type alt_shape; xt::resize_container(alt_shape, e.dimension() - 1); - // Excluding copy, copy all of shape except for axis - std::copy(e.shape().cbegin(), e.shape().cbegin() + std::ptrdiff_t(axis), alt_shape.begin()); - std::copy( - e.shape().cbegin() + std::ptrdiff_t(axis) + 1, - e.shape().cend(), - alt_shape.begin() + std::ptrdiff_t(axis) - ); + // Copy all of shape except for axis + for (std::size_t i = 0, j = 0; i < e.dimension(); ++i) + { + if (i != axis) + { + alt_shape[j++] = e.shape()[i]; + } + } result_type result = result_type::from_shape(std::move(alt_shape)); auto result_iter = result.template begin(); @@ -1260,8 +1264,7 @@ namespace xt { using value_type = typename E::value_type; auto&& ed = eval(e.derived_cast()); - std::size_t ax = normalize_axis(ed.dimension(), axis); - return detail::arg_func_impl(ed, ax, std::less()); + return detail::arg_func_impl(ed, axis, std::less()); } template @@ -1291,8 +1294,7 @@ namespace xt { using value_type = typename E::value_type; auto&& ed = eval(e.derived_cast()); - std::size_t ax = normalize_axis(ed.dimension(), axis); - return detail::arg_func_impl(ed, ax, std::greater()); + return detail::arg_func_impl(ed, axis, std::greater()); } /** diff --git a/include/xtensor/reducers/xblockwise_reducer.hpp b/include/xtensor/reducers/xblockwise_reducer.hpp index 902b41f08..6a21bb93a 100644 --- a/include/xtensor/reducers/xblockwise_reducer.hpp +++ b/include/xtensor/reducers/xblockwise_reducer.hpp @@ -270,12 +270,23 @@ namespace xt { using functor_type = std::decay_t; using closure_type = xtl::const_closure_type_t; - using axes_type = std::decay_t; + + // The result shape is computed from the axes before any block is reduced, + // so they have to be valid and distinct here already. + decltype(auto) normalized_axes = normalize_axis(e, std::forward(axes)); + using axes_type = std::decay_t; + for (auto it = normalized_axes.begin(); it != normalized_axes.end(); ++it) + { + if (std::find(normalized_axes.begin(), it, *it) != it) + { + XTENSOR_THROW(std::runtime_error, "Reducing axes should not contain duplicates."); + } + } return xblockwise_reducer( std::forward(e), std::forward(chunk_shape), - std::forward(axes), + std::forward(normalized_axes), std::forward(raw_options), std::forward(functor) ); diff --git a/include/xtensor/utils/xutils.hpp b/include/xtensor/utils/xutils.hpp index ed4a04f48..e700d3ed7 100644 --- a/include/xtensor/utils/xutils.hpp +++ b/include/xtensor/utils/xutils.hpp @@ -12,22 +12,27 @@ #include #include +#include #include #include #include #include #include +#include +#include #include #include #include #include +#include #include #include #include #include #include "../core/xtensor_config.hpp" +#include "xexception.hpp" namespace xt { @@ -74,7 +79,8 @@ namespace xt template using rebind_container_t = typename rebind_container::type; - std::size_t normalize_axis(std::size_t dim, std::ptrdiff_t axis); + template + std::size_t normalize_axis(std::size_t dim, A axis); // gcc 4.9 is affected by C++14 defect CGW 1558 // see http://open-std.org/JTC1/SC22/WG21/docs/cwg_defects.html#1558 @@ -340,11 +346,57 @@ namespace xt * normalize_axis implementation * *********************************/ - // scalar normalize axis - inline std::size_t normalize_axis(std::size_t dim, std::ptrdiff_t axis) + namespace detail { - return axis < 0 ? static_cast(static_cast(dim) + axis) - : static_cast(axis); + // Axes come from the user and are used to index shape and strides + // containers, so an out-of-range axis reads or writes past their end. + // Functions taking an axis validate it here, through normalize_axis, + // and the check stays on in release builds for that reason. + // Signed axes may be negative and count from the end, as in NumPy. + template + inline void check_axis(std::size_t dim, A axis) + { + if (xtl::cmp_greater_equal(axis, dim) || xtl::cmp_less(axis, -static_cast(dim))) + { + XTENSOR_THROW( + std::runtime_error, + "Axis " + std::to_string(axis) + " is out of bounds for an expression of dimension " + + std::to_string(dim) + "." + ); + } + } + + template + inline void check_axes(const E& expr, const C& axes) + { + const std::size_t dim = expr.dimension(); + for (const auto& ax : axes) + { + check_axis(dim, ax); + } + } + } + + /** + * Returns the position of @p axis in an expression of dimension @p dim. + * + * Negative values of a signed @p axis count from the end. Throws + * std::runtime_error when @p axis is not in [-dim, dim). + * To validate the position of a new axis (as in stack or expand_dims), + * pass ``dim + 1``. + */ + template + inline std::size_t normalize_axis(std::size_t dim, A axis) + { + detail::check_axis(dim, axis); + if constexpr (std::is_signed_v) + { + if (axis < 0) + { + return static_cast(static_cast(dim) + axis); + } + } + return static_cast(axis); } template @@ -361,15 +413,6 @@ namespace xt res[i] = normalize_axis(expr.dimension(), axes[i]); } - XTENSOR_ASSERT(std::all_of( - res.begin(), - res.end(), - [&expr](auto ax_el) - { - return ax_el < expr.dimension(); - } - )); - return res; } @@ -379,15 +422,7 @@ namespace xt C&&> normalize_axis(E& expr, C&& axes) { - static_cast(expr); - XTENSOR_ASSERT(std::all_of( - axes.begin(), - axes.end(), - [&expr](auto ax_el) - { - return ax_el < expr.dimension(); - } - )); + detail::check_axes(expr, axes); return std::forward(axes); } @@ -408,15 +443,6 @@ namespace xt } ); - XTENSOR_ASSERT(std::all_of( - res.begin(), - res.end(), - [&expr](auto ax_el) - { - return ax_el < expr.dimension(); - } - )); - return res; } @@ -425,19 +451,11 @@ namespace xt !xtl::is_signed>::value && !std::is_same>::value, R> { - static_cast(expr); + detail::check_axes(expr, axes); R res; xt::resize_container(res, std::size(axes)); std::copy(std::begin(axes), std::end(axes), std::begin(res)); - XTENSOR_ASSERT(std::all_of( - res.begin(), - res.end(), - [&expr](auto ax_el) - { - return ax_el < expr.dimension(); - } - )); return res; } @@ -446,15 +464,7 @@ namespace xt !xtl::is_signed>::value && std::is_same>::value, R&&> { - static_cast(expr); - XTENSOR_ASSERT(std::all_of( - std::begin(axes), - std::end(axes), - [&expr](auto ax_el) - { - return ax_el < expr.dimension(); - } - )); + detail::check_axes(expr, axes); return std::move(axes); } diff --git a/include/xtensor/views/xaxis_iterator.hpp b/include/xtensor/views/xaxis_iterator.hpp index 2b54748d4..badf01696 100644 --- a/include/xtensor/views/xaxis_iterator.hpp +++ b/include/xtensor/views/xaxis_iterator.hpp @@ -296,6 +296,7 @@ namespace xt inline auto axis_begin(E&& e, typename std::decay_t::size_type axis) { using return_type = xaxis_iterator>; + axis = normalize_axis(e.dimension(), axis); return return_type(std::forward(e), axis); } @@ -332,6 +333,7 @@ namespace xt { using size_type = typename std::decay_t::size_type; using return_type = xaxis_iterator>; + axis = normalize_axis(e.dimension(), axis); return return_type( std::forward(e), axis, diff --git a/include/xtensor/views/xaxis_slice_iterator.hpp b/include/xtensor/views/xaxis_slice_iterator.hpp index 6ee904df9..62d5abf50 100644 --- a/include/xtensor/views/xaxis_slice_iterator.hpp +++ b/include/xtensor/views/xaxis_slice_iterator.hpp @@ -308,6 +308,7 @@ namespace xt inline auto axis_slice_begin(E&& e, typename std::decay_t::size_type axis) { using return_type = xaxis_slice_iterator>; + axis = normalize_axis(e.dimension(), axis); return return_type(std::forward(e), axis, 0, e.data_offset()); } @@ -342,6 +343,7 @@ namespace xt inline auto axis_slice_end(E&& e, typename std::decay_t::size_type axis) { using return_type = xaxis_slice_iterator>; + axis = normalize_axis(e.dimension(), axis); auto index_sum = std::accumulate( e.shape().begin(), e.shape().begin() + axis, diff --git a/test/test_xaxis_iterator.cpp b/test/test_xaxis_iterator.cpp index 0f52d2df7..3123565c0 100644 --- a/test/test_xaxis_iterator.cpp +++ b/test/test_xaxis_iterator.cpp @@ -209,4 +209,11 @@ namespace xt EXPECT_EQ(a(1, 1, 3), (*iter)(1, 1)); EXPECT_EQ(a(1, 2, 3), (*iter)(1, 2)); } + + TEST(xaxis_iterator, invalid_axis) + { + xarray a = get_test_array(); + XT_EXPECT_THROW(axis_begin(a, 3), std::runtime_error); + XT_EXPECT_THROW(axis_end(a, 3), std::runtime_error); + } } diff --git a/test/test_xaxis_slice_iterator.cpp b/test/test_xaxis_slice_iterator.cpp index fa0888e2d..8e5d904ae 100644 --- a/test/test_xaxis_slice_iterator.cpp +++ b/test/test_xaxis_slice_iterator.cpp @@ -318,4 +318,11 @@ namespace xt EXPECT_EQ(a(1, 2, 2), (*iter)(2)); EXPECT_EQ(a(1, 2, 3), (*iter)(3)); } + + TEST(xaxis_slice_iterator, invalid_axis) + { + xarray a = get_slice_test_array(); + XT_EXPECT_THROW(axis_slice_begin(a, 3), std::runtime_error); + XT_EXPECT_THROW(axis_slice_end(a, 3), std::runtime_error); + } } diff --git a/test/test_xblockwise_reducer.cpp b/test/test_xblockwise_reducer.cpp index d888ec396..bb3904cbc 100644 --- a/test/test_xblockwise_reducer.cpp +++ b/test/test_xblockwise_reducer.cpp @@ -314,6 +314,30 @@ namespace xt } } } + + TEST_CASE("invalid_axes") + { + dynamic_shape shape({6, 4, 2}); + dynamic_shape chunk_shape({3, 2, 2}); + xarray input_exp(shape, 1); + + XT_EXPECT_THROW(xt::blockwise::sum(input_exp, chunk_shape, 3), std::runtime_error); + XT_EXPECT_THROW(xt::blockwise::sum(input_exp, chunk_shape, -4), std::runtime_error); + XT_EXPECT_THROW( + xt::blockwise::sum(input_exp, chunk_shape, dynamic_shape{0, 3}), + std::runtime_error + ); + XT_EXPECT_THROW( + xt::blockwise::sum(input_exp, chunk_shape, dynamic_shape{1, 1}), + std::runtime_error + ); + + // Negative axes count from the end, as for the non-blockwise reducers. + auto reducer = xt::blockwise::sum(input_exp, chunk_shape, -1); + auto res = xarray::from_shape(reducer.shape()); + reducer.assign_to(res); + EXPECT_EQ(res, xt::eval(xt::sum(input_exp, {2}))); + } } } diff --git a/test/test_xbuilder.cpp b/test/test_xbuilder.cpp index d35847a8e..32c3af5df 100644 --- a/test/test_xbuilder.cpp +++ b/test/test_xbuilder.cpp @@ -706,4 +706,22 @@ namespace xt b = std::is_same>::value; EXPECT_TRUE(b); } + + TEST(xbuilder, invalid_axis) + { + xarray a = {{1., 2.}, {3., 4.}}; + xarray b = {{5., 6.}, {7., 8.}}; + + XT_EXPECT_THROW(concatenate(xtuple(a, b), 2), std::runtime_error); + XT_EXPECT_THROW(stack(xtuple(a, b), 3), std::runtime_error); + + xtensor ta = a; + xtensor tb = b; + XT_EXPECT_THROW(stack(xtuple(ta, tb), 3), std::runtime_error); + + // Stacking inserts a new axis, so dimension() itself is valid. + auto s = stack(xtuple(a, b), 2); + EXPECT_EQ(s.shape(), (dynamic_shape{2, 2, 2})); + EXPECT_EQ(s(1, 0, 1), 7.); + } } diff --git a/test/test_xfft.cpp b/test/test_xfft.cpp index a1b050ce4..033e10c60 100644 --- a/test/test_xfft.cpp +++ b/test/test_xfft.cpp @@ -92,4 +92,12 @@ namespace xt REQUIRE(expected(i) == doctest::Approx(abs(i)).epsilon(.0001)); } } + + TEST(xfft, invalid_axis) + { + xt::xarray y = {1.f, 2.f, 3.f, 4.f}; + XT_EXPECT_THROW(xt::fft::fft(y, 1), std::runtime_error); + XT_EXPECT_THROW(xt::fft::ifft(y, -2), std::runtime_error); + XT_EXPECT_THROW(xt::fft::convolve(y, y, 1), std::runtime_error); + } } diff --git a/test/test_xmanipulation.cpp b/test/test_xmanipulation.cpp index 32f5d4370..94f46b735 100644 --- a/test/test_xmanipulation.cpp +++ b/test/test_xmanipulation.cpp @@ -637,4 +637,32 @@ namespace xt TEST(xmanipulation, zzzzzz) { } + + TEST(xmanipulation, invalid_axis) + { + xarray a = {{1., 2., 3.}, {4., 5., 6.}}; + + XT_EXPECT_THROW(expand_dims(a, 3), std::runtime_error); + EXPECT_EQ(expand_dims(a, 2).shape(), (dynamic_shape{2, 3, 1})); + + XT_EXPECT_THROW(flip(a, 2), std::runtime_error); + XT_EXPECT_THROW(split(a, 1, 2), std::runtime_error); + XT_EXPECT_THROW(repeat(a, 2, 2), std::runtime_error); + XT_EXPECT_THROW(repeat(a, std::vector{1, 1}, 2), std::runtime_error); + } + + TEST(xmanipulation, squeeze_invalid_axis) + { + auto b = xt::xarray::from_shape({3, 1, 2}); + using ds = xt::dynamic_shape; + + XT_EXPECT_THROW(squeeze(b, 3), std::runtime_error); + XT_EXPECT_THROW(squeeze(b, 3, check_policy::full()), std::runtime_error); + XT_EXPECT_THROW(squeeze(b, {1, 1}), std::runtime_error); + XT_EXPECT_THROW(squeeze(b, {1, -2}), std::runtime_error); + XT_EXPECT_THROW(squeeze(b, {-4}), std::runtime_error); + + EXPECT_EQ(squeeze(b, {-2}).shape(), ds({3, 2})); + EXPECT_EQ(squeeze(b, {-2}, check_policy::full()).shape(), ds({3, 2})); + } } diff --git a/test/test_xmath.cpp b/test/test_xmath.cpp index 57771859b..7aab05f2b 100644 --- a/test/test_xmath.cpp +++ b/test/test_xmath.cpp @@ -1025,4 +1025,23 @@ namespace xt xt::xtensor expected_sums = {1.0, 1.0}; EXPECT_TRUE(xt::allclose(row_sums, expected_sums)); } + + TEST(xmath, invalid_axis) + { + xarray a = {{1., 2., 3.}, {4., 5., 6.}}; + xarray x = {0., 1., 2.}; + xarray w = {1., 2.}; + + XT_EXPECT_THROW(diff(a, 1, 2), std::runtime_error); + XT_EXPECT_THROW(trapz(a, 1.0, 2), std::runtime_error); + XT_EXPECT_THROW(trapz(a, x, -3), std::runtime_error); + XT_EXPECT_THROW(unwrap(a, xnone(), 2), std::runtime_error); + XT_EXPECT_THROW(cumsum(a, 2), std::runtime_error); + XT_EXPECT_THROW(cumprod(a, -3), std::runtime_error); + + XT_EXPECT_THROW(average(a, w, {2}), std::runtime_error); + XT_EXPECT_THROW(average(a, w, {std::size_t(2)}), std::runtime_error); + XT_EXPECT_THROW(average(a, w, std::vector{2}), std::runtime_error); + XT_EXPECT_THROW(average(a, w, {0, 1}), std::runtime_error); + } } diff --git a/test/test_xsort.cpp b/test/test_xsort.cpp index bbe928d4d..dee8dd737 100644 --- a/test/test_xsort.cpp +++ b/test/test_xsort.cpp @@ -564,4 +564,30 @@ namespace xt EXPECT_EQ(ma0, ma0_exp); EXPECT_EQ(ma1, ma1_exp); } + + TEST(xsort, invalid_axis) + { + xtensor t = {{1., 2.}, {3., 0.}}; + xarray a = t; + xarray a1 = {3., 1., 2.}; + + // The fixed-size result shape of argmin / argmax used to overflow. + XT_EXPECT_THROW(argmax(t, 2), std::runtime_error); + XT_EXPECT_THROW(argmin(t, -3), std::runtime_error); + XT_EXPECT_THROW(argmax(a, 2), std::runtime_error); + + XT_EXPECT_THROW(sort(a, 2), std::runtime_error); + XT_EXPECT_THROW(argsort(a, -3), std::runtime_error); + XT_EXPECT_THROW(partition(a, 0, 2), std::runtime_error); + XT_EXPECT_THROW(argpartition(a, 0, 2), std::runtime_error); + XT_EXPECT_THROW(median(a, 2), std::runtime_error); + XT_EXPECT_THROW(quantile(a, {0.5}, 2), std::runtime_error); + + // 1-D inputs take a shortcut that used to ignore the axis. + XT_EXPECT_THROW(sort(a1, 1), std::runtime_error); + XT_EXPECT_THROW(partition(a1, 0, 1), std::runtime_error); + XT_EXPECT_THROW(argpartition(a1, 0, 1), std::runtime_error); + XT_EXPECT_THROW(argsort(a1, 1), std::runtime_error); + EXPECT_EQ(sort(a1, -1), (xarray{1., 2., 3.})); + } } diff --git a/test/test_xutils.cpp b/test/test_xutils.cpp index 6107c4bce..938404589 100644 --- a/test/test_xutils.cpp +++ b/test/test_xutils.cpp @@ -237,4 +237,37 @@ namespace xt sdim = static_dimension, 1, 4>>::value; EXPECT_EQ(sdim, 3); } + + TEST(utils, normalize_axis) + { + EXPECT_EQ(normalize_axis(3, 0), 0u); + EXPECT_EQ(normalize_axis(3, 2), 2u); + EXPECT_EQ(normalize_axis(3, -1), 2u); + EXPECT_EQ(normalize_axis(3, -3), 0u); + EXPECT_EQ(normalize_axis(3, std::size_t(2)), 2u); + + XT_EXPECT_THROW(normalize_axis(3, 3), std::runtime_error); + XT_EXPECT_THROW(normalize_axis(3, -4), std::runtime_error); + XT_EXPECT_THROW(normalize_axis(0, 0), std::runtime_error); + XT_EXPECT_THROW(normalize_axis(0, -1), std::runtime_error); + XT_EXPECT_THROW(normalize_axis(3, std::size_t(3)), std::runtime_error); + // An unsigned axis never counts from the end. + XT_EXPECT_THROW(normalize_axis(3, std::size_t(-1)), std::runtime_error); + } + + TEST(utils, normalize_axes) + { + xt::xarray a = {{1., 2.}, {3., 4.}}; + + auto signed_axes = normalize_axis(a, std::vector{0, -1}); + EXPECT_EQ(signed_axes, (std::vector{0, 1})); + XT_EXPECT_THROW(normalize_axis(a, std::vector{0, 2}), std::runtime_error); + XT_EXPECT_THROW(normalize_axis(a, std::vector{-3}), std::runtime_error); + XT_EXPECT_THROW(normalize_axis(a, std::vector{0, 2}), std::runtime_error); + + using array_type = std::array; + XT_EXPECT_THROW(forward_normalize(a, std::array{0, 2}), std::runtime_error); + XT_EXPECT_THROW(forward_normalize(a, std::vector{0, 2}), std::runtime_error); + XT_EXPECT_THROW(forward_normalize(a, array_type{0, 2}), std::runtime_error); + } }