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
1 change: 1 addition & 0 deletions CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -78,6 +78,7 @@ option(JSONCPP_STATIC_WINDOWS_RUNTIME "Use static (MT/MTd) Windows runtime" OFF)
option(BUILD_SHARED_LIBS "Build jsoncpp_lib as a shared library." ON)
option(BUILD_STATIC_LIBS "Build jsoncpp_lib as a static library." ON)
option(BUILD_OBJECT_LIBS "Build jsoncpp_lib as a object library." ON)
option(JSONCPP_WITH_FUZZING "Build the jsoncpp_fuzzer fuzz target (needs a fuzzing engine)" OFF)

# Adhere to GNU filesystem layout conventions
include(GNUInstallDirs)
Expand Down
2 changes: 2 additions & 0 deletions src/CMakeLists.txt
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
add_subdirectory(lib_json)
if(JSONCPP_WITH_TESTS)
add_subdirectory(jsontestrunner)
endif()
if(JSONCPP_WITH_TESTS OR JSONCPP_WITH_FUZZING)
add_subdirectory(test_lib_json)
endif()
43 changes: 43 additions & 0 deletions src/test_lib_json/CMakeLists.txt
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
# vim: et ts=4 sts=4 sw=4 tw=0

if(JSONCPP_WITH_TESTS)

add_executable(jsoncpp_test
jsontest.cpp
jsontest.h
Expand Down Expand Up @@ -37,3 +39,44 @@ if(JSONCPP_WITH_POST_BUILD_UNITTEST)
COMMAND ${CMAKE_CROSSCOMPILING_EMULATOR} $<TARGET_FILE:jsoncpp_test>
)
endif()

endif() # JSONCPP_WITH_TESTS

# Standalone fuzz target (used by OSS-Fuzz). The JSONCPP_WITH_FUZZING option
# is declared in the top-level CMakeLists.txt, so the fuzz target can also be
# built with -DJSONCPP_WITH_TESTS=OFF.
#
# Requires a fuzzing engine: compile everything with the engine's coverage
# instrumentation only (e.g. -DCMAKE_CXX_FLAGS="-fsanitize=fuzzer-no-link")
# and link the engine itself into the fuzz target
# (e.g. -DJSONCPP_FUZZ_ENGINE="-fsanitize=fuzzer"), so that the engine's
# main() is not pulled into compiler checks and other executables.
#
# The libprotobuf-mutator based fuzzer (jsoncpp_fuzz_proto.cc, json.proto,
# json_proto_converter.*) is built by OSS-Fuzz separately with LPM; the

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If the sources live here, I'd want CI to build them; otherwise they'll only break in OSS-Fuzz. A CIFuzz workflow would cover both fuzzers.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I can probably handle this as a follow up if needed.

# sources are kept here so they live with the other jsoncpp fuzzers.
set(JSONCPP_FUZZ_ENGINE "" CACHE STRING
"Fuzzing engine link flags for jsoncpp_fuzzer, e.g. -fsanitize=fuzzer")
if(JSONCPP_WITH_FUZZING)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nothing builds this target yet, since the OSS-Fuzz build.sh still compiles fuzz.cpp directly. Also, with this ON and JSONCPP_FUZZ_ENGINE empty, linking fails because fuzz.cpp has no main. Could we error out at configure time here (or require the engine flags)?

add_executable(jsoncpp_fuzzer
fuzz.cpp
fuzz.h
)
if(BUILD_SHARED_LIBS)
target_link_libraries(jsoncpp_fuzzer jsoncpp_lib)
else()
target_link_libraries(jsoncpp_fuzzer jsoncpp_static)
Comment on lines +65 to +68

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Object-only fuzz build fails

With BUILD_SHARED_LIBS=OFF, BUILD_STATIC_LIBS=OFF, BUILD_OBJECT_LIBS=ON, and tests disabled, enabling JSONCPP_WITH_FUZZING selects jsoncpp_static, which is never created. The fuzz target then fails to link even when a fuzzing engine is supplied. Select jsoncpp_object when only the object library is enabled.

Knowledge Base Used: CMake, Meson, and Bazel integration

endif()
if(JSONCPP_FUZZ_ENGINE)
set_target_properties(jsoncpp_fuzzer PROPERTIES
LINK_FLAGS "${JSONCPP_FUZZ_ENGINE}"
)
endif()
# Ship the dictionary next to the fuzzer binary.
add_custom_command(TARGET jsoncpp_fuzzer
POST_BUILD
COMMAND ${CMAKE_COMMAND} -E copy_if_different
${CMAKE_CURRENT_SOURCE_DIR}/fuzz.dict
$<TARGET_FILE_DIR:jsoncpp_fuzzer>/jsoncpp_fuzzer.dict
)
endif()
99 changes: 99 additions & 0 deletions src/test_lib_json/json.proto
Original file line number Diff line number Diff line change
@@ -0,0 +1,99 @@
// Copyright 2020 Google Inc.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
//
////////////////////////////////////////////////////////////////////////////////

// The proto definition for JSON format has been written based on
// http://www.ecma-international.org/publications/files/ECMA-ST/ECMA-404.pdf

syntax = "proto2";

package json_proto;

message JsonParseAPI {
required int32 settings = 1;
required JsonObject object_value = 2;
}

message JsonObject {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Since JsonObject has exactly one name/value, generated objects never have more than one member, so rejectDupKeys can never trigger and object-member handling barely gets exercised. JsonObject.settings is also unused.

required int32 settings = 1;
required string name = 2;
required JsonValue value = 3;
}

message JsonValue {
oneof value {
// Json value types:

// null: null, will be used when 'oneof' contains nothing

// object: another json object of any type
JsonObject object_value = 1;

// array: an array of values
ArrayValue array_value = 2;

// number: can be an integer, a float, an exponent
NumberValue number_value = 3;

// string: unicode string
StringValue string_value = 4;

// boolean: true or talse
BooleanValue boolean_value = 5;
}
}

message ArrayValue {
repeated JsonValue value = 1;
}

message NumberInteger {
required int64 value = 1;
}

message NumberFloat {
required double value = 1;
}

message NumberExponent {
required int32 base = 1;
required int32 exponent = 2;
required bool use_uppercase = 3;
}

message NumberExponentFrac {
required float base = 1;
required int32 exponent = 2;
required bool use_uppercase = 3;
}

message NumberValue {
required NumberInteger integer_value = 1;

// integer_value is used when oneof field below has nothing.
oneof value {
NumberFloat float_value = 2;
NumberExponent exponent_value = 3;
NumberExponentFrac exponent_frac_value = 4;
}
}

message StringValue {
required string value = 1;
}

message BooleanValue {
required bool value = 1;
}
158 changes: 158 additions & 0 deletions src/test_lib_json/json_proto_converter.cc
Original file line number Diff line number Diff line change
@@ -0,0 +1,158 @@
// Copyright 2020 Google Inc.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm not morally opposed to landing this PR, however introducing Apache licensed code has some licensing headaches that I would prefer to avoid. Can you relicense this as a new file matching JsonCpp's existing licensing? it is allowed under Google policy AFAIK.

//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
//
////////////////////////////////////////////////////////////////////////////////

#include "json_proto_converter.h"

#include <charconv>

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Floating-point std::to_chars needs C++17 and a fairly recent standard library (libc++ 14+, libstdc++ 11+). The library itself is C++11, and this only builds in OSS-Fuzz because build.sh seds the standard to 17.

#include <cmath>

namespace json_proto {

// Writes str as a quoted, JSON-escaped string, so that generated quotes,
// backslashes and control characters cannot break the surrounding structure.
void JsonProtoConverter::AppendEscapedString(const std::string& str) {
static const char kHexDigits[] = "0123456789abcdef";
data_ << '"';
for (char c : str) {
switch (c) {
case '"':
data_ << "\\\"";
break;
case '\\':
data_ << "\\\\";
break;
case '\b':
data_ << "\\b";
break;
case '\f':
data_ << "\\f";
break;
case '\n':
data_ << "\\n";
break;
case '\r':
data_ << "\\r";
break;
case '\t':
data_ << "\\t";
break;
default:
if (static_cast<unsigned char>(c) < 0x20) {
data_ << "\\u00" << kHexDigits[(c >> 4) & 0xF]
<< kHexDigits[c & 0xF];
} else {
data_ << c;
}
break;
}
}
data_ << '"';
}

void JsonProtoConverter::AppendArray(const ArrayValue& array_value) {
data_ << '[';
bool need_comma = false;
for (const auto& value : array_value.value()) {
// Trailing comma inside of an array makes JSON invalid, avoid adding that.
if (need_comma)
data_ << ',';
else
need_comma = true;

AppendValue(value);
}
data_ << ']';
}

// Writes value as a decimal number without an exponent: scientific notation
// would add a stray exponent to the generated number (e.g. "1e+20e3"), while
// std::fixed with the stream's default precision would truncate small values
// ("0.0001004" -> "0.000100"), collapsing distinct mutations. to_chars with
// chars_format::fixed writes the shortest fixed-point string that round-trips
// the value, avoiding both. Non-finite values have no decimal form; write 0
// instead ("inf"/"nan" text would not be a valid JSON token) so the rest of
// the generated structure remains parseable.
void JsonProtoConverter::AppendPlainFloat(float value) {
if (!std::isfinite(value)) {
data_ << '0';
return;
}
char buf[96]; // shortest round-trip fixed form of a float fits comfortably
const auto result =
std::to_chars(buf, buf + sizeof(buf), value, std::chars_format::fixed);
if (result.ec == std::errc()) {
data_.write(buf, result.ptr - buf);
} else {
data_ << '0';
}
}

void JsonProtoConverter::AppendNumber(const NumberValue& number_value) {
if (number_value.has_float_value()) {
const double value = number_value.float_value().value();
// Keep "inf"/"nan" out of the output; they are not valid JSON numbers.
data_ << (std::isfinite(value) ? value : 0.0);

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This still writes doubles at the stream's default precision of 6 significant digits, which is the same digit loss that was fixed for exponent_frac_value above.

} else if (number_value.has_exponent_value()) {
auto value = number_value.exponent_value();
data_ << value.base();
data_ << (value.use_uppercase() ? 'E' : 'e');
data_ << value.exponent();
} else if (number_value.has_exponent_frac_value()) {
auto value = number_value.exponent_frac_value();
Comment thread
arthurscchan marked this conversation as resolved.
AppendPlainFloat(value.base());
data_ << (value.use_uppercase() ? 'E' : 'e');
data_ << value.exponent();
} else {
data_ << number_value.integer_value().value();
}
}

void JsonProtoConverter::AppendObject(const JsonObject& json_object) {
data_ << '{';
AppendEscapedString(json_object.name());
data_ << ':';
AppendValue(json_object.value());
data_ << '}';
}

void JsonProtoConverter::AppendValue(const JsonValue& json_value) {
if (json_value.has_object_value()) {
AppendObject(json_value.object_value());
} else if (json_value.has_array_value()) {
AppendArray(json_value.array_value());
} else if (json_value.has_number_value()) {
AppendNumber(json_value.number_value());
} else if (json_value.has_string_value()) {
AppendEscapedString(json_value.string_value().value());
} else if (json_value.has_boolean_value()) {
data_ << (json_value.boolean_value().value() ? "true" : "false");
} else {
data_ << "null";
}
}

std::string JsonProtoConverter::Convert(const JsonObject& json_object) {
AppendObject(json_object);
return data_.str();
}

std::string JsonProtoConverter::Convert(
const json_proto::ArrayValue& json_array) {
AppendArray(json_array);
return data_.str();
}

} // namespace json_proto
45 changes: 45 additions & 0 deletions src/test_lib_json/json_proto_converter.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
// Copyright 2020 Google Inc.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
//
////////////////////////////////////////////////////////////////////////////////

#ifndef JSON_PROTO_CONVERTER_H_
#define JSON_PROTO_CONVERTER_H_

#include <sstream>
#include <string>

#include "json.pb.h"

namespace json_proto {

class JsonProtoConverter {
public:
std::string Convert(const json_proto::JsonObject&);
std::string Convert(const json_proto::ArrayValue&);

private:
std::stringstream data_;

void AppendArray(const json_proto::ArrayValue&);
void AppendEscapedString(const std::string& str);
void AppendNumber(const json_proto::NumberValue&);
void AppendPlainFloat(float value);
void AppendObject(const json_proto::JsonObject&);
void AppendValue(const json_proto::JsonValue&);
};

} // namespace json_proto

#endif // TESTING_LIBFUZZER_PROTO_JSON_PROTO_CONVERTER_H_

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: the comment doesn't match the guard name (JSON_PROTO_CONVERTER_H_).

Loading