[libc++] Treat negative counts in copy_n & friends as no-ops (#207086)
The standard specifies copy_n, fill_n and generate_n (both std and ranges) to be no-ops when passed a negative count. This patch fixes libc++ to abide by that requirement, with tests. It's worth noting that std::for_each_n makes n > 0 a precondition instead: we add the check and tests for it. Fixes #193613
This commit is contained in:
parent
0a933be62e
commit
f4120c4a1d
13 changed files with 239 additions and 24 deletions
|
|
@ -33,6 +33,8 @@ template <class _AlgPolicy,
|
|||
__enable_if_t<__has_random_access_iterator_category<_InIter>::value, int> = 0>
|
||||
_LIBCPP_HIDE_FROM_ABI _LIBCPP_CONSTEXPR_SINCE_CXX20 __in_out_result<_InIter, _OutIter>
|
||||
__copy_n(_InIter __first, typename _IterOps<_AlgPolicy>::template __difference_type<_InIter> __n, _OutIter __result) {
|
||||
if (__n <= 0)
|
||||
return {std::move(__first), std::move(__result)};
|
||||
return std::__copy(__first, __first + __n, std::move(__result));
|
||||
}
|
||||
|
||||
|
|
@ -42,7 +44,7 @@ template <class _AlgPolicy,
|
|||
__enable_if_t<!__has_random_access_iterator_category<_InIter>::value, int> = 0>
|
||||
_LIBCPP_HIDE_FROM_ABI _LIBCPP_CONSTEXPR_SINCE_CXX20 __in_out_result<_InIter, _OutIter>
|
||||
__copy_n(_InIter __first, typename _IterOps<_AlgPolicy>::template __difference_type<_InIter> __n, _OutIter __result) {
|
||||
while (__n != 0) {
|
||||
while (__n > 0) {
|
||||
*__result = *__first;
|
||||
++__first;
|
||||
++__result;
|
||||
|
|
|
|||
|
|
@ -12,6 +12,7 @@
|
|||
|
||||
#include <__algorithm/for_each.h>
|
||||
#include <__algorithm/for_each_n_segment.h>
|
||||
#include <__assert>
|
||||
#include <__config>
|
||||
#include <__functional/identity.h>
|
||||
#include <__iterator/iterator_traits.h>
|
||||
|
|
@ -32,15 +33,12 @@ _LIBCPP_BEGIN_NAMESPACE_STD
|
|||
|
||||
template <class _InputIterator, class _Size, class _Func, class _Proj>
|
||||
_LIBCPP_HIDE_FROM_ABI _LIBCPP_CONSTEXPR_SINCE_CXX20 _InputIterator
|
||||
__for_each_n(_InputIterator __first, _Size __orig_n, _Func&& __f, _Proj& __proj) {
|
||||
typedef decltype(std::__convert_to_integral(__orig_n)) _IntegralSize;
|
||||
_IntegralSize __n = __orig_n;
|
||||
|
||||
__for_each_n(_InputIterator __first, _Size __n, _Func&& __f, _Proj& __proj) {
|
||||
#ifndef _LIBCPP_CXX03_LANG
|
||||
if constexpr (__is_segmented_iterator_v<_InputIterator>) {
|
||||
using __local_iterator = typename __segmented_iterator_traits<_InputIterator>::__local_iterator;
|
||||
if constexpr (__has_random_access_iterator_category<__local_iterator>::value) {
|
||||
return std::__for_each_n_segment(__first, __orig_n, [&](__local_iterator __lfirst, __local_iterator __llast) {
|
||||
return std::__for_each_n_segment(__first, __n, [&](__local_iterator __lfirst, __local_iterator __llast) {
|
||||
std::__for_each(__lfirst, __llast, __f, __proj);
|
||||
});
|
||||
} else {
|
||||
|
|
@ -61,10 +59,12 @@ __for_each_n(_InputIterator __first, _Size __orig_n, _Func&& __f, _Proj& __proj)
|
|||
#if _LIBCPP_STD_VER >= 17
|
||||
|
||||
template <class _InputIterator, class _Size, class _Func>
|
||||
inline _LIBCPP_HIDE_FROM_ABI _LIBCPP_CONSTEXPR_SINCE_CXX20 _InputIterator
|
||||
for_each_n(_InputIterator __first, _Size __orig_n, _Func __f) {
|
||||
inline _LIBCPP_HIDE_FROM_ABI
|
||||
_LIBCPP_CONSTEXPR_SINCE_CXX20 _InputIterator for_each_n(_InputIterator __first, _Size __orig_n, _Func __f) {
|
||||
auto __n = std::__convert_to_integral(__orig_n);
|
||||
_LIBCPP_ASSERT_VALID_ELEMENT_ACCESS(__n >= 0, "for_each_n requires a non-negative count");
|
||||
__identity __proj;
|
||||
return std::__for_each_n(__first, __orig_n, __f, __proj);
|
||||
return std::__for_each_n(__first, __n, __f, __proj);
|
||||
}
|
||||
|
||||
#endif // _LIBCPP_STD_VER >= 17
|
||||
|
|
|
|||
|
|
@ -12,6 +12,7 @@
|
|||
#include <__algorithm/for_each_n.h>
|
||||
#include <__config>
|
||||
#include <__functional/identity.h>
|
||||
#include <__utility/convert_to_integral.h>
|
||||
#include <__utility/forward.h>
|
||||
#include <__utility/move.h>
|
||||
|
||||
|
|
@ -26,20 +27,20 @@ _LIBCPP_BEGIN_NAMESPACE_STD
|
|||
|
||||
template <class _OutputIterator, class _Size, class _Generator>
|
||||
inline _LIBCPP_HIDE_FROM_ABI _LIBCPP_CONSTEXPR_SINCE_CXX20 _OutputIterator
|
||||
__generate_n(_OutputIterator __first, _Size __orig_n, _Generator& __gen) {
|
||||
__generate_n(_OutputIterator __first, _Size __n, _Generator& __gen) {
|
||||
if (__n <= 0)
|
||||
return __first;
|
||||
|
||||
using __iter_ref = decltype(*__first);
|
||||
__identity __proj;
|
||||
return std::__for_each_n(
|
||||
std::move(__first),
|
||||
__orig_n,
|
||||
[&](__iter_ref __element) { std::forward<__iter_ref>(__element) = __gen(); },
|
||||
__proj);
|
||||
std::move(__first), __n, [&](__iter_ref __element) { std::forward<__iter_ref>(__element) = __gen(); }, __proj);
|
||||
}
|
||||
|
||||
template <class _OutputIterator, class _Size, class _Generator>
|
||||
inline _LIBCPP_HIDE_FROM_ABI _LIBCPP_CONSTEXPR_SINCE_CXX20 _OutputIterator
|
||||
generate_n(_OutputIterator __first, _Size __orig_n, _Generator __gen) {
|
||||
return std::__generate_n(std::move(__first), __orig_n, __gen);
|
||||
return std::__generate_n(std::move(__first), std::__convert_to_integral(__orig_n), __gen);
|
||||
}
|
||||
|
||||
_LIBCPP_END_NAMESPACE_STD
|
||||
|
|
|
|||
|
|
@ -11,6 +11,7 @@
|
|||
|
||||
#include <__algorithm/for_each_n.h>
|
||||
#include <__algorithm/in_fun_result.h>
|
||||
#include <__assert>
|
||||
#include <__config>
|
||||
#include <__functional/identity.h>
|
||||
#include <__iterator/concepts.h>
|
||||
|
|
@ -40,6 +41,7 @@ struct __for_each_n {
|
|||
template <input_iterator _Iter, class _Proj = identity, indirectly_unary_invocable<projected<_Iter, _Proj>> _Func>
|
||||
_LIBCPP_HIDE_FROM_ABI constexpr for_each_n_result<_Iter, _Func>
|
||||
operator()(_Iter __first, iter_difference_t<_Iter> __count, _Func __func, _Proj __proj = {}) const {
|
||||
_LIBCPP_ASSERT_VALID_ELEMENT_ACCESS(__count >= 0, "for_each_n requires a non-negative count");
|
||||
auto __last = std::__for_each_n(std::move(__first), __count, __func, __proj);
|
||||
return {std::move(__last), std::move(__func)};
|
||||
}
|
||||
|
|
|
|||
|
|
@ -424,8 +424,8 @@ public:
|
|||
}
|
||||
|
||||
#if _LIBCPP_STD_VER <= 17
|
||||
_LIBCPP_HIDE_FROM_ABI _LIBCPP_CONSTEXPR_SINCE_CXX20 friend bool
|
||||
operator!=(const __bit_iterator& __x, const __bit_iterator& __y) {
|
||||
_LIBCPP_HIDE_FROM_ABI
|
||||
_LIBCPP_CONSTEXPR_SINCE_CXX20 friend bool operator!=(const __bit_iterator& __x, const __bit_iterator& __y) {
|
||||
return !(__x == __y);
|
||||
}
|
||||
|
||||
|
|
@ -573,12 +573,13 @@ struct __specialized_algorithm<_Algorithm::__fill_n, __single_iterator<__bit_ite
|
|||
template <class _Size, class _Tp>
|
||||
_LIBCPP_HIDE_FROM_ABI _LIBCPP_CONSTEXPR_SINCE_CXX20 static __bit_iterator<_Cp, false>
|
||||
operator()(__bit_iterator<_Cp, false> __first, _Size __n, const _Tp& __value) {
|
||||
if (__n > 0) {
|
||||
if (__value)
|
||||
__impl<true>(__first, __n);
|
||||
else
|
||||
__impl<false>(__first, __n);
|
||||
}
|
||||
if (__n <= 0)
|
||||
return __first;
|
||||
|
||||
if (__value)
|
||||
__impl<true>(__first, __n);
|
||||
else
|
||||
__impl<false>(__first, __n);
|
||||
return __first + __n;
|
||||
}
|
||||
};
|
||||
|
|
|
|||
|
|
@ -0,0 +1,33 @@
|
|||
//===----------------------------------------------------------------------===//
|
||||
//
|
||||
// Part of the LLVM Project, under the Apache License v2.0 with LLVM Exceptions.
|
||||
// See https://llvm.org/LICENSE.txt for license information.
|
||||
// SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception
|
||||
//
|
||||
//===----------------------------------------------------------------------===//
|
||||
|
||||
// <algorithm>
|
||||
|
||||
// template<class InputIterator, class Size, class Function>
|
||||
// constexpr InputIterator
|
||||
// for_each_n(InputIterator first, Size n, Function f);
|
||||
//
|
||||
// [alg.foreach] requires `n >= 0`; passing a negative count is a precondition violation.
|
||||
|
||||
// REQUIRES: has-unix-headers
|
||||
// UNSUPPORTED: c++03, c++11, c++14
|
||||
// UNSUPPORTED: libcpp-hardening-mode=none
|
||||
// XFAIL: libcpp-hardening-mode=debug && availability-verbose_abort-missing
|
||||
|
||||
#include <algorithm>
|
||||
|
||||
#include "check_assertion.h"
|
||||
|
||||
int main(int, char**) {
|
||||
int a[] = {1, 2, 3};
|
||||
|
||||
TEST_LIBCPP_ASSERT_FAILURE(std::for_each_n(a, -1, [](int) {}), "for_each_n requires a non-negative count");
|
||||
TEST_LIBCPP_ASSERT_FAILURE(std::for_each_n(a, -10000000, [](int) {}), "for_each_n requires a non-negative count");
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
|
@ -0,0 +1,37 @@
|
|||
//===----------------------------------------------------------------------===//
|
||||
//
|
||||
// Part of the LLVM Project, under the Apache License v2.0 with LLVM Exceptions.
|
||||
// See https://llvm.org/LICENSE.txt for license information.
|
||||
// SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception
|
||||
//
|
||||
//===----------------------------------------------------------------------===//
|
||||
|
||||
// <algorithm>
|
||||
|
||||
// template<input_iterator I, class Proj = identity,
|
||||
// indirectly_unary_invocable<projected<I, Proj>> Fun>
|
||||
// constexpr ranges::for_each_n_result<I, Fun>
|
||||
// ranges::for_each_n(I first, iter_difference_t<I> n, Fun f, Proj proj = {});
|
||||
//
|
||||
// [alg.foreach] requires `n >= 0`; passing a negative count is a precondition violation.
|
||||
|
||||
// REQUIRES: has-unix-headers
|
||||
// UNSUPPORTED: c++03, c++11, c++14, c++17
|
||||
// UNSUPPORTED: libcpp-hardening-mode=none
|
||||
// XFAIL: libcpp-hardening-mode=debug && availability-verbose_abort-missing
|
||||
|
||||
#include <algorithm>
|
||||
#include <array>
|
||||
|
||||
#include "check_assertion.h"
|
||||
|
||||
int main(int, char**) {
|
||||
std::array a = {1, 2, 3};
|
||||
|
||||
TEST_LIBCPP_ASSERT_FAILURE(
|
||||
std::ranges::for_each_n(a.begin(), -1, [](int) {}), "for_each_n requires a non-negative count");
|
||||
TEST_LIBCPP_ASSERT_FAILURE(
|
||||
std::ranges::for_each_n(a.begin(), -10000000, [](int) {}), "for_each_n requires a non-negative count");
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
|
@ -12,9 +12,12 @@
|
|||
// constexpr OutIter // constexpr after C++17
|
||||
// copy_n(InIter first, InIter::difference_type n, OutIter result);
|
||||
|
||||
// XFAIL: FROZEN-CXX03-HEADERS-FIXME
|
||||
|
||||
#include <algorithm>
|
||||
#include <cassert>
|
||||
#include <cstdint>
|
||||
#include <iterator>
|
||||
#include <vector>
|
||||
|
||||
#include "test_macros.h"
|
||||
|
|
@ -24,6 +27,34 @@
|
|||
|
||||
typedef UserDefinedIntegral<unsigned> UDI;
|
||||
|
||||
// A minimal single-pass input iterator that counts how many times it is advanced. Used to verify that
|
||||
// copy_n reads exactly n elements (advancing the iterator only n - 1 times).
|
||||
struct CountingInputIterator {
|
||||
using iterator_category = std::input_iterator_tag;
|
||||
using value_type = int;
|
||||
using difference_type = long;
|
||||
using pointer = const int*;
|
||||
using reference = const int&;
|
||||
|
||||
const int* ptr_;
|
||||
int* increments_;
|
||||
|
||||
TEST_CONSTEXPR_CXX14 CountingInputIterator(const int* ptr, int* increment_counter)
|
||||
: ptr_(ptr), increments_(increment_counter) {}
|
||||
|
||||
TEST_CONSTEXPR_CXX14 reference operator*() const { return *ptr_; }
|
||||
TEST_CONSTEXPR_CXX14 CountingInputIterator& operator++() {
|
||||
++ptr_;
|
||||
++*increments_;
|
||||
return *this;
|
||||
}
|
||||
TEST_CONSTEXPR_CXX14 CountingInputIterator operator++(int) {
|
||||
CountingInputIterator __tmp = *this;
|
||||
++*this;
|
||||
return __tmp;
|
||||
}
|
||||
};
|
||||
|
||||
class PaddedBase {
|
||||
public:
|
||||
TEST_CONSTEXPR PaddedBase(std::int16_t a, std::int8_t b) : a_(a), b_(b) {}
|
||||
|
|
@ -61,6 +92,15 @@ struct TestIterators {
|
|||
assert(base(r) == ib + N / 2);
|
||||
for (unsigned i = 0; i < N / 2; ++i)
|
||||
assert(ia[i] == ib[i]);
|
||||
|
||||
{ // A negative count is a no-op that returns the unchanged output iterator.
|
||||
// Regression test for https://llvm.org/PR193613.
|
||||
int source[] = {1, 2, 3};
|
||||
int dest[] = {-1, -2, -3};
|
||||
OutIter ret = std::copy_n(InIter(source), -5, OutIter(dest));
|
||||
assert(base(ret) == dest);
|
||||
assert(dest[0] == -1 && dest[1] == -2 && dest[2] == -3);
|
||||
}
|
||||
}
|
||||
};
|
||||
};
|
||||
|
|
@ -81,6 +121,13 @@ TEST_CONSTEXPR_CXX20 bool test_vector_bool(std::size_t N) {
|
|||
for (std::size_t i = 0; i < N; ++i)
|
||||
assert(out[i + 4] == in[i]);
|
||||
}
|
||||
{ // Negative count test
|
||||
std::vector<bool> source(N, true);
|
||||
std::vector<bool> dest(N, false);
|
||||
std::vector<bool>::iterator r = std::copy_n(source.begin(), -5, dest.begin());
|
||||
assert(r == dest.begin());
|
||||
assert(dest == std::vector<bool>(N, false));
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
|
@ -114,6 +161,28 @@ TEST_CONSTEXPR_CXX20 bool test() {
|
|||
assert(test_vector_bool(256));
|
||||
}
|
||||
|
||||
{ // For a single-pass input iterator, copy_n reads exactly n elements -- advancing the iterator only
|
||||
// n - 1 times. For n == 0, it shouldn't advance the iterator. See 99847d2bf132.
|
||||
int in[] = {1, 2, 3, 4, 5};
|
||||
|
||||
{ // n == 0 is a no-op
|
||||
int out[3] = {-1, -2, -3};
|
||||
int increments = 0;
|
||||
int* r = std::copy_n(CountingInputIterator(in, &increments), 0, out);
|
||||
assert(r == out);
|
||||
assert(increments == 0);
|
||||
assert(out[0] == -1 && out[1] == -2 && out[2] == -3);
|
||||
}
|
||||
{ // n > 0 advances the iterator exactly n - 1 times
|
||||
int out[3] = {0, 0, 0};
|
||||
int increments = 0;
|
||||
int* r = std::copy_n(CountingInputIterator(in, &increments), 3, out);
|
||||
assert(r == out + 3);
|
||||
assert(increments == 2);
|
||||
assert(out[0] == 1 && out[1] == 2 && out[2] == 3);
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -60,6 +60,17 @@ constexpr void test_iterators() {
|
|||
assert(base(ret.in) == in.data());
|
||||
assert(base(ret.out) == out.data());
|
||||
}
|
||||
|
||||
{ // A negative count is a no-op that returns the unchanged iterators.
|
||||
// Regression test for https://llvm.org/PR193613.
|
||||
std::array in{1, 2, 3};
|
||||
std::array out{-1, -2, -3};
|
||||
std::same_as<std::ranges::in_out_result<In, Out>> auto ret =
|
||||
std::ranges::copy_n(In(in.data()), -5, Out(out.data()));
|
||||
assert(base(ret.in) == in.data());
|
||||
assert(base(ret.out) == out.data());
|
||||
assert((out == std::array{-1, -2, -3}));
|
||||
}
|
||||
}
|
||||
|
||||
#if TEST_STD_VER >= 23
|
||||
|
|
@ -79,6 +90,13 @@ constexpr bool test_vector_bool(std::size_t N) {
|
|||
for (std::size_t i = 0; i < N; ++i)
|
||||
assert(out[i + 4] == in[i]);
|
||||
}
|
||||
{ // Negative count test
|
||||
std::vector<bool> source(N, true);
|
||||
std::vector<bool> dest(N, false);
|
||||
auto ret = std::ranges::copy_n(source.begin(), -5, dest.begin());
|
||||
assert(ret.out == dest.begin());
|
||||
assert(dest == std::vector<bool>(N, false));
|
||||
}
|
||||
|
||||
return true;
|
||||
};
|
||||
|
|
|
|||
|
|
@ -13,6 +13,8 @@
|
|||
// constexpr OutputIterator // constexpr after C++17
|
||||
// fill_n(Iter first, Size n, const T& value);
|
||||
|
||||
// XFAIL: FROZEN-CXX03-HEADERS-FIXME
|
||||
|
||||
#include <algorithm>
|
||||
#include <array>
|
||||
#include <cassert>
|
||||
|
|
@ -52,6 +54,14 @@ struct Test {
|
|||
std::array<T, 4> expected = {1, 5, 5, 4};
|
||||
test<Iter>(in, 1, 2, 5, expected);
|
||||
}
|
||||
{ // A negative count is a no-op that returns the unchanged iterator.
|
||||
// Regression test for https://llvm.org/PR193613.
|
||||
std::array<T, 4> in = {1, 2, 3, 4};
|
||||
std::array<T, 4> expected = {1, 2, 3, 4};
|
||||
Iter it = std::fill_n(Iter(in.data()), -5, T(9));
|
||||
assert(base(it) == in.data());
|
||||
assert(in == expected);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
|
|
@ -126,6 +136,13 @@ TEST_CONSTEXPR_CXX20 bool test_vector_bool(std::size_t N) {
|
|||
}
|
||||
}
|
||||
|
||||
{ // Negative count test
|
||||
std::vector<bool> v(N, false);
|
||||
std::vector<bool>::iterator r = std::fill_n(v.begin(), -5, true);
|
||||
assert(r == v.begin());
|
||||
assert(v == std::vector<bool>(N, false));
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -51,6 +51,14 @@ constexpr void test_iterators() {
|
|||
auto ret = std::ranges::fill_n(It(a.data()), 0, 1);
|
||||
assert(base(ret) == a.data());
|
||||
}
|
||||
|
||||
{ // A negative count is a no-op that returns the unchanged iterator.
|
||||
// Regression test for https://llvm.org/PR193613.
|
||||
int a[3] = {-1, -2, -3};
|
||||
std::same_as<It> decltype(auto) ret = std::ranges::fill_n(It(a), -5, 1);
|
||||
assert(base(ret) == a);
|
||||
assert(a[0] == -1 && a[1] == -2 && a[2] == -3);
|
||||
}
|
||||
}
|
||||
|
||||
// The `ranges::{fill, fill_n}` algorithms require `vector<bool, Alloc>::iterator` to satisfy
|
||||
|
|
@ -99,6 +107,13 @@ constexpr bool test_vector_bool(std::size_t N) {
|
|||
}
|
||||
}
|
||||
|
||||
{ // Negative count test
|
||||
std::vector<bool> v(N, false);
|
||||
auto r = std::ranges::fill_n(v.begin(), -5, true);
|
||||
assert(r == v.begin());
|
||||
assert(v == std::vector<bool>(N, false));
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
#endif
|
||||
|
|
|
|||
|
|
@ -70,6 +70,13 @@ test()
|
|||
test2<Iter, float>();
|
||||
test2<Iter, double>(); // this is PR#35498
|
||||
test2<Iter, long double>();
|
||||
|
||||
{ // A negative count is a no-op that returns the unchanged iterator.
|
||||
// Regression test for https://llvm.org/PR193613.
|
||||
int ia[] = {1, 2, 3};
|
||||
assert(std::generate_n(Iter(ia), -5, gen_test()) == Iter(ia));
|
||||
assert(ia[0] == 1 && ia[1] == 2 && ia[2] == 3);
|
||||
}
|
||||
}
|
||||
|
||||
void deque_test() {
|
||||
|
|
|
|||
|
|
@ -92,6 +92,20 @@ constexpr void test_iter() {
|
|||
test_one<Iter>(std::array{-10, -20, -30, -40, -50}, 3, gen, {1, 2, 3, -40, -50});
|
||||
// Longer sequence, n = 5.
|
||||
test_one<Iter>(std::array<int, 5>{}, 5, gen, {1, 2, 3, 4, 5});
|
||||
|
||||
{ // A negative count is a no-op that returns the unchanged iterator.
|
||||
// Regression test for https://llvm.org/PR193613.
|
||||
std::array in{-10, -20, -30};
|
||||
int called = 0;
|
||||
auto counting_gen = [&called] {
|
||||
++called;
|
||||
return 42;
|
||||
};
|
||||
std::same_as<Iter> decltype(auto) result = std::ranges::generate_n(Iter(in.data()), -5, counting_gen);
|
||||
assert(base(result) == in.data());
|
||||
assert(called == 0);
|
||||
assert((in == std::array{-10, -20, -30}));
|
||||
}
|
||||
}
|
||||
|
||||
constexpr void test_iterators() {
|
||||
|
|
@ -131,7 +145,6 @@ constexpr bool test() {
|
|||
assert(gen_invocations == N2);
|
||||
}
|
||||
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
|
|
|
|||
Loading…
Reference in a new issue