/* * Copyright (c) 2022, the SerenityOS developers. * * SPDX-License-Identifier: BSD-2-Clause */ #include #include #include enum class PnpIdColumns { ManufacturerName, ManufacturerId, ApprovalDate, ColumnCount // Must be last }; struct ApprovalDate { unsigned year; unsigned month; unsigned day; }; struct PnpIdData { String manufacturer_name; ApprovalDate approval_date; }; static ErrorOr decode_html_entities(StringView const& str) { static constexpr struct { StringView entity_name; StringView value; } s_html_entities[] = { { "amp"sv, "&"sv }, }; StringBuilder decoded_str; size_t start = 0; for (;;) { auto entity_start = str.find('&', start); if (!entity_start.has_value()) { decoded_str.append(str.substring_view(start)); break; } auto entity_end = str.find(';', entity_start.value() + 1); if (!entity_end.has_value() || entity_end.value() == entity_start.value() + 1) { decoded_str.append(str.substring_view(start, entity_start.value() - start + 1)); start = entity_start.value() + 1; continue; } if (str[entity_start.value() + 1] == '#') { auto entity_number = str.substring_view(entity_start.value() + 2, entity_end.value() - entity_start.value() - 2).to_uint(); if (!entity_number.has_value()) { decoded_str.append(str.substring_view(start, entity_end.value() - start + 1)); start = entity_end.value() + 1; continue; } if (entity_start.value() != start) decoded_str.append(str.substring_view(start, entity_start.value() - start)); decoded_str.append_code_point(entity_number.value()); } else { auto entity_name = str.substring_view(entity_start.value() + 1, entity_end.value() - entity_start.value() - 1); bool found_entity = false; for (auto& html_entity : s_html_entities) { if (html_entity.entity_name == entity_name) { found_entity = true; if (entity_start.value() != start) decoded_str.append(str.substring_view(start, entity_start.value() - start)); decoded_str.append(html_entity.value); break; } } if (!found_entity) return Error::from_string_literal("Failed to decode html entity"); if (entity_start.value() != start) decoded_str.append(str.substring_view(start, entity_start.value() - start)); } start = entity_end.value() + 1; } return decoded_str.build(); } static ErrorOr parse_approval_date(StringView const& str) { auto parts = str.trim_whitespace().split_view('/', SplitBehavior::KeepEmpty); if (parts.size() != 3) return Error::from_string_literal("Failed to parse approval date parts (mm/dd/yyyy)"); auto month = parts[0].to_uint(); if (!month.has_value()) return Error::from_string_literal("Failed to parse month from approval date"); if (month.value() == 0 || month.value() > 12) return Error::from_string_literal("Invalid month in approval date"); auto day = parts[1].to_uint(); if (!day.has_value()) return Error::from_string_literal("Failed to parse day from approval date"); if (day.value() == 0 || day.value() > 31) return Error::from_string_literal("Invalid day in approval date"); auto year = parts[2].to_uint(); if (!year.has_value()) return Error::from_string_literal("Failed to parse year from approval date"); if (year.value() < 1900 || year.value() > 2999) return Error::from_string_literal("Invalid year approval date"); return ApprovalDate { .year = year.value(), .month = month.value(), .day = day.value() }; } static ErrorOr> parse_pnp_ids_database(Core::Stream::File& pnp_ids_file) { auto pnp_ids_file_bytes = TRY(pnp_ids_file.read_all()); StringView pnp_ids_file_contents(pnp_ids_file_bytes); HashMap pnp_id_data; for (size_t row_content_offset = 0;;) { static auto const row_start_tag = ""sv, row_start.value() + row_start_tag.length()); if (!row_start_tag_end.has_value()) return Error::from_string_literal("Incomplete row start tag"); static auto const row_end_tag = ""sv; auto row_end = pnp_ids_file_contents.find(row_end_tag, row_start.value()); if (!row_end.has_value()) return Error::from_string_literal("No matching row end tag found"); if (row_start_tag_end.value() > row_end.value() + row_end_tag.length()) return Error::from_string_literal("Invalid row start tag"); auto row_string = pnp_ids_file_contents.substring_view(row_start_tag_end.value() + 1, row_end.value() - row_start_tag_end.value() - 1); Vector columns; for (size_t column_row_offset = 0;;) { static auto const column_start_tag = ""sv; auto column_start = row_string.find(column_start_tag, column_row_offset); if (!column_start.has_value()) break; static auto const column_end_tag = ""sv; auto column_end = row_string.find(column_end_tag, column_start.value() + column_start_tag.length()); if (!column_end.has_value()) return Error::from_string_literal("No matching column end tag found"); auto column_content_row_offset = column_start.value() + column_start_tag.length(); auto column_str = row_string.substring_view(column_content_row_offset, column_end.value() - column_content_row_offset).trim_whitespace(); if (column_str.find('\"').has_value()) return Error::from_string_literal("Found '\"' in column content, escaping not supported!"); columns.append(column_str); column_row_offset = column_end.value() + column_end_tag.length(); } if (columns.size() != (size_t)PnpIdColumns::ColumnCount) return Error::from_string_literal("Unexpected number of columns found"); auto approval_date = TRY(parse_approval_date(columns[(size_t)PnpIdColumns::ApprovalDate])); auto decoded_manufacturer_name = TRY(decode_html_entities(columns[(size_t)PnpIdColumns::ManufacturerName])); auto hash_set_result = pnp_id_data.set(columns[(size_t)PnpIdColumns::ManufacturerId], PnpIdData { .manufacturer_name = decoded_manufacturer_name, .approval_date = move(approval_date) }); if (hash_set_result != AK::HashSetResult::InsertedNewEntry) return Error::from_string_literal("Duplicate manufacturer ID encountered"); row_content_offset = row_end.value() + row_end_tag.length(); } if (pnp_id_data.size() <= 1) return Error::from_string_literal("Expected more than one row"); return pnp_id_data; } static ErrorOr generate_header(Core::Stream::File& file, HashMap const& pnp_ids) { StringBuilder builder; SourceGenerator generator { builder }; generator.set("pnp_id_count", String::formatted("{}", pnp_ids.size())); generator.append(R"~~~( #pragma once #include #include #include namespace PnpIDs { struct PnpIDData { StringView manufacturer_id; StringView manufacturer_name; struct { u16 year{}; u8 month{}; u8 day{}; } approval_date; }; Optional find_by_manufacturer_id(StringView); IterationDecision for_each(Function); static constexpr size_t count = @pnp_id_count@; } )~~~"); TRY(file.write(generator.as_string_view().bytes())); return {}; } static ErrorOr generate_source(Core::Stream::File& file, HashMap const& pnp_ids) { StringBuilder builder; SourceGenerator generator { builder }; generator.append(R"~~~( #include "PnpIDs.h" namespace PnpIDs { static constexpr PnpIDData s_pnp_ids[] = { )~~~"); for (auto& pnp_id_data : pnp_ids) { generator.set("manufacturer_id", pnp_id_data.key); generator.set("manufacturer_name", pnp_id_data.value.manufacturer_name); generator.set("approval_year", String::formatted("{}", pnp_id_data.value.approval_date.year)); generator.set("approval_month", String::formatted("{}", pnp_id_data.value.approval_date.month)); generator.set("approval_day", String::formatted("{}", pnp_id_data.value.approval_date.day)); generator.append(R"~~~( { "@manufacturer_id@"sv, "@manufacturer_name@"sv, { @approval_year@, @approval_month@, @approval_day@ } }, )~~~"); } generator.append(R"~~~( }; Optional find_by_manufacturer_id(StringView manufacturer_id) { for (auto& pnp_data : s_pnp_ids) { if (pnp_data.manufacturer_id == manufacturer_id) return pnp_data; } return {}; } IterationDecision for_each(Function callback) { for (auto& pnp_data : s_pnp_ids) { auto decision = callback(pnp_data); if (decision != IterationDecision::Continue) return decision; } return IterationDecision::Continue; } } )~~~"); TRY(file.write(generator.as_string_view().bytes())); return {}; } ErrorOr serenity_main(Main::Arguments arguments) { StringView generated_header_path; StringView generated_implementation_path; StringView pnp_ids_file_path; Core::ArgsParser args_parser; args_parser.add_option(generated_header_path, "Path to the header file to generate", "generated-header-path", 'h', "generated-header-path"); args_parser.add_option(generated_implementation_path, "Path to the implementation file to generate", "generated-implementation-path", 'c', "generated-implementation-path"); args_parser.add_option(pnp_ids_file_path, "Path to the input PNP ID database file", "pnp-ids-file", 'p', "pnp-ids-file"); args_parser.parse(arguments); auto open_file = [&](StringView path, Core::Stream::OpenMode mode = Core::Stream::OpenMode::Read) -> ErrorOr> { if (path.is_empty()) { args_parser.print_usage(stderr, arguments.argv[0]); return Error::from_string_literal("Must provide all command line options"); } return Core::Stream::File::open(path, mode); }; auto generated_header_file = TRY(open_file(generated_header_path, Core::Stream::OpenMode::ReadWrite)); auto generated_implementation_file = TRY(open_file(generated_implementation_path, Core::Stream::OpenMode::ReadWrite)); auto pnp_ids_file = TRY(open_file(pnp_ids_file_path)); auto pnp_id_map = TRY(parse_pnp_ids_database(*pnp_ids_file)); TRY(generate_header(*generated_header_file, pnp_id_map)); TRY(generate_source(*generated_implementation_file, pnp_id_map)); return 0; }