mirror of
https://github.com/RGBCube/serenity
synced 2025-07-25 18:27:35 +00:00
LibXML: Add a fairly basic XML parser
Currently this can parse XML and resolve external resources/references, and read a DTD (but not apply or verify its rules). That's good enough for _most_ XHTML documents as the HTML 5 spec enforces its own rules about document well-formedness, and does not make use of XML DTDs (aside from a list of predefined entities). An accompanying `xml` utility is provided that can read and dump XML documents, and can also run the XML conformance test suite.
This commit is contained in:
parent
06cedf5bae
commit
67357fe984
15 changed files with 2895 additions and 0 deletions
|
@ -493,3 +493,7 @@
|
||||||
#ifndef WSSCREEN_DEBUG
|
#ifndef WSSCREEN_DEBUG
|
||||||
#cmakedefine01 WSSCREEN_DEBUG
|
#cmakedefine01 WSSCREEN_DEBUG
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
|
#ifndef XML_PARSER_DEBUG
|
||||||
|
#cmakedefine01 XML_PARSER_DEBUG
|
||||||
|
#endif
|
||||||
|
|
|
@ -209,6 +209,7 @@ set(WEB_WORKER_DEBUG ON)
|
||||||
set(WINDOWMANAGER_DEBUG ON)
|
set(WINDOWMANAGER_DEBUG ON)
|
||||||
set(WSMESSAGELOOP_DEBUG ON)
|
set(WSMESSAGELOOP_DEBUG ON)
|
||||||
set(WSSCREEN_DEBUG ON)
|
set(WSSCREEN_DEBUG ON)
|
||||||
|
set(XML_PARSER_DEBUG ON)
|
||||||
|
|
||||||
# False positive: DEBUG is a flag but it works differently.
|
# False positive: DEBUG is a flag but it works differently.
|
||||||
# set(DEBUG ON)
|
# set(DEBUG ON)
|
||||||
|
|
|
@ -478,6 +478,10 @@ if (BUILD_LAGOM)
|
||||||
)
|
)
|
||||||
endif()
|
endif()
|
||||||
|
|
||||||
|
file(GLOB LIBXML_SOURCES CONFIGURE_DEPENDS "../../Userland/Libraries/LibXML/*/*.cpp")
|
||||||
|
lagom_lib(XML xml
|
||||||
|
SOURCES ${LIBXML_SOURCES})
|
||||||
|
|
||||||
if (NOT ENABLE_OSS_FUZZ AND NOT ENABLE_FUZZER_SANITIZER AND NOT ENABLE_COMPILER_EXPLORER_BUILD)
|
if (NOT ENABLE_OSS_FUZZ AND NOT ENABLE_FUZZER_SANITIZER AND NOT ENABLE_COMPILER_EXPLORER_BUILD)
|
||||||
# Lagom Examples
|
# Lagom Examples
|
||||||
add_executable(TestApp TestApp.cpp)
|
add_executable(TestApp TestApp.cpp)
|
||||||
|
@ -523,6 +527,10 @@ if (BUILD_LAGOM)
|
||||||
set_target_properties(wasm_lagom PROPERTIES OUTPUT_NAME wasm)
|
set_target_properties(wasm_lagom PROPERTIES OUTPUT_NAME wasm)
|
||||||
target_link_libraries(wasm_lagom LagomCore LagomWasm LagomLine LagomMain)
|
target_link_libraries(wasm_lagom LagomCore LagomWasm LagomLine LagomMain)
|
||||||
|
|
||||||
|
add_executable(xml_lagom ../../Userland/Utilities/xml.cpp)
|
||||||
|
set_target_properties(xml_lagom PROPERTIES OUTPUT_NAME xml)
|
||||||
|
target_link_libraries(xml_lagom LagomCore LagomXML LagomMain)
|
||||||
|
|
||||||
enable_testing()
|
enable_testing()
|
||||||
# LibTest
|
# LibTest
|
||||||
file(GLOB LIBTEST_SOURCES CONFIGURE_DEPENDS "../../Userland/Libraries/LibTest/*.cpp")
|
file(GLOB LIBTEST_SOURCES CONFIGURE_DEPENDS "../../Userland/Libraries/LibTest/*.cpp")
|
||||||
|
|
|
@ -57,3 +57,4 @@ add_subdirectory(LibWasm)
|
||||||
add_subdirectory(LibWeb)
|
add_subdirectory(LibWeb)
|
||||||
add_subdirectory(LibWebSocket)
|
add_subdirectory(LibWebSocket)
|
||||||
add_subdirectory(LibX86)
|
add_subdirectory(LibX86)
|
||||||
|
add_subdirectory(LibXML)
|
||||||
|
|
7
Userland/Libraries/LibXML/CMakeLists.txt
Normal file
7
Userland/Libraries/LibXML/CMakeLists.txt
Normal file
|
@ -0,0 +1,7 @@
|
||||||
|
set(SOURCES
|
||||||
|
Parser/Parser.cpp
|
||||||
|
DOM/Node.cpp
|
||||||
|
)
|
||||||
|
|
||||||
|
serenity_lib(LibXML xml)
|
||||||
|
target_link_libraries(LibXML LibC)
|
53
Userland/Libraries/LibXML/DOM/Document.h
Normal file
53
Userland/Libraries/LibXML/DOM/Document.h
Normal file
|
@ -0,0 +1,53 @@
|
||||||
|
/*
|
||||||
|
* Copyright (c) 2022, Ali Mohammad Pur <mpfard@serenityos.org>
|
||||||
|
*
|
||||||
|
* SPDX-License-Identifier: BSD-2-Clause
|
||||||
|
*/
|
||||||
|
|
||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include <AK/HashMap.h>
|
||||||
|
#include <AK/NonnullOwnPtr.h>
|
||||||
|
#include <LibXML/DOM/DocumentTypeDeclaration.h>
|
||||||
|
#include <LibXML/DOM/Node.h>
|
||||||
|
#include <LibXML/Forward.h>
|
||||||
|
|
||||||
|
namespace XML {
|
||||||
|
|
||||||
|
enum class Version {
|
||||||
|
Version10,
|
||||||
|
Version11,
|
||||||
|
};
|
||||||
|
|
||||||
|
struct Doctype {
|
||||||
|
String type;
|
||||||
|
Vector<MarkupDeclaration> markup_declarations;
|
||||||
|
Optional<ExternalID> external_id;
|
||||||
|
};
|
||||||
|
|
||||||
|
class Document {
|
||||||
|
public:
|
||||||
|
explicit Document(NonnullOwnPtr<Node> root, Optional<Doctype> doctype, HashMap<Name, String> processing_instructions, Version version)
|
||||||
|
: m_root(move(root))
|
||||||
|
, m_processing_instructions(move(processing_instructions))
|
||||||
|
, m_version(version)
|
||||||
|
, m_explicit_doctype(move(doctype))
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
Node& root() { return *m_root; }
|
||||||
|
Node const& root() const { return *m_root; }
|
||||||
|
|
||||||
|
HashMap<Name, String> const& processing_instructions() const { return m_processing_instructions; }
|
||||||
|
|
||||||
|
Version version() const { return m_version; }
|
||||||
|
|
||||||
|
Optional<Doctype> const& doctype() const { return m_explicit_doctype; }
|
||||||
|
|
||||||
|
private:
|
||||||
|
NonnullOwnPtr<Node> m_root;
|
||||||
|
HashMap<Name, String> m_processing_instructions;
|
||||||
|
Version m_version;
|
||||||
|
Optional<Doctype> m_explicit_doctype;
|
||||||
|
};
|
||||||
|
}
|
138
Userland/Libraries/LibXML/DOM/DocumentTypeDeclaration.h
Normal file
138
Userland/Libraries/LibXML/DOM/DocumentTypeDeclaration.h
Normal file
|
@ -0,0 +1,138 @@
|
||||||
|
/*
|
||||||
|
* Copyright (c) 2022, Ali Mohammad Pur <mpfard@serenityos.org>
|
||||||
|
*
|
||||||
|
* SPDX-License-Identifier: BSD-2-Clause
|
||||||
|
*/
|
||||||
|
|
||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include <AK/HashTable.h>
|
||||||
|
#include <AK/String.h>
|
||||||
|
#include <AK/Variant.h>
|
||||||
|
#include <AK/Vector.h>
|
||||||
|
#include <LibXML/FundamentalTypes.h>
|
||||||
|
|
||||||
|
namespace XML {
|
||||||
|
|
||||||
|
struct ElementDeclaration {
|
||||||
|
struct Empty {
|
||||||
|
};
|
||||||
|
struct Any {
|
||||||
|
};
|
||||||
|
struct Mixed {
|
||||||
|
HashTable<Name> types;
|
||||||
|
bool many;
|
||||||
|
};
|
||||||
|
struct Children {
|
||||||
|
struct Entry;
|
||||||
|
enum class Qualifier {
|
||||||
|
ExactlyOnce,
|
||||||
|
Optional,
|
||||||
|
Any,
|
||||||
|
OneOrMore,
|
||||||
|
};
|
||||||
|
|
||||||
|
struct Choice {
|
||||||
|
Vector<Entry> entries;
|
||||||
|
Qualifier qualifier;
|
||||||
|
};
|
||||||
|
struct Sequence {
|
||||||
|
Vector<Entry> entries;
|
||||||
|
Qualifier qualifier;
|
||||||
|
};
|
||||||
|
|
||||||
|
struct Entry {
|
||||||
|
Variant<Name, Choice, Sequence> sub_entries;
|
||||||
|
Qualifier qualifier;
|
||||||
|
};
|
||||||
|
|
||||||
|
Variant<Choice, Sequence> contents;
|
||||||
|
Qualifier qualifier;
|
||||||
|
};
|
||||||
|
using ContentSpec = Variant<Empty, Any, Mixed, Children>;
|
||||||
|
|
||||||
|
Name type;
|
||||||
|
ContentSpec content_spec;
|
||||||
|
};
|
||||||
|
|
||||||
|
struct AttributeListDeclaration {
|
||||||
|
enum class StringType {
|
||||||
|
CData,
|
||||||
|
};
|
||||||
|
enum class TokenizedType {
|
||||||
|
ID,
|
||||||
|
IDRef,
|
||||||
|
IDRefs,
|
||||||
|
Entity,
|
||||||
|
Entities,
|
||||||
|
NMToken,
|
||||||
|
NMTokens,
|
||||||
|
};
|
||||||
|
struct NotationType {
|
||||||
|
HashTable<Name> names;
|
||||||
|
};
|
||||||
|
struct Enumeration {
|
||||||
|
// FIXME: NMToken
|
||||||
|
HashTable<String> tokens;
|
||||||
|
};
|
||||||
|
using Type = Variant<StringType, TokenizedType, NotationType, Enumeration>;
|
||||||
|
|
||||||
|
struct Required {
|
||||||
|
};
|
||||||
|
struct Implied {
|
||||||
|
};
|
||||||
|
struct Fixed {
|
||||||
|
String value;
|
||||||
|
};
|
||||||
|
struct DefaultValue {
|
||||||
|
String value;
|
||||||
|
};
|
||||||
|
|
||||||
|
using Default = Variant<Required, Implied, Fixed, DefaultValue>;
|
||||||
|
|
||||||
|
struct Definition {
|
||||||
|
Name name;
|
||||||
|
Type type;
|
||||||
|
Default default_;
|
||||||
|
};
|
||||||
|
Name type;
|
||||||
|
Vector<Definition> attributes;
|
||||||
|
};
|
||||||
|
|
||||||
|
struct PublicID {
|
||||||
|
String public_literal;
|
||||||
|
};
|
||||||
|
|
||||||
|
struct SystemID {
|
||||||
|
String system_literal;
|
||||||
|
};
|
||||||
|
|
||||||
|
struct ExternalID {
|
||||||
|
Optional<PublicID> public_id;
|
||||||
|
SystemID system_id;
|
||||||
|
};
|
||||||
|
|
||||||
|
struct EntityDefinition {
|
||||||
|
ExternalID id;
|
||||||
|
Optional<Name> notation;
|
||||||
|
};
|
||||||
|
|
||||||
|
struct GEDeclaration {
|
||||||
|
Name name;
|
||||||
|
Variant<String, EntityDefinition> definition;
|
||||||
|
};
|
||||||
|
|
||||||
|
struct PEDeclaration {
|
||||||
|
Name name;
|
||||||
|
Variant<String, ExternalID> definition;
|
||||||
|
};
|
||||||
|
|
||||||
|
using EntityDeclaration = Variant<GEDeclaration, PEDeclaration>;
|
||||||
|
|
||||||
|
struct NotationDeclaration {
|
||||||
|
Name name;
|
||||||
|
Variant<ExternalID, PublicID> notation;
|
||||||
|
};
|
||||||
|
|
||||||
|
using MarkupDeclaration = Variant<ElementDeclaration, AttributeListDeclaration, EntityDeclaration, NotationDeclaration>;
|
||||||
|
}
|
54
Userland/Libraries/LibXML/DOM/Node.cpp
Normal file
54
Userland/Libraries/LibXML/DOM/Node.cpp
Normal file
|
@ -0,0 +1,54 @@
|
||||||
|
/*
|
||||||
|
* Copyright (c) 2022, Ali Mohammad Pur <mpfard@serenityos.org>
|
||||||
|
*
|
||||||
|
* SPDX-License-Identifier: BSD-2-Clause
|
||||||
|
*/
|
||||||
|
|
||||||
|
#include <AK/HashMap.h>
|
||||||
|
#include <LibXML/DOM/Node.h>
|
||||||
|
|
||||||
|
namespace XML {
|
||||||
|
|
||||||
|
bool Node::operator==(Node const& other) const
|
||||||
|
{
|
||||||
|
return content.visit(
|
||||||
|
[&](Text const& text) -> bool {
|
||||||
|
auto other_text = other.content.get_pointer<Text>();
|
||||||
|
if (!other_text)
|
||||||
|
return false;
|
||||||
|
return text.builder.string_view() == other_text->builder.string_view();
|
||||||
|
},
|
||||||
|
[&](Comment const& comment) -> bool {
|
||||||
|
auto other_comment = other.content.get_pointer<Comment>();
|
||||||
|
if (!other_comment)
|
||||||
|
return false;
|
||||||
|
return comment.text == other_comment->text;
|
||||||
|
},
|
||||||
|
[&](Element const& element) -> bool {
|
||||||
|
auto other_element = other.content.get_pointer<Element>();
|
||||||
|
if (!other_element)
|
||||||
|
return false;
|
||||||
|
if (element.name != other_element->name)
|
||||||
|
return false;
|
||||||
|
if (element.attributes.size() != other_element->attributes.size())
|
||||||
|
return false;
|
||||||
|
|
||||||
|
for (auto& entry : element.attributes) {
|
||||||
|
auto it = other_element->attributes.find(entry.key);
|
||||||
|
if (it == other_element->attributes.end())
|
||||||
|
return false;
|
||||||
|
if (it->value != entry.value)
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (element.children.size() != other_element->children.size())
|
||||||
|
return false;
|
||||||
|
for (size_t i = 0; i < element.children.size(); ++i) {
|
||||||
|
if (element.children[i] != other_element->children[i])
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
40
Userland/Libraries/LibXML/DOM/Node.h
Normal file
40
Userland/Libraries/LibXML/DOM/Node.h
Normal file
|
@ -0,0 +1,40 @@
|
||||||
|
/*
|
||||||
|
* Copyright (c) 2022, Ali Mohammad Pur <mpfard@serenityos.org>
|
||||||
|
*
|
||||||
|
* SPDX-License-Identifier: BSD-2-Clause
|
||||||
|
*/
|
||||||
|
|
||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include <AK/NonnullOwnPtrVector.h>
|
||||||
|
#include <AK/String.h>
|
||||||
|
#include <AK/Variant.h>
|
||||||
|
#include <AK/Vector.h>
|
||||||
|
#include <LibXML/FundamentalTypes.h>
|
||||||
|
|
||||||
|
namespace XML {
|
||||||
|
|
||||||
|
struct Attribute {
|
||||||
|
Name name;
|
||||||
|
String value;
|
||||||
|
};
|
||||||
|
|
||||||
|
struct Node {
|
||||||
|
struct Text {
|
||||||
|
StringBuilder builder;
|
||||||
|
};
|
||||||
|
struct Comment {
|
||||||
|
String text;
|
||||||
|
};
|
||||||
|
struct Element {
|
||||||
|
Name name;
|
||||||
|
HashMap<Name, String> attributes;
|
||||||
|
NonnullOwnPtrVector<Node> children;
|
||||||
|
};
|
||||||
|
|
||||||
|
bool operator==(Node const&) const;
|
||||||
|
|
||||||
|
Variant<Text, Comment, Element> content;
|
||||||
|
Node* parent { nullptr };
|
||||||
|
};
|
||||||
|
}
|
15
Userland/Libraries/LibXML/Forward.h
Normal file
15
Userland/Libraries/LibXML/Forward.h
Normal file
|
@ -0,0 +1,15 @@
|
||||||
|
/*
|
||||||
|
* Copyright (c) 2022, Ali Mohammad Pur <mpfard@serenityos.org>
|
||||||
|
*
|
||||||
|
* SPDX-License-Identifier: BSD-2-Clause
|
||||||
|
*/
|
||||||
|
|
||||||
|
#pragma once
|
||||||
|
|
||||||
|
namespace XML {
|
||||||
|
class Parser;
|
||||||
|
class Document;
|
||||||
|
struct Node;
|
||||||
|
struct Attribute;
|
||||||
|
struct Listener;
|
||||||
|
}
|
16
Userland/Libraries/LibXML/FundamentalTypes.h
Normal file
16
Userland/Libraries/LibXML/FundamentalTypes.h
Normal file
|
@ -0,0 +1,16 @@
|
||||||
|
/*
|
||||||
|
* Copyright (c) 2022, Ali Mohammad Pur <mpfard@serenityos.org>
|
||||||
|
*
|
||||||
|
* SPDX-License-Identifier: BSD-2-Clause
|
||||||
|
*/
|
||||||
|
|
||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include <AK/String.h>
|
||||||
|
|
||||||
|
namespace XML {
|
||||||
|
|
||||||
|
// FIXME: Maybe extend this to something more sophisticated?
|
||||||
|
using Name = String;
|
||||||
|
|
||||||
|
}
|
1780
Userland/Libraries/LibXML/Parser/Parser.cpp
Normal file
1780
Userland/Libraries/LibXML/Parser/Parser.cpp
Normal file
File diff suppressed because it is too large
Load diff
223
Userland/Libraries/LibXML/Parser/Parser.h
Normal file
223
Userland/Libraries/LibXML/Parser/Parser.h
Normal file
|
@ -0,0 +1,223 @@
|
||||||
|
/*
|
||||||
|
* Copyright (c) 2022, Ali Mohammad Pur <mpfard@serenityos.org>
|
||||||
|
*
|
||||||
|
* SPDX-License-Identifier: BSD-2-Clause
|
||||||
|
*/
|
||||||
|
|
||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include <AK/Debug.h>
|
||||||
|
#include <AK/Function.h>
|
||||||
|
#include <AK/GenericLexer.h>
|
||||||
|
#include <AK/HashMap.h>
|
||||||
|
#include <AK/OwnPtr.h>
|
||||||
|
#include <AK/SourceLocation.h>
|
||||||
|
#include <AK/String.h>
|
||||||
|
#include <AK/TemporaryChange.h>
|
||||||
|
#include <LibXML/DOM/Document.h>
|
||||||
|
#include <LibXML/DOM/DocumentTypeDeclaration.h>
|
||||||
|
#include <LibXML/DOM/Node.h>
|
||||||
|
#include <LibXML/Forward.h>
|
||||||
|
|
||||||
|
namespace XML {
|
||||||
|
|
||||||
|
struct ParseError {
|
||||||
|
size_t offset;
|
||||||
|
String error;
|
||||||
|
};
|
||||||
|
|
||||||
|
struct Listener {
|
||||||
|
virtual ~Listener() { }
|
||||||
|
|
||||||
|
virtual void document_start() { }
|
||||||
|
virtual void document_end() { }
|
||||||
|
virtual void element_start(Name const&, HashMap<Name, String> const&) { }
|
||||||
|
virtual void element_end(Name const&) { }
|
||||||
|
virtual void text(String const&) { }
|
||||||
|
virtual void comment(String const&) { }
|
||||||
|
virtual void error(ParseError const&) { }
|
||||||
|
};
|
||||||
|
|
||||||
|
class Parser {
|
||||||
|
public:
|
||||||
|
struct Options {
|
||||||
|
bool preserve_cdata { true };
|
||||||
|
bool preserve_comments { false };
|
||||||
|
bool treat_errors_as_fatal { true };
|
||||||
|
Function<ErrorOr<String>(SystemID const&, Optional<PublicID> const&)> resolve_external_resource {};
|
||||||
|
};
|
||||||
|
|
||||||
|
Parser(StringView source, Options options)
|
||||||
|
: m_source(source)
|
||||||
|
, m_lexer(source)
|
||||||
|
, m_options(move(options))
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
explicit Parser(StringView source)
|
||||||
|
: m_source(source)
|
||||||
|
, m_lexer(source)
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
ErrorOr<Document, ParseError> parse();
|
||||||
|
ErrorOr<void, ParseError> parse_with_listener(Listener&);
|
||||||
|
|
||||||
|
Vector<ParseError> const& parse_error_causes() const { return m_parse_errors; }
|
||||||
|
|
||||||
|
private:
|
||||||
|
struct EntityReference {
|
||||||
|
Name name;
|
||||||
|
};
|
||||||
|
|
||||||
|
ErrorOr<void, ParseError> parse_internal();
|
||||||
|
void append_node(NonnullOwnPtr<Node>);
|
||||||
|
void append_text(String);
|
||||||
|
void append_comment(String);
|
||||||
|
void enter_node(Node&);
|
||||||
|
void leave_node();
|
||||||
|
|
||||||
|
enum class ReferencePlacement {
|
||||||
|
AttributeValue,
|
||||||
|
Content,
|
||||||
|
};
|
||||||
|
ErrorOr<String, ParseError> resolve_reference(EntityReference const&, ReferencePlacement);
|
||||||
|
ErrorOr<String, ParseError> resolve_parameter_entity_reference(EntityReference const&);
|
||||||
|
|
||||||
|
enum class Required {
|
||||||
|
No,
|
||||||
|
Yes,
|
||||||
|
};
|
||||||
|
ErrorOr<void, ParseError> skip_whitespace(Required = Required::No);
|
||||||
|
|
||||||
|
ErrorOr<void, ParseError> parse_prolog();
|
||||||
|
ErrorOr<void, ParseError> parse_element();
|
||||||
|
ErrorOr<void, ParseError> parse_misc();
|
||||||
|
ErrorOr<void, ParseError> parse_xml_decl();
|
||||||
|
ErrorOr<void, ParseError> parse_doctype_decl();
|
||||||
|
ErrorOr<void, ParseError> parse_version_info();
|
||||||
|
ErrorOr<void, ParseError> parse_encoding_decl();
|
||||||
|
ErrorOr<void, ParseError> parse_standalone_document_decl();
|
||||||
|
ErrorOr<void, ParseError> parse_eq();
|
||||||
|
ErrorOr<void, ParseError> parse_comment();
|
||||||
|
ErrorOr<void, ParseError> parse_processing_instruction();
|
||||||
|
ErrorOr<Name, ParseError> parse_processing_instruction_target();
|
||||||
|
ErrorOr<Name, ParseError> parse_name();
|
||||||
|
ErrorOr<NonnullOwnPtr<Node>, ParseError> parse_empty_element_tag();
|
||||||
|
ErrorOr<NonnullOwnPtr<Node>, ParseError> parse_start_tag();
|
||||||
|
ErrorOr<Name, ParseError> parse_end_tag();
|
||||||
|
ErrorOr<void, ParseError> parse_content();
|
||||||
|
ErrorOr<Attribute, ParseError> parse_attribute();
|
||||||
|
ErrorOr<String, ParseError> parse_attribute_value();
|
||||||
|
ErrorOr<Variant<EntityReference, String>, ParseError> parse_reference();
|
||||||
|
ErrorOr<StringView, ParseError> parse_char_data();
|
||||||
|
ErrorOr<Vector<MarkupDeclaration>, ParseError> parse_internal_subset();
|
||||||
|
ErrorOr<Optional<MarkupDeclaration>, ParseError> parse_markup_declaration();
|
||||||
|
ErrorOr<Optional<String>, ParseError> parse_declaration_separator();
|
||||||
|
ErrorOr<Vector<MarkupDeclaration>, ParseError> parse_external_subset_declaration();
|
||||||
|
ErrorOr<ElementDeclaration, ParseError> parse_element_declaration();
|
||||||
|
ErrorOr<AttributeListDeclaration, ParseError> parse_attribute_list_declaration();
|
||||||
|
ErrorOr<EntityDeclaration, ParseError> parse_entity_declaration();
|
||||||
|
ErrorOr<NotationDeclaration, ParseError> parse_notation_declaration();
|
||||||
|
ErrorOr<Name, ParseError> parse_parameter_entity_reference();
|
||||||
|
ErrorOr<ElementDeclaration::ContentSpec, ParseError> parse_content_spec();
|
||||||
|
ErrorOr<AttributeListDeclaration::Definition, ParseError> parse_attribute_definition();
|
||||||
|
ErrorOr<StringView, ParseError> parse_nm_token();
|
||||||
|
ErrorOr<EntityDeclaration, ParseError> parse_general_entity_declaration();
|
||||||
|
ErrorOr<EntityDeclaration, ParseError> parse_parameter_entity_declaration();
|
||||||
|
ErrorOr<PublicID, ParseError> parse_public_id();
|
||||||
|
ErrorOr<SystemID, ParseError> parse_system_id();
|
||||||
|
ErrorOr<ExternalID, ParseError> parse_external_id();
|
||||||
|
ErrorOr<String, ParseError> parse_entity_value();
|
||||||
|
ErrorOr<Name, ParseError> parse_notation_data_declaration();
|
||||||
|
ErrorOr<StringView, ParseError> parse_public_id_literal();
|
||||||
|
ErrorOr<StringView, ParseError> parse_system_id_literal();
|
||||||
|
ErrorOr<StringView, ParseError> parse_cdata_section();
|
||||||
|
ErrorOr<String, ParseError> parse_attribute_value_inner(StringView disallow);
|
||||||
|
ErrorOr<Vector<MarkupDeclaration>, ParseError> parse_external_subset();
|
||||||
|
ErrorOr<void, ParseError> parse_text_declaration();
|
||||||
|
|
||||||
|
ErrorOr<void, ParseError> expect(StringView);
|
||||||
|
template<typename Pred>
|
||||||
|
requires(IsCallableWithArguments<Pred, char>) ErrorOr<StringView, ParseError> expect(Pred, StringView description);
|
||||||
|
template<typename Pred>
|
||||||
|
requires(IsCallableWithArguments<Pred, char>) ErrorOr<StringView, ParseError> expect_many(Pred, StringView description);
|
||||||
|
|
||||||
|
static size_t s_debug_indent_level;
|
||||||
|
[[nodiscard]] auto rollback_point(SourceLocation location = SourceLocation::current())
|
||||||
|
{
|
||||||
|
return ArmedScopeGuard {
|
||||||
|
[this, position = m_lexer.tell(), location] {
|
||||||
|
m_lexer.retreat(m_lexer.tell() - position);
|
||||||
|
(void)location;
|
||||||
|
dbgln_if(XML_PARSER_DEBUG, "{:->{}}FAIL @ {} -- \x1b[31m{}\x1b[0m", " ", s_debug_indent_level * 2, location, m_lexer.remaining().substring_view(0, min(16, m_lexer.tell_remaining())).replace("\n", "\\n", true));
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
[[nodiscard]] auto accept_rule()
|
||||||
|
{
|
||||||
|
return TemporaryChange { m_current_rule.accept, true };
|
||||||
|
}
|
||||||
|
[[nodiscard]] auto enter_rule(SourceLocation location = SourceLocation::current())
|
||||||
|
{
|
||||||
|
dbgln_if(XML_PARSER_DEBUG, "{:->{}}Enter {}", " ", s_debug_indent_level * 2, location);
|
||||||
|
++s_debug_indent_level;
|
||||||
|
auto rule = m_current_rule;
|
||||||
|
m_current_rule = { location.function_name(), false };
|
||||||
|
return ScopeGuard {
|
||||||
|
[location, rule, this] {
|
||||||
|
m_current_rule = rule;
|
||||||
|
--s_debug_indent_level;
|
||||||
|
(void)location;
|
||||||
|
dbgln_if(XML_PARSER_DEBUG, "{:->{}}Leave {}", " ", s_debug_indent_level * 2, location);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
template<typename... Ts>
|
||||||
|
ParseError parse_error(Ts&&... args)
|
||||||
|
{
|
||||||
|
auto error = ParseError { forward<Ts>(args)... };
|
||||||
|
if (m_current_rule.accept) {
|
||||||
|
auto rule_name = m_current_rule.rule.value_or("<?>");
|
||||||
|
if (rule_name.starts_with("parse_"))
|
||||||
|
rule_name = rule_name.substring_view(6);
|
||||||
|
m_parse_errors.append({
|
||||||
|
error.offset,
|
||||||
|
String::formatted("{}: {}", rule_name, error.error),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
return error;
|
||||||
|
}
|
||||||
|
|
||||||
|
StringView m_source;
|
||||||
|
GenericLexer m_lexer;
|
||||||
|
Options m_options;
|
||||||
|
Listener* m_listener { nullptr };
|
||||||
|
|
||||||
|
OwnPtr<Node> m_root_node;
|
||||||
|
Node* m_entered_node { nullptr };
|
||||||
|
Version m_version { Version::Version11 };
|
||||||
|
bool m_in_compatibility_mode { false };
|
||||||
|
String m_encoding;
|
||||||
|
bool m_standalone { false };
|
||||||
|
HashMap<Name, String> m_processing_instructions;
|
||||||
|
struct AcceptedRule {
|
||||||
|
Optional<String> rule {};
|
||||||
|
bool accept { false };
|
||||||
|
} m_current_rule {};
|
||||||
|
|
||||||
|
Vector<ParseError> m_parse_errors;
|
||||||
|
|
||||||
|
Optional<Doctype> m_doctype;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
template<>
|
||||||
|
struct AK::Formatter<XML::ParseError> : public AK::Formatter<FormatString> {
|
||||||
|
ErrorOr<void> format(FormatBuilder& builder, XML::ParseError const& error)
|
||||||
|
{
|
||||||
|
return Formatter<FormatString>::format(builder, "{} at offset {}", error.error, error.offset);
|
||||||
|
}
|
||||||
|
};
|
|
@ -224,5 +224,6 @@ target_link_libraries(which LibMain)
|
||||||
target_link_libraries(whoami LibMain)
|
target_link_libraries(whoami LibMain)
|
||||||
target_link_libraries(wsctl LibGUI LibMain)
|
target_link_libraries(wsctl LibGUI LibMain)
|
||||||
target_link_libraries(xargs LibMain)
|
target_link_libraries(xargs LibMain)
|
||||||
|
target_link_libraries(xml LibMain LibXML)
|
||||||
target_link_libraries(yes LibMain)
|
target_link_libraries(yes LibMain)
|
||||||
target_link_libraries(zip LibArchive LibCompress LibCrypto LibMain)
|
target_link_libraries(zip LibArchive LibCompress LibCrypto LibMain)
|
||||||
|
|
554
Userland/Utilities/xml.cpp
Normal file
554
Userland/Utilities/xml.cpp
Normal file
|
@ -0,0 +1,554 @@
|
||||||
|
/*
|
||||||
|
* Copyright (c) 2022, Ali Mohammad Pur <mpfard@serenityos.org>
|
||||||
|
*
|
||||||
|
* SPDX-License-Identifier: BSD-2-Clause
|
||||||
|
*/
|
||||||
|
|
||||||
|
#include <AK/LexicalPath.h>
|
||||||
|
#include <AK/Queue.h>
|
||||||
|
#include <AK/URL.h>
|
||||||
|
#include <AK/URLParser.h>
|
||||||
|
#include <LibCore/ArgsParser.h>
|
||||||
|
#include <LibCore/File.h>
|
||||||
|
#include <LibMain/Main.h>
|
||||||
|
#include <LibXML/DOM/Document.h>
|
||||||
|
#include <LibXML/DOM/Node.h>
|
||||||
|
#include <LibXML/Parser/Parser.h>
|
||||||
|
|
||||||
|
static bool g_color = false;
|
||||||
|
static bool g_only_contents = false;
|
||||||
|
|
||||||
|
enum class ColorRole {
|
||||||
|
PITag,
|
||||||
|
PITarget,
|
||||||
|
PIData,
|
||||||
|
AttributeName,
|
||||||
|
Eq,
|
||||||
|
AttributeValue,
|
||||||
|
Tag,
|
||||||
|
Text,
|
||||||
|
Comment,
|
||||||
|
Reset,
|
||||||
|
Doctype,
|
||||||
|
Keyword,
|
||||||
|
};
|
||||||
|
static void color(ColorRole role)
|
||||||
|
{
|
||||||
|
if (!g_color)
|
||||||
|
return;
|
||||||
|
|
||||||
|
switch (role) {
|
||||||
|
case ColorRole::PITag:
|
||||||
|
case ColorRole::Doctype:
|
||||||
|
out("\x1b[{};{}m", 1, "38;5;223");
|
||||||
|
break;
|
||||||
|
case ColorRole::PITarget:
|
||||||
|
out("\x1b[{};{}m", 1, "38;5;23");
|
||||||
|
break;
|
||||||
|
case ColorRole::PIData:
|
||||||
|
out("\x1b[{};{}m", 1, "38;5;43");
|
||||||
|
break;
|
||||||
|
case ColorRole::AttributeName:
|
||||||
|
out("\x1b[38;5;27m");
|
||||||
|
break;
|
||||||
|
case ColorRole::Eq:
|
||||||
|
break;
|
||||||
|
case ColorRole::AttributeValue:
|
||||||
|
out("\x1b[38;5;46m");
|
||||||
|
break;
|
||||||
|
case ColorRole::Tag:
|
||||||
|
out("\x1b[{};{}m", 1, "38;5;220");
|
||||||
|
break;
|
||||||
|
case ColorRole::Text:
|
||||||
|
break;
|
||||||
|
case ColorRole::Comment:
|
||||||
|
out("\x1b[{};{}m", 3, "38;5;250");
|
||||||
|
break;
|
||||||
|
case ColorRole::Reset:
|
||||||
|
out("\x1b[0m");
|
||||||
|
break;
|
||||||
|
case ColorRole::Keyword:
|
||||||
|
out("\x1b[38;5;40m");
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
static void dump(XML::Node const& node)
|
||||||
|
{
|
||||||
|
node.content.visit(
|
||||||
|
[](XML::Node::Text const& text) {
|
||||||
|
out("{}", text.builder.string_view());
|
||||||
|
},
|
||||||
|
[](XML::Node::Comment const& comment) {
|
||||||
|
color(ColorRole::Comment);
|
||||||
|
out("<!--{}-->", comment.text);
|
||||||
|
color(ColorRole::Reset);
|
||||||
|
},
|
||||||
|
[](XML::Node::Element const& element) {
|
||||||
|
color(ColorRole::Tag);
|
||||||
|
out("<{}", element.name);
|
||||||
|
color(ColorRole::Reset);
|
||||||
|
|
||||||
|
if (!element.attributes.is_empty()) {
|
||||||
|
for (auto& attribute : element.attributes) {
|
||||||
|
auto quote = attribute.value.contains('"') ? '\'' : '"';
|
||||||
|
color(ColorRole::AttributeName);
|
||||||
|
out(" {}", attribute.key);
|
||||||
|
color(ColorRole::Eq);
|
||||||
|
out("=");
|
||||||
|
color(ColorRole::AttributeValue);
|
||||||
|
out("{}{}{}", quote, attribute.value, quote);
|
||||||
|
color(ColorRole::Reset);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (element.children.is_empty()) {
|
||||||
|
color(ColorRole::Tag);
|
||||||
|
out("/>");
|
||||||
|
color(ColorRole::Reset);
|
||||||
|
} else {
|
||||||
|
color(ColorRole::Tag);
|
||||||
|
out(">");
|
||||||
|
color(ColorRole::Reset);
|
||||||
|
|
||||||
|
for (auto& node : element.children)
|
||||||
|
dump(node);
|
||||||
|
|
||||||
|
color(ColorRole::Tag);
|
||||||
|
out("</{}>", element.name);
|
||||||
|
color(ColorRole::Reset);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
static void dump(XML::Document& document)
|
||||||
|
{
|
||||||
|
if (!g_only_contents) {
|
||||||
|
{
|
||||||
|
color(ColorRole::PITag);
|
||||||
|
out("<?");
|
||||||
|
color(ColorRole::Reset);
|
||||||
|
color(ColorRole::PITarget);
|
||||||
|
out("xml");
|
||||||
|
color(ColorRole::Reset);
|
||||||
|
color(ColorRole::PIData);
|
||||||
|
out(" version='{}'", document.version() == XML::Version::Version10 ? "1.0" : "1.1");
|
||||||
|
color(ColorRole::Reset);
|
||||||
|
color(ColorRole::PITag);
|
||||||
|
outln("?>");
|
||||||
|
}
|
||||||
|
|
||||||
|
for (auto& pi : document.processing_instructions()) {
|
||||||
|
color(ColorRole::PITag);
|
||||||
|
out("<?");
|
||||||
|
color(ColorRole::Reset);
|
||||||
|
color(ColorRole::PITarget);
|
||||||
|
out("{}", pi.key);
|
||||||
|
color(ColorRole::Reset);
|
||||||
|
if (!pi.value.is_empty()) {
|
||||||
|
color(ColorRole::PIData);
|
||||||
|
out(" {}", pi.value);
|
||||||
|
color(ColorRole::Reset);
|
||||||
|
}
|
||||||
|
color(ColorRole::PITag);
|
||||||
|
outln("?>");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (auto maybe_doctype = document.doctype(); maybe_doctype.has_value()) {
|
||||||
|
auto& doctype = *maybe_doctype;
|
||||||
|
color(ColorRole::Doctype);
|
||||||
|
out("<!DOCTYPE ");
|
||||||
|
color(ColorRole::Tag);
|
||||||
|
out("{}", doctype.type);
|
||||||
|
if (!doctype.markup_declarations.is_empty()) {
|
||||||
|
color(ColorRole::Reset);
|
||||||
|
out(" [\n");
|
||||||
|
for (auto& entry : doctype.markup_declarations) {
|
||||||
|
entry.visit(
|
||||||
|
[&](XML::ElementDeclaration const& element) {
|
||||||
|
color(ColorRole::Doctype);
|
||||||
|
out(" <!ELEMENT ");
|
||||||
|
color(ColorRole::Tag);
|
||||||
|
out("{} ", element.type);
|
||||||
|
element.content_spec.visit(
|
||||||
|
[&](XML::ElementDeclaration::Empty const&) {
|
||||||
|
color(ColorRole::Keyword);
|
||||||
|
out("EMPTY");
|
||||||
|
},
|
||||||
|
[&](XML::ElementDeclaration::Any const&) {
|
||||||
|
color(ColorRole::Keyword);
|
||||||
|
out("ANY");
|
||||||
|
},
|
||||||
|
[&](XML::ElementDeclaration::Mixed const&) {
|
||||||
|
},
|
||||||
|
[&](XML::ElementDeclaration::Children const&) {
|
||||||
|
});
|
||||||
|
color(ColorRole::Doctype);
|
||||||
|
outln(">");
|
||||||
|
},
|
||||||
|
[&](XML::AttributeListDeclaration const& list) {
|
||||||
|
color(ColorRole::Doctype);
|
||||||
|
out(" <!ATTLIST ");
|
||||||
|
color(ColorRole::Tag);
|
||||||
|
out("{}", list.type);
|
||||||
|
for (auto& attribute : list.attributes) {
|
||||||
|
color(ColorRole::AttributeName);
|
||||||
|
out(" {} ", attribute.name);
|
||||||
|
color(ColorRole::Keyword);
|
||||||
|
attribute.type.visit(
|
||||||
|
[](XML::AttributeListDeclaration::StringType) {
|
||||||
|
out("CDATA");
|
||||||
|
},
|
||||||
|
[](XML::AttributeListDeclaration::TokenizedType type) {
|
||||||
|
switch (type) {
|
||||||
|
case XML::AttributeListDeclaration::TokenizedType::ID:
|
||||||
|
out("ID");
|
||||||
|
break;
|
||||||
|
case XML::AttributeListDeclaration::TokenizedType::IDRef:
|
||||||
|
out("IDREF");
|
||||||
|
break;
|
||||||
|
case XML::AttributeListDeclaration::TokenizedType::IDRefs:
|
||||||
|
out("IDREFS");
|
||||||
|
break;
|
||||||
|
case XML::AttributeListDeclaration::TokenizedType::Entity:
|
||||||
|
out("ENTITY");
|
||||||
|
break;
|
||||||
|
case XML::AttributeListDeclaration::TokenizedType::Entities:
|
||||||
|
out("ENTITIES");
|
||||||
|
break;
|
||||||
|
case XML::AttributeListDeclaration::TokenizedType::NMToken:
|
||||||
|
out("NMTOKEN");
|
||||||
|
break;
|
||||||
|
case XML::AttributeListDeclaration::TokenizedType::NMTokens:
|
||||||
|
out("NMTOKENS");
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
[](XML::AttributeListDeclaration::NotationType const& type) {
|
||||||
|
out("NOTATION ");
|
||||||
|
color(ColorRole::Reset);
|
||||||
|
out("( ");
|
||||||
|
bool first = true;
|
||||||
|
for (auto& name : type.names) {
|
||||||
|
color(ColorRole::Reset);
|
||||||
|
if (first)
|
||||||
|
first = false;
|
||||||
|
else
|
||||||
|
out(" | ");
|
||||||
|
color(ColorRole::AttributeValue);
|
||||||
|
out("{}", name);
|
||||||
|
}
|
||||||
|
color(ColorRole::Reset);
|
||||||
|
out(" )");
|
||||||
|
},
|
||||||
|
[](XML::AttributeListDeclaration::Enumeration const& type) {
|
||||||
|
color(ColorRole::Reset);
|
||||||
|
out("( ");
|
||||||
|
bool first = true;
|
||||||
|
for (auto& name : type.tokens) {
|
||||||
|
color(ColorRole::Reset);
|
||||||
|
if (first)
|
||||||
|
first = false;
|
||||||
|
else
|
||||||
|
out(" | ");
|
||||||
|
color(ColorRole::AttributeValue);
|
||||||
|
out("{}", name);
|
||||||
|
}
|
||||||
|
color(ColorRole::Reset);
|
||||||
|
out(" )");
|
||||||
|
});
|
||||||
|
out(" ");
|
||||||
|
attribute.default_.visit(
|
||||||
|
[](XML::AttributeListDeclaration::Required) {
|
||||||
|
color(ColorRole::Keyword);
|
||||||
|
out("#REQUIRED");
|
||||||
|
},
|
||||||
|
[](XML::AttributeListDeclaration::Implied) {
|
||||||
|
color(ColorRole::Keyword);
|
||||||
|
out("#IMPLIED");
|
||||||
|
},
|
||||||
|
[](XML::AttributeListDeclaration::Fixed const& fixed) {
|
||||||
|
color(ColorRole::Keyword);
|
||||||
|
out("#FIXED ");
|
||||||
|
color(ColorRole::AttributeValue);
|
||||||
|
out("\"{}\"", fixed.value);
|
||||||
|
},
|
||||||
|
[](XML::AttributeListDeclaration::DefaultValue const& default_) {
|
||||||
|
color(ColorRole::AttributeValue);
|
||||||
|
out("\"{}\"", default_.value);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
color(ColorRole::Doctype);
|
||||||
|
outln(">");
|
||||||
|
},
|
||||||
|
[&](XML::EntityDeclaration const& entity) {
|
||||||
|
color(ColorRole::Doctype);
|
||||||
|
out(" <!ENTITY ");
|
||||||
|
entity.visit(
|
||||||
|
[](XML::GEDeclaration const& declaration) {
|
||||||
|
color(ColorRole::Tag);
|
||||||
|
out("{} ", declaration.name);
|
||||||
|
declaration.definition.visit(
|
||||||
|
[](String const& value) {
|
||||||
|
color(ColorRole::AttributeValue);
|
||||||
|
out("\"{}\"", value);
|
||||||
|
},
|
||||||
|
[](XML::EntityDefinition const& definition) {
|
||||||
|
if (definition.id.public_id.has_value()) {
|
||||||
|
color(ColorRole::Keyword);
|
||||||
|
out("PUBLIC ");
|
||||||
|
color(ColorRole::PITarget);
|
||||||
|
out("\"{}\" ", definition.id.public_id->public_literal);
|
||||||
|
} else {
|
||||||
|
color(ColorRole::Keyword);
|
||||||
|
out("SYSTEM ");
|
||||||
|
}
|
||||||
|
color(ColorRole::PITarget);
|
||||||
|
out("\"{}\" ", definition.id.system_id.system_literal);
|
||||||
|
|
||||||
|
if (definition.notation.has_value()) {
|
||||||
|
color(ColorRole::Keyword);
|
||||||
|
out(" NDATA ");
|
||||||
|
color(ColorRole::PITarget);
|
||||||
|
out("{}", *definition.notation);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
color(ColorRole::Tag);
|
||||||
|
outln(">");
|
||||||
|
},
|
||||||
|
[](XML::PEDeclaration const& declaration) {
|
||||||
|
color(ColorRole::Tag);
|
||||||
|
out("{} ", declaration.name);
|
||||||
|
declaration.definition.visit(
|
||||||
|
[](String const& value) {
|
||||||
|
color(ColorRole::AttributeValue);
|
||||||
|
out("\"{}\"", value);
|
||||||
|
},
|
||||||
|
[](XML::ExternalID const& id) {
|
||||||
|
if (id.public_id.has_value()) {
|
||||||
|
color(ColorRole::Keyword);
|
||||||
|
out("PUBLIC ");
|
||||||
|
color(ColorRole::PITarget);
|
||||||
|
out("\"{}\" ", id.public_id->public_literal);
|
||||||
|
} else {
|
||||||
|
color(ColorRole::Keyword);
|
||||||
|
out("SYSTEM ");
|
||||||
|
}
|
||||||
|
color(ColorRole::PITarget);
|
||||||
|
out("\"{}\"", id.system_id.system_literal);
|
||||||
|
});
|
||||||
|
color(ColorRole::Tag);
|
||||||
|
outln(">");
|
||||||
|
});
|
||||||
|
},
|
||||||
|
[&](XML::NotationDeclaration const&) {
|
||||||
|
|
||||||
|
});
|
||||||
|
}
|
||||||
|
color(ColorRole::Reset);
|
||||||
|
out("]");
|
||||||
|
}
|
||||||
|
color(ColorRole::Doctype);
|
||||||
|
outln(">");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
dump(document.root());
|
||||||
|
}
|
||||||
|
|
||||||
|
static String s_path;
|
||||||
|
static auto parse(StringView contents)
|
||||||
|
{
|
||||||
|
return XML::Parser {
|
||||||
|
contents,
|
||||||
|
{
|
||||||
|
.preserve_comments = true,
|
||||||
|
.resolve_external_resource = [&](XML::SystemID const& system_id, Optional<XML::PublicID> const&) -> ErrorOr<String> {
|
||||||
|
auto base = URL::create_with_file_scheme(s_path);
|
||||||
|
auto url = URLParser::parse(system_id.system_literal, &base);
|
||||||
|
if (!url.is_valid())
|
||||||
|
return Error::from_string_literal("Invalid URL");
|
||||||
|
|
||||||
|
if (url.scheme() != "file")
|
||||||
|
return Error::from_string_literal("NYI: Nonlocal entity");
|
||||||
|
|
||||||
|
auto file = TRY(Core::File::open(url.path(), Core::OpenMode::ReadOnly));
|
||||||
|
return String::copy(file->read_all());
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
enum class TestResult {
|
||||||
|
Passed,
|
||||||
|
Failed,
|
||||||
|
RunnerFailed,
|
||||||
|
};
|
||||||
|
static HashMap<String, TestResult> s_test_results {};
|
||||||
|
static void do_run_tests(XML::Document& document)
|
||||||
|
{
|
||||||
|
auto& root = document.root().content.get<XML::Node::Element>();
|
||||||
|
VERIFY(root.name == "TESTSUITE");
|
||||||
|
Queue<XML::Node*> suites;
|
||||||
|
auto dump_cases = [&](auto& root) {
|
||||||
|
for (auto& node : root.children) {
|
||||||
|
auto element = node.content.template get_pointer<XML::Node::Element>();
|
||||||
|
if (!element)
|
||||||
|
continue;
|
||||||
|
if (element->name != "TESTCASES" && element->name != "TEST")
|
||||||
|
continue;
|
||||||
|
suites.enqueue(&node);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
dump_cases(root);
|
||||||
|
|
||||||
|
auto base_path = LexicalPath::dirname(s_path);
|
||||||
|
|
||||||
|
while (!suites.is_empty()) {
|
||||||
|
auto& node = *suites.dequeue();
|
||||||
|
auto& suite = node.content.get<XML::Node::Element>();
|
||||||
|
if (suite.name == "TESTCASES") {
|
||||||
|
dump_cases(suite);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (suite.name == "TEST") {
|
||||||
|
Vector<StringView> bases;
|
||||||
|
for (auto* parent = node.parent; parent; parent = parent->parent) {
|
||||||
|
auto& attributes = parent->content.get<XML::Node::Element>().attributes;
|
||||||
|
auto it = attributes.find("xml:base");
|
||||||
|
if (it == attributes.end())
|
||||||
|
continue;
|
||||||
|
bases.append(it->value);
|
||||||
|
}
|
||||||
|
|
||||||
|
auto type = suite.attributes.find("TYPE")->value;
|
||||||
|
|
||||||
|
StringBuilder path_builder;
|
||||||
|
path_builder.append(base_path);
|
||||||
|
path_builder.append("/");
|
||||||
|
for (auto& entry : bases.in_reverse()) {
|
||||||
|
path_builder.append(entry);
|
||||||
|
path_builder.append("/");
|
||||||
|
}
|
||||||
|
auto test_base_path = path_builder.to_string();
|
||||||
|
|
||||||
|
path_builder.append(suite.attributes.find("URI")->value);
|
||||||
|
auto url = URL::create_with_file_scheme(path_builder.string_view());
|
||||||
|
if (!url.is_valid()) {
|
||||||
|
warnln("Invalid URL {}", path_builder.string_view());
|
||||||
|
s_test_results.set(path_builder.string_view(), TestResult::RunnerFailed);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
auto file_result = Core::File::open(url.path(), Core::OpenMode::ReadOnly);
|
||||||
|
if (file_result.is_error()) {
|
||||||
|
warnln("Read error for {}: {}", url.path(), file_result.error());
|
||||||
|
s_test_results.set(url.path(), TestResult::RunnerFailed);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
warnln("Running test {}", url.path());
|
||||||
|
|
||||||
|
auto contents = file_result.value()->read_all();
|
||||||
|
auto parser = parse(contents);
|
||||||
|
auto doc_or_error = parser.parse();
|
||||||
|
if (doc_or_error.is_error()) {
|
||||||
|
if (type == "invalid" || type == "error" || type == "not-wf")
|
||||||
|
s_test_results.set(url.path(), TestResult::Passed);
|
||||||
|
else
|
||||||
|
s_test_results.set(url.path(), TestResult::Failed);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
auto out = suite.attributes.find("OUTPUT");
|
||||||
|
if (out != suite.attributes.end()) {
|
||||||
|
auto out_path = LexicalPath::join(test_base_path, out->value).string();
|
||||||
|
auto file_result = Core::File::open(out_path, Core::OpenMode::ReadOnly);
|
||||||
|
if (file_result.is_error()) {
|
||||||
|
warnln("Read error for {}: {}", out_path, file_result.error());
|
||||||
|
s_test_results.set(url.path(), TestResult::RunnerFailed);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
auto contents = file_result.value()->read_all();
|
||||||
|
auto parser = parse(contents);
|
||||||
|
auto out_doc_or_error = parser.parse();
|
||||||
|
if (out_doc_or_error.is_error()) {
|
||||||
|
warnln("Parse error for {}: {}", out_path, out_doc_or_error.error());
|
||||||
|
s_test_results.set(url.path(), TestResult::RunnerFailed);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
auto out_doc = out_doc_or_error.release_value();
|
||||||
|
if (out_doc.root() != doc_or_error.value().root()) {
|
||||||
|
s_test_results.set(url.path(), TestResult::Failed);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (type == "invalid" || type == "error" || type == "not-wf")
|
||||||
|
s_test_results.set(url.path(), TestResult::Failed);
|
||||||
|
else
|
||||||
|
s_test_results.set(url.path(), TestResult::Passed);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
ErrorOr<int> serenity_main(Main::Arguments arguments)
|
||||||
|
{
|
||||||
|
StringView filename;
|
||||||
|
bool run_tests { false };
|
||||||
|
|
||||||
|
Core::ArgsParser parser;
|
||||||
|
parser.set_general_help("Parse and dump XML files");
|
||||||
|
parser.add_option(g_color, "Syntax highlight the output", "color", 'c');
|
||||||
|
parser.add_option(g_only_contents, "Only display markup and text", "only-contents", 'o');
|
||||||
|
parser.add_option(run_tests, "Run tests", "run-tests", 't');
|
||||||
|
parser.add_positional_argument(filename, "File to read from", "file");
|
||||||
|
parser.parse(arguments);
|
||||||
|
|
||||||
|
s_path = Core::File::real_path_for(filename);
|
||||||
|
auto file = TRY(Core::File::open(s_path, Core::OpenMode::ReadOnly));
|
||||||
|
auto contents = file->read_all();
|
||||||
|
|
||||||
|
auto xml_parser = parse(contents);
|
||||||
|
auto result = xml_parser.parse();
|
||||||
|
if (result.is_error()) {
|
||||||
|
// Technically this is a UAF, but the referenced string data won't be overwritten by anything at this point.
|
||||||
|
if (xml_parser.parse_error_causes().is_empty())
|
||||||
|
return Error::from_string_literal(String::formatted("{}", result.error()));
|
||||||
|
|
||||||
|
StringBuilder builder;
|
||||||
|
builder.join("\n", xml_parser.parse_error_causes(), " {}");
|
||||||
|
return Error::from_string_literal(
|
||||||
|
String::formatted("{}; caused by:\n{}", result.error(), builder.string_view()));
|
||||||
|
}
|
||||||
|
|
||||||
|
auto doc = result.release_value();
|
||||||
|
if (run_tests) {
|
||||||
|
do_run_tests(doc);
|
||||||
|
size_t passed = 0;
|
||||||
|
size_t failed = 0;
|
||||||
|
size_t runner_error = 0;
|
||||||
|
size_t total = 0;
|
||||||
|
for (auto& entry : s_test_results) {
|
||||||
|
total++;
|
||||||
|
switch (entry.value) {
|
||||||
|
case TestResult::Passed:
|
||||||
|
passed++;
|
||||||
|
break;
|
||||||
|
case TestResult::Failed:
|
||||||
|
failed++;
|
||||||
|
break;
|
||||||
|
case TestResult::RunnerFailed:
|
||||||
|
runner_error++;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
outln("{} passed, {} failed, {} runner failed of {} tests run.", passed, failed, runner_error, total);
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
dump(doc);
|
||||||
|
if (!g_only_contents)
|
||||||
|
outln();
|
||||||
|
|
||||||
|
return 0;
|
||||||
|
}
|
Loading…
Add table
Add a link
Reference in a new issue