diff --git a/tests/CMakeLists.txt b/tests/CMakeLists.txt index 79a9aca..a590b08 100644 --- a/tests/CMakeLists.txt +++ b/tests/CMakeLists.txt @@ -249,6 +249,11 @@ target_compile_definitions(schema_compiler_tool_test PRIVATE QUARRY_TEST_GENERATED_INCLUDE_DIR="${QUARRY_GENERATED_INCLUDE_DIR}") target_compile_definitions(schema_compiler_tool_test PRIVATE QUARRY_TEST_SOURCE_DIR="${CMAKE_SOURCE_DIR}") +add_quarry_gtest(brf_inspect_tool_test tools/brf_inspect_tool_test.cpp quarry_compiler_support) +add_dependencies(brf_inspect_tool_test quarry_brf_inspect) +target_compile_definitions(brf_inspect_tool_test PRIVATE + QUARRY_BRF_INSPECT_TOOL="$" + QUARRY_TEST_SOURCE_DIR="${CMAKE_SOURCE_DIR}") add_quarry_gtest(protobuf_descriptor_loader_test tools/protobuf_descriptor_loader_test.cpp quarry_protobuf_descriptor_loader) diff --git a/tests/tools/brf_inspect_tool_test.cpp b/tests/tools/brf_inspect_tool_test.cpp new file mode 100644 index 0000000..547f82e --- /dev/null +++ b/tests/tools/brf_inspect_tool_test.cpp @@ -0,0 +1,64 @@ +#include +#include +#include +#include +#include + +#include + +#ifndef QUARRY_BRF_INSPECT_TOOL +#error "QUARRY_BRF_INSPECT_TOOL must be defined" +#endif +#ifndef QUARRY_TEST_SOURCE_DIR +#error "QUARRY_TEST_SOURCE_DIR must be defined" +#endif + +namespace { +const std::filesystem::path fixture = std::filesystem::path{QUARRY_TEST_SOURCE_DIR} / + "tests/fixtures/generic_runtime_conformance"; +std::string command(const std::string& args, const std::filesystem::path& input = {}) { + const auto dir = std::filesystem::temp_directory_path() / "quarry-brf-inspect-test"; + std::filesystem::create_directories(dir); + const auto out = dir / "out", err = dir / "err"; + std::string shell = std::string{"\""} + QUARRY_BRF_INSPECT_TOOL + "\" " + args + + " >\"" + out.string() + "\" 2>\"" + err.string() + "\""; + if (!input.empty()) shell += " <\"" + input.string() + "\""; + const int status = std::system(shell.c_str()); + std::ifstream o(out), e(err); + return std::to_string(status) + "\n" + + std::string{std::istreambuf_iterator(o), {}} + "\n---ERR---\n" + + std::string{std::istreambuf_iterator(e), {}}; +} +} + +TEST(BrfInspectToolTest, ReflectiveRecordIdIsDeterministic) { + const std::string args = "--qbs " + (fixture / "schema.qbs").string() + " --brf " + + (fixture / "record.brf").string() + " --record-id 1"; + EXPECT_EQ(command(args), command(args)); + EXPECT_NE(command(args).find("Parent {"), std::string::npos); +} +TEST(BrfInspectToolTest, ReflectiveRecordNameAndStdinWork) { + const std::string args = "--qbs " + (fixture / "schema.qbs").string() + + " --brf - --record-name Parent --indent-width 4 --max-output-bytes 4096"; + const auto result = command(args, fixture / "record.brf"); + EXPECT_NE(result.find("Parent {"), std::string::npos); + EXPECT_NE(result.find(" sequence: 42"), std::string::npos); +} +TEST(BrfInspectToolTest, MissingOrAmbiguousSelectorFailsOnStderr) { + const std::string prefix = "--qbs " + (fixture / "schema.qbs").string() + " --brf " + + (fixture / "record.brf").string(); + const auto missing = command(prefix); + EXPECT_NE(missing.find("---ERR---\n"), std::string::npos); + EXPECT_EQ(missing.find("Parent {"), std::string::npos); + EXPECT_NE(command(prefix + " --record-id 1 --record-name Parent").find("---ERR---"), std::string::npos); +} +TEST(BrfInspectToolTest, MalformedInputsFailWithoutStdout) { + const auto bad = std::filesystem::temp_directory_path() / "quarry-bad-brf"; + std::ofstream{bad} << "not BRF"; + const std::string args = "--qbs " + (fixture / "schema.qbs").string() + " --brf " + + bad.string() + " --record-id 1"; + const auto result = command(args); + EXPECT_EQ(result.find("Parent {"), std::string::npos); + EXPECT_NE(result.find("BRF validation failed"), std::string::npos); + std::filesystem::remove(bad); +} diff --git a/tools/CMakeLists.txt b/tools/CMakeLists.txt index 48641f6..6a2a7f7 100644 --- a/tools/CMakeLists.txt +++ b/tools/CMakeLists.txt @@ -31,3 +31,8 @@ install(TARGETS quarry_schema_compiler ) add_subdirectory(schema-translators/protobuf) + +add_executable(quarry_brf_inspect brf_inspect/main.cpp) +set_target_properties(quarry_brf_inspect PROPERTIES OUTPUT_NAME quarry-brf-inspect) +target_link_libraries(quarry_brf_inspect PRIVATE quarry_runtime_c) +install(TARGETS quarry_brf_inspect RUNTIME DESTINATION ${CMAKE_INSTALL_BINDIR}) diff --git a/tools/brf_inspect/main.cpp b/tools/brf_inspect/main.cpp new file mode 100644 index 0000000..37fb61a --- /dev/null +++ b/tools/brf_inspect/main.cpp @@ -0,0 +1,121 @@ +#include "quarry/runtime_c/generic_brf.h" + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +namespace { +constexpr size_t kMaxInput = 64U * 1024U * 1024U; +constexpr size_t kMaxEntries = 1U * 1024U * 1024U; + +void usage(FILE* out) { + std::fprintf(out, + "usage: quarry-brf-inspect --qbs PATH --brf PATH " + "(--record-id ID | --record-name NAME) [options]\n\n" + "Options:\n" + " --qbs PATH QBS schema image\n" + " --brf PATH BRF record image, or - for stdin\n" + " --record-id ID Select the record by numeric ID\n" + " --record-name NAME Select the record by reflective name\n" + " --indent-width N Spaces per nesting level (default: 2)\n" + " --max-output-bytes N Output limit (default: unlimited)\n" + " -h, --help Show this help text\n"); +} + +bool number(const char* text, size_t* out) { + if (!text || !*text || text[0] == '-') return false; + char* end = nullptr; errno = 0; + const unsigned long long value = std::strtoull(text, &end, 10); + if (errno || end == text || *end || value > SIZE_MAX) return false; + *out = static_cast(value); return true; +} +bool read_input(const std::string& path, std::vector& out) { + std::istream* input = nullptr; std::ifstream file; + if (path == "-") { input = &std::cin; } + else { file.open(path, std::ios::binary); input = &file; } + if (!*input) return false; + if (path == "-") { + std::vector bytes((std::istreambuf_iterator(*input)), {}); + if (bytes.size() > kMaxInput) return false; + out.assign(bytes.begin(), bytes.end()); + return !input->bad(); + } + input->seekg(0, std::ios::end); + const std::streamoff end = input->tellg(); + if (end < 0 || static_cast(end) > kMaxInput) return false; + input->seekg(0, std::ios::beg); + out.resize(static_cast(end)); + if (!out.empty()) input->read(reinterpret_cast(out.data()), static_cast(out.size())); + return static_cast(input->gcount()) == out.size(); +} +int write_stdout(const char* data, size_t size, void*) { + return std::fwrite(data, 1, size, stdout) == size ? 0 : 1; +} +size_t cap(size_t n, size_t divisor) { + const size_t value = n / divisor + 1U; + return value < kMaxEntries ? value : kMaxEntries; +} +void error_status(const char* what, quarry_generic_status_t status) { + std::fprintf(stderr, "quarry-brf-inspect: %s (status %d)\n", what, static_cast(status)); +} +} + +int main(int argc, char** argv) { + std::string qbs_path, brf_path, record_name; size_t record_id = 0, indent = 2, output_limit = SIZE_MAX; + bool have_id = false, have_name = false; + for (int i = 1; i < argc; ++i) { + const std::string a = argv[i]; + if (a == "-h" || a == "--help") { usage(stdout); return 0; } + auto value = [&](const char* option, std::string& dst) -> bool { + if (a != option || i + 1 >= argc || !*argv[i + 1]) return false; + dst = argv[++i]; return true; + }; + if (value("--qbs", qbs_path) || value("--brf", brf_path) || value("--record-name", record_name)) { + if (a == "--record-name") have_name = true; continue; + } + if (a == "--record-id" && i + 1 < argc && number(argv[i + 1], &record_id)) { ++i; have_id = true; continue; } + size_t parsed = 0; + if (a == "--indent-width" && i + 1 < argc && number(argv[i + 1], &parsed)) { indent = parsed; ++i; continue; } + if (a == "--max-output-bytes" && i + 1 < argc && number(argv[i + 1], &output_limit)) { ++i; continue; } + std::fprintf(stderr, "quarry-brf-inspect: invalid option or value: %s\n", a.c_str()); usage(stderr); return 2; + } + if (qbs_path.empty() || brf_path.empty() || have_id == have_name || + (have_id && record_id > UINT32_MAX)) { + std::fprintf(stderr, "quarry-brf-inspect: exactly one record selector is required\n"); usage(stderr); return 2; + } + std::vector qbs_bytes, brf_bytes; + if (!read_input(qbs_path, qbs_bytes) || !read_input(brf_path, brf_bytes)) { + std::fprintf(stderr, "quarry-brf-inspect: unable to read input\n"); return 3; + } + const size_t rc = cap(qbs_bytes.size(), 29), fc = cap(qbs_bytes.size(), 28), tc = cap(qbs_bytes.size(), 16), ec = cap(qbs_bytes.size(), 16); + const size_t work = cap(qbs_bytes.size() + brf_bytes.size(), 1); + std::vector records(rc); std::vector fields(fc); + std::vector types(tc); std::vector enums(ec); std::vector values(ec); + std::vector nodes(work); std::vector field_states(work); + std::vector maps(work), array_elements(work); std::vector children(work); + std::vector arrays(work); std::vector frames(work); + quarry_workspace_t ws{}; ws.records=records.data(); ws.record_capacity=rc; ws.fields=fields.data(); ws.field_capacity=fc; ws.types=types.data(); ws.type_capacity=tc; ws.enums=enums.data(); ws.enum_capacity=ec; ws.enum_values=values.data(); ws.enum_value_capacity=ec; ws.nodes=nodes.data(); ws.node_capacity=work; ws.field_states=field_states.data(); ws.field_state_capacity=work; ws.field_maps=maps.data(); ws.field_map_capacity=work; ws.children=children.data(); ws.child_capacity=work; ws.arrays=arrays.data(); ws.array_capacity=work; ws.array_elements=array_elements.data(); ws.array_element_capacity=work; ws.frames=frames.data(); ws.frame_capacity=work; + const quarry_generic_limits_t generic_limits{kMaxInput, kMaxInput, work, work, work}; + quarry_qbs_view_t schema{}; auto status = quarry_qbs_parse(qbs_bytes.data(), qbs_bytes.size(), &schema, &ws, &generic_limits); + if (status != QUARRY_GENERIC_OK) { error_status("QBS parsing failed", status); return 4; } + const quarry_qbs_record_view_t* record = nullptr; + status = have_id ? quarry_qbs_find_record_by_id(&schema, static_cast(record_id), &record) : quarry_qbs_find_record_by_name(&schema, record_name.c_str(), record_name.size(), &record); + if (status != QUARRY_GENERIC_OK) { error_status("record selection failed", status); return 5; } + quarry_brf_record_view_t view{}; status = quarry_brf_validate_with_workspace(&schema, record, brf_bytes.data(), brf_bytes.size(), &view, &ws, &generic_limits); + if (status != QUARRY_GENERIC_OK) { error_status("BRF validation failed", status); return 6; } + const size_t depth = work < 4096U ? work : 4096U; std::vector traversal(depth); std::vector print(depth); + quarry_brf_traversal_workspace_t tw{traversal.data(), depth, 0, 0}; quarry_brf_print_workspace_t pw{print.data(), depth, 0, 0}; + const quarry_brf_traversal_limits_t limits{work, depth}; const quarry_brf_print_options_t options{indent, output_limit}; + const auto result = quarry_brf_print(&view, write_stdout, nullptr, &pw, &tw, &limits, &options); + if (result != QUARRY_BRF_PRINT_COMPLETED) { std::fprintf(stderr, "quarry-brf-inspect: printing failed (status %d)\n", static_cast(result)); return 7; } + return 0; +}