ladybird/Userland/Libraries/LibWeb/HTML/Parser/HTMLEncodingDetection.cpp
Shannon Booth 3bd04d2c58 LibWeb: Port Attr interface from DeprecatedString to String
There are an unfortunate number of DeprecatedString conversions required
here, but these should all fall away and look much more pretty again
when other places are also ported away from DeprecatedString.

Leaves only the Element IDL interface left :^)
2023-09-25 15:39:29 +02:00

288 lines
12 KiB
C++

/*
* Copyright (c) 2021, Max Wipfli <mail@maxwipfli.ch>
*
* SPDX-License-Identifier: BSD-2-Clause
*/
#include <AK/CharacterTypes.h>
#include <AK/GenericLexer.h>
#include <AK/StringView.h>
#include <AK/Utf8View.h>
#include <LibTextCodec/Decoder.h>
#include <LibWeb/DOM/Attr.h>
#include <LibWeb/DOM/Document.h>
#include <LibWeb/HTML/Parser/HTMLEncodingDetection.h>
#include <LibWeb/Infra/CharacterTypes.h>
#include <ctype.h>
namespace Web::HTML {
bool prescan_should_abort(ByteBuffer const& input, size_t const& position)
{
return position >= input.size() || position >= 1024;
}
bool prescan_is_whitespace_or_slash(u8 const& byte)
{
return byte == '\t' || byte == '\n' || byte == '\f' || byte == '\r' || byte == ' ' || byte == '/';
}
bool prescan_skip_whitespace_and_slashes(ByteBuffer const& input, size_t& position)
{
while (!prescan_should_abort(input, position) && (input[position] == '\t' || input[position] == '\n' || input[position] == '\f' || input[position] == '\r' || input[position] == ' ' || input[position] == '/'))
++position;
return !prescan_should_abort(input, position);
}
// https://html.spec.whatwg.org/multipage/urls-and-fetching.html#algorithm-for-extracting-a-character-encoding-from-a-meta-element
Optional<StringView> extract_character_encoding_from_meta_element(DeprecatedString const& string)
{
// Checking for "charset" is case insensitive, as is getting an encoding.
// Therefore, stick to lowercase from the start for simplicity.
auto lowercase_string = string.to_lowercase();
GenericLexer lexer(lowercase_string);
for (;;) {
auto charset_index = lexer.remaining().find("charset"sv);
if (!charset_index.has_value())
return {};
// 7 is the length of "charset".
lexer.ignore(charset_index.value() + 7);
lexer.ignore_while([](char c) {
return Infra::is_ascii_whitespace(c);
});
if (lexer.peek() != '=')
continue;
break;
}
// Ignore the '='.
lexer.ignore();
lexer.ignore_while([](char c) {
return Infra::is_ascii_whitespace(c);
});
if (lexer.is_eof())
return {};
if (lexer.consume_specific('"')) {
auto matching_double_quote = lexer.remaining().find('"');
if (!matching_double_quote.has_value())
return {};
auto encoding = lexer.remaining().substring_view(0, matching_double_quote.value());
return TextCodec::get_standardized_encoding(encoding);
}
if (lexer.consume_specific('\'')) {
auto matching_single_quote = lexer.remaining().find('\'');
if (!matching_single_quote.has_value())
return {};
auto encoding = lexer.remaining().substring_view(0, matching_single_quote.value());
return TextCodec::get_standardized_encoding(encoding);
}
auto encoding = lexer.consume_until([](char c) {
return Infra::is_ascii_whitespace(c) || c == ';';
});
return TextCodec::get_standardized_encoding(encoding);
}
JS::GCPtr<DOM::Attr> prescan_get_attribute(DOM::Document& document, ByteBuffer const& input, size_t& position)
{
if (!prescan_skip_whitespace_and_slashes(input, position))
return {};
if (input[position] == '>')
return {};
StringBuilder attribute_name;
while (true) {
if (input[position] == '=' && !attribute_name.is_empty()) {
++position;
goto value;
} else if (input[position] == '\t' || input[position] == '\n' || input[position] == '\f' || input[position] == '\r' || input[position] == ' ')
goto spaces;
else if (input[position] == '/' || input[position] == '>')
return *DOM::Attr::create(document, MUST(attribute_name.to_string()), String {});
else
attribute_name.append_as_lowercase(input[position]);
++position;
if (prescan_should_abort(input, position))
return {};
}
spaces:
if (!prescan_skip_whitespace_and_slashes(input, position))
return {};
if (input[position] != '=')
return DOM::Attr::create(document, MUST(attribute_name.to_string()), String {});
++position;
value:
if (!prescan_skip_whitespace_and_slashes(input, position))
return {};
StringBuilder attribute_value;
if (input[position] == '"' || input[position] == '\'') {
u8 quote_character = input[position];
++position;
for (; !prescan_should_abort(input, position); ++position) {
if (input[position] == quote_character)
return DOM::Attr::create(document, MUST(attribute_name.to_string()), MUST(attribute_value.to_string()));
else
attribute_value.append_as_lowercase(input[position]);
}
return {};
} else if (input[position] == '>')
return DOM::Attr::create(document, MUST(attribute_name.to_string()), String {});
else
attribute_value.append_as_lowercase(input[position]);
++position;
if (prescan_should_abort(input, position))
return {};
for (; !prescan_should_abort(input, position); ++position) {
if (input[position] == '\t' || input[position] == '\n' || input[position] == '\f' || input[position] == '\r' || input[position] == ' ' || input[position] == '>')
return DOM::Attr::create(document, MUST(attribute_name.to_string()), MUST(attribute_value.to_string()));
else
attribute_value.append_as_lowercase(input[position]);
}
return {};
}
// https://html.spec.whatwg.org/multipage/parsing.html#prescan-a-byte-stream-to-determine-its-encoding
Optional<DeprecatedString> run_prescan_byte_stream_algorithm(DOM::Document& document, ByteBuffer const& input)
{
// https://html.spec.whatwg.org/multipage/parsing.html#prescan-a-byte-stream-to-determine-its-encoding
// Detects '<?x'
if (!prescan_should_abort(input, 6)) {
if (input[0] == 0x3C && input[1] == 0x00 && input[2] == 0x3F && input[3] == 0x00 && input[4] == 0x78 && input[5] == 0x00)
return "utf-16le";
if (input[0] == 0x00 && input[1] == 0x3C && input[2] == 0x00 && input[4] == 0x3F && input[5] == 0x00 && input[6] == 0x78)
return "utf-16be";
}
for (size_t position = 0; !prescan_should_abort(input, position); ++position) {
if (!prescan_should_abort(input, position + 5) && input[position] == '<' && input[position + 1] == '!'
&& input[position + 2] == '-' && input[position + 3] == '-') {
position += 2;
for (; !prescan_should_abort(input, position + 3); ++position) {
if (input[position] == '-' && input[position + 1] == '-' && input[position + 2] == '>') {
position += 2;
break;
}
}
} else if (!prescan_should_abort(input, position + 6)
&& input[position] == '<'
&& (input[position + 1] == 'M' || input[position + 1] == 'm')
&& (input[position + 2] == 'E' || input[position + 2] == 'e')
&& (input[position + 3] == 'T' || input[position + 3] == 't')
&& (input[position + 4] == 'A' || input[position + 4] == 'a')
&& prescan_is_whitespace_or_slash(input[position + 5])) {
position += 6;
Vector<FlyString> attribute_list {};
bool got_pragma = false;
Optional<bool> need_pragma {};
Optional<DeprecatedString> charset {};
while (true) {
auto attribute = prescan_get_attribute(document, input, position);
if (!attribute)
break;
if (attribute_list.contains_slow(attribute->name()))
continue;
auto const& attribute_name = attribute->name();
attribute_list.append(attribute->name());
if (attribute_name == "http-equiv") {
got_pragma = attribute->value() == "content-type";
} else if (attribute_name == "content") {
auto encoding = extract_character_encoding_from_meta_element(attribute->value().to_deprecated_string());
if (encoding.has_value() && !charset.has_value()) {
charset = encoding.value();
need_pragma = true;
}
} else if (attribute_name == "charset") {
auto maybe_charset = TextCodec::get_standardized_encoding(attribute->value());
if (maybe_charset.has_value()) {
charset = Optional<DeprecatedString> { maybe_charset };
need_pragma = { false };
}
}
}
if (!need_pragma.has_value() || (need_pragma.value() && !got_pragma) || !charset.has_value())
continue;
if (charset.value() == "UTF-16BE/LE")
return "UTF-8";
else if (charset.value() == "x-user-defined")
return "windows-1252";
else
return charset.value();
} else if (!prescan_should_abort(input, position + 3) && input[position] == '<'
&& ((input[position + 1] == '/' && isalpha(input[position + 2])) || isalpha(input[position + 1]))) {
position += 2;
prescan_skip_whitespace_and_slashes(input, position);
while (prescan_get_attribute(document, input, position)) { };
} else if (!prescan_should_abort(input, position + 1) && input[position] == '<' && (input[position + 1] == '!' || input[position + 1] == '/' || input[position + 1] == '?')) {
position += 2;
while (input[position] != '>') {
++position;
if (prescan_should_abort(input, position))
return {};
}
} else {
// Do nothing.
}
}
return {};
}
// https://html.spec.whatwg.org/multipage/parsing.html#determining-the-character-encoding
DeprecatedString run_encoding_sniffing_algorithm(DOM::Document& document, ByteBuffer const& input)
{
if (input.size() >= 2) {
if (input[0] == 0xFE && input[1] == 0xFF) {
return "UTF-16BE";
} else if (input[0] == 0xFF && input[1] == 0xFE) {
return "UTF-16LE";
} else if (input.size() >= 3 && input[0] == 0xEF && input[1] == 0xBB && input[2] == 0xBF) {
return "UTF-8";
}
}
// FIXME: If the user has explicitly instructed the user agent to override the document's character
// encoding with a specific encoding.
// FIXME: The user agent may wait for more bytes of the resource to be available, either in this step or
// at any later step in this algorithm.
// FIXME: If the transport layer specifies a character encoding, and it is supported.
auto optional_encoding = run_prescan_byte_stream_algorithm(document, input);
if (optional_encoding.has_value()) {
return optional_encoding.value();
}
// FIXME: If the HTML parser for which this algorithm is being run is associated with a Document whose browsing context
// is non-null and a child browsing context.
// FIXME: If the user agent has information on the likely encoding for this page, e.g. based on the encoding of the page
// when it was last visited.
if (!Utf8View(StringView(input)).validate()) {
// FIXME: As soon as Locale is supported, this should sometimes return a different encoding based on the locale.
return "windows-1252";
}
// NOTE: This is the authoritative place to actually decide on using the default encoding as per the HTML specification.
// "Otherwise, return an implementation-defined or user-specified default character encoding, [...]."
return "UTF-8";
}
}