Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
38 changes: 36 additions & 2 deletions src/rest_utils.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@
//*****************************************************************************
#include "rest_utils.hpp"

#include <cmath>
#include <optional>
#include <set>
#include <sstream>
Expand Down Expand Up @@ -59,6 +60,21 @@ enum : unsigned int {

namespace ovms {

namespace {
// Integral types are always JSON-representable; only floating values can be NaN/Inf,
// which rapidjson cannot serialize (and which are invalid per the JSON spec).
template <typename T>
inline bool isJsonRepresentable(T) {
return true;
}
inline bool isJsonRepresentable(float v) {
return std::isfinite(v);
}
inline bool isJsonRepresentable(double v) {
return std::isfinite(v);
}
} // namespace

static Status checkValField(const size_t& fieldSize, const size_t& expectedElementsNumber) {
if (fieldSize != expectedElementsNumber) {
std::stringstream ss;
Expand Down Expand Up @@ -107,6 +123,11 @@ Status makeJsonFromPredictResponse(
for (size_t i = 0; i < tensor.tensor_content().size(); i += sizeof(float))
tensor.add_float_val(*reinterpret_cast<float*>(tensor.mutable_tensor_content()->data() + i));
}
for (float v : tensor.float_val()) {
if (!isJsonRepresentable(v))
return Status(StatusCode::JSON_SERIALIZATION_ERROR,
"Output \"" + kv.first + "\" contains a non-finite (NaN/Inf) value that cannot be serialized to JSON");
}
break;
case DataType::DT_INT32:
if (seekDataInValField) {
Expand Down Expand Up @@ -147,6 +168,11 @@ Status makeJsonFromPredictResponse(
for (size_t i = 0; i < tensor.tensor_content().size(); i += sizeof(double))
tensor.add_double_val(*reinterpret_cast<double*>(tensor.mutable_tensor_content()->data() + i));
}
for (double v : tensor.double_val()) {
if (!isJsonRepresentable(v))
return Status(StatusCode::JSON_SERIALIZATION_ERROR,
"Output \"" + kv.first + "\" contains a non-finite (NaN/Inf) value that cannot be serialized to JSON");
}
break;
case DataType::DT_INT16:
if (seekDataInValField) {
Expand Down Expand Up @@ -291,15 +317,23 @@ static void appendBinaryOutput(std::string& bytesOutputsBuffer, char* output, si
appendBinaryOutput(bytesOutputsBuffer, (char*)tensor.contents().CONTENTS_FIELD().data(), expectedContentSize); \
} else { \
for (auto& number : tensor.contents().CONTENTS_FIELD()) { \
if (!isJsonRepresentable(number)) \
return Status(StatusCode::JSON_SERIALIZATION_ERROR, \
"Output \"" + tensor.name() + "\" contains a non-finite (NaN/Inf) value that cannot be serialized to JSON"); \
writer.WRITER_TYPE(number); \
} \
} \
} else { \
if (binaryOutput) { \
appendBinaryOutput(bytesOutputsBuffer, (char*)response_proto.raw_output_contents(tensor_it).data(), expectedContentSize); \
} else { \
for (size_t i = 0; i < response_proto.raw_output_contents(tensor_it).size(); i += sizeof(DATATYPE)) \
writer.WRITER_TYPE(*(reinterpret_cast<const DATATYPE*>(response_proto.raw_output_contents(tensor_it).data() + i))); \
for (size_t i = 0; i < response_proto.raw_output_contents(tensor_it).size(); i += sizeof(DATATYPE)) { \
DATATYPE valToWrite = *(reinterpret_cast<const DATATYPE*>(response_proto.raw_output_contents(tensor_it).data() + i)); \
if (!isJsonRepresentable(valToWrite)) \
return Status(StatusCode::JSON_SERIALIZATION_ERROR, \
"Output \"" + tensor.name() + "\" contains a non-finite (NaN/Inf) value that cannot be serialized to JSON"); \
writer.WRITER_TYPE(valToWrite); \
} \
} \
}

Expand Down
45 changes: 45 additions & 0 deletions src/test/rest_utils_test.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@
// See the License for the specific language governing permissions and
// limitations under the License.
//*****************************************************************************
#include <limits>
#include <optional>

#include <gmock/gmock.h>
Expand Down Expand Up @@ -133,6 +134,33 @@ TEST_F(TFSMakeJsonFromPredictResponseRawTest, CannotConvertInvalidPrecision) {
EXPECT_EQ(makeJsonFromPredictResponse(proto, &json, Order::COLUMN), StatusCode::REST_UNSUPPORTED_PRECISION);
}

TEST_F(TFSMakeJsonFromPredictResponseRawTest, NonFiniteInfFloatError) {
float data[8] = {5.0f, std::numeric_limits<float>::infinity(), -3.0f, 2.5f,
9.0f, 55.5f, -0.5f, -1.5f};
output1->mutable_tensor_content()->assign(reinterpret_cast<const char*>(data), 8 * sizeof(float));
EXPECT_EQ(makeJsonFromPredictResponse(proto, &json, Order::COLUMN), StatusCode::JSON_SERIALIZATION_ERROR);
}

TEST_F(TFSMakeJsonFromPredictResponseRawTest, NonFiniteNaNFloatError) {
float data[8] = {5.0f, 10.0f, std::numeric_limits<float>::quiet_NaN(), 2.5f,
9.0f, 55.5f, -0.5f, -1.5f};
output1->mutable_tensor_content()->assign(reinterpret_cast<const char*>(data), 8 * sizeof(float));
EXPECT_EQ(makeJsonFromPredictResponse(proto, &json, Order::COLUMN), StatusCode::JSON_SERIALIZATION_ERROR);
}

TEST_F(TFSMakeJsonFromPredictResponseRawTest, NonFiniteDoubleError) {
output1->set_dtype(tensorflow::DataType::DT_DOUBLE);
double data[2] = {1.0, std::numeric_limits<double>::infinity()};
output1->mutable_tensor_content()->assign(reinterpret_cast<const char*>(data), 2 * sizeof(double));
output1->clear_tensor_shape();
output1->mutable_tensor_shape()->add_dim()->set_size(2);
EXPECT_EQ(makeJsonFromPredictResponse(proto, &json, Order::COLUMN), StatusCode::JSON_SERIALIZATION_ERROR);
}

TEST_F(TFSMakeJsonFromPredictResponseRawTest, IntOutputsUnaffectedByFiniteCheck) {
EXPECT_EQ(makeJsonFromPredictResponse(proto, &json, Order::COLUMN), StatusCode::OK);
}

const char* rawPositiveFirstOrderResponseRow = R"({
"predictions": [
{
Expand Down Expand Up @@ -791,6 +819,23 @@ TEST_F(KFSMakeJsonFromPredictResponseRawTest, Positive) {
})");
}

TEST_F(KFSMakeJsonFromPredictResponseRawTest, NonFiniteInfFloatError) {
float bad[8] = {5.0f, std::numeric_limits<float>::infinity(), -3.0f, 2.5f, 9.0f, 55.5f, -0.5f, -1.5f};
proto.mutable_raw_output_contents(0)->assign(reinterpret_cast<const char*>(bad), 8 * sizeof(float));
EXPECT_EQ(makeJsonFromPredictResponse(proto, &json, inferenceHeaderContentLength), StatusCode::JSON_SERIALIZATION_ERROR);
}

TEST_F(KFSMakeJsonFromPredictResponseRawTest, NonFiniteNaNFloatError) {
float bad[8] = {std::numeric_limits<float>::quiet_NaN(), 10.0f, -3.0f, 2.5f, 9.0f, 55.5f, -0.5f, -1.5f};
proto.mutable_raw_output_contents(0)->assign(reinterpret_cast<const char*>(bad), 8 * sizeof(float));
EXPECT_EQ(makeJsonFromPredictResponse(proto, &json, inferenceHeaderContentLength), StatusCode::JSON_SERIALIZATION_ERROR);
}

TEST_F(KFSMakeJsonFromPredictResponseRawTest, IntOutputsUnaffectedByFiniteCheck) {
// sanity: integer outputs must still serialize fine (helper returns true for them)
ASSERT_EQ(makeJsonFromPredictResponse(proto, &json, inferenceHeaderContentLength), StatusCode::OK);
}

TEST_F(KFSMakeJsonFromPredictResponseRawTest, EmptyRawOutputContentsError) {
proto.mutable_raw_output_contents()->Clear();
EXPECT_EQ(makeJsonFromPredictResponse(proto, &json, inferenceHeaderContentLength), StatusCode::REST_SERIALIZE_VAL_FIELD_INVALID_SIZE);
Expand Down