diff --git a/include/quarry/runtime_c/generic_brf.h b/include/quarry/runtime_c/generic_brf.h index f58fa3e..214f0ea 100644 --- a/include/quarry/runtime_c/generic_brf.h +++ b/include/quarry/runtime_c/generic_brf.h @@ -235,6 +235,10 @@ quarry_generic_status_t quarry_qbs_get_string(const quarry_qbs_view_t*, uint16_t quarry_generic_status_t quarry_qbs_record_name(const quarry_qbs_view_t*, const quarry_qbs_record_view_t*, quarry_string_view_t*); +/* Returns a non-owning view into the mandatory QBS identity string section. */ +quarry_generic_status_t quarry_qbs_record_identity(const quarry_qbs_view_t*, + const quarry_qbs_record_view_t*, + quarry_string_view_t*); quarry_generic_status_t quarry_qbs_field_name(const quarry_qbs_view_t*, const quarry_qbs_record_view_t*, uint16_t, quarry_string_view_t*); diff --git a/runtime_c/generic_brf.c b/runtime_c/generic_brf.c index a782e49..e9f7429 100644 --- a/runtime_c/generic_brf.c +++ b/runtime_c/generic_brf.c @@ -278,6 +278,39 @@ quarry_generic_status_t quarry_qbs_record_name(const quarry_qbs_view_t* q, return quarry_qbs_get_string(q, record->name_index, out); } +quarry_generic_status_t quarry_qbs_record_identity(const quarry_qbs_view_t* q, + const quarry_qbs_record_view_t* record, + quarry_string_view_t* out) { + uint32_t offset; + size_t i; + bool belongs = false; + size_t start, length = 0U; + if (out != NULL) + *out = (quarry_string_view_t){NULL, 0U}; + if (q == NULL || record == NULL || out == NULL || q->bytes == NULL || q->records == NULL) + return QUARRY_GENERIC_INVALID_ARGUMENT; + for (i = 0U; i < q->record_count; ++i) { + if (&q->records[i] == record) { + belongs = true; + break; + } + } + if (!belongs) + return QUARRY_GENERIC_INVALID_ARGUMENT; + if (q->iss_offset > q->size || q->iss_size > q->size - q->iss_offset) + return QUARRY_GENERIC_MALFORMED_QBS; + offset = record->identity_offset; + if (offset >= q->iss_size) + return QUARRY_GENERIC_MALFORMED_QBS; + start = (size_t)q->iss_offset + (size_t)offset; + while (length < q->iss_size - (size_t)offset && q->bytes[start + length] != 0U) + ++length; + if (length == q->iss_size - (size_t)offset) + return QUARRY_GENERIC_MALFORMED_QBS; + *out = (quarry_string_view_t){(const char*)q->bytes + start, length}; + return QUARRY_GENERIC_OK; +} + quarry_generic_status_t quarry_qbs_field_name(const quarry_qbs_view_t* q, const quarry_qbs_record_view_t* record, uint16_t index, quarry_string_view_t* out) { diff --git a/tests/tools/brf_inspect_tool_test.cpp b/tests/tools/brf_inspect_tool_test.cpp index 547f82e..6dc81b0 100644 --- a/tests/tools/brf_inspect_tool_test.cpp +++ b/tests/tools/brf_inspect_tool_test.cpp @@ -62,3 +62,17 @@ TEST(BrfInspectToolTest, MalformedInputsFailWithoutStdout) { EXPECT_NE(result.find("BRF validation failed"), std::string::npos); std::filesystem::remove(bad); } + +TEST(BrfInspectToolTest, ListsReflectiveRecordsInQbsTableOrder) { + // The checked-in conformance QBS is reflective and exercises the public listing path. + const auto result = command("--qbs " + (fixture / "schema.qbs").string() + " --list-records"); + EXPECT_EQ(result, "0\n2 Child (Child)\n3 Item (Item)\n1 Parent (Parent)\n\n---ERR---\n"); + EXPECT_EQ(result.find("---ERR---\n\n"), std::string::npos); +} + +TEST(BrfInspectToolTest, ListingRejectsBrfAndSelectors) { + const std::string prefix = "--qbs " + (fixture / "schema.qbs").string() + " --list-records"; + EXPECT_NE(command(prefix + " --brf " + (fixture / "record.brf").string()).find("standalone QBS"), std::string::npos); + EXPECT_NE(command(prefix + " --record-id 1").find("standalone QBS"), std::string::npos); + EXPECT_NE(command(prefix + " --record-name Parent").find("standalone QBS"), std::string::npos); +} diff --git a/tools/README.md b/tools/README.md index 46f341b..57857a0 100644 --- a/tools/README.md +++ b/tools/README.md @@ -348,6 +348,22 @@ tool binary. It may still depend on system or package-manager-provided dynamic libraries such as libyaml, Protobuf, and absl according to the platform and build configuration. Those third-party libraries are not bundled by Quarry. +## Record discovery with `quarry-brf-inspect` + +To discover the records available in a QBS image, use the installed generic +BRF inspection tool without a BRF input: + +```sh +quarry-brf-inspect --qbs build/workload.qbs --list-records +``` + +This prints one record per line in validated QBS record-table order. Each line +starts with the exact numeric ID accepted by `--record-id`, followed by the +canonical record identity accepted by `--record-name`. Reflective QBS images +append the optional display name in parentheses; minimal QBS images still +provide the canonical identity. QBS has no runtime root-record concept, so all +records present in the image, including referenced records, are listed. + ## Protobuf translator toolchain The isolated `quarry-protobuf-translator` uses the protobuf descriptor-set diff --git a/tools/brf_inspect/main.cpp b/tools/brf_inspect/main.cpp index 37fb61a..78d8625 100644 --- a/tools/brf_inspect/main.cpp +++ b/tools/brf_inspect/main.cpp @@ -26,6 +26,7 @@ void usage(FILE* out) { " --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" + " --list-records List QBS records and exit (standalone mode)\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"); @@ -71,10 +72,11 @@ void error_status(const char* what, quarry_generic_status_t 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; + bool have_id = false, have_name = false, list_records = false; for (int i = 1; i < argc; ++i) { const std::string a = argv[i]; if (a == "-h" || a == "--help") { usage(stdout); return 0; } + if (a == "--list-records") { if (list_records) { std::fprintf(stderr, "quarry-brf-inspect: duplicate --list-records option\n"); return 2; } list_records = true; continue; } 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; @@ -88,12 +90,16 @@ int main(int argc, char** argv) { 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)) { + if (list_records && (qbs_path.empty() || !brf_path.empty() || have_id || have_name)) { + std::fprintf(stderr, "quarry-brf-inspect: --list-records is a standalone QBS discovery mode and cannot be combined with BRF input or record selectors\n"); + return 2; + } + if (!list_records && (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)) { + if (!read_input(qbs_path, qbs_bytes) || (!list_records && !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); @@ -107,6 +113,23 @@ int main(int argc, char** argv) { 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; } + if (list_records) { + for (size_t index = 0U; index < schema.record_count; ++index) { + const quarry_qbs_record_view_t* listed = &schema.records[index]; + quarry_string_view_t identity{}; + status = quarry_qbs_record_identity(&schema, listed, &identity); + if (status != QUARRY_GENERIC_OK) { error_status("record identity lookup failed", status); return 4; } + std::printf("%" PRIu32 " %.*s", listed->record_id, static_cast(identity.size), identity.data); + quarry_string_view_t display{}; + status = quarry_qbs_record_name(&schema, listed, &display); + if (status == QUARRY_GENERIC_OK) + std::printf(" (%.*s)", static_cast(display.size), display.data); + else if (status != QUARRY_GENERIC_FIELD_ABSENT) + { error_status("record display-name lookup failed", status); return 4; } + std::putchar('\n'); + } + return 0; + } 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; }