Skip to content

Commit 5146b3f

Browse files
committed
fable: Fix {N,D}::validate_bounds() incorrect validation
This happened when incorrectly casting from signed to unsigned. Tests have been added to test these edge cases.
1 parent 5e95143 commit 5146b3f

File tree

4 files changed

+216
-52
lines changed

4 files changed

+216
-52
lines changed

fable/include/fable/schema/duration.hpp

+25-22
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,8 @@
2929
#include <string> // for string
3030
#include <utility> // for move
3131

32-
#include <fable/schema/interface.hpp> // for Base<>
32+
#include <fable/schema/interface.hpp> // for Base<>
33+
#include <fable/utility/templates.hpp> // for is_safe_cast<>, typeinfo<>
3334

3435
namespace fable::schema {
3536

@@ -171,34 +172,36 @@ class Duration : public Base<Duration<T, Period>> {
171172
*/
172173
template <typename B>
173174
bool validate_bounds(const Conf& c, std::optional<SchemaError>& err) const {
174-
auto v = c.get<B>();
175-
if (!std::numeric_limits<B>::is_signed && value_min_ < 0) {
176-
// If B is unsigned and value_min_ is less than 0, there is no way
177-
// that v cannot fulfill the minimum requirements. Trying to use the
178-
// other branches will "underflow" the value_min_ which will invalidate
179-
// any comparison.
180-
} else if (exclusive_min_) {
181-
if (v <= static_cast<B>(value_min_)) {
182-
return this->set_error(err, c, "expected exclusive minimum of {}, got {}", value_min_, v);
175+
auto original = c.get<B>();
176+
auto value = static_cast<T>(original);
177+
if constexpr (!std::is_floating_point_v<T>) {
178+
if (!is_cast_safe<T>(original)) {
179+
return this->set_error(err, c,
180+
"failed to convert input to destination type {}, got {}( {} ) = {}",
181+
typeinfo<T>::name, typeinfo<B>::name, original, value);
182+
}
183+
}
184+
185+
// Check minimum value:
186+
if (exclusive_min_) {
187+
if (value <= value_min_) {
188+
return this->set_error(err, c, "expected exclusive minimum of {}, got {}", value_min_,
189+
value);
183190
}
184191
} else {
185-
if (v < static_cast<B>(value_min_)) {
186-
return this->set_error(err, c, "expected minimum of {}, got {}", value_min_, v);
192+
if (value < value_min_) {
193+
return this->set_error(err, c, "expected minimum of {}, got {}", value_min_, value);
187194
}
188195
}
189196

190-
if (!std::numeric_limits<B>::is_signed && value_max_ < 0) {
191-
// If B is unsigned, but our maximum value is somewhere below 0, then v
192-
// will by definition always be out-of-bounds.
193-
return this->set_error(err, c, "expected {}maximum of {}, got {}",
194-
(exclusive_max_ ? "exclusive " : ""), value_max_, v);
195-
} else if (exclusive_max_) {
196-
if (v >= static_cast<B>(value_max_)) {
197-
return this->set_error(err, c, "expected exclusive maximum of {}, got {}", value_max_, v);
197+
if (exclusive_max_) {
198+
if (value >= value_max_) {
199+
return this->set_error(err, c, "expected exclusive maximum of {}, got {}", value_max_,
200+
value);
198201
}
199202
} else {
200-
if (v > static_cast<B>(value_max_)) {
201-
return this->set_error(err, c, "expected maximum of {}, got {}", value_max_, v);
203+
if (value > value_max_) {
204+
return this->set_error(err, c, "expected maximum of {}, got {}", value_max_, value);
202205
}
203206
}
204207

fable/include/fable/schema/number_impl.hpp

+36-26
Original file line numberDiff line numberDiff line change
@@ -34,7 +34,8 @@
3434
#include <utility> // for move
3535
#include <vector> // for vector<>
3636

37-
#include <fable/schema/number.hpp> // for Number<>
37+
#include <fable/schema/number.hpp> // for Number<>
38+
#include <fable/utility/templates.hpp> // for is_safe_cast<>, typeinfo<>
3839

3940
namespace fable::schema {
4041

@@ -273,49 +274,58 @@ void Number<T>::reset_ptr() {
273274

274275
/**
275276
* Check that the min and max bounds are held by c.
277+
*
278+
* Use-cases with the bounds T={} and the input B=[]:
279+
* (a) ---{---[--0--]---}--- B within T
280+
* Note that this doesn't mean we can stop looking, since the actual range
281+
* might be in the top or bottom of T:
282+
* -------[--0--]{--}---
283+
*
284+
* (b) ---[---{--0--}---]--- T within B
285+
*
286+
* (c) ---[------{------]--} T overlaps B
276287
*/
277288
template <typename T>
278289
template <typename B>
279290
bool Number<T>::validate_bounds(const Conf& c, std::optional<SchemaError>& err) const {
280-
auto v = c.get<B>();
291+
auto original = c.get<B>();
292+
auto value = static_cast<T>(original);
293+
if constexpr (!std::is_floating_point_v<T>) {
294+
if (!is_cast_safe<T>(original)) {
295+
return this->set_error(err, c,
296+
"failed to convert input to destination type {}, got {}( {} ) = {}",
297+
typeinfo<T>::name, typeinfo<B>::name, original, value);
298+
}
299+
}
281300

282-
// Check whitelist and blacklist first.
301+
// Check whitelist and blacklist first:
283302
if (!std::is_floating_point_v<T>) {
284-
if (whitelist_.count(v)) {
303+
if (whitelist_.count(value)) {
285304
return true;
286305
}
287-
if (blacklist_.count(v)) {
288-
return this->set_error(err, c, "unexpected blacklisted value {}", v);
306+
if (blacklist_.count(value)) {
307+
return this->set_error(err, c, "unexpected blacklisted value {}", value);
289308
}
290309
}
291310

292-
if (!std::numeric_limits<B>::is_signed && value_min_ < 0) {
293-
// If B is unsigned and value_min_ is less than 0, there is no way
294-
// that v cannot fulfill the minimum requirements. Trying to use the
295-
// other branches will "underflow" the value_min_ which will invalidate
296-
// any comparison.
297-
} else if (exclusive_min_) {
298-
if (v <= static_cast<B>(value_min_)) {
299-
return this->set_error(err, c, "expected exclusive minimum of {}, got {}", value_min_, v);
311+
// Check minimum value:
312+
if (exclusive_min_) {
313+
if (value <= value_min_) {
314+
return this->set_error(err, c, "expected exclusive minimum > {}, got {}", value_min_, value);
300315
}
301316
} else {
302-
if (v < static_cast<B>(value_min_)) {
303-
return this->set_error(err, c, "expected minimum of {}, got {}", value_min_, v);
317+
if (value < value_min_) {
318+
return this->set_error(err, c, "expected minimum >= {}, got {}", value_min_, value);
304319
}
305320
}
306321

307-
if (!std::numeric_limits<B>::is_signed && value_max_ < 0) {
308-
// If B is unsigned, but our maximum value is somewhere below 0, then v
309-
// will by definition always be out-of-bounds.
310-
return this->set_error(err, c, "expected {}maximum of {}, got {}",
311-
(exclusive_max_ ? "exclusive " : ""), value_max_, v);
312-
} else if (exclusive_max_) {
313-
if (v >= static_cast<B>(value_max_)) {
314-
return this->set_error(err, c, "expected exclusive maximum of {}, got {}", value_max_, v);
322+
if (exclusive_max_) {
323+
if (value >= value_max_) {
324+
return this->set_error(err, c, "expected exclusive maximum < {}, got {}", value_max_, value);
315325
}
316326
} else {
317-
if (v > static_cast<B>(value_max_)) {
318-
return this->set_error(err, c, "expected maximum of {}, got {}", value_max_, v);
327+
if (value > value_max_) {
328+
return this->set_error(err, c, "expected maximum <= {}, got {}", value_max_, value);
319329
}
320330
}
321331

+82
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,82 @@
1+
/*
2+
* Copyright 2023 Robert Bosch GmbH
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*
16+
* SPDX-License-Identifier: Apache-2.0
17+
*/
18+
/**
19+
* \file fable/utility/number.hpp
20+
*/
21+
22+
#pragma once
23+
24+
#include <cstdint> // for int8_t, ...
25+
#include <limits> // for numeric_limits
26+
27+
namespace fable {
28+
29+
/**
30+
* Return true if casting value to target type is safe.
31+
*
32+
* Safe means the specific value provided will not have its sign or value changed
33+
* by the conversion.
34+
*/
35+
template <typename T, typename S>
36+
constexpr bool is_cast_safe(S value) {
37+
if (static_cast<S>(static_cast<T>(value)) != value) {
38+
// Ensure no narrowing is occurring.
39+
return false;
40+
}
41+
42+
if constexpr (std::numeric_limits<T>::is_signed != std::numeric_limits<S>::is_signed) {
43+
// Mismatch in signedness can go wrong in two ways that aren't caught above.
44+
if constexpr (std::numeric_limits<S>::is_signed) {
45+
// Check value will not underflow.
46+
return value >= 0;
47+
} else {
48+
// Check value will not overflow.
49+
return static_cast<unsigned long long>(value) <=
50+
static_cast<unsigned long long>(std::numeric_limits<T>::max());
51+
}
52+
}
53+
54+
return true;
55+
}
56+
57+
/**
58+
* Encode the string name of a type as a static member.
59+
* This is used for error messages.
60+
*
61+
* Example:
62+
*
63+
* std::cout << typeinfo<int8_t>::name << std::endl;
64+
*/
65+
template <typename T>
66+
struct typeinfo {
67+
static const constexpr char* name = "unknown";
68+
};
69+
70+
// clang-format off
71+
template <> struct typeinfo<bool> { static const constexpr char* name = "bool"; };
72+
template <> struct typeinfo<int8_t> { static const constexpr char* name = "int8_t"; };
73+
template <> struct typeinfo<int16_t> { static const constexpr char* name = "int16_t"; };
74+
template <> struct typeinfo<int32_t> { static const constexpr char* name = "int32_t"; };
75+
template <> struct typeinfo<int64_t> { static const constexpr char* name = "int64_t"; };
76+
template <> struct typeinfo<uint8_t> { static const constexpr char* name = "uint8_t"; };
77+
template <> struct typeinfo<uint16_t> { static const constexpr char* name = "uint16_t"; };
78+
template <> struct typeinfo<uint32_t> { static const constexpr char* name = "uint32_t"; };
79+
template <> struct typeinfo<uint64_t> { static const constexpr char* name = "uint64_t"; };
80+
// clang-format on
81+
82+
} // namespace fable

fable/src/fable/schema/number_test.cpp

+73-4
Original file line numberDiff line numberDiff line change
@@ -26,12 +26,13 @@ using namespace std; // NOLINT(build/namespaces)
2626

2727
#include <gtest/gtest.h>
2828

29-
#include <fable/confable.hpp> // for Confable
30-
#include <fable/schema.hpp> // for Number
31-
#include <fable/utility/gtest.hpp> // for assert_validate
29+
#include <fable/confable.hpp> // for Confable
30+
#include <fable/schema.hpp> // for Number
31+
#include <fable/utility/gtest.hpp> // for assert_validate
32+
#include <fable/utility/templates.hpp> // for typeinfo
3233

3334
struct MyNumberStruct : public fable::Confable {
34-
uint8_t number;
35+
uint8_t number = 0;
3536
CONFABLE_SCHEMA(MyNumberStruct) {
3637
using namespace fable::schema; // NOLINT(build/namespace)
3738
return Struct{
@@ -76,3 +77,71 @@ TEST(fable_schema_number, validate) {
7677
});
7778
}
7879
}
80+
81+
template <typename T>
82+
void assert_validate_t(const fable::Schema& schema, const std::vector<T>& xs) {
83+
for (T x : xs) {
84+
std::cerr << "Expect ok " << fable::typeinfo<T>::name << ":\t" << (+x) << std::endl;
85+
fable::assert_validate(schema, fable::Json(x));
86+
}
87+
}
88+
89+
template <typename T>
90+
void assert_invalidate_t(const fable::Schema& schema, const std::vector<T>& xs) {
91+
for (T x : xs) {
92+
std::cerr << "Expect fail " << fable::typeinfo<T>::name << ":\t" << (+x) << std::endl;
93+
fable::assert_invalidate(schema, fable::Json(x));
94+
}
95+
}
96+
97+
TEST(fable_schema_number, validate_u64) {
98+
auto schema = fable::schema::Number<uint64_t>(nullptr, "");
99+
100+
assert_validate_t<uint64_t>(schema, {0, 1, std::numeric_limits<uint64_t>::max()});
101+
assert_validate_t<int64_t>(schema, {0, 1, std::numeric_limits<int64_t>::max()});
102+
103+
assert_invalidate_t<int8_t>(schema, {-1, -5, std::numeric_limits<int8_t>::min()});
104+
assert_invalidate_t<int64_t>(schema, {-1, -5, std::numeric_limits<int8_t>::min()});
105+
}
106+
107+
TEST(fable_schema_number, validate_u64_narrow_high) {
108+
auto schema = fable::schema::Number<uint64_t>(nullptr, "")
109+
.minimum(std::numeric_limits<uint64_t>::max() - 2);
110+
111+
assert_validate_t<uint64_t>(
112+
schema, {std::numeric_limits<uint64_t>::max() - 2, std::numeric_limits<uint64_t>::max()});
113+
assert_invalidate_t<uint64_t>(schema, {0, std::numeric_limits<uint64_t>::max() - 3});
114+
assert_invalidate_t<uint32_t>(schema, {0, std::numeric_limits<uint32_t>::max()});
115+
assert_invalidate_t<int32_t>(schema, {-1, std::numeric_limits<int32_t>::max()});
116+
}
117+
118+
TEST(fable_schema_number, validate_u8) {
119+
auto schema = fable::schema::Number<uint8_t>(nullptr, "");
120+
121+
assert_validate_t<uint8_t>(schema, {0, 1, std::numeric_limits<uint8_t>::max()});
122+
assert_validate_t<int8_t>(schema, {0, 1, std::numeric_limits<int8_t>::max()});
123+
124+
assert_invalidate_t<uint32_t>(schema, {std::numeric_limits<uint32_t>::max()});
125+
assert_invalidate_t<int8_t>(schema, {-1, -5, std::numeric_limits<int8_t>::min()});
126+
assert_invalidate_t<int64_t>(schema, {-1, -5, std::numeric_limits<int8_t>::min()});
127+
}
128+
129+
TEST(fable_schema_number, validate_i64) {
130+
auto schema = fable::schema::Number<int64_t>(nullptr, "");
131+
132+
assert_validate_t<uint64_t>(schema, {0, 1, std::numeric_limits<uint64_t>::max() / 2});
133+
assert_validate_t<int64_t>(schema, {0, 1, std::numeric_limits<int64_t>::max()});
134+
135+
assert_invalidate_t<uint64_t>(schema, {std::numeric_limits<uint64_t>::max()});
136+
}
137+
138+
TEST(fable_schema_number, validate_i8) {
139+
auto schema = fable::schema::Number<int8_t>(nullptr, "");
140+
141+
assert_validate_t<uint8_t>(schema, {0, 1, std::numeric_limits<uint8_t>::max() / 2});
142+
assert_validate_t<int8_t>(schema, {-1, 0, 1, std::numeric_limits<int8_t>::max()});
143+
144+
assert_invalidate_t<uint64_t>(schema, {std::numeric_limits<uint32_t>::max()});
145+
assert_invalidate_t<int64_t>(
146+
schema, {std::numeric_limits<int16_t>::min(), std::numeric_limits<int64_t>::max()});
147+
}

0 commit comments

Comments
 (0)