diff --git a/Meta/CMake/pnp_ids.cmake b/Meta/CMake/pnp_ids.cmake new file mode 100644 index 0000000000..4ee371f8f2 --- /dev/null +++ b/Meta/CMake/pnp_ids.cmake @@ -0,0 +1,31 @@ +include(${CMAKE_CURRENT_LIST_DIR}/utils.cmake) + +set(PNP_IDS_FILE pnp.ids) +set(PNP_IDS_URL http://www.uefi.org/uefi-pnp-export) +set(PNP_IDS_EXPORT_PATH ${CMAKE_BINARY_DIR}/pnp.ids.html) +set(PNP_IDS_INSTALL_PATH ${CMAKE_INSTALL_DATAROOTDIR}/${PNP_IDS_FILE}) + +if(ENABLE_PNP_IDS_DOWNLOAD AND NOT EXISTS ${PNP_IDS_PATH}) + message(STATUS "Downloading PNP ID database from ${PNP_IDS_URL}...") + file(MAKE_DIRECTORY ${CMAKE_INSTALL_DATAROOTDIR}) + file(DOWNLOAD ${PNP_IDS_URL} ${PNP_IDS_EXPORT_PATH} INACTIVITY_TIMEOUT 10) + + set(PNP_IDS_HEADER LibEDID/PnpIDs.h) + set(PNP_IDS_IMPLEMENTATION LibEDID/PnpIDs.cpp) + set(PNP_IDS_TARGET_PREFIX LibEDID_) + + invoke_generator( + "PnpIDsData" + Lagom::GeneratePnpIDsData + "${PNP_IDS_EXPORT_PATH}" + "${PNP_IDS_TARGET_PREFIX}" + "${PNP_IDS_HEADER}" + "${PNP_IDS_IMPLEMENTATION}" + arguments -p "${PNP_IDS_EXPORT_PATH}" + ) + + set(PNP_IDS_SOURCES + ${PNP_IDS_HEADER} + ${PNP_IDS_IMPLEMENTATION} + ) +endif() diff --git a/Meta/CMake/serenity_options.cmake b/Meta/CMake/serenity_options.cmake index f5cbc5dec0..7c74bf1a08 100644 --- a/Meta/CMake/serenity_options.cmake +++ b/Meta/CMake/serenity_options.cmake @@ -6,6 +6,7 @@ include(${CMAKE_CURRENT_LIST_DIR}/common_options.cmake) serenity_option(ENABLE_PCI_IDS_DOWNLOAD ON CACHE BOOL "Enable download of the pci.ids database at build time") serenity_option(ENABLE_USB_IDS_DOWNLOAD ON CACHE BOOL "Enable download of the usb.ids database at build time") +serenity_option(ENABLE_PNP_IDS_DOWNLOAD ON CACHE BOOL "Enable download of the pnp.ids database at build time") serenity_option(ENABLE_KERNEL_ADDRESS_SANITIZER OFF CACHE BOOL "Enable kernel address sanitizer testing in gcc/clang") serenity_option(ENABLE_KERNEL_COVERAGE_COLLECTION OFF CACHE BOOL "Enable KCOV and kernel coverage instrumentation in gcc/clang") serenity_option(ENABLE_KERNEL_LTO OFF CACHE BOOL "Build the kernel with link-time optimization") diff --git a/Meta/Lagom/Tools/CodeGenerators/CMakeLists.txt b/Meta/Lagom/Tools/CodeGenerators/CMakeLists.txt index 66d33a0599..ce4975ec67 100644 --- a/Meta/Lagom/Tools/CodeGenerators/CMakeLists.txt +++ b/Meta/Lagom/Tools/CodeGenerators/CMakeLists.txt @@ -1,4 +1,5 @@ add_subdirectory(IPCCompiler) +add_subdirectory(LibEDID) add_subdirectory(LibTimeZone) add_subdirectory(LibUnicode) add_subdirectory(LibWeb) diff --git a/Meta/Lagom/Tools/CodeGenerators/LibEDID/CMakeLists.txt b/Meta/Lagom/Tools/CodeGenerators/LibEDID/CMakeLists.txt new file mode 100644 index 0000000000..4c877ca829 --- /dev/null +++ b/Meta/Lagom/Tools/CodeGenerators/LibEDID/CMakeLists.txt @@ -0,0 +1 @@ +lagom_tool(GeneratePnpIDsData SOURCES GeneratePnpIDs.cpp LIBS LagomMain) diff --git a/Meta/Lagom/Tools/CodeGenerators/LibEDID/GeneratePnpIDs.cpp b/Meta/Lagom/Tools/CodeGenerators/LibEDID/GeneratePnpIDs.cpp new file mode 100644 index 0000000000..7aee4e4ab6 --- /dev/null +++ b/Meta/Lagom/Tools/CodeGenerators/LibEDID/GeneratePnpIDs.cpp @@ -0,0 +1,301 @@ +/* + * Copyright (c) 2022, the SerenityOS developers. + * + * SPDX-License-Identifier: BSD-2-Clause + */ + +#include +#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"sv); + + 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('/', true); + if (parts.size() != 3) + return Error::from_string_literal("Failed to parse approval date parts (mm/dd/yyyy)"sv); + + auto month = parts[0].to_uint(); + if (!month.has_value()) + return Error::from_string_literal("Failed to parse month from approval date"sv); + if (month.value() == 0 || month.value() > 12) + return Error::from_string_literal("Invalid month in approval date"sv); + + auto day = parts[1].to_uint(); + if (!day.has_value()) + return Error::from_string_literal("Failed to parse day from approval date"sv); + if (day.value() == 0 || day.value() > 31) + return Error::from_string_literal("Invalid day in approval date"sv); + + auto year = parts[2].to_uint(); + if (!year.has_value()) + return Error::from_string_literal("Failed to parse year from approval date"sv); + if (year.value() < 1900 || year.value() > 2999) + return Error::from_string_literal("Invalid year approval date"sv); + + return ApprovalDate { .year = year.value(), .month = month.value(), .day = day.value() }; +} + +static ErrorOr> parse_pnp_ids_database(Core::File& pnp_ids_file) +{ + auto pnp_ids_file_bytes = 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 const auto 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"sv); + + static const auto 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"sv); + + if (row_start_tag_end.value() > row_end.value() + row_end_tag.length()) + return Error::from_string_literal("Invalid row start tag"sv); + + 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 const auto column_start_tag = ""sv; + auto column_start = row_string.find(column_start_tag, column_row_offset); + if (!column_start.has_value()) + break; + + static const auto 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"sv); + + 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!"sv); + 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"sv); + + 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"sv); + + 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"sv); + + return pnp_id_data; +} + +static void generate_header(Core::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@; +} +)~~~"); + + VERIFY(file.write(generator.as_string_view())); +} + +static void generate_source(Core::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; +} + +} +)~~~"); + + VERIFY(file.write(generator.as_string_view())); +} + +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::OpenMode mode = Core::OpenMode::ReadOnly) -> ErrorOr> { + if (path.is_empty()) { + args_parser.print_usage(stderr, arguments.argv[0]); + return Error::from_string_literal("Must provide all command line options"sv); + } + + return Core::File::open(path, mode); + }; + + auto generated_header_file = TRY(open_file(generated_header_path, Core::OpenMode::ReadWrite)); + auto generated_implementation_file = TRY(open_file(generated_implementation_path, Core::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)); + + generate_header(*generated_header_file, pnp_id_map); + generate_source(*generated_implementation_file, pnp_id_map); + return 0; +} diff --git a/Userland/Libraries/LibEDID/CMakeLists.txt b/Userland/Libraries/LibEDID/CMakeLists.txt index a3f981385f..e50d8022d1 100644 --- a/Userland/Libraries/LibEDID/CMakeLists.txt +++ b/Userland/Libraries/LibEDID/CMakeLists.txt @@ -1,8 +1,12 @@ +include(${SerenityOS_SOURCE_DIR}/Meta/CMake/pnp_ids.cmake) + set(SOURCES DMT.cpp EDID.cpp VIC.cpp + ${PNP_IDS_SOURCES} ) serenity_lib(LibEDID edid) target_link_libraries(LibEDID LibC) +target_compile_definitions(LibEDID PRIVATE ENABLE_PNP_IDS_DATA=$) diff --git a/Userland/Libraries/LibEDID/EDID.cpp b/Userland/Libraries/LibEDID/EDID.cpp index ecf333fbed..c8ee308481 100644 --- a/Userland/Libraries/LibEDID/EDID.cpp +++ b/Userland/Libraries/LibEDID/EDID.cpp @@ -13,6 +13,10 @@ # include # include # include + +# ifdef ENABLE_PNP_IDS_DATA +# include +# endif #endif namespace EDID { @@ -552,6 +556,18 @@ String Parser::legacy_manufacturer_id() const return id; } +#ifndef KERNEL +String Parser::manufacturer_name() const +{ + auto manufacturer_id = legacy_manufacturer_id(); +# ifdef ENABLE_PNP_IDS_DATA + if (auto pnp_id_data = PnpIDs::find_by_manufacturer_id(manufacturer_id); pnp_id_data.has_value()) + return pnp_id_data.value().manufacturer_name; +# endif + return manufacturer_id; +} +#endif + u16 Parser::product_code() const { return read_le(&raw_edid().vendor.product_code); diff --git a/Userland/Libraries/LibEDID/EDID.h b/Userland/Libraries/LibEDID/EDID.h index da966708cd..5cb1317164 100644 --- a/Userland/Libraries/LibEDID/EDID.h +++ b/Userland/Libraries/LibEDID/EDID.h @@ -83,6 +83,10 @@ public: #endif String legacy_manufacturer_id() const; +#ifndef KERNEL + String manufacturer_name() const; +#endif + u16 product_code() const; u32 serial_number() const;