Serenity Operating System
at master 302 lines 12 kB view raw
1/* 2 * Copyright (c) 2022, the SerenityOS developers. 3 * 4 * SPDX-License-Identifier: BSD-2-Clause 5 */ 6 7#include <AK/SourceGenerator.h> 8#include <LibCore/ArgsParser.h> 9#include <LibCore/File.h> 10 11enum class PnpIdColumns { 12 ManufacturerName, 13 ManufacturerId, 14 ApprovalDate, 15 16 ColumnCount // Must be last 17}; 18 19struct ApprovalDate { 20 unsigned year; 21 unsigned month; 22 unsigned day; 23}; 24 25struct PnpIdData { 26 DeprecatedString manufacturer_name; 27 ApprovalDate approval_date; 28}; 29 30static ErrorOr<DeprecatedString> decode_html_entities(StringView const& str) 31{ 32 static constexpr struct { 33 StringView entity_name; 34 StringView value; 35 } s_html_entities[] = { 36 { "amp"sv, "&"sv }, 37 }; 38 39 StringBuilder decoded_str; 40 size_t start = 0; 41 for (;;) { 42 auto entity_start = str.find('&', start); 43 if (!entity_start.has_value()) { 44 decoded_str.append(str.substring_view(start)); 45 break; 46 } 47 48 auto entity_end = str.find(';', entity_start.value() + 1); 49 if (!entity_end.has_value() || entity_end.value() == entity_start.value() + 1) { 50 decoded_str.append(str.substring_view(start, entity_start.value() - start + 1)); 51 start = entity_start.value() + 1; 52 continue; 53 } 54 55 if (str[entity_start.value() + 1] == '#') { 56 auto entity_number = str.substring_view(entity_start.value() + 2, entity_end.value() - entity_start.value() - 2).to_uint(); 57 if (!entity_number.has_value()) { 58 decoded_str.append(str.substring_view(start, entity_end.value() - start + 1)); 59 start = entity_end.value() + 1; 60 continue; 61 } 62 63 if (entity_start.value() != start) 64 decoded_str.append(str.substring_view(start, entity_start.value() - start)); 65 66 decoded_str.append_code_point(entity_number.value()); 67 } else { 68 auto entity_name = str.substring_view(entity_start.value() + 1, entity_end.value() - entity_start.value() - 1); 69 bool found_entity = false; 70 for (auto& html_entity : s_html_entities) { 71 if (html_entity.entity_name == entity_name) { 72 found_entity = true; 73 if (entity_start.value() != start) 74 decoded_str.append(str.substring_view(start, entity_start.value() - start)); 75 decoded_str.append(html_entity.value); 76 break; 77 } 78 } 79 80 if (!found_entity) 81 return Error::from_string_literal("Failed to decode html entity"); 82 83 if (entity_start.value() != start) 84 decoded_str.append(str.substring_view(start, entity_start.value() - start)); 85 } 86 87 start = entity_end.value() + 1; 88 } 89 return decoded_str.to_deprecated_string(); 90} 91 92static ErrorOr<ApprovalDate> parse_approval_date(StringView const& str) 93{ 94 auto parts = str.trim_whitespace().split_view('/', SplitBehavior::KeepEmpty); 95 if (parts.size() != 3) 96 return Error::from_string_literal("Failed to parse approval date parts (mm/dd/yyyy)"); 97 98 auto month = parts[0].to_uint(); 99 if (!month.has_value()) 100 return Error::from_string_literal("Failed to parse month from approval date"); 101 if (month.value() == 0 || month.value() > 12) 102 return Error::from_string_literal("Invalid month in approval date"); 103 104 auto day = parts[1].to_uint(); 105 if (!day.has_value()) 106 return Error::from_string_literal("Failed to parse day from approval date"); 107 if (day.value() == 0 || day.value() > 31) 108 return Error::from_string_literal("Invalid day in approval date"); 109 110 auto year = parts[2].to_uint(); 111 if (!year.has_value()) 112 return Error::from_string_literal("Failed to parse year from approval date"); 113 if (year.value() < 1900 || year.value() > 2999) 114 return Error::from_string_literal("Invalid year approval date"); 115 116 return ApprovalDate { .year = year.value(), .month = month.value(), .day = day.value() }; 117} 118 119static ErrorOr<HashMap<DeprecatedString, PnpIdData>> parse_pnp_ids_database(Core::File& pnp_ids_file) 120{ 121 auto pnp_ids_file_bytes = TRY(pnp_ids_file.read_until_eof()); 122 StringView pnp_ids_file_contents(pnp_ids_file_bytes); 123 124 HashMap<DeprecatedString, PnpIdData> pnp_id_data; 125 126 for (size_t row_content_offset = 0;;) { 127 static auto const row_start_tag = "<tr class=\""sv; 128 auto row_start = pnp_ids_file_contents.find(row_start_tag, row_content_offset); 129 if (!row_start.has_value()) 130 break; 131 132 auto row_start_tag_end = pnp_ids_file_contents.find(">"sv, row_start.value() + row_start_tag.length()); 133 if (!row_start_tag_end.has_value()) 134 return Error::from_string_literal("Incomplete row start tag"); 135 136 static auto const row_end_tag = "</tr>"sv; 137 auto row_end = pnp_ids_file_contents.find(row_end_tag, row_start.value()); 138 if (!row_end.has_value()) 139 return Error::from_string_literal("No matching row end tag found"); 140 141 if (row_start_tag_end.value() > row_end.value() + row_end_tag.length()) 142 return Error::from_string_literal("Invalid row start tag"); 143 144 auto row_string = pnp_ids_file_contents.substring_view(row_start_tag_end.value() + 1, row_end.value() - row_start_tag_end.value() - 1); 145 Vector<DeprecatedString, (size_t)PnpIdColumns::ColumnCount> columns; 146 for (size_t column_row_offset = 0;;) { 147 static auto const column_start_tag = "<td>"sv; 148 auto column_start = row_string.find(column_start_tag, column_row_offset); 149 if (!column_start.has_value()) 150 break; 151 152 static auto const column_end_tag = "</td>"sv; 153 auto column_end = row_string.find(column_end_tag, column_start.value() + column_start_tag.length()); 154 if (!column_end.has_value()) 155 return Error::from_string_literal("No matching column end tag found"); 156 157 auto column_content_row_offset = column_start.value() + column_start_tag.length(); 158 auto column_str = row_string.substring_view(column_content_row_offset, column_end.value() - column_content_row_offset).trim_whitespace(); 159 if (column_str.find('\"').has_value()) 160 return Error::from_string_literal("Found '\"' in column content, escaping not supported!"); 161 columns.append(column_str); 162 163 column_row_offset = column_end.value() + column_end_tag.length(); 164 } 165 166 if (columns.size() != (size_t)PnpIdColumns::ColumnCount) 167 return Error::from_string_literal("Unexpected number of columns found"); 168 169 auto approval_date = TRY(parse_approval_date(columns[(size_t)PnpIdColumns::ApprovalDate])); 170 auto decoded_manufacturer_name = TRY(decode_html_entities(columns[(size_t)PnpIdColumns::ManufacturerName])); 171 auto hash_set_result = pnp_id_data.set(columns[(size_t)PnpIdColumns::ManufacturerId], PnpIdData { .manufacturer_name = decoded_manufacturer_name, .approval_date = move(approval_date) }); 172 if (hash_set_result != AK::HashSetResult::InsertedNewEntry) 173 return Error::from_string_literal("Duplicate manufacturer ID encountered"); 174 175 row_content_offset = row_end.value() + row_end_tag.length(); 176 } 177 178 if (pnp_id_data.size() <= 1) 179 return Error::from_string_literal("Expected more than one row"); 180 181 return pnp_id_data; 182} 183 184static ErrorOr<void> generate_header(Core::File& file, HashMap<DeprecatedString, PnpIdData> const& pnp_ids) 185{ 186 StringBuilder builder; 187 SourceGenerator generator { builder }; 188 189 generator.set("pnp_id_count", DeprecatedString::formatted("{}", pnp_ids.size())); 190 generator.append(R"~~~( 191#pragma once 192 193#include <AK/Function.h> 194#include <AK/StringView.h> 195#include <AK/Types.h> 196 197namespace PnpIDs { 198 struct PnpIDData { 199 StringView manufacturer_id; 200 StringView manufacturer_name; 201 struct { 202 u16 year{}; 203 u8 month{}; 204 u8 day{}; 205 } approval_date; 206 }; 207 208 Optional<PnpIDData> find_by_manufacturer_id(StringView); 209 IterationDecision for_each(Function<IterationDecision(PnpIDData const&)>); 210 static constexpr size_t count = @pnp_id_count@; 211} 212)~~~"); 213 214 TRY(file.write_until_depleted(generator.as_string_view().bytes())); 215 return {}; 216} 217 218static ErrorOr<void> generate_source(Core::File& file, HashMap<DeprecatedString, PnpIdData> const& pnp_ids) 219{ 220 StringBuilder builder; 221 SourceGenerator generator { builder }; 222 223 generator.append(R"~~~( 224#include "PnpIDs.h" 225 226namespace PnpIDs { 227 228static constexpr PnpIDData s_pnp_ids[] = { 229)~~~"); 230 231 for (auto& pnp_id_data : pnp_ids) { 232 generator.set("manufacturer_id", pnp_id_data.key); 233 generator.set("manufacturer_name", pnp_id_data.value.manufacturer_name); 234 generator.set("approval_year", DeprecatedString::formatted("{}", pnp_id_data.value.approval_date.year)); 235 generator.set("approval_month", DeprecatedString::formatted("{}", pnp_id_data.value.approval_date.month)); 236 generator.set("approval_day", DeprecatedString::formatted("{}", pnp_id_data.value.approval_date.day)); 237 238 generator.append(R"~~~( 239{ "@manufacturer_id@"sv, "@manufacturer_name@"sv, { @approval_year@, @approval_month@, @approval_day@ } }, 240)~~~"); 241 } 242 243 generator.append(R"~~~( 244}; 245 246Optional<PnpIDData> find_by_manufacturer_id(StringView manufacturer_id) 247{ 248 for (auto& pnp_data : s_pnp_ids) { 249 if (pnp_data.manufacturer_id == manufacturer_id) 250 return pnp_data; 251 } 252 return {}; 253} 254 255IterationDecision for_each(Function<IterationDecision(PnpIDData const&)> callback) 256{ 257 for (auto& pnp_data : s_pnp_ids) { 258 auto decision = callback(pnp_data); 259 if (decision != IterationDecision::Continue) 260 return decision; 261 } 262 return IterationDecision::Continue; 263} 264 265} 266)~~~"); 267 268 TRY(file.write_until_depleted(generator.as_string_view().bytes())); 269 return {}; 270} 271 272ErrorOr<int> serenity_main(Main::Arguments arguments) 273{ 274 StringView generated_header_path; 275 StringView generated_implementation_path; 276 StringView pnp_ids_file_path; 277 278 Core::ArgsParser args_parser; 279 args_parser.add_option(generated_header_path, "Path to the header file to generate", "generated-header-path", 'h', "generated-header-path"); 280 args_parser.add_option(generated_implementation_path, "Path to the implementation file to generate", "generated-implementation-path", 'c', "generated-implementation-path"); 281 args_parser.add_option(pnp_ids_file_path, "Path to the input PNP ID database file", "pnp-ids-file", 'p', "pnp-ids-file"); 282 args_parser.parse(arguments); 283 284 auto open_file = [&](StringView path, Core::File::OpenMode mode = Core::File::OpenMode::Read) -> ErrorOr<NonnullOwnPtr<Core::File>> { 285 if (path.is_empty()) { 286 args_parser.print_usage(stderr, arguments.strings[0]); 287 return Error::from_string_literal("Must provide all command line options"); 288 } 289 290 return Core::File::open(path, mode); 291 }; 292 293 auto generated_header_file = TRY(open_file(generated_header_path, Core::File::OpenMode::ReadWrite)); 294 auto generated_implementation_file = TRY(open_file(generated_implementation_path, Core::File::OpenMode::ReadWrite)); 295 auto pnp_ids_file = TRY(open_file(pnp_ids_file_path)); 296 297 auto pnp_id_map = TRY(parse_pnp_ids_database(*pnp_ids_file)); 298 299 TRY(generate_header(*generated_header_file, pnp_id_map)); 300 TRY(generate_source(*generated_implementation_file, pnp_id_map)); 301 return 0; 302}