Serenity Operating System
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}