diff options
Diffstat (limited to 'Userland')
124 files changed, 20454 insertions, 0 deletions
diff --git a/Userland/CMakeLists.txt b/Userland/CMakeLists.txt index 8d83e2237e..f99c8daaa9 100644 --- a/Userland/CMakeLists.txt +++ b/Userland/CMakeLists.txt @@ -1,5 +1,6 @@ add_subdirectory(Applications) add_subdirectory(Demos) +add_subdirectory(DevTools) add_subdirectory(DynamicLoader) add_subdirectory(Games) add_subdirectory(Libraries) diff --git a/Userland/DevTools/CMakeLists.txt b/Userland/DevTools/CMakeLists.txt new file mode 100644 index 0000000000..b0d5aee0e3 --- /dev/null +++ b/Userland/DevTools/CMakeLists.txt @@ -0,0 +1,5 @@ +add_subdirectory(HackStudio) +add_subdirectory(Inspector) +add_subdirectory(Playground) +add_subdirectory(Profiler) +add_subdirectory(UserspaceEmulator) diff --git a/Userland/DevTools/HackStudio/AutoCompleteResponse.h b/Userland/DevTools/HackStudio/AutoCompleteResponse.h new file mode 100644 index 0000000000..70023bf7ec --- /dev/null +++ b/Userland/DevTools/HackStudio/AutoCompleteResponse.h @@ -0,0 +1,67 @@ +/* + * Copyright (c) 2020, the SerenityOS developers. + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * 1. Redistributions of source code must retain the above copyright notice, this + * list of conditions and the following disclaimer. + * + * 2. Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE + * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL + * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR + * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER + * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, + * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE + * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +#pragma once + +#include <AK/String.h> +#include <AK/Types.h> +#include <LibGUI/AutocompleteProvider.h> +#include <LibIPC/Decoder.h> +#include <LibIPC/Encoder.h> + +namespace IPC { + +template<> +inline bool encode(IPC::Encoder& encoder, const GUI::AutocompleteProvider::Entry& response) +{ + encoder << response.completion; + encoder << (u64)response.partial_input_length; + encoder << (u32)response.kind; + encoder << (u32)response.language; + return true; +} + +template<> +inline bool decode(IPC::Decoder& decoder, GUI::AutocompleteProvider::Entry& response) +{ + u32 kind = 0; + u32 language = 0; + u64 partial_input_length = 0; + bool ok = decoder.decode(response.completion) + && decoder.decode(partial_input_length) + && decoder.decode(kind) + && decoder.decode(language); + + if (ok) { + response.kind = static_cast<GUI::AutocompleteProvider::CompletionKind>(kind); + response.language = static_cast<GUI::AutocompleteProvider::Language>(language); + response.partial_input_length = partial_input_length; + } + + return ok; +} + +} diff --git a/Userland/DevTools/HackStudio/CMakeLists.txt b/Userland/DevTools/HackStudio/CMakeLists.txt new file mode 100644 index 0000000000..59406cbcbc --- /dev/null +++ b/Userland/DevTools/HackStudio/CMakeLists.txt @@ -0,0 +1,37 @@ +add_subdirectory(LanguageServers) +add_subdirectory(LanguageClients) + +set(SOURCES + CodeDocument.cpp + CursorTool.cpp + Debugger/BacktraceModel.cpp + Debugger/DebugInfoWidget.cpp + Debugger/Debugger.cpp + Debugger/DisassemblyModel.cpp + Debugger/DisassemblyWidget.cpp + Debugger/RegistersModel.cpp + Debugger/VariablesModel.cpp + Editor.cpp + EditorWrapper.cpp + FindInFilesWidget.cpp + FormEditorWidget.cpp + FormWidget.cpp + Git/DiffViewer.cpp + Git/GitFilesModel.cpp + Git/GitFilesView.cpp + Git/GitRepo.cpp + Git/GitWidget.cpp + HackStudioWidget.cpp + LanguageClient.cpp + Locator.cpp + Project.cpp + ProjectFile.cpp + TerminalWrapper.cpp + WidgetTool.cpp + WidgetTreeModel.cpp + main.cpp +) + +serenity_app(HackStudio ICON app-hack-studio) +target_link_libraries(HackStudio LibWeb LibMarkdown LibGUI LibGfx LibCore LibVT LibDebug LibX86 LibDiff LibShell) +add_dependencies(HackStudio CppLanguageServer) diff --git a/Userland/DevTools/HackStudio/CodeDocument.cpp b/Userland/DevTools/HackStudio/CodeDocument.cpp new file mode 100644 index 0000000000..54f7e4d5f6 --- /dev/null +++ b/Userland/DevTools/HackStudio/CodeDocument.cpp @@ -0,0 +1,68 @@ +/* + * Copyright (c) 2020, Itamar S. <itamar8910@gmail.com> + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * 1. Redistributions of source code must retain the above copyright notice, this + * list of conditions and the following disclaimer. + * + * 2. Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE + * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL + * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR + * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER + * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, + * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE + * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +#include "CodeDocument.h" + +namespace HackStudio { + +NonnullRefPtr<CodeDocument> CodeDocument::create(const String& file_path, Client* client) +{ + return adopt(*new CodeDocument(file_path, client)); +} + +NonnullRefPtr<CodeDocument> CodeDocument::create(Client* client) +{ + return adopt(*new CodeDocument(client)); +} + +CodeDocument::CodeDocument(const String& file_path, Client* client) + : TextDocument(client) + , m_file_path(file_path) +{ + LexicalPath lexical_path(file_path); + + if (lexical_path.has_extension(".cpp") || lexical_path.has_extension(".h")) + m_language = Language::Cpp; + else if (lexical_path.has_extension(".js")) + m_language = Language::JavaScript; + else if (lexical_path.has_extension(".gml")) + m_language = Language::GML; + else if (lexical_path.has_extension(".ini")) + m_language = Language::Ini; + else if (lexical_path.has_extension(".sh")) + m_language = Language::Shell; +} + +CodeDocument::CodeDocument(Client* client) + : TextDocument(client) +{ +} + +CodeDocument::~CodeDocument() +{ +} + +} diff --git a/Userland/DevTools/HackStudio/CodeDocument.h b/Userland/DevTools/HackStudio/CodeDocument.h new file mode 100644 index 0000000000..a0e2f3136f --- /dev/null +++ b/Userland/DevTools/HackStudio/CodeDocument.h @@ -0,0 +1,61 @@ +/* + * Copyright (c) 2020, Itamar S. <itamar8910@gmail.com> + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * 1. Redistributions of source code must retain the above copyright notice, this + * list of conditions and the following disclaimer. + * + * 2. Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE + * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL + * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR + * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER + * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, + * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE + * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +#pragma once + +#include "Language.h" +#include <AK/LexicalPath.h> +#include <LibGUI/TextDocument.h> + +namespace HackStudio { + +class CodeDocument final : public GUI::TextDocument { +public: + virtual ~CodeDocument() override; + static NonnullRefPtr<CodeDocument> create(const String& file_path, Client* client = nullptr); + static NonnullRefPtr<CodeDocument> create(Client* client = nullptr); + + const Vector<size_t>& breakpoint_lines() const { return m_breakpoint_lines; } + Vector<size_t>& breakpoint_lines() { return m_breakpoint_lines; } + Optional<size_t> execution_position() const { return m_execution_position; } + void set_execution_position(size_t line) { m_execution_position = line; } + void clear_execution_position() { m_execution_position.clear(); } + const String& file_path() const { return m_file_path; } + Language language() const { return m_language; } + + virtual bool is_code_document() const override final { return true; } + +private: + explicit CodeDocument(const String& file_path, Client* client = nullptr); + explicit CodeDocument(Client* client = nullptr); + + String m_file_path; + Language m_language { Language::Unknown }; + Vector<size_t> m_breakpoint_lines; + Optional<size_t> m_execution_position; +}; + +} diff --git a/Userland/DevTools/HackStudio/CursorTool.cpp b/Userland/DevTools/HackStudio/CursorTool.cpp new file mode 100644 index 0000000000..b59963af7c --- /dev/null +++ b/Userland/DevTools/HackStudio/CursorTool.cpp @@ -0,0 +1,204 @@ +/* + * Copyright (c) 2018-2020, Andreas Kling <kling@serenityos.org> + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * 1. Redistributions of source code must retain the above copyright notice, this + * list of conditions and the following disclaimer. + * + * 2. Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE + * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL + * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR + * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER + * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, + * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE + * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +#include "CursorTool.h" +#include "FormEditorWidget.h" +#include "FormWidget.h" +#include "WidgetTreeModel.h" +#include <AK/LogStream.h> +#include <LibGfx/Palette.h> + +//#define DEBUG_CURSOR_TOOL + +namespace HackStudio { + +void CursorTool::on_mousedown(GUI::MouseEvent& event) +{ +#ifdef DEBUG_CURSOR_TOOL + dbgln("CursorTool::on_mousedown"); +#endif + auto& form_widget = m_editor.form_widget(); + auto result = form_widget.hit_test(event.position(), GUI::Widget::ShouldRespectGreediness::No); + + if (event.button() == GUI::MouseButton::Left) { + if (result.widget && result.widget != &form_widget) { + if (event.modifiers() & Mod_Ctrl) { + m_editor.selection().toggle(*result.widget); + } else if (!event.modifiers()) { + if (!m_editor.selection().contains(*result.widget)) { +#ifdef DEBUG_CURSOR_TOOL + dbg() << "Selection didn't contain " << *result.widget << ", making it the only selected one"; +#endif + m_editor.selection().set(*result.widget); + } + + m_drag_origin = event.position(); + m_positions_before_drag.clear(); + m_editor.selection().for_each([&](auto& widget) { + m_positions_before_drag.set(&widget, widget.relative_position()); + return IterationDecision::Continue; + }); + } + } else { + m_editor.selection().clear(); + m_rubber_banding = true; + m_rubber_band_origin = event.position(); + m_rubber_band_position = event.position(); + form_widget.update(); + } + // FIXME: Do we need to update any part of the FormEditorWidget outside the FormWidget? + form_widget.update(); + } +} + +void CursorTool::on_mouseup(GUI::MouseEvent& event) +{ +#ifdef DEBUG_CURSOR_TOOL + dbgln("CursorTool::on_mouseup"); +#endif + if (event.button() == GUI::MouseButton::Left) { + auto& form_widget = m_editor.form_widget(); + auto result = form_widget.hit_test(event.position(), GUI::Widget::ShouldRespectGreediness::No); + if (!m_dragging && !(event.modifiers() & Mod_Ctrl)) { + if (result.widget && result.widget != &form_widget) { + m_editor.selection().set(*result.widget); + // FIXME: Do we need to update any part of the FormEditorWidget outside the FormWidget? + form_widget.update(); + } + } + m_dragging = false; + m_rubber_banding = false; + form_widget.update(); + } +} + +void CursorTool::on_mousemove(GUI::MouseEvent& event) +{ +#ifdef DEBUG_CURSOR_TOOL + dbgln("CursorTool::on_mousemove"); +#endif + auto& form_widget = m_editor.form_widget(); + + if (m_rubber_banding) { + set_rubber_band_position(event.position()); + return; + } + + if (!m_dragging && event.buttons() & GUI::MouseButton::Left && event.position() != m_drag_origin) { + auto result = form_widget.hit_test(event.position(), GUI::Widget::ShouldRespectGreediness::No); + if (result.widget && result.widget != &form_widget) { + if (!m_editor.selection().contains(*result.widget)) { + m_editor.selection().set(*result.widget); + // FIXME: Do we need to update any part of the FormEditorWidget outside the FormWidget? + form_widget.update(); + } + } + m_dragging = true; + } + + if (m_dragging) { + auto movement_delta = event.position() - m_drag_origin; + m_editor.selection().for_each([&](auto& widget) { + auto new_rect = widget.relative_rect(); + new_rect.set_location(m_positions_before_drag.get(&widget).value_or({}).translated(movement_delta)); + new_rect.set_x(new_rect.x() - (new_rect.x() % m_editor.form_widget().grid_size())); + new_rect.set_y(new_rect.y() - (new_rect.y() % m_editor.form_widget().grid_size())); + widget.set_relative_rect(new_rect); + return IterationDecision::Continue; + }); + m_editor.model().update(); + return; + } +} + +void CursorTool::on_keydown(GUI::KeyEvent& event) +{ +#ifdef DEBUG_CURSOR_TOOL + dbgln("CursorTool::on_keydown"); +#endif + + auto move_selected_widgets_by = [this](int x, int y) { + m_editor.selection().for_each([&](auto& widget) { + widget.move_by(x, y); + return IterationDecision::Continue; + }); + }; + + if (event.modifiers() == 0) { + switch (event.key()) { + case Key_Down: + move_selected_widgets_by(0, m_editor.form_widget().grid_size()); + break; + case Key_Up: + move_selected_widgets_by(0, -m_editor.form_widget().grid_size()); + break; + case Key_Left: + move_selected_widgets_by(-m_editor.form_widget().grid_size(), 0); + break; + case Key_Right: + move_selected_widgets_by(m_editor.form_widget().grid_size(), 0); + break; + default: + break; + } + } +} + +void CursorTool::set_rubber_band_position(const Gfx::IntPoint& position) +{ + if (m_rubber_band_position == position) + return; + m_rubber_band_position = position; + + auto rubber_band_rect = this->rubber_band_rect(); + + m_editor.selection().clear(); + m_editor.form_widget().for_each_child_widget([&](auto& child) { + if (child.relative_rect().intersects(rubber_band_rect)) + m_editor.selection().add(child); + return IterationDecision::Continue; + }); + + m_editor.form_widget().update(); +} + +Gfx::IntRect CursorTool::rubber_band_rect() const +{ + if (!m_rubber_banding) + return {}; + return Gfx::IntRect::from_two_points(m_rubber_band_origin, m_rubber_band_position); +} + +void CursorTool::on_second_paint(GUI::Painter& painter, GUI::PaintEvent&) +{ + if (!m_rubber_banding) + return; + auto rect = rubber_band_rect(); + painter.fill_rect(rect, m_editor.palette().rubber_band_fill()); + painter.draw_rect(rect, m_editor.palette().rubber_band_border()); +} + +} diff --git a/Userland/DevTools/HackStudio/CursorTool.h b/Userland/DevTools/HackStudio/CursorTool.h new file mode 100644 index 0000000000..cf216e77c4 --- /dev/null +++ b/Userland/DevTools/HackStudio/CursorTool.h @@ -0,0 +1,64 @@ +/* + * Copyright (c) 2018-2020, Andreas Kling <kling@serenityos.org> + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * 1. Redistributions of source code must retain the above copyright notice, this + * list of conditions and the following disclaimer. + * + * 2. Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE + * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL + * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR + * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER + * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, + * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE + * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +#pragma once + +#include "Tool.h" +#include <AK/HashMap.h> +#include <LibGUI/Forward.h> +#include <LibGfx/Point.h> + +namespace HackStudio { + +class CursorTool final : public Tool { +public: + explicit CursorTool(FormEditorWidget& editor) + : Tool(editor) + { + } + virtual ~CursorTool() override { } + +private: + virtual const char* class_name() const override { return "CursorTool"; } + virtual void on_mousedown(GUI::MouseEvent&) override; + virtual void on_mouseup(GUI::MouseEvent&) override; + virtual void on_mousemove(GUI::MouseEvent&) override; + virtual void on_keydown(GUI::KeyEvent&) override; + virtual void on_second_paint(GUI::Painter&, GUI::PaintEvent&) override; + + void set_rubber_band_position(const Gfx::IntPoint&); + Gfx::IntRect rubber_band_rect() const; + + Gfx::IntPoint m_drag_origin; + HashMap<GUI::Widget*, Gfx::IntPoint> m_positions_before_drag; + bool m_dragging { false }; + + bool m_rubber_banding { false }; + Gfx::IntPoint m_rubber_band_origin; + Gfx::IntPoint m_rubber_band_position; +}; + +} diff --git a/Userland/DevTools/HackStudio/Debugger/BacktraceModel.cpp b/Userland/DevTools/HackStudio/Debugger/BacktraceModel.cpp new file mode 100644 index 0000000000..fe0f6a31f5 --- /dev/null +++ b/Userland/DevTools/HackStudio/Debugger/BacktraceModel.cpp @@ -0,0 +1,78 @@ +/* + * Copyright (c) 2020, Itamar S. <itamar8910@gmail.com> + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * 1. Redistributions of source code must retain the above copyright notice, this + * list of conditions and the following disclaimer. + * + * 2. Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE + * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL + * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR + * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER + * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, + * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE + * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +#include "BacktraceModel.h" +#include "Debugger.h" +#include <LibDebug/StackFrameUtils.h> + +namespace HackStudio { + +NonnullRefPtr<BacktraceModel> BacktraceModel::create(const Debug::DebugSession& debug_session, const PtraceRegisters& regs) +{ + return adopt(*new BacktraceModel(create_backtrace(debug_session, regs))); +} + +GUI::Variant BacktraceModel::data(const GUI::ModelIndex& index, GUI::ModelRole role) const +{ + if (role == GUI::ModelRole::Display) { + auto& frame = m_frames.at(index.row()); + return frame.function_name; + } + return {}; +} + +GUI::ModelIndex BacktraceModel::index(int row, int column, const GUI::ModelIndex&) const +{ + if (row < 0 || row >= static_cast<int>(m_frames.size())) + return {}; + return create_index(row, column, &m_frames.at(row)); +} + +Vector<BacktraceModel::FrameInfo> BacktraceModel::create_backtrace(const Debug::DebugSession& debug_session, const PtraceRegisters& regs) +{ + u32 current_ebp = regs.ebp; + u32 current_instruction = regs.eip; + Vector<BacktraceModel::FrameInfo> frames; + do { + auto lib = debug_session.library_at(regs.eip); + if (!lib) + continue; + String name = lib->debug_info->name_of_containing_function(current_instruction - lib->base_address); + if (name.is_null()) { + dbgln("BacktraceModel: couldn't find containing function for address: {:p}", current_instruction); + name = "<missing>"; + } + + frames.append({ name, current_instruction, current_ebp }); + auto frame_info = Debug::StackFrameUtils::get_info(*Debugger::the().session(), current_ebp); + ASSERT(frame_info.has_value()); + current_instruction = frame_info.value().return_address; + current_ebp = frame_info.value().next_ebp; + } while (current_ebp && current_instruction); + return frames; +} + +} diff --git a/Userland/DevTools/HackStudio/Debugger/BacktraceModel.h b/Userland/DevTools/HackStudio/Debugger/BacktraceModel.h new file mode 100644 index 0000000000..06d697c388 --- /dev/null +++ b/Userland/DevTools/HackStudio/Debugger/BacktraceModel.h @@ -0,0 +1,78 @@ +/* + * Copyright (c) 2020, Itamar S. <itamar8910@gmail.com> + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * 1. Redistributions of source code must retain the above copyright notice, this + * list of conditions and the following disclaimer. + * + * 2. Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE + * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL + * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR + * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER + * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, + * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE + * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +#pragma once + +#include <AK/Vector.h> +#include <LibGUI/ListView.h> +#include <LibGUI/Model.h> +#include <sys/arch/i386/regs.h> + +namespace Debug { + +class DebugSession; + +} + +namespace HackStudio { + +class BacktraceModel final : public GUI::Model { +public: + static NonnullRefPtr<BacktraceModel> create(const Debug::DebugSession&, const PtraceRegisters& regs); + + virtual int row_count(const GUI::ModelIndex& = GUI::ModelIndex()) const override { return m_frames.size(); } + virtual int column_count(const GUI::ModelIndex& = GUI::ModelIndex()) const override { return 1; } + + virtual String column_name(int) const override + { + return ""; + } + + virtual GUI::Variant data(const GUI::ModelIndex&, GUI::ModelRole) const override; + + virtual void update() override { } + virtual GUI::ModelIndex index(int row, int column, const GUI::ModelIndex&) const override; + + struct FrameInfo { + String function_name; + u32 instruction_address; + u32 frame_base; + }; + + const Vector<FrameInfo>& frames() const { return m_frames; } + +private: + explicit BacktraceModel(Vector<FrameInfo>&& frames) + : m_frames(move(frames)) + { + } + + static Vector<FrameInfo> create_backtrace(const Debug::DebugSession&, const PtraceRegisters&); + + Vector<FrameInfo> m_frames; +}; + +} diff --git a/Userland/DevTools/HackStudio/Debugger/BreakpointCallback.h b/Userland/DevTools/HackStudio/Debugger/BreakpointCallback.h new file mode 100644 index 0000000000..6131e492ec --- /dev/null +++ b/Userland/DevTools/HackStudio/Debugger/BreakpointCallback.h @@ -0,0 +1,42 @@ +/* + * Copyright (c) 2020, Itamar S. <itamar8910@gmail.com> + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * 1. Redistributions of source code must retain the above copyright notice, this + * list of conditions and the following disclaimer. + * + * 2. Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE + * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL + * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR + * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER + * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, + * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE + * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +#pragma once + +#include <AK/Function.h> +#include <AK/String.h> +#include <AK/Types.h> + +namespace HackStudio { + +enum class BreakpointChange { + Added, + Removed, +}; + +typedef Function<void(const String& file, size_t line, BreakpointChange)> BreakpointChangeCallback; + +} diff --git a/Userland/DevTools/HackStudio/Debugger/DebugInfoWidget.cpp b/Userland/DevTools/HackStudio/Debugger/DebugInfoWidget.cpp new file mode 100644 index 0000000000..622538057d --- /dev/null +++ b/Userland/DevTools/HackStudio/Debugger/DebugInfoWidget.cpp @@ -0,0 +1,188 @@ +/* + * Copyright (c) 2020, Itamar S. <itamar8910@gmail.com> + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * 1. Redistributions of source code must retain the above copyright notice, this + * list of conditions and the following disclaimer. + * + * 2. Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE + * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL + * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR + * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER + * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, + * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE + * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +#include "DebugInfoWidget.h" +#include "BacktraceModel.h" +#include "Debugger.h" +#include "RegistersModel.h" +#include "VariablesModel.h" +#include <AK/StringBuilder.h> +#include <LibGUI/Action.h> +#include <LibGUI/BoxLayout.h> +#include <LibGUI/InputBox.h> +#include <LibGUI/Layout.h> +#include <LibGUI/ListView.h> +#include <LibGUI/Menu.h> +#include <LibGUI/Model.h> +#include <LibGUI/Splitter.h> +#include <LibGUI/TabWidget.h> +#include <LibGUI/TreeView.h> + +namespace HackStudio { + +void DebugInfoWidget::init_toolbar() +{ + m_continue_action = GUI::Action::create("Continue", Gfx::Bitmap::load_from_file("/res/icons/16x16/debug-continue.png"), [](auto&) { + Debugger::the().set_requested_debugger_action(Debugger::DebuggerAction::Continue); + }); + + m_singlestep_action = GUI::Action::create("Step Over", { Mod_None, Key_F10 }, Gfx::Bitmap::load_from_file("/res/icons/16x16/debug-step-over.png"), [](auto&) { + Debugger::the().set_requested_debugger_action(Debugger::DebuggerAction::SourceStepOver); + }); + + m_step_in_action = GUI::Action::create("Step In", { Mod_None, Key_F11 }, Gfx::Bitmap::load_from_file("/res/icons/16x16/debug-step-in.png"), [](auto&) { + Debugger::the().set_requested_debugger_action(Debugger::DebuggerAction::SourceSingleStep); + }); + + m_step_out_action = GUI::Action::create("Step Out", { Mod_Shift, Key_F11 }, Gfx::Bitmap::load_from_file("/res/icons/16x16/debug-step-out.png"), [](auto&) { + Debugger::the().set_requested_debugger_action(Debugger::DebuggerAction::SourceStepOut); + }); + + m_toolbar->add_action(*m_continue_action); + m_toolbar->add_action(*m_singlestep_action); + m_toolbar->add_action(*m_step_in_action); + m_toolbar->add_action(*m_step_out_action); + + set_debug_actions_enabled(false); +} + +DebugInfoWidget::DebugInfoWidget() +{ + set_layout<GUI::VerticalBoxLayout>(); + auto& toolbar_container = add<GUI::ToolBarContainer>(); + m_toolbar = toolbar_container.add<GUI::ToolBar>(); + init_toolbar(); + auto& bottom_box = add<GUI::Widget>(); + bottom_box.set_layout<GUI::HorizontalBoxLayout>(); + + auto& splitter = bottom_box.add<GUI::HorizontalSplitter>(); + m_backtrace_view = splitter.add<GUI::ListView>(); + auto& variables_tab_widget = splitter.add<GUI::TabWidget>(); + variables_tab_widget.set_tab_position(GUI::TabWidget::TabPosition::Bottom); + variables_tab_widget.add_widget("Variables", build_variables_tab()); + variables_tab_widget.add_widget("Registers", build_registers_tab()); + + m_backtrace_view->on_selection = [this](auto& index) { + auto& model = static_cast<BacktraceModel&>(*m_backtrace_view->model()); + + // Note: The reconstruction of the register set here is obviously incomplete. + // We currently only reconstruct eip & ebp. Ideally would also reconstruct the other registers somehow. + // (Other registers may be needed to get the values of variables who are not stored on the stack) + PtraceRegisters frame_regs {}; + frame_regs.eip = model.frames()[index.row()].instruction_address; + frame_regs.ebp = model.frames()[index.row()].frame_base; + + m_variables_view->set_model(VariablesModel::create(frame_regs)); + }; +} + +NonnullRefPtr<GUI::Widget> DebugInfoWidget::build_variables_tab() +{ + auto variables_widget = GUI::Widget::construct(); + variables_widget->set_layout<GUI::HorizontalBoxLayout>(); + + m_variables_view = variables_widget->add<GUI::TreeView>(); + + auto is_valid_index = [](auto& index) { + if (!index.is_valid()) + return false; + auto* variable = static_cast<const Debug::DebugInfo::VariableInfo*>(index.internal_data()); + if (variable->location_type != Debug::DebugInfo::VariableInfo::LocationType::Address) + return false; + return variable->is_enum_type() || variable->type_name.is_one_of("int", "bool"); + }; + + m_variables_view->on_context_menu_request = [this, is_valid_index](auto& index, auto& event) { + if (!is_valid_index(index)) + return; + m_variable_context_menu->popup(event.screen_position()); + }; + + m_variables_view->on_activation = [this, is_valid_index](auto& index) { + if (!is_valid_index(index)) + return; + + String value; + if (GUI::InputBox::show(value, window(), "Enter new value:", "Set variable value") == GUI::InputBox::ExecOK) { + auto& model = static_cast<VariablesModel&>(*m_variables_view->model()); + model.set_variable_value(index, value, window()); + } + }; + + auto edit_variable_action = GUI::Action::create("Change value", [this](auto&) { + m_variables_view->on_activation(m_variables_view->selection().first()); + }); + + m_variable_context_menu = GUI::Menu::construct(); + m_variable_context_menu->add_action(edit_variable_action); + + return variables_widget; +} + +NonnullRefPtr<GUI::Widget> DebugInfoWidget::build_registers_tab() +{ + auto registers_widget = GUI::Widget::construct(); + registers_widget->set_layout<GUI::HorizontalBoxLayout>(); + + m_registers_view = registers_widget->add<GUI::TableView>(); + + return registers_widget; +} + +void DebugInfoWidget::update_state(const Debug::DebugSession& debug_session, const PtraceRegisters& regs) +{ + m_variables_view->set_model(VariablesModel::create(regs)); + m_backtrace_view->set_model(BacktraceModel::create(debug_session, regs)); + if (m_registers_view->model()) { + auto& previous_registers = static_cast<RegistersModel*>(m_registers_view->model())->raw_registers(); + m_registers_view->set_model(RegistersModel::create(regs, previous_registers)); + } else { + m_registers_view->set_model(RegistersModel::create(regs)); + } + auto selected_index = m_backtrace_view->model()->index(0); + if (!selected_index.is_valid()) { + dbgln("Warning: DebugInfoWidget: backtrace selected index is invalid"); + return; + } + m_backtrace_view->selection().set(selected_index); +} + +void DebugInfoWidget::program_stopped() +{ + m_variables_view->set_model({}); + m_backtrace_view->set_model({}); + m_registers_view->set_model({}); +} + +void DebugInfoWidget::set_debug_actions_enabled(bool enabled) +{ + m_continue_action->set_enabled(enabled); + m_singlestep_action->set_enabled(enabled); + m_step_in_action->set_enabled(enabled); + m_step_out_action->set_enabled(enabled); +} + +} diff --git a/Userland/DevTools/HackStudio/Debugger/DebugInfoWidget.h b/Userland/DevTools/HackStudio/Debugger/DebugInfoWidget.h new file mode 100644 index 0000000000..efc6f4b958 --- /dev/null +++ b/Userland/DevTools/HackStudio/Debugger/DebugInfoWidget.h @@ -0,0 +1,71 @@ +/* + * Copyright (c) 2020, Itamar S. <itamar8910@gmail.com> + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * 1. Redistributions of source code must retain the above copyright notice, this + * list of conditions and the following disclaimer. + * + * 2. Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE + * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL + * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR + * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER + * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, + * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE + * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +#pragma once + +#include "Debugger.h" +#include <AK/NonnullOwnPtr.h> +#include <LibGUI/Action.h> +#include <LibGUI/ListView.h> +#include <LibGUI/Menu.h> +#include <LibGUI/Model.h> +#include <LibGUI/TableView.h> +#include <LibGUI/ToolBar.h> +#include <LibGUI/ToolBarContainer.h> +#include <LibGUI/TreeView.h> +#include <LibGUI/Widget.h> +#include <sys/arch/i386/regs.h> + +namespace HackStudio { + +class DebugInfoWidget final : public GUI::Widget { + C_OBJECT(DebugInfoWidget) +public: + virtual ~DebugInfoWidget() override { } + + void update_state(const Debug::DebugSession&, const PtraceRegisters&); + void program_stopped(); + void set_debug_actions_enabled(bool enabled); + +private: + explicit DebugInfoWidget(); + void init_toolbar(); + + NonnullRefPtr<GUI::Widget> build_variables_tab(); + NonnullRefPtr<GUI::Widget> build_registers_tab(); + + RefPtr<GUI::TreeView> m_variables_view; + RefPtr<GUI::TableView> m_registers_view; + RefPtr<GUI::ListView> m_backtrace_view; + RefPtr<GUI::Menu> m_variable_context_menu; + RefPtr<GUI::ToolBar> m_toolbar; + RefPtr<GUI::Action> m_continue_action; + RefPtr<GUI::Action> m_singlestep_action; + RefPtr<GUI::Action> m_step_in_action; + RefPtr<GUI::Action> m_step_out_action; +}; + +} diff --git a/Userland/DevTools/HackStudio/Debugger/Debugger.cpp b/Userland/DevTools/HackStudio/Debugger/Debugger.cpp new file mode 100644 index 0000000000..20527e379d --- /dev/null +++ b/Userland/DevTools/HackStudio/Debugger/Debugger.cpp @@ -0,0 +1,295 @@ +/* + * Copyright (c) 2020, Itamar S. <itamar8910@gmail.com> + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * 1. Redistributions of source code must retain the above copyright notice, this + * list of conditions and the following disclaimer. + * + * 2. Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE + * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL + * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR + * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER + * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, + * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE + * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +#include "Debugger.h" +#include <LibDebug/StackFrameUtils.h> + +namespace HackStudio { + +static Debugger* s_the; + +Debugger& Debugger::the() +{ + ASSERT(s_the); + return *s_the; +} + +void Debugger::initialize( + String source_root, + Function<HasControlPassedToUser(const PtraceRegisters&)> on_stop_callback, + Function<void()> on_continue_callback, + Function<void()> on_exit_callback) +{ + s_the = new Debugger(source_root, move(on_stop_callback), move(on_continue_callback), move(on_exit_callback)); +} + +bool Debugger::is_initialized() +{ + return s_the; +} + +Debugger::Debugger( + String source_root, + Function<HasControlPassedToUser(const PtraceRegisters&)> on_stop_callback, + Function<void()> on_continue_callback, + Function<void()> on_exit_callback) + : m_source_root(source_root) + , m_on_stopped_callback(move(on_stop_callback)) + , m_on_continue_callback(move(on_continue_callback)) + , m_on_exit_callback(move(on_exit_callback)) +{ + pthread_mutex_init(&m_ui_action_mutex, nullptr); + pthread_cond_init(&m_ui_action_cond, nullptr); +} + +void Debugger::on_breakpoint_change(const String& file, size_t line, BreakpointChange change_type) +{ + auto position = create_source_position(file, line); + + if (change_type == BreakpointChange::Added) { + Debugger::the().m_breakpoints.append(position); + } else { + Debugger::the().m_breakpoints.remove_all_matching([&](Debug::DebugInfo::SourcePosition val) { return val == position; }); + } + + auto session = Debugger::the().session(); + if (!session) + return; + + auto address = session->get_address_from_source_position(position.file_path, position.line_number); + if (!address.has_value()) { + dbgln("Warning: couldn't get instruction address from source"); + // TODO: Currently, the GUI will indicate that a breakpoint was inserted/removed at this line, + // regardless of whether we actually succeeded to insert it. (For example a breakpoint on a comment, or an include statement). + // We should indicate failure via a return value from this function, and not update the breakpoint GUI if we fail. + return; + } + + if (change_type == BreakpointChange::Added) { + bool success = session->insert_breakpoint(reinterpret_cast<void*>(address.value().address)); + ASSERT(success); + } else { + bool success = session->remove_breakpoint(reinterpret_cast<void*>(address.value().address)); + ASSERT(success); + } +} + +Debug::DebugInfo::SourcePosition Debugger::create_source_position(const String& file, size_t line) +{ + if (!file.starts_with('/') && !file.starts_with("./")) + return { String::formatted("./{}", file), line + 1 }; + return { file, line + 1 }; +} + +int Debugger::start_static() +{ + Debugger::the().start(); + return 0; +} + +void Debugger::start() +{ + m_debug_session = Debug::DebugSession::exec_and_attach(m_executable_path, m_source_root); + ASSERT(!!m_debug_session); + + for (const auto& breakpoint : m_breakpoints) { + dbgln("inserting breakpoint at: {}:{}", breakpoint.file_path, breakpoint.line_number); + auto address = m_debug_session->get_address_from_source_position(breakpoint.file_path, breakpoint.line_number); + if (address.has_value()) { + bool success = m_debug_session->insert_breakpoint(reinterpret_cast<void*>(address.value().address)); + ASSERT(success); + } else { + dbgln("couldn't insert breakpoint"); + } + } + + debugger_loop(); +} + +int Debugger::debugger_loop() +{ + ASSERT(m_debug_session); + + m_debug_session->run(Debug::DebugSession::DesiredInitialDebugeeState::Running, [this](Debug::DebugSession::DebugBreakReason reason, Optional<PtraceRegisters> optional_regs) { + if (reason == Debug::DebugSession::DebugBreakReason::Exited) { + dbgln("Program exited"); + m_on_exit_callback(); + return Debug::DebugSession::DebugDecision::Detach; + } + remove_temporary_breakpoints(); + ASSERT(optional_regs.has_value()); + const PtraceRegisters& regs = optional_regs.value(); + + auto source_position = m_debug_session->get_source_position(regs.eip); + if (!source_position.has_value()) + return Debug::DebugSession::DebugDecision::SingleStep; + + // We currently do no support stepping through assembly source + if (source_position.value().file_path.ends_with(".S")) + return Debug::DebugSession::DebugDecision::SingleStep; + + ASSERT(source_position.has_value()); + if (m_state.get() == Debugger::DebuggingState::SingleStepping) { + if (m_state.should_stop_single_stepping(source_position.value())) { + m_state.set_normal(); + } else { + return Debug::DebugSession::DebugDecision::SingleStep; + } + } + + auto control_passed_to_user = m_on_stopped_callback(regs); + + if (control_passed_to_user == HasControlPassedToUser::Yes) { + pthread_mutex_lock(&m_ui_action_mutex); + pthread_cond_wait(&m_ui_action_cond, &m_ui_action_mutex); + pthread_mutex_unlock(&m_ui_action_mutex); + + if (m_requested_debugger_action != DebuggerAction::Exit) + m_on_continue_callback(); + + } else { + m_requested_debugger_action = DebuggerAction::Continue; + } + + switch (m_requested_debugger_action) { + case DebuggerAction::Continue: + m_state.set_normal(); + return Debug::DebugSession::DebugDecision::Continue; + case DebuggerAction::SourceSingleStep: + m_state.set_single_stepping(source_position.value()); + return Debug::DebugSession::DebugDecision::SingleStep; + case DebuggerAction::SourceStepOut: + m_state.set_stepping_out(); + do_step_out(regs); + return Debug::DebugSession::DebugDecision::Continue; + case DebuggerAction::SourceStepOver: + m_state.set_stepping_over(); + do_step_over(regs); + return Debug::DebugSession::DebugDecision::Continue; + case DebuggerAction::Exit: + // NOTE: Is detaching from the debuggee the best thing to do here? + // We could display a dialog in the UI, remind the user that there is + // a live debugged process, and ask whether they want to terminate/detach. + dbgln("Debugger exiting"); + return Debug::DebugSession::DebugDecision::Detach; + } + ASSERT_NOT_REACHED(); + }); + m_debug_session.clear(); + return 0; +} + +void Debugger::DebuggingState::set_normal() +{ + m_state = State::Normal; + m_original_source_position.clear(); +} + +void Debugger::DebuggingState::set_single_stepping(Debug::DebugInfo::SourcePosition original_source_position) +{ + m_state = State::SingleStepping; + m_original_source_position = original_source_position; +} + +bool Debugger::DebuggingState::should_stop_single_stepping(const Debug::DebugInfo::SourcePosition& current_source_position) const +{ + ASSERT(m_state == State::SingleStepping); + return m_original_source_position.value() != current_source_position; +} + +void Debugger::remove_temporary_breakpoints() +{ + for (auto breakpoint_address : m_state.temporary_breakpoints()) { + ASSERT(m_debug_session->breakpoint_exists((void*)breakpoint_address)); + bool rc = m_debug_session->remove_breakpoint((void*)breakpoint_address); + ASSERT(rc); + } + m_state.clear_temporary_breakpoints(); +} + +void Debugger::DebuggingState::clear_temporary_breakpoints() +{ + m_addresses_of_temporary_breakpoints.clear(); +} +void Debugger::DebuggingState::add_temporary_breakpoint(u32 address) +{ + m_addresses_of_temporary_breakpoints.append(address); +} + +void Debugger::do_step_out(const PtraceRegisters& regs) +{ + // To step out, we simply insert a temporary breakpoint at the + // instruction the current function returns to, and continue + // execution until we hit that instruction (or some other breakpoint). + insert_temporary_breakpoint_at_return_address(regs); +} + +void Debugger::do_step_over(const PtraceRegisters& regs) +{ + // To step over, we insert a temporary breakpoint at each line in the current function, + // as well as at the current function's return point, and continue execution. + auto lib = m_debug_session->library_at(regs.eip); + if (!lib) + return; + auto current_function = lib->debug_info->get_containing_function(regs.eip - lib->base_address); + if (!current_function.has_value()) { + dbgln("cannot perform step_over, failed to find containing function of: {:p}", regs.eip); + return; + } + ASSERT(current_function.has_value()); + auto lines_in_current_function = lib->debug_info->source_lines_in_scope(current_function.value()); + for (const auto& line : lines_in_current_function) { + insert_temporary_breakpoint(line.address_of_first_statement.value() + lib->base_address); + } + insert_temporary_breakpoint_at_return_address(regs); +} + +void Debugger::insert_temporary_breakpoint_at_return_address(const PtraceRegisters& regs) +{ + auto frame_info = Debug::StackFrameUtils::get_info(*m_debug_session, regs.ebp); + ASSERT(frame_info.has_value()); + u32 return_address = frame_info.value().return_address; + insert_temporary_breakpoint(return_address); +} + +void Debugger::insert_temporary_breakpoint(FlatPtr address) +{ + if (m_debug_session->breakpoint_exists((void*)address)) + return; + bool success = m_debug_session->insert_breakpoint(reinterpret_cast<void*>(address)); + ASSERT(success); + m_state.add_temporary_breakpoint(address); +} + +void Debugger::set_requested_debugger_action(DebuggerAction action) +{ + pthread_mutex_lock(continue_mutex()); + m_requested_debugger_action = action; + pthread_cond_signal(continue_cond()); + pthread_mutex_unlock(continue_mutex()); +} + +} diff --git a/Userland/DevTools/HackStudio/Debugger/Debugger.h b/Userland/DevTools/HackStudio/Debugger/Debugger.h new file mode 100644 index 0000000000..0d2371f86e --- /dev/null +++ b/Userland/DevTools/HackStudio/Debugger/Debugger.h @@ -0,0 +1,139 @@ +/* + * Copyright (c) 2020, Itamar S. <itamar8910@gmail.com> + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * 1. Redistributions of source code must retain the above copyright notice, this + * list of conditions and the following disclaimer. + * + * 2. Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE + * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL + * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR + * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER + * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, + * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE + * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +#pragma once + +#include "BreakpointCallback.h" +#include <AK/Function.h> +#include <AK/Vector.h> +#include <LibDebug/DebugSession.h> +#include <LibThread/Lock.h> +#include <LibThread/Thread.h> + +namespace HackStudio { + +class Debugger { +public: + static Debugger& the(); + + enum class HasControlPassedToUser { + No, + Yes, + }; + + static void initialize( + String source_root, + Function<HasControlPassedToUser(const PtraceRegisters&)> on_stop_callback, + Function<void()> on_continue_callback, + Function<void()> on_exit_callback); + + static bool is_initialized(); + + static void on_breakpoint_change(const String& file, size_t line, BreakpointChange change_type); + + void set_executable_path(const String& path) { m_executable_path = path; } + + Debug::DebugSession* session() { return m_debug_session.ptr(); } + + // Thread entry point + static int start_static(); + + pthread_mutex_t* continue_mutex() { return &m_ui_action_mutex; } + pthread_cond_t* continue_cond() { return &m_ui_action_cond; } + + enum class DebuggerAction { + Continue, + SourceSingleStep, + SourceStepOut, + SourceStepOver, + Exit, + }; + + void set_requested_debugger_action(DebuggerAction); + void reset_breakpoints() { m_breakpoints.clear(); } + +private: + class DebuggingState { + public: + enum State { + Normal, // Continue normally until we hit a breakpoint / program terminates + SingleStepping, + SteppingOut, + SteppingOver, + }; + State get() const { return m_state; } + + void set_normal(); + void set_single_stepping(Debug::DebugInfo::SourcePosition original_source_position); + void set_stepping_out() { m_state = State::SteppingOut; } + void set_stepping_over() { m_state = State::SteppingOver; } + + bool should_stop_single_stepping(const Debug::DebugInfo::SourcePosition& current_source_position) const; + void clear_temporary_breakpoints(); + void add_temporary_breakpoint(u32 address); + const Vector<u32>& temporary_breakpoints() const { return m_addresses_of_temporary_breakpoints; } + + private: + State m_state { Normal }; + Optional<Debug::DebugInfo::SourcePosition> m_original_source_position; // The source position at which we started the current single step + Vector<u32> m_addresses_of_temporary_breakpoints; + }; + + explicit Debugger( + String source_root, + Function<HasControlPassedToUser(const PtraceRegisters&)> on_stop_callback, + Function<void()> on_continue_callback, + Function<void()> on_exit_callback); + + static Debug::DebugInfo::SourcePosition create_source_position(const String& file, size_t line); + + void start(); + int debugger_loop(); + + void remove_temporary_breakpoints(); + void do_step_out(const PtraceRegisters&); + void do_step_over(const PtraceRegisters&); + void insert_temporary_breakpoint(FlatPtr address); + void insert_temporary_breakpoint_at_return_address(const PtraceRegisters&); + + OwnPtr<Debug::DebugSession> m_debug_session; + String m_source_root; + DebuggingState m_state; + + pthread_mutex_t m_ui_action_mutex {}; + pthread_cond_t m_ui_action_cond {}; + DebuggerAction m_requested_debugger_action { DebuggerAction::Continue }; + + Vector<Debug::DebugInfo::SourcePosition> m_breakpoints; + + String m_executable_path; + + Function<HasControlPassedToUser(const PtraceRegisters&)> m_on_stopped_callback; + Function<void()> m_on_continue_callback; + Function<void()> m_on_exit_callback; +}; + +} diff --git a/Userland/DevTools/HackStudio/Debugger/DisassemblyModel.cpp b/Userland/DevTools/HackStudio/Debugger/DisassemblyModel.cpp new file mode 100644 index 0000000000..cb7822c60a --- /dev/null +++ b/Userland/DevTools/HackStudio/Debugger/DisassemblyModel.cpp @@ -0,0 +1,137 @@ +/* + * Copyright (c) 2020, Luke Wilde <luke.wilde@live.co.uk> + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * 1. Redistributions of source code must retain the above copyright notice, this + * list of conditions and the following disclaimer. + * + * 2. Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE + * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL + * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR + * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER + * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, + * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE + * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +#include "DisassemblyModel.h" +#include <AK/MappedFile.h> +#include <AK/StringBuilder.h> +#include <LibDebug/DebugSession.h> +#include <LibELF/Image.h> +#include <LibX86/Disassembler.h> +#include <LibX86/ELFSymbolProvider.h> +#include <ctype.h> +#include <stdio.h> + +namespace HackStudio { + +DisassemblyModel::DisassemblyModel(const Debug::DebugSession& debug_session, const PtraceRegisters& regs) +{ + auto lib = debug_session.library_at(regs.eip); + if (!lib) + return; + auto containing_function = lib->debug_info->get_containing_function(regs.eip - lib->base_address); + if (!containing_function.has_value()) { + dbgln("Cannot disassemble as the containing function was not found."); + return; + } + + OwnPtr<ELF::Image> kernel_elf; + const ELF::Image* elf = nullptr; + + if (containing_function.value().address_low >= 0xc0000000) { + auto file_or_error = MappedFile::map("/boot/Kernel"); + if (file_or_error.is_error()) + return; + kernel_elf = make<ELF::Image>(file_or_error.value()->bytes()); + elf = kernel_elf.ptr(); + } else { + elf = &lib->debug_info->elf(); + } + + auto symbol = elf->find_symbol(containing_function.value().address_low); + if (!symbol.has_value()) + return; + ASSERT(symbol.has_value()); + + auto view = symbol.value().raw_data(); + + X86::ELFSymbolProvider symbol_provider(*elf); + X86::SimpleInstructionStream stream((const u8*)view.characters_without_null_termination(), view.length()); + X86::Disassembler disassembler(stream); + + size_t offset_into_symbol = 0; + for (;;) { + auto insn = disassembler.next(); + if (!insn.has_value()) + break; + FlatPtr address_in_profiled_program = symbol.value().value() + offset_into_symbol; + auto disassembly = insn.value().to_string(address_in_profiled_program, &symbol_provider); + StringView instruction_bytes = view.substring_view(offset_into_symbol, insn.value().length()); + m_instructions.append({ insn.value(), disassembly, instruction_bytes, address_in_profiled_program }); + + offset_into_symbol += insn.value().length(); + } +} + +DisassemblyModel::~DisassemblyModel() +{ +} + +int DisassemblyModel::row_count(const GUI::ModelIndex&) const +{ + return m_instructions.size(); +} + +String DisassemblyModel::column_name(int column) const +{ + switch (column) { + case Column::Address: + return "Address"; + case Column::InstructionBytes: + return "Insn Bytes"; + case Column::Disassembly: + return "Disassembly"; + default: + ASSERT_NOT_REACHED(); + return {}; + } +} + +GUI::Variant DisassemblyModel::data(const GUI::ModelIndex& index, GUI::ModelRole role) const +{ + auto& insn = m_instructions[index.row()]; + + if (role == GUI::ModelRole::Display) { + if (index.column() == Column::Address) + return String::formatted("{:p}", insn.address); + if (index.column() == Column::InstructionBytes) { + StringBuilder builder; + for (auto ch : insn.bytes) + builder.appendff("{:02x} ", static_cast<unsigned char>(ch)); + return builder.to_string(); + } + if (index.column() == Column::Disassembly) + return insn.disassembly; + return {}; + } + return {}; +} + +void DisassemblyModel::update() +{ + did_update(); +} + +} diff --git a/Userland/DevTools/HackStudio/Debugger/DisassemblyModel.h b/Userland/DevTools/HackStudio/Debugger/DisassemblyModel.h new file mode 100644 index 0000000000..c09d23a04a --- /dev/null +++ b/Userland/DevTools/HackStudio/Debugger/DisassemblyModel.h @@ -0,0 +1,77 @@ +/* + * Copyright (c) 2020, Luke Wilde <luke.wilde@live.co.uk> + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * 1. Redistributions of source code must retain the above copyright notice, this + * list of conditions and the following disclaimer. + * + * 2. Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE + * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL + * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR + * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER + * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, + * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE + * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +#pragma once + +#include <AK/Vector.h> +#include <LibGUI/Model.h> +#include <LibX86/Instruction.h> +#include <sys/arch/i386/regs.h> + +namespace Debug { + +class DebugSession; + +} + +namespace HackStudio { + +struct InstructionData { + X86::Instruction insn; + String disassembly; + StringView bytes; + FlatPtr address { 0 }; +}; + +class DisassemblyModel final : public GUI::Model { +public: + static NonnullRefPtr<DisassemblyModel> create(const Debug::DebugSession& debug_session, const PtraceRegisters& regs) + { + return adopt(*new DisassemblyModel(debug_session, regs)); + } + + enum Column { + Address, + InstructionBytes, + Disassembly, + __Count + }; + + virtual ~DisassemblyModel() override; + + virtual int row_count(const GUI::ModelIndex& = GUI::ModelIndex()) const override; + virtual int column_count(const GUI::ModelIndex& = GUI::ModelIndex()) const override { return Column::__Count; } + virtual String column_name(int) const override; + virtual GUI::Variant data(const GUI::ModelIndex&, GUI::ModelRole) const override; + virtual void update() override; + +private: + DisassemblyModel(const Debug::DebugSession&, const PtraceRegisters&); + + Vector<InstructionData> m_instructions; +}; + +} diff --git a/Userland/DevTools/HackStudio/Debugger/DisassemblyWidget.cpp b/Userland/DevTools/HackStudio/Debugger/DisassemblyWidget.cpp new file mode 100644 index 0000000000..89a670b2a2 --- /dev/null +++ b/Userland/DevTools/HackStudio/Debugger/DisassemblyWidget.cpp @@ -0,0 +1,105 @@ +/* + * Copyright (c) 2020, Luke Wilde <luke.wilde@live.co.uk> + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * 1. Redistributions of source code must retain the above copyright notice, this + * list of conditions and the following disclaimer. + * + * 2. Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE + * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL + * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR + * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER + * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, + * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE + * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +#include "DisassemblyWidget.h" +#include "DisassemblyModel.h" +#include <LibGUI/BoxLayout.h> +#include <LibGUI/Painter.h> +#include <LibGfx/Palette.h> + +namespace HackStudio { + +void UnavailableDisassemblyWidget::paint_event(GUI::PaintEvent& event) +{ + Frame::paint_event(event); + if (reason().is_empty()) + return; + GUI::Painter painter(*this); + painter.add_clip_rect(event.rect()); + painter.draw_text(frame_inner_rect(), reason(), Gfx::TextAlignment::Center, palette().window_text(), Gfx::TextElision::Right); +} + +DisassemblyWidget::DisassemblyWidget() +{ + set_layout<GUI::VerticalBoxLayout>(); + + m_top_container = add<GUI::Widget>(); + m_top_container->set_layout<GUI::HorizontalBoxLayout>(); + m_top_container->set_fixed_height(20); + + m_function_name_label = m_top_container->add<GUI::Label>(""); + + m_disassembly_view = add<GUI::TableView>(); + + m_unavailable_disassembly_widget = add<UnavailableDisassemblyWidget>(""); + + hide_disassembly("Program isn't running"); +} + +void DisassemblyWidget::update_state(const Debug::DebugSession& debug_session, const PtraceRegisters& regs) +{ + m_disassembly_view->set_model(DisassemblyModel::create(debug_session, regs)); + + if (m_disassembly_view->model()->row_count() > 0) { + auto lib = debug_session.library_at(regs.eip); + if (!lib) + return; + auto containing_function = lib->debug_info->get_containing_function(regs.eip - lib->base_address); + if (containing_function.has_value()) + m_function_name_label->set_text(containing_function.value().name); + else + m_function_name_label->set_text("<missing>"); + show_disassembly(); + } else { + hide_disassembly("No disassembly to show for this function"); + } +} + +void DisassemblyWidget::program_stopped() +{ + m_disassembly_view->set_model({}); + m_function_name_label->set_text(""); + hide_disassembly("Program isn't running"); +} + +void DisassemblyWidget::show_disassembly() +{ + m_top_container->set_visible(true); + m_disassembly_view->set_visible(true); + m_function_name_label->set_visible(true); + m_unavailable_disassembly_widget->set_visible(false); +} + +void DisassemblyWidget::hide_disassembly(const String& reason) +{ + m_top_container->set_visible(false); + m_disassembly_view->set_visible(false); + m_function_name_label->set_visible(false); + m_unavailable_disassembly_widget->set_visible(true); + m_unavailable_disassembly_widget->set_reason(reason); +} + +} diff --git a/Userland/DevTools/HackStudio/Debugger/DisassemblyWidget.h b/Userland/DevTools/HackStudio/Debugger/DisassemblyWidget.h new file mode 100644 index 0000000000..77d36b0edd --- /dev/null +++ b/Userland/DevTools/HackStudio/Debugger/DisassemblyWidget.h @@ -0,0 +1,78 @@ +/* + * Copyright (c) 2020, Luke Wilde <luke.wilde@live.co.uk> + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * 1. Redistributions of source code must retain the above copyright notice, this + * list of conditions and the following disclaimer. + * + * 2. Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE + * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL + * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR + * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER + * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, + * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE + * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +#pragma once + +#include "Debugger.h" +#include <AK/NonnullOwnPtr.h> +#include <LibGUI/Label.h> +#include <LibGUI/Model.h> +#include <LibGUI/TableView.h> +#include <LibGUI/Widget.h> +#include <sys/arch/i386/regs.h> + +namespace HackStudio { + +class UnavailableDisassemblyWidget final : public GUI::Frame { + C_OBJECT(UnavailableDisassemblyWidget) +public: + virtual ~UnavailableDisassemblyWidget() override { } + + const String& reason() const { return m_reason; } + void set_reason(const String& text) { m_reason = text; } + +private: + UnavailableDisassemblyWidget(const String& reason) + : m_reason(reason) + { + } + + virtual void paint_event(GUI::PaintEvent& event) override; + + String m_reason; +}; + +class DisassemblyWidget final : public GUI::Widget { + C_OBJECT(DisassemblyWidget) +public: + virtual ~DisassemblyWidget() override { } + + void update_state(const Debug::DebugSession&, const PtraceRegisters&); + void program_stopped(); + +private: + DisassemblyWidget(); + + void show_disassembly(); + void hide_disassembly(const String&); + + RefPtr<GUI::Widget> m_top_container; + RefPtr<GUI::TableView> m_disassembly_view; + RefPtr<GUI::Label> m_function_name_label; + RefPtr<UnavailableDisassemblyWidget> m_unavailable_disassembly_widget; +}; + +} diff --git a/Userland/DevTools/HackStudio/Debugger/RegistersModel.cpp b/Userland/DevTools/HackStudio/Debugger/RegistersModel.cpp new file mode 100644 index 0000000000..92199a4c6f --- /dev/null +++ b/Userland/DevTools/HackStudio/Debugger/RegistersModel.cpp @@ -0,0 +1,121 @@ +/* + * Copyright (c) 2020, Luke Wilde <luke.wilde@live.co.uk> + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * 1. Redistributions of source code must retain the above copyright notice, this + * list of conditions and the following disclaimer. + * + * 2. Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE + * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL + * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR + * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER + * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, + * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE + * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +#include "RegistersModel.h" + +namespace HackStudio { + +RegistersModel::RegistersModel(const PtraceRegisters& regs) + : m_raw_registers(regs) +{ + m_registers.append({ "eax", regs.eax }); + m_registers.append({ "ebx", regs.ebx }); + m_registers.append({ "ecx", regs.ecx }); + m_registers.append({ "edx", regs.edx }); + m_registers.append({ "esp", regs.esp }); + m_registers.append({ "ebp", regs.ebp }); + m_registers.append({ "esi", regs.esi }); + m_registers.append({ "edi", regs.edi }); + m_registers.append({ "eip", regs.eip }); + m_registers.append({ "eflags", regs.eflags }); + m_registers.append({ "cs", regs.cs }); + m_registers.append({ "ss", regs.ss }); + m_registers.append({ "ds", regs.ds }); + m_registers.append({ "es", regs.es }); + m_registers.append({ "fs", regs.fs }); + m_registers.append({ "gs", regs.gs }); +} + +RegistersModel::RegistersModel(const PtraceRegisters& current_regs, const PtraceRegisters& previous_regs) + : m_raw_registers(current_regs) +{ + m_registers.append({ "eax", current_regs.eax, current_regs.eax != previous_regs.eax }); + m_registers.append({ "ebx", current_regs.ebx, current_regs.ebx != previous_regs.ebx }); + m_registers.append({ "ecx", current_regs.ecx, current_regs.ecx != previous_regs.ecx }); + m_registers.append({ "edx", current_regs.edx, current_regs.edx != previous_regs.edx }); + m_registers.append({ "esp", current_regs.esp, current_regs.esp != previous_regs.esp }); + m_registers.append({ "ebp", current_regs.ebp, current_regs.ebp != previous_regs.ebp }); + m_registers.append({ "esi", current_regs.esi, current_regs.esi != previous_regs.esi }); + m_registers.append({ "edi", current_regs.edi, current_regs.edi != previous_regs.edi }); + m_registers.append({ "eip", current_regs.eip, current_regs.eip != previous_regs.eip }); + m_registers.append({ "eflags", current_regs.eflags, current_regs.eflags != previous_regs.eflags }); + m_registers.append({ "cs", current_regs.cs, current_regs.cs != previous_regs.cs }); + m_registers.append({ "ss", current_regs.ss, current_regs.ss != previous_regs.ss }); + m_registers.append({ "ds", current_regs.ds, current_regs.ds != previous_regs.ds }); + m_registers.append({ "es", current_regs.es, current_regs.es != previous_regs.es }); + m_registers.append({ "fs", current_regs.fs, current_regs.ds != previous_regs.fs }); + m_registers.append({ "gs", current_regs.gs, current_regs.gs != previous_regs.gs }); +} + +RegistersModel::~RegistersModel() +{ +} + +int RegistersModel::row_count(const GUI::ModelIndex&) const +{ + return m_registers.size(); +} + +String RegistersModel::column_name(int column) const +{ + switch (column) { + case Column::Register: + return "Register"; + case Column::Value: + return "Value"; + default: + ASSERT_NOT_REACHED(); + return {}; + } +} + +GUI::Variant RegistersModel::data(const GUI::ModelIndex& index, GUI::ModelRole role) const +{ + auto& reg = m_registers[index.row()]; + + if (role == GUI::ModelRole::ForegroundColor) { + if (reg.changed) + return Color(Color::Red); + else + return Color(Color::Black); + } + + if (role == GUI::ModelRole::Display) { + if (index.column() == Column::Register) + return reg.name; + if (index.column() == Column::Value) + return String::formatted("{:08x}", reg.value); + return {}; + } + return {}; +} + +void RegistersModel::update() +{ + did_update(); +} + +} diff --git a/Userland/DevTools/HackStudio/Debugger/RegistersModel.h b/Userland/DevTools/HackStudio/Debugger/RegistersModel.h new file mode 100644 index 0000000000..8252f16943 --- /dev/null +++ b/Userland/DevTools/HackStudio/Debugger/RegistersModel.h @@ -0,0 +1,77 @@ +/* + * Copyright (c) 2020, Luke Wilde <luke.wilde@live.co.uk> + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * 1. Redistributions of source code must retain the above copyright notice, this + * list of conditions and the following disclaimer. + * + * 2. Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE + * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL + * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR + * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER + * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, + * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE + * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +#pragma once + +#include <AK/Vector.h> +#include <LibGUI/Model.h> +#include <sys/arch/i386/regs.h> + +namespace HackStudio { + +struct RegisterData { + String name; + u32 value; + bool changed { false }; +}; + +class RegistersModel final : public GUI::Model { +public: + static RefPtr<RegistersModel> create(const PtraceRegisters& regs) + { + return adopt(*new RegistersModel(regs)); + } + + static RefPtr<RegistersModel> create(const PtraceRegisters& current_regs, const PtraceRegisters& previous_regs) + { + return adopt(*new RegistersModel(current_regs, previous_regs)); + } + + enum Column { + Register, + Value, + __Count + }; + + virtual ~RegistersModel() override; + + virtual int row_count(const GUI::ModelIndex& = GUI::ModelIndex()) const override; + virtual int column_count(const GUI::ModelIndex& = GUI::ModelIndex()) const override { return Column::__Count; } + virtual String column_name(int) const override; + virtual GUI::Variant data(const GUI::ModelIndex&, GUI::ModelRole) const override; + virtual void update() override; + + const PtraceRegisters& raw_registers() const { return m_raw_registers; } + +private: + explicit RegistersModel(const PtraceRegisters& regs); + RegistersModel(const PtraceRegisters& current_regs, const PtraceRegisters& previous_regs); + + PtraceRegisters m_raw_registers; + Vector<RegisterData> m_registers; +}; + +} diff --git a/Userland/DevTools/HackStudio/Debugger/VariablesModel.cpp b/Userland/DevTools/HackStudio/Debugger/VariablesModel.cpp new file mode 100644 index 0000000000..425129046c --- /dev/null +++ b/Userland/DevTools/HackStudio/Debugger/VariablesModel.cpp @@ -0,0 +1,194 @@ +/* + * Copyright (c) 2020, Itamar S. <itamar8910@gmail.com> + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * 1. Redistributions of source code must retain the above copyright notice, this + * list of conditions and the following disclaimer. + * + * 2. Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE + * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL + * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR + * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER + * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, + * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE + * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +#include "VariablesModel.h" +#include <LibGUI/Application.h> +#include <LibGUI/MessageBox.h> + +namespace HackStudio { + +GUI::ModelIndex VariablesModel::index(int row, int column, const GUI::ModelIndex& parent_index) const +{ + if (!parent_index.is_valid()) + return create_index(row, column, &m_variables[row]); + auto* parent = static_cast<const Debug::DebugInfo::VariableInfo*>(parent_index.internal_data()); + auto* child = &parent->members[row]; + return create_index(row, column, child); +} + +GUI::ModelIndex VariablesModel::parent_index(const GUI::ModelIndex& index) const +{ + if (!index.is_valid()) + return {}; + auto* child = static_cast<const Debug::DebugInfo::VariableInfo*>(index.internal_data()); + auto* parent = child->parent; + if (parent == nullptr) + return {}; + + if (parent->parent == nullptr) { + for (size_t row = 0; row < m_variables.size(); row++) + if (m_variables.ptr_at(row).ptr() == parent) + return create_index(row, 0, parent); + ASSERT_NOT_REACHED(); + } + for (size_t row = 0; row < parent->parent->members.size(); row++) { + Debug::DebugInfo::VariableInfo* child_at_row = parent->parent->members.ptr_at(row).ptr(); + if (child_at_row == parent) + return create_index(row, 0, parent); + } + ASSERT_NOT_REACHED(); +} + +int VariablesModel::row_count(const GUI::ModelIndex& index) const +{ + if (!index.is_valid()) + return m_variables.size(); + auto* node = static_cast<const Debug::DebugInfo::VariableInfo*>(index.internal_data()); + return node->members.size(); +} + +static String variable_value_as_string(const Debug::DebugInfo::VariableInfo& variable) +{ + if (variable.location_type != Debug::DebugInfo::VariableInfo::LocationType::Address) + return "N/A"; + + auto variable_address = variable.location_data.address; + + if (variable.is_enum_type()) { + auto value = Debugger::the().session()->peek((u32*)variable_address); + ASSERT(value.has_value()); + auto it = variable.type->members.find_if([&enumerator_value = value.value()](const auto& enumerator) { + return enumerator->constant_data.as_u32 == enumerator_value; + }); + ASSERT(!it.is_end()); + return String::formatted("{}::{}", variable.type_name, (*it)->name); + } + + if (variable.type_name == "int") { + auto value = Debugger::the().session()->peek((u32*)variable_address); + ASSERT(value.has_value()); + return String::formatted("{}", static_cast<int>(value.value())); + } + + if (variable.type_name == "char") { + auto value = Debugger::the().session()->peek((u32*)variable_address); + ASSERT(value.has_value()); + return String::formatted("'{0:c}' ({0:d})", value.value()); + } + + if (variable.type_name == "bool") { + auto value = Debugger::the().session()->peek((u32*)variable_address); + ASSERT(value.has_value()); + return (value.value() & 1) ? "true" : "false"; + } + + return String::formatted("type: {} @ {:p}, ", variable.type_name, variable_address); +} + +static Optional<u32> string_to_variable_value(const StringView& string_value, const Debug::DebugInfo::VariableInfo& variable) +{ + if (variable.is_enum_type()) { + auto prefix_string = String::formatted("{}::", variable.type_name); + auto string_to_use = string_value; + if (string_value.starts_with(prefix_string)) + string_to_use = string_value.substring_view(prefix_string.length(), string_value.length() - prefix_string.length()); + + auto it = variable.type->members.find_if([string_to_use](const auto& enumerator) { + return enumerator->name == string_to_use; + }); + + if (it.is_end()) + return {}; + return (*it)->constant_data.as_u32; + } + + if (variable.type_name == "int") { + auto value = string_value.to_int(); + if (value.has_value()) + return value.value(); + return {}; + } + + if (variable.type_name == "bool") { + if (string_value == "true") + return true; + if (string_value == "false") + return false; + return {}; + } + + return {}; +} + +void VariablesModel::set_variable_value(const GUI::ModelIndex& index, const StringView& string_value, GUI::Window* parent_window) +{ + auto variable = static_cast<const Debug::DebugInfo::VariableInfo*>(index.internal_data()); + + auto value = string_to_variable_value(string_value, *variable); + + if (value.has_value()) { + auto success = Debugger::the().session()->poke((u32*)variable->location_data.address, value.value()); + ASSERT(success); + return; + } + + GUI::MessageBox::show( + parent_window, + String::formatted("String value \"{}\" could not be converted to a value of type {}.", string_value, variable->type_name), + "Set value failed", + GUI::MessageBox::Type::Error); +} + +GUI::Variant VariablesModel::data(const GUI::ModelIndex& index, GUI::ModelRole role) const +{ + auto* variable = static_cast<const Debug::DebugInfo::VariableInfo*>(index.internal_data()); + switch (role) { + case GUI::ModelRole::Display: { + auto value_as_string = variable_value_as_string(*variable); + return String::formatted("{}: {}", variable->name, value_as_string); + } + case GUI::ModelRole::Icon: + return m_variable_icon; + default: + return {}; + } +} + +void VariablesModel::update() +{ + did_update(); +} + +RefPtr<VariablesModel> VariablesModel::create(const PtraceRegisters& regs) +{ + auto lib = Debugger::the().session()->library_at(regs.eip); + if (!lib) + return nullptr; + auto variables = lib->debug_info->get_variables_in_current_scope(regs); + return adopt(*new VariablesModel(move(variables), regs)); +} + +} diff --git a/Userland/DevTools/HackStudio/Debugger/VariablesModel.h b/Userland/DevTools/HackStudio/Debugger/VariablesModel.h new file mode 100644 index 0000000000..41ed5e3ec9 --- /dev/null +++ b/Userland/DevTools/HackStudio/Debugger/VariablesModel.h @@ -0,0 +1,63 @@ +/* + * Copyright (c) 2020, Itamar S. <itamar8910@gmail.com> + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * 1. Redistributions of source code must retain the above copyright notice, this + * list of conditions and the following disclaimer. + * + * 2. Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE + * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL + * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR + * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER + * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, + * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE + * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +#pragma once + +#include "Debugger.h" +#include <AK/NonnullOwnPtr.h> +#include <LibGUI/Model.h> +#include <LibGUI/TreeView.h> +#include <sys/arch/i386/regs.h> + +namespace HackStudio { + +class VariablesModel final : public GUI::Model { +public: + static RefPtr<VariablesModel> create(const PtraceRegisters& regs); + + void set_variable_value(const GUI::ModelIndex&, const StringView&, GUI::Window*); + + virtual int row_count(const GUI::ModelIndex& = GUI::ModelIndex()) const override; + virtual int column_count(const GUI::ModelIndex& = GUI::ModelIndex()) const override { return 1; } + virtual GUI::Variant data(const GUI::ModelIndex& index, GUI::ModelRole role) const override; + virtual void update() override; + virtual GUI::ModelIndex parent_index(const GUI::ModelIndex&) const override; + virtual GUI::ModelIndex index(int row, int column = 0, const GUI::ModelIndex& = GUI::ModelIndex()) const override; + +private: + explicit VariablesModel(NonnullOwnPtrVector<Debug::DebugInfo::VariableInfo>&& variables, const PtraceRegisters& regs) + : m_variables(move(variables)) + , m_regs(regs) + { + m_variable_icon.set_bitmap_for_size(16, Gfx::Bitmap::load_from_file("/res/icons/16x16/inspector-object.png")); + } + NonnullOwnPtrVector<Debug::DebugInfo::VariableInfo> m_variables; + PtraceRegisters m_regs; + + GUI::Icon m_variable_icon; +}; + +} diff --git a/Userland/DevTools/HackStudio/Editor.cpp b/Userland/DevTools/HackStudio/Editor.cpp new file mode 100644 index 0000000000..3c2d3b636b --- /dev/null +++ b/Userland/DevTools/HackStudio/Editor.cpp @@ -0,0 +1,530 @@ +/* + * Copyright (c) 2018-2020, Andreas Kling <kling@serenityos.org> + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * 1. Redistributions of source code must retain the above copyright notice, this + * list of conditions and the following disclaimer. + * + * 2. Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE + * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL + * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR + * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER + * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, + * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE + * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +#include "Editor.h" +#include "Debugger/Debugger.h" +#include "EditorWrapper.h" +#include "HackStudio.h" +#include "Language.h" +#include <AK/ByteBuffer.h> +#include <AK/LexicalPath.h> +#include <LibCore/DirIterator.h> +#include <LibCore/File.h> +#include <LibGUI/Application.h> +#include <LibGUI/CppSyntaxHighlighter.h> +#include <LibGUI/GMLSyntaxHighlighter.h> +#include <LibGUI/INISyntaxHighlighter.h> +#include <LibGUI/JSSyntaxHighlighter.h> +#include <LibGUI/Label.h> +#include <LibGUI/Painter.h> +#include <LibGUI/ScrollBar.h> +#include <LibGUI/ShellSyntaxHighlighter.h> +#include <LibGUI/SyntaxHighlighter.h> +#include <LibGUI/Window.h> +#include <LibMarkdown/Document.h> +#include <LibWeb/DOM/ElementFactory.h> +#include <LibWeb/DOM/Text.h> +#include <LibWeb/HTML/HTMLHeadElement.h> +#include <LibWeb/OutOfProcessWebView.h> +#include <fcntl.h> + +// #define EDITOR_DEBUG + +namespace HackStudio { + +Editor::Editor() +{ + set_document(CodeDocument::create()); + m_documentation_tooltip_window = GUI::Window::construct(); + m_documentation_tooltip_window->set_rect(0, 0, 500, 400); + m_documentation_tooltip_window->set_window_type(GUI::WindowType::Tooltip); + m_documentation_page_view = m_documentation_tooltip_window->set_main_widget<Web::OutOfProcessWebView>(); +} + +Editor::~Editor() +{ +} + +EditorWrapper& Editor::wrapper() +{ + return static_cast<EditorWrapper&>(*parent()); +} +const EditorWrapper& Editor::wrapper() const +{ + return static_cast<const EditorWrapper&>(*parent()); +} + +void Editor::focusin_event(GUI::FocusEvent& event) +{ + wrapper().set_editor_has_focus({}, true); + if (on_focus) + on_focus(); + GUI::TextEditor::focusin_event(event); +} + +void Editor::focusout_event(GUI::FocusEvent& event) +{ + wrapper().set_editor_has_focus({}, false); + GUI::TextEditor::focusout_event(event); +} + +Gfx::IntRect Editor::breakpoint_icon_rect(size_t line_number) const +{ + auto ruler_line_rect = ruler_content_rect(line_number); + + auto scroll_value = vertical_scrollbar().value(); + ruler_line_rect = ruler_line_rect.translated({ 0, -scroll_value }); + auto center = ruler_line_rect.center().translated({ ruler_line_rect.width() - 10, -line_spacing() - 3 }); + constexpr int size = 32; + return { center.x() - size / 2, center.y() - size / 2, size, size }; +} + +void Editor::paint_event(GUI::PaintEvent& event) +{ + GUI::TextEditor::paint_event(event); + + GUI::Painter painter(*this); + if (is_focused()) { + painter.add_clip_rect(event.rect()); + + auto rect = frame_inner_rect(); + if (vertical_scrollbar().is_visible()) + rect.set_width(rect.width() - vertical_scrollbar().width()); + if (horizontal_scrollbar().is_visible()) + rect.set_height(rect.height() - horizontal_scrollbar().height()); + painter.draw_rect(rect, palette().selection()); + } + + if (ruler_visible()) { + size_t first_visible_line = text_position_at(event.rect().top_left()).line(); + size_t last_visible_line = text_position_at(event.rect().bottom_right()).line(); + for (size_t line : breakpoint_lines()) { + if (line < first_visible_line || line > last_visible_line) { + continue; + } + const auto& icon = breakpoint_icon_bitmap(); + painter.blit(breakpoint_icon_rect(line).center(), icon, icon.rect()); + } + if (execution_position().has_value()) { + const auto& icon = current_position_icon_bitmap(); + painter.blit(breakpoint_icon_rect(execution_position().value()).center(), icon, icon.rect()); + } + } +} + +static HashMap<String, String>& man_paths() +{ + static HashMap<String, String> paths; + if (paths.is_empty()) { + // FIXME: This should also search man3, possibly other places.. + Core::DirIterator it("/usr/share/man/man2", Core::DirIterator::Flags::SkipDots); + while (it.has_next()) { + auto path = String::formatted("/usr/share/man/man2/{}", it.next_path()); + auto title = LexicalPath(path).title(); + paths.set(title, path); + } + } + + return paths; +} + +void Editor::show_documentation_tooltip_if_available(const String& hovered_token, const Gfx::IntPoint& screen_location) +{ + auto it = man_paths().find(hovered_token); + if (it == man_paths().end()) { +#ifdef EDITOR_DEBUG + dbgln("no man path for {}", hovered_token); +#endif + m_documentation_tooltip_window->hide(); + return; + } + + if (m_documentation_tooltip_window->is_visible() && hovered_token == m_last_parsed_token) { + return; + } + +#ifdef EDITOR_DEBUG + dbgln("opening {}", it->value); +#endif + auto file = Core::File::construct(it->value); + if (!file->open(Core::File::ReadOnly)) { + dbgln("failed to open {}, {}", it->value, file->error_string()); + return; + } + + auto man_document = Markdown::Document::parse(file->read_all()); + + if (!man_document) { + dbgln("failed to parse markdown"); + return; + } + + StringBuilder html; + // FIXME: With the InProcessWebView we used to manipulate the document body directly, + // With OutOfProcessWebView this isn't possible (at the moment). The ideal solution + // is probably to tweak Markdown::Document::render_to_html() so we can inject styles + // into the rendered HTML easily. + html.append(man_document->render_to_html()); + html.append("<style>body { background-color: #dac7b5; }</style>"); + m_documentation_page_view->load_html(html.build(), {}); + + m_documentation_tooltip_window->move_to(screen_location.translated(4, 4)); + m_documentation_tooltip_window->show(); + + m_last_parsed_token = hovered_token; +} + +void Editor::mousemove_event(GUI::MouseEvent& event) +{ + GUI::TextEditor::mousemove_event(event); + + if (document().spans().is_empty()) + return; + + auto text_position = text_position_at(event.position()); + if (!text_position.is_valid()) { + m_documentation_tooltip_window->hide(); + return; + } + + auto highlighter = wrapper().editor().syntax_highlighter(); + if (!highlighter) + return; + + bool hide_tooltip = true; + bool is_over_link = false; + + auto ruler_line_rect = ruler_content_rect(text_position.line()); + auto hovering_lines_ruler = (event.position().x() < ruler_line_rect.width()); + if (hovering_lines_ruler && !is_in_drag_select()) + set_override_cursor(Gfx::StandardCursor::Arrow); + else if (m_hovering_editor) + set_override_cursor(m_hovering_link && event.ctrl() ? Gfx::StandardCursor::Hand : Gfx::StandardCursor::IBeam); + + for (auto& span : document().spans()) { + if (span.range.contains(m_previous_text_position) && !span.range.contains(text_position)) { + if (highlighter->is_navigatable(span.data) && span.attributes.underline) { + span.attributes.underline = false; + wrapper().editor().update(); + } + } + + if (span.range.contains(text_position)) { + auto adjusted_range = span.range; + auto end_line_length = document().line(span.range.end().line()).length(); + adjusted_range.end().set_column(min(end_line_length, adjusted_range.end().column() + 1)); + auto hovered_span_text = document().text_in_range(adjusted_range); +#ifdef EDITOR_DEBUG + dbgln("Hovering: {} \"{}\"", adjusted_range, hovered_span_text); +#endif + + if (highlighter->is_navigatable(span.data)) { + is_over_link = true; + bool was_underlined = span.attributes.underline; + span.attributes.underline = event.modifiers() & Mod_Ctrl; + if (span.attributes.underline != was_underlined) { + wrapper().editor().update(); + } + } + if (highlighter->is_identifier(span.data)) { + show_documentation_tooltip_if_available(hovered_span_text, event.position().translated(screen_relative_rect().location())); + hide_tooltip = false; + } + } + } + + m_previous_text_position = text_position; + if (hide_tooltip) + m_documentation_tooltip_window->hide(); + + m_hovering_link = is_over_link && (event.modifiers() & Mod_Ctrl); +} + +void Editor::mousedown_event(GUI::MouseEvent& event) +{ + auto highlighter = wrapper().editor().syntax_highlighter(); + if (!highlighter) { + GUI::TextEditor::mousedown_event(event); + return; + } + + auto text_position = text_position_at(event.position()); + auto ruler_line_rect = ruler_content_rect(text_position.line()); + if (event.button() == GUI::MouseButton::Left && event.position().x() < ruler_line_rect.width()) { + if (!breakpoint_lines().contains_slow(text_position.line())) { + breakpoint_lines().append(text_position.line()); + Debugger::on_breakpoint_change(wrapper().filename_label().text(), text_position.line(), BreakpointChange::Added); + } else { + breakpoint_lines().remove_first_matching([&](size_t line) { return line == text_position.line(); }); + Debugger::on_breakpoint_change(wrapper().filename_label().text(), text_position.line(), BreakpointChange::Removed); + } + } + + if (!(event.modifiers() & Mod_Ctrl)) { + GUI::TextEditor::mousedown_event(event); + return; + } + + if (!text_position.is_valid()) { + GUI::TextEditor::mousedown_event(event); + return; + } + + if (auto* span = document().span_at(text_position)) { + if (!highlighter->is_navigatable(span->data)) { + GUI::TextEditor::mousedown_event(event); + return; + } + + auto adjusted_range = span->range; + adjusted_range.end().set_column(adjusted_range.end().column() + 1); + auto span_text = document().text_in_range(adjusted_range); + auto header_path = span_text.substring(1, span_text.length() - 2); +#ifdef EDITOR_DEBUG + dbgln("Ctrl+click: {} \"{}\"", adjusted_range, header_path); +#endif + navigate_to_include_if_available(header_path); + return; + } + + GUI::TextEditor::mousedown_event(event); +} + +void Editor::enter_event(Core::Event& event) +{ + m_hovering_editor = true; + GUI::TextEditor::enter_event(event); +} + +void Editor::leave_event(Core::Event& event) +{ + m_hovering_editor = false; + GUI::TextEditor::leave_event(event); +} + +static HashMap<String, String>& include_paths() +{ + static HashMap<String, String> paths; + + auto add_directory = [](String base, Optional<String> recursive, auto handle_directory) -> void { + Core::DirIterator it(recursive.value_or(base), Core::DirIterator::Flags::SkipDots); + while (it.has_next()) { + auto path = it.next_full_path(); + if (!Core::File::is_directory(path)) { + auto key = path.substring(base.length() + 1, path.length() - base.length() - 1); +#ifdef EDITOR_DEBUG + dbgln("Adding header \"{}\" in path \"{}\"", key, path); +#endif + paths.set(key, path); + } else { + handle_directory(base, path, handle_directory); + } + } + }; + + if (paths.is_empty()) { + add_directory(".", {}, add_directory); + add_directory("/usr/local/include", {}, add_directory); + add_directory("/usr/local/include/c++/9.2.0", {}, add_directory); + add_directory("/usr/include", {}, add_directory); + } + + return paths; +} + +void Editor::navigate_to_include_if_available(String path) +{ + auto it = include_paths().find(path); + if (it == include_paths().end()) { +#ifdef EDITOR_DEBUG + dbgln("no header {} found.", path); +#endif + return; + } + + on_open(it->value); +} + +void Editor::set_execution_position(size_t line_number) +{ + code_document().set_execution_position(line_number); + scroll_position_into_view({ line_number, 0 }); + update(breakpoint_icon_rect(line_number)); +} + +void Editor::clear_execution_position() +{ + if (!execution_position().has_value()) { + return; + } + size_t previous_position = execution_position().value(); + code_document().clear_execution_position(); + update(breakpoint_icon_rect(previous_position)); +} + +const Gfx::Bitmap& Editor::breakpoint_icon_bitmap() +{ + static auto bitmap = Gfx::Bitmap::load_from_file("/res/icons/16x16/breakpoint.png"); + return *bitmap; +} + +const Gfx::Bitmap& Editor::current_position_icon_bitmap() +{ + static auto bitmap = Gfx::Bitmap::load_from_file("/res/icons/16x16/go-forward.png"); + return *bitmap; +} + +const CodeDocument& Editor::code_document() const +{ + const auto& doc = document(); + ASSERT(doc.is_code_document()); + return static_cast<const CodeDocument&>(doc); +} + +CodeDocument& Editor::code_document() +{ + return const_cast<CodeDocument&>(static_cast<const Editor&>(*this).code_document()); +} + +void Editor::set_document(GUI::TextDocument& doc) +{ + ASSERT(doc.is_code_document()); + GUI::TextEditor::set_document(doc); + + set_override_cursor(Gfx::StandardCursor::IBeam); + + CodeDocument& code_document = static_cast<CodeDocument&>(doc); + switch (code_document.language()) { + case Language::Cpp: + set_syntax_highlighter(make<GUI::CppSyntaxHighlighter>()); + m_language_client = get_language_client<LanguageClients::Cpp::ServerConnection>(project().root_path()); + break; + case Language::GML: + set_syntax_highlighter(make<GUI::GMLSyntaxHighlighter>()); + break; + case Language::JavaScript: + set_syntax_highlighter(make<GUI::JSSyntaxHighlighter>()); + break; + case Language::Ini: + set_syntax_highlighter(make<GUI::IniSyntaxHighlighter>()); + break; + case Language::Shell: + set_syntax_highlighter(make<GUI::ShellSyntaxHighlighter>()); + m_language_client = get_language_client<LanguageClients::Shell::ServerConnection>(project().root_path()); + break; + default: + set_syntax_highlighter({}); + } + + if (m_language_client) { + set_autocomplete_provider(make<LanguageServerAidedAutocompleteProvider>(*m_language_client)); + dbgln("Opening {}", code_document.file_path()); + int fd = open(code_document.file_path().characters(), O_RDONLY | O_NOCTTY); + if (fd < 0) { + perror("open"); + return; + } + m_language_client->open_file(code_document.file_path(), fd); + close(fd); + } +} + +Optional<Editor::AutoCompleteRequestData> Editor::get_autocomplete_request_data() +{ + if (!wrapper().editor().m_language_client) + return {}; + + return Editor::AutoCompleteRequestData { cursor() }; +} + +void Editor::LanguageServerAidedAutocompleteProvider::provide_completions(Function<void(Vector<Entry>)> callback) +{ + auto& editor = static_cast<Editor&>(*m_editor).wrapper().editor(); + auto data = editor.get_autocomplete_request_data(); + if (!data.has_value()) + callback({}); + + m_language_client.on_autocomplete_suggestions = [callback = move(callback)](auto suggestions) { + callback(suggestions); + }; + + m_language_client.request_autocomplete( + editor.code_document().file_path(), + data.value().position.line(), + data.value().position.column()); +} + +void Editor::on_edit_action(const GUI::Command& command) +{ + if (!m_language_client) + return; + + if (command.is_insert_text()) { + const GUI::InsertTextCommand& insert_command = static_cast<const GUI::InsertTextCommand&>(command); + m_language_client->insert_text( + code_document().file_path(), + insert_command.text(), + insert_command.range().start().line(), + insert_command.range().start().column()); + return; + } + + if (command.is_remove_text()) { + const GUI::RemoveTextCommand& remove_command = static_cast<const GUI::RemoveTextCommand&>(command); + m_language_client->remove_text( + code_document().file_path(), + remove_command.range().start().line(), + remove_command.range().start().column(), + remove_command.range().end().line(), + remove_command.range().end().column()); + return; + } + + ASSERT_NOT_REACHED(); +} + +void Editor::undo() +{ + TextEditor::undo(); + flush_file_content_to_langauge_server(); +} + +void Editor::redo() +{ + TextEditor::redo(); + flush_file_content_to_langauge_server(); +} + +void Editor::flush_file_content_to_langauge_server() +{ + if (!m_language_client) + return; + + m_language_client->set_file_content( + code_document().file_path(), + document().text()); +} +} diff --git a/Userland/DevTools/HackStudio/Editor.h b/Userland/DevTools/HackStudio/Editor.h new file mode 100644 index 0000000000..10c83a59d0 --- /dev/null +++ b/Userland/DevTools/HackStudio/Editor.h @@ -0,0 +1,118 @@ +/* + * Copyright (c) 2018-2020, Andreas Kling <kling@serenityos.org> + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * 1. Redistributions of source code must retain the above copyright notice, this + * list of conditions and the following disclaimer. + * + * 2. Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE + * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL + * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR + * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER + * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, + * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE + * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +#pragma once + +#include "CodeDocument.h" +#include "Debugger/BreakpointCallback.h" +#include "LanguageClient.h" +#include <AK/Optional.h> +#include <AK/OwnPtr.h> +#include <LibGUI/TextEditor.h> +#include <LibWeb/Forward.h> + +namespace HackStudio { + +class EditorWrapper; + +class Editor final : public GUI::TextEditor { + C_OBJECT(Editor) +public: + virtual ~Editor() override; + + Function<void()> on_focus; + Function<void(String)> on_open; + + EditorWrapper& wrapper(); + const EditorWrapper& wrapper() const; + + const Vector<size_t>& breakpoint_lines() const { return code_document().breakpoint_lines(); } + Vector<size_t>& breakpoint_lines() { return code_document().breakpoint_lines(); } + Optional<size_t> execution_position() const { return code_document().execution_position(); } + void set_execution_position(size_t line_number); + void clear_execution_position(); + + const CodeDocument& code_document() const; + CodeDocument& code_document(); + + virtual void set_document(GUI::TextDocument&) override; + + virtual void on_edit_action(const GUI::Command&) override; + + virtual void undo() override; + virtual void redo() override; + +private: + virtual void focusin_event(GUI::FocusEvent&) override; + virtual void focusout_event(GUI::FocusEvent&) override; + virtual void paint_event(GUI::PaintEvent&) override; + virtual void mousemove_event(GUI::MouseEvent&) override; + virtual void mousedown_event(GUI::MouseEvent&) override; + virtual void enter_event(Core::Event&) override; + virtual void leave_event(Core::Event&) override; + + void show_documentation_tooltip_if_available(const String&, const Gfx::IntPoint& screen_location); + void navigate_to_include_if_available(String); + + Gfx::IntRect breakpoint_icon_rect(size_t line_number) const; + static const Gfx::Bitmap& breakpoint_icon_bitmap(); + static const Gfx::Bitmap& current_position_icon_bitmap(); + + struct AutoCompleteRequestData { + GUI::TextPosition position; + }; + + class LanguageServerAidedAutocompleteProvider final : virtual public GUI::AutocompleteProvider { + public: + LanguageServerAidedAutocompleteProvider(LanguageClient& client) + : m_language_client(client) + { + } + virtual ~LanguageServerAidedAutocompleteProvider() override { } + + private: + virtual void provide_completions(Function<void(Vector<Entry>)> callback) override; + LanguageClient& m_language_client; + }; + + Optional<AutoCompleteRequestData> get_autocomplete_request_data(); + + void flush_file_content_to_langauge_server(); + + explicit Editor(); + + RefPtr<GUI::Window> m_documentation_tooltip_window; + RefPtr<Web::OutOfProcessWebView> m_documentation_page_view; + String m_last_parsed_token; + GUI::TextPosition m_previous_text_position { 0, 0 }; + bool m_hovering_editor { false }; + bool m_hovering_link { false }; + bool m_autocomplete_in_focus { false }; + + OwnPtr<LanguageClient> m_language_client; +}; + +} diff --git a/Userland/DevTools/HackStudio/EditorWrapper.cpp b/Userland/DevTools/HackStudio/EditorWrapper.cpp new file mode 100644 index 0000000000..e4b6a346c7 --- /dev/null +++ b/Userland/DevTools/HackStudio/EditorWrapper.cpp @@ -0,0 +1,84 @@ +/* + * Copyright (c) 2018-2020, Andreas Kling <kling@serenityos.org> + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * 1. Redistributions of source code must retain the above copyright notice, this + * list of conditions and the following disclaimer. + * + * 2. Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE + * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL + * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR + * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER + * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, + * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE + * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +#include "EditorWrapper.h" +#include "Editor.h" +#include "HackStudio.h" +#include <LibGUI/Action.h> +#include <LibGUI/BoxLayout.h> +#include <LibGUI/InputBox.h> +#include <LibGUI/Label.h> +#include <LibGfx/Font.h> +#include <LibGfx/FontDatabase.h> + +namespace HackStudio { + +EditorWrapper::EditorWrapper() +{ + set_layout<GUI::VerticalBoxLayout>(); + + auto& label_wrapper = add<GUI::Widget>(); + label_wrapper.set_fixed_height(14); + label_wrapper.set_fill_with_background_color(true); + label_wrapper.set_layout<GUI::HorizontalBoxLayout>(); + label_wrapper.layout()->set_margins({ 2, 0, 2, 0 }); + + m_filename_label = label_wrapper.add<GUI::Label>("(Untitled)"); + m_filename_label->set_text_alignment(Gfx::TextAlignment::CenterLeft); + m_filename_label->set_fixed_height(14); + + m_cursor_label = label_wrapper.add<GUI::Label>("(Cursor)"); + m_cursor_label->set_text_alignment(Gfx::TextAlignment::CenterRight); + m_cursor_label->set_fixed_height(14); + + m_editor = add<Editor>(); + m_editor->set_ruler_visible(true); + m_editor->set_line_wrapping_enabled(true); + m_editor->set_automatic_indentation_enabled(true); + + m_editor->on_cursor_change = [this] { + m_cursor_label->set_text(String::formatted("Line: {}, Column: {}", m_editor->cursor().line() + 1, m_editor->cursor().column())); + }; + + m_editor->on_focus = [this] { + set_current_editor_wrapper(this); + }; + + m_editor->on_open = [](String path) { + open_file(path); + }; +} + +EditorWrapper::~EditorWrapper() +{ +} + +void EditorWrapper::set_editor_has_focus(Badge<Editor>, bool focus) +{ + m_filename_label->set_font(focus ? Gfx::FontDatabase::default_bold_font() : Gfx::FontDatabase::default_font()); +} + +} diff --git a/Userland/DevTools/HackStudio/EditorWrapper.h b/Userland/DevTools/HackStudio/EditorWrapper.h new file mode 100644 index 0000000000..0cf67032a2 --- /dev/null +++ b/Userland/DevTools/HackStudio/EditorWrapper.h @@ -0,0 +1,60 @@ +/* + * Copyright (c) 2018-2020, Andreas Kling <kling@serenityos.org> + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * 1. Redistributions of source code must retain the above copyright notice, this + * list of conditions and the following disclaimer. + * + * 2. Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE + * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL + * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR + * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER + * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, + * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE + * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +#pragma once + +#include "Debugger/BreakpointCallback.h" +#include <AK/Function.h> +#include <AK/Vector.h> +#include <LibGUI/Widget.h> +#include <string.h> + +namespace HackStudio { + +class Editor; + +class EditorWrapper : public GUI::Widget { + C_OBJECT(EditorWrapper) +public: + virtual ~EditorWrapper() override; + + Editor& editor() { return *m_editor; } + const Editor& editor() const { return *m_editor; } + + GUI::Label& filename_label() { return *m_filename_label; } + const GUI::Label& filename_label() const { return *m_filename_label; } + + void set_editor_has_focus(Badge<Editor>, bool); + +private: + EditorWrapper(); + + RefPtr<GUI::Label> m_filename_label; + RefPtr<GUI::Label> m_cursor_label; + RefPtr<Editor> m_editor; +}; + +} diff --git a/Userland/DevTools/HackStudio/FindInFilesWidget.cpp b/Userland/DevTools/HackStudio/FindInFilesWidget.cpp new file mode 100644 index 0000000000..8347e73c81 --- /dev/null +++ b/Userland/DevTools/HackStudio/FindInFilesWidget.cpp @@ -0,0 +1,173 @@ +/* + * Copyright (c) 2018-2020, Andreas Kling <kling@serenityos.org> + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * 1. Redistributions of source code must retain the above copyright notice, this + * list of conditions and the following disclaimer. + * + * 2. Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE + * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL + * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR + * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER + * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, + * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE + * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +#include "FindInFilesWidget.h" +#include "HackStudio.h" +#include "Project.h" +#include <AK/StringBuilder.h> +#include <LibGUI/BoxLayout.h> +#include <LibGUI/Button.h> +#include <LibGUI/TableView.h> +#include <LibGUI/TextBox.h> +#include <LibGfx/FontDatabase.h> + +namespace HackStudio { + +struct Match { + String filename; + GUI::TextRange range; + String text; +}; + +class SearchResultsModel final : public GUI::Model { +public: + enum Column { + Filename, + Location, + MatchedText, + __Count + }; + + explicit SearchResultsModel(const Vector<Match>&& matches) + : m_matches(move(matches)) + { + } + + virtual int row_count(const GUI::ModelIndex& = GUI::ModelIndex()) const override { return m_matches.size(); } + virtual int column_count(const GUI::ModelIndex& = GUI::ModelIndex()) const override { return Column::__Count; } + + virtual String column_name(int column) const override + { + switch (column) { + case Column::Filename: + return "Filename"; + case Column::Location: + return "#"; + case Column::MatchedText: + return "Text"; + default: + ASSERT_NOT_REACHED(); + } + } + + virtual GUI::Variant data(const GUI::ModelIndex& index, GUI::ModelRole role) const override + { + if (role == GUI::ModelRole::TextAlignment) + return Gfx::TextAlignment::CenterLeft; + if (role == GUI::ModelRole::Font) { + if (index.column() == Column::MatchedText) + return Gfx::FontDatabase::default_fixed_width_font(); + return {}; + } + if (role == GUI::ModelRole::Display) { + auto& match = m_matches.at(index.row()); + switch (index.column()) { + case Column::Filename: + return match.filename; + case Column::Location: + return (int)match.range.start().line(); + case Column::MatchedText: + return match.text; + } + } + return {}; + } + + virtual void update() override { } + virtual GUI::ModelIndex index(int row, int column = 0, const GUI::ModelIndex& = GUI::ModelIndex()) const override + { + if (row < 0 || row >= (int)m_matches.size()) + return {}; + if (column < 0 || column >= Column::__Count) + return {}; + return create_index(row, column, &m_matches.at(row)); + } + +private: + Vector<Match> m_matches; +}; + +static RefPtr<SearchResultsModel> find_in_files(const StringView& text) +{ + Vector<Match> matches; + project().for_each_text_file([&](auto& file) { + auto matches_in_file = file.document().find_all(text); + for (auto& range : matches_in_file) { + auto whole_line_range = file.document().range_for_entire_line(range.start().line()); + auto whole_line_containing_match = file.document().text_in_range(whole_line_range); + auto left_part = whole_line_containing_match.substring(0, range.start().column()); + auto right_part = whole_line_containing_match.substring(range.end().column(), whole_line_containing_match.length() - range.end().column()); + StringBuilder builder; + builder.append(left_part); + builder.append(0x01); + builder.append(file.document().text_in_range(range)); + builder.append(0x02); + builder.append(right_part); + matches.append({ file.name(), range, builder.to_string() }); + } + }); + + return adopt(*new SearchResultsModel(move(matches))); +} + +FindInFilesWidget::FindInFilesWidget() +{ + set_layout<GUI::VerticalBoxLayout>(); + + auto& top_container = add<Widget>(); + top_container.set_layout<GUI::HorizontalBoxLayout>(); + top_container.set_fixed_height(20); + + m_textbox = top_container.add<GUI::TextBox>(); + + m_button = top_container.add<GUI::Button>("Find in files"); + m_button->set_fixed_width(100); + + m_result_view = add<GUI::TableView>(); + + m_result_view->on_activation = [](auto& index) { + auto& match = *(const Match*)index.internal_data(); + open_file(match.filename); + current_editor().set_selection(match.range); + current_editor().set_focus(true); + }; + + m_button->on_click = [this](auto) { + auto results_model = find_in_files(m_textbox->text()); + m_result_view->set_model(results_model); + }; + m_textbox->on_return_pressed = [this] { + m_button->click(); + }; +} + +void FindInFilesWidget::focus_textbox_and_select_all() +{ + m_textbox->select_all(); + m_textbox->set_focus(true); +} + +} diff --git a/Userland/DevTools/HackStudio/FindInFilesWidget.h b/Userland/DevTools/HackStudio/FindInFilesWidget.h new file mode 100644 index 0000000000..759d116374 --- /dev/null +++ b/Userland/DevTools/HackStudio/FindInFilesWidget.h @@ -0,0 +1,50 @@ +/* + * Copyright (c) 2018-2020, Andreas Kling <kling@serenityos.org> + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * 1. Redistributions of source code must retain the above copyright notice, this + * list of conditions and the following disclaimer. + * + * 2. Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE + * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL + * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR + * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER + * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, + * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE + * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +#pragma once + +#include <LibGUI/Button.h> +#include <LibGUI/TextBox.h> +#include <LibGUI/Widget.h> + +namespace HackStudio { + +class FindInFilesWidget final : public GUI::Widget { + C_OBJECT(FindInFilesWidget) +public: + virtual ~FindInFilesWidget() override { } + + void focus_textbox_and_select_all(); + +private: + explicit FindInFilesWidget(); + + RefPtr<GUI::TextBox> m_textbox; + RefPtr<GUI::Button> m_button; + RefPtr<GUI::TableView> m_result_view; +}; + +} diff --git a/Userland/DevTools/HackStudio/FormEditorWidget.cpp b/Userland/DevTools/HackStudio/FormEditorWidget.cpp new file mode 100644 index 0000000000..9e9773d2c7 --- /dev/null +++ b/Userland/DevTools/HackStudio/FormEditorWidget.cpp @@ -0,0 +1,68 @@ +/* + * Copyright (c) 2018-2020, Andreas Kling <kling@serenityos.org> + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * 1. Redistributions of source code must retain the above copyright notice, this + * list of conditions and the following disclaimer. + * + * 2. Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE + * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL + * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR + * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER + * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, + * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE + * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +#include "FormEditorWidget.h" +#include "CursorTool.h" +#include "FormWidget.h" +#include "WidgetTreeModel.h" +#include <LibGUI/Painter.h> + +namespace HackStudio { + +FormEditorWidget::FormEditorWidget() + : m_tool(make<CursorTool>(*this)) +{ + set_fill_with_background_color(true); + + m_form_widget = add<FormWidget>(); + m_widget_tree_model = WidgetTreeModel::create(*m_form_widget); +} + +FormEditorWidget::~FormEditorWidget() +{ +} + +void FormEditorWidget::paint_event(GUI::PaintEvent& event) +{ + GUI::Frame::paint_event(event); + + GUI::Painter painter(*this); + painter.add_clip_rect(event.rect()); +} + +void FormEditorWidget::set_tool(NonnullOwnPtr<Tool> tool) +{ + m_tool->detach(); + m_tool = move(tool); + m_tool->attach(); +} + +WidgetTreeModel& FormEditorWidget::model() +{ + return *m_widget_tree_model; +} + +} diff --git a/Userland/DevTools/HackStudio/FormEditorWidget.h b/Userland/DevTools/HackStudio/FormEditorWidget.h new file mode 100644 index 0000000000..a913eed01e --- /dev/null +++ b/Userland/DevTools/HackStudio/FormEditorWidget.h @@ -0,0 +1,137 @@ +/* + * Copyright (c) 2018-2020, Andreas Kling <kling@serenityos.org> + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * 1. Redistributions of source code must retain the above copyright notice, this + * list of conditions and the following disclaimer. + * + * 2. Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE + * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL + * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR + * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER + * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, + * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE + * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +#pragma once + +#include <AK/HashTable.h> +#include <LibGUI/ScrollableWidget.h> + +namespace HackStudio { + +class FormWidget; +class Tool; +class WidgetTreeModel; + +class FormEditorWidget final : public GUI::ScrollableWidget { + C_OBJECT(FormEditorWidget) +public: + virtual ~FormEditorWidget() override; + + FormWidget& form_widget() { return *m_form_widget; } + const FormWidget& form_widget() const { return *m_form_widget; } + + Tool& tool() { return *m_tool; } + const Tool& tool() const { return *m_tool; } + + void set_tool(NonnullOwnPtr<Tool>); + + WidgetTreeModel& model(); + + class WidgetSelection { + public: + Function<void(GUI::Widget&)> on_remove; + Function<void(GUI::Widget&)> on_add; + Function<void()> on_clear; + + void enable_hooks() { m_hooks_enabled = true; } + void disable_hooks() { m_hooks_enabled = false; } + + bool is_empty() const + { + return m_widgets.is_empty(); + } + + bool contains(GUI::Widget& widget) const + { + return m_widgets.contains(&widget); + } + + void toggle(GUI::Widget& widget) + { + if (contains(widget)) + remove(widget); + else + add(widget); + } + + void set(GUI::Widget& widget) + { + clear(); + add(widget); + } + + void remove(GUI::Widget& widget) + { + ASSERT(m_widgets.contains(&widget)); + m_widgets.remove(&widget); + if (m_hooks_enabled && on_remove) + on_remove(widget); + } + + void add(GUI::Widget& widget) + { + m_widgets.set(&widget); + if (m_hooks_enabled && on_add) + on_add(widget); + } + + void clear() + { + m_widgets.clear(); + if (m_hooks_enabled && on_clear) + on_clear(); + } + + template<typename Callback> + void for_each(Callback callback) + { + for (auto& it : m_widgets) { + if (callback(*it) == IterationDecision::Break) + break; + } + } + + WidgetSelection() { } + + private: + HashTable<GUI::Widget*> m_widgets; + bool m_hooks_enabled { true }; + }; + + WidgetSelection& selection() { return m_selection; } + +private: + virtual void paint_event(GUI::PaintEvent&) override; + + FormEditorWidget(); + + RefPtr<FormWidget> m_form_widget; + RefPtr<WidgetTreeModel> m_widget_tree_model; + NonnullOwnPtr<Tool> m_tool; + WidgetSelection m_selection; +}; + +} diff --git a/Userland/DevTools/HackStudio/FormWidget.cpp b/Userland/DevTools/HackStudio/FormWidget.cpp new file mode 100644 index 0000000000..a9924a6a2b --- /dev/null +++ b/Userland/DevTools/HackStudio/FormWidget.cpp @@ -0,0 +1,106 @@ +/* + * Copyright (c) 2018-2020, Andreas Kling <kling@serenityos.org> + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * 1. Redistributions of source code must retain the above copyright notice, this + * list of conditions and the following disclaimer. + * + * 2. Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE + * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL + * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR + * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER + * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, + * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE + * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +#include "FormWidget.h" +#include "FormEditorWidget.h" +#include "Tool.h" +#include <LibGUI/Painter.h> + +namespace HackStudio { + +FormWidget::FormWidget() +{ + set_focus_policy(GUI::FocusPolicy::StrongFocus); + set_fill_with_background_color(true); + set_relative_rect(5, 5, 400, 300); + + set_greedy_for_hits(true); +} + +FormWidget::~FormWidget() +{ +} + +FormEditorWidget& FormWidget::editor() +{ + return static_cast<FormEditorWidget&>(*parent()); +} + +const FormEditorWidget& FormWidget::editor() const +{ + return static_cast<const FormEditorWidget&>(*parent()); +} + +void FormWidget::paint_event(GUI::PaintEvent& event) +{ + GUI::Painter painter(*this); + painter.add_clip_rect(event.rect()); + + for (int y = 0; y < height(); y += m_grid_size) { + for (int x = 0; x < width(); x += m_grid_size) { + painter.set_pixel({ x, y }, Color::from_rgb(0x404040)); + } + } +} + +void FormWidget::second_paint_event(GUI::PaintEvent& event) +{ + GUI::Painter painter(*this); + painter.add_clip_rect(event.rect()); + + if (!editor().selection().is_empty()) { + for_each_child_widget([&](auto& child) { + if (editor().selection().contains(child)) { + painter.draw_rect(child.relative_rect(), Color::Blue); + } + return IterationDecision::Continue; + }); + } + + editor().tool().on_second_paint(painter, event); +} + +void FormWidget::mousedown_event(GUI::MouseEvent& event) +{ + editor().tool().on_mousedown(event); +} + +void FormWidget::mouseup_event(GUI::MouseEvent& event) +{ + editor().tool().on_mouseup(event); +} + +void FormWidget::mousemove_event(GUI::MouseEvent& event) +{ + editor().tool().on_mousemove(event); +} + +void FormWidget::keydown_event(GUI::KeyEvent& event) +{ + editor().tool().on_keydown(event); +} + +} diff --git a/Userland/DevTools/HackStudio/FormWidget.h b/Userland/DevTools/HackStudio/FormWidget.h new file mode 100644 index 0000000000..32f10109f1 --- /dev/null +++ b/Userland/DevTools/HackStudio/FormWidget.h @@ -0,0 +1,61 @@ +/* + * Copyright (c) 2018-2020, Andreas Kling <kling@serenityos.org> + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * 1. Redistributions of source code must retain the above copyright notice, this + * list of conditions and the following disclaimer. + * + * 2. Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE + * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL + * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR + * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER + * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, + * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE + * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +#pragma once + +#include <AK/Badge.h> +#include <LibGUI/Widget.h> + +namespace HackStudio { + +class CursorTool; +class FormEditorWidget; + +class FormWidget final : public GUI::Widget { + C_OBJECT(FormWidget) +public: + virtual ~FormWidget() override; + + FormEditorWidget& editor(); + const FormEditorWidget& editor() const; + + // FIXME: This should be an app-wide preference instead. + int grid_size() const { return m_grid_size; } + +private: + virtual void paint_event(GUI::PaintEvent&) override; + virtual void second_paint_event(GUI::PaintEvent&) override; + virtual void mousedown_event(GUI::MouseEvent&) override; + virtual void mouseup_event(GUI::MouseEvent&) override; + virtual void mousemove_event(GUI::MouseEvent&) override; + virtual void keydown_event(GUI::KeyEvent&) override; + + FormWidget(); + + int m_grid_size { 5 }; +}; + +} diff --git a/Userland/DevTools/HackStudio/Git/DiffViewer.cpp b/Userland/DevTools/HackStudio/Git/DiffViewer.cpp new file mode 100644 index 0000000000..9633ff6d1d --- /dev/null +++ b/Userland/DevTools/HackStudio/Git/DiffViewer.cpp @@ -0,0 +1,253 @@ +/* + * Copyright (c) 2020, Itamar S. <itamar8910@gmail.com> + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * 1. Redistributions of source code must retain the above copyright notice, this + * list of conditions and the following disclaimer. + * + * 2. Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE + * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL + * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR + * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER + * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, + * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE + * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +#include "DiffViewer.h" +#include <LibDiff/Hunks.h> +#include <LibGUI/AbstractView.h> +#include <LibGUI/Painter.h> +#include <LibGUI/ScrollBar.h> +#include <LibGfx/Color.h> +#include <LibGfx/Font.h> +#include <LibGfx/FontDatabase.h> +#include <LibGfx/Palette.h> + +// #define DEBUG_DIFF + +namespace HackStudio { + +DiffViewer::~DiffViewer() +{ +} + +void DiffViewer::paint_event(GUI::PaintEvent& event) +{ + GUI::Painter painter(*this); + painter.add_clip_rect(widget_inner_rect()); + painter.add_clip_rect(event.rect()); + painter.fill_rect(event.rect(), palette().color(background_role())); + painter.translate(frame_thickness(), frame_thickness()); + painter.translate(-horizontal_scrollbar().value(), -vertical_scrollbar().value()); + + // Why we need to translate here again? We've already translated the painter. + // Anyways, it paints correctly so I'll leave it like this. + painter.fill_rect_with_dither_pattern( + separator_rect().translated(horizontal_scrollbar().value(), vertical_scrollbar().value()), + Gfx::Color::LightGray, + Gfx::Color::White); + + size_t y_offset = 10; + size_t current_original_line_index = 0; + for (const auto& hunk : m_hunks) { + for (size_t i = current_original_line_index; i < hunk.original_start_line; ++i) { + draw_line(painter, m_original_lines[i], y_offset, LinePosition::Both, LineType::Normal); + y_offset += line_height(); + } + current_original_line_index = hunk.original_start_line + hunk.removed_lines.size(); + + size_t left_y_offset = y_offset; + for (const auto& removed_line : hunk.removed_lines) { + draw_line(painter, removed_line, left_y_offset, LinePosition::Left, LineType::Diff); + left_y_offset += line_height(); + } + for (int i = 0; i < (int)hunk.added_lines.size() - (int)hunk.removed_lines.size(); ++i) { + draw_line(painter, "", left_y_offset, LinePosition::Left, LineType::Missing); + left_y_offset += line_height(); + } + + size_t right_y_offset = y_offset; + for (const auto& added_line : hunk.added_lines) { + draw_line(painter, added_line, right_y_offset, LinePosition::Right, LineType::Diff); + right_y_offset += line_height(); + } + for (int i = 0; i < (int)hunk.removed_lines.size() - (int)hunk.added_lines.size(); ++i) { + draw_line(painter, "", right_y_offset, LinePosition::Right, LineType::Missing); + right_y_offset += line_height(); + } + + ASSERT(left_y_offset == right_y_offset); + y_offset = left_y_offset; + } + for (size_t i = current_original_line_index; i < m_original_lines.size(); ++i) { + draw_line(painter, m_original_lines[i], y_offset, LinePosition::Both, LineType::Normal); + y_offset += line_height(); + } +} + +void DiffViewer::draw_line(GUI::Painter& painter, const String& line, size_t y_offset, LinePosition line_position, LineType line_type) +{ + size_t line_width = font().width(line); + + constexpr size_t padding = 10; + size_t left_side_x_offset = padding; + size_t right_side_x_offset = separator_rect().x() + padding; + + // FIXME: Long lines will overflow out of their side of the diff view + Gfx::IntRect left_line_rect { (int)left_side_x_offset, (int)y_offset, (int)line_width, (int)line_height() }; + Gfx::IntRect right_line_rect { (int)right_side_x_offset, (int)y_offset, (int)line_width, (int)line_height() }; + auto color = palette().color(foreground_role()); + + if (line_position == LinePosition::Left || line_position == LinePosition::Both) { + painter.draw_text(left_line_rect, line, Gfx::TextAlignment::TopLeft, color); + if (line_type != LineType::Normal) { + Gfx::IntRect outline = { (int)left_side_x_offset, ((int)y_offset) - 2, separator_rect().x() - (int)(padding * 2), (int)line_height() }; + if (line_type == LineType::Diff) { + painter.fill_rect( + outline, + red_background()); + } + if (line_type == LineType::Missing) { + painter.fill_rect( + outline, + gray_background()); + } + } + } + if (line_position == LinePosition::Right || line_position == LinePosition::Both) { + painter.draw_text(right_line_rect, line, Gfx::TextAlignment::TopLeft, color); + if (line_type != LineType::Normal) { + Gfx::IntRect outline = { (int)right_side_x_offset, ((int)y_offset) - 2, frame_inner_rect().width() - separator_rect().x() - (int)(padding * 2) - 10, (int)line_height() }; + if (line_type == LineType::Diff) { + painter.fill_rect( + outline, + green_background()); + } + if (line_type == LineType::Missing) { + painter.fill_rect( + outline, + gray_background()); + } + } + } +} + +size_t DiffViewer::line_height() const +{ + return font().glyph_height() + 4; +} + +Gfx::IntRect DiffViewer::separator_rect() const +{ + return Gfx::IntRect { frame_inner_rect().width() / 2 - 2, + 0, + 4, + frame_inner_rect().height() }; +} + +void DiffViewer::set_content(const String& original, const String& diff) +{ + m_original_lines = split_to_lines(original); + m_hunks = Diff::parse_hunks(diff); + +#ifdef DEBUG_DIFF + for (size_t i = 0; i < m_original_lines.size(); ++i) + dbgln("{}:{}", i, m_original_lines[i]); +#endif +} + +DiffViewer::DiffViewer() +{ + setup_properties(); +} + +DiffViewer::DiffViewer(const String& original, const String& diff) + : m_original_lines(split_to_lines(original)) + , m_hunks(Diff::parse_hunks(diff)) +{ + setup_properties(); +} + +void DiffViewer::setup_properties() +{ + set_font(Gfx::FontDatabase::default_fixed_width_font()); + set_background_role(ColorRole::Base); + set_foreground_role(ColorRole::BaseText); +} + +Vector<String> DiffViewer::split_to_lines(const String& text) +{ + // NOTE: This is slightly different than text.split('\n') + Vector<String> lines; + size_t next_line_start_index = 0; + for (size_t i = 0; i < text.length(); ++i) { + if (text[i] == '\n') { + auto line_text = text.substring(next_line_start_index, i - next_line_start_index); + lines.append(move(line_text)); + next_line_start_index = i + 1; + } + } + lines.append(text.substring(next_line_start_index, text.length() - next_line_start_index)); + return lines; +} + +Gfx::Color DiffViewer::red_background() +{ + static Gfx::Color color = Gfx::Color::from_rgba(0x88ff0000); + return color; +} + +Gfx::Color DiffViewer::green_background() +{ + static Gfx::Color color = Gfx::Color::from_rgba(0x8800ff00); + return color; +} + +Gfx::Color DiffViewer::gray_background() +{ + static Gfx::Color color = Gfx::Color::from_rgba(0x88888888); + return color; +} + +void DiffViewer::update_content_size() +{ + if (m_hunks.is_empty()) { + set_content_size({ 0, 0 }); + return; + } + + size_t num_lines = 0; + size_t current_original_line_index = 0; + for (const auto& hunk : m_hunks) { + num_lines += ((int)hunk.original_start_line - (int)current_original_line_index); + + num_lines += hunk.removed_lines.size(); + if (hunk.added_lines.size() > hunk.removed_lines.size()) { + num_lines += ((int)hunk.added_lines.size() - (int)hunk.removed_lines.size()); + } + current_original_line_index = hunk.original_start_line + hunk.removed_lines.size(); + } + num_lines += ((int)m_original_lines.size() - (int)current_original_line_index); + + // TODO: Support Horizontal scrolling + set_content_size({ 0, (int)(num_lines * line_height()) }); +} + +void DiffViewer::resize_event(GUI::ResizeEvent& event) +{ + ScrollableWidget::resize_event(event); + update_content_size(); +} + +} diff --git a/Userland/DevTools/HackStudio/Git/DiffViewer.h b/Userland/DevTools/HackStudio/Git/DiffViewer.h new file mode 100644 index 0000000000..1cc293e63d --- /dev/null +++ b/Userland/DevTools/HackStudio/Git/DiffViewer.h @@ -0,0 +1,80 @@ +/* + * Copyright (c) 2020, Itamar S. <itamar8910@gmail.com> + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * 1. Redistributions of source code must retain the above copyright notice, this + * list of conditions and the following disclaimer. + * + * 2. Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE + * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL + * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR + * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER + * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, + * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE + * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +#pragma once + +#include <AK/String.h> +#include <AK/Vector.h> +#include <LibDiff/Hunks.h> +#include <LibGUI/ScrollableWidget.h> + +namespace HackStudio { +class DiffViewer final : public GUI::ScrollableWidget { + C_OBJECT(DiffViewer) +public: + virtual ~DiffViewer() override; + + void set_content(const String& original, const String& diff); + +private: + DiffViewer(const String& original, const String& diff); + DiffViewer(); + + void setup_properties(); + + virtual void paint_event(GUI::PaintEvent&) override; + virtual void resize_event(GUI::ResizeEvent&) override; + + void update_content_size(); + + enum class LinePosition { + Left, + Right, + Both, + }; + + enum class LineType { + Normal, + Diff, + Missing, + }; + + void draw_line(GUI::Painter&, const String& line, size_t y_offset, LinePosition, LineType); + + static Vector<String> split_to_lines(const String& text); + + static Gfx::Color red_background(); + static Gfx::Color green_background(); + static Gfx::Color gray_background(); + + size_t line_height() const; + + Gfx::IntRect separator_rect() const; + + Vector<String> m_original_lines; + Vector<Diff::Hunk> m_hunks; +}; +} diff --git a/Userland/DevTools/HackStudio/Git/GitFilesModel.cpp b/Userland/DevTools/HackStudio/Git/GitFilesModel.cpp new file mode 100644 index 0000000000..720198586a --- /dev/null +++ b/Userland/DevTools/HackStudio/Git/GitFilesModel.cpp @@ -0,0 +1,56 @@ +/* + * Copyright (c) 2020, Itamar S. <itamar8910@gmail.com> + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * 1. Redistributions of source code must retain the above copyright notice, this + * list of conditions and the following disclaimer. + * + * 2. Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE + * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL + * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR + * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER + * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, + * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE + * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +#include "GitFilesModel.h" + +namespace HackStudio { + +NonnullRefPtr<GitFilesModel> GitFilesModel::create(Vector<LexicalPath>&& files) +{ + return adopt(*new GitFilesModel(move(files))); +} + +GitFilesModel::GitFilesModel(Vector<LexicalPath>&& files) + : m_files(move(files)) +{ +} + +GUI::Variant GitFilesModel::data(const GUI::ModelIndex& index, GUI::ModelRole role) const +{ + if (role == GUI::ModelRole::Display) { + return m_files.at(index.row()).string(); + } + return {}; +} + +GUI::ModelIndex GitFilesModel::index(int row, int column, const GUI::ModelIndex&) const +{ + if (row < 0 || row >= static_cast<int>(m_files.size())) + return {}; + return create_index(row, column, &m_files.at(row)); +} + +}; diff --git a/Userland/DevTools/HackStudio/Git/GitFilesModel.h b/Userland/DevTools/HackStudio/Git/GitFilesModel.h new file mode 100644 index 0000000000..a3e1483ded --- /dev/null +++ b/Userland/DevTools/HackStudio/Git/GitFilesModel.h @@ -0,0 +1,57 @@ +/* + * Copyright (c) 2020, Itamar S. <itamar8910@gmail.com> + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * 1. Redistributions of source code must retain the above copyright notice, this + * list of conditions and the following disclaimer. + * + * 2. Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE + * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL + * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR + * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER + * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, + * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE + * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +#pragma once + +#include "GitRepo.h" +#include <AK/LexicalPath.h> +#include <AK/NonnullRefPtr.h> +#include <LibGUI/Model.h> + +namespace HackStudio { + +class GitFilesModel final : public GUI::Model { +public: + static NonnullRefPtr<GitFilesModel> create(Vector<LexicalPath>&& files); + + virtual int row_count(const GUI::ModelIndex& = GUI::ModelIndex()) const override { return m_files.size(); } + virtual int column_count(const GUI::ModelIndex& = GUI::ModelIndex()) const override { return 1; } + + virtual String column_name(int) const override + { + return ""; + } + + virtual GUI::Variant data(const GUI::ModelIndex&, GUI::ModelRole) const override; + + virtual void update() override { } + virtual GUI::ModelIndex index(int row, int column, const GUI::ModelIndex&) const override; + +private: + explicit GitFilesModel(Vector<LexicalPath>&& files); + Vector<LexicalPath> m_files; +}; +} diff --git a/Userland/DevTools/HackStudio/Git/GitFilesView.cpp b/Userland/DevTools/HackStudio/Git/GitFilesView.cpp new file mode 100644 index 0000000000..76aa3abda8 --- /dev/null +++ b/Userland/DevTools/HackStudio/Git/GitFilesView.cpp @@ -0,0 +1,82 @@ +/* + * Copyright (c) 2020, Itamar S. <itamar8910@gmail.com> + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * 1. Redistributions of source code must retain the above copyright notice, this + * list of conditions and the following disclaimer. + * + * 2. Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE + * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL + * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR + * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER + * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, + * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE + * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +#include "GitFilesView.h" +#include <LibGUI/Model.h> +#include <LibGUI/Painter.h> +#include <LibGUI/ScrollBar.h> +#include <LibGfx/Palette.h> + +namespace HackStudio { +GitFilesView::~GitFilesView() +{ +} + +void GitFilesView::paint_list_item(GUI::Painter& painter, int row_index, int painted_item_index) +{ + ListView::paint_list_item(painter, row_index, painted_item_index); + + painter.blit(action_icon_rect((size_t)painted_item_index).top_left(), *m_action_icon, m_action_icon->rect()); +} + +Gfx::IntRect GitFilesView::action_icon_rect(size_t painted_item_index) +{ + int y = painted_item_index * item_height(); + return { content_width() - 20, y, m_action_icon->rect().width(), m_action_icon->rect().height() }; +} + +GitFilesView::GitFilesView(GitFileActionCallback callback, NonnullRefPtr<Gfx::Bitmap> action_icon) + : m_action_callback(move(callback)) + , m_action_icon(action_icon) +{ + set_alternating_row_colors(false); +} + +void GitFilesView::mousedown_event(GUI::MouseEvent& event) +{ + if (event.button() != GUI::MouseButton::Left) { + ListView::mousedown_event(event); + return; + } + + if (event.x() < action_icon_rect(0).x() || event.x() > action_icon_rect(0).top_right().x()) { + ListView::mousedown_event(event); + return; + } + + size_t item_index = (event.y() + vertical_scrollbar().value()) / item_height(); + if (model()->row_count() == 0 || item_index > (size_t)model()->row_count()) { + ListView::mousedown_event(event); + return; + } + + auto data = model()->index(item_index, model_column()).data(); + + ASSERT(data.is_string()); + m_action_callback(LexicalPath(data.to_string())); +} + +}; diff --git a/Userland/DevTools/HackStudio/Git/GitFilesView.h b/Userland/DevTools/HackStudio/Git/GitFilesView.h new file mode 100644 index 0000000000..8eb636304c --- /dev/null +++ b/Userland/DevTools/HackStudio/Git/GitFilesView.h @@ -0,0 +1,56 @@ +/* + * Copyright (c) 2020, Itamar S. <itamar8910@gmail.com> + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * 1. Redistributions of source code must retain the above copyright notice, this + * list of conditions and the following disclaimer. + * + * 2. Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE + * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL + * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR + * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER + * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, + * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE + * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +#pragma once + +#include <AK/LexicalPath.h> +#include <LibGUI/ListView.h> +#include <LibGfx/Bitmap.h> + +namespace HackStudio { + +// A "GitFileAction" is either the staging or the unstaging of a file. +typedef Function<void(const LexicalPath& file)> GitFileActionCallback; + +class GitFilesView : public GUI::ListView { + C_OBJECT(GitFilesView) +public: + virtual ~GitFilesView() override; + +protected: + GitFilesView(GitFileActionCallback, NonnullRefPtr<Gfx::Bitmap> action_icon); + +private: + virtual void paint_list_item(GUI::Painter& painter, int row_index, int painted_item_index); + + virtual void mousedown_event(GUI::MouseEvent&) override; + virtual Gfx::IntRect action_icon_rect(size_t painted_item_index); + + GitFileActionCallback m_action_callback; + NonnullRefPtr<Gfx::Bitmap> m_action_icon; +}; + +} diff --git a/Userland/DevTools/HackStudio/Git/GitRepo.cpp b/Userland/DevTools/HackStudio/Git/GitRepo.cpp new file mode 100644 index 0000000000..ea68c88ff6 --- /dev/null +++ b/Userland/DevTools/HackStudio/Git/GitRepo.cpp @@ -0,0 +1,151 @@ +/* + * Copyright (c) 2020, Itamar S. <itamar8910@gmail.com> + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * 1. Redistributions of source code must retain the above copyright notice, this + * list of conditions and the following disclaimer. + * + * 2. Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE + * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL + * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR + * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER + * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, + * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE + * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +#include "GitRepo.h" +#include <LibCore/Command.h> +#include <stdio.h> +#include <stdlib.h> + +namespace HackStudio { + +GitRepo::CreateResult GitRepo::try_to_create(const LexicalPath& repository_root) +{ + if (!git_is_installed()) { + return { CreateResult::Type::GitProgramNotFound, nullptr }; + } + if (!git_repo_exists(repository_root)) { + return { CreateResult::Type::NoGitRepo, nullptr }; + } + + return { CreateResult::Type::Success, adopt(*new GitRepo(repository_root)) }; +} + +RefPtr<GitRepo> GitRepo::initialize_repository(const LexicalPath& repository_root) +{ + auto res = command_wrapper({ "init" }, repository_root); + if (res.is_null()) + return {}; + + ASSERT(git_repo_exists(repository_root)); + return adopt(*new GitRepo(repository_root)); +} + +Vector<LexicalPath> GitRepo::unstaged_files() const +{ + auto modified = modified_files(); + auto untracked = untracked_files(); + modified.append(move(untracked)); + return modified; +} +// +Vector<LexicalPath> GitRepo::staged_files() const +{ + auto raw_result = command({ "diff", "--cached", "--name-only" }); + if (raw_result.is_null()) + return {}; + return parse_files_list(raw_result); +} + +Vector<LexicalPath> GitRepo::modified_files() const +{ + auto raw_result = command({ "ls-files", "--modified", "--exclude-standard" }); + if (raw_result.is_null()) + return {}; + return parse_files_list(raw_result); +} + +Vector<LexicalPath> GitRepo::untracked_files() const +{ + auto raw_result = command({ "ls-files", "--others", "--exclude-standard" }); + if (raw_result.is_null()) + return {}; + return parse_files_list(raw_result); +} + +Vector<LexicalPath> GitRepo::parse_files_list(const String& raw_result) +{ + auto lines = raw_result.split('\n'); + Vector<LexicalPath> files; + for (const auto& line : lines) { + files.empend(line); + } + return files; +} + +String GitRepo::command(const Vector<String>& command_parts) const +{ + return command_wrapper(command_parts, m_repository_root); +} + +String GitRepo::command_wrapper(const Vector<String>& command_parts, const LexicalPath& chdir) +{ + return Core::command("git", command_parts, chdir); +} + +bool GitRepo::git_is_installed() +{ + return !command_wrapper({ "--help" }, LexicalPath("/")).is_null(); +} + +bool GitRepo::git_repo_exists(const LexicalPath& repo_root) +{ + return !command_wrapper({ "status" }, repo_root).is_null(); +} + +bool GitRepo::stage(const LexicalPath& file) +{ + return !command({ "add", file.string() }).is_null(); +} + +bool GitRepo::unstage(const LexicalPath& file) +{ + return !command({ "reset", "HEAD", "--", file.string() }).is_null(); +} + +bool GitRepo::commit(const String& message) +{ + return !command({ "commit", "-m", message }).is_null(); +} + +Optional<String> GitRepo::original_file_content(const LexicalPath& file) const +{ + return command({ "show", String::formatted("HEAD:{}", file) }); +} + +Optional<String> GitRepo::unstaged_diff(const LexicalPath& file) const +{ + return command({ "diff", file.string().characters() }); +} + +bool GitRepo::is_tracked(const LexicalPath& file) const +{ + auto res = command({ "ls-files", file.string() }); + if (res.is_null()) + return false; + return !res.is_empty(); +} + +} diff --git a/Userland/DevTools/HackStudio/Git/GitRepo.h b/Userland/DevTools/HackStudio/Git/GitRepo.h new file mode 100644 index 0000000000..03037b224a --- /dev/null +++ b/Userland/DevTools/HackStudio/Git/GitRepo.h @@ -0,0 +1,81 @@ +/* + * Copyright (c) 2020, Itamar S. <itamar8910@gmail.com> + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * 1. Redistributions of source code must retain the above copyright notice, this + * list of conditions and the following disclaimer. + * + * 2. Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE + * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL + * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR + * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER + * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, + * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE + * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +#pragma once + +#include <AK/LexicalPath.h> +#include <AK/Optional.h> +#include <AK/RefCounted.h> +#include <AK/RefPtr.h> +#include <AK/String.h> +#include <AK/Vector.h> + +namespace HackStudio { + +class GitRepo final : public RefCounted<GitRepo> { +public: + struct CreateResult { + enum class Type { + Success, + NoGitRepo, + GitProgramNotFound, + }; + Type type; + RefPtr<GitRepo> repo; + }; + + static CreateResult try_to_create(const LexicalPath& repository_root); + static RefPtr<GitRepo> initialize_repository(const LexicalPath& repository_root); + + Vector<LexicalPath> unstaged_files() const; + Vector<LexicalPath> staged_files() const; + bool stage(const LexicalPath& file); + bool unstage(const LexicalPath& file); + bool commit(const String& message); + Optional<String> original_file_content(const LexicalPath& file) const; + Optional<String> unstaged_diff(const LexicalPath& file) const; + bool is_tracked(const LexicalPath& file) const; + +private: + static String command_wrapper(const Vector<String>& command_parts, const LexicalPath& chdir); + static bool git_is_installed(); + static bool git_repo_exists(const LexicalPath& repo_root); + static Vector<LexicalPath> parse_files_list(const String&); + + explicit GitRepo(const LexicalPath& repository_root) + : m_repository_root(repository_root) + { + } + + Vector<LexicalPath> modified_files() const; + Vector<LexicalPath> untracked_files() const; + + String command(const Vector<String>& command_parts) const; + + LexicalPath m_repository_root; +}; + +} diff --git a/Userland/DevTools/HackStudio/Git/GitWidget.cpp b/Userland/DevTools/HackStudio/Git/GitWidget.cpp new file mode 100644 index 0000000000..95ef6928ec --- /dev/null +++ b/Userland/DevTools/HackStudio/Git/GitWidget.cpp @@ -0,0 +1,188 @@ +/* + * Copyright (c) 2020, Itamar S. <itamar8910@gmail.com> + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * 1. Redistributions of source code must retain the above copyright notice, this + * list of conditions and the following disclaimer. + * + * 2. Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE + * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL + * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR + * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER + * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, + * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE + * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +#include "GitWidget.h" +#include "GitFilesModel.h" +#include <AK/LogStream.h> +#include <LibCore/File.h> +#include <LibDiff/Format.h> +#include <LibGUI/Application.h> +#include <LibGUI/BoxLayout.h> +#include <LibGUI/Button.h> +#include <LibGUI/InputBox.h> +#include <LibGUI/Label.h> +#include <LibGUI/MessageBox.h> +#include <LibGUI/Model.h> +#include <LibGUI/Painter.h> +#include <LibGfx/Bitmap.h> +#include <stdio.h> + +namespace HackStudio { + +GitWidget::GitWidget(const LexicalPath& repo_root) + : m_repo_root(repo_root) +{ + set_layout<GUI::HorizontalBoxLayout>(); + + auto& unstaged = add<GUI::Widget>(); + unstaged.set_layout<GUI::VerticalBoxLayout>(); + auto& unstaged_header = unstaged.add<GUI::Widget>(); + unstaged_header.set_layout<GUI::HorizontalBoxLayout>(); + + auto& refresh_button = unstaged_header.add<GUI::Button>(); + refresh_button.set_icon(Gfx::Bitmap::load_from_file("/res/icons/16x16/reload.png")); + refresh_button.set_fixed_size(16, 16); + refresh_button.set_tooltip("refresh"); + refresh_button.on_click = [this](int) { refresh(); }; + + auto& unstaged_label = unstaged_header.add<GUI::Label>(); + unstaged_label.set_text("Unstaged"); + + unstaged_header.set_fixed_height(20); + m_unstaged_files = unstaged.add<GitFilesView>( + [this](const auto& file) { stage_file(file); }, + Gfx::Bitmap::load_from_file("/res/icons/16x16/plus.png").release_nonnull()); + m_unstaged_files->on_selection = [this](const GUI::ModelIndex& index) { + const auto& selected = index.data().as_string(); + show_diff(LexicalPath(selected)); + }; + + auto& staged = add<GUI::Widget>(); + staged.set_layout<GUI::VerticalBoxLayout>(); + + auto& staged_header = staged.add<GUI::Widget>(); + staged_header.set_layout<GUI::HorizontalBoxLayout>(); + + auto& commit_button = staged_header.add<GUI::Button>(); + commit_button.set_icon(Gfx::Bitmap::load_from_file("/res/icons/16x16/commit.png")); + commit_button.set_fixed_size(16, 16); + commit_button.set_tooltip("commit"); + commit_button.on_click = [this](int) { commit(); }; + + auto& staged_label = staged_header.add<GUI::Label>(); + staged_label.set_text("Staged"); + + staged_header.set_fixed_height(20); + m_staged_files = staged.add<GitFilesView>( + [this](const auto& file) { unstage_file(file); }, + Gfx::Bitmap::load_from_file("/res/icons/16x16/minus.png").release_nonnull()); +} + +bool GitWidget::initialize() +{ + auto result = GitRepo::try_to_create(m_repo_root); + switch (result.type) { + case GitRepo::CreateResult::Type::Success: + m_git_repo = result.repo; + return true; + case GitRepo::CreateResult::Type::GitProgramNotFound: + GUI::MessageBox::show(window(), "Please install the Git port", "Error", GUI::MessageBox::Type::Error); + return false; + case GitRepo::CreateResult::Type::NoGitRepo: { + auto decision = GUI::MessageBox::show(window(), "Create git repository?", "Git", GUI::MessageBox::Type::Question, GUI::MessageBox::InputType::YesNo); + if (decision != GUI::Dialog::ExecResult::ExecYes) + return false; + m_git_repo = GitRepo::initialize_repository(m_repo_root); + return true; + } + default: + ASSERT_NOT_REACHED(); + } +} + +bool GitWidget::initialize_if_needed() +{ + if (initialized()) + return true; + + return initialize(); +} + +void GitWidget::refresh() +{ + if (!initialize_if_needed()) { + dbgln("GitWidget initialization failed"); + return; + } + + ASSERT(!m_git_repo.is_null()); + + m_unstaged_files->set_model(GitFilesModel::create(m_git_repo->unstaged_files())); + m_staged_files->set_model(GitFilesModel::create(m_git_repo->staged_files())); +} + +void GitWidget::stage_file(const LexicalPath& file) +{ + dbgln("staging: {}", file); + bool rc = m_git_repo->stage(file); + ASSERT(rc); + refresh(); +} + +void GitWidget::unstage_file(const LexicalPath& file) +{ + dbgln("unstaging: {}", file); + bool rc = m_git_repo->unstage(file); + ASSERT(rc); + refresh(); +} + +void GitWidget::commit() +{ + String message; + auto res = GUI::InputBox::show(message, window(), "Commit message:", "Commit"); + if (res != GUI::InputBox::ExecOK || message.is_empty()) + return; + dbgln("commit message: {}", message); + m_git_repo->commit(message); + refresh(); +} + +void GitWidget::set_view_diff_callback(ViewDiffCallback callback) +{ + m_view_diff_callback = move(callback); +} + +void GitWidget::show_diff(const LexicalPath& file_path) +{ + if (!m_git_repo->is_tracked(file_path)) { + auto file = Core::File::construct(file_path.string()); + if (!file->open(Core::IODevice::ReadOnly)) { + perror("open"); + ASSERT_NOT_REACHED(); + } + + auto content = file->read_all(); + String content_string((char*)content.data(), content.size()); + m_view_diff_callback("", Diff::generate_only_additions(content_string)); + return; + } + const auto& original_content = m_git_repo->original_file_content(file_path); + const auto& diff = m_git_repo->unstaged_diff(file_path); + ASSERT(original_content.has_value() && diff.has_value()); + m_view_diff_callback(original_content.value(), diff.value()); +} +} diff --git a/Userland/DevTools/HackStudio/Git/GitWidget.h b/Userland/DevTools/HackStudio/Git/GitWidget.h new file mode 100644 index 0000000000..a17e9f9fd4 --- /dev/null +++ b/Userland/DevTools/HackStudio/Git/GitWidget.h @@ -0,0 +1,65 @@ +/* + * Copyright (c) 2020, Itamar S. <itamar8910@gmail.com> + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * 1. Redistributions of source code must retain the above copyright notice, this + * list of conditions and the following disclaimer. + * + * 2. Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE + * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL + * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR + * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER + * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, + * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE + * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +#pragma once + +#include "GitFilesView.h" +#include "GitRepo.h" +#include <AK/Function.h> +#include <LibGUI/Forward.h> +#include <LibGUI/Widget.h> + +namespace HackStudio { + +typedef Function<void(const String& original_content, const String& diff)> ViewDiffCallback; + +class GitWidget final : public GUI::Widget { + C_OBJECT(GitWidget) +public: + virtual ~GitWidget() override { } + + void refresh(); + void set_view_diff_callback(ViewDiffCallback callback); + bool initialized() const { return !m_git_repo.is_null(); }; + +private: + explicit GitWidget(const LexicalPath& repo_root); + + bool initialize(); + bool initialize_if_needed(); + void stage_file(const LexicalPath&); + void unstage_file(const LexicalPath&); + void commit(); + void show_diff(const LexicalPath&); + + LexicalPath m_repo_root; + RefPtr<GitFilesView> m_unstaged_files; + RefPtr<GitFilesView> m_staged_files; + RefPtr<GitRepo> m_git_repo; + ViewDiffCallback m_view_diff_callback; +}; + +} diff --git a/Userland/DevTools/HackStudio/HackStudio.h b/Userland/DevTools/HackStudio/HackStudio.h new file mode 100644 index 0000000000..85247453d8 --- /dev/null +++ b/Userland/DevTools/HackStudio/HackStudio.h @@ -0,0 +1,45 @@ +/* + * Copyright (c) 2020, the SerenityOS developers. + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * 1. Redistributions of source code must retain the above copyright notice, this + * list of conditions and the following disclaimer. + * + * 2. Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE + * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL + * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR + * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER + * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, + * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE + * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +#pragma once + +#include "EditorWrapper.h" +#include "LanguageClients/ServerConnections.h" +#include "Project.h" +#include <AK/String.h> +#include <LibGUI/TextEditor.h> + +namespace HackStudio { + +GUI::TextEditor& current_editor(); +void open_file(const String&); +RefPtr<EditorWrapper> current_editor_wrapper(); +void open_file(const String&); +Project& project(); +String currently_open_file(); +void set_current_editor_wrapper(RefPtr<EditorWrapper>); + +} diff --git a/Userland/DevTools/HackStudio/HackStudioWidget.cpp b/Userland/DevTools/HackStudio/HackStudioWidget.cpp new file mode 100644 index 0000000000..7ef0eb33d2 --- /dev/null +++ b/Userland/DevTools/HackStudio/HackStudioWidget.cpp @@ -0,0 +1,945 @@ +/* + * Copyright (c) 2018-2020, Andreas Kling <kling@serenityos.org> + * Copyright (c) 2020, Itamar S. <itamar8910@gmail.com> + * Copyright (c) 2020, the SerenityOS developers + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * 1. Redistributions of source code must retain the above copyright notice, this + * list of conditions and the following disclaimer. + * + * 2. Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE + * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL + * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR + * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER + * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, + * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE + * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +#include "HackStudioWidget.h" +#include "CursorTool.h" +#include "Debugger/DebugInfoWidget.h" +#include "Debugger/Debugger.h" +#include "Debugger/DisassemblyWidget.h" +#include "Editor.h" +#include "EditorWrapper.h" +#include "FindInFilesWidget.h" +#include "FormEditorWidget.h" +#include "FormWidget.h" +#include "Git/DiffViewer.h" +#include "Git/GitWidget.h" +#include "HackStudio.h" +#include "HackStudioWidget.h" +#include "Locator.h" +#include "Project.h" +#include "TerminalWrapper.h" +#include "WidgetTool.h" +#include "WidgetTreeModel.h" +#include <AK/StringBuilder.h> +#include <LibCore/ArgsParser.h> +#include <LibCore/Event.h> +#include <LibCore/EventLoop.h> +#include <LibCore/File.h> +#include <LibDebug/DebugSession.h> +#include <LibGUI/Action.h> +#include <LibGUI/ActionGroup.h> +#include <LibGUI/Application.h> +#include <LibGUI/BoxLayout.h> +#include <LibGUI/Button.h> +#include <LibGUI/EditingEngine.h> +#include <LibGUI/FilePicker.h> +#include <LibGUI/InputBox.h> +#include <LibGUI/ItemListModel.h> +#include <LibGUI/Label.h> +#include <LibGUI/Menu.h> +#include <LibGUI/MenuBar.h> +#include <LibGUI/MessageBox.h> +#include <LibGUI/RegularEditingEngine.h> +#include <LibGUI/Splitter.h> +#include <LibGUI/StackWidget.h> +#include <LibGUI/TabWidget.h> +#include <LibGUI/TableView.h> +#include <LibGUI/TextBox.h> +#include <LibGUI/TextEditor.h> +#include <LibGUI/ToolBar.h> +#include <LibGUI/ToolBarContainer.h> +#include <LibGUI/TreeView.h> +#include <LibGUI/VimEditingEngine.h> +#include <LibGUI/Widget.h> +#include <LibGUI/Window.h> +#include <LibGfx/FontDatabase.h> +#include <LibThread/Lock.h> +#include <LibThread/Thread.h> +#include <LibVT/TerminalWidget.h> +#include <fcntl.h> +#include <spawn.h> +#include <stdio.h> +#include <sys/types.h> +#include <sys/wait.h> +#include <unistd.h> + +namespace HackStudio { + +HackStudioWidget::HackStudioWidget(const String& path_to_project) +{ + set_fill_with_background_color(true); + set_layout<GUI::VerticalBoxLayout>(); + layout()->set_spacing(2); + + open_project(path_to_project); + + auto& toolbar_container = add<GUI::ToolBarContainer>(); + + auto& outer_splitter = add<GUI::HorizontalSplitter>(); + + auto& left_hand_splitter = outer_splitter.add<GUI::VerticalSplitter>(); + left_hand_splitter.set_fixed_width(150); + create_project_tree_view(left_hand_splitter); + m_project_tree_view_context_menu = create_project_tree_view_context_menu(); + + create_open_files_view(left_hand_splitter); + + m_right_hand_splitter = outer_splitter.add<GUI::VerticalSplitter>(); + m_right_hand_stack = m_right_hand_splitter->add<GUI::StackWidget>(); + + // Put a placeholder widget front & center since we don't have a file open yet. + m_right_hand_stack->add<GUI::Widget>(); + + create_form_editor(*m_right_hand_stack); + + m_diff_viewer = m_right_hand_stack->add<DiffViewer>(); + + m_editors_splitter = m_right_hand_stack->add<GUI::VerticalSplitter>(); + m_editors_splitter->layout()->set_margins({ 0, 3, 0, 0 }); + add_new_editor(*m_editors_splitter); + + m_switch_to_next_editor = create_switch_to_next_editor_action(); + m_switch_to_previous_editor = create_switch_to_previous_editor_action(); + + m_remove_current_editor_action = create_remove_current_editor_action(); + m_open_action = create_open_action(); + m_save_action = create_save_action(); + + create_action_tab(*m_right_hand_splitter); + + m_add_editor_action = create_add_editor_action(); + m_add_terminal_action = create_add_terminal_action(); + m_remove_current_terminal_action = create_remove_current_terminal_action(); + + m_locator = add<Locator>(); + + m_terminal_wrapper->on_command_exit = [this] { + m_stop_action->set_enabled(false); + }; + + m_build_action = create_build_action(); + m_run_action = create_run_action(); + m_stop_action = create_stop_action(); + m_debug_action = create_debug_action(); + + initialize_debugger(); + + create_toolbar(toolbar_container); +} + +void HackStudioWidget::update_actions() +{ + auto is_remove_terminal_enabled = [this]() { + auto widget = m_action_tab_widget->active_widget(); + if (!widget) + return false; + if (StringView { "TerminalWrapper" } != widget->class_name()) + return false; + if (!reinterpret_cast<TerminalWrapper*>(widget)->user_spawned()) + return false; + return true; + }; + + m_remove_current_editor_action->set_enabled(m_all_editor_wrappers.size() > 1); + m_remove_current_terminal_action->set_enabled(is_remove_terminal_enabled()); +} + +void HackStudioWidget::on_action_tab_change() +{ + update_actions(); + auto git_widget = m_action_tab_widget->active_widget(); + if (!git_widget) + return; + if (StringView { "GitWidget" } != git_widget->class_name()) + return; + reinterpret_cast<GitWidget*>(git_widget)->refresh(); +} + +void HackStudioWidget::open_project(const String& root_path) +{ + if (chdir(root_path.characters()) < 0) { + perror("chdir"); + exit(1); + } + m_project = Project::open_with_root_path(root_path); + ASSERT(m_project); + if (m_project_tree_view) { + m_project_tree_view->set_model(m_project->model()); + m_project_tree_view->toggle_index(m_project_tree_view->model()->index(0, 0)); + m_project_tree_view->update(); + } + if (Debugger::is_initialized()) { + Debugger::the().reset_breakpoints(); + } +} + +Vector<String> HackStudioWidget::selected_file_names() const +{ + Vector<String> files; + m_project_tree_view->selection().for_each_index([&](const GUI::ModelIndex& index) { + files.append(index.data().as_string()); + }); + return files; +} + +void HackStudioWidget::open_file(const String& filename) +{ + if (!currently_open_file().is_empty()) { + // Since the file is previously open, it should always be in m_open_files. + ASSERT(m_open_files.find(currently_open_file()) != m_open_files.end()); + auto previous_open_project_file = m_open_files.get(currently_open_file()).value(); + + // Update the scrollbar values of the previous_open_project_file and save them to m_open_files. + previous_open_project_file->vertical_scroll_value(current_editor().vertical_scrollbar().value()); + previous_open_project_file->horizontal_scroll_value(current_editor().horizontal_scrollbar().value()); + m_open_files.set(currently_open_file(), previous_open_project_file); + } + + RefPtr<ProjectFile> new_project_file = nullptr; + if (auto it = m_open_files.find(filename); it != m_open_files.end()) { + new_project_file = it->value; + } else { + new_project_file = m_project->get_file(filename); + if (!new_project_file) { + new_project_file = ProjectFile::construct_with_name(filename); + } + m_open_files.set(filename, *new_project_file); + m_open_files_vector.append(filename); + m_open_files_view->model()->update(); + } + + current_editor().set_document(const_cast<GUI::TextDocument&>(new_project_file->document())); + current_editor().set_mode(GUI::TextEditor::Editable); + current_editor().horizontal_scrollbar().set_value(new_project_file->horizontal_scroll_value()); + current_editor().vertical_scrollbar().set_value(new_project_file->vertical_scroll_value()); + current_editor().set_editing_engine(make<GUI::RegularEditingEngine>()); + + if (filename.ends_with(".frm")) { + set_edit_mode(EditMode::Form); + } else { + set_edit_mode(EditMode::Text); + } + + m_currently_open_file = filename; + + String relative_file_path = m_currently_open_file; + if (m_currently_open_file.starts_with(m_project->root_path())) + relative_file_path = m_currently_open_file.substring(m_project->root_path().length() + 1); + + window()->set_title(String::formatted("{} - {} - Hack Studio", relative_file_path, m_project->name())); + m_project_tree_view->update(); + + current_editor_wrapper().filename_label().set_text(filename); + + current_editor().set_focus(true); +} + +EditorWrapper& HackStudioWidget::current_editor_wrapper() +{ + ASSERT(m_current_editor_wrapper); + return *m_current_editor_wrapper; +} + +GUI::TextEditor& HackStudioWidget::current_editor() +{ + return current_editor_wrapper().editor(); +} + +void HackStudioWidget::set_edit_mode(EditMode mode) +{ + if (mode == EditMode::Text) { + m_right_hand_stack->set_active_widget(m_editors_splitter); + } else if (mode == EditMode::Form) { + m_right_hand_stack->set_active_widget(m_form_inner_container); + } else if (mode == EditMode::Diff) { + m_right_hand_stack->set_active_widget(m_diff_viewer); + } else { + ASSERT_NOT_REACHED(); + } + m_right_hand_stack->active_widget()->update(); +} + +NonnullRefPtr<GUI::Menu> HackStudioWidget::create_project_tree_view_context_menu() +{ + m_open_selected_action = create_open_selected_action(); + m_new_action = create_new_action(); + m_delete_action = create_delete_action(); + auto project_tree_view_context_menu = GUI::Menu::construct("Project Files"); + project_tree_view_context_menu->add_action(*m_open_selected_action); + // TODO: Rename, cut, copy, duplicate with new name, show containing folder ... + project_tree_view_context_menu->add_separator(); + project_tree_view_context_menu->add_action(*m_new_action); + project_tree_view_context_menu->add_action(*m_delete_action); + return project_tree_view_context_menu; +} + +NonnullRefPtr<GUI::Action> HackStudioWidget::create_new_action() +{ + return GUI::Action::create("Add new file to project...", { Mod_Ctrl, Key_N }, Gfx::Bitmap::load_from_file("/res/icons/16x16/new.png"), [this](const GUI::Action&) { + String filename; + if (GUI::InputBox::show(filename, window(), "Enter name of new file:", "Add new file to project") != GUI::InputBox::ExecOK) + return; + auto file = Core::File::construct(filename); + if (!file->open((Core::IODevice::OpenMode)(Core::IODevice::WriteOnly | Core::IODevice::MustBeNew))) { + GUI::MessageBox::show(window(), String::formatted("Failed to create '{}'", filename), "Error", GUI::MessageBox::Type::Error); + return; + } + m_project_tree_view->toggle_index(m_project_tree_view->model()->index(0, 0)); + open_file(filename); + }); +} + +NonnullRefPtr<GUI::Action> HackStudioWidget::create_open_selected_action() +{ + + auto open_selected_action = GUI::Action::create("Open", [this](const GUI::Action&) { + auto files = selected_file_names(); + for (auto& file : files) + open_file(file); + }); + open_selected_action->set_enabled(true); + return open_selected_action; +} + +NonnullRefPtr<GUI::Action> HackStudioWidget::create_delete_action() +{ + auto delete_action = GUI::CommonActions::make_delete_action([this](const GUI::Action&) { + auto files = selected_file_names(); + if (files.is_empty()) + return; + + String message; + if (files.size() == 1) { + message = String::formatted("Really remove {} from disk?", LexicalPath(files[0]).basename()); + } else { + message = String::formatted("Really remove {} files from disk?", files.size()); + } + + auto result = GUI::MessageBox::show(window(), + message, + "Confirm deletion", + GUI::MessageBox::Type::Warning, + GUI::MessageBox::InputType::OKCancel); + if (result == GUI::MessageBox::ExecCancel) + return; + + for (auto& file : files) { + if (1) { + // FIXME: Remove `file` from disk + } else { + GUI::MessageBox::show(window(), + String::formatted("Removing file {} from the project failed.", file), + "Removal failed", + GUI::MessageBox::Type::Error); + break; + } + } + }); + delete_action->set_enabled(false); + return delete_action; +} + +void HackStudioWidget::add_new_editor(GUI::Widget& parent) +{ + auto wrapper = EditorWrapper::construct(); + if (m_action_tab_widget) { + parent.insert_child_before(wrapper, *m_action_tab_widget); + } else { + parent.add_child(wrapper); + } + m_current_editor_wrapper = wrapper; + m_all_editor_wrappers.append(wrapper); + wrapper->editor().set_focus(true); +} + +NonnullRefPtr<GUI::Action> HackStudioWidget::create_switch_to_next_editor_action() +{ + return GUI::Action::create("Switch to next editor", { Mod_Ctrl, Key_E }, [this](auto&) { + if (m_all_editor_wrappers.size() <= 1) + return; + Vector<EditorWrapper*> wrappers; + m_editors_splitter->for_each_child_of_type<EditorWrapper>([this, &wrappers](auto& child) { + wrappers.append(&child); + return IterationDecision::Continue; + }); + for (size_t i = 0; i < wrappers.size(); ++i) { + if (m_current_editor_wrapper.ptr() == wrappers[i]) { + if (i == wrappers.size() - 1) + wrappers[0]->editor().set_focus(true); + else + wrappers[i + 1]->editor().set_focus(true); + } + } + }); +} + +NonnullRefPtr<GUI::Action> HackStudioWidget::create_switch_to_previous_editor_action() +{ + return GUI::Action::create("Switch to previous editor", { Mod_Ctrl | Mod_Shift, Key_E }, [this](auto&) { + if (m_all_editor_wrappers.size() <= 1) + return; + Vector<EditorWrapper*> wrappers; + m_editors_splitter->for_each_child_of_type<EditorWrapper>([this, &wrappers](auto& child) { + wrappers.append(&child); + return IterationDecision::Continue; + }); + for (int i = wrappers.size() - 1; i >= 0; --i) { + if (m_current_editor_wrapper.ptr() == wrappers[i]) { + if (i == 0) + wrappers.last()->editor().set_focus(true); + else + wrappers[i - 1]->editor().set_focus(true); + } + } + }); +} + +NonnullRefPtr<GUI::Action> HackStudioWidget::create_remove_current_editor_action() +{ + return GUI::Action::create("Remove current editor", { Mod_Alt | Mod_Shift, Key_E }, [this](auto&) { + if (m_all_editor_wrappers.size() <= 1) + return; + auto wrapper = m_current_editor_wrapper; + m_switch_to_next_editor->activate(); + m_editors_splitter->remove_child(*wrapper); + m_all_editor_wrappers.remove_first_matching([&wrapper](auto& entry) { return entry == wrapper.ptr(); }); + update_actions(); + }); +} + +NonnullRefPtr<GUI::Action> HackStudioWidget::create_open_action() +{ + return GUI::Action::create("Open project...", { Mod_Ctrl | Mod_Shift, Key_O }, Gfx::Bitmap::load_from_file("/res/icons/16x16/open.png"), [this](auto&) { + auto open_path = GUI::FilePicker::get_open_filepath(window(), "Open project"); + if (!open_path.has_value()) + return; + open_project(open_path.value()); + update_actions(); + }); +} + +NonnullRefPtr<GUI::Action> HackStudioWidget::create_save_action() +{ + return GUI::Action::create("Save", { Mod_Ctrl, Key_S }, Gfx::Bitmap::load_from_file("/res/icons/16x16/save.png"), [this](auto&) { + if (m_currently_open_file.is_empty()) + return; + + current_editor().write_to_file(m_currently_open_file); + + if (m_git_widget->initialized()) + m_git_widget->refresh(); + }); +} + +NonnullRefPtr<GUI::Action> HackStudioWidget::create_remove_current_terminal_action() +{ + return GUI::Action::create("Remove current Terminal", { Mod_Alt | Mod_Shift, Key_T }, [this](auto&) { + auto widget = m_action_tab_widget->active_widget(); + if (!widget) + return; + if (!is<TerminalWrapper>(widget)) + return; + auto& terminal = *static_cast<TerminalWrapper*>(widget); + if (!terminal.user_spawned()) + return; + m_action_tab_widget->remove_tab(terminal); + update_actions(); + }); +} + +NonnullRefPtr<GUI::Action> HackStudioWidget::create_add_editor_action() +{ + return GUI::Action::create("Add new editor", { Mod_Ctrl | Mod_Alt, Key_E }, + Gfx::Bitmap::load_from_file("/res/icons/16x16/app-text-editor.png"), + [this](auto&) { + add_new_editor(*m_editors_splitter); + update_actions(); + }); +} + +NonnullRefPtr<GUI::Action> HackStudioWidget::create_add_terminal_action() +{ + return GUI::Action::create("Add new Terminal", { Mod_Ctrl | Mod_Alt, Key_T }, + Gfx::Bitmap::load_from_file("/res/icons/16x16/app-terminal.png"), + [this](auto&) { + auto& terminal_wrapper = m_action_tab_widget->add_tab<TerminalWrapper>("Terminal"); + reveal_action_tab(terminal_wrapper); + update_actions(); + terminal_wrapper.terminal().set_focus(true); + }); +} + +void HackStudioWidget::reveal_action_tab(GUI::Widget& widget) +{ + if (m_action_tab_widget->min_height() < 200) + m_action_tab_widget->set_fixed_height(200); + m_action_tab_widget->set_active_widget(&widget); +} + +NonnullRefPtr<GUI::Action> HackStudioWidget::create_debug_action() +{ + return GUI::Action::create("Debug", Gfx::Bitmap::load_from_file("/res/icons/16x16/debug-run.png"), [this](auto&) { + if (!GUI::FilePicker::file_exists(get_project_executable_path())) { + GUI::MessageBox::show(window(), String::formatted("Could not find file: {}. (did you build the project?)", get_project_executable_path()), "Error", GUI::MessageBox::Type::Error); + return; + } + if (Debugger::the().session()) { + GUI::MessageBox::show(window(), "Debugger is already running", "Error", GUI::MessageBox::Type::Error); + return; + } + + Debugger::the().set_executable_path(get_project_executable_path()); + m_debugger_thread = LibThread::Thread::construct(Debugger::start_static); + m_debugger_thread->start(); + }); +} + +void HackStudioWidget::initialize_debugger() +{ + Debugger::initialize( + m_project->root_path(), + [this](const PtraceRegisters& regs) { + ASSERT(Debugger::the().session()); + const auto& debug_session = *Debugger::the().session(); + auto source_position = debug_session.get_source_position(regs.eip); + if (!source_position.has_value()) { + dbgln("Could not find source position for address: {:p}", regs.eip); + return Debugger::HasControlPassedToUser::No; + } + dbgln("Debugger stopped at source position: {}:{}", source_position.value().file_path, source_position.value().line_number); + + Core::EventLoop::main().post_event( + *window(), + make<Core::DeferredInvocationEvent>( + [this, source_position, ®s](auto&) { + m_current_editor_in_execution = get_editor_of_file(source_position.value().file_path); + m_current_editor_in_execution->editor().set_execution_position(source_position.value().line_number - 1); + m_debug_info_widget->update_state(*Debugger::the().session(), regs); + m_debug_info_widget->set_debug_actions_enabled(true); + m_disassembly_widget->update_state(*Debugger::the().session(), regs); + HackStudioWidget::reveal_action_tab(*m_debug_info_widget); + })); + Core::EventLoop::wake(); + + return Debugger::HasControlPassedToUser::Yes; + }, + [this]() { + Core::EventLoop::main().post_event(*window(), make<Core::DeferredInvocationEvent>([this](auto&) { + m_debug_info_widget->set_debug_actions_enabled(false); + if (m_current_editor_in_execution) { + m_current_editor_in_execution->editor().clear_execution_position(); + } + })); + Core::EventLoop::wake(); + }, + [this]() { + Core::EventLoop::main().post_event(*window(), make<Core::DeferredInvocationEvent>([this](auto&) { + m_debug_info_widget->program_stopped(); + m_disassembly_widget->program_stopped(); + HackStudioWidget::hide_action_tabs(); + GUI::MessageBox::show(window(), "Program Exited", "Debugger", GUI::MessageBox::Type::Information); + })); + Core::EventLoop::wake(); + }); +} + +String HackStudioWidget::get_full_path_of_serenity_source(const String& file) +{ + auto path_parts = LexicalPath(file).parts(); + ASSERT(path_parts[0] == ".."); + path_parts.remove(0); + StringBuilder relative_path_builder; + relative_path_builder.join("/", path_parts); + constexpr char SERENITY_LIBS_PREFIX[] = "/usr/src/serenity"; + LexicalPath serenity_sources_base(SERENITY_LIBS_PREFIX); + return String::formatted("{}/{}", serenity_sources_base, relative_path_builder.to_string()); +} + +NonnullRefPtr<EditorWrapper> HackStudioWidget::get_editor_of_file(const String& file_name) +{ + + String file_path = file_name; + + // TODO: We can probably do a more specific condition here, something like + // "if (file.starts_with("../Libraries/") || file.starts_with("../AK/"))" + if (file_name.starts_with("../")) { + file_path = get_full_path_of_serenity_source(file_name); + } + + open_file(file_path); + return current_editor_wrapper(); +} + +String HackStudioWidget::get_project_executable_path() const +{ + // FIXME: Dumb heuristic ahead! + // e.g /my/project => /my/project/project + // TODO: Perhaps a Makefile rule for getting the value of $(PROGRAM) would be better? + return String::formatted("{}/{}", m_project->root_path(), LexicalPath(m_project->root_path()).basename()); +} + +void HackStudioWidget::build(TerminalWrapper& wrapper) +{ + if (m_currently_open_file.ends_with(".js")) + wrapper.run_command(String::formatted("js -A {}", m_currently_open_file)); + else + wrapper.run_command("make"); +} + +void HackStudioWidget::run(TerminalWrapper& wrapper) +{ + if (m_currently_open_file.ends_with(".js")) + wrapper.run_command(String::formatted("js {}", m_currently_open_file)); + else + wrapper.run_command("make run"); +} + +void HackStudioWidget::hide_action_tabs() +{ + m_action_tab_widget->set_fixed_height(24); +}; + +Project& HackStudioWidget::project() +{ + return *m_project; +} + +void HackStudioWidget::set_current_editor_wrapper(RefPtr<EditorWrapper> editor_wrapper) +{ + m_current_editor_wrapper = editor_wrapper; +} + +void HackStudioWidget::create_project_tree_view(GUI::Widget& parent) +{ + m_project_tree_view = parent.add<GUI::TreeView>(); + m_project_tree_view->set_model(m_project->model()); + + for (int column_index = 0; column_index < m_project->model().column_count(); ++column_index) + m_project_tree_view->set_column_hidden(column_index, true); + + m_project_tree_view->set_column_hidden(GUI::FileSystemModel::Column::Name, false); + + m_project_tree_view->on_context_menu_request = [this](const GUI::ModelIndex& index, const GUI::ContextMenuEvent& event) { + if (index.is_valid()) { + m_project_tree_view_context_menu->popup(event.screen_position(), m_open_selected_action); + } + }; + + m_project_tree_view->on_selection_change = [this] { + m_open_selected_action->set_enabled(!m_project_tree_view->selection().is_empty()); + m_delete_action->set_enabled(!m_project_tree_view->selection().is_empty()); + }; + + m_project_tree_view->on_activation = [this](auto& index) { + auto filename = index.data(GUI::ModelRole::Custom).to_string(); + open_file(filename); + }; +} + +void HackStudioWidget::create_open_files_view(GUI::Widget& parent) +{ + m_open_files_view = parent.add<GUI::ListView>(); + auto open_files_model = GUI::ItemListModel<String>::create(m_open_files_vector); + m_open_files_view->set_model(open_files_model); + + m_open_files_view->on_activation = [this](auto& index) { + open_file(index.data().to_string()); + }; +} + +void HackStudioWidget::create_form_editor(GUI::Widget& parent) +{ + m_form_inner_container = parent.add<GUI::Widget>(); + m_form_inner_container->set_layout<GUI::HorizontalBoxLayout>(); + auto& form_widgets_toolbar = m_form_inner_container->add<GUI::ToolBar>(Orientation::Vertical, 26); + form_widgets_toolbar.set_fixed_width(38); + + GUI::ActionGroup tool_actions; + tool_actions.set_exclusive(true); + + auto cursor_tool_action = GUI::Action::create_checkable("Cursor", Gfx::Bitmap::load_from_file("/res/icons/hackstudio/Cursor.png"), [this](auto&) { + m_form_editor_widget->set_tool(make<CursorTool>(*m_form_editor_widget)); + }); + cursor_tool_action->set_checked(true); + tool_actions.add_action(cursor_tool_action); + + form_widgets_toolbar.add_action(cursor_tool_action); + + GUI::WidgetClassRegistration::for_each([&, this](const GUI::WidgetClassRegistration& reg) { + constexpr size_t gui_namespace_prefix_length = sizeof("GUI::") - 1; + auto icon_path = String::formatted("/res/icons/hackstudio/G{}.png", + reg.class_name().substring(gui_namespace_prefix_length, reg.class_name().length() - gui_namespace_prefix_length)); + if (!Core::File::exists(icon_path)) + return; + + auto action = GUI::Action::create_checkable(reg.class_name(), Gfx::Bitmap::load_from_file(icon_path), [®, this](auto&) { + m_form_editor_widget->set_tool(make<WidgetTool>(*m_form_editor_widget, reg)); + auto widget = reg.construct(); + m_form_editor_widget->form_widget().add_child(widget); + widget->set_relative_rect(30, 30, 30, 30); + m_form_editor_widget->model().update(); + }); + action->set_checked(false); + tool_actions.add_action(action); + form_widgets_toolbar.add_action(move(action)); + }); + + auto& form_editor_inner_splitter = m_form_inner_container->add<GUI::HorizontalSplitter>(); + + m_form_editor_widget = form_editor_inner_splitter.add<FormEditorWidget>(); + + auto& form_editing_pane_container = form_editor_inner_splitter.add<GUI::VerticalSplitter>(); + form_editing_pane_container.set_fixed_width(190); + form_editing_pane_container.set_layout<GUI::VerticalBoxLayout>(); + + auto add_properties_pane = [&](auto& text, auto& pane_widget) { + auto& wrapper = form_editing_pane_container.add<GUI::Widget>(); + wrapper.set_layout<GUI::VerticalBoxLayout>(); + auto& label = wrapper.add<GUI::Label>(text); + label.set_fill_with_background_color(true); + label.set_text_alignment(Gfx::TextAlignment::CenterLeft); + label.set_font(Gfx::FontDatabase::default_bold_font()); + label.set_fixed_height(16); + wrapper.add_child(pane_widget); + }; + + m_form_widget_tree_view = GUI::TreeView::construct(); + m_form_widget_tree_view->set_model(m_form_editor_widget->model()); + m_form_widget_tree_view->on_selection_change = [this] { + m_form_editor_widget->selection().disable_hooks(); + m_form_editor_widget->selection().clear(); + m_form_widget_tree_view->selection().for_each_index([this](auto& index) { + // NOTE: Make sure we don't add the FormWidget itself to the selection, + // since that would allow you to drag-move the FormWidget. + if (index.internal_data() != &m_form_editor_widget->form_widget()) + m_form_editor_widget->selection().add(*(GUI::Widget*)index.internal_data()); + }); + m_form_editor_widget->update(); + m_form_editor_widget->selection().enable_hooks(); + }; + + m_form_editor_widget->selection().on_add = [this](auto& widget) { + m_form_widget_tree_view->selection().add(m_form_editor_widget->model().index_for_widget(widget)); + }; + m_form_editor_widget->selection().on_remove = [this](auto& widget) { + m_form_widget_tree_view->selection().remove(m_form_editor_widget->model().index_for_widget(widget)); + }; + m_form_editor_widget->selection().on_clear = [this] { + m_form_widget_tree_view->selection().clear(); + }; + + add_properties_pane("Form widget tree:", *m_form_widget_tree_view); + add_properties_pane("Widget properties:", *GUI::TableView::construct()); +} + +void HackStudioWidget::create_toolbar(GUI::Widget& parent) +{ + auto& toolbar = parent.add<GUI::ToolBar>(); + toolbar.add_action(*m_new_action); + toolbar.add_action(*m_save_action); + toolbar.add_action(*m_delete_action); + toolbar.add_separator(); + + toolbar.add_action(GUI::CommonActions::make_cut_action([this](auto&) { current_editor().cut_action().activate(); })); + toolbar.add_action(GUI::CommonActions::make_copy_action([this](auto&) { current_editor().copy_action().activate(); })); + toolbar.add_action(GUI::CommonActions::make_paste_action([this](auto&) { current_editor().paste_action().activate(); })); + toolbar.add_separator(); + toolbar.add_action(GUI::CommonActions::make_undo_action([this](auto&) { current_editor().undo_action().activate(); })); + toolbar.add_action(GUI::CommonActions::make_redo_action([this](auto&) { current_editor().redo_action().activate(); })); + toolbar.add_separator(); + + toolbar.add_action(*m_build_action); + toolbar.add_separator(); + + toolbar.add_action(*m_run_action); + toolbar.add_action(*m_stop_action); + toolbar.add_separator(); + + toolbar.add_action(*m_debug_action); +} + +NonnullRefPtr<GUI::Action> HackStudioWidget::create_build_action() +{ + return GUI::Action::create("Build", { Mod_Ctrl, Key_B }, Gfx::Bitmap::load_from_file("/res/icons/16x16/build.png"), [this](auto&) { + reveal_action_tab(*m_terminal_wrapper); + build(*m_terminal_wrapper); + m_stop_action->set_enabled(true); + }); +} + +NonnullRefPtr<GUI::Action> HackStudioWidget::create_run_action() +{ + return GUI::Action::create("Run", { Mod_Ctrl, Key_R }, Gfx::Bitmap::load_from_file("/res/icons/16x16/program-run.png"), [this](auto&) { + reveal_action_tab(*m_terminal_wrapper); + run(*m_terminal_wrapper); + m_stop_action->set_enabled(true); + }); +} + +void HackStudioWidget::create_action_tab(GUI::Widget& parent) +{ + m_action_tab_widget = parent.add<GUI::TabWidget>(); + + m_action_tab_widget->set_fixed_height(24); + m_action_tab_widget->on_change = [this](auto&) { + on_action_tab_change(); + + static bool first_time = true; + if (!first_time) + m_action_tab_widget->set_fixed_height(200); + first_time = false; + }; + + m_find_in_files_widget = m_action_tab_widget->add_tab<FindInFilesWidget>("Find in files"); + m_terminal_wrapper = m_action_tab_widget->add_tab<TerminalWrapper>("Build", false); + m_debug_info_widget = m_action_tab_widget->add_tab<DebugInfoWidget>("Debug"); + m_disassembly_widget = m_action_tab_widget->add_tab<DisassemblyWidget>("Disassembly"); + m_git_widget = m_action_tab_widget->add_tab<GitWidget>("Git", LexicalPath(m_project->root_path())); + m_git_widget->set_view_diff_callback([this](const auto& original_content, const auto& diff) { + m_diff_viewer->set_content(original_content, diff); + set_edit_mode(EditMode::Diff); + }); +} + +void HackStudioWidget::create_app_menubar(GUI::MenuBar& menubar) +{ + auto& app_menu = menubar.add_menu("Hack Studio"); + app_menu.add_action(*m_open_action); + app_menu.add_action(*m_save_action); + app_menu.add_separator(); + app_menu.add_action(GUI::CommonActions::make_quit_action([](auto&) { + GUI::Application::the()->quit(); + })); +} + +void HackStudioWidget::create_project_menubar(GUI::MenuBar& menubar) +{ + auto& project_menu = menubar.add_menu("Project"); + project_menu.add_action(*m_new_action); +} + +void HackStudioWidget::create_edit_menubar(GUI::MenuBar& menubar) +{ + auto& edit_menu = menubar.add_menu("Edit"); + edit_menu.add_action(GUI::Action::create("Find in files...", { Mod_Ctrl | Mod_Shift, Key_F }, Gfx::Bitmap::load_from_file("/res/icons/16x16/find.png"), [this](auto&) { + reveal_action_tab(*m_find_in_files_widget); + m_find_in_files_widget->focus_textbox_and_select_all(); + })); + + edit_menu.add_separator(); + + auto line_wrapping_action = GUI::Action::create_checkable("Line wrapping", [this](auto& action) { + for (auto& wrapper : m_all_editor_wrappers) { + wrapper.editor().set_line_wrapping_enabled(action.is_checked()); + } + }); + line_wrapping_action->set_checked(current_editor().is_line_wrapping_enabled()); + edit_menu.add_action(line_wrapping_action); + + edit_menu.add_separator(); + + auto vim_emulation_setting_action = GUI::Action::create_checkable("Vim emulation", { Mod_Ctrl | Mod_Shift | Mod_Alt, Key_V }, [this](auto& action) { + if (action.is_checked()) + current_editor().set_editing_engine(make<GUI::VimEditingEngine>()); + else + current_editor().set_editing_engine(make<GUI::RegularEditingEngine>()); + }); + vim_emulation_setting_action->set_checked(false); + edit_menu.add_action(vim_emulation_setting_action); +} + +void HackStudioWidget::create_build_menubar(GUI::MenuBar& menubar) +{ + auto& build_menu = menubar.add_menu("Build"); + build_menu.add_action(*m_build_action); + build_menu.add_separator(); + build_menu.add_action(*m_run_action); + build_menu.add_action(*m_stop_action); + build_menu.add_separator(); + build_menu.add_action(*m_debug_action); +} + +void HackStudioWidget::create_view_menubar(GUI::MenuBar& menubar) +{ + auto hide_action_tabs_action = GUI::Action::create("Hide action tabs", { Mod_Ctrl | Mod_Shift, Key_X }, [this](auto&) { + hide_action_tabs(); + }); + auto open_locator_action = GUI::Action::create("Open locator", { Mod_Ctrl, Key_K }, [this](auto&) { + m_locator->open(); + }); + + auto& view_menu = menubar.add_menu("View"); + view_menu.add_action(hide_action_tabs_action); + view_menu.add_action(open_locator_action); + view_menu.add_separator(); + view_menu.add_action(*m_add_editor_action); + view_menu.add_action(*m_remove_current_editor_action); + view_menu.add_action(*m_add_terminal_action); + view_menu.add_action(*m_remove_current_terminal_action); +} + +void HackStudioWidget::create_help_menubar(GUI::MenuBar& menubar) +{ + auto& help_menu = menubar.add_menu("Help"); + help_menu.add_action(GUI::CommonActions::make_about_action("Hack Studio", GUI::Icon::default_icon("app-hack-studio"), window())); +} + +NonnullRefPtr<GUI::Action> HackStudioWidget::create_stop_action() +{ + auto action = GUI::Action::create("Stop", Gfx::Bitmap::load_from_file("/res/icons/16x16/program-stop.png"), [this](auto&) { + m_terminal_wrapper->kill_running_command(); + }); + + action->set_enabled(false); + return action; +} + +void HackStudioWidget::initialize_menubar(GUI::MenuBar& menubar) +{ + create_app_menubar(menubar); + create_project_menubar(menubar); + create_edit_menubar(menubar); + create_build_menubar(menubar); + create_view_menubar(menubar); + create_help_menubar(menubar); +} + +HackStudioWidget::~HackStudioWidget() +{ + if (!m_debugger_thread.is_null()) { + Debugger::the().set_requested_debugger_action(Debugger::DebuggerAction::Exit); + dbgln("Waiting for debugger thread to terminate"); + auto rc = m_debugger_thread->join(); + if (rc.is_error()) { + warnln("pthread_join: {}", strerror(rc.error().value())); + dbgln("error joining debugger thread"); + } + } +} + +} diff --git a/Userland/DevTools/HackStudio/HackStudioWidget.h b/Userland/DevTools/HackStudio/HackStudioWidget.h new file mode 100644 index 0000000000..29ef3701e4 --- /dev/null +++ b/Userland/DevTools/HackStudio/HackStudioWidget.h @@ -0,0 +1,170 @@ +/* + * Copyright (c) 2018-2020, Andreas Kling <kling@serenityos.org> + * Copyright (c) 2020, Itamar S. <itamar8910@gmail.com> + * Copyright (c) 2020, the SerenityOS developers + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * 1. Redistributions of source code must retain the above copyright notice, this + * list of conditions and the following disclaimer. + * + * 2. Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE + * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL + * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR + * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER + * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, + * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE + * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +#pragma once + +#include "Debugger/DebugInfoWidget.h" +#include "Debugger/DisassemblyWidget.h" +#include "EditorWrapper.h" +#include "FindInFilesWidget.h" +#include "FormEditorWidget.h" +#include "Git/DiffViewer.h" +#include "Git/GitWidget.h" +#include "Locator.h" +#include "Project.h" +#include "ProjectFile.h" +#include "TerminalWrapper.h" +#include <LibGUI/ScrollBar.h> +#include <LibGUI/Splitter.h> +#include <LibGUI/Widget.h> +#include <LibThread/Thread.h> + +namespace HackStudio { + +class HackStudioWidget : public GUI::Widget { + C_OBJECT(HackStudioWidget) + +public: + virtual ~HackStudioWidget() override; + void open_file(const String& filename); + + Vector<String> selected_file_names() const; + + void update_actions(); + Project& project(); + GUI::TextEditor& current_editor(); + EditorWrapper& current_editor_wrapper(); + void set_current_editor_wrapper(RefPtr<EditorWrapper>); + + String currently_open_file() const { return m_currently_open_file; } + void initialize_menubar(GUI::MenuBar&); + +private: + static String get_full_path_of_serenity_source(const String& file); + + HackStudioWidget(const String& path_to_project); + void open_project(const String& root_path); + + enum class EditMode { + Text, + Form, + Diff, + }; + + void set_edit_mode(EditMode); + + NonnullRefPtr<GUI::Menu> create_project_tree_view_context_menu(); + NonnullRefPtr<GUI::Action> create_new_action(); + NonnullRefPtr<GUI::Action> create_open_selected_action(); + NonnullRefPtr<GUI::Action> create_delete_action(); + NonnullRefPtr<GUI::Action> create_switch_to_next_editor_action(); + NonnullRefPtr<GUI::Action> create_switch_to_previous_editor_action(); + NonnullRefPtr<GUI::Action> create_remove_current_editor_action(); + NonnullRefPtr<GUI::Action> create_open_action(); + NonnullRefPtr<GUI::Action> create_save_action(); + NonnullRefPtr<GUI::Action> create_add_editor_action(); + NonnullRefPtr<GUI::Action> create_add_terminal_action(); + NonnullRefPtr<GUI::Action> create_remove_current_terminal_action(); + NonnullRefPtr<GUI::Action> create_debug_action(); + NonnullRefPtr<GUI::Action> create_build_action(); + NonnullRefPtr<GUI::Action> create_run_action(); + NonnullRefPtr<GUI::Action> create_stop_action(); + + void add_new_editor(GUI::Widget& parent); + NonnullRefPtr<EditorWrapper> get_editor_of_file(const String& file_name); + String get_project_executable_path() const; + + void on_action_tab_change(); + void reveal_action_tab(GUI::Widget&); + void initialize_debugger(); + + void create_project_tree_view(GUI::Widget& parent); + void create_open_files_view(GUI::Widget& parent); + void create_form_editor(GUI::Widget& parent); + void create_toolbar(GUI::Widget& parent); + void create_action_tab(GUI::Widget& parent); + void create_app_menubar(GUI::MenuBar&); + void create_project_menubar(GUI::MenuBar&); + void create_edit_menubar(GUI::MenuBar&); + void create_build_menubar(GUI::MenuBar&); + void create_view_menubar(GUI::MenuBar&); + void create_help_menubar(GUI::MenuBar&); + + void run(TerminalWrapper& wrapper); + void build(TerminalWrapper& wrapper); + + void hide_action_tabs(); + + NonnullRefPtrVector<EditorWrapper> m_all_editor_wrappers; + RefPtr<EditorWrapper> m_current_editor_wrapper; + + // FIXME: This doesn't seem compatible with multiple split editors + String m_currently_open_file; + + HashMap<String, NonnullRefPtr<ProjectFile>> m_open_files; + Vector<String> m_open_files_vector; // NOTE: This contains the keys from m_open_files + + OwnPtr<Project> m_project; + + RefPtr<GUI::TreeView> m_project_tree_view; + RefPtr<GUI::ListView> m_open_files_view; + RefPtr<GUI::VerticalSplitter> m_right_hand_splitter; + RefPtr<GUI::StackWidget> m_right_hand_stack; + RefPtr<GUI::Splitter> m_editors_splitter; + RefPtr<GUI::Widget> m_form_inner_container; + RefPtr<FormEditorWidget> m_form_editor_widget; + RefPtr<GUI::TreeView> m_form_widget_tree_view; + RefPtr<DiffViewer> m_diff_viewer; + RefPtr<GitWidget> m_git_widget; + RefPtr<GUI::Menu> m_project_tree_view_context_menu; + RefPtr<GUI::TabWidget> m_action_tab_widget; + RefPtr<TerminalWrapper> m_terminal_wrapper; + RefPtr<Locator> m_locator; + RefPtr<FindInFilesWidget> m_find_in_files_widget; + RefPtr<DebugInfoWidget> m_debug_info_widget; + RefPtr<DisassemblyWidget> m_disassembly_widget; + RefPtr<LibThread::Thread> m_debugger_thread; + RefPtr<EditorWrapper> m_current_editor_in_execution; + + RefPtr<GUI::Action> m_new_action; + RefPtr<GUI::Action> m_open_selected_action; + RefPtr<GUI::Action> m_delete_action; + RefPtr<GUI::Action> m_switch_to_next_editor; + RefPtr<GUI::Action> m_switch_to_previous_editor; + RefPtr<GUI::Action> m_remove_current_editor_action; + RefPtr<GUI::Action> m_open_action; + RefPtr<GUI::Action> m_save_action; + RefPtr<GUI::Action> m_add_editor_action; + RefPtr<GUI::Action> m_add_terminal_action; + RefPtr<GUI::Action> m_remove_current_terminal_action; + RefPtr<GUI::Action> m_stop_action; + RefPtr<GUI::Action> m_debug_action; + RefPtr<GUI::Action> m_build_action; + RefPtr<GUI::Action> m_run_action; +}; +} diff --git a/Userland/DevTools/HackStudio/Language.h b/Userland/DevTools/HackStudio/Language.h new file mode 100644 index 0000000000..d763a8ac8a --- /dev/null +++ b/Userland/DevTools/HackStudio/Language.h @@ -0,0 +1,38 @@ +/* + * Copyright (c) 2020, the SerenityOS developers. + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * 1. Redistributions of source code must retain the above copyright notice, this + * list of conditions and the following disclaimer. + * + * 2. Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE + * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL + * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR + * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER + * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, + * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE + * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +#pragma once + +namespace HackStudio { +enum class Language { + Unknown, + Cpp, + JavaScript, + GML, + Ini, + Shell, +}; +} diff --git a/Userland/DevTools/HackStudio/LanguageClient.cpp b/Userland/DevTools/HackStudio/LanguageClient.cpp new file mode 100644 index 0000000000..a10df6d4b6 --- /dev/null +++ b/Userland/DevTools/HackStudio/LanguageClient.cpp @@ -0,0 +1,72 @@ +/* + * Copyright (c) 2020, the SerenityOS developers. + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * 1. Redistributions of source code must retain the above copyright notice, this + * list of conditions and the following disclaimer. + * + * 2. Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE + * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL + * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR + * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER + * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, + * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE + * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +#include "LanguageClient.h" +#include <AK/String.h> +#include <AK/Vector.h> + +namespace HackStudio { + +void ServerConnection::handle(const Messages::LanguageClient::AutoCompleteSuggestions& message) +{ + if (m_language_client) + m_language_client->provide_autocomplete_suggestions(message.suggestions()); +} + +void LanguageClient::open_file(const String& path, int fd) +{ + m_connection.post_message(Messages::LanguageServer::FileOpened(path, fd)); +} + +void LanguageClient::set_file_content(const String& path, const String& content) +{ + m_connection.post_message(Messages::LanguageServer::SetFileContent(path, content)); +} + +void LanguageClient::insert_text(const String& path, const String& text, size_t line, size_t column) +{ + m_connection.post_message(Messages::LanguageServer::FileEditInsertText(path, text, line, column)); +} + +void LanguageClient::remove_text(const String& path, size_t from_line, size_t from_column, size_t to_line, size_t to_column) +{ + m_connection.post_message(Messages::LanguageServer::FileEditRemoveText(path, from_line, from_column, to_line, to_column)); +} + +void LanguageClient::request_autocomplete(const String& path, size_t cursor_line, size_t cursor_column) +{ + m_connection.post_message(Messages::LanguageServer::AutoCompleteSuggestions(path, cursor_line, cursor_column)); +} + +void LanguageClient::provide_autocomplete_suggestions(const Vector<GUI::AutocompleteProvider::Entry>& suggestions) +{ + if (on_autocomplete_suggestions) + on_autocomplete_suggestions(suggestions); + + // Otherwise, drop it on the floor :shrug: +} + +} diff --git a/Userland/DevTools/HackStudio/LanguageClient.h b/Userland/DevTools/HackStudio/LanguageClient.h new file mode 100644 index 0000000000..bf294a7ea6 --- /dev/null +++ b/Userland/DevTools/HackStudio/LanguageClient.h @@ -0,0 +1,122 @@ +/* + * Copyright (c) 2020, the SerenityOS developers. + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * 1. Redistributions of source code must retain the above copyright notice, this + * list of conditions and the following disclaimer. + * + * 2. Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE + * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL + * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR + * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER + * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, + * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE + * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +#pragma once + +#include "AutoCompleteResponse.h" +#include <AK/Forward.h> +#include <AK/LexicalPath.h> +#include <AK/Types.h> +#include <LibIPC/ServerConnection.h> + +#include <DevTools/HackStudio/LanguageServers/LanguageClientEndpoint.h> +#include <DevTools/HackStudio/LanguageServers/LanguageServerEndpoint.h> + +namespace HackStudio { + +class LanguageClient; + +class ServerConnection + : public IPC::ServerConnection<LanguageClientEndpoint, LanguageServerEndpoint> + , public LanguageClientEndpoint { +public: + ServerConnection(const StringView& socket) + : IPC::ServerConnection<LanguageClientEndpoint, LanguageServerEndpoint>(*this, socket) + { + } + + void attach(LanguageClient& client) + { + m_language_client = &client; + } + + void detach() + { + m_language_client = nullptr; + } + + virtual void handshake() override + { + auto response = send_sync<Messages::LanguageServer::Greet>(); + set_my_client_id(response->client_id()); + } + + template<typename ConcreteType> + static NonnullRefPtr<ServerConnection> get_or_create(const String& project_path) + { + static HashMap<String, NonnullRefPtr<ConcreteType>> s_instances_for_projects; + auto key = LexicalPath { project_path }.string(); + if (auto instance = s_instances_for_projects.get(key); instance.has_value()) + return *instance.value(); + + auto connection = ConcreteType::construct(); + connection->handshake(); + s_instances_for_projects.set(key, *connection); + return *connection; + } + +protected: + virtual void handle(const Messages::LanguageClient::AutoCompleteSuggestions&) override; + + LanguageClient* m_language_client { nullptr }; +}; + +class LanguageClient { +public: + explicit LanguageClient(NonnullRefPtr<ServerConnection>&& connection) + : m_connection(*connection) + , m_server_connection(move(connection)) + { + m_connection.attach(*this); + } + + virtual ~LanguageClient() + { + m_connection.detach(); + } + + virtual void open_file(const String& path, int fd); + virtual void set_file_content(const String& path, const String& content); + virtual void insert_text(const String& path, const String& text, size_t line, size_t column); + virtual void remove_text(const String& path, size_t from_line, size_t from_column, size_t to_line, size_t to_column); + virtual void request_autocomplete(const String& path, size_t cursor_line, size_t cursor_column); + + void provide_autocomplete_suggestions(const Vector<GUI::AutocompleteProvider::Entry>&); + + Function<void(Vector<GUI::AutocompleteProvider::Entry>)> on_autocomplete_suggestions; + +private: + ServerConnection& m_connection; + NonnullRefPtr<ServerConnection> m_server_connection; +}; + +template<typename ServerConnectionT> +static inline NonnullOwnPtr<LanguageClient> get_language_client(const String& project_path) +{ + return make<LanguageClient>(ServerConnection::get_or_create<ServerConnectionT>(project_path)); +} + +} diff --git a/Userland/DevTools/HackStudio/LanguageClients/CMakeLists.txt b/Userland/DevTools/HackStudio/LanguageClients/CMakeLists.txt new file mode 100644 index 0000000000..e06c45f98c --- /dev/null +++ b/Userland/DevTools/HackStudio/LanguageClients/CMakeLists.txt @@ -0,0 +1,4 @@ +set(GENERATED_SOURCES + ../../LanguageServers/LanguageServerEndpoint.h + ../../LanguageServers/LanguageClientEndpoint.h +) diff --git a/Userland/DevTools/HackStudio/LanguageClients/ServerConnections.h b/Userland/DevTools/HackStudio/LanguageClients/ServerConnections.h new file mode 100644 index 0000000000..bcebdb419e --- /dev/null +++ b/Userland/DevTools/HackStudio/LanguageClients/ServerConnections.h @@ -0,0 +1,54 @@ +/* + * Copyright (c) 2020, Itamar S. <itamar8910@gmail.com> + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * 1. Redistributions of source code must retain the above copyright notice, this + * list of conditions and the following disclaimer. + * + * 2. Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE + * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL + * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR + * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER + * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, + * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE + * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +#pragma once + +#include "../LanguageClient.h" +#include <AK/LexicalPath.h> +#include <DevTools/HackStudio/LanguageServers/LanguageClientEndpoint.h> +#include <DevTools/HackStudio/LanguageServers/LanguageServerEndpoint.h> +#include <LibIPC/ServerConnection.h> + +#define LANGUAGE_CLIENT(namespace_, socket_name) \ + namespace namespace_ { \ + class ServerConnection : public HackStudio::ServerConnection { \ + C_OBJECT(ServerConnection) \ + private: \ + ServerConnection() \ + : HackStudio::ServerConnection("/tmp/portal/language/" #socket_name) \ + { \ + } \ + }; \ + } + +namespace LanguageClients { + +LANGUAGE_CLIENT(Cpp, cpp) +LANGUAGE_CLIENT(Shell, shell) + +} + +#undef LANGUAGE_CLIENT diff --git a/Userland/DevTools/HackStudio/LanguageServers/CMakeLists.txt b/Userland/DevTools/HackStudio/LanguageServers/CMakeLists.txt new file mode 100644 index 0000000000..afcc0118d0 --- /dev/null +++ b/Userland/DevTools/HackStudio/LanguageServers/CMakeLists.txt @@ -0,0 +1,5 @@ +compile_ipc(LanguageServer.ipc LanguageServerEndpoint.h) +compile_ipc(LanguageClient.ipc LanguageClientEndpoint.h) + +add_subdirectory(Cpp) +add_subdirectory(Shell) diff --git a/Userland/DevTools/HackStudio/LanguageServers/Cpp/AutoComplete.cpp b/Userland/DevTools/HackStudio/LanguageServers/Cpp/AutoComplete.cpp new file mode 100644 index 0000000000..a6fae1c425 --- /dev/null +++ b/Userland/DevTools/HackStudio/LanguageServers/Cpp/AutoComplete.cpp @@ -0,0 +1,97 @@ +/* + * Copyright (c) 2020, Itamar S. <itamar8910@gmail.com> + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * 1. Redistributions of source code must retain the above copyright notice, this + * list of conditions and the following disclaimer. + * + * 2. Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE + * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL + * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR + * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER + * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, + * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE + * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +#include "AutoComplete.h" +#include <AK/HashTable.h> +#include <LibCpp/Lexer.h> + +// #define DEBUG_AUTOCOMPLETE + +namespace LanguageServers::Cpp { + +Vector<GUI::AutocompleteProvider::Entry> AutoComplete::get_suggestions(const String& code, const GUI::TextPosition& autocomplete_position) +{ + auto lines = code.split('\n', true); + Cpp::Lexer lexer(code); + auto tokens = lexer.lex(); + + auto index_of_target_token = token_in_position(tokens, autocomplete_position); + if (!index_of_target_token.has_value()) + return {}; + + auto suggestions = identifier_prefixes(lines, tokens, index_of_target_token.value()); + +#ifdef DEBUG_AUTOCOMPLETE + for (auto& suggestion : suggestions) { + dbgln("suggestion: {}", suggestion.completion); + } +#endif + + return suggestions; +} + +StringView AutoComplete::text_of_token(const Vector<String>& lines, const Cpp::Token& token) +{ + ASSERT(token.m_start.line == token.m_end.line); + ASSERT(token.m_start.column <= token.m_end.column); + return lines[token.m_start.line].substring_view(token.m_start.column, token.m_end.column - token.m_start.column + 1); +} + +Optional<size_t> AutoComplete::token_in_position(const Vector<Cpp::Token>& tokens, const GUI::TextPosition& position) +{ + for (size_t token_index = 0; token_index < tokens.size(); ++token_index) { + auto& token = tokens[token_index]; + if (token.m_start.line != token.m_end.line) + continue; + if (token.m_start.line != position.line()) + continue; + if (token.m_start.column + 1 > position.column() || token.m_end.column + 1 < position.column()) + continue; + return token_index; + } + return {}; +} + +Vector<GUI::AutocompleteProvider::Entry> AutoComplete::identifier_prefixes(const Vector<String>& lines, const Vector<Cpp::Token>& tokens, size_t target_token_index) +{ + auto partial_input = text_of_token(lines, tokens[target_token_index]); + Vector<GUI::AutocompleteProvider::Entry> suggestions; + + HashTable<String> suggestions_lookup; // To avoid duplicate results + + for (size_t i = 0; i < target_token_index; ++i) { + auto& token = tokens[i]; + if (token.m_type != Cpp::Token::Type::Identifier) + continue; + auto text = text_of_token(lines, token); + if (text.starts_with(partial_input) && suggestions_lookup.set(text) == AK::HashSetResult::InsertedNewEntry) { + suggestions.append({ text, partial_input.length(), GUI::AutocompleteProvider::CompletionKind::Identifier }); + } + } + return suggestions; +} + +} diff --git a/Userland/DevTools/HackStudio/LanguageServers/Cpp/AutoComplete.h b/Userland/DevTools/HackStudio/LanguageServers/Cpp/AutoComplete.h new file mode 100644 index 0000000000..b4096bd793 --- /dev/null +++ b/Userland/DevTools/HackStudio/LanguageServers/Cpp/AutoComplete.h @@ -0,0 +1,51 @@ +/* + * Copyright (c) 2020, Itamar S. <itamar8910@gmail.com> + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * 1. Redistributions of source code must retain the above copyright notice, this + * list of conditions and the following disclaimer. + * + * 2. Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE + * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL + * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR + * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER + * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, + * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE + * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +#pragma once + +#include <AK/String.h> +#include <AK/Vector.h> +#include <DevTools/HackStudio/AutoCompleteResponse.h> +#include <LibCpp/Lexer.h> +#include <LibGUI/TextPosition.h> + +namespace LanguageServers::Cpp { + +using namespace ::Cpp; + +class AutoComplete { +public: + AutoComplete() = delete; + + static Vector<GUI::AutocompleteProvider::Entry> get_suggestions(const String& code, const GUI::TextPosition& autocomplete_position); + +private: + static Optional<size_t> token_in_position(const Vector<Cpp::Token>&, const GUI::TextPosition&); + static StringView text_of_token(const Vector<String>& lines, const Cpp::Token&); + static Vector<GUI::AutocompleteProvider::Entry> identifier_prefixes(const Vector<String>& lines, const Vector<Cpp::Token>&, size_t target_token_index); +}; + +} diff --git a/Userland/DevTools/HackStudio/LanguageServers/Cpp/CMakeLists.txt b/Userland/DevTools/HackStudio/LanguageServers/Cpp/CMakeLists.txt new file mode 100644 index 0000000000..de9569d8ca --- /dev/null +++ b/Userland/DevTools/HackStudio/LanguageServers/Cpp/CMakeLists.txt @@ -0,0 +1,15 @@ +set(SOURCES + ClientConnection.cpp + main.cpp + AutoComplete.cpp +) + +set(GENERATED_SOURCES + ../LanguageServerEndpoint.h + ../LanguageClientEndpoint.h) + +serenity_bin(CppLanguageServer) + +# We link with LibGUI because we use GUI::TextDocument to update +# the content of files according to the edit actions we receive over IPC. +target_link_libraries(CppLanguageServer LibIPC LibCpp LibGUI) diff --git a/Userland/DevTools/HackStudio/LanguageServers/Cpp/ClientConnection.cpp b/Userland/DevTools/HackStudio/LanguageServers/Cpp/ClientConnection.cpp new file mode 100644 index 0000000000..7952696b60 --- /dev/null +++ b/Userland/DevTools/HackStudio/LanguageServers/Cpp/ClientConnection.cpp @@ -0,0 +1,177 @@ +/* + * Copyright (c) 2020, Itamar S. <itamar8910@gmail.com> + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * 1. Redistributions of source code must retain the above copyright notice, this + * list of conditions and the following disclaimer. + * + * 2. Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE + * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL + * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR + * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER + * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, + * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE + * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +#include "ClientConnection.h" +#include "AutoComplete.h" +#include <AK/HashMap.h> +#include <LibCore/File.h> +#include <LibGUI/TextDocument.h> + +// #define DEBUG_CPP_LANGUAGE_SERVER +// #define DEBUG_FILE_CONTENT + +namespace LanguageServers::Cpp { + +static HashMap<int, RefPtr<ClientConnection>> s_connections; + +ClientConnection::ClientConnection(NonnullRefPtr<Core::LocalSocket> socket, int client_id) + : IPC::ClientConnection<LanguageClientEndpoint, LanguageServerEndpoint>(*this, move(socket), client_id) +{ + s_connections.set(client_id, *this); +} + +ClientConnection::~ClientConnection() +{ +} + +void ClientConnection::die() +{ + s_connections.remove(client_id()); + exit(0); +} + +OwnPtr<Messages::LanguageServer::GreetResponse> ClientConnection::handle(const Messages::LanguageServer::Greet&) +{ + return make<Messages::LanguageServer::GreetResponse>(client_id()); +} + +class DefaultDocumentClient final : public GUI::TextDocument::Client { +public: + virtual ~DefaultDocumentClient() override = default; + virtual void document_did_append_line() override {}; + virtual void document_did_insert_line(size_t) override {}; + virtual void document_did_remove_line(size_t) override {}; + virtual void document_did_remove_all_lines() override {}; + virtual void document_did_change() override {}; + virtual void document_did_set_text() override {}; + virtual void document_did_set_cursor(const GUI::TextPosition&) override {}; + + virtual bool is_automatic_indentation_enabled() const override { return false; } + virtual int soft_tab_width() const override { return 4; } +}; + +static DefaultDocumentClient s_default_document_client; + +void ClientConnection::handle(const Messages::LanguageServer::FileOpened& message) +{ + auto file = Core::File::construct(this); + if (!file->open(message.file().fd(), Core::IODevice::ReadOnly, Core::File::ShouldCloseFileDescriptor::Yes)) { + errno = file->error(); + perror("open"); + dbgln("Failed to open project file"); + return; + } + auto content = file->read_all(); + StringView content_view(content); + auto document = GUI::TextDocument::create(&s_default_document_client); + document->set_text(content_view); + m_open_files.set(message.file_name(), document); +#ifdef DEBUG_FILE_CONTENT + dbg() << document->text(); +#endif +} + +void ClientConnection::handle(const Messages::LanguageServer::FileEditInsertText& message) +{ +#ifdef DEBUG_CPP_LANGUAGE_SERVER + dbgln("InsertText for file: {}", message.file_name()); + dbgln("Text: {}", message.text()); + dbgln("[{}:{}]", message.start_line(), message.start_column()); +#endif + auto document = document_for(message.file_name()); + if (!document) { + dbgln("file {} has not been opened", message.file_name()); + return; + } + GUI::TextPosition start_position { (size_t)message.start_line(), (size_t)message.start_column() }; + document->insert_at(start_position, message.text(), &s_default_document_client); +#ifdef DEBUG_FILE_CONTENT + dbgln("{}", document->text()); +#endif +} + +void ClientConnection::handle(const Messages::LanguageServer::FileEditRemoveText& message) +{ +#ifdef DEBUG_CPP_LANGUAGE_SERVER + dbgln("RemoveText for file: {}", message.file_name()); + dbgln("[{}:{} - {}:{}]", message.start_line(), message.start_column(), message.end_line(), message.end_column()); +#endif + auto document = document_for(message.file_name()); + if (!document) { + dbgln("file {} has not been opened", message.file_name()); + return; + } + GUI::TextPosition start_position { (size_t)message.start_line(), (size_t)message.start_column() }; + GUI::TextRange range { + GUI::TextPosition { (size_t)message.start_line(), + (size_t)message.start_column() }, + GUI::TextPosition { (size_t)message.end_line(), + (size_t)message.end_column() } + }; + + document->remove(range); +#ifdef DEBUG_FILE_CONTENT + dbgln("{}", document->text()); +#endif +} + +void ClientConnection::handle(const Messages::LanguageServer::AutoCompleteSuggestions& message) +{ +#ifdef DEBUG_CPP_LANGUAGE_SERVER + dbgln("AutoCompleteSuggestions for: {} {}:{}", message.file_name(), message.cursor_line(), message.cursor_column()); +#endif + + auto document = document_for(message.file_name()); + if (!document) { + dbgln("file {} has not been opened", message.file_name()); + return; + } + + auto suggestions = AutoComplete::get_suggestions(document->text(), { (size_t)message.cursor_line(), (size_t)max(message.cursor_column(), message.cursor_column() - 1) }); + post_message(Messages::LanguageClient::AutoCompleteSuggestions(move(suggestions))); +} + +RefPtr<GUI::TextDocument> ClientConnection::document_for(const String& file_name) +{ + auto document_optional = m_open_files.get(file_name); + if (!document_optional.has_value()) + return nullptr; + + return document_optional.value(); +} + +void ClientConnection::handle(const Messages::LanguageServer::SetFileContent& message) +{ + auto document = document_for(message.file_name()); + if (!document) { + dbgln("file {} has not been opened", message.file_name()); + return; + } + auto content = message.content(); + document->set_text(content.view()); +} + +} diff --git a/Userland/DevTools/HackStudio/LanguageServers/Cpp/ClientConnection.h b/Userland/DevTools/HackStudio/LanguageServers/Cpp/ClientConnection.h new file mode 100644 index 0000000000..d0a7b4558c --- /dev/null +++ b/Userland/DevTools/HackStudio/LanguageServers/Cpp/ClientConnection.h @@ -0,0 +1,64 @@ +/* + * Copyright (c) 2020, Itamar S. <itamar8910@gmail.com> + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * 1. Redistributions of source code must retain the above copyright notice, this + * list of conditions and the following disclaimer. + * + * 2. Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE + * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL + * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR + * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER + * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, + * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE + * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +#pragma once + +#include <AK/HashMap.h> +#include <AK/LexicalPath.h> +#include <DevTools/HackStudio/AutoCompleteResponse.h> +#include <LibGUI/TextDocument.h> +#include <LibIPC/ClientConnection.h> + +#include <DevTools/HackStudio/LanguageServers/LanguageClientEndpoint.h> +#include <DevTools/HackStudio/LanguageServers/LanguageServerEndpoint.h> + +namespace LanguageServers::Cpp { + +class ClientConnection final + : public IPC::ClientConnection<LanguageClientEndpoint, LanguageServerEndpoint> + , public LanguageServerEndpoint { + C_OBJECT(ClientConnection); + +public: + explicit ClientConnection(NonnullRefPtr<Core::LocalSocket>, int client_id); + ~ClientConnection() override; + + virtual void die() override; + +private: + virtual OwnPtr<Messages::LanguageServer::GreetResponse> handle(const Messages::LanguageServer::Greet&) override; + virtual void handle(const Messages::LanguageServer::FileOpened&) override; + virtual void handle(const Messages::LanguageServer::FileEditInsertText&) override; + virtual void handle(const Messages::LanguageServer::FileEditRemoveText&) override; + virtual void handle(const Messages::LanguageServer::SetFileContent&) override; + virtual void handle(const Messages::LanguageServer::AutoCompleteSuggestions&) override; + + RefPtr<GUI::TextDocument> document_for(const String& file_name); + + HashMap<String, NonnullRefPtr<GUI::TextDocument>> m_open_files; +}; + +} diff --git a/Userland/DevTools/HackStudio/LanguageServers/Cpp/main.cpp b/Userland/DevTools/HackStudio/LanguageServers/Cpp/main.cpp new file mode 100644 index 0000000000..197fd91f27 --- /dev/null +++ b/Userland/DevTools/HackStudio/LanguageServers/Cpp/main.cpp @@ -0,0 +1,55 @@ +/* + * Copyright (c) 2020, Itamar S. <itamar8910@gmail.com> + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * 1. Redistributions of source code must retain the above copyright notice, this + * list of conditions and the following disclaimer. + * + * 2. Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE + * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL + * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR + * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER + * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, + * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE + * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +#include <AK/LexicalPath.h> +#include <DevTools/HackStudio/LanguageServers/Cpp/ClientConnection.h> +#include <LibCore/EventLoop.h> +#include <LibCore/File.h> +#include <LibCore/LocalServer.h> +#include <LibIPC/ClientConnection.h> +#include <sys/stat.h> +#include <unistd.h> + +int main(int, char**) +{ + Core::EventLoop event_loop; + if (pledge("stdio unix recvfd", nullptr) < 0) { + perror("pledge"); + return 1; + } + + auto socket = Core::LocalSocket::take_over_accepted_socket_from_system_server(); + IPC::new_client_connection<LanguageServers::Cpp::ClientConnection>(socket.release_nonnull(), 1); + if (pledge("stdio recvfd", nullptr) < 0) { + perror("pledge"); + return 1; + } + if (unveil(nullptr, nullptr) < 0) { + perror("unveil"); + return 1; + } + return event_loop.exec(); +} diff --git a/Userland/DevTools/HackStudio/LanguageServers/LanguageClient.ipc b/Userland/DevTools/HackStudio/LanguageServers/LanguageClient.ipc new file mode 100644 index 0000000000..31b5d5b2c5 --- /dev/null +++ b/Userland/DevTools/HackStudio/LanguageServers/LanguageClient.ipc @@ -0,0 +1,4 @@ +endpoint LanguageClient = 8002 +{ + AutoCompleteSuggestions(Vector<GUI::AutocompleteProvider::Entry> suggestions) =| +} diff --git a/Userland/DevTools/HackStudio/LanguageServers/LanguageServer.ipc b/Userland/DevTools/HackStudio/LanguageServers/LanguageServer.ipc new file mode 100644 index 0000000000..de16fa14ec --- /dev/null +++ b/Userland/DevTools/HackStudio/LanguageServers/LanguageServer.ipc @@ -0,0 +1,11 @@ +endpoint LanguageServer = 8001 +{ + Greet() => (i32 client_id) + + FileOpened(String file_name, IPC::File file) =| + FileEditInsertText(String file_name, String text, i32 start_line, i32 start_column) =| + FileEditRemoveText(String file_name, i32 start_line, i32 start_column, i32 end_line, i32 end_column) =| + SetFileContent(String file_name, String content) =| + + AutoCompleteSuggestions(String file_name, i32 cursor_line, i32 cursor_column) =| +} diff --git a/Userland/DevTools/HackStudio/LanguageServers/Shell/AutoComplete.cpp b/Userland/DevTools/HackStudio/LanguageServers/Shell/AutoComplete.cpp new file mode 100644 index 0000000000..89df78365a --- /dev/null +++ b/Userland/DevTools/HackStudio/LanguageServers/Shell/AutoComplete.cpp @@ -0,0 +1,63 @@ +/* + * Copyright (c) 2020, the SerenityOS developers. + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * 1. Redistributions of source code must retain the above copyright notice, this + * list of conditions and the following disclaimer. + * + * 2. Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE + * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL + * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR + * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER + * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, + * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE + * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +#include "AutoComplete.h" +#include <AK/HashTable.h> +#include <LibLine/SuggestionManager.h> +#include <Shell/AST.h> +#include <Shell/Parser.h> +#include <Shell/Shell.h> + +// #define DEBUG_AUTOCOMPLETE + +namespace LanguageServers::Shell { + +Vector<GUI::AutocompleteProvider::Entry> AutoComplete::get_suggestions(const String& code, size_t offset) +{ + // FIXME: No need to reparse this every time! + auto ast = ::Shell::Parser { code }.parse(); + if (!ast) + return {}; + +#ifdef DEBUG_AUTOCOMPLETE + dbgln("Complete '{}'", code); + ast->dump(1); + dbgln("At offset {}", offset); +#endif + + auto result = ast->complete_for_editor(m_shell, offset); + Vector<GUI::AutocompleteProvider::Entry> completions; + for (auto& entry : result) { +#ifdef DEBUG_AUTOCOMPLETE + dbgln("Suggestion: '{}' starting at {}", entry.text_string, entry.input_offset); +#endif + completions.append({ entry.text_string, entry.input_offset }); + } + + return completions; +} + +} diff --git a/Userland/DevTools/HackStudio/LanguageServers/Shell/AutoComplete.h b/Userland/DevTools/HackStudio/LanguageServers/Shell/AutoComplete.h new file mode 100644 index 0000000000..fece37dcb7 --- /dev/null +++ b/Userland/DevTools/HackStudio/LanguageServers/Shell/AutoComplete.h @@ -0,0 +1,50 @@ +/* + * Copyright (c) 2020, the SerenityOS developers. + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * 1. Redistributions of source code must retain the above copyright notice, this + * list of conditions and the following disclaimer. + * + * 2. Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE + * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL + * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR + * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER + * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, + * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE + * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +#pragma once + +#include <AK/String.h> +#include <AK/Vector.h> +#include <DevTools/HackStudio/AutoCompleteResponse.h> +#include <LibGUI/TextPosition.h> +#include <Shell/Shell.h> + +namespace LanguageServers::Shell { + +class AutoComplete { +public: + AutoComplete() + : m_shell(::Shell::Shell::construct()) + { + } + + Vector<GUI::AutocompleteProvider::Entry> get_suggestions(const String& code, size_t autocomplete_position); + +private: + NonnullRefPtr<::Shell::Shell> m_shell; +}; + +} diff --git a/Userland/DevTools/HackStudio/LanguageServers/Shell/CMakeLists.txt b/Userland/DevTools/HackStudio/LanguageServers/Shell/CMakeLists.txt new file mode 100644 index 0000000000..144a4526a3 --- /dev/null +++ b/Userland/DevTools/HackStudio/LanguageServers/Shell/CMakeLists.txt @@ -0,0 +1,15 @@ +set(SOURCES + ClientConnection.cpp + main.cpp + AutoComplete.cpp +) + +set(GENERATED_SOURCES + ../LanguageServerEndpoint.h + ../LanguageClientEndpoint.h) + +serenity_bin(ShellLanguageServer) + +# We link with LibGUI because we use GUI::TextDocument to update +# the content of files according to the edit actions we receive over IPC. +target_link_libraries(ShellLanguageServer LibIPC LibShell LibGUI) diff --git a/Userland/DevTools/HackStudio/LanguageServers/Shell/ClientConnection.cpp b/Userland/DevTools/HackStudio/LanguageServers/Shell/ClientConnection.cpp new file mode 100644 index 0000000000..794085f618 --- /dev/null +++ b/Userland/DevTools/HackStudio/LanguageServers/Shell/ClientConnection.cpp @@ -0,0 +1,186 @@ +/* + * Copyright (c) 2020, the SerenityOS developers. + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * 1. Redistributions of source code must retain the above copyright notice, this + * list of conditions and the following disclaimer. + * + * 2. Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE + * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL + * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR + * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER + * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, + * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE + * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +#include "ClientConnection.h" +#include "AutoComplete.h" +#include <AK/HashMap.h> +#include <LibCore/File.h> +#include <LibGUI/TextDocument.h> + +// #define DEBUG_SH_LANGUAGE_SERVER +// #define DEBUG_FILE_CONTENT + +namespace LanguageServers::Shell { + +static HashMap<int, RefPtr<ClientConnection>> s_connections; + +ClientConnection::ClientConnection(NonnullRefPtr<Core::LocalSocket> socket, int client_id) + : IPC::ClientConnection<LanguageClientEndpoint, LanguageServerEndpoint>(*this, move(socket), client_id) +{ + s_connections.set(client_id, *this); +} + +ClientConnection::~ClientConnection() +{ +} + +void ClientConnection::die() +{ + s_connections.remove(client_id()); + exit(0); +} + +OwnPtr<Messages::LanguageServer::GreetResponse> ClientConnection::handle(const Messages::LanguageServer::Greet&) +{ + return make<Messages::LanguageServer::GreetResponse>(client_id()); +} + +class DefaultDocumentClient final : public GUI::TextDocument::Client { +public: + virtual ~DefaultDocumentClient() override = default; + virtual void document_did_append_line() override {}; + virtual void document_did_insert_line(size_t) override {}; + virtual void document_did_remove_line(size_t) override {}; + virtual void document_did_remove_all_lines() override {}; + virtual void document_did_change() override {}; + virtual void document_did_set_text() override {}; + virtual void document_did_set_cursor(const GUI::TextPosition&) override {}; + + virtual bool is_automatic_indentation_enabled() const override { return false; } + virtual int soft_tab_width() const override { return 4; } +}; + +static DefaultDocumentClient s_default_document_client; + +void ClientConnection::handle(const Messages::LanguageServer::FileOpened& message) +{ + auto file = Core::File::construct(this); + if (!file->open(message.file().fd(), Core::IODevice::ReadOnly, Core::File::ShouldCloseFileDescriptor::Yes)) { + errno = file->error(); + perror("open"); + dbgln("Failed to open project file"); + return; + } + auto content = file->read_all(); + StringView content_view(content); + auto document = GUI::TextDocument::create(&s_default_document_client); + document->set_text(content_view); + m_open_files.set(message.file_name(), document); +#ifdef DEBUG_FILE_CONTENT + dbg() << document->text(); +#endif +} + +void ClientConnection::handle(const Messages::LanguageServer::FileEditInsertText& message) +{ +#ifdef DEBUG_SH_LANGUAGE_SERVER + dbgln("InsertText for file: {}", message.file_name()); + dbgln("Text: {}", message.text()); + dbgln("[{}:{}]", message.start_line(), message.start_column()); +#endif + auto document = document_for(message.file_name()); + if (!document) { + dbgln("file {} has not been opened", message.file_name()); + return; + } + GUI::TextPosition start_position { (size_t)message.start_line(), (size_t)message.start_column() }; + document->insert_at(start_position, message.text(), &s_default_document_client); +#ifdef DEBUG_FILE_CONTENT + dbgln("{}", document->text()); +#endif +} + +void ClientConnection::handle(const Messages::LanguageServer::FileEditRemoveText& message) +{ +#ifdef DEBUG_SH_LANGUAGE_SERVER + dbgln("RemoveText for file: {}", message.file_name()); + dbgln("[{}:{} - {}:{}]", message.start_line(), message.start_column(), message.end_line(), message.end_column()); +#endif + auto document = document_for(message.file_name()); + if (!document) { + dbgln("file {} has not been opened", message.file_name()); + return; + } + GUI::TextPosition start_position { (size_t)message.start_line(), (size_t)message.start_column() }; + GUI::TextRange range { + GUI::TextPosition { (size_t)message.start_line(), + (size_t)message.start_column() }, + GUI::TextPosition { (size_t)message.end_line(), + (size_t)message.end_column() } + }; + + document->remove(range); +#ifdef DEBUG_FILE_CONTENT + dbg() << document->text(); +#endif +} + +void ClientConnection::handle(const Messages::LanguageServer::AutoCompleteSuggestions& message) +{ +#ifdef DEBUG_SH_LANGUAGE_SERVER + dbgln("AutoCompleteSuggestions for: {} {}:{}", message.file_name(), message.cursor_line(), message.cursor_column()); +#endif + + auto document = document_for(message.file_name()); + if (!document) { + dbgln("file {} has not been opened", message.file_name()); + return; + } + + auto& lines = document->lines(); + size_t offset = 0; + + if (message.cursor_line() > 0) { + for (auto i = 0; i < message.cursor_line(); ++i) + offset += lines[i].length() + 1; + } + offset += message.cursor_column(); + + auto suggestions = m_autocomplete.get_suggestions(document->text(), offset); + post_message(Messages::LanguageClient::AutoCompleteSuggestions(move(suggestions))); +} + +RefPtr<GUI::TextDocument> ClientConnection::document_for(const String& file_name) +{ + auto document_optional = m_open_files.get(file_name); + if (!document_optional.has_value()) + return nullptr; + + return document_optional.value(); +} + +void ClientConnection::handle(const Messages::LanguageServer::SetFileContent& message) +{ + auto document = document_for(message.file_name()); + if (!document) { + dbgln("file {} has not been opened", message.file_name()); + return; + } + auto content = message.content(); + document->set_text(content.view()); +} + +} diff --git a/Userland/DevTools/HackStudio/LanguageServers/Shell/ClientConnection.h b/Userland/DevTools/HackStudio/LanguageServers/Shell/ClientConnection.h new file mode 100644 index 0000000000..bb7fc14bd9 --- /dev/null +++ b/Userland/DevTools/HackStudio/LanguageServers/Shell/ClientConnection.h @@ -0,0 +1,67 @@ +/* + * Copyright (c) 2020, the SerenityOS developers. + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * 1. Redistributions of source code must retain the above copyright notice, this + * list of conditions and the following disclaimer. + * + * 2. Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE + * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL + * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR + * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER + * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, + * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE + * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +#pragma once + +#include "AutoComplete.h" +#include <AK/HashMap.h> +#include <AK/LexicalPath.h> +#include <DevTools/HackStudio/AutoCompleteResponse.h> +#include <LibGUI/TextDocument.h> +#include <LibIPC/ClientConnection.h> + +#include <DevTools/HackStudio/LanguageServers/LanguageClientEndpoint.h> +#include <DevTools/HackStudio/LanguageServers/LanguageServerEndpoint.h> + +namespace LanguageServers::Shell { + +class ClientConnection final + : public IPC::ClientConnection<LanguageClientEndpoint, LanguageServerEndpoint> + , public LanguageServerEndpoint { + C_OBJECT(ClientConnection); + +public: + explicit ClientConnection(NonnullRefPtr<Core::LocalSocket>, int client_id); + ~ClientConnection() override; + + virtual void die() override; + +private: + virtual OwnPtr<Messages::LanguageServer::GreetResponse> handle(const Messages::LanguageServer::Greet&) override; + virtual void handle(const Messages::LanguageServer::FileOpened&) override; + virtual void handle(const Messages::LanguageServer::FileEditInsertText&) override; + virtual void handle(const Messages::LanguageServer::FileEditRemoveText&) override; + virtual void handle(const Messages::LanguageServer::SetFileContent&) override; + virtual void handle(const Messages::LanguageServer::AutoCompleteSuggestions&) override; + + RefPtr<GUI::TextDocument> document_for(const String& file_name); + + HashMap<String, NonnullRefPtr<GUI::TextDocument>> m_open_files; + + AutoComplete m_autocomplete; +}; + +} diff --git a/Userland/DevTools/HackStudio/LanguageServers/Shell/main.cpp b/Userland/DevTools/HackStudio/LanguageServers/Shell/main.cpp new file mode 100644 index 0000000000..90e3f62271 --- /dev/null +++ b/Userland/DevTools/HackStudio/LanguageServers/Shell/main.cpp @@ -0,0 +1,63 @@ +/* + * Copyright (c) 2020, the SerenityOS developers. + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * 1. Redistributions of source code must retain the above copyright notice, this + * list of conditions and the following disclaimer. + * + * 2. Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE + * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL + * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR + * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER + * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, + * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE + * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +#include <AK/LexicalPath.h> +#include <DevTools/HackStudio/LanguageServers/Shell/ClientConnection.h> +#include <LibCore/EventLoop.h> +#include <LibCore/File.h> +#include <LibCore/LocalServer.h> +#include <LibIPC/ClientConnection.h> +#include <sys/stat.h> +#include <unistd.h> + +int main(int, char**) +{ + Core::EventLoop event_loop; + if (pledge("stdio unix rpath recvfd", nullptr) < 0) { + perror("pledge"); + return 1; + } + + auto socket = Core::LocalSocket::take_over_accepted_socket_from_system_server(); + IPC::new_client_connection<LanguageServers::Shell::ClientConnection>(socket.release_nonnull(), 1); + if (pledge("stdio rpath recvfd", nullptr) < 0) { + perror("pledge"); + return 1; + } + if (unveil("/etc/passwd", "r") < 0) { + perror("unveil"); + return 1; + } + if (unveil("/", "b") < 0) { + perror("unveil"); + return 1; + } + if (unveil(nullptr, nullptr) < 0) { + perror("unveil"); + return 1; + } + return event_loop.exec(); +} diff --git a/Userland/DevTools/HackStudio/Locator.cpp b/Userland/DevTools/HackStudio/Locator.cpp new file mode 100644 index 0000000000..50c16fde1e --- /dev/null +++ b/Userland/DevTools/HackStudio/Locator.cpp @@ -0,0 +1,178 @@ +/* + * Copyright (c) 2018-2020, Andreas Kling <kling@serenityos.org> + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * 1. Redistributions of source code must retain the above copyright notice, this + * list of conditions and the following disclaimer. + * + * 2. Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE + * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL + * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR + * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER + * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, + * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE + * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +#include "Locator.h" +#include "HackStudio.h" +#include "Project.h" +#include <LibGUI/BoxLayout.h> +#include <LibGUI/FileIconProvider.h> +#include <LibGUI/TableView.h> +#include <LibGUI/TextBox.h> +#include <LibGUI/Window.h> + +namespace HackStudio { + +class LocatorSuggestionModel final : public GUI::Model { +public: + explicit LocatorSuggestionModel(Vector<String>&& suggestions) + : m_suggestions(move(suggestions)) + { + } + + enum Column { + Icon, + Name, + __Column_Count, + }; + virtual int row_count(const GUI::ModelIndex& = GUI::ModelIndex()) const override { return m_suggestions.size(); } + virtual int column_count(const GUI::ModelIndex& = GUI::ModelIndex()) const override { return Column::__Column_Count; } + virtual GUI::Variant data(const GUI::ModelIndex& index, GUI::ModelRole role) const override + { + auto& suggestion = m_suggestions.at(index.row()); + if (role == GUI::ModelRole::Display) { + if (index.column() == Column::Name) + return suggestion; + if (index.column() == Column::Icon) + return GUI::FileIconProvider::icon_for_path(suggestion); + } + return {}; + } + virtual void update() override {}; + +private: + Vector<String> m_suggestions; +}; + +Locator::Locator() +{ + set_layout<GUI::VerticalBoxLayout>(); + set_fixed_height(20); + m_textbox = add<GUI::TextBox>(); + m_textbox->on_change = [this] { + update_suggestions(); + }; + m_textbox->on_escape_pressed = [this] { + m_popup_window->hide(); + }; + m_textbox->on_up_pressed = [this] { + GUI::ModelIndex new_index = m_suggestion_view->selection().first(); + if (new_index.is_valid()) + new_index = m_suggestion_view->model()->index(new_index.row() - 1); + else + new_index = m_suggestion_view->model()->index(0); + + if (m_suggestion_view->model()->is_valid(new_index)) { + m_suggestion_view->selection().set(new_index); + m_suggestion_view->scroll_into_view(new_index, Orientation::Vertical); + } + }; + m_textbox->on_down_pressed = [this] { + GUI::ModelIndex new_index = m_suggestion_view->selection().first(); + if (new_index.is_valid()) + new_index = m_suggestion_view->model()->index(new_index.row() + 1); + else + new_index = m_suggestion_view->model()->index(0); + + if (m_suggestion_view->model()->is_valid(new_index)) { + m_suggestion_view->selection().set(new_index); + m_suggestion_view->scroll_into_view(new_index, Orientation::Vertical); + } + }; + + m_textbox->on_return_pressed = [this] { + auto selected_index = m_suggestion_view->selection().first(); + if (!selected_index.is_valid()) + return; + open_suggestion(selected_index); + }; + + m_popup_window = GUI::Window::construct(); + // FIXME: This is obviously not a tooltip window, but it's the closest thing to what we want atm. + m_popup_window->set_window_type(GUI::WindowType::Tooltip); + m_popup_window->set_rect(0, 0, 500, 200); + + m_suggestion_view = m_popup_window->set_main_widget<GUI::TableView>(); + m_suggestion_view->set_column_headers_visible(false); + + m_suggestion_view->on_activation = [this](auto& index) { + open_suggestion(index); + }; +} + +Locator::~Locator() +{ +} + +void Locator::open_suggestion(const GUI::ModelIndex& index) +{ + auto filename_index = m_suggestion_view->model()->index(index.row(), LocatorSuggestionModel::Column::Name); + auto filename = filename_index.data().to_string(); + open_file(filename); + close(); +} + +void Locator::open() +{ + m_textbox->set_focus(true); + if (!m_textbox->text().is_empty()) { + m_textbox->select_all(); + m_popup_window->show(); + } +} + +void Locator::close() +{ + m_popup_window->hide(); +} + +void Locator::update_suggestions() +{ + auto typed_text = m_textbox->text(); + Vector<String> suggestions; + project().for_each_text_file([&](auto& file) { + if (file.name().contains(typed_text, CaseSensitivity::CaseInsensitive)) + suggestions.append(file.name()); + }); + dbgln("I have {} suggestion(s):", suggestions.size()); + for (auto& s : suggestions) { + dbgln(" {}", s); + } + + bool has_suggestions = !suggestions.is_empty(); + + m_suggestion_view->set_model(adopt(*new LocatorSuggestionModel(move(suggestions)))); + + if (!has_suggestions) + m_suggestion_view->selection().clear(); + else + m_suggestion_view->selection().set(m_suggestion_view->model()->index(0)); + + m_popup_window->move_to(screen_relative_rect().top_left().translated(0, -m_popup_window->height())); + dbgln("Popup rect: {}", m_popup_window->rect()); + m_popup_window->show(); +} + +} diff --git a/Userland/DevTools/HackStudio/Locator.h b/Userland/DevTools/HackStudio/Locator.h new file mode 100644 index 0000000000..e52eb02447 --- /dev/null +++ b/Userland/DevTools/HackStudio/Locator.h @@ -0,0 +1,52 @@ +/* + * Copyright (c) 2018-2020, Andreas Kling <kling@serenityos.org> + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * 1. Redistributions of source code must retain the above copyright notice, this + * list of conditions and the following disclaimer. + * + * 2. Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE + * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL + * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR + * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER + * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, + * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE + * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +#pragma once + +#include <LibGUI/Widget.h> + +namespace HackStudio { + +class Locator final : public GUI::Widget { + C_OBJECT(Locator) +public: + virtual ~Locator() override; + + void open(); + void close(); + +private: + void update_suggestions(); + void open_suggestion(const GUI::ModelIndex&); + + Locator(); + + RefPtr<GUI::TextBox> m_textbox; + RefPtr<GUI::Window> m_popup_window; + RefPtr<GUI::TableView> m_suggestion_view; +}; + +} diff --git a/Userland/DevTools/HackStudio/Project.cpp b/Userland/DevTools/HackStudio/Project.cpp new file mode 100644 index 0000000000..f83f2e8940 --- /dev/null +++ b/Userland/DevTools/HackStudio/Project.cpp @@ -0,0 +1,84 @@ +/* + * Copyright (c) 2018-2020, Andreas Kling <kling@serenityos.org> + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * 1. Redistributions of source code must retain the above copyright notice, this + * list of conditions and the following disclaimer. + * + * 2. Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE + * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL + * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR + * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER + * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, + * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE + * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +#include "Project.h" +#include "HackStudio.h" +#include <LibCore/File.h> + +namespace HackStudio { + +Project::Project(const String& root_path) + : m_root_path(root_path) +{ + m_model = GUI::FileSystemModel::create(root_path, GUI::FileSystemModel::Mode::FilesAndDirectories); +} + +Project::~Project() +{ +} + +OwnPtr<Project> Project::open_with_root_path(const String& root_path) +{ + if (!Core::File::is_directory(root_path)) + return {}; + return adopt_own(*new Project(root_path)); +} + +template<typename Callback> +static void traverse_model(const GUI::FileSystemModel& model, const GUI::ModelIndex& index, Callback callback) +{ + if (index.is_valid()) + callback(index); + auto row_count = model.row_count(index); + if (!row_count) + return; + for (int row = 0; row < row_count; ++row) { + auto child_index = model.index(row, GUI::FileSystemModel::Column::Name, index); + traverse_model(model, child_index, callback); + } +} + +void Project::for_each_text_file(Function<void(const ProjectFile&)> callback) const +{ + traverse_model(model(), {}, [&](auto& index) { + auto file = get_file(model().full_path(index)); + if (file) + callback(*file); + }); +} + +RefPtr<ProjectFile> Project::get_file(const String& path) const +{ + for (auto& file : m_files) { + if (file.name() == path) + return file; + } + auto file = ProjectFile::construct_with_name(path); + m_files.append(file); + return file; +} + +} diff --git a/Userland/DevTools/HackStudio/Project.h b/Userland/DevTools/HackStudio/Project.h new file mode 100644 index 0000000000..6d3453e84e --- /dev/null +++ b/Userland/DevTools/HackStudio/Project.h @@ -0,0 +1,64 @@ +/* + * Copyright (c) 2018-2020, Andreas Kling <kling@serenityos.org> + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * 1. Redistributions of source code must retain the above copyright notice, this + * list of conditions and the following disclaimer. + * + * 2. Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE + * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL + * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR + * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER + * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, + * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE + * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +#pragma once + +#include "ProjectFile.h" +#include <AK/LexicalPath.h> +#include <AK/Noncopyable.h> +#include <AK/OwnPtr.h> +#include <LibGUI/FileSystemModel.h> + +namespace HackStudio { + +class Project { + AK_MAKE_NONCOPYABLE(Project); + AK_MAKE_NONMOVABLE(Project); + +public: + ~Project(); + + static OwnPtr<Project> open_with_root_path(const String& root_path); + + GUI::FileSystemModel& model() { return *m_model; } + const GUI::FileSystemModel& model() const { return *m_model; } + String name() const { return LexicalPath(m_root_path).basename(); } + String root_path() const { return m_root_path; } + + RefPtr<ProjectFile> get_file(const String& path) const; + + void for_each_text_file(Function<void(const ProjectFile&)>) const; + +private: + explicit Project(const String& root_path); + + RefPtr<GUI::FileSystemModel> m_model; + mutable NonnullRefPtrVector<ProjectFile> m_files; + + String m_root_path; +}; + +} diff --git a/Userland/DevTools/HackStudio/ProjectFile.cpp b/Userland/DevTools/HackStudio/ProjectFile.cpp new file mode 100644 index 0000000000..989632b480 --- /dev/null +++ b/Userland/DevTools/HackStudio/ProjectFile.cpp @@ -0,0 +1,74 @@ +/* + * Copyright (c) 2018-2020, Andreas Kling <kling@serenityos.org> + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * 1. Redistributions of source code must retain the above copyright notice, this + * list of conditions and the following disclaimer. + * + * 2. Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE + * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL + * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR + * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER + * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, + * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE + * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +#include "ProjectFile.h" +#include <LibCore/File.h> +#include <string.h> + +namespace HackStudio { + +ProjectFile::ProjectFile(const String& name) + : m_name(name) +{ +} + +GUI::TextDocument& ProjectFile::document() const +{ + if (!m_document) { + m_document = CodeDocument::create(m_name); + auto file_or_error = Core::File::open(m_name, Core::File::ReadOnly); + if (file_or_error.is_error()) { + warnln("Couldn't open '{}': {}", m_name, file_or_error.error()); + // This is okay though, we'll just go with an empty document and create the file when saving. + } else { + auto& file = *file_or_error.value(); + m_document->set_text(file.read_all()); + } + } + return *m_document; +} + +int ProjectFile::vertical_scroll_value() const +{ + return m_vertical_scroll_value; +} + +void ProjectFile::vertical_scroll_value(int vertical_scroll_value) +{ + m_vertical_scroll_value = vertical_scroll_value; +} + +int ProjectFile::horizontal_scroll_value() const +{ + return m_horizontal_scroll_value; +} + +void ProjectFile::horizontal_scroll_value(int horizontal_scroll_value) +{ + m_horizontal_scroll_value = horizontal_scroll_value; +} + +} diff --git a/Userland/DevTools/HackStudio/ProjectFile.h b/Userland/DevTools/HackStudio/ProjectFile.h new file mode 100644 index 0000000000..c679cd79e0 --- /dev/null +++ b/Userland/DevTools/HackStudio/ProjectFile.h @@ -0,0 +1,62 @@ +/* + * Copyright (c) 2018-2020, Andreas Kling <kling@serenityos.org> + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * 1. Redistributions of source code must retain the above copyright notice, this + * list of conditions and the following disclaimer. + * + * 2. Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE + * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL + * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR + * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER + * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, + * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE + * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +#pragma once + +#include "CodeDocument.h" +#include <AK/ByteBuffer.h> +#include <AK/NonnullRefPtr.h> +#include <AK/RefCounted.h> +#include <AK/String.h> + +namespace HackStudio { + +class ProjectFile : public RefCounted<ProjectFile> { +public: + static NonnullRefPtr<ProjectFile> construct_with_name(const String& name) + { + return adopt(*new ProjectFile(name)); + } + + const String& name() const { return m_name; } + + GUI::TextDocument& document() const; + + int vertical_scroll_value() const; + void vertical_scroll_value(int); + int horizontal_scroll_value() const; + void horizontal_scroll_value(int); + +private: + explicit ProjectFile(const String& name); + + String m_name; + mutable RefPtr<CodeDocument> m_document; + int m_vertical_scroll_value { 0 }; + int m_horizontal_scroll_value { 0 }; +}; + +} diff --git a/Userland/DevTools/HackStudio/TerminalWrapper.cpp b/Userland/DevTools/HackStudio/TerminalWrapper.cpp new file mode 100644 index 0000000000..ef7c220c42 --- /dev/null +++ b/Userland/DevTools/HackStudio/TerminalWrapper.cpp @@ -0,0 +1,186 @@ +/* + * Copyright (c) 2018-2020, Andreas Kling <kling@serenityos.org> + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * 1. Redistributions of source code must retain the above copyright notice, this + * list of conditions and the following disclaimer. + * + * 2. Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE + * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL + * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR + * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER + * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, + * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE + * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +#include "TerminalWrapper.h" +#include <AK/String.h> +#include <LibCore/ConfigFile.h> +#include <LibGUI/BoxLayout.h> +#include <LibGUI/MessageBox.h> +#include <LibVT/TerminalWidget.h> +#include <fcntl.h> +#include <signal.h> +#include <stdio.h> +#include <stdlib.h> +#include <string.h> +#include <sys/ioctl.h> +#include <sys/types.h> +#include <sys/wait.h> +#include <unistd.h> + +namespace HackStudio { + +void TerminalWrapper::run_command(const String& command) +{ + if (m_pid != -1) { + GUI::MessageBox::show(window(), + "A command is already running in this TerminalWrapper", + "Can't run command", + GUI::MessageBox::Type::Error); + return; + } + + int ptm_fd = posix_openpt(O_RDWR | O_CLOEXEC); + if (ptm_fd < 0) { + perror("posix_openpt"); + ASSERT_NOT_REACHED(); + } + if (grantpt(ptm_fd) < 0) { + perror("grantpt"); + ASSERT_NOT_REACHED(); + } + if (unlockpt(ptm_fd) < 0) { + perror("unlockpt"); + ASSERT_NOT_REACHED(); + } + + m_terminal_widget->set_pty_master_fd(ptm_fd); + m_terminal_widget->on_command_exit = [this] { + int wstatus; + int rc = waitpid(m_pid, &wstatus, 0); + if (rc < 0) { + perror("waitpid"); + ASSERT_NOT_REACHED(); + } + if (WIFEXITED(wstatus)) { + m_terminal_widget->inject_string(String::formatted("\033[{};1m(Command exited with code {})\033[0m\n", wstatus == 0 ? 32 : 31, WEXITSTATUS(wstatus))); + } else if (WIFSTOPPED(wstatus)) { + m_terminal_widget->inject_string("\033[34;1m(Command stopped!)\033[0m\n"); + } else if (WIFSIGNALED(wstatus)) { + m_terminal_widget->inject_string(String::formatted("\033[34;1m(Command signaled with {}!)\033[0m\n", strsignal(WTERMSIG(wstatus)))); + } + m_pid = -1; + + if (on_command_exit) + on_command_exit(); + }; + + m_pid = fork(); + if (m_pid == 0) { + // Create a new process group. + setsid(); + + const char* tty_name = ptsname(ptm_fd); + if (!tty_name) { + perror("ptsname"); + exit(1); + } + close(ptm_fd); + int pts_fd = open(tty_name, O_RDWR); + if (pts_fd < 0) { + perror("open"); + exit(1); + } + + tcsetpgrp(pts_fd, getpid()); + + // NOTE: It's okay if this fails. + int rc = ioctl(0, TIOCNOTTY); + + close(0); + close(1); + close(2); + + rc = dup2(pts_fd, 0); + if (rc < 0) { + perror("dup2"); + exit(1); + } + rc = dup2(pts_fd, 1); + if (rc < 0) { + perror("dup2"); + exit(1); + } + rc = dup2(pts_fd, 2); + if (rc < 0) { + perror("dup2"); + exit(1); + } + rc = close(pts_fd); + if (rc < 0) { + perror("close"); + exit(1); + } + rc = ioctl(0, TIOCSCTTY); + if (rc < 0) { + perror("ioctl(TIOCSCTTY)"); + exit(1); + } + + setenv("TERM", "xterm", true); + + auto parts = command.split(' '); + ASSERT(!parts.is_empty()); + const char** args = (const char**)calloc(parts.size() + 1, sizeof(const char*)); + for (size_t i = 0; i < parts.size(); i++) { + args[i] = parts[i].characters(); + } + rc = execvp(args[0], const_cast<char**>(args)); + if (rc < 0) { + perror("execve"); + exit(1); + } + ASSERT_NOT_REACHED(); + } + + // (In parent process) + terminal().scroll_to_bottom(); +} + +void TerminalWrapper::kill_running_command() +{ + ASSERT(m_pid != -1); + + // Kill our child process and its whole process group. + [[maybe_unused]] auto rc = killpg(m_pid, SIGTERM); +} + +TerminalWrapper::TerminalWrapper(bool user_spawned) + : m_user_spawned(user_spawned) +{ + set_layout<GUI::VerticalBoxLayout>(); + + RefPtr<Core::ConfigFile> config = Core::ConfigFile::get_for_app("Terminal"); + m_terminal_widget = add<TerminalWidget>(-1, false, config); + + if (user_spawned) + run_command("Shell"); +} + +TerminalWrapper::~TerminalWrapper() +{ +} + +} diff --git a/Userland/DevTools/HackStudio/TerminalWrapper.h b/Userland/DevTools/HackStudio/TerminalWrapper.h new file mode 100644 index 0000000000..9b5a8ed0a3 --- /dev/null +++ b/Userland/DevTools/HackStudio/TerminalWrapper.h @@ -0,0 +1,56 @@ +/* + * Copyright (c) 2018-2020, Andreas Kling <kling@serenityos.org> + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * 1. Redistributions of source code must retain the above copyright notice, this + * list of conditions and the following disclaimer. + * + * 2. Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE + * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL + * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR + * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER + * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, + * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE + * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +#pragma once + +#include <LibGUI/Widget.h> + +class TerminalWidget; + +namespace HackStudio { + +class TerminalWrapper final : public GUI::Widget { + C_OBJECT(TerminalWrapper) +public: + virtual ~TerminalWrapper() override; + + void run_command(const String&); + void kill_running_command(); + + bool user_spawned() const { return m_user_spawned; } + TerminalWidget& terminal() { return *m_terminal_widget; } + + Function<void()> on_command_exit; + +private: + explicit TerminalWrapper(bool user_spawned = true); + + RefPtr<TerminalWidget> m_terminal_widget; + pid_t m_pid { -1 }; + bool m_user_spawned { true }; +}; + +} diff --git a/Userland/DevTools/HackStudio/Tool.h b/Userland/DevTools/HackStudio/Tool.h new file mode 100644 index 0000000000..63c9960a9d --- /dev/null +++ b/Userland/DevTools/HackStudio/Tool.h @@ -0,0 +1,63 @@ +/* + * Copyright (c) 2018-2020, Andreas Kling <kling@serenityos.org> + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * 1. Redistributions of source code must retain the above copyright notice, this + * list of conditions and the following disclaimer. + * + * 2. Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE + * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL + * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR + * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER + * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, + * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE + * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +#pragma once + +#include <AK/Noncopyable.h> +#include <LibGUI/Forward.h> + +namespace HackStudio { + +class FormEditorWidget; + +class Tool { + AK_MAKE_NONCOPYABLE(Tool); + AK_MAKE_NONMOVABLE(Tool); + +public: + virtual ~Tool() { } + + virtual void on_mousedown(GUI::MouseEvent&) = 0; + virtual void on_mouseup(GUI::MouseEvent&) = 0; + virtual void on_mousemove(GUI::MouseEvent&) = 0; + virtual void on_keydown(GUI::KeyEvent&) = 0; + virtual void on_second_paint(GUI::Painter&, GUI::PaintEvent&) { } + + virtual const char* class_name() const = 0; + + virtual void attach() { } + virtual void detach() { } + +protected: + explicit Tool(FormEditorWidget& editor) + : m_editor(editor) + { + } + + FormEditorWidget& m_editor; +}; + +} diff --git a/Userland/DevTools/HackStudio/WidgetTool.cpp b/Userland/DevTools/HackStudio/WidgetTool.cpp new file mode 100644 index 0000000000..73626ebbf8 --- /dev/null +++ b/Userland/DevTools/HackStudio/WidgetTool.cpp @@ -0,0 +1,52 @@ +/* + * Copyright (c) 2018-2020, Andreas Kling <kling@serenityos.org> + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * 1. Redistributions of source code must retain the above copyright notice, this + * list of conditions and the following disclaimer. + * + * 2. Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE + * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL + * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR + * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER + * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, + * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE + * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +#include "WidgetTool.h" +#include <AK/LogStream.h> + +namespace HackStudio { + +void WidgetTool::on_mousedown([[maybe_unused]] GUI::MouseEvent& event) +{ + dbgln("WidgetTool::on_mousedown"); +} + +void WidgetTool::on_mouseup([[maybe_unused]] GUI::MouseEvent& event) +{ + dbgln("WidgetTool::on_mouseup"); +} + +void WidgetTool::on_mousemove([[maybe_unused]] GUI::MouseEvent& event) +{ + dbgln("WidgetTool::on_mousemove"); +} + +void WidgetTool::on_keydown([[maybe_unused]] GUI::KeyEvent& event) +{ + dbgln("WidgetTool::on_keydown"); +} + +} diff --git a/Userland/DevTools/HackStudio/WidgetTool.h b/Userland/DevTools/HackStudio/WidgetTool.h new file mode 100644 index 0000000000..dae5e805d3 --- /dev/null +++ b/Userland/DevTools/HackStudio/WidgetTool.h @@ -0,0 +1,52 @@ +/* + * Copyright (c) 2018-2020, Andreas Kling <kling@serenityos.org> + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * 1. Redistributions of source code must retain the above copyright notice, this + * list of conditions and the following disclaimer. + * + * 2. Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE + * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL + * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR + * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER + * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, + * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE + * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +#pragma once + +#include "Tool.h" + +namespace HackStudio { + +class WidgetTool final : public Tool { +public: + explicit WidgetTool(FormEditorWidget& editor, const GUI::WidgetClassRegistration& meta_class) + : Tool(editor) + , m_meta_class(meta_class) + { + } + virtual ~WidgetTool() override { } + +private: + virtual const char* class_name() const override { return "WidgetTool"; } + virtual void on_mousedown(GUI::MouseEvent&) override; + virtual void on_mouseup(GUI::MouseEvent&) override; + virtual void on_mousemove(GUI::MouseEvent&) override; + virtual void on_keydown(GUI::KeyEvent&) override; + + const GUI::WidgetClassRegistration& m_meta_class; +}; + +} diff --git a/Userland/DevTools/HackStudio/WidgetTreeModel.cpp b/Userland/DevTools/HackStudio/WidgetTreeModel.cpp new file mode 100644 index 0000000000..c0f8b403dc --- /dev/null +++ b/Userland/DevTools/HackStudio/WidgetTreeModel.cpp @@ -0,0 +1,117 @@ +/* + * Copyright (c) 2018-2020, Andreas Kling <kling@serenityos.org> + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * 1. Redistributions of source code must retain the above copyright notice, this + * list of conditions and the following disclaimer. + * + * 2. Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE + * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL + * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR + * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER + * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, + * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE + * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +#include "WidgetTreeModel.h" +#include <AK/StringBuilder.h> +#include <LibGUI/Widget.h> + +namespace HackStudio { + +WidgetTreeModel::WidgetTreeModel(GUI::Widget& root) + : m_root(root) +{ + m_widget_icon.set_bitmap_for_size(16, Gfx::Bitmap::load_from_file("/res/icons/16x16/inspector-object.png")); +} + +WidgetTreeModel::~WidgetTreeModel() +{ +} + +GUI::ModelIndex WidgetTreeModel::index(int row, int column, const GUI::ModelIndex& parent) const +{ + if (!parent.is_valid()) { + return create_index(row, column, m_root.ptr()); + } + auto& parent_node = *static_cast<GUI::Widget*>(parent.internal_data()); + return create_index(row, column, parent_node.child_widgets().at(row)); +} + +GUI::ModelIndex WidgetTreeModel::parent_index(const GUI::ModelIndex& index) const +{ + if (!index.is_valid()) + return {}; + auto& widget = *static_cast<GUI::Widget*>(index.internal_data()); + if (&widget == m_root.ptr()) + return {}; + + if (widget.parent_widget() == m_root.ptr()) + return create_index(0, 0, m_root.ptr()); + + // Walk the grandparent's children to find the index of widget's parent in its parent. + // (This is needed to produce the row number of the GUI::ModelIndex corresponding to widget's parent.) + int grandparent_child_index = 0; + for (auto& grandparent_child : widget.parent_widget()->parent_widget()->child_widgets()) { + if (grandparent_child == widget.parent_widget()) + return create_index(grandparent_child_index, 0, widget.parent_widget()); + ++grandparent_child_index; + } + + ASSERT_NOT_REACHED(); + return {}; +} + +int WidgetTreeModel::row_count(const GUI::ModelIndex& index) const +{ + if (!index.is_valid()) + return 1; + auto& widget = *static_cast<GUI::Widget*>(index.internal_data()); + return widget.child_widgets().size(); +} + +int WidgetTreeModel::column_count(const GUI::ModelIndex&) const +{ + return 1; +} + +GUI::Variant WidgetTreeModel::data(const GUI::ModelIndex& index, GUI::ModelRole role) const +{ + auto* widget = static_cast<GUI::Widget*>(index.internal_data()); + if (role == GUI::ModelRole::Icon) { + return m_widget_icon; + } + if (role == GUI::ModelRole::Display) { + return String::formatted("{} ({})", widget->class_name(), widget->relative_rect()); + } + return {}; +} + +void WidgetTreeModel::update() +{ + did_update(); +} + +GUI::ModelIndex WidgetTreeModel::index_for_widget(GUI::Widget& widget) const +{ + int parent_child_index = 0; + for (auto& parent_child : widget.parent_widget()->child_widgets()) { + if (parent_child == &widget) + return create_index(parent_child_index, 0, &widget); + ++parent_child_index; + } + return {}; +} + +} diff --git a/Userland/DevTools/HackStudio/WidgetTreeModel.h b/Userland/DevTools/HackStudio/WidgetTreeModel.h new file mode 100644 index 0000000000..cfffbd6add --- /dev/null +++ b/Userland/DevTools/HackStudio/WidgetTreeModel.h @@ -0,0 +1,55 @@ +/* + * Copyright (c) 2018-2020, Andreas Kling <kling@serenityos.org> + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * 1. Redistributions of source code must retain the above copyright notice, this + * list of conditions and the following disclaimer. + * + * 2. Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE + * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL + * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR + * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER + * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, + * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE + * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +#pragma once + +#include <LibGUI/Model.h> +#include <LibGUI/Painter.h> + +namespace HackStudio { + +class WidgetTreeModel final : public GUI::Model { +public: + static NonnullRefPtr<WidgetTreeModel> create(GUI::Widget& root) { return adopt(*new WidgetTreeModel(root)); } + virtual ~WidgetTreeModel() override; + + virtual int row_count(const GUI::ModelIndex& = GUI::ModelIndex()) const override; + virtual int column_count(const GUI::ModelIndex& = GUI::ModelIndex()) const override; + virtual GUI::Variant data(const GUI::ModelIndex&, GUI::ModelRole) const override; + virtual GUI::ModelIndex index(int row, int column, const GUI::ModelIndex& parent = GUI::ModelIndex()) const override; + virtual GUI::ModelIndex parent_index(const GUI::ModelIndex&) const override; + virtual void update() override; + + GUI::ModelIndex index_for_widget(GUI::Widget&) const; + +private: + explicit WidgetTreeModel(GUI::Widget&); + + NonnullRefPtr<GUI::Widget> m_root; + GUI::Icon m_widget_icon; +}; + +} diff --git a/Userland/DevTools/HackStudio/main.cpp b/Userland/DevTools/HackStudio/main.cpp new file mode 100644 index 0000000000..3ca8b1a8a0 --- /dev/null +++ b/Userland/DevTools/HackStudio/main.cpp @@ -0,0 +1,171 @@ +/* + * Copyright (c) 2018-2020, Andreas Kling <kling@serenityos.org> + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * 1. Redistributions of source code must retain the above copyright notice, this + * list of conditions and the following disclaimer. + * + * 2. Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE + * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL + * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR + * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER + * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, + * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE + * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +#include "HackStudio.h" +#include "HackStudioWidget.h" +#include "Project.h" +#include <AK/StringBuilder.h> +#include <LibCore/ArgsParser.h> +#include <LibCore/Event.h> +#include <LibCore/EventLoop.h> +#include <LibCore/File.h> +#include <LibGUI/Application.h> +#include <LibGUI/MenuBar.h> +#include <LibGUI/MessageBox.h> +#include <LibGUI/Widget.h> +#include <LibGUI/Window.h> +#include <LibThread/Lock.h> +#include <LibThread/Thread.h> +#include <LibVT/TerminalWidget.h> +#include <fcntl.h> +#include <spawn.h> +#include <stdio.h> +#include <sys/types.h> +#include <sys/wait.h> +#include <unistd.h> + +using namespace HackStudio; + +static RefPtr<GUI::Window> s_window; +static RefPtr<HackStudioWidget> s_hack_studio_widget; + +static bool make_is_available(); +static void update_path_environment_variable(); + +int main(int argc, char** argv) +{ + if (pledge("stdio tty accept rpath cpath wpath shared_buffer proc exec unix fattr thread unix sendfd ptrace", nullptr) < 0) { + perror("pledge"); + return 1; + } + + auto app = GUI::Application::construct(argc, argv); + + if (pledge("stdio tty accept rpath cpath wpath shared_buffer proc exec fattr thread unix sendfd ptrace", nullptr) < 0) { + perror("pledge"); + return 1; + } + + s_window = GUI::Window::construct(); + s_window->resize(840, 600); + s_window->set_icon(Gfx::Bitmap::load_from_file("/res/icons/16x16/app-hack-studio.png")); + + update_path_environment_variable(); + + if (!make_is_available()) + GUI::MessageBox::show(s_window, "The 'make' command is not available. You probably want to install the binutils, gcc, and make ports from the root of the Serenity repository.", "Error", GUI::MessageBox::Type::Error); + + const char* path_argument = nullptr; + Core::ArgsParser args_parser; + args_parser.add_positional_argument(path_argument, "Path to a workspace or a file", "path", Core::ArgsParser::Required::No); + args_parser.parse(argc, argv); + + auto argument_absolute_path = Core::File::real_path_for(path_argument); + + auto project_path = argument_absolute_path; + if (argument_absolute_path.is_null()) + project_path = Core::File::real_path_for("."); + + s_hack_studio_widget = s_window->set_main_widget<HackStudioWidget>(project_path); + + s_window->set_title(String::formatted("{} - Hack Studio", s_hack_studio_widget->project().name())); + + auto menubar = GUI::MenuBar::construct(); + s_hack_studio_widget->initialize_menubar(menubar); + app->set_menubar(menubar); + + s_window->show(); + + s_hack_studio_widget->update_actions(); + + return app->exec(); +} + +static bool make_is_available() +{ + pid_t pid; + const char* argv[] = { "make", "--version", nullptr }; + posix_spawn_file_actions_t action; + posix_spawn_file_actions_init(&action); + posix_spawn_file_actions_addopen(&action, STDOUT_FILENO, "/dev/null", O_WRONLY, 0); + + if ((errno = posix_spawnp(&pid, "make", &action, nullptr, const_cast<char**>(argv), environ))) { + perror("posix_spawn"); + return false; + } + int wstatus; + waitpid(pid, &wstatus, 0); + posix_spawn_file_actions_destroy(&action); + return WEXITSTATUS(wstatus) == 0; +} + +static void update_path_environment_variable() +{ + StringBuilder path; + path.append(getenv("PATH")); + if (path.length()) + path.append(":"); + path.append("/bin:/usr/bin:/usr/local/bin"); + setenv("PATH", path.to_string().characters(), true); +} + +namespace HackStudio { + +GUI::TextEditor& current_editor() +{ + return s_hack_studio_widget->current_editor(); +} + +void open_file(const String& file_name) +{ + return s_hack_studio_widget->open_file(file_name); +} + +RefPtr<EditorWrapper> current_editor_wrapper() +{ + if (!s_hack_studio_widget) + return nullptr; + return s_hack_studio_widget->current_editor_wrapper(); +} + +Project& project() +{ + return s_hack_studio_widget->project(); +} + +String currently_open_file() +{ + if (!s_hack_studio_widget) + return {}; + return s_hack_studio_widget->currently_open_file(); +} + +void set_current_editor_wrapper(RefPtr<EditorWrapper> wrapper) +{ + s_hack_studio_widget->set_current_editor_wrapper(wrapper); +} + +} diff --git a/Userland/DevTools/IPCCompiler/CMakeLists.txt b/Userland/DevTools/IPCCompiler/CMakeLists.txt new file mode 100644 index 0000000000..6969cef777 --- /dev/null +++ b/Userland/DevTools/IPCCompiler/CMakeLists.txt @@ -0,0 +1,6 @@ +set(SOURCES + main.cpp +) + +add_executable(IPCCompiler ${SOURCES}) +target_link_libraries(IPCCompiler LagomCore) diff --git a/Userland/DevTools/IPCCompiler/main.cpp b/Userland/DevTools/IPCCompiler/main.cpp new file mode 100644 index 0000000000..6b3ef51a50 --- /dev/null +++ b/Userland/DevTools/IPCCompiler/main.cpp @@ -0,0 +1,613 @@ +/* + * Copyright (c) 2018-2020, Andreas Kling <kling@serenityos.org> + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * 1. Redistributions of source code must retain the above copyright notice, this + * list of conditions and the following disclaimer. + * + * 2. Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE + * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL + * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR + * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER + * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, + * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE + * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +#include <AK/Function.h> +#include <AK/GenericLexer.h> +#include <AK/HashMap.h> +#include <AK/SourceGenerator.h> +#include <AK/StringBuilder.h> +#include <LibCore/File.h> +#include <ctype.h> +#include <stdio.h> + +//#define GENERATE_DEBUG_CODE + +struct Parameter { + Vector<String> attributes; + String type; + String name; +}; + +struct Message { + String name; + bool is_synchronous { false }; + Vector<Parameter> inputs; + Vector<Parameter> outputs; + + String response_name() const + { + StringBuilder builder; + builder.append(name); + builder.append("Response"); + return builder.to_string(); + } +}; + +struct Endpoint { + String name; + int magic; + Vector<Message> messages; +}; + +int main(int argc, char** argv) +{ + if (argc != 2) { + outln("usage: {} <IPC endpoint definition file>", argv[0]); + return 0; + } + + auto file = Core::File::construct(argv[1]); + if (!file->open(Core::IODevice::ReadOnly)) { + warnln("Error: Cannot open {}: {}", argv[1], file->error_string()); + return 1; + } + + auto file_contents = file->read_all(); + GenericLexer lexer(file_contents); + + Vector<Endpoint> endpoints; + + auto assert_specific = [&](char ch) { + if (lexer.peek() != ch) + warnln("assert_specific: wanted '{}', but got '{}' at index {}", ch, lexer.peek(), lexer.tell()); + bool saw_expected = lexer.consume_specific(ch); + ASSERT(saw_expected); + }; + + auto consume_whitespace = [&] { + lexer.ignore_while([](char ch) { return isspace(ch); }); + if (lexer.peek() == '/' && lexer.peek(1) == '/') + lexer.ignore_until([](char ch) { return ch == '\n'; }); + }; + + auto parse_parameter = [&](Vector<Parameter>& storage) { + for (;;) { + Parameter parameter; + consume_whitespace(); + if (lexer.peek() == ')') + break; + if (lexer.consume_specific('[')) { + for (;;) { + if (lexer.consume_specific(']')) { + consume_whitespace(); + break; + } + if (lexer.consume_specific(',')) { + consume_whitespace(); + } + auto attribute = lexer.consume_until([](char ch) { return ch == ']' || ch == ','; }); + parameter.attributes.append(attribute); + consume_whitespace(); + } + } + parameter.type = lexer.consume_until([](char ch) { return isspace(ch); }); + consume_whitespace(); + parameter.name = lexer.consume_until([](char ch) { return isspace(ch) || ch == ',' || ch == ')'; }); + consume_whitespace(); + storage.append(move(parameter)); + if (lexer.consume_specific(',')) + continue; + if (lexer.peek() == ')') + break; + } + }; + + auto parse_parameters = [&](Vector<Parameter>& storage) { + for (;;) { + consume_whitespace(); + parse_parameter(storage); + consume_whitespace(); + if (lexer.consume_specific(',')) + continue; + if (lexer.peek() == ')') + break; + } + }; + + auto parse_message = [&] { + Message message; + consume_whitespace(); + message.name = lexer.consume_until([](char ch) { return isspace(ch) || ch == '('; }); + consume_whitespace(); + assert_specific('('); + parse_parameters(message.inputs); + assert_specific(')'); + consume_whitespace(); + assert_specific('='); + + auto type = lexer.consume(); + if (type == '>') + message.is_synchronous = true; + else if (type == '|') + message.is_synchronous = false; + else + ASSERT_NOT_REACHED(); + + consume_whitespace(); + + if (message.is_synchronous) { + assert_specific('('); + parse_parameters(message.outputs); + assert_specific(')'); + } + + consume_whitespace(); + + endpoints.last().messages.append(move(message)); + }; + + auto parse_messages = [&] { + for (;;) { + consume_whitespace(); + parse_message(); + consume_whitespace(); + if (lexer.peek() == '}') + break; + } + }; + + auto parse_endpoint = [&] { + endpoints.empend(); + consume_whitespace(); + lexer.consume_specific("endpoint"); + consume_whitespace(); + endpoints.last().name = lexer.consume_while([](char ch) { return !isspace(ch); }); + consume_whitespace(); + assert_specific('='); + consume_whitespace(); + auto magic_string = lexer.consume_while([](char ch) { return !isspace(ch) && ch != '{'; }); + endpoints.last().magic = magic_string.to_int().value(); + consume_whitespace(); + assert_specific('{'); + parse_messages(); + assert_specific('}'); + consume_whitespace(); + }; + + while (lexer.tell() < file_contents.size()) + parse_endpoint(); + + StringBuilder builder; + SourceGenerator generator { builder }; + + generator.append(R"~~~( +#pragma once +#include <AK/MemoryStream.h> +#include <AK/OwnPtr.h> +#include <AK/URL.h> +#include <AK/Utf8View.h> +#include <LibGfx/Color.h> +#include <LibGfx/Rect.h> +#include <LibGfx/ShareableBitmap.h> +#include <LibIPC/Decoder.h> +#include <LibIPC/Dictionary.h> +#include <LibIPC/Encoder.h> +#include <LibIPC/Endpoint.h> +#include <LibIPC/File.h> +#include <LibIPC/Message.h> +)~~~"); + + for (auto& endpoint : endpoints) { + auto endpoint_generator = generator.fork(); + + endpoint_generator.set("endpoint.name", endpoint.name); + endpoint_generator.set("endpoint.magic", String::number(endpoint.magic)); + + endpoint_generator.append(R"~~~( +namespace Messages::@endpoint.name@ { +)~~~"); + + HashMap<String, int> message_ids; + + endpoint_generator.append(R"~~~( +enum class MessageID : i32 { +)~~~"); + for (auto& message : endpoint.messages) { + auto message_generator = endpoint_generator.fork(); + + message_ids.set(message.name, message_ids.size() + 1); + message_generator.set("message.name", message.name); + message_generator.set("message.id", String::number(message_ids.size())); + + message_generator.append(R"~~~( + @message.name@ = @message.id@, +)~~~"); + if (message.is_synchronous) { + message_ids.set(message.response_name(), message_ids.size() + 1); + message_generator.set("message.name", message.response_name()); + message_generator.set("message.id", String::number(message_ids.size())); + + message_generator.append(R"~~~( + @message.name@ = @message.id@, +)~~~"); + } + } + endpoint_generator.append(R"~~~( +}; +)~~~"); + + auto constructor_for_message = [&](const String& name, const Vector<Parameter>& parameters) { + StringBuilder builder; + builder.append(name); + + if (parameters.is_empty()) { + builder.append("() {}"); + return builder.to_string(); + } + + builder.append('('); + for (size_t i = 0; i < parameters.size(); ++i) { + auto& parameter = parameters[i]; + builder.append("const "); + builder.append(parameter.type); + builder.append("& "); + builder.append(parameter.name); + if (i != parameters.size() - 1) + builder.append(", "); + } + builder.append(") : "); + for (size_t i = 0; i < parameters.size(); ++i) { + auto& parameter = parameters[i]; + builder.append("m_"); + builder.append(parameter.name); + builder.append("("); + builder.append(parameter.name); + builder.append(")"); + if (i != parameters.size() - 1) + builder.append(", "); + } + builder.append(" {}"); + return builder.to_string(); + }; + + auto do_message = [&](const String& name, const Vector<Parameter>& parameters, const String& response_type = {}) { + auto message_generator = endpoint_generator.fork(); + message_generator.set("message.name", name); + message_generator.set("message.response_type", response_type); + message_generator.set("message.constructor", constructor_for_message(name, parameters)); + + message_generator.append(R"~~~( +class @message.name@ final : public IPC::Message { +public: +)~~~"); + + if (!response_type.is_null()) + message_generator.append(R"~~~( + typedef class @message.response_type@ ResponseType; +)~~~"); + + message_generator.append(R"~~~( + @message.constructor@ + virtual ~@message.name@() override {} + + virtual i32 endpoint_magic() const override { return @endpoint.magic@; } + virtual i32 message_id() const override { return (int)MessageID::@message.name@; } + static i32 static_message_id() { return (int)MessageID::@message.name@; } + virtual const char* message_name() const override { return "@endpoint.name@::@message.name@"; } + + static OwnPtr<@message.name@> decode(InputMemoryStream& stream, int sockfd) + { + IPC::Decoder decoder { stream, sockfd }; +)~~~"); + + for (auto& parameter : parameters) { + auto parameter_generator = message_generator.fork(); + + parameter_generator.set("parameter.type", parameter.type); + parameter_generator.set("parameter.name", parameter.name); + + if (parameter.type == "bool") + parameter_generator.set("parameter.initial_value", "false"); + else + parameter_generator.set("parameter.initial_value", "{}"); + + parameter_generator.append(R"~~~( + @parameter.type@ @parameter.name@ = @parameter.initial_value@; + if (!decoder.decode(@parameter.name@)) + return {}; +)~~~"); + + if (parameter.attributes.contains_slow("UTF8")) { + parameter_generator.append(R"~~~( + if (!Utf8View(@parameter.name@).validate()) + return {}; +)~~~"); + } + } + + StringBuilder builder; + for (size_t i = 0; i < parameters.size(); ++i) { + auto& parameter = parameters[i]; + builder.append(parameter.name); + if (i != parameters.size() - 1) + builder.append(", "); + } + + message_generator.set("message.constructor_call_parameters", builder.build()); + + message_generator.append(R"~~~( + return make<@message.name@>(@message.constructor_call_parameters@); + } +)~~~"); + + message_generator.append(R"~~~( + virtual IPC::MessageBuffer encode() const override + { + IPC::MessageBuffer buffer; + IPC::Encoder stream(buffer); + stream << endpoint_magic(); + stream << (int)MessageID::@message.name@; +)~~~"); + + for (auto& parameter : parameters) { + auto parameter_generator = message_generator.fork(); + + parameter_generator.set("parameter.name", parameter.name); + parameter_generator.append(R"~~~( + stream << m_@parameter.name@; +)~~~"); + } + + message_generator.append(R"~~~( + return buffer; + } +)~~~"); + + for (auto& parameter : parameters) { + auto parameter_generator = message_generator.fork(); + parameter_generator.set("parameter.type", parameter.type); + parameter_generator.set("parameter.name", parameter.name); + parameter_generator.append(R"~~~( + const @parameter.type@& @parameter.name@() const { return m_@parameter.name@; } +)~~~"); + } + + message_generator.append(R"~~~( +private: + )~~~"); + + for (auto& parameter : parameters) { + auto parameter_generator = message_generator.fork(); + parameter_generator.set("parameter.type", parameter.type); + parameter_generator.set("parameter.name", parameter.name); + parameter_generator.append(R"~~~( + @parameter.type@ m_@parameter.name@; +)~~~"); + } + + message_generator.append(R"~~~( +}; + )~~~"); + }; + for (auto& message : endpoint.messages) { + String response_name; + if (message.is_synchronous) { + response_name = message.response_name(); + do_message(response_name, message.outputs); + } + do_message(message.name, message.inputs, response_name); + } + + endpoint_generator.append(R"~~~( +} // namespace Messages::@endpoint.name@ + )~~~"); + + endpoint_generator.append(R"~~~( +class @endpoint.name@Endpoint : public IPC::Endpoint { +public: + @endpoint.name@Endpoint() { } + virtual ~@endpoint.name@Endpoint() override { } + + static int static_magic() { return @endpoint.magic@; } + virtual int magic() const override { return @endpoint.magic@; } + static String static_name() { return "@endpoint.name@"; } + virtual String name() const override { return "@endpoint.name@"; } + + static OwnPtr<IPC::Message> decode_message(ReadonlyBytes buffer, int sockfd) + { + InputMemoryStream stream { buffer }; + i32 message_endpoint_magic = 0; + stream >> message_endpoint_magic; + if (stream.handle_any_error()) { +)~~~"); +#ifdef GENERATE_DEBUG_CODE + endpoint_generator.append(R"~~~( + dbgln("Failed to read message endpoint magic"); +)~~~"); +#endif + endpoint_generator.append(R"~~~( + return {}; + } + + if (message_endpoint_magic != @endpoint.magic@) { +)~~~"); +#ifdef GENERATE_DEBUG_CODE + endpoint_generator.append(R"~~~( + dbgln("Endpoint magic number message_endpoint_magic != @endpoint.magic@"); +)~~~"); +#endif + endpoint_generator.append(R"~~~( + return {}; + } + + i32 message_id = 0; + stream >> message_id; + if (stream.handle_any_error()) { +)~~~"); +#ifdef GENERATE_DEBUG_CODE + endpoint_generator.append(R"~~~( + dbgln("Failed to read message ID"); +)~~~"); +#endif + endpoint_generator.append(R"~~~( + return {}; + } + + OwnPtr<IPC::Message> message; + switch (message_id) { +)~~~"); + + for (auto& message : endpoint.messages) { + auto do_decode_message = [&](const String& name) { + auto message_generator = endpoint_generator.fork(); + + message_generator.set("message.name", name); + + message_generator.append(R"~~~( + case (int)Messages::@endpoint.name@::MessageID::@message.name@: + message = Messages::@endpoint.name@::@message.name@::decode(stream, sockfd); + break; +)~~~"); + }; + + do_decode_message(message.name); + if (message.is_synchronous) + do_decode_message(message.response_name()); + } + + endpoint_generator.append(R"~~~( + default: +)~~~"); +#ifdef GENERATE_DEBUG_CODE + endpoint_generator.append(R"~~~( + dbgln("Failed to decode @endpoint.name@.({})", message_id); +)~~~"); +#endif + endpoint_generator.append(R"~~~( + return {}; + } + + if (stream.handle_any_error()) { +)~~~"); +#ifdef GENERATE_DEBUG_CODE + endpoint_generator.append(R"~~~( + dbgln("Failed to read the message"); +)~~~"); +#endif + endpoint_generator.append(R"~~~( + return {}; + } + + return message; + } + + virtual OwnPtr<IPC::Message> handle(const IPC::Message& message) override + { + switch (message.message_id()) { +)~~~"); + for (auto& message : endpoint.messages) { + auto do_decode_message = [&](const String& name, bool returns_something) { + auto message_generator = endpoint_generator.fork(); + + message_generator.set("message.name", name); + message_generator.append(R"~~~( + case (int)Messages::@endpoint.name@::MessageID::@message.name@: +)~~~"); + if (returns_something) { + message_generator.append(R"~~~( + return handle(static_cast<const Messages::@endpoint.name@::@message.name@&>(message)); +)~~~"); + } else { + message_generator.append(R"~~~( + handle(static_cast<const Messages::@endpoint.name@::@message.name@&>(message)); + return {}; +)~~~"); + } + }; + do_decode_message(message.name, message.is_synchronous); + if (message.is_synchronous) + do_decode_message(message.response_name(), false); + } + endpoint_generator.append(R"~~~( + default: + return {}; + } + } +)~~~"); + + for (auto& message : endpoint.messages) { + auto message_generator = endpoint_generator.fork(); + + message_generator.set("message.name", message.name); + + String return_type = "void"; + if (message.is_synchronous) { + StringBuilder builder; + builder.append("OwnPtr<Messages::"); + builder.append(endpoint.name); + builder.append("::"); + builder.append(message.name); + builder.append("Response"); + builder.append(">"); + return_type = builder.to_string(); + } + message_generator.set("message.complex_return_type", return_type); + + message_generator.append(R"~~~( + virtual @message.complex_return_type@ handle(const Messages::@endpoint.name@::@message.name@&) = 0; +)~~~"); + } + + endpoint_generator.append(R"~~~( +private: +}; +)~~~"); + } + + outln("{}", generator.as_string_view()); + +#ifdef DEBUG + for (auto& endpoint : endpoints) { + warnln("Endpoint '{}' (magic: {})", endpoint.name, endpoint.magic); + for (auto& message : endpoint.messages) { + warnln(" Message: '{}'", message.name); + warnln(" Sync: {}", message.is_synchronous); + warnln(" Inputs:"); + for (auto& parameter : message.inputs) + warnln(" Parameter: {} ({})", parameter.name, parameter.type); + if (message.inputs.is_empty()) + warnln(" (none)"); + if (message.is_synchronous) { + warnln(" Outputs:"); + for (auto& parameter : message.outputs) + warnln(" Parameter: {} ({})", parameter.name, parameter.type); + if (message.outputs.is_empty()) + warnln(" (none)"); + } + } + } +#endif +} diff --git a/Userland/DevTools/Inspector/CMakeLists.txt b/Userland/DevTools/Inspector/CMakeLists.txt new file mode 100644 index 0000000000..dd980cec99 --- /dev/null +++ b/Userland/DevTools/Inspector/CMakeLists.txt @@ -0,0 +1,10 @@ +set(SOURCES + main.cpp + RemoteObject.cpp + RemoteObjectGraphModel.cpp + RemoteObjectPropertyModel.cpp + RemoteProcess.cpp +) + +serenity_app(Inspector ICON app-inspector) +target_link_libraries(Inspector LibGUI) diff --git a/Userland/DevTools/Inspector/RemoteObject.cpp b/Userland/DevTools/Inspector/RemoteObject.cpp new file mode 100644 index 0000000000..ce88259482 --- /dev/null +++ b/Userland/DevTools/Inspector/RemoteObject.cpp @@ -0,0 +1,43 @@ +/* + * Copyright (c) 2018-2020, Andreas Kling <kling@serenityos.org> + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * 1. Redistributions of source code must retain the above copyright notice, this + * list of conditions and the following disclaimer. + * + * 2. Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE + * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL + * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR + * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER + * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, + * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE + * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +#include "RemoteObject.h" +#include "RemoteObjectPropertyModel.h" + +namespace Inspector { + +RemoteObject::RemoteObject() + : m_property_model(RemoteObjectPropertyModel::create(*this)) +{ +} + +RemoteObjectPropertyModel& RemoteObject::property_model() +{ + m_property_model->update(); + return *m_property_model; +} + +} diff --git a/Userland/DevTools/Inspector/RemoteObject.h b/Userland/DevTools/Inspector/RemoteObject.h new file mode 100644 index 0000000000..1c231f2f01 --- /dev/null +++ b/Userland/DevTools/Inspector/RemoteObject.h @@ -0,0 +1,57 @@ +/* + * Copyright (c) 2018-2020, Andreas Kling <kling@serenityos.org> + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * 1. Redistributions of source code must retain the above copyright notice, this + * list of conditions and the following disclaimer. + * + * 2. Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE + * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL + * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR + * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER + * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, + * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE + * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +#pragma once + +#include <AK/JsonObject.h> +#include <AK/NonnullOwnPtrVector.h> +#include <AK/String.h> +#include <AK/Vector.h> + +namespace Inspector { + +class RemoteObjectPropertyModel; + +class RemoteObject { +public: + RemoteObject(); + + RemoteObjectPropertyModel& property_model(); + + RemoteObject* parent { nullptr }; + NonnullOwnPtrVector<RemoteObject> children; + + FlatPtr address { 0 }; + FlatPtr parent_address { 0 }; + String class_name; + String name; + + JsonObject json; + + NonnullRefPtr<RemoteObjectPropertyModel> m_property_model; +}; + +} diff --git a/Userland/DevTools/Inspector/RemoteObjectGraphModel.cpp b/Userland/DevTools/Inspector/RemoteObjectGraphModel.cpp new file mode 100644 index 0000000000..32caac30ec --- /dev/null +++ b/Userland/DevTools/Inspector/RemoteObjectGraphModel.cpp @@ -0,0 +1,124 @@ +/* + * Copyright (c) 2018-2020, Andreas Kling <kling@serenityos.org> + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * 1. Redistributions of source code must retain the above copyright notice, this + * list of conditions and the following disclaimer. + * + * 2. Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE + * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL + * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR + * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER + * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, + * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE + * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +#include "RemoteObjectGraphModel.h" +#include "RemoteObject.h" +#include "RemoteProcess.h" +#include <AK/JsonObject.h> +#include <AK/JsonValue.h> +#include <LibGUI/Application.h> +#include <stdio.h> + +namespace Inspector { + +RemoteObjectGraphModel::RemoteObjectGraphModel(RemoteProcess& process) + : m_process(process) +{ + m_object_icon.set_bitmap_for_size(16, Gfx::Bitmap::load_from_file("/res/icons/16x16/inspector-object.png")); + m_window_icon.set_bitmap_for_size(16, Gfx::Bitmap::load_from_file("/res/icons/16x16/window.png")); + m_layout_icon.set_bitmap_for_size(16, Gfx::Bitmap::load_from_file("/res/icons/16x16/layout.png")); + m_timer_icon.set_bitmap_for_size(16, Gfx::Bitmap::load_from_file("/res/icons/16x16/timer.png")); +} + +RemoteObjectGraphModel::~RemoteObjectGraphModel() +{ +} + +GUI::ModelIndex RemoteObjectGraphModel::index(int row, int column, const GUI::ModelIndex& parent) const +{ + if (!parent.is_valid()) { + if (m_process.roots().is_empty()) + return {}; + return create_index(row, column, &m_process.roots().at(row)); + } + auto& remote_parent = *static_cast<RemoteObject*>(parent.internal_data()); + return create_index(row, column, &remote_parent.children.at(row)); +} + +GUI::ModelIndex RemoteObjectGraphModel::parent_index(const GUI::ModelIndex& index) const +{ + if (!index.is_valid()) + return {}; + auto& remote_object = *static_cast<RemoteObject*>(index.internal_data()); + if (!remote_object.parent) + return {}; + + // NOTE: If the parent has no parent, it's a root, so we have to look among the remote roots. + if (!remote_object.parent->parent) { + for (size_t row = 0; row < m_process.roots().size(); ++row) { + if (&m_process.roots()[row] == remote_object.parent) + return create_index(row, 0, remote_object.parent); + } + ASSERT_NOT_REACHED(); + return {}; + } + + for (size_t row = 0; row < remote_object.parent->parent->children.size(); ++row) { + if (&remote_object.parent->parent->children[row] == remote_object.parent) + return create_index(row, 0, remote_object.parent); + } + + ASSERT_NOT_REACHED(); + return {}; +} + +int RemoteObjectGraphModel::row_count(const GUI::ModelIndex& index) const +{ + if (!index.is_valid()) + return m_process.roots().size(); + auto& remote_object = *static_cast<RemoteObject*>(index.internal_data()); + return remote_object.children.size(); +} + +int RemoteObjectGraphModel::column_count(const GUI::ModelIndex&) const +{ + return 1; +} + +GUI::Variant RemoteObjectGraphModel::data(const GUI::ModelIndex& index, GUI::ModelRole role) const +{ + auto* remote_object = static_cast<RemoteObject*>(index.internal_data()); + if (role == GUI::ModelRole::Icon) { + if (remote_object->class_name == "Window") + return m_window_icon; + if (remote_object->class_name == "Timer") + return m_timer_icon; + if (remote_object->class_name.ends_with("Layout")) + return m_layout_icon; + return m_object_icon; + } + if (role == GUI::ModelRole::Display) + return String::formatted("{}({:p})", remote_object->class_name, remote_object->address); + + return {}; +} + +void RemoteObjectGraphModel::update() +{ + did_update(); +} + +} diff --git a/Userland/DevTools/Inspector/RemoteObjectGraphModel.h b/Userland/DevTools/Inspector/RemoteObjectGraphModel.h new file mode 100644 index 0000000000..efcdab3cf4 --- /dev/null +++ b/Userland/DevTools/Inspector/RemoteObjectGraphModel.h @@ -0,0 +1,66 @@ +/* + * Copyright (c) 2018-2020, Andreas Kling <kling@serenityos.org> + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * 1. Redistributions of source code must retain the above copyright notice, this + * list of conditions and the following disclaimer. + * + * 2. Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE + * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL + * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR + * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER + * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, + * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE + * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +#pragma once + +#include <AK/JsonArray.h> +#include <AK/JsonObject.h> +#include <AK/NonnullOwnPtrVector.h> +#include <LibCore/LocalSocket.h> +#include <LibGUI/Model.h> + +namespace Inspector { + +class RemoteProcess; + +class RemoteObjectGraphModel final : public GUI::Model { +public: + static NonnullRefPtr<RemoteObjectGraphModel> create(RemoteProcess& process) + { + return adopt(*new RemoteObjectGraphModel(process)); + } + + virtual ~RemoteObjectGraphModel() override; + + virtual int row_count(const GUI::ModelIndex& = GUI::ModelIndex()) const override; + virtual int column_count(const GUI::ModelIndex& = GUI::ModelIndex()) const override; + virtual GUI::Variant data(const GUI::ModelIndex&, GUI::ModelRole) const override; + virtual GUI::ModelIndex index(int row, int column, const GUI::ModelIndex& parent = GUI::ModelIndex()) const override; + virtual GUI::ModelIndex parent_index(const GUI::ModelIndex&) const override; + virtual void update() override; + +private: + explicit RemoteObjectGraphModel(RemoteProcess&); + + RemoteProcess& m_process; + + GUI::Icon m_object_icon; + GUI::Icon m_window_icon; + GUI::Icon m_layout_icon; + GUI::Icon m_timer_icon; +}; + +} diff --git a/Userland/DevTools/Inspector/RemoteObjectPropertyModel.cpp b/Userland/DevTools/Inspector/RemoteObjectPropertyModel.cpp new file mode 100644 index 0000000000..6c9e82f3a7 --- /dev/null +++ b/Userland/DevTools/Inspector/RemoteObjectPropertyModel.cpp @@ -0,0 +1,241 @@ +/* + * Copyright (c) 2018-2020, Andreas Kling <kling@serenityos.org> + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * 1. Redistributions of source code must retain the above copyright notice, this + * list of conditions and the following disclaimer. + * + * 2. Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE + * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL + * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR + * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER + * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, + * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE + * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +#include "RemoteObjectPropertyModel.h" +#include "RemoteObject.h" +#include "RemoteProcess.h" + +namespace Inspector { + +RemoteObjectPropertyModel::RemoteObjectPropertyModel(RemoteObject& object) + : m_object(object) +{ +} + +int RemoteObjectPropertyModel::row_count(const GUI::ModelIndex& index) const +{ + Function<int(const JsonValue&)> do_count = [&](const JsonValue& value) { + if (value.is_array()) + return value.as_array().size(); + else if (value.is_object()) + return value.as_object().size(); + return 0; + }; + + if (index.is_valid()) { + auto* path = static_cast<const JsonPath*>(index.internal_data()); + return do_count(path->resolve(m_object.json)); + } else { + return do_count(m_object.json); + } +} + +String RemoteObjectPropertyModel::column_name(int column) const +{ + switch (column) { + case Column::Name: + return "Name"; + case Column::Value: + return "Value"; + } + ASSERT_NOT_REACHED(); +} + +GUI::Variant RemoteObjectPropertyModel::data(const GUI::ModelIndex& index, GUI::ModelRole role) const +{ + auto* path = static_cast<const JsonPath*>(index.internal_data()); + if (!path) + return {}; + + if (role == GUI::ModelRole::Display) { + switch (index.column()) { + case Column::Name: + return path->last().to_string(); + case Column::Value: { + auto data = path->resolve(m_object.json); + if (data.is_array()) + return String::formatted("<Array with {} element{}", data.as_array().size(), data.as_array().size() == 1 ? ">" : "s>"); + if (data.is_object()) + return String::formatted("<Object with {} entr{}", data.as_object().size(), data.as_object().size() == 1 ? "y>" : "ies>"); + return data; + } + } + } + return {}; +} + +void RemoteObjectPropertyModel::update() +{ + did_update(); +} + +void RemoteObjectPropertyModel::set_data(const GUI::ModelIndex& index, const GUI::Variant& new_value) +{ + if (!index.is_valid()) + return; + + auto* path = static_cast<const JsonPath*>(index.internal_data()); + if (path->size() != 1) + return; + + FlatPtr address = m_object.address; + RemoteProcess::the().set_property(address, path->first().to_string(), new_value.to_string()); + did_update(); +} + +GUI::ModelIndex RemoteObjectPropertyModel::index(int row, int column, const GUI::ModelIndex& parent) const +{ + const auto& parent_path = parent.is_valid() ? *static_cast<const JsonPath*>(parent.internal_data()) : JsonPath {}; + + auto nth_child = [&](int n, const JsonValue& value) -> const JsonPath* { + auto path = make<JsonPath>(); + path->append(parent_path); + int row_index = n; + if (value.is_object()) { + String property_name; + auto& object = value.as_object(); + object.for_each_member([&](auto& name, auto&) { + if (row_index > 0) { + --row_index; + } else if (row_index == 0) { + property_name = name; + --row_index; + } + }); + if (property_name.is_null()) + return nullptr; + + path->append({ property_name }); + m_paths.append(move(path)); + } else if (value.is_array()) { + path->append(JsonPathElement { (size_t)n }); + m_paths.append(move(path)); + } else { + return nullptr; + } + return &m_paths.last(); + }; + + if (!parent.is_valid()) { + if (m_object.json.is_empty()) + return {}; + } + + auto index_path = cached_path_at(row, parent_path); + + if (!index_path) + index_path = nth_child(row, parent_path.resolve(m_object.json)); + + if (!index_path) + return {}; + + return create_index(row, column, index_path); +} + +GUI::ModelIndex RemoteObjectPropertyModel::parent_index(const GUI::ModelIndex& index) const +{ + if (!index.is_valid()) + return index; + + auto path = *static_cast<const JsonPath*>(index.internal_data()); + if (path.is_empty()) + return {}; + + path.take_last(); + if (path.is_empty()) + return {}; + + auto* cpath = find_cached_path(path); + if (cpath) { + int index_in_parent = 0; + if (cpath->last().kind() == JsonPathElement::Kind::Index) + index_in_parent = cpath->last().index(); + else if (cpath->last().kind() == JsonPathElement::Kind::Key) { + auto path_copy = path; + auto last = path_copy.take_last(); + bool found = false; + path_copy.resolve(m_object.json).as_object().for_each_member([&](auto& name, auto&) { + if (!found) { + if (last.key() == name) + found = true; + else + index_in_parent++; + } + }); + } + return create_index(index_in_parent, 0, cpath); + } + + dbgln("No cached path found for path {}", path.to_string()); + return {}; +} + +const JsonPath* RemoteObjectPropertyModel::cached_path_at(int n, const Vector<JsonPathElement>& prefix) const +{ + // FIXME: ModelIndex wants a void*, so we have to keep these + // indices alive, but allocating a new path every time + // we're asked for an index is silly, so we have to look for existing ones first. + const JsonPath* index_path = nullptr; + int row_index = n; + for (auto& path : m_paths) { + if (path.size() != prefix.size() + 1) + continue; + + for (size_t i = 0; i < prefix.size(); ++i) { + if (path[i] != prefix[i]) + goto do_continue; + } + + if (row_index == 0) { + index_path = &path; + break; + } + --row_index; + do_continue:; + } + + return index_path; +}; + +const JsonPath* RemoteObjectPropertyModel::find_cached_path(const Vector<JsonPathElement>& path) const +{ + for (auto& cpath : m_paths) { + if (cpath.size() != path.size()) + continue; + + for (size_t i = 0; i < cpath.size(); ++i) { + if (cpath[i] != path[i]) + goto do_continue; + } + + return &cpath; + do_continue:; + } + + return nullptr; +} + +} diff --git a/Userland/DevTools/Inspector/RemoteObjectPropertyModel.h b/Userland/DevTools/Inspector/RemoteObjectPropertyModel.h new file mode 100644 index 0000000000..f770fcf61e --- /dev/null +++ b/Userland/DevTools/Inspector/RemoteObjectPropertyModel.h @@ -0,0 +1,72 @@ +/* + * Copyright (c) 2018-2020, Andreas Kling <kling@serenityos.org> + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * 1. Redistributions of source code must retain the above copyright notice, this + * list of conditions and the following disclaimer. + * + * 2. Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE + * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL + * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR + * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER + * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, + * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE + * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +#pragma once + +#include <AK/JsonPath.h> +#include <AK/JsonValue.h> +#include <AK/NonnullOwnPtrVector.h> +#include <LibGUI/Model.h> + +namespace Inspector { + +class RemoteObject; + +class RemoteObjectPropertyModel final : public GUI::Model { +public: + virtual ~RemoteObjectPropertyModel() override { } + static NonnullRefPtr<RemoteObjectPropertyModel> create(RemoteObject& object) + { + return adopt(*new RemoteObjectPropertyModel(object)); + } + + enum Column { + Name, + Value, + __Count, + }; + + virtual int row_count(const GUI::ModelIndex& = GUI::ModelIndex()) const override; + virtual int column_count(const GUI::ModelIndex& = GUI::ModelIndex()) const override { return Column::__Count; } + virtual String column_name(int) const override; + virtual GUI::Variant data(const GUI::ModelIndex&, GUI::ModelRole) const override; + virtual void set_data(const GUI::ModelIndex&, const GUI::Variant&) override; + virtual void update() override; + virtual bool is_editable(const GUI::ModelIndex& index) const override { return index.column() == Column::Value; } + virtual GUI::ModelIndex index(int row, int column, const GUI::ModelIndex& parent = GUI::ModelIndex()) const override; + virtual GUI::ModelIndex parent_index(const GUI::ModelIndex&) const override; + +private: + explicit RemoteObjectPropertyModel(RemoteObject&); + + const JsonPath* cached_path_at(int n, const Vector<JsonPathElement>& prefix) const; + const JsonPath* find_cached_path(const Vector<JsonPathElement>& path) const; + + RemoteObject& m_object; + mutable NonnullOwnPtrVector<JsonPath> m_paths; +}; + +} diff --git a/Userland/DevTools/Inspector/RemoteProcess.cpp b/Userland/DevTools/Inspector/RemoteProcess.cpp new file mode 100644 index 0000000000..aa36b46cae --- /dev/null +++ b/Userland/DevTools/Inspector/RemoteProcess.cpp @@ -0,0 +1,201 @@ +/* + * Copyright (c) 2018-2020, Andreas Kling <kling@serenityos.org> + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * 1. Redistributions of source code must retain the above copyright notice, this + * list of conditions and the following disclaimer. + * + * 2. Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE + * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL + * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR + * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER + * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, + * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE + * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +#include "RemoteProcess.h" +#include "RemoteObject.h" +#include "RemoteObjectGraphModel.h" +#include "RemoteObjectPropertyModel.h" +#include <stdio.h> +#include <stdlib.h> + +namespace Inspector { + +RemoteProcess* s_the; + +RemoteProcess& RemoteProcess::the() +{ + return *s_the; +} + +RemoteProcess::RemoteProcess(pid_t pid) + : m_pid(pid) + , m_object_graph_model(RemoteObjectGraphModel::create(*this)) + , m_socket(Core::LocalSocket::construct()) +{ + s_the = this; + m_socket->set_blocking(true); +} + +void RemoteProcess::handle_identify_response(const JsonObject& response) +{ + int pid = response.get("pid").to_int(); + ASSERT(pid == m_pid); + + m_process_name = response.get("process_name").as_string_or({}); + + if (on_update) + on_update(); +} + +void RemoteProcess::handle_get_all_objects_response(const JsonObject& response) +{ + // FIXME: It would be good if we didn't have to make a local copy of the array value here! + auto objects = response.get("objects"); + auto& object_array = objects.as_array(); + + NonnullOwnPtrVector<RemoteObject> remote_objects; + HashMap<FlatPtr, RemoteObject*> objects_by_address; + + for (auto& value : object_array.values()) { + ASSERT(value.is_object()); + auto& object = value.as_object(); + auto remote_object = make<RemoteObject>(); + remote_object->address = object.get("address").to_number<FlatPtr>(); + remote_object->parent_address = object.get("parent").to_number<FlatPtr>(); + remote_object->name = object.get("name").to_string(); + remote_object->class_name = object.get("class_name").to_string(); + remote_object->json = object; + objects_by_address.set(remote_object->address, remote_object); + remote_objects.append(move(remote_object)); + } + + for (size_t i = 0; i < remote_objects.size(); ++i) { + auto& remote_object = remote_objects.ptr_at(i); + auto* parent = objects_by_address.get(remote_object->parent_address).value_or(nullptr); + if (!parent) { + m_roots.append(move(remote_object)); + } else { + remote_object->parent = parent; + parent->children.append(move(remote_object)); + } + } + + m_object_graph_model->update(); + + if (on_update) + on_update(); +} + +void RemoteProcess::send_request(const JsonObject& request) +{ + auto serialized = request.to_string(); + i32 length = serialized.length(); + m_socket->write((const u8*)&length, sizeof(length)); + m_socket->write(serialized); +} + +void RemoteProcess::set_inspected_object(FlatPtr address) +{ + JsonObject request; + request.set("type", "SetInspectedObject"); + request.set("address", address); + send_request(request); +} + +void RemoteProcess::set_property(FlatPtr object, const StringView& name, const JsonValue& value) +{ + JsonObject request; + request.set("type", "SetProperty"); + request.set("address", object); + request.set("name", JsonValue(name)); + request.set("value", value); + send_request(request); +} + +void RemoteProcess::update() +{ + m_socket->on_connected = [this] { + dbgln("Connected to PID {}", m_pid); + + { + JsonObject request; + request.set("type", "Identify"); + send_request(request); + } + + { + JsonObject request; + request.set("type", "GetAllObjects"); + send_request(request); + } + }; + + m_socket->on_ready_to_read = [this] { + if (m_socket->eof()) { + dbgln("Disconnected from PID {}", m_pid); + m_socket->close(); + return; + } + + u32 length; + int nread = m_socket->read((u8*)&length, sizeof(length)); + ASSERT(nread == sizeof(length)); + + ByteBuffer data; + size_t remaining_bytes = length; + + while (remaining_bytes) { + auto packet = m_socket->read(remaining_bytes); + if (packet.size() == 0) + break; + data.append(packet.data(), packet.size()); + remaining_bytes -= packet.size(); + } + + ASSERT(data.size() == length); + dbgln("Got data size {} and read that many bytes", length); + + auto json_value = JsonValue::from_string(data); + ASSERT(json_value.has_value()); + ASSERT(json_value.value().is_object()); + + dbgln("Got JSON response {}", json_value.value()); + + auto& response = json_value.value().as_object(); + + auto response_type = response.get("type").as_string_or({}); + if (response_type.is_null()) + return; + + if (response_type == "GetAllObjects") { + handle_get_all_objects_response(response); + return; + } + + if (response_type == "Identify") { + handle_identify_response(response); + return; + } + }; + + auto success = m_socket->connect(Core::SocketAddress::local(String::format("/tmp/rpc/%d", m_pid))); + if (!success) { + warnln("Couldn't connect to PID {}", m_pid); + exit(1); + } +} + +} diff --git a/Userland/DevTools/Inspector/RemoteProcess.h b/Userland/DevTools/Inspector/RemoteProcess.h new file mode 100644 index 0000000000..427b7183b5 --- /dev/null +++ b/Userland/DevTools/Inspector/RemoteProcess.h @@ -0,0 +1,69 @@ +/* + * Copyright (c) 2018-2020, Andreas Kling <kling@serenityos.org> + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * 1. Redistributions of source code must retain the above copyright notice, this + * list of conditions and the following disclaimer. + * + * 2. Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE + * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL + * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR + * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER + * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, + * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE + * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +#pragma once + +#include <AK/NonnullOwnPtrVector.h> +#include <LibCore/LocalSocket.h> + +namespace Inspector { + +class RemoteObjectGraphModel; +class RemoteObject; + +class RemoteProcess { +public: + static RemoteProcess& the(); + + explicit RemoteProcess(pid_t); + void update(); + + pid_t pid() const { return m_pid; } + const String& process_name() const { return m_process_name; } + + RemoteObjectGraphModel& object_graph_model() { return *m_object_graph_model; } + const NonnullOwnPtrVector<RemoteObject>& roots() const { return m_roots; } + + void set_inspected_object(FlatPtr); + + void set_property(FlatPtr object, const StringView& name, const JsonValue& value); + + Function<void()> on_update; + +private: + void handle_get_all_objects_response(const AK::JsonObject&); + void handle_identify_response(const AK::JsonObject&); + + void send_request(const AK::JsonObject&); + + pid_t m_pid { -1 }; + String m_process_name; + NonnullRefPtr<RemoteObjectGraphModel> m_object_graph_model; + RefPtr<Core::LocalSocket> m_socket; + NonnullOwnPtrVector<RemoteObject> m_roots; +}; + +} diff --git a/Userland/DevTools/Inspector/main.cpp b/Userland/DevTools/Inspector/main.cpp new file mode 100644 index 0000000000..19819b725d --- /dev/null +++ b/Userland/DevTools/Inspector/main.cpp @@ -0,0 +1,152 @@ +/* + * Copyright (c) 2018-2020, Andreas Kling <kling@serenityos.org> + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * 1. Redistributions of source code must retain the above copyright notice, this + * list of conditions and the following disclaimer. + * + * 2. Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE + * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL + * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR + * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER + * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, + * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE + * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +#include "RemoteObject.h" +#include "RemoteObjectGraphModel.h" +#include "RemoteObjectPropertyModel.h" +#include "RemoteProcess.h" +#include <LibGUI/Application.h> +#include <LibGUI/BoxLayout.h> +#include <LibGUI/Menu.h> +#include <LibGUI/MenuBar.h> +#include <LibGUI/ModelEditingDelegate.h> +#include <LibGUI/ProcessChooser.h> +#include <LibGUI/Splitter.h> +#include <LibGUI/TreeView.h> +#include <LibGUI/Window.h> +#include <stdio.h> + +using namespace Inspector; + +[[noreturn]] static void print_usage_and_exit() +{ + outln("usage: Inspector <pid>"); + exit(0); +} + +int main(int argc, char** argv) +{ + if (pledge("stdio shared_buffer rpath accept unix cpath fattr", nullptr) < 0) { + perror("pledge"); + return 1; + } + + if (unveil("/res", "r") < 0) { + perror("unveil"); + return 1; + } + + if (unveil("/bin", "r") < 0) { + perror("unveil"); + return 1; + } + + if (unveil("/tmp", "rwc") < 0) { + perror("unveil"); + return 1; + } + + if (unveil("/proc/all", "r") < 0) { + perror("unveil"); + return 1; + } + + if (unveil("/etc/passwd", "r") < 0) { + perror("unveil"); + return 1; + } + + unveil(nullptr, nullptr); + + pid_t pid; + + auto app = GUI::Application::construct(argc, argv); + auto app_icon = GUI::Icon::default_icon("app-inspector"); + if (argc != 2) { + auto process_chooser = GUI::ProcessChooser::construct("Inspector", "Inspect", app_icon.bitmap_for_size(16)); + if (process_chooser->exec() == GUI::Dialog::ExecCancel) + return 0; + pid = process_chooser->pid(); + } else { + auto pid_opt = String(argv[1]).to_int(); + if (!pid_opt.has_value()) + print_usage_and_exit(); + pid = pid_opt.value(); + } + + auto window = GUI::Window::construct(); + window->set_title("Inspector"); + window->resize(685, 500); + window->set_icon(app_icon.bitmap_for_size(16)); + + auto menubar = GUI::MenuBar::construct(); + auto& app_menu = menubar->add_menu("Inspector"); + app_menu.add_action(GUI::CommonActions::make_quit_action([&](auto&) { app->quit(); })); + + auto& help_menu = menubar->add_menu("Help"); + help_menu.add_action(GUI::CommonActions::make_about_action("Inspector", app_icon, window)); + + auto& widget = window->set_main_widget<GUI::Widget>(); + widget.set_fill_with_background_color(true); + widget.set_layout<GUI::VerticalBoxLayout>(); + + auto& splitter = widget.add<GUI::HorizontalSplitter>(); + + RemoteProcess remote_process(pid); + + remote_process.on_update = [&] { + if (!remote_process.process_name().is_null()) + window->set_title(String::formatted("{} ({}) - Inspector", remote_process.process_name(), remote_process.pid())); + }; + + auto& tree_view = splitter.add<GUI::TreeView>(); + tree_view.set_model(remote_process.object_graph_model()); + tree_view.set_activates_on_selection(true); + tree_view.set_fixed_width(286); + + auto& properties_tree_view = splitter.add<GUI::TreeView>(); + properties_tree_view.set_editable(true); + properties_tree_view.aid_create_editing_delegate = [](auto&) { + return make<GUI::StringModelEditingDelegate>(); + }; + + tree_view.on_activation = [&](auto& index) { + auto* remote_object = static_cast<RemoteObject*>(index.internal_data()); + properties_tree_view.set_model(remote_object->property_model()); + remote_process.set_inspected_object(remote_object->address); + }; + + app->set_menubar(move(menubar)); + window->show(); + remote_process.update(); + + if (pledge("stdio shared_buffer rpath accept unix", nullptr) < 0) { + perror("pledge"); + return 1; + } + + return app->exec(); +} diff --git a/Userland/DevTools/Playground/CMakeLists.txt b/Userland/DevTools/Playground/CMakeLists.txt new file mode 100644 index 0000000000..e0fa186766 --- /dev/null +++ b/Userland/DevTools/Playground/CMakeLists.txt @@ -0,0 +1,6 @@ +set(SOURCES + main.cpp +) + +serenity_app(Playground ICON app-playground) +target_link_libraries(Playground LibDesktop LibGUI) diff --git a/Userland/DevTools/Playground/main.cpp b/Userland/DevTools/Playground/main.cpp new file mode 100644 index 0000000000..8ed1cdbda8 --- /dev/null +++ b/Userland/DevTools/Playground/main.cpp @@ -0,0 +1,407 @@ +/* + * Copyright (c) 2020, Andreas Kling <kling@serenityos.org> + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * 1. Redistributions of source code must retain the above copyright notice, this + * list of conditions and the following disclaimer. + * + * 2. Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE + * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL + * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR + * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER + * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, + * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE + * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +#include <AK/QuickSort.h> +#include <AK/URL.h> +#include <LibCore/ArgsParser.h> +#include <LibCore/File.h> +#include <LibCore/Property.h> +#include <LibDesktop/Launcher.h> +#include <LibGUI/Application.h> +#include <LibGUI/AutocompleteProvider.h> +#include <LibGUI/FilePicker.h> +#include <LibGUI/GMLFormatter.h> +#include <LibGUI/GMLLexer.h> +#include <LibGUI/GMLSyntaxHighlighter.h> +#include <LibGUI/Icon.h> +#include <LibGUI/Menu.h> +#include <LibGUI/MenuBar.h> +#include <LibGUI/MessageBox.h> +#include <LibGUI/Painter.h> +#include <LibGUI/Splitter.h> +#include <LibGUI/TextEditor.h> +#include <LibGUI/Window.h> +#include <string.h> + +namespace { + +class UnregisteredWidget final : public GUI::Widget { + C_OBJECT(UnregisteredWidget); + +private: + UnregisteredWidget(const String& class_name); + + virtual void paint_event(GUI::PaintEvent& event) override; + + String m_text; +}; + +UnregisteredWidget::UnregisteredWidget(const String& class_name) +{ + StringBuilder builder; + builder.append(class_name); + builder.append("\nnot registered"); + m_text = builder.to_string(); +} + +void UnregisteredWidget::paint_event(GUI::PaintEvent& event) +{ + GUI::Painter painter(*this); + painter.add_clip_rect(event.rect()); + painter.fill_rect(event.rect(), Gfx::Color::DarkRed); + painter.draw_text(rect(), m_text, Gfx::TextAlignment::Center, Color::White); +} + +} + +class GMLAutocompleteProvider final : public virtual GUI::AutocompleteProvider { +public: + GMLAutocompleteProvider() { } + virtual ~GMLAutocompleteProvider() override { } + +private: + static bool can_have_declared_layout(const StringView& class_name) + { + return class_name.is_one_of("GUI::Widget", "GUI::Frame"); + } + + virtual void provide_completions(Function<void(Vector<Entry>)> callback) override + { + auto cursor = m_editor->cursor(); + auto text = m_editor->text(); + GUI::GMLLexer lexer(text); + // FIXME: Provide a begin() and end() for lexers PLEASE! + auto all_tokens = lexer.lex(); + enum State { + Free, + InClassName, + AfterClassName, + InIdentifier, + AfterIdentifier, // Can we introspect this? + } state { Free }; + String identifier_string; + Vector<String> class_names; + Vector<State> previous_states; + bool should_push_state { true }; + GUI::GMLToken* last_seen_token { nullptr }; + + for (auto& token : all_tokens) { + if (token.m_start.line > cursor.line() || (token.m_start.line == cursor.line() && token.m_start.column > cursor.column())) + break; + + last_seen_token = &token; + switch (state) { + case Free: + if (token.m_type == GUI::GMLToken::Type::ClassName) { + if (should_push_state) + previous_states.append(state); + else + should_push_state = true; + state = InClassName; + class_names.append(token.m_view); + break; + } + break; + case InClassName: + state = AfterClassName; + break; + case AfterClassName: + if (token.m_type == GUI::GMLToken::Type::Identifier) { + state = InIdentifier; + identifier_string = token.m_view; + break; + } + if (token.m_type == GUI::GMLToken::Type::RightCurly) { + class_names.take_last(); + state = previous_states.take_last(); + break; + } + if (token.m_type == GUI::GMLToken::Type::ClassMarker) { + previous_states.append(AfterClassName); + state = Free; + should_push_state = false; + } + break; + case InIdentifier: + if (token.m_type == GUI::GMLToken::Type::Colon) + state = AfterIdentifier; + break; + case AfterIdentifier: + if (token.m_type == GUI::GMLToken::Type::RightCurly || token.m_type == GUI::GMLToken::Type::LeftCurly) + break; + if (token.m_type == GUI::GMLToken::Type::ClassMarker) { + previous_states.append(AfterClassName); + state = Free; + should_push_state = false; + } else { + state = AfterClassName; + } + break; + } + } + + Vector<GUI::AutocompleteProvider::Entry> class_entries, identifier_entries; + switch (state) { + case Free: + if (last_seen_token && last_seen_token->m_end.column + 1 != cursor.column() && last_seen_token->m_end.line == cursor.line()) { + // After some token, but with extra space, not on a new line. + // Nothing to put here. + break; + } + GUI::WidgetClassRegistration::for_each([&](const GUI::WidgetClassRegistration& registration) { + class_entries.empend(String::formatted("@{}", registration.class_name()), 0u); + }); + break; + case InClassName: + if (class_names.is_empty()) + break; + if (last_seen_token && last_seen_token->m_end.column + 1 != cursor.column() && last_seen_token->m_end.line == cursor.line()) { + // After a class name, but haven't seen braces. + // TODO: Suggest braces? + break; + } + GUI::WidgetClassRegistration::for_each([&](const GUI::WidgetClassRegistration& registration) { + if (registration.class_name().starts_with(class_names.last())) + identifier_entries.empend(registration.class_name(), class_names.last().length()); + }); + break; + case InIdentifier: + if (class_names.is_empty()) + break; + if (last_seen_token && last_seen_token->m_end.column + 1 != cursor.column() && last_seen_token->m_end.line == cursor.line()) { + // After an identifier, but with extra space + // TODO: Maybe suggest a colon? + break; + } + if (auto registration = GUI::WidgetClassRegistration::find(class_names.last())) { + auto instance = registration->construct(); + for (auto& it : instance->properties()) { + if (it.key.starts_with(identifier_string)) + identifier_entries.empend(it.key, identifier_string.length()); + } + } + if (can_have_declared_layout(class_names.last()) && StringView { "layout" }.starts_with(identifier_string)) + identifier_entries.empend("layout", identifier_string.length()); + // No need to suggest anything if it's already completely typed out! + if (identifier_entries.size() == 1 && identifier_entries.first().completion == identifier_string) + identifier_entries.clear(); + break; + case AfterClassName: + if (last_seen_token && last_seen_token->m_end.line == cursor.line()) { + if (last_seen_token->m_type != GUI::GMLToken::Type::Identifier || last_seen_token->m_end.column + 1 != cursor.column()) { + // Inside braces, but on the same line as some other stuff (and not the continuation of one!) + // The user expects nothing here. + break; + } + } + if (!class_names.is_empty()) { + if (auto registration = GUI::WidgetClassRegistration::find(class_names.last())) { + auto instance = registration->construct(); + for (auto& it : instance->properties()) { + if (!it.value->is_readonly()) + identifier_entries.empend(it.key, 0u); + } + } + } + GUI::WidgetClassRegistration::for_each([&](const GUI::WidgetClassRegistration& registration) { + class_entries.empend(String::formatted("@{}", registration.class_name()), 0u); + }); + break; + case AfterIdentifier: + if (last_seen_token && last_seen_token->m_end.line != cursor.line()) { + break; + } + if (identifier_string == "layout") { + GUI::WidgetClassRegistration::for_each([&](const GUI::WidgetClassRegistration& registration) { + if (registration.class_name().contains("Layout")) + class_entries.empend(String::formatted("@{}", registration.class_name()), 0u); + }); + } + break; + default: + break; + } + + quick_sort(class_entries, [](auto& a, auto& b) { return a.completion < b.completion; }); + quick_sort(identifier_entries, [](auto& a, auto& b) { return a.completion < b.completion; }); + + Vector<GUI::AutocompleteProvider::Entry> entries; + entries.append(move(identifier_entries)); + entries.append(move(class_entries)); + + callback(move(entries)); + } +}; + +int main(int argc, char** argv) +{ + if (pledge("stdio thread shared_buffer accept cpath rpath wpath unix fattr", nullptr) < 0) { + perror("pledge"); + return 1; + } + + auto app = GUI::Application::construct(argc, argv); + + if (pledge("stdio thread shared_buffer accept rpath cpath wpath unix", nullptr) < 0) { + perror("pledge"); + return 1; + } + + if (!Desktop::Launcher::add_allowed_handler_with_only_specific_urls( + "/bin/Help", + { URL::create_with_file_protocol("/usr/share/man/man1/Playground.md") }) + || !Desktop::Launcher::seal_allowlist()) { + warnln("Failed to set up allowed launch URLs"); + return 1; + } + + if (pledge("stdio thread shared_buffer accept rpath cpath wpath", nullptr) < 0) { + perror("pledge"); + return 1; + } + + const char* path = nullptr; + Core::ArgsParser args_parser; + args_parser.add_positional_argument(path, "GML file to edit", "file", Core::ArgsParser::Required::No); + args_parser.parse(argc, argv); + + auto app_icon = GUI::Icon::default_icon("app-playground"); + auto window = GUI::Window::construct(); + window->set_title("GML Playground"); + window->set_icon(app_icon.bitmap_for_size(16)); + window->resize(800, 600); + + auto& splitter = window->set_main_widget<GUI::HorizontalSplitter>(); + + auto& editor = splitter.add<GUI::TextEditor>(); + auto& preview = splitter.add<GUI::Widget>(); + + editor.set_syntax_highlighter(make<GUI::GMLSyntaxHighlighter>()); + editor.set_autocomplete_provider(make<GMLAutocompleteProvider>()); + editor.set_should_autocomplete_automatically(true); + editor.set_automatic_indentation_enabled(true); + + if (String(path).is_empty()) { + editor.set_text(R"~~~(@GUI::Widget { + layout: @GUI::VerticalBoxLayout { + } + + // Now add some widgets! +} +)~~~"); + editor.set_cursor(4, 28); // after "...widgets!" + } else { + auto file = Core::File::construct(path); + if (!file->open(Core::IODevice::ReadOnly)) { + GUI::MessageBox::show(window, String::formatted("Opening \"{}\" failed: {}", path, strerror(errno)), "Error", GUI::MessageBox::Type::Error); + return 1; + } + editor.set_text(file->read_all()); + } + + editor.on_change = [&] { + preview.remove_all_children(); + preview.load_from_gml(editor.text(), [](const String& class_name) -> RefPtr<GUI::Widget> { + return UnregisteredWidget::construct(class_name); + }); + }; + + auto menubar = GUI::MenuBar::construct(); + auto& app_menu = menubar->add_menu("GML Playground"); + + app_menu.add_action(GUI::CommonActions::make_open_action([&](auto&) { + Optional<String> open_path = GUI::FilePicker::get_open_filepath(window); + + if (!open_path.has_value()) + return; + + auto file = Core::File::construct(open_path.value()); + if (!file->open(Core::IODevice::ReadOnly) && file->error() != ENOENT) { + GUI::MessageBox::show(window, String::formatted("Opening \"{}\" failed: {}", open_path.value(), strerror(errno)), "Error", GUI::MessageBox::Type::Error); + return; + } + + editor.set_text(file->read_all()); + editor.set_focus(true); + })); + + app_menu.add_action(GUI::CommonActions::make_save_as_action([&](auto&) { + Optional<String> save_path = GUI::FilePicker::get_save_filepath(window, "Untitled", "gml"); + if (!save_path.has_value()) + return; + + if (!editor.write_to_file(save_path.value())) { + GUI::MessageBox::show(window, "Unable to save file.\n", "Error", GUI::MessageBox::Type::Error); + return; + } + })); + + app_menu.add_separator(); + + app_menu.add_action(GUI::CommonActions::make_quit_action([&](auto&) { + app->quit(); + })); + + auto& edit_menu = menubar->add_menu("Edit"); + edit_menu.add_action(GUI::Action::create("Format GML", { Mod_Ctrl | Mod_Shift, Key_I }, [&](auto&) { + auto source = editor.text(); + GUI::GMLLexer lexer(source); + for (auto& token : lexer.lex()) { + if (token.m_type == GUI::GMLToken::Type::Comment) { + auto result = GUI::MessageBox::show( + window, + "Your GML contains comments, which currently are not supported by the formatter and will be removed. Proceed?", + "Warning", + GUI::MessageBox::Type::Warning, + GUI::MessageBox::InputType::OKCancel); + if (result == GUI::MessageBox::ExecCancel) + return; + break; + } + } + auto formatted_gml = GUI::format_gml(source); + if (!formatted_gml.is_null()) { + editor.set_text(formatted_gml); + } else { + GUI::MessageBox::show( + window, + "GML could not be formatted, please check the debug console for parsing errors.", + "Error", + GUI::MessageBox::Type::Error); + } + })); + + auto& help_menu = menubar->add_menu("Help"); + help_menu.add_action(GUI::CommonActions::make_help_action([](auto&) { + Desktop::Launcher::open(URL::create_with_file_protocol("/usr/share/man/man1/Playground.md"), "/bin/Help"); + })); + help_menu.add_action(GUI::CommonActions::make_about_action("GML Playground", app_icon, window)); + + app->set_menubar(move(menubar)); + + window->show(); + return app->exec(); +} diff --git a/Userland/DevTools/Profiler/CMakeLists.txt b/Userland/DevTools/Profiler/CMakeLists.txt new file mode 100644 index 0000000000..1cb7b95140 --- /dev/null +++ b/Userland/DevTools/Profiler/CMakeLists.txt @@ -0,0 +1,10 @@ +set(SOURCES + DisassemblyModel.cpp + main.cpp + Profile.cpp + ProfileModel.cpp + ProfileTimelineWidget.cpp +) + +serenity_app(Profiler ICON app-profiler) +target_link_libraries(Profiler LibGUI LibX86 LibCoreDump) diff --git a/Userland/DevTools/Profiler/DisassemblyModel.cpp b/Userland/DevTools/Profiler/DisassemblyModel.cpp new file mode 100644 index 0000000000..4195f103cd --- /dev/null +++ b/Userland/DevTools/Profiler/DisassemblyModel.cpp @@ -0,0 +1,202 @@ +/* + * Copyright (c) 2020, Andreas Kling <kling@serenityos.org> + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * 1. Redistributions of source code must retain the above copyright notice, this + * list of conditions and the following disclaimer. + * + * 2. Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE + * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL + * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR + * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER + * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, + * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE + * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +#include "DisassemblyModel.h" +#include "Profile.h" +#include <AK/MappedFile.h> +#include <LibELF/Image.h> +#include <LibGUI/Painter.h> +#include <LibX86/Disassembler.h> +#include <LibX86/ELFSymbolProvider.h> +#include <ctype.h> +#include <stdio.h> + +static const Gfx::Bitmap& heat_gradient() +{ + static RefPtr<Gfx::Bitmap> bitmap; + if (!bitmap) { + bitmap = Gfx::Bitmap::create(Gfx::BitmapFormat::RGB32, { 101, 1 }); + GUI::Painter painter(*bitmap); + painter.fill_rect_with_gradient(Orientation::Horizontal, bitmap->rect(), Color::from_rgb(0xffc080), Color::from_rgb(0xff3000)); + } + return *bitmap; +} + +static Color color_for_percent(int percent) +{ + ASSERT(percent >= 0 && percent <= 100); + return heat_gradient().get_pixel(percent, 0); +} + +DisassemblyModel::DisassemblyModel(Profile& profile, ProfileNode& node) + : m_profile(profile) + , m_node(node) +{ + OwnPtr<ELF::Image> kernel_elf; + const ELF::Image* elf; + FlatPtr base_address = 0; + if (m_node.address() >= 0xc0000000) { + if (!m_kernel_file) { + auto file_or_error = MappedFile::map("/boot/Kernel"); + if (file_or_error.is_error()) + return; + m_kernel_file = file_or_error.release_value(); + } + kernel_elf = make<ELF::Image>((const u8*)m_kernel_file->data(), m_kernel_file->size()); + elf = kernel_elf.ptr(); + } else { + auto library_data = profile.libraries().library_containing(node.address()); + if (!library_data) { + dbgln("no library data"); + return; + } + elf = &library_data->elf; + base_address = library_data->base; + } + + ASSERT(elf != nullptr); + + auto symbol = elf->find_symbol(node.address() - base_address); + if (!symbol.has_value()) { + dbgln("DisassemblyModel: symbol not found"); + return; + } + ASSERT(symbol.has_value()); + + auto view = symbol.value().raw_data(); + + X86::ELFSymbolProvider symbol_provider(*elf); + X86::SimpleInstructionStream stream((const u8*)view.characters_without_null_termination(), view.length()); + X86::Disassembler disassembler(stream); + + size_t offset_into_symbol = 0; + for (;;) { + auto insn = disassembler.next(); + if (!insn.has_value()) + break; + FlatPtr address_in_profiled_program = symbol.value().value() + offset_into_symbol; + + auto disassembly = insn.value().to_string(address_in_profiled_program, &symbol_provider); + + StringView instruction_bytes = view.substring_view(offset_into_symbol, insn.value().length()); + size_t samples_at_this_instruction = m_node.events_per_address().get(address_in_profiled_program).value_or(0); + float percent = ((float)samples_at_this_instruction / (float)m_node.event_count()) * 100.0f; + + m_instructions.append({ insn.value(), disassembly, instruction_bytes, address_in_profiled_program, samples_at_this_instruction, percent }); + + offset_into_symbol += insn.value().length(); + } +} + +DisassemblyModel::~DisassemblyModel() +{ +} + +int DisassemblyModel::row_count(const GUI::ModelIndex&) const +{ + return m_instructions.size(); +} + +String DisassemblyModel::column_name(int column) const +{ + switch (column) { + case Column::SampleCount: + return m_profile.show_percentages() ? "% Samples" : "# Samples"; + case Column::Address: + return "Address"; + case Column::InstructionBytes: + return "Insn Bytes"; + case Column::Disassembly: + return "Disassembly"; + default: + ASSERT_NOT_REACHED(); + return {}; + } +} + +struct ColorPair { + Color background; + Color foreground; +}; + +static Optional<ColorPair> color_pair_for(const InstructionData& insn) +{ + if (insn.percent == 0) + return {}; + + Color background = color_for_percent(insn.percent); + Color foreground; + if (insn.percent > 50) + foreground = Color::White; + else + foreground = Color::Black; + return ColorPair { background, foreground }; +} + +GUI::Variant DisassemblyModel::data(const GUI::ModelIndex& index, GUI::ModelRole role) const +{ + auto& insn = m_instructions[index.row()]; + + if (role == GUI::ModelRole::BackgroundColor) { + auto colors = color_pair_for(insn); + if (!colors.has_value()) + return {}; + return colors.value().background; + } + + if (role == GUI::ModelRole::ForegroundColor) { + auto colors = color_pair_for(insn); + if (!colors.has_value()) + return {}; + return colors.value().foreground; + } + + if (role == GUI::ModelRole::Display) { + if (index.column() == Column::SampleCount) { + if (m_profile.show_percentages()) + return ((float)insn.event_count / (float)m_node.event_count()) * 100.0f; + return insn.event_count; + } + if (index.column() == Column::Address) + return String::formatted("{:p}", insn.address); + if (index.column() == Column::InstructionBytes) { + StringBuilder builder; + for (auto ch : insn.bytes) { + builder.appendff("{:02x} ", (u8)ch); + } + return builder.to_string(); + } + if (index.column() == Column::Disassembly) + return insn.disassembly; + return {}; + } + return {}; +} + +void DisassemblyModel::update() +{ + did_update(); +} diff --git a/Userland/DevTools/Profiler/DisassemblyModel.h b/Userland/DevTools/Profiler/DisassemblyModel.h new file mode 100644 index 0000000000..0d0e643292 --- /dev/null +++ b/Userland/DevTools/Profiler/DisassemblyModel.h @@ -0,0 +1,75 @@ +/* + * Copyright (c) 2020, Andreas Kling <kling@serenityos.org> + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * 1. Redistributions of source code must retain the above copyright notice, this + * list of conditions and the following disclaimer. + * + * 2. Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE + * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL + * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR + * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER + * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, + * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE + * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +#pragma once + +#include <LibGUI/Model.h> +#include <LibX86/Instruction.h> + +class Profile; +class ProfileNode; + +struct InstructionData { + X86::Instruction insn; + String disassembly; + StringView bytes; + FlatPtr address { 0 }; + u32 event_count { 0 }; + float percent { 0 }; +}; + +class DisassemblyModel final : public GUI::Model { +public: + static NonnullRefPtr<DisassemblyModel> create(Profile& profile, ProfileNode& node) + { + return adopt(*new DisassemblyModel(profile, node)); + } + + enum Column { + Address, + SampleCount, + InstructionBytes, + Disassembly, + __Count + }; + + virtual ~DisassemblyModel() override; + + virtual int row_count(const GUI::ModelIndex& = GUI::ModelIndex()) const override; + virtual int column_count(const GUI::ModelIndex& = GUI::ModelIndex()) const override { return Column::__Count; } + virtual String column_name(int) const override; + virtual GUI::Variant data(const GUI::ModelIndex&, GUI::ModelRole) const override; + virtual void update() override; + +private: + DisassemblyModel(Profile&, ProfileNode&); + + Profile& m_profile; + ProfileNode& m_node; + RefPtr<MappedFile> m_kernel_file; + + Vector<InstructionData> m_instructions; +}; diff --git a/Userland/DevTools/Profiler/Profile.cpp b/Userland/DevTools/Profiler/Profile.cpp new file mode 100644 index 0000000000..8e8e986841 --- /dev/null +++ b/Userland/DevTools/Profiler/Profile.cpp @@ -0,0 +1,399 @@ +/* + * Copyright (c) 2018-2021, Andreas Kling <kling@serenityos.org> + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * 1. Redistributions of source code must retain the above copyright notice, this + * list of conditions and the following disclaimer. + * + * 2. Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE + * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL + * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR + * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER + * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, + * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE + * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +#include "Profile.h" +#include "DisassemblyModel.h" +#include "ProfileModel.h" +#include <AK/HashTable.h> +#include <AK/MappedFile.h> +#include <AK/QuickSort.h> +#include <AK/RefPtr.h> +#include <LibCore/File.h> +#include <LibELF/Image.h> +#include <stdio.h> +#include <sys/stat.h> + +static void sort_profile_nodes(Vector<NonnullRefPtr<ProfileNode>>& nodes) +{ + quick_sort(nodes.begin(), nodes.end(), [](auto& a, auto& b) { + return a->event_count() >= b->event_count(); + }); + + for (auto& child : nodes) + child->sort_children(); +} + +Profile::Profile(String executable_path, Vector<Event> events, NonnullOwnPtr<LibraryMetadata> library_metadata) + : m_executable_path(move(executable_path)) + , m_events(move(events)) + , m_library_metadata(move(library_metadata)) +{ + m_first_timestamp = m_events.first().timestamp; + m_last_timestamp = m_events.last().timestamp; + + m_model = ProfileModel::create(*this); + + for (auto& event : m_events) { + m_deepest_stack_depth = max((u32)event.frames.size(), m_deepest_stack_depth); + } + + rebuild_tree(); +} + +Profile::~Profile() +{ +} + +GUI::Model& Profile::model() +{ + return *m_model; +} + +void Profile::rebuild_tree() +{ + u32 filtered_event_count = 0; + Vector<NonnullRefPtr<ProfileNode>> roots; + + auto find_or_create_root = [&roots](const String& symbol, u32 address, u32 offset, u64 timestamp) -> ProfileNode& { + for (size_t i = 0; i < roots.size(); ++i) { + auto& root = roots[i]; + if (root->symbol() == symbol) { + return root; + } + } + auto new_root = ProfileNode::create(symbol, address, offset, timestamp); + roots.append(new_root); + return new_root; + }; + + HashTable<FlatPtr> live_allocations; + + for (auto& event : m_events) { + if (has_timestamp_filter_range()) { + auto timestamp = event.timestamp; + if (timestamp < m_timestamp_filter_range_start || timestamp > m_timestamp_filter_range_end) + continue; + } + + if (event.type == "malloc") + live_allocations.set(event.ptr); + else if (event.type == "free") + live_allocations.remove(event.ptr); + } + + for (size_t event_index = 0; event_index < m_events.size(); ++event_index) { + auto& event = m_events.at(event_index); + if (has_timestamp_filter_range()) { + auto timestamp = event.timestamp; + if (timestamp < m_timestamp_filter_range_start || timestamp > m_timestamp_filter_range_end) + continue; + } + + if (event.type == "malloc" && !live_allocations.contains(event.ptr)) + continue; + + if (event.type == "free") + continue; + + auto for_each_frame = [&]<typename Callback>(Callback callback) { + if (!m_inverted) { + for (size_t i = 0; i < event.frames.size(); ++i) { + if (callback(event.frames.at(i), i == event.frames.size() - 1) == IterationDecision::Break) + break; + } + } else { + for (ssize_t i = event.frames.size() - 1; i >= 0; --i) { + if (callback(event.frames.at(i), static_cast<size_t>(i) == event.frames.size() - 1) == IterationDecision::Break) + break; + } + } + }; + + if (!m_show_top_functions) { + ProfileNode* node = nullptr; + for_each_frame([&](const Frame& frame, bool is_innermost_frame) { + auto& symbol = frame.symbol; + auto& address = frame.address; + auto& offset = frame.offset; + + if (symbol.is_empty()) + return IterationDecision::Break; + + if (!node) + node = &find_or_create_root(symbol, address, offset, event.timestamp); + else + node = &node->find_or_create_child(symbol, address, offset, event.timestamp); + + node->increment_event_count(); + if (is_innermost_frame) { + node->add_event_address(address); + node->increment_self_count(); + } + return IterationDecision::Continue; + }); + } else { + for (size_t i = 0; i < event.frames.size(); ++i) { + ProfileNode* node = nullptr; + ProfileNode* root = nullptr; + for (size_t j = i; j < event.frames.size(); ++j) { + auto& frame = event.frames.at(j); + auto& symbol = frame.symbol; + auto& address = frame.address; + auto& offset = frame.offset; + if (symbol.is_empty()) + break; + + if (!node) { + node = &find_or_create_root(symbol, address, offset, event.timestamp); + root = node; + root->will_track_seen_events(m_events.size()); + } else { + node = &node->find_or_create_child(symbol, address, offset, event.timestamp); + } + + if (!root->has_seen_event(event_index)) { + root->did_see_event(event_index); + root->increment_event_count(); + } else if (node != root) { + node->increment_event_count(); + } + + if (j == event.frames.size() - 1) { + node->add_event_address(address); + node->increment_self_count(); + } + } + } + } + + ++filtered_event_count; + } + + sort_profile_nodes(roots); + + m_filtered_event_count = filtered_event_count; + m_roots = move(roots); + m_model->update(); +} + +Result<NonnullOwnPtr<Profile>, String> Profile::load_from_perfcore_file(const StringView& path) +{ + auto file = Core::File::construct(path); + if (!file->open(Core::IODevice::ReadOnly)) + return String::formatted("Unable to open {}, error: {}", path, file->error_string()); + + auto json = JsonValue::from_string(file->read_all()); + if (!json.has_value() || !json.value().is_object()) + return String { "Invalid perfcore format (not a JSON object)" }; + + auto& object = json.value().as_object(); + auto executable_path = object.get("executable").to_string(); + + auto pid = object.get("pid"); + if (!pid.is_u32()) + return String { "Invalid perfcore format (no process ID)" }; + + auto file_or_error = MappedFile::map("/boot/Kernel"); + OwnPtr<ELF::Image> kernel_elf; + if (!file_or_error.is_error()) + kernel_elf = make<ELF::Image>(file_or_error.value()->bytes()); + + auto events_value = object.get("events"); + if (!events_value.is_array()) + return String { "Malformed profile (events is not an array)" }; + + auto regions_value = object.get("regions"); + if (!regions_value.is_array() || regions_value.as_array().is_empty()) + return String { "Malformed profile (regions is not an array, or it is empty)" }; + + auto& perf_events = events_value.as_array(); + if (perf_events.is_empty()) + return String { "No events captured (targeted process was never on CPU)" }; + + auto library_metadata = make<LibraryMetadata>(regions_value.as_array()); + + Vector<Event> events; + + for (auto& perf_event_value : perf_events.values()) { + auto& perf_event = perf_event_value.as_object(); + + Event event; + + event.timestamp = perf_event.get("timestamp").to_number<u64>(); + event.type = perf_event.get("type").to_string(); + + if (event.type == "malloc") { + event.ptr = perf_event.get("ptr").to_number<FlatPtr>(); + event.size = perf_event.get("size").to_number<size_t>(); + } else if (event.type == "free") { + event.ptr = perf_event.get("ptr").to_number<FlatPtr>(); + } + + auto stack_array = perf_event.get("stack").as_array(); + for (ssize_t i = stack_array.values().size() - 1; i >= 0; --i) { + auto& frame = stack_array.at(i); + auto ptr = frame.to_number<u32>(); + u32 offset = 0; + String symbol; + + if (ptr >= 0xc0000000) { + if (kernel_elf) { + symbol = kernel_elf->symbolicate(ptr, &offset); + } else { + symbol = "??"; + } + } else { + symbol = library_metadata->symbolicate(ptr, offset); + } + + event.frames.append({ symbol, ptr, offset }); + } + + if (event.frames.size() < 2) + continue; + + FlatPtr innermost_frame_address = event.frames.at(1).address; + event.in_kernel = innermost_frame_address >= 0xc0000000; + + events.append(move(event)); + } + + return adopt_own(*new Profile(executable_path, move(events), move(library_metadata))); +} + +void ProfileNode::sort_children() +{ + sort_profile_nodes(m_children); +} + +void Profile::set_timestamp_filter_range(u64 start, u64 end) +{ + if (m_has_timestamp_filter_range && m_timestamp_filter_range_start == start && m_timestamp_filter_range_end == end) + return; + m_has_timestamp_filter_range = true; + + m_timestamp_filter_range_start = min(start, end); + m_timestamp_filter_range_end = max(start, end); + + rebuild_tree(); +} + +void Profile::clear_timestamp_filter_range() +{ + if (!m_has_timestamp_filter_range) + return; + m_has_timestamp_filter_range = false; + rebuild_tree(); +} + +void Profile::set_inverted(bool inverted) +{ + if (m_inverted == inverted) + return; + m_inverted = inverted; + rebuild_tree(); +} + +void Profile::set_show_top_functions(bool show) +{ + if (m_show_top_functions == show) + return; + m_show_top_functions = show; + rebuild_tree(); +} + +void Profile::set_show_percentages(bool show_percentages) +{ + if (m_show_percentages == show_percentages) + return; + m_show_percentages = show_percentages; +} + +void Profile::set_disassembly_index(const GUI::ModelIndex& index) +{ + if (m_disassembly_index == index) + return; + m_disassembly_index = index; + auto* node = static_cast<ProfileNode*>(index.internal_data()); + m_disassembly_model = DisassemblyModel::create(*this, *node); +} + +GUI::Model* Profile::disassembly_model() +{ + return m_disassembly_model; +} + +Profile::LibraryMetadata::LibraryMetadata(JsonArray regions) + : m_regions(move(regions)) +{ + for (auto& region_value : m_regions.values()) { + auto& region = region_value.as_object(); + auto base = region.get("base").as_u32(); + auto size = region.get("size").as_u32(); + auto name = region.get("name").as_string(); + + String path; + if (name.contains("Loader.so")) + path = "Loader.so"; + else if (!name.contains(":")) + continue; + else + path = name.substring(0, name.view().find_first_of(":").value()); + + if (name.contains(".so")) + path = String::formatted("/usr/lib/{}", path); + + auto file_or_error = MappedFile::map(path); + if (file_or_error.is_error()) { + m_libraries.set(name, {}); + continue; + } + auto elf = ELF::Image(file_or_error.value()->bytes()); + if (!elf.is_valid()) + continue; + auto library = make<Library>(base, size, name, file_or_error.release_value(), move(elf)); + m_libraries.set(name, move(library)); + } +} + +const Profile::LibraryMetadata::Library* Profile::LibraryMetadata::library_containing(FlatPtr ptr) const +{ + for (auto& it : m_libraries) { + if (!it.value) + continue; + auto& library = *it.value; + if (ptr >= library.base && ptr < (library.base + library.size)) + return &library; + } + return nullptr; +} + +String Profile::LibraryMetadata::symbolicate(FlatPtr ptr, u32& offset) const +{ + if (auto* library = library_containing(ptr)) + return String::formatted("[{}] {}", library->name, library->elf.symbolicate(ptr - library->base, &offset)); + return "??"; +} diff --git a/Userland/DevTools/Profiler/Profile.h b/Userland/DevTools/Profiler/Profile.h new file mode 100644 index 0000000000..0acf3e917f --- /dev/null +++ b/Userland/DevTools/Profiler/Profile.h @@ -0,0 +1,234 @@ +/* + * Copyright (c) 2018-2021, Andreas Kling <kling@serenityos.org> + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * 1. Redistributions of source code must retain the above copyright notice, this + * list of conditions and the following disclaimer. + * + * 2. Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE + * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL + * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR + * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER + * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, + * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE + * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +#pragma once + +#include <AK/Bitmap.h> +#include <AK/JsonArray.h> +#include <AK/JsonObject.h> +#include <AK/JsonValue.h> +#include <AK/MappedFile.h> +#include <AK/NonnullRefPtrVector.h> +#include <AK/OwnPtr.h> +#include <AK/Result.h> +#include <LibELF/Image.h> +#include <LibGUI/Forward.h> +#include <LibGUI/ModelIndex.h> + +class ProfileModel; +class DisassemblyModel; + +class ProfileNode : public RefCounted<ProfileNode> { +public: + static NonnullRefPtr<ProfileNode> create(const String& symbol, u32 address, u32 offset, u64 timestamp) + { + return adopt(*new ProfileNode(symbol, address, offset, timestamp)); + } + + // These functions are only relevant for root nodes + void will_track_seen_events(size_t profile_event_count) + { + if (m_seen_events.size() != profile_event_count) + m_seen_events = Bitmap::create(profile_event_count, false); + } + bool has_seen_event(size_t event_index) const { return m_seen_events.get(event_index); } + void did_see_event(size_t event_index) { m_seen_events.set(event_index, true); } + + const String& symbol() const { return m_symbol; } + u32 address() const { return m_address; } + u32 offset() const { return m_offset; } + u64 timestamp() const { return m_timestamp; } + + u32 event_count() const { return m_event_count; } + u32 self_count() const { return m_self_count; } + + int child_count() const { return m_children.size(); } + const Vector<NonnullRefPtr<ProfileNode>>& children() const { return m_children; } + + void add_child(ProfileNode& child) + { + if (child.m_parent == this) + return; + ASSERT(!child.m_parent); + child.m_parent = this; + m_children.append(child); + } + + ProfileNode& find_or_create_child(const String& symbol, u32 address, u32 offset, u64 timestamp) + { + for (size_t i = 0; i < m_children.size(); ++i) { + auto& child = m_children[i]; + if (child->symbol() == symbol) { + return child; + } + } + auto new_child = ProfileNode::create(symbol, address, offset, timestamp); + add_child(new_child); + return new_child; + }; + + ProfileNode* parent() { return m_parent; } + const ProfileNode* parent() const { return m_parent; } + + void increment_event_count() { ++m_event_count; } + void increment_self_count() { ++m_self_count; } + + void sort_children(); + + const HashMap<FlatPtr, size_t>& events_per_address() const { return m_events_per_address; } + void add_event_address(FlatPtr address) + { + auto it = m_events_per_address.find(address); + if (it == m_events_per_address.end()) + m_events_per_address.set(address, 1); + else + m_events_per_address.set(address, it->value + 1); + } + +private: + explicit ProfileNode(const String& symbol, u32 address, u32 offset, u64 timestamp) + : m_symbol(symbol) + , m_address(address) + , m_offset(offset) + , m_timestamp(timestamp) + { + } + + ProfileNode* m_parent { nullptr }; + String m_symbol; + u32 m_address { 0 }; + u32 m_offset { 0 }; + u32 m_event_count { 0 }; + u32 m_self_count { 0 }; + u64 m_timestamp { 0 }; + Vector<NonnullRefPtr<ProfileNode>> m_children; + HashMap<FlatPtr, size_t> m_events_per_address; + Bitmap m_seen_events; +}; + +class Profile { +public: + static Result<NonnullOwnPtr<Profile>, String> load_from_perfcore_file(const StringView& path); + ~Profile(); + + GUI::Model& model(); + GUI::Model* disassembly_model(); + + void set_disassembly_index(const GUI::ModelIndex&); + + const Vector<NonnullRefPtr<ProfileNode>>& roots() const { return m_roots; } + + struct Frame { + String symbol; + u32 address { 0 }; + u32 offset { 0 }; + }; + + struct Event { + u64 timestamp { 0 }; + String type; + FlatPtr ptr { 0 }; + size_t size { 0 }; + bool in_kernel { false }; + Vector<Frame> frames; + }; + + u32 filtered_event_count() const { return m_filtered_event_count; } + + const Vector<Event>& events() const { return m_events; } + + u64 length_in_ms() const { return m_last_timestamp - m_first_timestamp; } + u64 first_timestamp() const { return m_first_timestamp; } + u64 last_timestamp() const { return m_last_timestamp; } + u32 deepest_stack_depth() const { return m_deepest_stack_depth; } + + void set_timestamp_filter_range(u64 start, u64 end); + void clear_timestamp_filter_range(); + bool has_timestamp_filter_range() const { return m_has_timestamp_filter_range; } + + bool is_inverted() const { return m_inverted; } + void set_inverted(bool); + + void set_show_top_functions(bool); + + bool show_percentages() const { return m_show_percentages; } + void set_show_percentages(bool); + + const String& executable_path() const { return m_executable_path; } + + class LibraryMetadata { + public: + LibraryMetadata(JsonArray regions); + + String symbolicate(FlatPtr ptr, u32& offset) const; + + struct Library { + FlatPtr base; + size_t size; + String name; + NonnullRefPtr<MappedFile> file; + ELF::Image elf; + }; + + const Library* library_containing(FlatPtr) const; + + private: + mutable HashMap<String, OwnPtr<Library>> m_libraries; + JsonArray m_regions; + }; + + const LibraryMetadata& libraries() const { return *m_library_metadata; } + +private: + Profile(String executable_path, Vector<Event>, NonnullOwnPtr<LibraryMetadata>); + + void rebuild_tree(); + + String m_executable_path; + + RefPtr<ProfileModel> m_model; + RefPtr<DisassemblyModel> m_disassembly_model; + + GUI::ModelIndex m_disassembly_index; + + Vector<NonnullRefPtr<ProfileNode>> m_roots; + u32 m_filtered_event_count { 0 }; + u64 m_first_timestamp { 0 }; + u64 m_last_timestamp { 0 }; + + Vector<Event> m_events; + + NonnullOwnPtr<LibraryMetadata> m_library_metadata; + + bool m_has_timestamp_filter_range { false }; + u64 m_timestamp_filter_range_start { 0 }; + u64 m_timestamp_filter_range_end { 0 }; + + u32 m_deepest_stack_depth { 0 }; + bool m_inverted { false }; + bool m_show_top_functions { false }; + bool m_show_percentages { false }; +}; diff --git a/Userland/DevTools/Profiler/ProfileModel.cpp b/Userland/DevTools/Profiler/ProfileModel.cpp new file mode 100644 index 0000000000..caf1819446 --- /dev/null +++ b/Userland/DevTools/Profiler/ProfileModel.cpp @@ -0,0 +1,147 @@ +/* + * Copyright (c) 2018-2020, Andreas Kling <kling@serenityos.org> + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * 1. Redistributions of source code must retain the above copyright notice, this + * list of conditions and the following disclaimer. + * + * 2. Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE + * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL + * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR + * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER + * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, + * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE + * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +#include "ProfileModel.h" +#include "Profile.h" +#include <AK/StringBuilder.h> +#include <ctype.h> +#include <stdio.h> + +ProfileModel::ProfileModel(Profile& profile) + : m_profile(profile) +{ + m_user_frame_icon.set_bitmap_for_size(16, Gfx::Bitmap::load_from_file("/res/icons/16x16/inspector-object.png")); + m_kernel_frame_icon.set_bitmap_for_size(16, Gfx::Bitmap::load_from_file("/res/icons/16x16/inspector-object-red.png")); +} + +ProfileModel::~ProfileModel() +{ +} + +GUI::ModelIndex ProfileModel::index(int row, int column, const GUI::ModelIndex& parent) const +{ + if (!parent.is_valid()) { + if (m_profile.roots().is_empty()) + return {}; + return create_index(row, column, m_profile.roots().at(row).ptr()); + } + auto& remote_parent = *static_cast<ProfileNode*>(parent.internal_data()); + return create_index(row, column, remote_parent.children().at(row).ptr()); +} + +GUI::ModelIndex ProfileModel::parent_index(const GUI::ModelIndex& index) const +{ + if (!index.is_valid()) + return {}; + auto& node = *static_cast<ProfileNode*>(index.internal_data()); + if (!node.parent()) + return {}; + + // NOTE: If the parent has no parent, it's a root, so we have to look among the roots. + if (!node.parent()->parent()) { + for (size_t row = 0; row < m_profile.roots().size(); ++row) { + if (m_profile.roots()[row].ptr() == node.parent()) { + return create_index(row, index.column(), node.parent()); + } + } + ASSERT_NOT_REACHED(); + return {}; + } + + for (size_t row = 0; row < node.parent()->parent()->children().size(); ++row) { + if (node.parent()->parent()->children()[row].ptr() == node.parent()) + return create_index(row, index.column(), node.parent()); + } + + ASSERT_NOT_REACHED(); + return {}; +} + +int ProfileModel::row_count(const GUI::ModelIndex& index) const +{ + if (!index.is_valid()) + return m_profile.roots().size(); + auto& node = *static_cast<ProfileNode*>(index.internal_data()); + return node.children().size(); +} + +int ProfileModel::column_count(const GUI::ModelIndex&) const +{ + return Column::__Count; +} + +String ProfileModel::column_name(int column) const +{ + switch (column) { + case Column::SampleCount: + return m_profile.show_percentages() ? "% Samples" : "# Samples"; + case Column::SelfCount: + return m_profile.show_percentages() ? "% Self" : "# Self"; + case Column::StackFrame: + return "Stack Frame"; + default: + ASSERT_NOT_REACHED(); + return {}; + } +} + +GUI::Variant ProfileModel::data(const GUI::ModelIndex& index, GUI::ModelRole role) const +{ + auto* node = static_cast<ProfileNode*>(index.internal_data()); + if (role == GUI::ModelRole::TextAlignment) { + if (index.column() == Column::SampleCount || index.column() == Column::SelfCount) + return Gfx::TextAlignment::CenterRight; + } + if (role == GUI::ModelRole::Icon) { + if (index.column() == Column::StackFrame) { + if (node->address() >= 0xc0000000) + return m_kernel_frame_icon; + return m_user_frame_icon; + } + return {}; + } + if (role == GUI::ModelRole::Display) { + if (index.column() == Column::SampleCount) { + if (m_profile.show_percentages()) + return ((float)node->event_count() / (float)m_profile.filtered_event_count()) * 100.0f; + return node->event_count(); + } + if (index.column() == Column::SelfCount) { + if (m_profile.show_percentages()) + return ((float)node->self_count() / (float)m_profile.filtered_event_count()) * 100.0f; + return node->self_count(); + } + if (index.column() == Column::StackFrame) + return node->symbol(); + return {}; + } + return {}; +} + +void ProfileModel::update() +{ + did_update(Model::InvalidateAllIndexes); +} diff --git a/Userland/DevTools/Profiler/ProfileModel.h b/Userland/DevTools/Profiler/ProfileModel.h new file mode 100644 index 0000000000..9b61dd5137 --- /dev/null +++ b/Userland/DevTools/Profiler/ProfileModel.h @@ -0,0 +1,65 @@ +/* + * Copyright (c) 2018-2020, Andreas Kling <kling@serenityos.org> + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * 1. Redistributions of source code must retain the above copyright notice, this + * list of conditions and the following disclaimer. + * + * 2. Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE + * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL + * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR + * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER + * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, + * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE + * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +#pragma once + +#include <LibGUI/Model.h> + +class Profile; + +class ProfileModel final : public GUI::Model { +public: + static NonnullRefPtr<ProfileModel> create(Profile& profile) + { + return adopt(*new ProfileModel(profile)); + } + + enum Column { + SampleCount, + SelfCount, + StackFrame, + __Count + }; + + virtual ~ProfileModel() override; + + virtual int row_count(const GUI::ModelIndex& = GUI::ModelIndex()) const override; + virtual int column_count(const GUI::ModelIndex& = GUI::ModelIndex()) const override; + virtual String column_name(int) const override; + virtual GUI::Variant data(const GUI::ModelIndex&, GUI::ModelRole) const override; + virtual GUI::ModelIndex index(int row, int column, const GUI::ModelIndex& parent = GUI::ModelIndex()) const override; + virtual GUI::ModelIndex parent_index(const GUI::ModelIndex&) const override; + virtual void update() override; + virtual int tree_column() const override { return Column::StackFrame; } + +private: + explicit ProfileModel(Profile&); + + Profile& m_profile; + + GUI::Icon m_user_frame_icon; + GUI::Icon m_kernel_frame_icon; +}; diff --git a/Userland/DevTools/Profiler/ProfileTimelineWidget.cpp b/Userland/DevTools/Profiler/ProfileTimelineWidget.cpp new file mode 100644 index 0000000000..7d4552d300 --- /dev/null +++ b/Userland/DevTools/Profiler/ProfileTimelineWidget.cpp @@ -0,0 +1,110 @@ +/* + * Copyright (c) 2018-2020, Andreas Kling <kling@serenityos.org> + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * 1. Redistributions of source code must retain the above copyright notice, this + * list of conditions and the following disclaimer. + * + * 2. Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE + * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL + * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR + * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER + * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, + * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE + * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +#include "ProfileTimelineWidget.h" +#include "Profile.h" +#include <LibGUI/Painter.h> + +ProfileTimelineWidget::ProfileTimelineWidget(Profile& profile) + : m_profile(profile) +{ + set_fill_with_background_color(true); + set_fixed_height(80); +} + +ProfileTimelineWidget::~ProfileTimelineWidget() +{ +} + +void ProfileTimelineWidget::paint_event(GUI::PaintEvent& event) +{ + GUI::Frame::paint_event(event); + + GUI::Painter painter(*this); + painter.add_clip_rect(event.rect()); + + float column_width = (float)frame_inner_rect().width() / (float)m_profile.length_in_ms(); + float frame_height = (float)frame_inner_rect().height() / (float)m_profile.deepest_stack_depth(); + + for (auto& event : m_profile.events()) { + u64 t = event.timestamp - m_profile.first_timestamp(); + int x = (int)((float)t * column_width); + int cw = max(1, (int)column_width); + + int column_height = frame_inner_rect().height() - (int)((float)event.frames.size() * frame_height); + + bool in_kernel = event.in_kernel; + Color color = in_kernel ? Color::from_rgb(0xc25e5a) : Color::from_rgb(0x5a65c2); + for (int i = 0; i < cw; ++i) + painter.draw_line({ x + i, frame_thickness() + column_height }, { x + i, height() - frame_thickness() * 2 }, color); + } + + u64 normalized_start_time = min(m_select_start_time, m_select_end_time); + u64 normalized_end_time = max(m_select_start_time, m_select_end_time); + + int select_start_x = (int)((float)(normalized_start_time - m_profile.first_timestamp()) * column_width); + int select_end_x = (int)((float)(normalized_end_time - m_profile.first_timestamp()) * column_width); + painter.fill_rect({ select_start_x, frame_thickness(), select_end_x - select_start_x, height() - frame_thickness() * 2 }, Color(0, 0, 0, 60)); +} + +u64 ProfileTimelineWidget::timestamp_at_x(int x) const +{ + float column_width = (float)frame_inner_rect().width() / (float)m_profile.length_in_ms(); + float ms_into_profile = (float)x / column_width; + return m_profile.first_timestamp() + (u64)ms_into_profile; +} + +void ProfileTimelineWidget::mousedown_event(GUI::MouseEvent& event) +{ + if (event.button() != GUI::MouseButton::Left) + return; + + m_selecting = true; + m_select_start_time = timestamp_at_x(event.x()); + m_select_end_time = m_select_start_time; + m_profile.set_timestamp_filter_range(m_select_start_time, m_select_end_time); + update(); +} + +void ProfileTimelineWidget::mousemove_event(GUI::MouseEvent& event) +{ + if (!m_selecting) + return; + + m_select_end_time = timestamp_at_x(event.x()); + m_profile.set_timestamp_filter_range(m_select_start_time, m_select_end_time); + update(); +} + +void ProfileTimelineWidget::mouseup_event(GUI::MouseEvent& event) +{ + if (event.button() != GUI::MouseButton::Left) + return; + + m_selecting = false; + if (m_select_start_time == m_select_end_time) + m_profile.clear_timestamp_filter_range(); +} diff --git a/Userland/DevTools/Profiler/ProfileTimelineWidget.h b/Userland/DevTools/Profiler/ProfileTimelineWidget.h new file mode 100644 index 0000000000..056e07769e --- /dev/null +++ b/Userland/DevTools/Profiler/ProfileTimelineWidget.h @@ -0,0 +1,53 @@ +/* + * Copyright (c) 2018-2020, Andreas Kling <kling@serenityos.org> + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * 1. Redistributions of source code must retain the above copyright notice, this + * list of conditions and the following disclaimer. + * + * 2. Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE + * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL + * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR + * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER + * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, + * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE + * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +#pragma once + +#include <LibGUI/Frame.h> + +class Profile; + +class ProfileTimelineWidget final : public GUI::Frame { + C_OBJECT(ProfileTimelineWidget) +public: + virtual ~ProfileTimelineWidget() override; + +private: + virtual void paint_event(GUI::PaintEvent&) override; + virtual void mousedown_event(GUI::MouseEvent&) override; + virtual void mousemove_event(GUI::MouseEvent&) override; + virtual void mouseup_event(GUI::MouseEvent&) override; + + explicit ProfileTimelineWidget(Profile&); + + u64 timestamp_at_x(int x) const; + + Profile& m_profile; + + bool m_selecting { false }; + u64 m_select_start_time { 0 }; + u64 m_select_end_time { 0 }; +}; diff --git a/Userland/DevTools/Profiler/main.cpp b/Userland/DevTools/Profiler/main.cpp new file mode 100644 index 0000000000..5f801936c0 --- /dev/null +++ b/Userland/DevTools/Profiler/main.cpp @@ -0,0 +1,220 @@ +/* + * Copyright (c) 2018-2021, Andreas Kling <kling@serenityos.org> + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * 1. Redistributions of source code must retain the above copyright notice, this + * list of conditions and the following disclaimer. + * + * 2. Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE + * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL + * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR + * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER + * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, + * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE + * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +#include "Profile.h" +#include "ProfileTimelineWidget.h" +#include <LibCore/ArgsParser.h> +#include <LibCore/ElapsedTimer.h> +#include <LibCore/EventLoop.h> +#include <LibCore/ProcessStatisticsReader.h> +#include <LibCore/Timer.h> +#include <LibGUI/Action.h> +#include <LibGUI/Application.h> +#include <LibGUI/BoxLayout.h> +#include <LibGUI/Button.h> +#include <LibGUI/Desktop.h> +#include <LibGUI/Label.h> +#include <LibGUI/Menu.h> +#include <LibGUI/MenuBar.h> +#include <LibGUI/MessageBox.h> +#include <LibGUI/Model.h> +#include <LibGUI/ProcessChooser.h> +#include <LibGUI/Splitter.h> +#include <LibGUI/TableView.h> +#include <LibGUI/TreeView.h> +#include <LibGUI/Window.h> +#include <serenity.h> +#include <stdio.h> +#include <string.h> + +static bool generate_profile(pid_t& pid); + +int main(int argc, char** argv) +{ + Core::ArgsParser args_parser; + int pid = 0; + args_parser.add_option(pid, "PID to profile", "pid", 'p', "PID"); + args_parser.parse(argc, argv, false); + + auto app = GUI::Application::construct(argc, argv); + auto app_icon = GUI::Icon::default_icon("app-profiler"); + + String path; + if (argc != 2) { + if (!generate_profile(pid)) + return 0; + path = String::formatted("/proc/{}/perf_events", pid); + } else { + path = argv[1]; + } + + auto profile_or_error = Profile::load_from_perfcore_file(path); + if (profile_or_error.is_error()) { + GUI::MessageBox::show(nullptr, profile_or_error.error(), "Profiler", GUI::MessageBox::Type::Error); + return 0; + } + + auto& profile = profile_or_error.value(); + + auto window = GUI::Window::construct(); + window->set_title("Profiler"); + window->set_icon(app_icon.bitmap_for_size(16)); + window->resize(800, 600); + + auto& main_widget = window->set_main_widget<GUI::Widget>(); + main_widget.set_fill_with_background_color(true); + main_widget.set_layout<GUI::VerticalBoxLayout>(); + + main_widget.add<ProfileTimelineWidget>(*profile); + + auto& bottom_splitter = main_widget.add<GUI::VerticalSplitter>(); + + auto& tree_view = bottom_splitter.add<GUI::TreeView>(); + tree_view.set_should_fill_selected_rows(true); + tree_view.set_column_headers_visible(true); + tree_view.set_model(profile->model()); + + auto& disassembly_view = bottom_splitter.add<GUI::TableView>(); + + tree_view.on_selection = [&](auto& index) { + profile->set_disassembly_index(index); + disassembly_view.set_model(profile->disassembly_model()); + }; + + auto menubar = GUI::MenuBar::construct(); + auto& app_menu = menubar->add_menu("Profiler"); + app_menu.add_action(GUI::CommonActions::make_quit_action([&](auto&) { app->quit(); })); + + auto& view_menu = menubar->add_menu("View"); + + auto update_window_title = [&](auto title, bool is_checked) { + StringBuilder name; + name.append("Profiler"); + if (is_checked) { + name.append(" - "); + name.append(title); + } + window->set_title(name.to_string()); + }; + + auto invert_action = GUI::Action::create_checkable("Invert tree", { Mod_Ctrl, Key_I }, [&](auto& action) { + profile->set_inverted(action.is_checked()); + update_window_title("Invert tree", action.is_checked()); + }); + invert_action->set_checked(false); + view_menu.add_action(invert_action); + + auto top_functions_action = GUI::Action::create_checkable("Top functions", { Mod_Ctrl, Key_T }, [&](auto& action) { + profile->set_show_top_functions(action.is_checked()); + update_window_title("Top functions", action.is_checked()); + }); + top_functions_action->set_checked(false); + view_menu.add_action(top_functions_action); + + auto percent_action = GUI::Action::create_checkable("Show percentages", { Mod_Ctrl, Key_P }, [&](auto& action) { + profile->set_show_percentages(action.is_checked()); + tree_view.update(); + disassembly_view.update(); + update_window_title("Show percentages", action.is_checked()); + }); + percent_action->set_checked(false); + view_menu.add_action(percent_action); + + auto& help_menu = menubar->add_menu("Help"); + help_menu.add_action(GUI::CommonActions::make_about_action("Profiler", app_icon, window)); + + app->set_menubar(move(menubar)); + + window->show(); + return app->exec(); +} + +static bool prompt_to_stop_profiling(pid_t pid, const String& process_name) +{ + auto window = GUI::Window::construct(); + window->set_title(String::formatted("Profiling {}({})", process_name, pid)); + window->resize(240, 100); + window->set_icon(Gfx::Bitmap::load_from_file("/res/icons/16x16/app-profiler.png")); + window->center_on_screen(); + + auto& widget = window->set_main_widget<GUI::Widget>(); + widget.set_fill_with_background_color(true); + auto& layout = widget.set_layout<GUI::VerticalBoxLayout>(); + layout.set_margins(GUI::Margins(0, 0, 0, 16)); + + auto& timer_label = widget.add<GUI::Label>("..."); + Core::ElapsedTimer clock; + clock.start(); + auto update_timer = Core::Timer::construct(100, [&] { + timer_label.set_text(String::format("%.1f seconds", (float)clock.elapsed() / 1000.0f)); + }); + + auto& stop_button = widget.add<GUI::Button>("Stop"); + stop_button.set_fixed_size(140, 22); + stop_button.on_click = [&](auto) { + GUI::Application::the()->quit(); + }; + + window->show(); + return GUI::Application::the()->exec() == 0; +} + +bool generate_profile(pid_t& pid) +{ + if (!pid) { + auto process_chooser = GUI::ProcessChooser::construct("Profiler", "Profile", Gfx::Bitmap::load_from_file("/res/icons/16x16/app-profiler.png")); + if (process_chooser->exec() == GUI::Dialog::ExecCancel) + return false; + pid = process_chooser->pid(); + } + + String process_name; + + auto all_processes = Core::ProcessStatisticsReader::get_all(); + if (all_processes.has_value()) { + if (auto it = all_processes.value().find(pid); it != all_processes.value().end()) + process_name = it->value.name; + else + process_name = "(unknown)"; + } else { + process_name = "(unknown)"; + } + + if (profiling_enable(pid) < 0) { + int saved_errno = errno; + GUI::MessageBox::show(nullptr, String::formatted("Unable to profile process {}({}): {}", process_name, pid, strerror(saved_errno)), "Profiler", GUI::MessageBox::Type::Error); + return false; + } + + if (!prompt_to_stop_profiling(pid, process_name)) + return false; + + if (profiling_disable(pid) < 0) { + return false; + } + + return true; +} diff --git a/Userland/DevTools/UserspaceEmulator/CMakeLists.txt b/Userland/DevTools/UserspaceEmulator/CMakeLists.txt new file mode 100644 index 0000000000..dd2a2b37ad --- /dev/null +++ b/Userland/DevTools/UserspaceEmulator/CMakeLists.txt @@ -0,0 +1,14 @@ +set(SOURCES + Emulator.cpp + MallocTracer.cpp + MmapRegion.cpp + Region.cpp + SharedBufferRegion.cpp + SimpleRegion.cpp + SoftCPU.cpp + SoftMMU.cpp + main.cpp +) + +serenity_bin(UserspaceEmulator) +target_link_libraries(UserspaceEmulator LibX86 LibDebug LibCore LibPthread) diff --git a/Userland/DevTools/UserspaceEmulator/Emulator.cpp b/Userland/DevTools/UserspaceEmulator/Emulator.cpp new file mode 100644 index 0000000000..c463d24618 --- /dev/null +++ b/Userland/DevTools/UserspaceEmulator/Emulator.cpp @@ -0,0 +1,1816 @@ +/* + * Copyright (c) 2020, Andreas Kling <kling@serenityos.org> + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * 1. Redistributions of source code must retain the above copyright notice, this + * list of conditions and the following disclaimer. + * + * 2. Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE + * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL + * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR + * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER + * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, + * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE + * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +#include "Emulator.h" +#include "MmapRegion.h" +#include "SharedBufferRegion.h" +#include "SimpleRegion.h" +#include "SoftCPU.h" +#include <AK/Format.h> +#include <AK/LexicalPath.h> +#include <AK/MappedFile.h> +#include <Kernel/API/Syscall.h> +#include <LibELF/AuxiliaryVector.h> +#include <LibELF/Image.h> +#include <LibELF/Validation.h> +#include <LibPthread/pthread.h> +#include <LibX86/ELFSymbolProvider.h> +#include <fcntl.h> +#include <net/if.h> +#include <net/route.h> +#include <sched.h> +#include <serenity.h> +#include <stdio.h> +#include <string.h> +#include <strings.h> +#include <sys/ioctl.h> +#include <sys/mman.h> +#include <sys/select.h> +#include <sys/socket.h> +#include <sys/stat.h> +#include <sys/time.h> +#include <sys/uio.h> +#include <termios.h> +#include <unistd.h> + +#if defined(__GNUC__) && !defined(__clang__) +# pragma GCC optimize("O3") +#endif + +// #define DEBUG_SPAM + +namespace UserspaceEmulator { + +static constexpr u32 stack_location = 0x10000000; +static constexpr size_t stack_size = 64 * KiB; + +static Emulator* s_the; + +Emulator& Emulator::the() +{ + ASSERT(s_the); + return *s_the; +} + +Emulator::Emulator(const String& executable_path, const Vector<String>& arguments, const Vector<String>& environment) + : m_executable_path(executable_path) + , m_arguments(arguments) + , m_environment(environment) + , m_mmu(*this) + , m_cpu(*this) +{ + m_malloc_tracer = make<MallocTracer>(*this); + ASSERT(!s_the); + s_the = this; + // setup_stack(arguments, environment); + register_signal_handlers(); + setup_signal_trampoline(); +} + +Vector<ELF::AuxiliaryValue> Emulator::generate_auxiliary_vector(FlatPtr load_base, FlatPtr entry_eip, String executable_path, int executable_fd) const +{ + // FIXME: This is not fully compatible with the auxiliary vector the kernel generates, this is just the bare + // minimum to get the loader going. + Vector<ELF::AuxiliaryValue> auxv; + // PHDR/EXECFD + // PH* + auxv.append({ ELF::AuxiliaryValue::PageSize, PAGE_SIZE }); + auxv.append({ ELF::AuxiliaryValue::BaseAddress, (void*)load_base }); + + auxv.append({ ELF::AuxiliaryValue::Entry, (void*)entry_eip }); + + // FIXME: Don't hard code this? We might support other platforms later.. (e.g. x86_64) + auxv.append({ ELF::AuxiliaryValue::Platform, "i386" }); + + auxv.append({ ELF::AuxiliaryValue::ExecFilename, executable_path }); + + auxv.append({ ELF::AuxiliaryValue::ExecFileDescriptor, executable_fd }); + + auxv.append({ ELF::AuxiliaryValue::Null, 0L }); + return auxv; +} + +void Emulator::setup_stack(Vector<ELF::AuxiliaryValue> aux_vector) +{ + auto stack_region = make<SimpleRegion>(stack_location, stack_size); + stack_region->set_stack(true); + m_mmu.add_region(move(stack_region)); + m_cpu.set_esp(shadow_wrap_as_initialized<u32>(stack_location + stack_size)); + + Vector<u32> argv_entries; + + for (auto& argument : m_arguments) { + m_cpu.push_string(argument.characters()); + argv_entries.append(m_cpu.esp().value()); + } + + Vector<u32> env_entries; + + for (auto& variable : m_environment) { + m_cpu.push_string(variable.characters()); + env_entries.append(m_cpu.esp().value()); + } + + for (auto& auxv : aux_vector) { + if (!auxv.optional_string.is_empty()) { + m_cpu.push_string(auxv.optional_string.characters()); + auxv.auxv.a_un.a_ptr = (void*)m_cpu.esp().value(); + } + } + + for (ssize_t i = aux_vector.size() - 1; i >= 0; --i) { + auto& value = aux_vector[i].auxv; + m_cpu.push_buffer((const u8*)&value, sizeof(value)); + } + + m_cpu.push32(shadow_wrap_as_initialized<u32>(0)); // char** envp = { envv_entries..., nullptr } + for (ssize_t i = env_entries.size() - 1; i >= 0; --i) + m_cpu.push32(shadow_wrap_as_initialized(env_entries[i])); + u32 envp = m_cpu.esp().value(); + + m_cpu.push32(shadow_wrap_as_initialized<u32>(0)); // char** argv = { argv_entries..., nullptr } + for (ssize_t i = argv_entries.size() - 1; i >= 0; --i) + m_cpu.push32(shadow_wrap_as_initialized(argv_entries[i])); + u32 argv = m_cpu.esp().value(); + + m_cpu.push32(shadow_wrap_as_initialized<u32>(0)); // (alignment) + + u32 argc = argv_entries.size(); + m_cpu.push32(shadow_wrap_as_initialized(envp)); + m_cpu.push32(shadow_wrap_as_initialized(argv)); + m_cpu.push32(shadow_wrap_as_initialized(argc)); + m_cpu.push32(shadow_wrap_as_initialized<u32>(0)); // (alignment) +} + +bool Emulator::load_elf() +{ + auto file_or_error = MappedFile::map(m_executable_path); + if (file_or_error.is_error()) { + reportln("Unable to map {}: {}", m_executable_path, file_or_error.error()); + return false; + } + + auto elf_image_data = file_or_error.value()->bytes(); + ELF::Image executable_elf(elf_image_data); + + if (!executable_elf.is_dynamic()) { + // FIXME: Support static objects + ASSERT_NOT_REACHED(); + } + + String interpreter_path; + if (!ELF::validate_program_headers(*(const Elf32_Ehdr*)elf_image_data.data(), elf_image_data.size(), (const u8*)elf_image_data.data(), elf_image_data.size(), &interpreter_path)) { + reportln("failed to validate ELF file"); + return false; + } + + ASSERT(!interpreter_path.is_null()); + dbgln("interpreter: {}", interpreter_path); + + auto interpreter_file_or_error = MappedFile::map(interpreter_path); + ASSERT(!interpreter_file_or_error.is_error()); + auto interpreter_image_data = interpreter_file_or_error.value()->bytes(); + ELF::Image interpreter_image(interpreter_image_data); + + constexpr FlatPtr interpreter_load_offset = 0x08000000; + interpreter_image.for_each_program_header([&](const ELF::Image::ProgramHeader& program_header) { + // Loader is not allowed to have its own TLS regions + ASSERT(program_header.type() != PT_TLS); + + if (program_header.type() == PT_LOAD) { + auto region = make<SimpleRegion>(program_header.vaddr().offset(interpreter_load_offset).get(), program_header.size_in_memory()); + if (program_header.is_executable() && !program_header.is_writable()) + region->set_text(true); + memcpy(region->data(), program_header.raw_data(), program_header.size_in_image()); + memset(region->shadow_data(), 0x01, program_header.size_in_memory()); + if (program_header.is_executable()) { + m_loader_text_base = region->base(); + m_loader_text_size = region->size(); + } + mmu().add_region(move(region)); + return IterationDecision::Continue; + } + + return IterationDecision::Continue; + }); + + auto entry_point = interpreter_image.entry().offset(interpreter_load_offset).get(); + m_cpu.set_eip(entry_point); + + // executable_fd will be used by the loader + int executable_fd = open(m_executable_path.characters(), O_RDONLY); + if (executable_fd < 0) + return false; + + auto aux_vector = generate_auxiliary_vector(interpreter_load_offset, entry_point, m_executable_path, executable_fd); + setup_stack(move(aux_vector)); + + return true; +} + +int Emulator::exec() +{ + // X86::ELFSymbolProvider symbol_provider(*m_elf); + X86::ELFSymbolProvider* symbol_provider = nullptr; + + bool trace = false; + + while (!m_shutdown) { + m_cpu.save_base_eip(); + + auto insn = X86::Instruction::from_stream(m_cpu, true, true); + + if (trace) + outln("{:p} \033[33;1m{}\033[0m", m_cpu.base_eip(), insn.to_string(m_cpu.base_eip(), symbol_provider)); + + (m_cpu.*insn.handler())(insn); + + if (trace) + m_cpu.dump(); + + if (m_pending_signals) + dispatch_one_pending_signal(); + } + + if (auto* tracer = malloc_tracer()) + tracer->dump_leak_report(); + + return m_exit_status; +} + +Vector<FlatPtr> Emulator::raw_backtrace() +{ + Vector<FlatPtr, 128> backtrace; + backtrace.append(m_cpu.base_eip()); + + // FIXME: Maybe do something if the backtrace has uninitialized data in the frame chain. + + u32 frame_ptr = m_cpu.ebp().value(); + while (frame_ptr) { + u32 ret_ptr = m_mmu.read32({ 0x23, frame_ptr + 4 }).value(); + if (!ret_ptr) + break; + backtrace.append(ret_ptr); + frame_ptr = m_mmu.read32({ 0x23, frame_ptr }).value(); + } + return backtrace; +} + +const MmapRegion* Emulator::find_text_region(FlatPtr address) +{ + const MmapRegion* matching_region = nullptr; + mmu().for_each_region([&](auto& region) { + if (!is<MmapRegion>(region)) + return IterationDecision::Continue; + const auto& mmap_region = static_cast<const MmapRegion&>(region); + if (!(mmap_region.is_executable() && address >= mmap_region.base() && address < mmap_region.base() + mmap_region.size())) + return IterationDecision::Continue; + matching_region = &mmap_region; + return IterationDecision::Break; + }); + return matching_region; +} + +String Emulator::create_backtrace_line(FlatPtr address) +{ + String minimal = String::format("=={%d}== %p", getpid(), (void*)address); + const auto* region = find_text_region(address); + if (!region) + return minimal; + auto separator_index = region->name().index_of(":"); + if (!separator_index.has_value()) + return minimal; + + String lib_name = region->name().substring(0, separator_index.value()); + String lib_path = lib_name; + if (region->name().contains(".so")) + lib_path = String::formatted("/usr/lib/{}", lib_path); + + if (!m_dynamic_library_cache.contains(lib_path)) { + auto file_or_error = MappedFile::map(lib_path); + if (file_or_error.is_error()) + return minimal; + + auto debug_info = make<Debug::DebugInfo>(make<ELF::Image>(file_or_error.value()->bytes())); + m_dynamic_library_cache.set(lib_path, CachedELF { file_or_error.release_value(), move(debug_info) }); + } + + auto it = m_dynamic_library_cache.find(lib_path); + auto& elf = it->value.debug_info->elf(); + String symbol = elf.symbolicate(address - region->base()); + + auto line_without_source_info = String::format("=={%d}== %p [%s]: %s", getpid(), (void*)address, lib_name.characters(), symbol.characters()); + + auto source_position = it->value.debug_info->get_source_position(address - region->base()); + if (source_position.has_value()) + return String::format("=={%d}== %p [%s]: %s (\033[34;1m%s\033[0m:%zu)", getpid(), (void*)address, lib_name.characters(), symbol.characters(), LexicalPath(source_position.value().file_path).basename().characters(), source_position.value().line_number); + + return line_without_source_info; +} + +void Emulator::dump_backtrace(const Vector<FlatPtr>& backtrace) +{ + for (auto& address : backtrace) { + reportln("{}", create_backtrace_line(address)); + } +} + +void Emulator::dump_backtrace() +{ + dump_backtrace(raw_backtrace()); +} + +u32 Emulator::virt_syscall(u32 function, u32 arg1, u32 arg2, u32 arg3) +{ +#ifdef DEBUG_SPAM + reportln("Syscall: {} ({:x})", Syscall::to_string((Syscall::Function)function), function); +#endif + switch (function) { + case SC_chdir: + return virt$chdir(arg1, arg2); + case SC_dup2: + return virt$dup2(arg1, arg2); + case SC_get_stack_bounds: + return virt$get_stack_bounds(arg1, arg2); + case SC_access: + return virt$access(arg1, arg2, arg3); + case SC_waitid: + return virt$waitid(arg1); + case SC_getcwd: + return virt$getcwd(arg1, arg2); + case SC_ttyname: + return virt$ttyname(arg1, arg2, arg3); + case SC_getpgrp: + return virt$getpgrp(); + case SC_getpgid: + return virt$getpgid(arg1); + case SC_setpgid: + return virt$setpgid(arg1, arg2); + case SC_execve: + return virt$execve(arg1); + case SC_sigaction: + return virt$sigaction(arg1, arg2, arg3); + case SC_sigreturn: + return virt$sigreturn(); + case SC_stat: + return virt$stat(arg1); + case SC_realpath: + return virt$realpath(arg1); + case SC_gethostname: + return virt$gethostname(arg1, arg2); + case SC_ioctl: + return virt$ioctl(arg1, arg2, arg3); + case SC_get_dir_entries: + return virt$get_dir_entries(arg1, arg2, arg3); + case SC_shbuf_create: + return virt$shbuf_create(arg1, arg2); + case SC_shbuf_allow_pid: + return virt$shbuf_allow_pid(arg1, arg2); + case SC_shbuf_allow_all: + return virt$shbuf_allow_all(arg1); + case SC_shbuf_get: + return virt$shbuf_get(arg1, arg2); + case SC_shbuf_release: + return virt$shbuf_release(arg1); + case SC_shbuf_seal: + return virt$shbuf_seal(arg1); + case SC_shbuf_set_volatile: + return virt$shbuf_set_volatile(arg1, arg2); + case SC_profiling_enable: + return virt$profiling_enable(arg1); + case SC_profiling_disable: + return virt$profiling_disable(arg1); + case SC_disown: + return virt$disown(arg1); + case SC_purge: + return virt$purge(arg1); + case SC_mmap: + return virt$mmap(arg1); + case SC_mount: + return virt$mount(arg1); + case SC_munmap: + return virt$munmap(arg1, arg2); + case SC_mremap: + return virt$mremap(arg1); + case SC_gettid: + return virt$gettid(); + case SC_getpid: + return virt$getpid(); + case SC_getsid: + return virt$getsid(arg1); + case SC_pledge: + return virt$pledge(arg1); + case SC_unveil: + return virt$unveil(arg1); + case SC_getuid: + return virt$getuid(); + case SC_geteuid: + return virt$geteuid(); + case SC_getgid: + return virt$getgid(); + case SC_getegid: + return virt$getegid(); + case SC_setuid: + return virt$setuid(arg1); + case SC_setgid: + return virt$setgid(arg2); + case SC_close: + return virt$close(arg1); + case SC_fstat: + return virt$fstat(arg1, arg2); + case SC_mkdir: + return virt$mkdir(arg1, arg2, arg3); + case SC_unlink: + return virt$unlink(arg1, arg2); + case SC_write: + return virt$write(arg1, arg2, arg3); + case SC_read: + return virt$read(arg1, arg2, arg3); + case SC_mprotect: + return virt$mprotect(arg1, arg2, arg3); + case SC_madvise: + return virt$madvise(arg1, arg2, arg3); + case SC_open: + return virt$open(arg1); + case SC_pipe: + return virt$pipe(arg1, arg2); + case SC_fcntl: + return virt$fcntl(arg1, arg2, arg3); + case SC_getgroups: + return virt$getgroups(arg1, arg2); + case SC_setgroups: + return virt$setgroups(arg1, arg2); + case SC_lseek: + return virt$lseek(arg1, arg2, arg3); + case SC_socket: + return virt$socket(arg1, arg2, arg3); + case SC_getsockopt: + return virt$getsockopt(arg1); + case SC_get_process_name: + return virt$get_process_name(arg1, arg2); + case SC_dbgputstr: + return virt$dbgputstr(arg1, arg2); + case SC_dbgputch: + return virt$dbgputch(arg1); + case SC_chmod: + return virt$chmod(arg1, arg2, arg3); + case SC_fchmod: + return virt$fchmod(arg1, arg2); + case SC_fchown: + return virt$fchown(arg1, arg2, arg3); + case SC_accept: + return virt$accept(arg1, arg2, arg3); + case SC_setsockopt: + return virt$setsockopt(arg1); + case SC_bind: + return virt$bind(arg1, arg2, arg3); + case SC_connect: + return virt$connect(arg1, arg2, arg3); + case SC_listen: + return virt$listen(arg1, arg2); + case SC_select: + return virt$select(arg1); + case SC_recvmsg: + return virt$recvmsg(arg1, arg2, arg3); + case SC_sendmsg: + return virt$sendmsg(arg1, arg2, arg3); + case SC_kill: + return virt$kill(arg1, arg2); + case SC_set_mmap_name: + return virt$set_mmap_name(arg1); + case SC_exit: + virt$exit((int)arg1); + return 0; + case SC_gettimeofday: + return virt$gettimeofday(arg1); + case SC_clock_gettime: + return virt$clock_gettime(arg1, arg2); + case SC_clock_settime: + return virt$clock_settime(arg1, arg2); + case SC_getrandom: + return virt$getrandom(arg1, arg2, arg3); + case SC_fork: + return virt$fork(); + case SC_sched_getparam: + return virt$sched_getparam(arg1, arg2); + case SC_sched_setparam: + return virt$sched_setparam(arg1, arg2); + case SC_set_thread_name: + return virt$set_thread_name(arg1, arg2, arg3); + case SC_setsid: + return virt$setsid(); + case SC_watch_file: + return virt$watch_file(arg1, arg2); + case SC_clock_nanosleep: + return virt$clock_nanosleep(arg1); + case SC_readlink: + return virt$readlink(arg1); + case SC_ptsname: + return virt$ptsname(arg1, arg2, arg3); + case SC_allocate_tls: + return virt$allocate_tls(arg1); + case SC_beep: + return virt$beep(); + case SC_ftruncate: + return virt$ftruncate(arg1, arg2); + case SC_umask: + return virt$umask(arg1); + default: + reportln("\n=={}== \033[31;1mUnimplemented syscall: {}\033[0m, {:p}", getpid(), Syscall::to_string((Syscall::Function)function), function); + dump_backtrace(); + TODO(); + } +} + +int Emulator::virt$shbuf_create(int size, FlatPtr buffer) +{ + u8* host_data = nullptr; + int shbuf_id = syscall(SC_shbuf_create, size, &host_data); + if (shbuf_id < 0) + return shbuf_id; + FlatPtr address = allocate_vm(size, PAGE_SIZE); + auto region = SharedBufferRegion::create_with_shbuf_id(address, size, shbuf_id, host_data); + m_mmu.add_region(move(region)); + m_mmu.copy_to_vm(buffer, &address, sizeof(address)); + return shbuf_id; +} + +FlatPtr Emulator::virt$shbuf_get(int shbuf_id, FlatPtr size_ptr) +{ + size_t host_size = 0; + void* host_data = (void*)syscall(SC_shbuf_get, shbuf_id, &host_size); + if (host_data == (void*)-1) + return (FlatPtr)host_data; + FlatPtr address = allocate_vm(host_size, PAGE_SIZE); + auto region = SharedBufferRegion::create_with_shbuf_id(address, host_size, shbuf_id, (u8*)host_data); + m_mmu.add_region(move(region)); + m_mmu.copy_to_vm(size_ptr, &host_size, sizeof(host_size)); + return address; +} + +int Emulator::virt$shbuf_allow_pid(int shbuf_id, pid_t peer_pid) +{ + auto* region = m_mmu.shbuf_region(shbuf_id); + ASSERT(region); + return region->allow_pid(peer_pid); +} + +int Emulator::virt$shbuf_allow_all(int shbuf_id) +{ + auto* region = m_mmu.shbuf_region(shbuf_id); + ASSERT(region); + return region->allow_all(); +} + +int Emulator::virt$shbuf_release(int shbuf_id) +{ + auto* region = m_mmu.shbuf_region(shbuf_id); + ASSERT(region); + auto rc = region->release(); + m_mmu.remove_region(*region); + return rc; +} + +int Emulator::virt$shbuf_seal(int shbuf_id) +{ + auto* region = m_mmu.shbuf_region(shbuf_id); + ASSERT(region); + return region->seal(); +} + +int Emulator::virt$shbuf_set_volatile(int shbuf_id, bool is_volatile) +{ + auto* region = m_mmu.shbuf_region(shbuf_id); + ASSERT(region); + return region->set_volatile(is_volatile); +} + +int Emulator::virt$profiling_enable(pid_t pid) +{ + return syscall(SC_profiling_enable, pid); +} + +int Emulator::virt$profiling_disable(pid_t pid) +{ + return syscall(SC_profiling_disable, pid); +} + +int Emulator::virt$disown(pid_t pid) +{ + return syscall(SC_disown, pid); +} + +int Emulator::virt$purge(int mode) +{ + return syscall(SC_purge, mode); +} + +int Emulator::virt$fstat(int fd, FlatPtr statbuf) +{ + struct stat local_statbuf; + int rc = syscall(SC_fstat, fd, &local_statbuf); + if (rc < 0) + return rc; + mmu().copy_to_vm(statbuf, &local_statbuf, sizeof(local_statbuf)); + return rc; +} + +int Emulator::virt$close(int fd) +{ + return syscall(SC_close, fd); +} + +int Emulator::virt$mkdir(FlatPtr path, size_t path_length, mode_t mode) +{ + auto buffer = mmu().copy_buffer_from_vm(path, path_length); + return syscall(SC_mkdir, buffer.data(), buffer.size(), mode); +} + +int Emulator::virt$unlink(FlatPtr path, size_t path_length) +{ + auto buffer = mmu().copy_buffer_from_vm(path, path_length); + return syscall(SC_unlink, buffer.data(), buffer.size()); +} + +int Emulator::virt$dbgputstr(FlatPtr characters, int length) +{ + auto buffer = mmu().copy_buffer_from_vm(characters, length); + dbgputstr((const char*)buffer.data(), buffer.size()); + return 0; +} + +int Emulator::virt$chmod(FlatPtr path_addr, size_t path_length, mode_t mode) +{ + auto path = mmu().copy_buffer_from_vm(path_addr, path_length); + return syscall(SC_chmod, path.data(), path.size(), mode); +} + +int Emulator::virt$fchmod(int fd, mode_t mode) +{ + return syscall(SC_fchmod, fd, mode); +} + +int Emulator::virt$fchown(int fd, uid_t uid, gid_t gid) +{ + return syscall(SC_fchown, fd, uid, gid); +} + +int Emulator::virt$setsockopt(FlatPtr params_addr) +{ + Syscall::SC_setsockopt_params params; + mmu().copy_from_vm(¶ms, params_addr, sizeof(params)); + + if (params.option == SO_RCVTIMEO || params.option == SO_TIMESTAMP) { + auto host_value_buffer = ByteBuffer::create_zeroed(params.value_size); + mmu().copy_from_vm(host_value_buffer.data(), (FlatPtr)params.value, params.value_size); + int rc = setsockopt(params.sockfd, params.level, params.option, host_value_buffer.data(), host_value_buffer.size()); + if (rc < 0) + return -errno; + return rc; + } + + if (params.option == SO_BINDTODEVICE) { + auto ifname = mmu().copy_buffer_from_vm((FlatPtr)params.value, params.value_size); + params.value = ifname.data(); + params.value_size = ifname.size(); + return syscall(SC_setsockopt, ¶ms); + } + + TODO(); +} + +int Emulator::virt$get_stack_bounds(FlatPtr base, FlatPtr size) +{ + auto* region = mmu().find_region({ m_cpu.ss(), m_cpu.esp().value() }); + FlatPtr b = region->base(); + size_t s = region->size(); + mmu().copy_to_vm(base, &b, sizeof(b)); + mmu().copy_to_vm(size, &s, sizeof(s)); + return 0; +} + +int Emulator::virt$ftruncate(int fd, off_t length) +{ + return syscall(SC_ftruncate, fd, length); +} + +mode_t Emulator::virt$umask(mode_t mask) +{ + return syscall(SC_umask, mask); +} + +int Emulator::virt$accept(int sockfd, FlatPtr address, FlatPtr address_length) +{ + socklen_t host_address_length = 0; + mmu().copy_from_vm(&host_address_length, address_length, sizeof(host_address_length)); + auto host_buffer = ByteBuffer::create_zeroed(host_address_length); + int rc = syscall(SC_accept, sockfd, host_buffer.data(), &host_address_length); + if (rc < 0) + return rc; + mmu().copy_to_vm(address, host_buffer.data(), min((socklen_t)host_buffer.size(), host_address_length)); + mmu().copy_to_vm(address_length, &host_address_length, sizeof(host_address_length)); + return rc; +} + +int Emulator::virt$bind(int sockfd, FlatPtr address, socklen_t address_length) +{ + auto buffer = mmu().copy_buffer_from_vm(address, address_length); + return syscall(SC_bind, sockfd, buffer.data(), buffer.size()); +} + +int Emulator::virt$connect(int sockfd, FlatPtr address, socklen_t address_size) +{ + auto buffer = mmu().copy_buffer_from_vm(address, address_size); + return syscall(SC_connect, sockfd, buffer.data(), buffer.size()); +} + +int Emulator::virt$dbgputch(char ch) +{ + dbgputch(ch); + return 0; +} + +int Emulator::virt$listen(int fd, int backlog) +{ + return syscall(SC_listen, fd, backlog); +} + +int Emulator::virt$kill(pid_t pid, int signal) +{ + return syscall(SC_kill, pid, signal); +} + +int Emulator::virt$gettimeofday(FlatPtr timeval) +{ + struct timeval host_timeval; + int rc = syscall(SC_gettimeofday, &host_timeval); + if (rc < 0) + return rc; + mmu().copy_to_vm(timeval, &host_timeval, sizeof(host_timeval)); + return rc; +} + +int Emulator::virt$clock_gettime(int clockid, FlatPtr timespec) +{ + struct timespec host_timespec; + int rc = syscall(SC_clock_gettime, clockid, &host_timespec); + if (rc < 0) + return rc; + mmu().copy_to_vm(timespec, &host_timespec, sizeof(host_timespec)); + return rc; +} + +int Emulator::virt$clock_settime(uint32_t clock_id, FlatPtr user_ts) +{ + struct timespec user_timespec; + mmu().copy_from_vm(&user_timespec, user_ts, sizeof(user_timespec)); + int rc = syscall(SC_clock_settime, clock_id, &user_timespec); + return rc; +} + +int Emulator::virt$set_mmap_name(FlatPtr) +{ + // FIXME: Implement. + return 0; +} + +int Emulator::virt$get_process_name(FlatPtr buffer, int size) +{ + if (size < 0) + return -EINVAL; + auto host_buffer = ByteBuffer::create_zeroed((size_t)size); + int rc = syscall(SC_get_process_name, host_buffer.data(), host_buffer.size()); + mmu().copy_to_vm(buffer, host_buffer.data(), host_buffer.size()); + return rc; +} + +int Emulator::virt$lseek(int fd, off_t offset, int whence) +{ + return syscall(SC_lseek, fd, offset, whence); +} + +int Emulator::virt$socket(int domain, int type, int protocol) +{ + return syscall(SC_socket, domain, type, protocol); +} + +int Emulator::virt$recvmsg(int sockfd, FlatPtr msg_addr, int flags) +{ + msghdr mmu_msg; + mmu().copy_from_vm(&mmu_msg, msg_addr, sizeof(mmu_msg)); + + Vector<iovec, 1> mmu_iovs; + mmu_iovs.resize(mmu_msg.msg_iovlen); + mmu().copy_from_vm(mmu_iovs.data(), (FlatPtr)mmu_msg.msg_iov, mmu_msg.msg_iovlen * sizeof(iovec)); + Vector<ByteBuffer, 1> buffers; + Vector<iovec, 1> iovs; + for (const auto& iov : mmu_iovs) { + buffers.append(ByteBuffer::create_uninitialized(iov.iov_len)); + iovs.append({ buffers.last().data(), buffers.last().size() }); + } + + ByteBuffer control_buffer; + if (mmu_msg.msg_control) + control_buffer = ByteBuffer::create_uninitialized(mmu_msg.msg_controllen); + + sockaddr_storage addr; + msghdr msg = { &addr, sizeof(addr), iovs.data(), (int)iovs.size(), mmu_msg.msg_control ? control_buffer.data() : nullptr, mmu_msg.msg_controllen, mmu_msg.msg_flags }; + int rc = recvmsg(sockfd, &msg, flags); + if (rc < 0) + return -errno; + + for (size_t i = 0; i < buffers.size(); ++i) + mmu().copy_to_vm((FlatPtr)mmu_iovs[i].iov_base, buffers[i].data(), mmu_iovs[i].iov_len); + + if (mmu_msg.msg_name) + mmu().copy_to_vm((FlatPtr)mmu_msg.msg_name, &addr, min(sizeof(addr), (size_t)mmu_msg.msg_namelen)); + if (mmu_msg.msg_control) + mmu().copy_to_vm((FlatPtr)mmu_msg.msg_control, control_buffer.data(), min(mmu_msg.msg_controllen, msg.msg_controllen)); + mmu_msg.msg_namelen = msg.msg_namelen; + mmu_msg.msg_controllen = msg.msg_controllen; + mmu_msg.msg_flags = msg.msg_flags; + mmu().copy_to_vm(msg_addr, &mmu_msg, sizeof(mmu_msg)); + return rc; +} + +int Emulator::virt$sendmsg(int sockfd, FlatPtr msg_addr, int flags) +{ + msghdr mmu_msg; + mmu().copy_from_vm(&mmu_msg, msg_addr, sizeof(mmu_msg)); + + Vector<iovec, 1> iovs; + iovs.resize(mmu_msg.msg_iovlen); + mmu().copy_from_vm(iovs.data(), (FlatPtr)mmu_msg.msg_iov, mmu_msg.msg_iovlen * sizeof(iovec)); + Vector<ByteBuffer, 1> buffers; + for (auto& iov : iovs) { + buffers.append(mmu().copy_buffer_from_vm((FlatPtr)iov.iov_base, iov.iov_len)); + iov = { buffers.last().data(), buffers.last().size() }; + } + + ByteBuffer control_buffer; + if (mmu_msg.msg_control) + control_buffer = ByteBuffer::create_uninitialized(mmu_msg.msg_controllen); + + sockaddr_storage address; + socklen_t address_length = 0; + if (mmu_msg.msg_name) { + address_length = min(sizeof(address), (size_t)mmu_msg.msg_namelen); + mmu().copy_from_vm(&address, (FlatPtr)mmu_msg.msg_name, address_length); + } + + msghdr msg = { mmu_msg.msg_name ? &address : nullptr, address_length, iovs.data(), (int)iovs.size(), mmu_msg.msg_control ? control_buffer.data() : nullptr, mmu_msg.msg_controllen, mmu_msg.msg_flags }; + return sendmsg(sockfd, &msg, flags); +} + +int Emulator::virt$select(FlatPtr params_addr) +{ + Syscall::SC_select_params params; + mmu().copy_from_vm(¶ms, params_addr, sizeof(params)); + + fd_set readfds {}; + fd_set writefds {}; + fd_set exceptfds {}; + struct timespec timeout; + u32 sigmask; + + if (params.readfds) + mmu().copy_from_vm(&readfds, (FlatPtr)params.readfds, sizeof(readfds)); + if (params.writefds) + mmu().copy_from_vm(&writefds, (FlatPtr)params.writefds, sizeof(writefds)); + if (params.exceptfds) + mmu().copy_from_vm(&exceptfds, (FlatPtr)params.exceptfds, sizeof(exceptfds)); + if (params.timeout) + mmu().copy_from_vm(&timeout, (FlatPtr)params.timeout, sizeof(timeout)); + if (params.sigmask) + mmu().copy_from_vm(&sigmask, (FlatPtr)params.sigmask, sizeof(sigmask)); + + int rc = pselect(params.nfds, &readfds, &writefds, &exceptfds, params.timeout ? &timeout : nullptr, params.sigmask ? &sigmask : nullptr); + if (rc < 0) + return -errno; + + if (params.readfds) + mmu().copy_to_vm((FlatPtr)params.readfds, &readfds, sizeof(readfds)); + if (params.writefds) + mmu().copy_to_vm((FlatPtr)params.writefds, &writefds, sizeof(writefds)); + if (params.exceptfds) + mmu().copy_to_vm((FlatPtr)params.exceptfds, &exceptfds, sizeof(exceptfds)); + if (params.timeout) + mmu().copy_to_vm((FlatPtr)params.timeout, &timeout, sizeof(timeout)); + + return rc; +} + +int Emulator::virt$getsockopt(FlatPtr params_addr) +{ + Syscall::SC_getsockopt_params params; + mmu().copy_from_vm(¶ms, params_addr, sizeof(params)); + + if (params.option == SO_PEERCRED) { + struct ucred creds = {}; + socklen_t creds_size = sizeof(creds); + int rc = getsockopt(params.sockfd, params.level, SO_PEERCRED, &creds, &creds_size); + if (rc < 0) + return -errno; + // FIXME: Check params.value_size + mmu().copy_to_vm((FlatPtr)params.value, &creds, sizeof(creds)); + return rc; + } + + TODO(); +} + +int Emulator::virt$getgroups(ssize_t count, FlatPtr groups) +{ + if (!count) + return syscall(SC_getgroups, 0, nullptr); + + auto buffer = ByteBuffer::create_uninitialized(count * sizeof(gid_t)); + int rc = syscall(SC_getgroups, count, buffer.data()); + if (rc < 0) + return rc; + mmu().copy_to_vm(groups, buffer.data(), buffer.size()); + return 0; +} + +int Emulator::virt$setgroups(ssize_t count, FlatPtr groups) +{ + if (!count) + return syscall(SC_setgroups, 0, nullptr); + + auto buffer = mmu().copy_buffer_from_vm(groups, count * sizeof(gid_t)); + return syscall(SC_setgroups, count, buffer.data()); +} + +u32 Emulator::virt$fcntl(int fd, int cmd, u32 arg) +{ + switch (cmd) { + case F_DUPFD: + case F_GETFD: + case F_SETFD: + case F_GETFL: + case F_SETFL: + case F_ISTTY: + break; + default: + TODO(); + } + + return syscall(SC_fcntl, fd, cmd, arg); +} + +u32 Emulator::virt$open(u32 params_addr) +{ + Syscall::SC_open_params params; + mmu().copy_from_vm(¶ms, params_addr, sizeof(params)); + + auto path = mmu().copy_buffer_from_vm((FlatPtr)params.path.characters, params.path.length); + + int fd = openat_with_path_length(params.dirfd, (const char*)path.data(), path.size(), params.options, params.mode); + if (fd < 0) + return -errno; + return fd; +} + +int Emulator::virt$pipe(FlatPtr vm_pipefd, int flags) +{ + int pipefd[2]; + int rc = syscall(SC_pipe, pipefd, flags); + if (rc < 0) + return rc; + mmu().copy_to_vm(vm_pipefd, pipefd, sizeof(pipefd)); + return rc; +} + +u32 Emulator::virt$munmap(FlatPtr address, u32 size) +{ + auto* region = mmu().find_region({ 0x23, address }); + ASSERT(region); + if (region->size() != round_up_to_power_of_two(size, PAGE_SIZE)) + TODO(); + mmu().remove_region(*region); + return 0; +} + +FlatPtr Emulator::allocate_vm(size_t size, size_t alignment) +{ + // FIXME: Write a proper VM allocator + static FlatPtr next_address = 0x30000000; + + FlatPtr final_address; + + if (!alignment) + alignment = PAGE_SIZE; + + // FIXME: What if alignment is not a power of 2? + final_address = round_up_to_power_of_two(next_address, alignment); + next_address = round_up_to_power_of_two(final_address + size, PAGE_SIZE); + return final_address; +} + +u32 Emulator::virt$mmap(u32 params_addr) +{ + Syscall::SC_mmap_params params; + mmu().copy_from_vm(¶ms, params_addr, sizeof(params)); + + u32 final_size = round_up_to_power_of_two(params.size, PAGE_SIZE); + u32 final_address = allocate_vm(final_size, params.alignment); + if (params.addr != 0) { + // NOTE: We currently do not support allocating VM at a requeted address in the emulator. + // The loader needs this functionality to load .data just after .text. + // Luckily, since the loader calls mmap for .data right after it calls mmap for .text, + // the emulator will allocate a chunk of memory that is just after what we allocated for .text + // because of the way we currently allocate VM. + ASSERT(params.addr == final_address); + } + + if (params.flags & MAP_ANONYMOUS) + mmu().add_region(MmapRegion::create_anonymous(final_address, final_size, params.prot)); + else { + String name_str; + if (params.name.characters) { + auto name = ByteBuffer::create_uninitialized(params.name.length); + mmu().copy_from_vm(name.data(), (FlatPtr)params.name.characters, params.name.length); + name_str = { name.data(), name.size() }; + } + auto region = MmapRegion::create_file_backed(final_address, final_size, params.prot, params.flags, params.fd, params.offset, name_str); + if (region->name() == "libc.so: .text (Emulated)") { + bool rc = find_malloc_symbols(*region); + ASSERT(rc); + } + mmu().add_region(move(region)); + } + + return final_address; +} + +FlatPtr Emulator::virt$mremap(FlatPtr params_addr) +{ + Syscall::SC_mremap_params params; + mmu().copy_from_vm(¶ms, params_addr, sizeof(params)); + + if (auto* region = mmu().find_region({ m_cpu.ds(), params.old_address })) { + if (!is<MmapRegion>(*region)) + return -EINVAL; + ASSERT(region->size() == params.old_size); + auto& mmap_region = *(MmapRegion*)region; + auto* ptr = mremap(mmap_region.data(), mmap_region.size(), mmap_region.size(), params.flags); + if (ptr == MAP_FAILED) + return -errno; + return (FlatPtr)ptr; + } + return -EINVAL; +} + +u32 Emulator::virt$mount(u32 params_addr) +{ + Syscall::SC_mount_params params; + mmu().copy_from_vm(¶ms, params_addr, sizeof(params)); + auto target = mmu().copy_buffer_from_vm((FlatPtr)params.target.characters, params.target.length); + auto fs_path = mmu().copy_buffer_from_vm((FlatPtr)params.fs_type.characters, params.fs_type.length); + params.fs_type.characters = (char*)fs_path.data(); + params.fs_type.length = fs_path.size(); + params.target.characters = (char*)target.data(); + params.target.length = target.size(); + + return syscall(SC_mount, ¶ms); +} + +u32 Emulator::virt$gettid() +{ + return gettid(); +} + +u32 Emulator::virt$getpid() +{ + return getpid(); +} + +u32 Emulator::virt$pledge(u32) +{ + return 0; +} + +u32 Emulator::virt$unveil(u32) +{ + return 0; +} + +u32 Emulator::virt$mprotect(FlatPtr base, size_t size, int prot) +{ + if (auto* region = mmu().find_region({ m_cpu.ds(), base })) { + if (!is<MmapRegion>(*region)) + return -EINVAL; + ASSERT(region->size() == size); + auto& mmap_region = *(MmapRegion*)region; + mmap_region.set_prot(prot); + return 0; + } + return -EINVAL; +} + +u32 Emulator::virt$madvise(FlatPtr, size_t, int) +{ + return 0; +} + +uid_t Emulator::virt$getuid() +{ + return getuid(); +} + +uid_t Emulator::virt$geteuid() +{ + return geteuid(); +} + +gid_t Emulator::virt$getgid() +{ + return getgid(); +} + +gid_t Emulator::virt$getegid() +{ + return getegid(); +} + +int Emulator::virt$setuid(uid_t uid) +{ + return syscall(SC_setuid, uid); +} + +int Emulator::virt$setgid(gid_t gid) +{ + return syscall(SC_setgid, gid); +} + +u32 Emulator::virt$write(int fd, FlatPtr data, ssize_t size) +{ + if (size < 0) + return -EINVAL; + auto buffer = mmu().copy_buffer_from_vm(data, size); + return syscall(SC_write, fd, buffer.data(), buffer.size()); +} + +u32 Emulator::virt$read(int fd, FlatPtr buffer, ssize_t size) +{ + if (size < 0) + return -EINVAL; + auto local_buffer = ByteBuffer::create_uninitialized(size); + int nread = syscall(SC_read, fd, local_buffer.data(), local_buffer.size()); + if (nread < 0) { + if (nread == -EPERM) { + dump_backtrace(); + TODO(); + } + return nread; + } + mmu().copy_to_vm(buffer, local_buffer.data(), local_buffer.size()); + return nread; +} + +void Emulator::virt$exit(int status) +{ + reportln("\n=={}== \033[33;1mSyscall: exit({})\033[0m, shutting down!", getpid(), status); + m_exit_status = status; + m_shutdown = true; +} + +ssize_t Emulator::virt$getrandom(FlatPtr buffer, size_t buffer_size, unsigned int flags) +{ + auto host_buffer = ByteBuffer::create_uninitialized(buffer_size); + int rc = syscall(SC_getrandom, host_buffer.data(), host_buffer.size(), flags); + if (rc < 0) + return rc; + mmu().copy_to_vm(buffer, host_buffer.data(), host_buffer.size()); + return rc; +} + +int Emulator::virt$get_dir_entries(int fd, FlatPtr buffer, ssize_t size) +{ + auto host_buffer = ByteBuffer::create_uninitialized(size); + int rc = syscall(SC_get_dir_entries, fd, host_buffer.data(), host_buffer.size()); + if (rc < 0) + return rc; + mmu().copy_to_vm(buffer, host_buffer.data(), host_buffer.size()); + return rc; +} + +int Emulator::virt$ioctl([[maybe_unused]] int fd, unsigned request, [[maybe_unused]] FlatPtr arg) +{ + if (request == TIOCGWINSZ) { + struct winsize ws; + int rc = syscall(SC_ioctl, fd, TIOCGWINSZ, &ws); + if (rc < 0) + return rc; + mmu().copy_to_vm(arg, &ws, sizeof(winsize)); + return 0; + } + if (request == TIOCSPGRP) { + return syscall(SC_ioctl, fd, request, arg); + } + if (request == TCGETS) { + struct termios termios; + int rc = syscall(SC_ioctl, fd, request, &termios); + if (rc < 0) + return rc; + mmu().copy_to_vm(arg, &termios, sizeof(termios)); + return rc; + } + if (request == TCSETS) { + struct termios termios; + mmu().copy_from_vm(&termios, arg, sizeof(termios)); + return syscall(SC_ioctl, fd, request, &termios); + } + if (request == TIOCNOTTY || request == TIOCSCTTY) { + return syscall(SC_ioctl, fd, request, 0); + } + if (request == FB_IOCTL_GET_SIZE_IN_BYTES) { + size_t size = 0; + auto rc = syscall(SC_ioctl, fd, request, &size); + mmu().copy_to_vm(arg, &size, sizeof(size)); + return rc; + } + if (request == FB_IOCTL_SET_RESOLUTION) { + FBResolution user_resolution; + mmu().copy_from_vm(&user_resolution, arg, sizeof(user_resolution)); + auto rc = syscall(SC_ioctl, fd, request, &user_resolution); + mmu().copy_to_vm(arg, &user_resolution, sizeof(user_resolution)); + return rc; + } + if (request == FB_IOCTL_SET_BUFFER) { + return syscall(SC_ioctl, fd, request, arg); + } + reportln("Unsupported ioctl: {}", request); + dump_backtrace(); + TODO(); +} + +int Emulator::virt$fork() +{ + int rc = fork(); + if (rc < 0) + return -errno; + return rc; +} + +int Emulator::virt$execve(FlatPtr params_addr) +{ + Syscall::SC_execve_params params; + mmu().copy_from_vm(¶ms, params_addr, sizeof(params)); + + auto path = String::copy(mmu().copy_buffer_from_vm((FlatPtr)params.path.characters, params.path.length)); + Vector<String> arguments; + Vector<String> environment; + + auto copy_string_list = [this](auto& output_vector, auto& string_list) { + for (size_t i = 0; i < string_list.length; ++i) { + Syscall::StringArgument string; + mmu().copy_from_vm(&string, (FlatPtr)&string_list.strings[i], sizeof(string)); + output_vector.append(String::copy(mmu().copy_buffer_from_vm((FlatPtr)string.characters, string.length))); + } + }; + + copy_string_list(arguments, params.arguments); + copy_string_list(environment, params.environment); + + reportln("\n=={}== \033[33;1mSyscall:\033[0m execve", getpid()); + reportln("=={}== @ {}", getpid(), path); + for (auto& argument : arguments) + reportln("=={}== - {}", getpid(), argument); + + Vector<char*> argv; + Vector<char*> envp; + + argv.append(const_cast<char*>("/bin/UserspaceEmulator")); + argv.append(const_cast<char*>(path.characters())); + if (g_report_to_debug) + argv.append(const_cast<char*>("--report-to-debug")); + argv.append(const_cast<char*>("--")); + + auto create_string_vector = [](auto& output_vector, auto& input_vector) { + for (auto& string : input_vector) + output_vector.append(const_cast<char*>(string.characters())); + output_vector.append(nullptr); + }; + + create_string_vector(argv, arguments); + create_string_vector(envp, environment); + + // Yoink duplicated program name. + argv.remove(3 + (g_report_to_debug ? 1 : 0)); + + return execve(argv[0], (char* const*)argv.data(), (char* const*)envp.data()); +} + +int Emulator::virt$stat(FlatPtr params_addr) +{ + Syscall::SC_stat_params params; + mmu().copy_from_vm(¶ms, params_addr, sizeof(params)); + + auto path = String::copy(mmu().copy_buffer_from_vm((FlatPtr)params.path.characters, params.path.length)); + struct stat host_statbuf; + int rc; + if (params.follow_symlinks) + rc = stat(path.characters(), &host_statbuf); + else + rc = lstat(path.characters(), &host_statbuf); + if (rc < 0) + return -errno; + mmu().copy_to_vm((FlatPtr)params.statbuf, &host_statbuf, sizeof(host_statbuf)); + return rc; +} + +int Emulator::virt$realpath(FlatPtr params_addr) +{ + Syscall::SC_realpath_params params; + mmu().copy_from_vm(¶ms, params_addr, sizeof(params)); + + if (params.path.length > PATH_MAX) { + return -ENAMETOOLONG; + } + auto path = mmu().copy_buffer_from_vm((FlatPtr)params.path.characters, params.path.length); + char host_buffer[PATH_MAX] = {}; + size_t host_buffer_size = min(sizeof(host_buffer), params.buffer.size); + + Syscall::SC_realpath_params host_params; + host_params.path = { (const char*)path.data(), path.size() }; + host_params.buffer = { host_buffer, host_buffer_size }; + int rc = syscall(SC_realpath, &host_params); + if (rc < 0) + return rc; + mmu().copy_to_vm((FlatPtr)params.buffer.data, host_buffer, host_buffer_size); + return rc; +} + +int Emulator::virt$gethostname(FlatPtr buffer, ssize_t buffer_size) +{ + if (buffer_size < 0) + return -EINVAL; + auto host_buffer = ByteBuffer::create_zeroed(buffer_size); + int rc = syscall(SC_gethostname, host_buffer.data(), host_buffer.size()); + if (rc < 0) + return rc; + mmu().copy_to_vm(buffer, host_buffer.data(), host_buffer.size()); + return rc; +} + +static void emulator_signal_handler(int signum) +{ + Emulator::the().did_receive_signal(signum); +} + +void Emulator::register_signal_handlers() +{ + for (int signum = 0; signum < NSIG; ++signum) + signal(signum, emulator_signal_handler); +} + +int Emulator::virt$sigaction(int signum, FlatPtr act, FlatPtr oldact) +{ + if (signum == SIGKILL) { + reportln("Attempted to sigaction() with SIGKILL"); + return -EINVAL; + } + + if (signum <= 0 || signum >= NSIG) + return -EINVAL; + + struct sigaction host_act; + mmu().copy_from_vm(&host_act, act, sizeof(host_act)); + + auto& handler = m_signal_handler[signum]; + handler.handler = (FlatPtr)host_act.sa_handler; + handler.mask = host_act.sa_mask; + handler.flags = host_act.sa_flags; + + if (oldact) { + struct sigaction host_oldact; + auto& old_handler = m_signal_handler[signum]; + host_oldact.sa_handler = (void (*)(int))(old_handler.handler); + host_oldact.sa_mask = old_handler.mask; + host_oldact.sa_flags = old_handler.flags; + mmu().copy_to_vm(oldact, &host_oldact, sizeof(host_oldact)); + } + return 0; +} + +int Emulator::virt$sigreturn() +{ + u32 stack_ptr = m_cpu.esp().value(); + auto local_pop = [&]() -> ValueWithShadow<u32> { + auto value = m_cpu.read_memory32({ m_cpu.ss(), stack_ptr }); + stack_ptr += sizeof(u32); + return value; + }; + + auto smuggled_eax = local_pop(); + + stack_ptr += 4 * sizeof(u32); + + m_signal_mask = local_pop().value(); + + m_cpu.set_edi(local_pop()); + m_cpu.set_esi(local_pop()); + m_cpu.set_ebp(local_pop()); + m_cpu.set_esp(local_pop()); + m_cpu.set_ebx(local_pop()); + m_cpu.set_edx(local_pop()); + m_cpu.set_ecx(local_pop()); + m_cpu.set_eax(local_pop()); + + m_cpu.set_eip(local_pop().value()); + m_cpu.set_eflags(local_pop()); + + // FIXME: We're losing shadow bits here. + return smuggled_eax.value(); +} + +enum class DefaultSignalAction { + Terminate, + Ignore, + DumpCore, + Stop, + Continue, +}; + +static DefaultSignalAction default_signal_action(int signal) +{ + ASSERT(signal && signal < NSIG); + + switch (signal) { + case SIGHUP: + case SIGINT: + case SIGKILL: + case SIGPIPE: + case SIGALRM: + case SIGUSR1: + case SIGUSR2: + case SIGVTALRM: + case SIGSTKFLT: + case SIGIO: + case SIGPROF: + case SIGTERM: + return DefaultSignalAction::Terminate; + case SIGCHLD: + case SIGURG: + case SIGWINCH: + case SIGINFO: + return DefaultSignalAction::Ignore; + case SIGQUIT: + case SIGILL: + case SIGTRAP: + case SIGABRT: + case SIGBUS: + case SIGFPE: + case SIGSEGV: + case SIGXCPU: + case SIGXFSZ: + case SIGSYS: + return DefaultSignalAction::DumpCore; + case SIGCONT: + return DefaultSignalAction::Continue; + case SIGSTOP: + case SIGTSTP: + case SIGTTIN: + case SIGTTOU: + return DefaultSignalAction::Stop; + } + ASSERT_NOT_REACHED(); +} + +void Emulator::dispatch_one_pending_signal() +{ + int signum = -1; + for (signum = 1; signum < NSIG; ++signum) { + int mask = 1 << signum; + if (m_pending_signals & mask) + break; + } + ASSERT(signum != -1); + m_pending_signals &= ~(1 << signum); + + auto& handler = m_signal_handler[signum]; + + if (handler.handler == 0) { + // SIG_DFL + auto action = default_signal_action(signum); + if (action == DefaultSignalAction::Ignore) + return; + reportln("\n=={}== Got signal {} ({}), no handler registered", getpid(), signum, strsignal(signum)); + m_shutdown = true; + return; + } + + if (handler.handler == 1) { + // SIG_IGN + return; + } + + reportln("\n=={}== Got signal {} ({}), handler at {:p}", getpid(), signum, strsignal(signum), handler.handler); + + auto old_esp = m_cpu.esp(); + + u32 stack_alignment = (m_cpu.esp().value() - 56) % 16; + m_cpu.set_esp(shadow_wrap_as_initialized(m_cpu.esp().value() - stack_alignment)); + + m_cpu.push32(shadow_wrap_as_initialized(m_cpu.eflags())); + m_cpu.push32(shadow_wrap_as_initialized(m_cpu.eip())); + m_cpu.push32(m_cpu.eax()); + m_cpu.push32(m_cpu.ecx()); + m_cpu.push32(m_cpu.edx()); + m_cpu.push32(m_cpu.ebx()); + m_cpu.push32(old_esp); + m_cpu.push32(m_cpu.ebp()); + m_cpu.push32(m_cpu.esi()); + m_cpu.push32(m_cpu.edi()); + + // FIXME: Push old signal mask here. + m_cpu.push32(shadow_wrap_as_initialized(0u)); + + m_cpu.push32(shadow_wrap_as_initialized((u32)signum)); + m_cpu.push32(shadow_wrap_as_initialized(handler.handler)); + m_cpu.push32(shadow_wrap_as_initialized(0u)); + + ASSERT((m_cpu.esp().value() % 16) == 0); + + m_cpu.set_eip(m_signal_trampoline); +} + +// Make sure the compiler doesn't "optimize away" this function: +extern void signal_trampoline_dummy(); +void signal_trampoline_dummy() +{ + // The trampoline preserves the current eax, pushes the signal code and + // then calls the signal handler. We do this because, when interrupting a + // blocking syscall, that syscall may return some special error code in eax; + // This error code would likely be overwritten by the signal handler, so it's + // necessary to preserve it here. + asm( + ".intel_syntax noprefix\n" + "asm_signal_trampoline:\n" + "push ebp\n" + "mov ebp, esp\n" + "push eax\n" // we have to store eax 'cause it might be the return value from a syscall + "sub esp, 4\n" // align the stack to 16 bytes + "mov eax, [ebp+12]\n" // push the signal code + "push eax\n" + "call [ebp+8]\n" // call the signal handler + "add esp, 8\n" + "mov eax, %P0\n" + "int 0x82\n" // sigreturn syscall + "asm_signal_trampoline_end:\n" + ".att_syntax" ::"i"(Syscall::SC_sigreturn)); +} + +extern "C" void asm_signal_trampoline(void); +extern "C" void asm_signal_trampoline_end(void); + +void Emulator::setup_signal_trampoline() +{ + auto trampoline_region = make<SimpleRegion>(0xb0000000, 4096); + + u8* trampoline = (u8*)asm_signal_trampoline; + u8* trampoline_end = (u8*)asm_signal_trampoline_end; + size_t trampoline_size = trampoline_end - trampoline; + + u8* code_ptr = trampoline_region->data(); + memcpy(code_ptr, trampoline, trampoline_size); + + m_signal_trampoline = trampoline_region->base(); + mmu().add_region(move(trampoline_region)); +} + +int Emulator::virt$getpgrp() +{ + return syscall(SC_getpgrp); +} + +int Emulator::virt$getpgid(pid_t pid) +{ + return syscall(SC_getpgid, pid); +} + +int Emulator::virt$setpgid(pid_t pid, pid_t pgid) +{ + return syscall(SC_setpgid, pid, pgid); +} + +int Emulator::virt$ttyname(int fd, FlatPtr buffer, size_t buffer_size) +{ + auto host_buffer = ByteBuffer::create_zeroed(buffer_size); + int rc = syscall(SC_ttyname, fd, host_buffer.data(), host_buffer.size()); + if (rc < 0) + return rc; + mmu().copy_to_vm(buffer, host_buffer.data(), host_buffer.size()); + return rc; +} + +int Emulator::virt$getcwd(FlatPtr buffer, size_t buffer_size) +{ + auto host_buffer = ByteBuffer::create_zeroed(buffer_size); + int rc = syscall(SC_getcwd, host_buffer.data(), host_buffer.size()); + if (rc < 0) + return rc; + mmu().copy_to_vm(buffer, host_buffer.data(), host_buffer.size()); + return rc; +} + +int Emulator::virt$getsid(pid_t pid) +{ + return syscall(SC_getsid, pid); +} + +int Emulator::virt$access(FlatPtr path, size_t path_length, int type) +{ + auto host_path = mmu().copy_buffer_from_vm(path, path_length); + return syscall(SC_access, host_path.data(), host_path.size(), type); +} + +int Emulator::virt$waitid(FlatPtr params_addr) +{ + Syscall::SC_waitid_params params; + mmu().copy_from_vm(¶ms, params_addr, sizeof(params)); + + Syscall::SC_waitid_params host_params = params; + siginfo info; + host_params.infop = &info; + + int rc = syscall(SC_waitid, &host_params); + if (rc < 0) + return rc; + + if (info.si_addr) { + // FIXME: Translate this somehow. + TODO(); + } + + if (params.infop) + mmu().copy_to_vm((FlatPtr)params.infop, &info, sizeof(info)); + + return rc; +} + +int Emulator::virt$chdir(FlatPtr path, size_t path_length) +{ + auto host_path = mmu().copy_buffer_from_vm(path, path_length); + return syscall(SC_chdir, host_path.data(), host_path.size()); +} + +int Emulator::virt$dup2(int old_fd, int new_fd) +{ + return syscall(SC_dup2, old_fd, new_fd); +} + +int Emulator::virt$sched_getparam(pid_t pid, FlatPtr user_addr) +{ + sched_param user_param; + mmu().copy_from_vm(&user_param, user_addr, sizeof(user_param)); + auto rc = syscall(SC_sched_getparam, pid, &user_param); + mmu().copy_to_vm(user_addr, &user_param, sizeof(user_param)); + return rc; +} + +int Emulator::virt$sched_setparam(int pid, FlatPtr user_addr) +{ + sched_param user_param; + mmu().copy_from_vm(&user_param, user_addr, sizeof(user_param)); + return syscall(SC_sched_setparam, pid, &user_param); +} + +int Emulator::virt$set_thread_name(pid_t pid, FlatPtr name_addr, size_t name_length) +{ + auto user_name = mmu().copy_buffer_from_vm(name_addr, name_length); + auto name = String::formatted("(UE) {}", StringView { user_name.data(), user_name.size() }); + return syscall(SC_set_thread_name, pid, name.characters(), name.length()); +} + +pid_t Emulator::virt$setsid() +{ + return syscall(SC_setsid); +} + +int Emulator::virt$watch_file(FlatPtr user_path_addr, size_t path_length) +{ + auto user_path = mmu().copy_buffer_from_vm(user_path_addr, path_length); + return syscall(SC_watch_file, user_path.data(), user_path.size()); +} + +int Emulator::virt$clock_nanosleep(FlatPtr params_addr) +{ + Syscall::SC_clock_nanosleep_params params; + mmu().copy_from_vm(¶ms, params_addr, sizeof(params)); + + timespec requested_sleep; + mmu().copy_from_vm(&requested_sleep, (FlatPtr)params.requested_sleep, sizeof(timespec)); + params.requested_sleep = &requested_sleep; + + auto remaining_vm_addr = params.remaining_sleep; + auto remaining = ByteBuffer::create_zeroed(sizeof(timespec)); + params.remaining_sleep = (timespec*)remaining.data(); + + int rc = syscall(SC_clock_nanosleep, ¶ms); + if (remaining_vm_addr) + mmu().copy_to_vm((FlatPtr)remaining_vm_addr, remaining.data(), sizeof(timespec)); + + return rc; +} + +int Emulator::virt$readlink(FlatPtr params_addr) +{ + Syscall::SC_readlink_params params; + mmu().copy_from_vm(¶ms, params_addr, sizeof(params)); + + if (params.path.length > PATH_MAX) { + return -ENAMETOOLONG; + } + auto path = mmu().copy_buffer_from_vm((FlatPtr)params.path.characters, params.path.length); + char host_buffer[PATH_MAX] = {}; + size_t host_buffer_size = min(sizeof(host_buffer), params.buffer.size); + + Syscall::SC_readlink_params host_params; + host_params.path = { (const char*)path.data(), path.size() }; + host_params.buffer = { host_buffer, host_buffer_size }; + int rc = syscall(SC_readlink, &host_params); + if (rc < 0) + return rc; + mmu().copy_to_vm((FlatPtr)params.buffer.data, host_buffer, host_buffer_size); + return rc; +} + +u32 Emulator::virt$allocate_tls(size_t size) +{ + // TODO: Why is this needed? without this, the loader overflows the bounds of the TLS region. + constexpr size_t TLS_SIZE_HACK = 8; + auto tcb_region = make<SimpleRegion>(0x20000000, size + TLS_SIZE_HACK); + bzero(tcb_region->data(), size); + memset(tcb_region->shadow_data(), 0x01, size); + + auto tls_region = make<SimpleRegion>(0, 4); + tls_region->write32(0, shadow_wrap_as_initialized(tcb_region->base() + (u32)size)); + memset(tls_region->shadow_data(), 0x01, 4); + + u32 tls_base = tcb_region->base(); + mmu().add_region(move(tcb_region)); + mmu().set_tls_region(move(tls_region)); + return tls_base; +} + +int Emulator::virt$ptsname(int fd, FlatPtr buffer, size_t buffer_size) +{ + auto pts = mmu().copy_buffer_from_vm(buffer, buffer_size); + return syscall(SC_ptsname, fd, pts.data(), pts.size()); +} + +int Emulator::virt$beep() +{ + return syscall(SC_beep); +} + +bool Emulator::find_malloc_symbols(const MmapRegion& libc_text) +{ + auto file_or_error = MappedFile::map("/usr/lib/libc.so"); + if (file_or_error.is_error()) + return false; + + ELF::Image image(file_or_error.value()->bytes()); + auto malloc_symbol = image.find_demangled_function("malloc"); + auto free_symbol = image.find_demangled_function("free"); + auto realloc_symbol = image.find_demangled_function("realloc"); + auto malloc_size_symbol = image.find_demangled_function("malloc_size"); + if (!malloc_symbol.has_value() || !free_symbol.has_value() || !realloc_symbol.has_value() || !malloc_size_symbol.has_value()) + return false; + + m_malloc_symbol_start = malloc_symbol.value().value() + libc_text.base(); + m_malloc_symbol_end = m_malloc_symbol_start + malloc_symbol.value().size(); + m_free_symbol_start = free_symbol.value().value() + libc_text.base(); + m_free_symbol_end = m_free_symbol_start + free_symbol.value().size(); + m_realloc_symbol_start = realloc_symbol.value().value() + libc_text.base(); + m_realloc_symbol_end = m_realloc_symbol_start + realloc_symbol.value().size(); + m_malloc_size_symbol_start = malloc_size_symbol.value().value() + libc_text.base(); + m_malloc_size_symbol_end = m_malloc_size_symbol_start + malloc_size_symbol.value().size(); + return true; +} +} diff --git a/Userland/DevTools/UserspaceEmulator/Emulator.h b/Userland/DevTools/UserspaceEmulator/Emulator.h new file mode 100644 index 0000000000..ff8fad1235 --- /dev/null +++ b/Userland/DevTools/UserspaceEmulator/Emulator.h @@ -0,0 +1,235 @@ +/* + * Copyright (c) 2020, Andreas Kling <kling@serenityos.org> + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * 1. Redistributions of source code must retain the above copyright notice, this + * list of conditions and the following disclaimer. + * + * 2. Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE + * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL + * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR + * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER + * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, + * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE + * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +#pragma once + +#include "MallocTracer.h" +#include "Report.h" +#include "SoftCPU.h" +#include "SoftMMU.h" +#include <AK/MappedFile.h> +#include <AK/Types.h> +#include <LibDebug/DebugInfo.h> +#include <LibELF/AuxiliaryVector.h> +#include <LibELF/Image.h> +#include <LibX86/Instruction.h> +#include <signal.h> +#include <sys/types.h> + +namespace UserspaceEmulator { + +class MallocTracer; + +class Emulator { +public: + static Emulator& the(); + + Emulator(const String& executable_path, const Vector<String>& arguments, const Vector<String>& environment); + + bool load_elf(); + void dump_backtrace(); + void dump_backtrace(const Vector<FlatPtr>&); + Vector<FlatPtr> raw_backtrace(); + + int exec(); + u32 virt_syscall(u32 function, u32 arg1, u32 arg2, u32 arg3); + + SoftMMU& mmu() { return m_mmu; } + + MallocTracer* malloc_tracer() { return m_malloc_tracer; } + + bool is_in_malloc_or_free() const; + bool is_in_loader_code() const; + + void did_receive_signal(int signum) { m_pending_signals |= (1 << signum); } + +private: + const String m_executable_path; + const Vector<String> m_arguments; + const Vector<String> m_environment; + + SoftMMU m_mmu; + SoftCPU m_cpu; + + OwnPtr<MallocTracer> m_malloc_tracer; + + void setup_stack(Vector<ELF::AuxiliaryValue>); + Vector<ELF::AuxiliaryValue> generate_auxiliary_vector(FlatPtr load_base, FlatPtr entry_eip, String executable_path, int executable_fd) const; + void register_signal_handlers(); + void setup_signal_trampoline(); + + int virt$fork(); + int virt$execve(FlatPtr); + int virt$access(FlatPtr, size_t, int); + int virt$sigaction(int, FlatPtr, FlatPtr); + int virt$sigreturn(); + int virt$get_dir_entries(int fd, FlatPtr buffer, ssize_t); + int virt$ioctl(int fd, unsigned, FlatPtr); + int virt$stat(FlatPtr); + int virt$realpath(FlatPtr); + int virt$gethostname(FlatPtr, ssize_t); + int virt$shbuf_create(int size, FlatPtr buffer); + int virt$shbuf_allow_pid(int, pid_t peer_pid); + int virt$shbuf_allow_all(int); + FlatPtr virt$shbuf_get(int shbuf_id, FlatPtr size); + int virt$shbuf_release(int shbuf_id); + int virt$shbuf_seal(int shbuf_id); + int virt$shbuf_set_volatile(int shbuf_id, bool); + int virt$profiling_enable(pid_t); + int virt$profiling_disable(pid_t); + int virt$disown(pid_t); + int virt$purge(int mode); + u32 virt$mmap(u32); + FlatPtr virt$mremap(FlatPtr); + u32 virt$mount(u32); + u32 virt$munmap(FlatPtr address, u32 size); + u32 virt$gettid(); + u32 virt$getpid(); + u32 virt$unveil(u32); + u32 virt$pledge(u32); + uid_t virt$getuid(); + uid_t virt$geteuid(); + gid_t virt$getgid(); + gid_t virt$getegid(); + int virt$setuid(uid_t); + int virt$setgid(gid_t); + u32 virt$read(int, FlatPtr, ssize_t); + u32 virt$write(int, FlatPtr, ssize_t); + u32 virt$mprotect(FlatPtr, size_t, int); + u32 virt$madvise(FlatPtr, size_t, int); + u32 virt$open(u32); + int virt$pipe(FlatPtr pipefd, int flags); + int virt$close(int); + int virt$mkdir(FlatPtr path, size_t path_length, mode_t mode); + int virt$unlink(FlatPtr path, size_t path_length); + int virt$get_process_name(FlatPtr buffer, int size); + int virt$set_mmap_name(FlatPtr); + int virt$gettimeofday(FlatPtr); + int virt$clock_gettime(int, FlatPtr); + int virt$clock_nanosleep(FlatPtr); + int virt$dbgputstr(FlatPtr characters, int length); + int virt$dbgputch(char); + int virt$chmod(FlatPtr, size_t, mode_t); + int virt$fchmod(int, mode_t); + int virt$fchown(int, uid_t, gid_t); + int virt$clock_settime(uint32_t clock_id, FlatPtr user_ts); + int virt$listen(int, int); + int virt$kill(pid_t, int); + int virt$fstat(int, FlatPtr); + u32 virt$fcntl(int fd, int, u32); + int virt$getgroups(ssize_t count, FlatPtr); + int virt$setgroups(ssize_t count, FlatPtr); + int virt$lseek(int fd, off_t offset, int whence); + int virt$socket(int, int, int); + int virt$getsockopt(FlatPtr); + int virt$setsockopt(FlatPtr); + int virt$select(FlatPtr); + int virt$get_stack_bounds(FlatPtr, FlatPtr); + int virt$accept(int sockfd, FlatPtr address, FlatPtr address_length); + int virt$bind(int sockfd, FlatPtr address, socklen_t address_length); + int virt$recvmsg(int sockfd, FlatPtr msg_addr, int flags); + int virt$sendmsg(int sockfd, FlatPtr msg_addr, int flags); + int virt$connect(int sockfd, FlatPtr address, socklen_t address_size); + void virt$exit(int); + ssize_t virt$getrandom(FlatPtr buffer, size_t buffer_size, unsigned int flags); + int virt$chdir(FlatPtr, size_t); + int virt$dup2(int, int); + int virt$getpgrp(); + int virt$getpgid(pid_t); + int virt$setpgid(pid_t pid, pid_t pgid); + int virt$ttyname(int fd, FlatPtr buffer, size_t buffer_size); + int virt$getcwd(FlatPtr buffer, size_t buffer_size); + int virt$waitid(FlatPtr); + int virt$getsid(pid_t); + int virt$sched_setparam(int, FlatPtr); + int virt$sched_getparam(pid_t, FlatPtr); + int virt$set_thread_name(pid_t, FlatPtr, size_t); + pid_t virt$setsid(); + int virt$watch_file(FlatPtr, size_t); + int virt$readlink(FlatPtr); + u32 virt$allocate_tls(size_t); + int virt$ptsname(int fd, FlatPtr buffer, size_t buffer_size); + int virt$beep(); + int virt$ftruncate(int fd, off_t); + mode_t virt$umask(mode_t); + + FlatPtr allocate_vm(size_t size, size_t alignment); + bool find_malloc_symbols(const MmapRegion& libc_text); + + void dispatch_one_pending_signal(); + const MmapRegion* find_text_region(FlatPtr address); + String create_backtrace_line(FlatPtr address); + + bool m_shutdown { false }; + int m_exit_status { 0 }; + + FlatPtr m_malloc_symbol_start { 0 }; + FlatPtr m_malloc_symbol_end { 0 }; + FlatPtr m_realloc_symbol_start { 0 }; + FlatPtr m_realloc_symbol_end { 0 }; + FlatPtr m_free_symbol_start { 0 }; + FlatPtr m_free_symbol_end { 0 }; + FlatPtr m_malloc_size_symbol_start { 0 }; + FlatPtr m_malloc_size_symbol_end { 0 }; + + sigset_t m_pending_signals { 0 }; + sigset_t m_signal_mask { 0 }; + + struct SignalHandlerInfo { + FlatPtr handler { 0 }; + sigset_t mask { 0 }; + int flags { 0 }; + }; + SignalHandlerInfo m_signal_handler[NSIG]; + + FlatPtr m_signal_trampoline { 0 }; + Optional<FlatPtr> m_loader_text_base; + Optional<size_t> m_loader_text_size; + + struct CachedELF { + NonnullRefPtr<MappedFile> mapped_file; + NonnullOwnPtr<Debug::DebugInfo> debug_info; + }; + + HashMap<String, CachedELF> m_dynamic_library_cache; +}; + +ALWAYS_INLINE bool Emulator::is_in_malloc_or_free() const +{ + return (m_cpu.base_eip() >= m_malloc_symbol_start && m_cpu.base_eip() < m_malloc_symbol_end) + || (m_cpu.base_eip() >= m_free_symbol_start && m_cpu.base_eip() < m_free_symbol_end) + || (m_cpu.base_eip() >= m_realloc_symbol_start && m_cpu.base_eip() < m_realloc_symbol_end) + || (m_cpu.base_eip() >= m_malloc_size_symbol_start && m_cpu.base_eip() < m_malloc_size_symbol_end); +} + +ALWAYS_INLINE bool Emulator::is_in_loader_code() const +{ + if (!m_loader_text_base.has_value() || !m_loader_text_size.has_value()) + return false; + return (m_cpu.base_eip() >= m_loader_text_base.value() && m_cpu.base_eip() < m_loader_text_base.value() + m_loader_text_size.value()); +} + +} diff --git a/Userland/DevTools/UserspaceEmulator/MallocTracer.cpp b/Userland/DevTools/UserspaceEmulator/MallocTracer.cpp new file mode 100644 index 0000000000..c2ead0409d --- /dev/null +++ b/Userland/DevTools/UserspaceEmulator/MallocTracer.cpp @@ -0,0 +1,379 @@ +/* + * Copyright (c) 2020, Andreas Kling <kling@serenityos.org> + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * 1. Redistributions of source code must retain the above copyright notice, this + * list of conditions and the following disclaimer. + * + * 2. Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE + * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL + * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR + * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER + * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, + * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE + * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +#include "MallocTracer.h" +#include "Emulator.h" +#include "MmapRegion.h" +#include <AK/LogStream.h> +#include <AK/TemporaryChange.h> +#include <mallocdefs.h> +#include <string.h> + +//#define REACHABLE_DEBUG + +namespace UserspaceEmulator { + +MallocTracer::MallocTracer(Emulator& emulator) + : m_emulator(emulator) +{ +} + +template<typename Callback> +inline void MallocTracer::for_each_mallocation(Callback callback) const +{ + m_emulator.mmu().for_each_region([&](auto& region) { + if (is<MmapRegion>(region) && static_cast<const MmapRegion&>(region).is_malloc_block()) { + auto* malloc_data = static_cast<MmapRegion&>(region).malloc_metadata(); + for (auto& mallocation : malloc_data->mallocations) { + if (mallocation.used && callback(mallocation) == IterationDecision::Break) + return IterationDecision::Break; + } + } + return IterationDecision::Continue; + }); +} + +void MallocTracer::target_did_malloc(Badge<SoftCPU>, FlatPtr address, size_t size) +{ + if (m_emulator.is_in_loader_code()) + return; + auto* region = m_emulator.mmu().find_region({ 0x23, address }); + ASSERT(region); + ASSERT(is<MmapRegion>(*region)); + auto& mmap_region = static_cast<MmapRegion&>(*region); + + // Mark the containing mmap region as a malloc block! + mmap_region.set_malloc(true); + + auto* shadow_bits = mmap_region.shadow_data() + address - mmap_region.base(); + memset(shadow_bits, 0, size); + + if (auto* existing_mallocation = find_mallocation(address)) { + ASSERT(existing_mallocation->freed); + existing_mallocation->size = size; + existing_mallocation->freed = false; + existing_mallocation->malloc_backtrace = m_emulator.raw_backtrace(); + existing_mallocation->free_backtrace.clear(); + return; + } + + MallocRegionMetadata* malloc_data = static_cast<MmapRegion&>(*region).malloc_metadata(); + if (!malloc_data) { + auto new_malloc_data = make<MallocRegionMetadata>(); + malloc_data = new_malloc_data.ptr(); + static_cast<MmapRegion&>(*region).set_malloc_metadata({}, move(new_malloc_data)); + malloc_data->address = region->base(); + malloc_data->chunk_size = mmap_region.read32(offsetof(CommonHeader, m_size)).value(); + + bool is_chunked_block = malloc_data->chunk_size <= size_classes[num_size_classes - 1]; + if (is_chunked_block) + malloc_data->mallocations.resize((ChunkedBlock::block_size - sizeof(ChunkedBlock)) / malloc_data->chunk_size); + else + malloc_data->mallocations.resize(1); + dbgln("Tracking malloc block @ {:p} with chunk_size={}, chunk_count={}", malloc_data->address, malloc_data->chunk_size, malloc_data->mallocations.size()); + } + malloc_data->mallocation_for_address(address) = { address, size, true, false, m_emulator.raw_backtrace(), Vector<FlatPtr>() }; +} + +ALWAYS_INLINE Mallocation& MallocRegionMetadata::mallocation_for_address(FlatPtr address) const +{ + return const_cast<Mallocation&>(this->mallocations[chunk_index_for_address(address)]); +} + +ALWAYS_INLINE size_t MallocRegionMetadata::chunk_index_for_address(FlatPtr address) const +{ + bool is_chunked_block = chunk_size <= size_classes[num_size_classes - 1]; + if (!is_chunked_block) { + // This is a BigAllocationBlock + return 0; + } + auto chunk_offset = address - (this->address + sizeof(ChunkedBlock)); + ASSERT(this->chunk_size); + return chunk_offset / this->chunk_size; +} + +void MallocTracer::target_did_free(Badge<SoftCPU>, FlatPtr address) +{ + if (!address) + return; + if (m_emulator.is_in_loader_code()) + return; + + if (auto* mallocation = find_mallocation(address)) { + if (mallocation->freed) { + reportln("\n=={}== \033[31;1mDouble free()\033[0m, {:p}", getpid(), address); + reportln("=={}== Address {} has already been passed to free()", getpid(), address); + m_emulator.dump_backtrace(); + } else { + mallocation->freed = true; + mallocation->free_backtrace = m_emulator.raw_backtrace(); + } + return; + } + + reportln("\n=={}== \033[31;1mInvalid free()\033[0m, {:p}", getpid(), address); + reportln("=={}== Address {} has never been returned by malloc()", getpid(), address); + m_emulator.dump_backtrace(); +} + +void MallocTracer::target_did_realloc(Badge<SoftCPU>, FlatPtr address, size_t size) +{ + if (m_emulator.is_in_loader_code()) + return; + auto* region = m_emulator.mmu().find_region({ 0x23, address }); + ASSERT(region); + ASSERT(is<MmapRegion>(*region)); + auto& mmap_region = static_cast<MmapRegion&>(*region); + + ASSERT(mmap_region.is_malloc_block()); + + auto* existing_mallocation = find_mallocation(address); + ASSERT(existing_mallocation); + ASSERT(!existing_mallocation->freed); + + size_t old_size = existing_mallocation->size; + + auto* shadow_bits = mmap_region.shadow_data() + address - mmap_region.base(); + + if (size > old_size) { + memset(shadow_bits + old_size, 1, size - old_size); + } else { + memset(shadow_bits + size, 1, old_size - size); + } + + existing_mallocation->size = size; + // FIXME: Should we track malloc/realloc backtrace separately perhaps? + existing_mallocation->malloc_backtrace = m_emulator.raw_backtrace(); +} + +Mallocation* MallocTracer::find_mallocation(FlatPtr address) +{ + auto* region = m_emulator.mmu().find_region({ 0x23, address }); + if (!region) + return nullptr; + return find_mallocation(*region, address); +} + +Mallocation* MallocTracer::find_mallocation_before(FlatPtr address) +{ + Mallocation* found_mallocation = nullptr; + for_each_mallocation([&](auto& mallocation) { + if (mallocation.address >= address) + return IterationDecision::Continue; + if (!found_mallocation || (mallocation.address > found_mallocation->address)) + found_mallocation = const_cast<Mallocation*>(&mallocation); + return IterationDecision::Continue; + }); + return found_mallocation; +} + +Mallocation* MallocTracer::find_mallocation_after(FlatPtr address) +{ + Mallocation* found_mallocation = nullptr; + for_each_mallocation([&](auto& mallocation) { + if (mallocation.address <= address) + return IterationDecision::Continue; + if (!found_mallocation || (mallocation.address < found_mallocation->address)) + found_mallocation = const_cast<Mallocation*>(&mallocation); + return IterationDecision::Continue; + }); + return found_mallocation; +} + +void MallocTracer::audit_read(const Region& region, FlatPtr address, size_t size) +{ + if (!m_auditing_enabled) + return; + + if (m_emulator.is_in_malloc_or_free()) { + return; + } + + if (m_emulator.is_in_loader_code()) { + return; + } + + auto* mallocation = find_mallocation(region, address); + + if (!mallocation) { + reportln("\n=={}== \033[31;1mHeap buffer overflow\033[0m, invalid {}-byte read at address {:p}", getpid(), size, address); + m_emulator.dump_backtrace(); + auto* mallocation_before = find_mallocation_before(address); + auto* mallocation_after = find_mallocation_after(address); + size_t distance_to_mallocation_before = mallocation_before ? (address - mallocation_before->address - mallocation_before->size) : 0; + size_t distance_to_mallocation_after = mallocation_after ? (mallocation_after->address - address) : 0; + if (mallocation_before && (!mallocation_after || distance_to_mallocation_before < distance_to_mallocation_after)) { + reportln("=={}== Address is {} byte(s) after block of size {}, identity {:p}, allocated at:", getpid(), distance_to_mallocation_before, mallocation_before->size, mallocation_before->address); + m_emulator.dump_backtrace(mallocation_before->malloc_backtrace); + return; + } + if (mallocation_after && (!mallocation_before || distance_to_mallocation_after < distance_to_mallocation_before)) { + reportln("=={}== Address is {} byte(s) before block of size {}, identity {:p}, allocated at:", getpid(), distance_to_mallocation_after, mallocation_after->size, mallocation_after->address); + m_emulator.dump_backtrace(mallocation_after->malloc_backtrace); + } + return; + } + + size_t offset_into_mallocation = address - mallocation->address; + + if (mallocation->freed) { + reportln("\n=={}== \033[31;1mUse-after-free\033[0m, invalid {}-byte read at address {:p}", getpid(), size, address); + m_emulator.dump_backtrace(); + reportln("=={}== Address is {} byte(s) into block of size {}, allocated at:", getpid(), offset_into_mallocation, mallocation->size); + m_emulator.dump_backtrace(mallocation->malloc_backtrace); + reportln("=={}== Later freed at:", getpid()); + m_emulator.dump_backtrace(mallocation->free_backtrace); + return; + } +} + +void MallocTracer::audit_write(const Region& region, FlatPtr address, size_t size) +{ + if (!m_auditing_enabled) + return; + + if (m_emulator.is_in_malloc_or_free()) + return; + + if (m_emulator.is_in_loader_code()) { + return; + } + + auto* mallocation = find_mallocation(region, address); + if (!mallocation) { + reportln("\n=={}== \033[31;1mHeap buffer overflow\033[0m, invalid {}-byte write at address {:p}", getpid(), size, address); + m_emulator.dump_backtrace(); + auto* mallocation_before = find_mallocation_before(address); + auto* mallocation_after = find_mallocation_after(address); + size_t distance_to_mallocation_before = mallocation_before ? (address - mallocation_before->address - mallocation_before->size) : 0; + size_t distance_to_mallocation_after = mallocation_after ? (mallocation_after->address - address) : 0; + if (mallocation_before && (!mallocation_after || distance_to_mallocation_before < distance_to_mallocation_after)) { + reportln("=={}== Address is {} byte(s) after block of size {}, identity {:p}, allocated at:", getpid(), distance_to_mallocation_before, mallocation_before->size, mallocation_before->address); + m_emulator.dump_backtrace(mallocation_before->malloc_backtrace); + return; + } + if (mallocation_after && (!mallocation_before || distance_to_mallocation_after < distance_to_mallocation_before)) { + reportln("=={}== Address is {} byte(s) before block of size {}, identity {:p}, allocated at:", getpid(), distance_to_mallocation_after, mallocation_after->size, mallocation_after->address); + m_emulator.dump_backtrace(mallocation_after->malloc_backtrace); + } + return; + } + + size_t offset_into_mallocation = address - mallocation->address; + + if (mallocation->freed) { + reportln("\n=={}== \033[31;1mUse-after-free\033[0m, invalid {}-byte write at address {:p}", getpid(), size, address); + m_emulator.dump_backtrace(); + reportln("=={}== Address is {} byte(s) into block of size {}, allocated at:", getpid(), offset_into_mallocation, mallocation->size); + m_emulator.dump_backtrace(mallocation->malloc_backtrace); + reportln("=={}== Later freed at:", getpid()); + m_emulator.dump_backtrace(mallocation->free_backtrace); + return; + } +} + +bool MallocTracer::is_reachable(const Mallocation& mallocation) const +{ + ASSERT(!mallocation.freed); + + bool reachable = false; + + // 1. Search in active (non-freed) mallocations for pointers to this mallocation + for_each_mallocation([&](auto& other_mallocation) { + if (&mallocation == &other_mallocation) + return IterationDecision::Continue; + if (other_mallocation.freed) + return IterationDecision::Continue; + size_t pointers_in_mallocation = other_mallocation.size / sizeof(u32); + for (size_t i = 0; i < pointers_in_mallocation; ++i) { + auto value = m_emulator.mmu().read32({ 0x23, other_mallocation.address + i * sizeof(u32) }); + if (value.value() == mallocation.address && !value.is_uninitialized()) { +#ifdef REACHABLE_DEBUG + reportln("mallocation {:p} is reachable from other mallocation {:p}", mallocation.address, other_mallocation.address); +#endif + reachable = true; + return IterationDecision::Break; + } + } + return IterationDecision::Continue; + }); + + if (reachable) + return true; + + // 2. Search in other memory regions for pointers to this mallocation + m_emulator.mmu().for_each_region([&](auto& region) { + // Skip the stack + if (region.is_stack()) + return IterationDecision::Continue; + if (region.is_text()) + return IterationDecision::Continue; + if (!region.is_readable()) + return IterationDecision::Continue; + // Skip malloc blocks + if (is<MmapRegion>(region) && static_cast<const MmapRegion&>(region).is_malloc_block()) + return IterationDecision::Continue; + + size_t pointers_in_region = region.size() / sizeof(u32); + for (size_t i = 0; i < pointers_in_region; ++i) { + auto value = region.read32(i * sizeof(u32)); + if (value.value() == mallocation.address && !value.is_uninitialized()) { +#ifdef REACHABLE_DEBUG + reportln("mallocation {:p} is reachable from region {:p}-{:p}", mallocation.address, region.base(), region.end() - 1); +#endif + reachable = true; + return IterationDecision::Break; + } + } + return IterationDecision::Continue; + }); + return reachable; +} + +void MallocTracer::dump_leak_report() +{ + TemporaryChange change(m_auditing_enabled, false); + + size_t bytes_leaked = 0; + size_t leaks_found = 0; + for_each_mallocation([&](auto& mallocation) { + if (mallocation.freed) + return IterationDecision::Continue; + if (is_reachable(mallocation)) + return IterationDecision::Continue; + ++leaks_found; + bytes_leaked += mallocation.size; + reportln("\n=={}== \033[31;1mLeak\033[0m, {}-byte allocation at address {:p}", getpid(), mallocation.size, mallocation.address); + m_emulator.dump_backtrace(mallocation.malloc_backtrace); + return IterationDecision::Continue; + }); + + if (!leaks_found) + reportln("\n=={}== \033[32;1mNo leaks found!\033[0m", getpid()); + else + reportln("\n=={}== \033[31;1m{} leak(s) found: {} byte(s) leaked\033[0m", getpid(), leaks_found, bytes_leaked); +} +} diff --git a/Userland/DevTools/UserspaceEmulator/MallocTracer.h b/Userland/DevTools/UserspaceEmulator/MallocTracer.h new file mode 100644 index 0000000000..53da97acf1 --- /dev/null +++ b/Userland/DevTools/UserspaceEmulator/MallocTracer.h @@ -0,0 +1,112 @@ +/* + * Copyright (c) 2020, Andreas Kling <kling@serenityos.org> + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * 1. Redistributions of source code must retain the above copyright notice, this + * list of conditions and the following disclaimer. + * + * 2. Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE + * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL + * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR + * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER + * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, + * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE + * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +#pragma once + +#include "MmapRegion.h" +#include "SoftMMU.h" +#include <AK/Badge.h> +#include <AK/HashMap.h> +#include <AK/OwnPtr.h> +#include <AK/Types.h> +#include <AK/Vector.h> + +namespace UserspaceEmulator { + +class Emulator; +class SoftCPU; + +struct Mallocation { + bool contains(FlatPtr a) const + { + return a >= address && a < (address + size); + } + + FlatPtr address { 0 }; + size_t size { 0 }; + bool used { false }; + bool freed { false }; + + Vector<FlatPtr> malloc_backtrace; + Vector<FlatPtr> free_backtrace; +}; + +class MallocRegionMetadata { +public: + FlatPtr address { 0 }; + size_t chunk_size { 0 }; + + size_t chunk_index_for_address(FlatPtr) const; + Mallocation& mallocation_for_address(FlatPtr) const; + + Vector<Mallocation> mallocations; +}; + +class MallocTracer { +public: + explicit MallocTracer(Emulator&); + + void target_did_malloc(Badge<SoftCPU>, FlatPtr address, size_t); + void target_did_free(Badge<SoftCPU>, FlatPtr address); + void target_did_realloc(Badge<SoftCPU>, FlatPtr address, size_t); + + void audit_read(const Region&, FlatPtr address, size_t); + void audit_write(const Region&, FlatPtr address, size_t); + + void dump_leak_report(); + +private: + template<typename Callback> + void for_each_mallocation(Callback callback) const; + + Mallocation* find_mallocation(const Region&, FlatPtr); + Mallocation* find_mallocation(FlatPtr); + Mallocation* find_mallocation_before(FlatPtr); + Mallocation* find_mallocation_after(FlatPtr); + bool is_reachable(const Mallocation&) const; + + Emulator& m_emulator; + + bool m_auditing_enabled { true }; +}; + +ALWAYS_INLINE Mallocation* MallocTracer::find_mallocation(const Region& region, FlatPtr address) +{ + if (!is<MmapRegion>(region)) + return nullptr; + if (!static_cast<const MmapRegion&>(region).is_malloc_block()) + return nullptr; + auto* malloc_data = static_cast<MmapRegion&>(const_cast<Region&>(region)).malloc_metadata(); + if (!malloc_data) + return nullptr; + auto& mallocation = malloc_data->mallocation_for_address(address); + if (!mallocation.used) + return nullptr; + ASSERT(mallocation.contains(address)); + return &mallocation; +} + +} diff --git a/Userland/DevTools/UserspaceEmulator/MmapRegion.cpp b/Userland/DevTools/UserspaceEmulator/MmapRegion.cpp new file mode 100644 index 0000000000..9e5eda4e56 --- /dev/null +++ b/Userland/DevTools/UserspaceEmulator/MmapRegion.cpp @@ -0,0 +1,224 @@ +/* + * Copyright (c) 2020, Andreas Kling <kling@serenityos.org> + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * 1. Redistributions of source code must retain the above copyright notice, this + * list of conditions and the following disclaimer. + * + * 2. Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE + * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL + * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR + * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER + * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, + * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE + * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +#include "MmapRegion.h" +#include "Emulator.h" +#include <string.h> +#include <sys/mman.h> + +namespace UserspaceEmulator { + +NonnullOwnPtr<MmapRegion> MmapRegion::create_anonymous(u32 base, u32 size, u32 prot) +{ + auto region = adopt_own(*new MmapRegion(base, size, prot)); + region->m_file_backed = false; + region->m_data = (u8*)calloc(1, size); + return region; +} + +NonnullOwnPtr<MmapRegion> MmapRegion::create_file_backed(u32 base, u32 size, u32 prot, int flags, int fd, off_t offset, String name) +{ + auto region = adopt_own(*new MmapRegion(base, size, prot)); + region->m_file_backed = true; + if (!name.is_empty()) { + name = String::format("%s (Emulated)", name.characters()); + region->m_name = name; + } + region->m_data = (u8*)mmap_with_name(nullptr, size, prot, flags, fd, offset, name.is_empty() ? nullptr : name.characters()); + ASSERT(region->m_data != MAP_FAILED); + return region; +} + +MmapRegion::MmapRegion(u32 base, u32 size, int prot) + : Region(base, size) +{ + set_prot(prot); + m_shadow_data = (u8*)malloc(size); + memset(m_shadow_data, 1, size); +} + +MmapRegion::~MmapRegion() +{ + free(m_shadow_data); + if (m_file_backed) + munmap(m_data, size()); + else + free(m_data); +} + +ValueWithShadow<u8> MmapRegion::read8(FlatPtr offset) +{ + if (!is_readable()) { + reportln("8-bit read from unreadable MmapRegion @ {:p}", base() + offset); + emulator().dump_backtrace(); + TODO(); + } + + if (is_malloc_block()) { + if (auto* tracer = emulator().malloc_tracer()) + tracer->audit_read(*this, base() + offset, 1); + } + + ASSERT(offset < size()); + return { *reinterpret_cast<const u8*>(m_data + offset), *reinterpret_cast<const u8*>(m_shadow_data + offset) }; +} + +ValueWithShadow<u16> MmapRegion::read16(u32 offset) +{ + if (!is_readable()) { + reportln("16-bit read from unreadable MmapRegion @ {:p}", base() + offset); + emulator().dump_backtrace(); + TODO(); + } + + if (is_malloc_block()) { + if (auto* tracer = emulator().malloc_tracer()) + tracer->audit_read(*this, base() + offset, 2); + } + + ASSERT(offset + 1 < size()); + return { *reinterpret_cast<const u16*>(m_data + offset), *reinterpret_cast<const u16*>(m_shadow_data + offset) }; +} + +ValueWithShadow<u32> MmapRegion::read32(u32 offset) +{ + if (!is_readable()) { + reportln("32-bit read from unreadable MmapRegion @ {:p}", base() + offset); + emulator().dump_backtrace(); + TODO(); + } + + if (is_malloc_block()) { + if (auto* tracer = emulator().malloc_tracer()) + tracer->audit_read(*this, base() + offset, 4); + } + + ASSERT(offset + 3 < size()); + return { *reinterpret_cast<const u32*>(m_data + offset), *reinterpret_cast<const u32*>(m_shadow_data + offset) }; +} + +ValueWithShadow<u64> MmapRegion::read64(u32 offset) +{ + if (!is_readable()) { + reportln("64-bit read from unreadable MmapRegion @ {:p}", base() + offset); + emulator().dump_backtrace(); + TODO(); + } + + if (is_malloc_block()) { + if (auto* tracer = emulator().malloc_tracer()) + tracer->audit_read(*this, base() + offset, 8); + } + + ASSERT(offset + 7 < size()); + return { *reinterpret_cast<const u64*>(m_data + offset), *reinterpret_cast<const u64*>(m_shadow_data + offset) }; +} + +void MmapRegion::write8(u32 offset, ValueWithShadow<u8> value) +{ + if (!is_writable()) { + reportln("8-bit write from unwritable MmapRegion @ {:p}", base() + offset); + emulator().dump_backtrace(); + TODO(); + } + + if (is_malloc_block()) { + if (auto* tracer = emulator().malloc_tracer()) + tracer->audit_write(*this, base() + offset, 1); + } + + ASSERT(offset < size()); + *reinterpret_cast<u8*>(m_data + offset) = value.value(); + *reinterpret_cast<u8*>(m_shadow_data + offset) = value.shadow(); +} + +void MmapRegion::write16(u32 offset, ValueWithShadow<u16> value) +{ + if (!is_writable()) { + reportln("16-bit write from unwritable MmapRegion @ {:p}", base() + offset); + emulator().dump_backtrace(); + TODO(); + } + + if (is_malloc_block()) { + if (auto* tracer = emulator().malloc_tracer()) + tracer->audit_write(*this, base() + offset, 2); + } + + ASSERT(offset + 1 < size()); + *reinterpret_cast<u16*>(m_data + offset) = value.value(); + *reinterpret_cast<u16*>(m_shadow_data + offset) = value.shadow(); +} + +void MmapRegion::write32(u32 offset, ValueWithShadow<u32> value) +{ + if (!is_writable()) { + reportln("32-bit write from unwritable MmapRegion @ {:p}", base() + offset); + emulator().dump_backtrace(); + TODO(); + } + + if (is_malloc_block()) { + if (auto* tracer = emulator().malloc_tracer()) + tracer->audit_write(*this, base() + offset, 4); + } + + ASSERT(offset + 3 < size()); + ASSERT(m_data != m_shadow_data); + *reinterpret_cast<u32*>(m_data + offset) = value.value(); + *reinterpret_cast<u32*>(m_shadow_data + offset) = value.shadow(); +} + +void MmapRegion::write64(u32 offset, ValueWithShadow<u64> value) +{ + if (!is_writable()) { + reportln("64-bit write from unwritable MmapRegion @ {:p}", base() + offset); + emulator().dump_backtrace(); + TODO(); + } + + if (is_malloc_block()) { + if (auto* tracer = emulator().malloc_tracer()) + tracer->audit_write(*this, base() + offset, 8); + } + + ASSERT(offset + 7 < size()); + ASSERT(m_data != m_shadow_data); + *reinterpret_cast<u64*>(m_data + offset) = value.value(); + *reinterpret_cast<u64*>(m_shadow_data + offset) = value.shadow(); +} + +void MmapRegion::set_prot(int prot) +{ + set_readable(prot & PROT_READ); + set_writable(prot & PROT_WRITE); + set_executable(prot & PROT_EXEC); + if (m_file_backed) { + mprotect(m_data, size(), prot); + } +} + +} diff --git a/Userland/DevTools/UserspaceEmulator/MmapRegion.h b/Userland/DevTools/UserspaceEmulator/MmapRegion.h new file mode 100644 index 0000000000..75a42340ab --- /dev/null +++ b/Userland/DevTools/UserspaceEmulator/MmapRegion.h @@ -0,0 +1,78 @@ +/* + * Copyright (c) 2020, Andreas Kling <kling@serenityos.org> + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * 1. Redistributions of source code must retain the above copyright notice, this + * list of conditions and the following disclaimer. + * + * 2. Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE + * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL + * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR + * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER + * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, + * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE + * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +#pragma once + +#include "SoftMMU.h" +#include <sys/mman.h> + +namespace UserspaceEmulator { + +class MallocRegionMetadata; +class MallocTracer; + +class MmapRegion final : public Region { +public: + static NonnullOwnPtr<MmapRegion> create_anonymous(u32 base, u32 size, u32 prot); + static NonnullOwnPtr<MmapRegion> create_file_backed(u32 base, u32 size, u32 prot, int flags, int fd, off_t offset, String name = {}); + virtual ~MmapRegion() override; + + virtual ValueWithShadow<u8> read8(u32 offset) override; + virtual ValueWithShadow<u16> read16(u32 offset) override; + virtual ValueWithShadow<u32> read32(u32 offset) override; + virtual ValueWithShadow<u64> read64(u32 offset) override; + + virtual void write8(u32 offset, ValueWithShadow<u8>) override; + virtual void write16(u32 offset, ValueWithShadow<u16>) override; + virtual void write32(u32 offset, ValueWithShadow<u32>) override; + virtual void write64(u32 offset, ValueWithShadow<u64>) override; + + virtual u8* data() override { return m_data; } + virtual u8* shadow_data() override { return m_shadow_data; } + + bool is_malloc_block() const { return m_malloc; } + void set_malloc(bool b) { m_malloc = b; } + + void set_prot(int prot); + + MallocRegionMetadata* malloc_metadata() { return m_malloc_metadata; } + void set_malloc_metadata(Badge<MallocTracer>, NonnullOwnPtr<MallocRegionMetadata> metadata) { m_malloc_metadata = move(metadata); } + + const String& name() const { return m_name; } + +private: + MmapRegion(u32 base, u32 size, int prot); + + u8* m_data { nullptr }; + u8* m_shadow_data { nullptr }; + bool m_file_backed { false }; + bool m_malloc { false }; + + OwnPtr<MallocRegionMetadata> m_malloc_metadata; + String m_name; +}; + +} diff --git a/Userland/DevTools/UserspaceEmulator/Region.cpp b/Userland/DevTools/UserspaceEmulator/Region.cpp new file mode 100644 index 0000000000..c5f989c802 --- /dev/null +++ b/Userland/DevTools/UserspaceEmulator/Region.cpp @@ -0,0 +1,39 @@ +/* + * Copyright (c) 2020, Andreas Kling <kling@serenityos.org> + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * 1. Redistributions of source code must retain the above copyright notice, this + * list of conditions and the following disclaimer. + * + * 2. Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE + * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL + * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR + * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER + * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, + * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE + * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +#include "Region.h" +#include "Emulator.h" + +namespace UserspaceEmulator { + +Region::Region(u32 base, u32 size) + : m_emulator(Emulator::the()) + , m_base(base) + , m_size(size) +{ +} + +} diff --git a/Userland/DevTools/UserspaceEmulator/Region.h b/Userland/DevTools/UserspaceEmulator/Region.h new file mode 100644 index 0000000000..ffb15cf941 --- /dev/null +++ b/Userland/DevTools/UserspaceEmulator/Region.h @@ -0,0 +1,95 @@ +/* + * Copyright (c) 2020, Andreas Kling <kling@serenityos.org> + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * 1. Redistributions of source code must retain the above copyright notice, this + * list of conditions and the following disclaimer. + * + * 2. Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE + * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL + * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR + * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER + * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, + * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE + * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +#pragma once + +#include "ValueWithShadow.h" +#include <AK/TypeCasts.h> +#include <AK/Types.h> + +namespace UserspaceEmulator { + +class Emulator; + +class Region { +public: + virtual ~Region() { } + + u32 base() const { return m_base; } + u32 size() const { return m_size; } + u32 end() const { return m_base + m_size; } + + bool contains(u32 address) const { return address >= base() && address < end(); } + + virtual void write8(u32 offset, ValueWithShadow<u8>) = 0; + virtual void write16(u32 offset, ValueWithShadow<u16>) = 0; + virtual void write32(u32 offset, ValueWithShadow<u32>) = 0; + virtual void write64(u32 offset, ValueWithShadow<u64>) = 0; + + virtual ValueWithShadow<u8> read8(u32 offset) = 0; + virtual ValueWithShadow<u16> read16(u32 offset) = 0; + virtual ValueWithShadow<u32> read32(u32 offset) = 0; + virtual ValueWithShadow<u64> read64(u32 offset) = 0; + + virtual u8* cacheable_ptr([[maybe_unused]] u32 offset) { return nullptr; } + + bool is_stack() const { return m_stack; } + void set_stack(bool b) { m_stack = b; } + + bool is_text() const { return m_text; } + void set_text(bool b) { m_text = b; } + + bool is_readable() const { return m_readable; } + bool is_writable() const { return m_writable; } + bool is_executable() const { return m_executable; } + + void set_readable(bool b) { m_readable = b; } + void set_writable(bool b) { m_writable = b; } + void set_executable(bool b) { m_executable = b; } + + virtual u8* data() = 0; + virtual u8* shadow_data() = 0; + + Emulator& emulator() { return m_emulator; } + const Emulator& emulator() const { return m_emulator; } + +protected: + Region(u32 base, u32 size); + +private: + Emulator& m_emulator; + + u32 m_base { 0 }; + u32 m_size { 0 }; + + bool m_stack { false }; + bool m_text { false }; + bool m_readable { true }; + bool m_writable { true }; + bool m_executable { true }; +}; + +} diff --git a/Userland/DevTools/UserspaceEmulator/Report.h b/Userland/DevTools/UserspaceEmulator/Report.h new file mode 100644 index 0000000000..97dcf97537 --- /dev/null +++ b/Userland/DevTools/UserspaceEmulator/Report.h @@ -0,0 +1,40 @@ +/* + * Copyright (c) 2020, the SerenityOS developers. + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * 1. Redistributions of source code must retain the above copyright notice, this + * list of conditions and the following disclaimer. + * + * 2. Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE + * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL + * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR + * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER + * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, + * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE + * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +#pragma once + +#include <AK/LogStream.h> + +extern bool g_report_to_debug; + +template<typename... Ts> +void reportln(const StringView& format, Ts... args) +{ + if (g_report_to_debug) + dbgln(format, args...); + else + warnln(format, args...); +} diff --git a/Userland/DevTools/UserspaceEmulator/SharedBufferRegion.cpp b/Userland/DevTools/UserspaceEmulator/SharedBufferRegion.cpp new file mode 100644 index 0000000000..aec26c2082 --- /dev/null +++ b/Userland/DevTools/UserspaceEmulator/SharedBufferRegion.cpp @@ -0,0 +1,132 @@ +/* + * Copyright (c) 2020, Andreas Kling <kling@serenityos.org> + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * 1. Redistributions of source code must retain the above copyright notice, this + * list of conditions and the following disclaimer. + * + * 2. Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE + * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL + * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR + * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER + * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, + * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE + * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +#include "SharedBufferRegion.h" +#include "Emulator.h" +#include <Kernel/API/Syscall.h> +#include <serenity.h> +#include <string.h> +#include <sys/mman.h> + +namespace UserspaceEmulator { + +NonnullOwnPtr<SharedBufferRegion> SharedBufferRegion::create_with_shbuf_id(u32 base, u32 size, int shbuf_id, u8* host_data) +{ + return adopt_own(*new SharedBufferRegion(base, size, shbuf_id, host_data)); +} + +SharedBufferRegion::SharedBufferRegion(u32 base, u32 size, int shbuf_id, u8* host_data) + : Region(base, size) + , m_data(host_data) + , m_shbuf_id(shbuf_id) +{ + m_shadow_data = (u8*)malloc(size); + memset(m_shadow_data, 1, size); +} + +SharedBufferRegion::~SharedBufferRegion() +{ + free(m_shadow_data); +} + +ValueWithShadow<u8> SharedBufferRegion::read8(FlatPtr offset) +{ + ASSERT(offset < size()); + return { *reinterpret_cast<const u8*>(m_data + offset), *reinterpret_cast<const u8*>(m_shadow_data + offset) }; +} + +ValueWithShadow<u16> SharedBufferRegion::read16(u32 offset) +{ + ASSERT(offset + 1 < size()); + return { *reinterpret_cast<const u16*>(m_data + offset), *reinterpret_cast<const u16*>(m_shadow_data + offset) }; +} + +ValueWithShadow<u32> SharedBufferRegion::read32(u32 offset) +{ + ASSERT(offset + 3 < size()); + return { *reinterpret_cast<const u32*>(m_data + offset), *reinterpret_cast<const u32*>(m_shadow_data + offset) }; +} + +ValueWithShadow<u64> SharedBufferRegion::read64(u32 offset) +{ + ASSERT(offset + 7 < size()); + return { *reinterpret_cast<const u64*>(m_data + offset), *reinterpret_cast<const u64*>(m_shadow_data + offset) }; +} + +void SharedBufferRegion::write8(u32 offset, ValueWithShadow<u8> value) +{ + ASSERT(offset < size()); + *reinterpret_cast<u8*>(m_data + offset) = value.value(); + *reinterpret_cast<u8*>(m_shadow_data + offset) = value.shadow(); +} + +void SharedBufferRegion::write16(u32 offset, ValueWithShadow<u16> value) +{ + ASSERT(offset + 1 < size()); + *reinterpret_cast<u16*>(m_data + offset) = value.value(); + *reinterpret_cast<u16*>(m_shadow_data + offset) = value.shadow(); +} + +void SharedBufferRegion::write32(u32 offset, ValueWithShadow<u32> value) +{ + ASSERT(offset + 3 < size()); + *reinterpret_cast<u32*>(m_data + offset) = value.value(); + *reinterpret_cast<u32*>(m_shadow_data + offset) = value.shadow(); +} + +void SharedBufferRegion::write64(u32 offset, ValueWithShadow<u64> value) +{ + ASSERT(offset + 7 < size()); + *reinterpret_cast<u64*>(m_data + offset) = value.value(); + *reinterpret_cast<u64*>(m_shadow_data + offset) = value.shadow(); +} + +int SharedBufferRegion::allow_all() +{ + return syscall(SC_shbuf_allow_all, m_shbuf_id); +} + +int SharedBufferRegion::allow_pid(pid_t pid) +{ + return syscall(SC_shbuf_allow_pid, m_shbuf_id, pid); +} + +int SharedBufferRegion::seal() +{ + return syscall(SC_shbuf_seal, m_shbuf_id); +} + +int SharedBufferRegion::release() +{ + return syscall(SC_shbuf_release, m_shbuf_id); +} + +int SharedBufferRegion::set_volatile(bool is_volatile) +{ + return syscall(SC_shbuf_set_volatile, m_shbuf_id, is_volatile); +} + +} diff --git a/Userland/DevTools/UserspaceEmulator/SharedBufferRegion.h b/Userland/DevTools/UserspaceEmulator/SharedBufferRegion.h new file mode 100644 index 0000000000..b3c7d68d5c --- /dev/null +++ b/Userland/DevTools/UserspaceEmulator/SharedBufferRegion.h @@ -0,0 +1,68 @@ +/* + * Copyright (c) 2020, Andreas Kling <kling@serenityos.org> + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * 1. Redistributions of source code must retain the above copyright notice, this + * list of conditions and the following disclaimer. + * + * 2. Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE + * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL + * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR + * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER + * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, + * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE + * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +#pragma once + +#include "SoftMMU.h" +#include <sys/mman.h> + +namespace UserspaceEmulator { + +class SharedBufferRegion final : public Region { +public: + static NonnullOwnPtr<SharedBufferRegion> create_with_shbuf_id(u32 base, u32 size, int shbuf_id, u8* shbuf_data); + virtual ~SharedBufferRegion() override; + + virtual ValueWithShadow<u8> read8(u32 offset) override; + virtual ValueWithShadow<u16> read16(u32 offset) override; + virtual ValueWithShadow<u32> read32(u32 offset) override; + virtual ValueWithShadow<u64> read64(u32 offset) override; + + virtual void write8(u32 offset, ValueWithShadow<u8>) override; + virtual void write16(u32 offset, ValueWithShadow<u16>) override; + virtual void write32(u32 offset, ValueWithShadow<u32>) override; + virtual void write64(u32 offset, ValueWithShadow<u64>) override; + + virtual u8* data() override { return m_data; } + virtual u8* shadow_data() override { return m_shadow_data; } + + int shbuf_id() const { return m_shbuf_id; } + + int allow_all(); + int allow_pid(pid_t); + int seal(); + int release(); + int set_volatile(bool); + +private: + SharedBufferRegion(u32 base, u32 size, int shbuf_id, u8* shbuf_data); + + u8* m_data { nullptr }; + u8* m_shadow_data { nullptr }; + int m_shbuf_id { 0 }; +}; + +} diff --git a/Userland/DevTools/UserspaceEmulator/SimpleRegion.cpp b/Userland/DevTools/UserspaceEmulator/SimpleRegion.cpp new file mode 100644 index 0000000000..173efd0e32 --- /dev/null +++ b/Userland/DevTools/UserspaceEmulator/SimpleRegion.cpp @@ -0,0 +1,103 @@ +/* + * Copyright (c) 2020, Andreas Kling <kling@serenityos.org> + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * 1. Redistributions of source code must retain the above copyright notice, this + * list of conditions and the following disclaimer. + * + * 2. Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE + * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL + * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR + * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER + * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, + * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE + * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +#include "SimpleRegion.h" +#include <string.h> + +namespace UserspaceEmulator { + +SimpleRegion::SimpleRegion(u32 base, u32 size) + : Region(base, size) +{ + m_data = (u8*)calloc(1, size); + m_shadow_data = (u8*)malloc(size); + memset(m_shadow_data, 1, size); +} + +SimpleRegion::~SimpleRegion() +{ + free(m_shadow_data); + free(m_data); +} + +ValueWithShadow<u8> SimpleRegion::read8(FlatPtr offset) +{ + ASSERT(offset < size()); + return { *reinterpret_cast<const u8*>(m_data + offset), *reinterpret_cast<const u8*>(m_shadow_data + offset) }; +} + +ValueWithShadow<u16> SimpleRegion::read16(u32 offset) +{ + ASSERT(offset + 1 < size()); + return { *reinterpret_cast<const u16*>(m_data + offset), *reinterpret_cast<const u16*>(m_shadow_data + offset) }; +} + +ValueWithShadow<u32> SimpleRegion::read32(u32 offset) +{ + ASSERT(offset + 3 < size()); + return { *reinterpret_cast<const u32*>(m_data + offset), *reinterpret_cast<const u32*>(m_shadow_data + offset) }; +} + +ValueWithShadow<u64> SimpleRegion::read64(u32 offset) +{ + ASSERT(offset + 7 < size()); + return { *reinterpret_cast<const u64*>(m_data + offset), *reinterpret_cast<const u64*>(m_shadow_data + offset) }; +} + +void SimpleRegion::write8(u32 offset, ValueWithShadow<u8> value) +{ + ASSERT(offset < size()); + *reinterpret_cast<u8*>(m_data + offset) = value.value(); + *reinterpret_cast<u8*>(m_shadow_data + offset) = value.shadow(); +} + +void SimpleRegion::write16(u32 offset, ValueWithShadow<u16> value) +{ + ASSERT(offset + 1 < size()); + *reinterpret_cast<u16*>(m_data + offset) = value.value(); + *reinterpret_cast<u16*>(m_shadow_data + offset) = value.shadow(); +} + +void SimpleRegion::write32(u32 offset, ValueWithShadow<u32> value) +{ + ASSERT(offset + 3 < size()); + *reinterpret_cast<u32*>(m_data + offset) = value.value(); + *reinterpret_cast<u32*>(m_shadow_data + offset) = value.shadow(); +} + +void SimpleRegion::write64(u32 offset, ValueWithShadow<u64> value) +{ + ASSERT(offset + 7 < size()); + *reinterpret_cast<u64*>(m_data + offset) = value.value(); + *reinterpret_cast<u64*>(m_shadow_data + offset) = value.shadow(); +} + +u8* SimpleRegion::cacheable_ptr(u32 offset) +{ + return m_data + offset; +} + +} diff --git a/Userland/DevTools/UserspaceEmulator/SimpleRegion.h b/Userland/DevTools/UserspaceEmulator/SimpleRegion.h new file mode 100644 index 0000000000..202f283741 --- /dev/null +++ b/Userland/DevTools/UserspaceEmulator/SimpleRegion.h @@ -0,0 +1,58 @@ +/* + * Copyright (c) 2020, Andreas Kling <kling@serenityos.org> + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * 1. Redistributions of source code must retain the above copyright notice, this + * list of conditions and the following disclaimer. + * + * 2. Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE + * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL + * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR + * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER + * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, + * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE + * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +#pragma once + +#include "SoftMMU.h" + +namespace UserspaceEmulator { + +class SimpleRegion final : public Region { +public: + SimpleRegion(u32 base, u32 size); + virtual ~SimpleRegion() override; + + virtual ValueWithShadow<u8> read8(u32 offset) override; + virtual ValueWithShadow<u16> read16(u32 offset) override; + virtual ValueWithShadow<u32> read32(u32 offset) override; + virtual ValueWithShadow<u64> read64(u32 offset) override; + + virtual void write8(u32 offset, ValueWithShadow<u8>) override; + virtual void write16(u32 offset, ValueWithShadow<u16>) override; + virtual void write32(u32 offset, ValueWithShadow<u32>) override; + virtual void write64(u32 offset, ValueWithShadow<u64>) override; + + virtual u8* data() override { return m_data; } + virtual u8* shadow_data() override { return m_shadow_data; } + + virtual u8* cacheable_ptr(u32 offset) override; + +private: + u8* m_data { nullptr }; + u8* m_shadow_data { nullptr }; +}; + +} diff --git a/Userland/DevTools/UserspaceEmulator/SoftCPU.cpp b/Userland/DevTools/UserspaceEmulator/SoftCPU.cpp new file mode 100644 index 0000000000..add134037f --- /dev/null +++ b/Userland/DevTools/UserspaceEmulator/SoftCPU.cpp @@ -0,0 +1,3282 @@ +/* + * Copyright (c) 2020, Andreas Kling <kling@serenityos.org> + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * 1. Redistributions of source code must retain the above copyright notice, this + * list of conditions and the following disclaimer. + * + * 2. Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE + * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL + * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR + * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER + * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, + * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE + * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +#include "SoftCPU.h" +#include "Emulator.h" +#include <AK/Assertions.h> +#include <math.h> +#include <stdio.h> +#include <string.h> + +#if defined(__GNUC__) && !defined(__clang__) +# pragma GCC optimize("O3") +#endif + +#define TODO_INSN() \ + do { \ + reportln("\n=={}== Unimplemented instruction: {}\n", getpid(), __FUNCTION__); \ + m_emulator.dump_backtrace(); \ + _exit(0); \ + } while (0) + +//#define MEMORY_DEBUG + +#define DEFINE_GENERIC_SHIFT_ROTATE_INSN_HANDLERS(mnemonic, op) \ + void SoftCPU::mnemonic##_RM8_1(const X86::Instruction& insn) { generic_RM8_1(op<ValueWithShadow<u8>>, insn); } \ + void SoftCPU::mnemonic##_RM8_CL(const X86::Instruction& insn) { generic_RM8_CL(op<ValueWithShadow<u8>>, insn); } \ + void SoftCPU::mnemonic##_RM8_imm8(const X86::Instruction& insn) { generic_RM8_imm8<true, false>(op<ValueWithShadow<u8>>, insn); } \ + void SoftCPU::mnemonic##_RM16_1(const X86::Instruction& insn) { generic_RM16_1(op<ValueWithShadow<u16>>, insn); } \ + void SoftCPU::mnemonic##_RM16_CL(const X86::Instruction& insn) { generic_RM16_CL(op<ValueWithShadow<u16>>, insn); } \ + void SoftCPU::mnemonic##_RM16_imm8(const X86::Instruction& insn) { generic_RM16_unsigned_imm8<true>(op<ValueWithShadow<u16>>, insn); } \ + void SoftCPU::mnemonic##_RM32_1(const X86::Instruction& insn) { generic_RM32_1(op<ValueWithShadow<u32>>, insn); } \ + void SoftCPU::mnemonic##_RM32_CL(const X86::Instruction& insn) { generic_RM32_CL(op<ValueWithShadow<u32>>, insn); } \ + void SoftCPU::mnemonic##_RM32_imm8(const X86::Instruction& insn) { generic_RM32_unsigned_imm8<true>(op<ValueWithShadow<u32>>, insn); } + +namespace UserspaceEmulator { + +template<class Dest, class Source> +static inline Dest bit_cast(Source source) +{ + static_assert(sizeof(Dest) == sizeof(Source)); + Dest dest; + memcpy(&dest, &source, sizeof(dest)); + return dest; +} + +template<typename T> +void warn_if_uninitialized(T value_with_shadow, const char* message) +{ + if (value_with_shadow.is_uninitialized()) { + reportln("\033[31;1mWarning! Use of uninitialized value: {}\033[0m\n", message); + Emulator::the().dump_backtrace(); + } +} + +void SoftCPU::warn_if_flags_tainted(const char* message) const +{ + if (m_flags_tainted) { + reportln("\n=={}== \033[31;1mConditional depends on uninitialized data\033[0m ({})\n", getpid(), message); + Emulator::the().dump_backtrace(); + } +} + +template<typename T, typename U> +constexpr T sign_extended_to(U value) +{ + if (!(value & X86::TypeTrivia<U>::sign_bit)) + return value; + return (X86::TypeTrivia<T>::mask & ~X86::TypeTrivia<U>::mask) | value; +} + +SoftCPU::SoftCPU(Emulator& emulator) + : m_emulator(emulator) +{ + memset(m_gpr, 0, sizeof(m_gpr)); + memset(m_gpr_shadow, 1, sizeof(m_gpr_shadow)); + + m_segment[(int)X86::SegmentRegister::CS] = 0x1b; + m_segment[(int)X86::SegmentRegister::DS] = 0x23; + m_segment[(int)X86::SegmentRegister::ES] = 0x23; + m_segment[(int)X86::SegmentRegister::SS] = 0x23; + m_segment[(int)X86::SegmentRegister::GS] = 0x2b; +} + +void SoftCPU::dump() const +{ + outln(" eax={:08x} ebx={:08x} ecx={:08x} edx={:08x} ebp={:08x} esp={:08x} esi={:08x} edi={:08x} o={:d} s={:d} z={:d} a={:d} p={:d} c={:d}", + eax(), ebx(), ecx(), edx(), ebp(), esp(), esi(), edi(), of(), sf(), zf(), af(), pf(), cf()); + outln("#eax={:08x} #ebx={:08x} #ecx={:08x} #edx={:08x} #ebp={:08x} #esp={:08x} #esi={:08x} #edi={:08x} #f={}", + eax().shadow(), ebx().shadow(), ecx().shadow(), edx().shadow(), m_flags_tainted); + fflush(stdout); +} + +void SoftCPU::did_receive_secret_data() +{ + if (m_secret_data[0] == 1) { + if (auto* tracer = m_emulator.malloc_tracer()) + tracer->target_did_malloc({}, m_secret_data[2], m_secret_data[1]); + } else if (m_secret_data[0] == 2) { + if (auto* tracer = m_emulator.malloc_tracer()) + tracer->target_did_free({}, m_secret_data[1]); + } else if (m_secret_data[0] == 3) { + if (auto* tracer = m_emulator.malloc_tracer()) + tracer->target_did_realloc({}, m_secret_data[2], m_secret_data[1]); + } else { + ASSERT_NOT_REACHED(); + } +} + +void SoftCPU::update_code_cache() +{ + auto* region = m_emulator.mmu().find_region({ cs(), eip() }); + ASSERT(region); + + if (!region->is_executable()) { + reportln("SoftCPU::update_code_cache: Non-executable region @ {:p}", eip()); + Emulator::the().dump_backtrace(); + TODO(); + } + + // FIXME: This cache needs to be invalidated if the code region is ever unmapped. + m_cached_code_region = region; + m_cached_code_base_ptr = region->data(); +} + +ValueWithShadow<u8> SoftCPU::read_memory8(X86::LogicalAddress address) +{ + ASSERT(address.selector() == 0x1b || address.selector() == 0x23 || address.selector() == 0x2b); + auto value = m_emulator.mmu().read8(address); +#ifdef MEMORY_DEBUG + outln("\033[36;1mread_memory8: @{:04x}:{:08x} -> {:02x} ({:02x})\033[0m", address.selector(), address.offset(), value, value.shadow()); +#endif + return value; +} + +ValueWithShadow<u16> SoftCPU::read_memory16(X86::LogicalAddress address) +{ + ASSERT(address.selector() == 0x1b || address.selector() == 0x23 || address.selector() == 0x2b); + auto value = m_emulator.mmu().read16(address); +#ifdef MEMORY_DEBUG + outln("\033[36;1mread_memory16: @{:04x}:{:08x} -> {:04x} ({:04x})\033[0m", address.selector(), address.offset(), value, value.shadow()); +#endif + return value; +} + +ValueWithShadow<u32> SoftCPU::read_memory32(X86::LogicalAddress address) +{ + ASSERT(address.selector() == 0x1b || address.selector() == 0x23 || address.selector() == 0x2b); + auto value = m_emulator.mmu().read32(address); +#ifdef MEMORY_DEBUG + outln("\033[36;1mread_memory32: @{:04x}:{:08x} -> {:08x} ({:08x})\033[0m", address.selector(), address.offset(), value, value.shadow()); +#endif + return value; +} + +ValueWithShadow<u64> SoftCPU::read_memory64(X86::LogicalAddress address) +{ + ASSERT(address.selector() == 0x1b || address.selector() == 0x23 || address.selector() == 0x2b); + auto value = m_emulator.mmu().read64(address); +#ifdef MEMORY_DEBUG + outln("\033[36;1mread_memory64: @{:04x}:{:08x} -> {:016x} ({:016x})\033[0m", address.selector(), address.offset(), value, value.shadow()); +#endif + return value; +} + +void SoftCPU::write_memory8(X86::LogicalAddress address, ValueWithShadow<u8> value) +{ + ASSERT(address.selector() == 0x23 || address.selector() == 0x2b); +#ifdef MEMORY_DEBUG + outln("\033[36;1mwrite_memory8: @{:04x}:{:08x} <- {:02x} ({:02x})\033[0m", address.selector(), address.offset(), value, value.shadow()); +#endif + m_emulator.mmu().write8(address, value); +} + +void SoftCPU::write_memory16(X86::LogicalAddress address, ValueWithShadow<u16> value) +{ + ASSERT(address.selector() == 0x23 || address.selector() == 0x2b); +#ifdef MEMORY_DEBUG + outln("\033[36;1mwrite_memory16: @{:04x}:{:08x} <- {:04x} ({:04x})\033[0m", address.selector(), address.offset(), value, value.shadow()); +#endif + m_emulator.mmu().write16(address, value); +} + +void SoftCPU::write_memory32(X86::LogicalAddress address, ValueWithShadow<u32> value) +{ + ASSERT(address.selector() == 0x23 || address.selector() == 0x2b); +#ifdef MEMORY_DEBUG + outln("\033[36;1mwrite_memory32: @{:04x}:{:08x} <- {:08x} ({:08x})\033[0m", address.selector(), address.offset(), value, value.shadow()); +#endif + m_emulator.mmu().write32(address, value); +} + +void SoftCPU::write_memory64(X86::LogicalAddress address, ValueWithShadow<u64> value) +{ + ASSERT(address.selector() == 0x23 || address.selector() == 0x2b); +#ifdef MEMORY_DEBUG + outln("\033[36;1mwrite_memory64: @{:04x}:{:08x} <- {:016x} ({:016x})\033[0m", address.selector(), address.offset(), value, value.shadow()); +#endif + m_emulator.mmu().write64(address, value); +} + +void SoftCPU::push_string(const StringView& string) +{ + size_t space_to_allocate = round_up_to_power_of_two(string.length() + 1, 16); + set_esp({ esp().value() - space_to_allocate, esp().shadow() }); + m_emulator.mmu().copy_to_vm(esp().value(), string.characters_without_null_termination(), string.length()); + m_emulator.mmu().write8({ 0x23, esp().value() + string.length() }, shadow_wrap_as_initialized((u8)'\0')); +} + +void SoftCPU::push_buffer(const u8* data, size_t size) +{ + set_esp({ esp().value() - size, esp().shadow() }); + warn_if_uninitialized(esp(), "push_buffer"); + m_emulator.mmu().copy_to_vm(esp().value(), data, size); +} + +void SoftCPU::push32(ValueWithShadow<u32> value) +{ + set_esp({ esp().value() - sizeof(u32), esp().shadow() }); + warn_if_uninitialized(esp(), "push32"); + write_memory32({ ss(), esp().value() }, value); +} + +ValueWithShadow<u32> SoftCPU::pop32() +{ + warn_if_uninitialized(esp(), "pop32"); + auto value = read_memory32({ ss(), esp().value() }); + set_esp({ esp().value() + sizeof(u32), esp().shadow() }); + return value; +} + +void SoftCPU::push16(ValueWithShadow<u16> value) +{ + warn_if_uninitialized(esp(), "push16"); + set_esp({ esp().value() - sizeof(u16), esp().shadow() }); + write_memory16({ ss(), esp().value() }, value); +} + +ValueWithShadow<u16> SoftCPU::pop16() +{ + warn_if_uninitialized(esp(), "pop16"); + auto value = read_memory16({ ss(), esp().value() }); + set_esp({ esp().value() + sizeof(u16), esp().shadow() }); + return value; +} + +template<bool check_zf, typename Callback> +void SoftCPU::do_once_or_repeat(const X86::Instruction& insn, Callback callback) +{ + if (!insn.has_rep_prefix()) + return callback(); + + while (loop_index(insn.a32()).value()) { + callback(); + decrement_loop_index(insn.a32()); + if constexpr (check_zf) { + warn_if_flags_tainted("repz/repnz"); + if (insn.rep_prefix() == X86::Prefix::REPZ && !zf()) + break; + if (insn.rep_prefix() == X86::Prefix::REPNZ && zf()) + break; + } + } +} + +template<typename T> +ALWAYS_INLINE static T op_inc(SoftCPU& cpu, T data) +{ + typename T::ValueType result; + u32 new_flags = 0; + + if constexpr (sizeof(typename T::ValueType) == 4) { + asm volatile("incl %%eax\n" + : "=a"(result) + : "a"(data.value())); + } else if constexpr (sizeof(typename T::ValueType) == 2) { + asm volatile("incw %%ax\n" + : "=a"(result) + : "a"(data.value())); + } else if constexpr (sizeof(typename T::ValueType) == 1) { + asm volatile("incb %%al\n" + : "=a"(result) + : "a"(data.value())); + } + + asm volatile( + "pushf\n" + "pop %%ebx" + : "=b"(new_flags)); + + cpu.set_flags_oszap(new_flags); + cpu.taint_flags_from(data); + return shadow_wrap_with_taint_from(result, data); +} + +template<typename T> +ALWAYS_INLINE static T op_dec(SoftCPU& cpu, T data) +{ + typename T::ValueType result; + u32 new_flags = 0; + + if constexpr (sizeof(typename T::ValueType) == 4) { + asm volatile("decl %%eax\n" + : "=a"(result) + : "a"(data.value())); + } else if constexpr (sizeof(typename T::ValueType) == 2) { + asm volatile("decw %%ax\n" + : "=a"(result) + : "a"(data.value())); + } else if constexpr (sizeof(typename T::ValueType) == 1) { + asm volatile("decb %%al\n" + : "=a"(result) + : "a"(data.value())); + } + + asm volatile( + "pushf\n" + "pop %%ebx" + : "=b"(new_flags)); + + cpu.set_flags_oszap(new_flags); + cpu.taint_flags_from(data); + return shadow_wrap_with_taint_from(result, data); +} + +template<typename T> +ALWAYS_INLINE static T op_xor(SoftCPU& cpu, const T& dest, const T& src) +{ + typename T::ValueType result; + u32 new_flags = 0; + + if constexpr (sizeof(typename T::ValueType) == 4) { + asm volatile("xorl %%ecx, %%eax\n" + : "=a"(result) + : "a"(dest.value()), "c"(src.value())); + } else if constexpr (sizeof(typename T::ValueType) == 2) { + asm volatile("xor %%cx, %%ax\n" + : "=a"(result) + : "a"(dest.value()), "c"(src.value())); + } else if constexpr (sizeof(typename T::ValueType) == 1) { + asm volatile("xorb %%cl, %%al\n" + : "=a"(result) + : "a"(dest.value()), "c"(src.value())); + } else { + ASSERT_NOT_REACHED(); + } + + asm volatile( + "pushf\n" + "pop %%ebx" + : "=b"(new_flags)); + + cpu.set_flags_oszpc(new_flags); + cpu.taint_flags_from(dest, src); + return shadow_wrap_with_taint_from(result, dest, src); +} + +template<typename T> +ALWAYS_INLINE static T op_or(SoftCPU& cpu, const T& dest, const T& src) +{ + typename T::ValueType result = 0; + u32 new_flags = 0; + + if constexpr (sizeof(typename T::ValueType) == 4) { + asm volatile("orl %%ecx, %%eax\n" + : "=a"(result) + : "a"(dest.value()), "c"(src.value())); + } else if constexpr (sizeof(typename T::ValueType) == 2) { + asm volatile("or %%cx, %%ax\n" + : "=a"(result) + : "a"(dest.value()), "c"(src.value())); + } else if constexpr (sizeof(typename T::ValueType) == 1) { + asm volatile("orb %%cl, %%al\n" + : "=a"(result) + : "a"(dest.value()), "c"(src.value())); + } else { + ASSERT_NOT_REACHED(); + } + + asm volatile( + "pushf\n" + "pop %%ebx" + : "=b"(new_flags)); + + cpu.set_flags_oszpc(new_flags); + cpu.taint_flags_from(dest, src); + return shadow_wrap_with_taint_from(result, dest, src); +} + +template<typename T> +ALWAYS_INLINE static T op_sub(SoftCPU& cpu, const T& dest, const T& src) +{ + typename T::ValueType result = 0; + u32 new_flags = 0; + + if constexpr (sizeof(typename T::ValueType) == 4) { + asm volatile("subl %%ecx, %%eax\n" + : "=a"(result) + : "a"(dest.value()), "c"(src.value())); + } else if constexpr (sizeof(typename T::ValueType) == 2) { + asm volatile("subw %%cx, %%ax\n" + : "=a"(result) + : "a"(dest.value()), "c"(src.value())); + } else if constexpr (sizeof(typename T::ValueType) == 1) { + asm volatile("subb %%cl, %%al\n" + : "=a"(result) + : "a"(dest.value()), "c"(src.value())); + } else { + ASSERT_NOT_REACHED(); + } + + asm volatile( + "pushf\n" + "pop %%ebx" + : "=b"(new_flags)); + + cpu.set_flags_oszapc(new_flags); + cpu.taint_flags_from(dest, src); + return shadow_wrap_with_taint_from(result, dest, src); +} + +template<typename T, bool cf> +ALWAYS_INLINE static T op_sbb_impl(SoftCPU& cpu, const T& dest, const T& src) +{ + typename T::ValueType result = 0; + u32 new_flags = 0; + + if constexpr (cf) + asm volatile("stc"); + else + asm volatile("clc"); + + if constexpr (sizeof(typename T::ValueType) == 4) { + asm volatile("sbbl %%ecx, %%eax\n" + : "=a"(result) + : "a"(dest.value()), "c"(src.value())); + } else if constexpr (sizeof(typename T::ValueType) == 2) { + asm volatile("sbbw %%cx, %%ax\n" + : "=a"(result) + : "a"(dest.value()), "c"(src.value())); + } else if constexpr (sizeof(typename T::ValueType) == 1) { + asm volatile("sbbb %%cl, %%al\n" + : "=a"(result) + : "a"(dest.value()), "c"(src.value())); + } else { + ASSERT_NOT_REACHED(); + } + + asm volatile( + "pushf\n" + "pop %%ebx" + : "=b"(new_flags)); + + cpu.set_flags_oszapc(new_flags); + cpu.taint_flags_from(dest, src); + return shadow_wrap_with_taint_from<typename T::ValueType>(result, dest, src); +} + +template<typename T> +ALWAYS_INLINE static T op_sbb(SoftCPU& cpu, T& dest, const T& src) +{ + cpu.warn_if_flags_tainted("sbb"); + if (cpu.cf()) + return op_sbb_impl<T, true>(cpu, dest, src); + return op_sbb_impl<T, false>(cpu, dest, src); +} + +template<typename T> +ALWAYS_INLINE static T op_add(SoftCPU& cpu, T& dest, const T& src) +{ + typename T::ValueType result = 0; + u32 new_flags = 0; + + if constexpr (sizeof(typename T::ValueType) == 4) { + asm volatile("addl %%ecx, %%eax\n" + : "=a"(result) + : "a"(dest.value()), "c"(src.value())); + } else if constexpr (sizeof(typename T::ValueType) == 2) { + asm volatile("addw %%cx, %%ax\n" + : "=a"(result) + : "a"(dest.value()), "c"(src.value())); + } else if constexpr (sizeof(typename T::ValueType) == 1) { + asm volatile("addb %%cl, %%al\n" + : "=a"(result) + : "a"(dest.value()), "c"(src.value())); + } else { + ASSERT_NOT_REACHED(); + } + + asm volatile( + "pushf\n" + "pop %%ebx" + : "=b"(new_flags)); + + cpu.set_flags_oszapc(new_flags); + cpu.taint_flags_from(dest, src); + return shadow_wrap_with_taint_from<typename T::ValueType>(result, dest, src); +} + +template<typename T, bool cf> +ALWAYS_INLINE static T op_adc_impl(SoftCPU& cpu, T& dest, const T& src) +{ + typename T::ValueType result = 0; + u32 new_flags = 0; + + if constexpr (cf) + asm volatile("stc"); + else + asm volatile("clc"); + + if constexpr (sizeof(typename T::ValueType) == 4) { + asm volatile("adcl %%ecx, %%eax\n" + : "=a"(result) + : "a"(dest.value()), "c"(src.value())); + } else if constexpr (sizeof(typename T::ValueType) == 2) { + asm volatile("adcw %%cx, %%ax\n" + : "=a"(result) + : "a"(dest.value()), "c"(src.value())); + } else if constexpr (sizeof(typename T::ValueType) == 1) { + asm volatile("adcb %%cl, %%al\n" + : "=a"(result) + : "a"(dest.value()), "c"(src.value())); + } else { + ASSERT_NOT_REACHED(); + } + + asm volatile( + "pushf\n" + "pop %%ebx" + : "=b"(new_flags)); + + cpu.set_flags_oszapc(new_flags); + cpu.taint_flags_from(dest, src); + return shadow_wrap_with_taint_from<typename T::ValueType>(result, dest, src); +} + +template<typename T> +ALWAYS_INLINE static T op_adc(SoftCPU& cpu, T& dest, const T& src) +{ + cpu.warn_if_flags_tainted("adc"); + if (cpu.cf()) + return op_adc_impl<T, true>(cpu, dest, src); + return op_adc_impl<T, false>(cpu, dest, src); +} + +template<typename T> +ALWAYS_INLINE static T op_and(SoftCPU& cpu, const T& dest, const T& src) +{ + typename T::ValueType result = 0; + u32 new_flags = 0; + + if constexpr (sizeof(typename T::ValueType) == 4) { + asm volatile("andl %%ecx, %%eax\n" + : "=a"(result) + : "a"(dest.value()), "c"(src.value())); + } else if constexpr (sizeof(typename T::ValueType) == 2) { + asm volatile("andw %%cx, %%ax\n" + : "=a"(result) + : "a"(dest.value()), "c"(src.value())); + } else if constexpr (sizeof(typename T::ValueType) == 1) { + asm volatile("andb %%cl, %%al\n" + : "=a"(result) + : "a"(dest.value()), "c"(src.value())); + } else { + ASSERT_NOT_REACHED(); + } + + asm volatile( + "pushf\n" + "pop %%ebx" + : "=b"(new_flags)); + + cpu.set_flags_oszpc(new_flags); + cpu.taint_flags_from(dest, src); + return shadow_wrap_with_taint_from<typename T::ValueType>(result, dest, src); +} + +template<typename T> +ALWAYS_INLINE static void op_imul(SoftCPU& cpu, const T& dest, const T& src, T& result_high, T& result_low) +{ + bool did_overflow = false; + if constexpr (sizeof(T) == 4) { + i64 result = (i64)src * (i64)dest; + result_low = result & 0xffffffff; + result_high = result >> 32; + did_overflow = (result > NumericLimits<T>::max() || result < NumericLimits<T>::min()); + } else if constexpr (sizeof(T) == 2) { + i32 result = (i32)src * (i32)dest; + result_low = result & 0xffff; + result_high = result >> 16; + did_overflow = (result > NumericLimits<T>::max() || result < NumericLimits<T>::min()); + } else if constexpr (sizeof(T) == 1) { + i16 result = (i16)src * (i16)dest; + result_low = result & 0xff; + result_high = result >> 8; + did_overflow = (result > NumericLimits<T>::max() || result < NumericLimits<T>::min()); + } + + if (did_overflow) { + cpu.set_cf(true); + cpu.set_of(true); + } else { + cpu.set_cf(false); + cpu.set_of(false); + } +} + +template<typename T> +ALWAYS_INLINE static T op_shr(SoftCPU& cpu, T data, ValueWithShadow<u8> steps) +{ + if (steps.value() == 0) + return shadow_wrap_with_taint_from(data.value(), data, steps); + + u32 result = 0; + u32 new_flags = 0; + + if constexpr (sizeof(typename T::ValueType) == 4) { + asm volatile("shrl %%cl, %%eax\n" + : "=a"(result) + : "a"(data.value()), "c"(steps.value())); + } else if constexpr (sizeof(typename T::ValueType) == 2) { + asm volatile("shrw %%cl, %%ax\n" + : "=a"(result) + : "a"(data.value()), "c"(steps.value())); + } else if constexpr (sizeof(typename T::ValueType) == 1) { + asm volatile("shrb %%cl, %%al\n" + : "=a"(result) + : "a"(data.value()), "c"(steps.value())); + } + + asm volatile( + "pushf\n" + "pop %%ebx" + : "=b"(new_flags)); + + cpu.set_flags_oszapc(new_flags); + cpu.taint_flags_from(data, steps); + return shadow_wrap_with_taint_from<typename T::ValueType>(result, data, steps); +} + +template<typename T> +ALWAYS_INLINE static T op_shl(SoftCPU& cpu, T data, ValueWithShadow<u8> steps) +{ + if (steps.value() == 0) + return shadow_wrap_with_taint_from(data.value(), data, steps); + + u32 result = 0; + u32 new_flags = 0; + + if constexpr (sizeof(typename T::ValueType) == 4) { + asm volatile("shll %%cl, %%eax\n" + : "=a"(result) + : "a"(data.value()), "c"(steps.value())); + } else if constexpr (sizeof(typename T::ValueType) == 2) { + asm volatile("shlw %%cl, %%ax\n" + : "=a"(result) + : "a"(data.value()), "c"(steps.value())); + } else if constexpr (sizeof(typename T::ValueType) == 1) { + asm volatile("shlb %%cl, %%al\n" + : "=a"(result) + : "a"(data.value()), "c"(steps.value())); + } + + asm volatile( + "pushf\n" + "pop %%ebx" + : "=b"(new_flags)); + + cpu.set_flags_oszapc(new_flags); + cpu.taint_flags_from(data, steps); + return shadow_wrap_with_taint_from<typename T::ValueType>(result, data, steps); +} + +template<typename T> +ALWAYS_INLINE static T op_shrd(SoftCPU& cpu, T data, T extra_bits, ValueWithShadow<u8> steps) +{ + if (steps.value() == 0) + return shadow_wrap_with_taint_from(data.value(), data, steps); + + u32 result = 0; + u32 new_flags = 0; + + if constexpr (sizeof(typename T::ValueType) == 4) { + asm volatile("shrd %%cl, %%edx, %%eax\n" + : "=a"(result) + : "a"(data.value()), "d"(extra_bits.value()), "c"(steps.value())); + } else if constexpr (sizeof(typename T::ValueType) == 2) { + asm volatile("shrd %%cl, %%dx, %%ax\n" + : "=a"(result) + : "a"(data.value()), "d"(extra_bits.value()), "c"(steps.value())); + } + + asm volatile( + "pushf\n" + "pop %%ebx" + : "=b"(new_flags)); + + cpu.set_flags_oszapc(new_flags); + cpu.taint_flags_from(data, steps); + return shadow_wrap_with_taint_from<typename T::ValueType>(result, data, steps); +} + +template<typename T> +ALWAYS_INLINE static T op_shld(SoftCPU& cpu, T data, T extra_bits, ValueWithShadow<u8> steps) +{ + if (steps.value() == 0) + return shadow_wrap_with_taint_from(data.value(), data, steps); + + u32 result = 0; + u32 new_flags = 0; + + if constexpr (sizeof(typename T::ValueType) == 4) { + asm volatile("shld %%cl, %%edx, %%eax\n" + : "=a"(result) + : "a"(data.value()), "d"(extra_bits.value()), "c"(steps.value())); + } else if constexpr (sizeof(typename T::ValueType) == 2) { + asm volatile("shld %%cl, %%dx, %%ax\n" + : "=a"(result) + : "a"(data.value()), "d"(extra_bits.value()), "c"(steps.value())); + } + + asm volatile( + "pushf\n" + "pop %%ebx" + : "=b"(new_flags)); + + cpu.set_flags_oszapc(new_flags); + cpu.taint_flags_from(data, steps); + return shadow_wrap_with_taint_from<typename T::ValueType>(result, data, steps); +} + +template<bool update_dest, bool is_or, typename Op> +ALWAYS_INLINE void SoftCPU::generic_AL_imm8(Op op, const X86::Instruction& insn) +{ + auto dest = al(); + auto src = shadow_wrap_as_initialized(insn.imm8()); + auto result = op(*this, dest, src); + if (is_or && insn.imm8() == 0xff) + result.set_initialized(); + if (update_dest) + set_al(result); +} + +template<bool update_dest, bool is_or, typename Op> +ALWAYS_INLINE void SoftCPU::generic_AX_imm16(Op op, const X86::Instruction& insn) +{ + auto dest = ax(); + auto src = shadow_wrap_as_initialized(insn.imm16()); + auto result = op(*this, dest, src); + if (is_or && insn.imm16() == 0xffff) + result.set_initialized(); + if (update_dest) + set_ax(result); +} + +template<bool update_dest, bool is_or, typename Op> +ALWAYS_INLINE void SoftCPU::generic_EAX_imm32(Op op, const X86::Instruction& insn) +{ + auto dest = eax(); + auto src = shadow_wrap_as_initialized(insn.imm32()); + auto result = op(*this, dest, src); + if (is_or && insn.imm32() == 0xffffffff) + result.set_initialized(); + if (update_dest) + set_eax(result); +} + +template<bool update_dest, bool is_or, typename Op> +ALWAYS_INLINE void SoftCPU::generic_RM16_imm16(Op op, const X86::Instruction& insn) +{ + auto dest = insn.modrm().read16(*this, insn); + auto src = shadow_wrap_as_initialized(insn.imm16()); + auto result = op(*this, dest, src); + if (is_or && insn.imm16() == 0xffff) + result.set_initialized(); + if (update_dest) + insn.modrm().write16(*this, insn, result); +} + +template<bool update_dest, bool is_or, typename Op> +ALWAYS_INLINE void SoftCPU::generic_RM16_imm8(Op op, const X86::Instruction& insn) +{ + auto dest = insn.modrm().read16(*this, insn); + auto src = shadow_wrap_as_initialized<u16>(sign_extended_to<u16>(insn.imm8())); + auto result = op(*this, dest, src); + if (is_or && src.value() == 0xffff) + result.set_initialized(); + if (update_dest) + insn.modrm().write16(*this, insn, result); +} + +template<bool update_dest, typename Op> +ALWAYS_INLINE void SoftCPU::generic_RM16_unsigned_imm8(Op op, const X86::Instruction& insn) +{ + auto dest = insn.modrm().read16(*this, insn); + auto src = shadow_wrap_as_initialized(insn.imm8()); + auto result = op(*this, dest, src); + if (update_dest) + insn.modrm().write16(*this, insn, result); +} + +template<bool update_dest, bool dont_taint_for_same_operand, typename Op> +ALWAYS_INLINE void SoftCPU::generic_RM16_reg16(Op op, const X86::Instruction& insn) +{ + auto dest = insn.modrm().read16(*this, insn); + auto src = const_gpr16(insn.reg16()); + auto result = op(*this, dest, src); + if (dont_taint_for_same_operand && insn.modrm().is_register() && insn.modrm().register_index() == insn.register_index()) { + result.set_initialized(); + m_flags_tainted = false; + } + if (update_dest) + insn.modrm().write16(*this, insn, result); +} + +template<bool update_dest, bool is_or, typename Op> +ALWAYS_INLINE void SoftCPU::generic_RM32_imm32(Op op, const X86::Instruction& insn) +{ + auto dest = insn.modrm().read32(*this, insn); + auto src = insn.imm32(); + auto result = op(*this, dest, shadow_wrap_as_initialized(src)); + if (is_or && src == 0xffffffff) + result.set_initialized(); + if (update_dest) + insn.modrm().write32(*this, insn, result); +} + +template<bool update_dest, bool is_or, typename Op> +ALWAYS_INLINE void SoftCPU::generic_RM32_imm8(Op op, const X86::Instruction& insn) +{ + auto dest = insn.modrm().read32(*this, insn); + auto src = sign_extended_to<u32>(insn.imm8()); + auto result = op(*this, dest, shadow_wrap_as_initialized(src)); + if (is_or && src == 0xffffffff) + result.set_initialized(); + if (update_dest) + insn.modrm().write32(*this, insn, result); +} + +template<bool update_dest, typename Op> +ALWAYS_INLINE void SoftCPU::generic_RM32_unsigned_imm8(Op op, const X86::Instruction& insn) +{ + auto dest = insn.modrm().read32(*this, insn); + auto src = shadow_wrap_as_initialized(insn.imm8()); + auto result = op(*this, dest, src); + if (update_dest) + insn.modrm().write32(*this, insn, result); +} + +template<bool update_dest, bool dont_taint_for_same_operand, typename Op> +ALWAYS_INLINE void SoftCPU::generic_RM32_reg32(Op op, const X86::Instruction& insn) +{ + auto dest = insn.modrm().read32(*this, insn); + auto src = const_gpr32(insn.reg32()); + auto result = op(*this, dest, src); + if (dont_taint_for_same_operand && insn.modrm().is_register() && insn.modrm().register_index() == insn.register_index()) { + result.set_initialized(); + m_flags_tainted = false; + } + if (update_dest) + insn.modrm().write32(*this, insn, result); +} + +template<bool update_dest, bool is_or, typename Op> +ALWAYS_INLINE void SoftCPU::generic_RM8_imm8(Op op, const X86::Instruction& insn) +{ + auto dest = insn.modrm().read8(*this, insn); + auto src = insn.imm8(); + auto result = op(*this, dest, shadow_wrap_as_initialized(src)); + if (is_or && src == 0xff) + result.set_initialized(); + if (update_dest) + insn.modrm().write8(*this, insn, result); +} + +template<bool update_dest, bool dont_taint_for_same_operand, typename Op> +ALWAYS_INLINE void SoftCPU::generic_RM8_reg8(Op op, const X86::Instruction& insn) +{ + auto dest = insn.modrm().read8(*this, insn); + auto src = const_gpr8(insn.reg8()); + auto result = op(*this, dest, src); + if (dont_taint_for_same_operand && insn.modrm().is_register() && insn.modrm().register_index() == insn.register_index()) { + result.set_initialized(); + m_flags_tainted = false; + } + if (update_dest) + insn.modrm().write8(*this, insn, result); +} + +template<bool update_dest, bool dont_taint_for_same_operand, typename Op> +ALWAYS_INLINE void SoftCPU::generic_reg16_RM16(Op op, const X86::Instruction& insn) +{ + auto dest = const_gpr16(insn.reg16()); + auto src = insn.modrm().read16(*this, insn); + auto result = op(*this, dest, src); + if (dont_taint_for_same_operand && insn.modrm().is_register() && insn.modrm().register_index() == insn.register_index()) { + result.set_initialized(); + m_flags_tainted = false; + } + if (update_dest) + gpr16(insn.reg16()) = result; +} + +template<bool update_dest, bool dont_taint_for_same_operand, typename Op> +ALWAYS_INLINE void SoftCPU::generic_reg32_RM32(Op op, const X86::Instruction& insn) +{ + auto dest = const_gpr32(insn.reg32()); + auto src = insn.modrm().read32(*this, insn); + auto result = op(*this, dest, src); + if (dont_taint_for_same_operand && insn.modrm().is_register() && insn.modrm().register_index() == insn.register_index()) { + result.set_initialized(); + m_flags_tainted = false; + } + if (update_dest) + gpr32(insn.reg32()) = result; +} + +template<bool update_dest, bool dont_taint_for_same_operand, typename Op> +ALWAYS_INLINE void SoftCPU::generic_reg8_RM8(Op op, const X86::Instruction& insn) +{ + auto dest = const_gpr8(insn.reg8()); + auto src = insn.modrm().read8(*this, insn); + auto result = op(*this, dest, src); + if (dont_taint_for_same_operand && insn.modrm().is_register() && insn.modrm().register_index() == insn.register_index()) { + result.set_initialized(); + m_flags_tainted = false; + } + if (update_dest) + gpr8(insn.reg8()) = result; +} + +template<typename Op> +ALWAYS_INLINE void SoftCPU::generic_RM8_1(Op op, const X86::Instruction& insn) +{ + auto data = insn.modrm().read8(*this, insn); + insn.modrm().write8(*this, insn, op(*this, data, shadow_wrap_as_initialized<u8>(1))); +} + +template<typename Op> +ALWAYS_INLINE void SoftCPU::generic_RM8_CL(Op op, const X86::Instruction& insn) +{ + auto data = insn.modrm().read8(*this, insn); + insn.modrm().write8(*this, insn, op(*this, data, cl())); +} + +template<typename Op> +ALWAYS_INLINE void SoftCPU::generic_RM16_1(Op op, const X86::Instruction& insn) +{ + auto data = insn.modrm().read16(*this, insn); + insn.modrm().write16(*this, insn, op(*this, data, shadow_wrap_as_initialized<u8>(1))); +} + +template<typename Op> +ALWAYS_INLINE void SoftCPU::generic_RM16_CL(Op op, const X86::Instruction& insn) +{ + auto data = insn.modrm().read16(*this, insn); + insn.modrm().write16(*this, insn, op(*this, data, cl())); +} + +template<typename Op> +ALWAYS_INLINE void SoftCPU::generic_RM32_1(Op op, const X86::Instruction& insn) +{ + auto data = insn.modrm().read32(*this, insn); + insn.modrm().write32(*this, insn, op(*this, data, shadow_wrap_as_initialized<u8>(1))); +} + +template<typename Op> +ALWAYS_INLINE void SoftCPU::generic_RM32_CL(Op op, const X86::Instruction& insn) +{ + auto data = insn.modrm().read32(*this, insn); + insn.modrm().write32(*this, insn, op(*this, data, cl())); +} + +void SoftCPU::AAA(const X86::Instruction&) { TODO_INSN(); } +void SoftCPU::AAD(const X86::Instruction&) { TODO_INSN(); } +void SoftCPU::AAM(const X86::Instruction&) { TODO_INSN(); } +void SoftCPU::AAS(const X86::Instruction&) { TODO_INSN(); } +void SoftCPU::ARPL(const X86::Instruction&) { TODO_INSN(); } +void SoftCPU::BOUND(const X86::Instruction&) { TODO_INSN(); } + +template<typename T> +ALWAYS_INLINE static T op_bsf(SoftCPU&, T value) +{ + return { (typename T::ValueType)__builtin_ctz(value.value()), value.shadow() }; +} + +template<typename T> +ALWAYS_INLINE static T op_bsr(SoftCPU&, T value) +{ + typename T::ValueType bit_index = 0; + if constexpr (sizeof(typename T::ValueType) == 4) { + asm volatile("bsrl %%eax, %%edx" + : "=d"(bit_index) + : "a"(value.value())); + } + if constexpr (sizeof(typename T::ValueType) == 2) { + asm volatile("bsrw %%ax, %%dx" + : "=d"(bit_index) + : "a"(value.value())); + } + return shadow_wrap_with_taint_from(bit_index, value); +} + +void SoftCPU::BSF_reg16_RM16(const X86::Instruction& insn) +{ + auto src = insn.modrm().read16(*this, insn); + set_zf(!src.value()); + if (src.value()) + gpr16(insn.reg16()) = op_bsf(*this, src); + taint_flags_from(src); +} + +void SoftCPU::BSF_reg32_RM32(const X86::Instruction& insn) +{ + auto src = insn.modrm().read32(*this, insn); + set_zf(!src.value()); + if (src.value()) { + gpr32(insn.reg32()) = op_bsf(*this, src); + taint_flags_from(src); + } +} + +void SoftCPU::BSR_reg16_RM16(const X86::Instruction& insn) +{ + auto src = insn.modrm().read16(*this, insn); + set_zf(!src.value()); + if (src.value()) { + gpr16(insn.reg16()) = op_bsr(*this, src); + taint_flags_from(src); + } +} + +void SoftCPU::BSR_reg32_RM32(const X86::Instruction& insn) +{ + auto src = insn.modrm().read32(*this, insn); + set_zf(!src.value()); + if (src.value()) { + gpr32(insn.reg32()) = op_bsr(*this, src); + taint_flags_from(src); + } +} + +void SoftCPU::BSWAP_reg32(const X86::Instruction& insn) +{ + gpr32(insn.reg32()) = { __builtin_bswap32(gpr32(insn.reg32()).value()), __builtin_bswap32(gpr32(insn.reg32()).shadow()) }; +} + +template<typename T> +ALWAYS_INLINE static T op_bt(T value, T) +{ + return value; +} + +template<typename T> +ALWAYS_INLINE static T op_bts(T value, T bit_mask) +{ + return value | bit_mask; +} + +template<typename T> +ALWAYS_INLINE static T op_btr(T value, T bit_mask) +{ + return value & ~bit_mask; +} + +template<typename T> +ALWAYS_INLINE static T op_btc(T value, T bit_mask) +{ + return value ^ bit_mask; +} + +template<bool should_update, typename Op> +ALWAYS_INLINE void BTx_RM16_reg16(SoftCPU& cpu, const X86::Instruction& insn, Op op) +{ + if (insn.modrm().is_register()) { + unsigned bit_index = cpu.const_gpr16(insn.reg16()).value() & (X86::TypeTrivia<u16>::bits - 1); + auto original = insn.modrm().read16(cpu, insn); + u16 bit_mask = 1 << bit_index; + u16 result = op(original.value(), bit_mask); + cpu.set_cf((original.value() & bit_mask) != 0); + cpu.taint_flags_from(cpu.gpr16(insn.reg16()), original); + if (should_update) + insn.modrm().write16(cpu, insn, shadow_wrap_with_taint_from(result, cpu.gpr16(insn.reg16()), original)); + return; + } + // FIXME: Is this supposed to perform a full 16-bit read/modify/write? + unsigned bit_offset_in_array = cpu.const_gpr16(insn.reg16()).value() / 8; + unsigned bit_offset_in_byte = cpu.const_gpr16(insn.reg16()).value() & 7; + auto address = insn.modrm().resolve(cpu, insn); + address.set_offset(address.offset() + bit_offset_in_array); + auto dest = cpu.read_memory8(address); + u8 bit_mask = 1 << bit_offset_in_byte; + u8 result = op(dest.value(), bit_mask); + cpu.set_cf((dest.value() & bit_mask) != 0); + cpu.taint_flags_from(cpu.gpr16(insn.reg16()), dest); + if (should_update) + cpu.write_memory8(address, shadow_wrap_with_taint_from(result, cpu.gpr16(insn.reg16()), dest)); +} + +template<bool should_update, typename Op> +ALWAYS_INLINE void BTx_RM32_reg32(SoftCPU& cpu, const X86::Instruction& insn, Op op) +{ + if (insn.modrm().is_register()) { + unsigned bit_index = cpu.const_gpr32(insn.reg32()).value() & (X86::TypeTrivia<u32>::bits - 1); + auto original = insn.modrm().read32(cpu, insn); + u32 bit_mask = 1 << bit_index; + u32 result = op(original.value(), bit_mask); + cpu.set_cf((original.value() & bit_mask) != 0); + cpu.taint_flags_from(cpu.gpr32(insn.reg32()), original); + if (should_update) + insn.modrm().write32(cpu, insn, shadow_wrap_with_taint_from(result, cpu.gpr32(insn.reg32()), original)); + return; + } + // FIXME: Is this supposed to perform a full 32-bit read/modify/write? + unsigned bit_offset_in_array = cpu.const_gpr32(insn.reg32()).value() / 8; + unsigned bit_offset_in_byte = cpu.const_gpr32(insn.reg32()).value() & 7; + auto address = insn.modrm().resolve(cpu, insn); + address.set_offset(address.offset() + bit_offset_in_array); + auto dest = cpu.read_memory8(address); + u8 bit_mask = 1 << bit_offset_in_byte; + u8 result = op(dest.value(), bit_mask); + cpu.set_cf((dest.value() & bit_mask) != 0); + cpu.taint_flags_from(cpu.gpr32(insn.reg32()), dest); + if (should_update) + cpu.write_memory8(address, shadow_wrap_with_taint_from(result, cpu.gpr32(insn.reg32()), dest)); +} + +template<bool should_update, typename Op> +ALWAYS_INLINE void BTx_RM16_imm8(SoftCPU& cpu, const X86::Instruction& insn, Op op) +{ + unsigned bit_index = insn.imm8() & (X86::TypeTrivia<u16>::mask); + + // FIXME: Support higher bit indices + ASSERT(bit_index < 16); + + auto original = insn.modrm().read16(cpu, insn); + u16 bit_mask = 1 << bit_index; + auto result = op(original.value(), bit_mask); + cpu.set_cf((original.value() & bit_mask) != 0); + cpu.taint_flags_from(original); + if (should_update) + insn.modrm().write16(cpu, insn, shadow_wrap_with_taint_from(result, original)); +} + +template<bool should_update, typename Op> +ALWAYS_INLINE void BTx_RM32_imm8(SoftCPU& cpu, const X86::Instruction& insn, Op op) +{ + unsigned bit_index = insn.imm8() & (X86::TypeTrivia<u32>::mask); + + // FIXME: Support higher bit indices + ASSERT(bit_index < 32); + + auto original = insn.modrm().read32(cpu, insn); + u32 bit_mask = 1 << bit_index; + auto result = op(original.value(), bit_mask); + cpu.set_cf((original.value() & bit_mask) != 0); + cpu.taint_flags_from(original); + if (should_update) + insn.modrm().write32(cpu, insn, shadow_wrap_with_taint_from(result, original)); +} + +#define DEFINE_GENERIC_BTx_INSN_HANDLERS(mnemonic, op, update_dest) \ + void SoftCPU::mnemonic##_RM32_reg32(const X86::Instruction& insn) { BTx_RM32_reg32<update_dest>(*this, insn, op<u32>); } \ + void SoftCPU::mnemonic##_RM16_reg16(const X86::Instruction& insn) { BTx_RM16_reg16<update_dest>(*this, insn, op<u16>); } \ + void SoftCPU::mnemonic##_RM32_imm8(const X86::Instruction& insn) { BTx_RM32_imm8<update_dest>(*this, insn, op<u32>); } \ + void SoftCPU::mnemonic##_RM16_imm8(const X86::Instruction& insn) { BTx_RM16_imm8<update_dest>(*this, insn, op<u16>); } + +DEFINE_GENERIC_BTx_INSN_HANDLERS(BTS, op_bts, true); +DEFINE_GENERIC_BTx_INSN_HANDLERS(BTR, op_btr, true); +DEFINE_GENERIC_BTx_INSN_HANDLERS(BTC, op_btc, true); +DEFINE_GENERIC_BTx_INSN_HANDLERS(BT, op_bt, false); + +void SoftCPU::CALL_FAR_mem16(const X86::Instruction&) +{ + TODO(); +} +void SoftCPU::CALL_FAR_mem32(const X86::Instruction&) { TODO_INSN(); } +void SoftCPU::CALL_RM16(const X86::Instruction&) { TODO_INSN(); } + +void SoftCPU::CALL_RM32(const X86::Instruction& insn) +{ + push32(shadow_wrap_as_initialized(eip())); + auto address = insn.modrm().read32(*this, insn); + warn_if_uninitialized(address, "call rm32"); + set_eip(address.value()); +} + +void SoftCPU::CALL_imm16(const X86::Instruction&) { TODO_INSN(); } +void SoftCPU::CALL_imm16_imm16(const X86::Instruction&) { TODO_INSN(); } +void SoftCPU::CALL_imm16_imm32(const X86::Instruction&) { TODO_INSN(); } + +void SoftCPU::CALL_imm32(const X86::Instruction& insn) +{ + push32(shadow_wrap_as_initialized(eip())); + set_eip(eip() + (i32)insn.imm32()); +} + +void SoftCPU::CBW(const X86::Instruction&) +{ + set_ah(shadow_wrap_with_taint_from<u8>((al().value() & 0x80) ? 0xff : 0x00, al())); +} + +void SoftCPU::CDQ(const X86::Instruction&) +{ + if (eax().value() & 0x80000000) + set_edx(shadow_wrap_with_taint_from<u32>(0xffffffff, eax())); + else + set_edx(shadow_wrap_with_taint_from<u32>(0, eax())); +} + +void SoftCPU::CLC(const X86::Instruction&) +{ + set_cf(false); +} + +void SoftCPU::CLD(const X86::Instruction&) +{ + set_df(false); +} + +void SoftCPU::CLI(const X86::Instruction&) { TODO_INSN(); } +void SoftCPU::CLTS(const X86::Instruction&) { TODO_INSN(); } +void SoftCPU::CMC(const X86::Instruction&) { TODO_INSN(); } + +void SoftCPU::CMOVcc_reg16_RM16(const X86::Instruction& insn) +{ + warn_if_flags_tainted("cmovcc reg16, rm16"); + if (evaluate_condition(insn.cc())) + gpr16(insn.reg16()) = insn.modrm().read16(*this, insn); +} + +void SoftCPU::CMOVcc_reg32_RM32(const X86::Instruction& insn) +{ + warn_if_flags_tainted("cmovcc reg32, rm32"); + if (evaluate_condition(insn.cc())) + gpr32(insn.reg32()) = insn.modrm().read32(*this, insn); +} + +template<typename T> +ALWAYS_INLINE static void do_cmps(SoftCPU& cpu, const X86::Instruction& insn) +{ + auto src_segment = cpu.segment(insn.segment_prefix().value_or(X86::SegmentRegister::DS)); + cpu.do_once_or_repeat<true>(insn, [&] { + auto src = cpu.read_memory<T>({ src_segment, cpu.source_index(insn.a32()).value() }); + auto dest = cpu.read_memory<T>({ cpu.es(), cpu.destination_index(insn.a32()).value() }); + op_sub(cpu, dest, src); + cpu.step_source_index(insn.a32(), sizeof(T)); + cpu.step_destination_index(insn.a32(), sizeof(T)); + }); +} + +void SoftCPU::CMPSB(const X86::Instruction& insn) +{ + do_cmps<u8>(*this, insn); +} + +void SoftCPU::CMPSD(const X86::Instruction& insn) +{ + do_cmps<u32>(*this, insn); +} + +void SoftCPU::CMPSW(const X86::Instruction& insn) +{ + do_cmps<u16>(*this, insn); +} + +void SoftCPU::CMPXCHG_RM16_reg16(const X86::Instruction& insn) +{ + auto current = insn.modrm().read16(*this, insn); + taint_flags_from(current, ax()); + if (current.value() == ax().value()) { + set_zf(true); + insn.modrm().write16(*this, insn, const_gpr16(insn.reg16())); + } else { + set_zf(false); + set_ax(current); + } +} + +void SoftCPU::CMPXCHG_RM32_reg32(const X86::Instruction& insn) +{ + auto current = insn.modrm().read32(*this, insn); + taint_flags_from(current, eax()); + if (current.value() == eax().value()) { + set_zf(true); + insn.modrm().write32(*this, insn, const_gpr32(insn.reg32())); + } else { + set_zf(false); + set_eax(current); + } +} + +void SoftCPU::CMPXCHG_RM8_reg8(const X86::Instruction& insn) +{ + auto current = insn.modrm().read8(*this, insn); + taint_flags_from(current, al()); + if (current.value() == al().value()) { + set_zf(true); + insn.modrm().write8(*this, insn, const_gpr8(insn.reg8())); + } else { + set_zf(false); + set_al(current); + } +} + +void SoftCPU::CPUID(const X86::Instruction&) +{ + if (eax().value() == 0) { + set_eax(shadow_wrap_as_initialized<u32>(1)); + set_ebx(shadow_wrap_as_initialized<u32>(0x6c6c6548)); + set_edx(shadow_wrap_as_initialized<u32>(0x6972466f)); + set_ecx(shadow_wrap_as_initialized<u32>(0x73646e65)); + return; + } + + if (eax().value() == 1) { + u32 stepping = 0; + u32 model = 1; + u32 family = 3; + u32 type = 0; + set_eax(shadow_wrap_as_initialized<u32>(stepping | (model << 4) | (family << 8) | (type << 12))); + set_ebx(shadow_wrap_as_initialized<u32>(0)); + set_edx(shadow_wrap_as_initialized<u32>((1 << 15))); // Features (CMOV) + set_ecx(shadow_wrap_as_initialized<u32>(0)); + return; + } + + dbgln("Unhandled CPUID with eax={:08x}", eax().value()); +} + +void SoftCPU::CWD(const X86::Instruction&) +{ + set_dx(shadow_wrap_with_taint_from<u16>((ax().value() & 0x8000) ? 0xffff : 0x0000, ax())); +} + +void SoftCPU::CWDE(const X86::Instruction&) +{ + set_eax(shadow_wrap_with_taint_from(sign_extended_to<u32>(ax().value()), ax())); +} + +void SoftCPU::DAA(const X86::Instruction&) { TODO_INSN(); } +void SoftCPU::DAS(const X86::Instruction&) { TODO_INSN(); } + +void SoftCPU::DEC_RM16(const X86::Instruction& insn) +{ + insn.modrm().write16(*this, insn, op_dec(*this, insn.modrm().read16(*this, insn))); +} + +void SoftCPU::DEC_RM32(const X86::Instruction& insn) +{ + insn.modrm().write32(*this, insn, op_dec(*this, insn.modrm().read32(*this, insn))); +} + +void SoftCPU::DEC_RM8(const X86::Instruction& insn) +{ + insn.modrm().write8(*this, insn, op_dec(*this, insn.modrm().read8(*this, insn))); +} + +void SoftCPU::DEC_reg16(const X86::Instruction& insn) +{ + gpr16(insn.reg16()) = op_dec(*this, const_gpr16(insn.reg16())); +} + +void SoftCPU::DEC_reg32(const X86::Instruction& insn) +{ + gpr32(insn.reg32()) = op_dec(*this, const_gpr32(insn.reg32())); +} + +void SoftCPU::DIV_RM16(const X86::Instruction& insn) +{ + auto divisor = insn.modrm().read16(*this, insn); + if (divisor.value() == 0) { + reportln("Divide by zero"); + TODO(); + } + u32 dividend = ((u32)dx().value() << 16) | ax().value(); + auto quotient = dividend / divisor.value(); + if (quotient > NumericLimits<u16>::max()) { + reportln("Divide overflow"); + TODO(); + } + + auto remainder = dividend % divisor.value(); + auto original_ax = ax(); + + set_ax(shadow_wrap_with_taint_from<u16>(quotient, original_ax, dx())); + set_dx(shadow_wrap_with_taint_from<u16>(remainder, original_ax, dx())); +} + +void SoftCPU::DIV_RM32(const X86::Instruction& insn) +{ + auto divisor = insn.modrm().read32(*this, insn); + if (divisor.value() == 0) { + reportln("Divide by zero"); + TODO(); + } + u64 dividend = ((u64)edx().value() << 32) | eax().value(); + auto quotient = dividend / divisor.value(); + if (quotient > NumericLimits<u32>::max()) { + reportln("Divide overflow"); + TODO(); + } + + auto remainder = dividend % divisor.value(); + auto original_eax = eax(); + + set_eax(shadow_wrap_with_taint_from<u32>(quotient, original_eax, edx(), divisor)); + set_edx(shadow_wrap_with_taint_from<u32>(remainder, original_eax, edx(), divisor)); +} + +void SoftCPU::DIV_RM8(const X86::Instruction& insn) +{ + auto divisor = insn.modrm().read8(*this, insn); + if (divisor.value() == 0) { + reportln("Divide by zero"); + TODO(); + } + u16 dividend = ax().value(); + auto quotient = dividend / divisor.value(); + if (quotient > NumericLimits<u8>::max()) { + reportln("Divide overflow"); + TODO(); + } + + auto remainder = dividend % divisor.value(); + auto original_ax = ax(); + set_al(shadow_wrap_with_taint_from<u8>(quotient, original_ax, divisor)); + set_ah(shadow_wrap_with_taint_from<u8>(remainder, original_ax, divisor)); +} + +void SoftCPU::ENTER16(const X86::Instruction&) { TODO_INSN(); } +void SoftCPU::ENTER32(const X86::Instruction&) { TODO_INSN(); } + +void SoftCPU::ESCAPE(const X86::Instruction&) +{ + reportln("FIXME: x87 floating-point support"); + m_emulator.dump_backtrace(); + TODO(); +} + +void SoftCPU::FADD_RM32(const X86::Instruction& insn) +{ + // XXX look at ::INC_foo for how mem/reg stuff is handled, and use that here too to make sure this is only called for mem32 ops + if (insn.modrm().is_register()) { + fpu_set(0, fpu_get(insn.modrm().register_index()) + fpu_get(0)); + } else { + auto new_f32 = insn.modrm().read32(*this, insn); + // FIXME: Respect shadow values + auto f32 = bit_cast<float>(new_f32.value()); + fpu_set(0, fpu_get(0) + f32); + } +} + +void SoftCPU::FMUL_RM32(const X86::Instruction& insn) +{ + // XXX look at ::INC_foo for how mem/reg stuff is handled, and use that here too to make sure this is only called for mem32 ops + if (insn.modrm().is_register()) { + fpu_set(0, fpu_get(0) * fpu_get(insn.modrm().register_index())); + } else { + auto new_f32 = insn.modrm().read32(*this, insn); + // FIXME: Respect shadow values + auto f32 = bit_cast<float>(new_f32.value()); + fpu_set(0, fpu_get(0) * f32); + } +} + +void SoftCPU::FCOM_RM32(const X86::Instruction&) { TODO_INSN(); } +void SoftCPU::FCOMP_RM32(const X86::Instruction&) { TODO_INSN(); } + +void SoftCPU::FSUB_RM32(const X86::Instruction& insn) +{ + if (insn.modrm().is_register()) { + fpu_set(0, fpu_get(0) - fpu_get(insn.modrm().register_index())); + } else { + auto new_f32 = insn.modrm().read32(*this, insn); + // FIXME: Respect shadow values + auto f32 = bit_cast<float>(new_f32.value()); + fpu_set(0, fpu_get(0) - f32); + } +} + +void SoftCPU::FSUBR_RM32(const X86::Instruction& insn) +{ + if (insn.modrm().is_register()) { + fpu_set(0, fpu_get(insn.modrm().register_index()) - fpu_get(0)); + } else { + auto new_f32 = insn.modrm().read32(*this, insn); + // FIXME: Respect shadow values + auto f32 = bit_cast<float>(new_f32.value()); + fpu_set(0, f32 - fpu_get(0)); + } +} + +void SoftCPU::FDIV_RM32(const X86::Instruction& insn) +{ + if (insn.modrm().is_register()) { + fpu_set(0, fpu_get(0) / fpu_get(insn.modrm().register_index())); + } else { + auto new_f32 = insn.modrm().read32(*this, insn); + // FIXME: Respect shadow values + auto f32 = bit_cast<float>(new_f32.value()); + // FIXME: Raise IA on + infinity / +-infinitiy, +-0 / +-0, raise Z on finite / +-0 + fpu_set(0, fpu_get(0) / f32); + } +} + +void SoftCPU::FDIVR_RM32(const X86::Instruction& insn) +{ + if (insn.modrm().is_register()) { + fpu_set(0, fpu_get(insn.modrm().register_index()) / fpu_get(0)); + } else { + auto new_f32 = insn.modrm().read32(*this, insn); + // FIXME: Respect shadow values + auto f32 = bit_cast<float>(new_f32.value()); + // FIXME: Raise IA on + infinity / +-infinitiy, +-0 / +-0, raise Z on finite / +-0 + fpu_set(0, f32 / fpu_get(0)); + } +} + +void SoftCPU::FLD_RM32(const X86::Instruction& insn) +{ + if (insn.modrm().is_register()) { + fpu_push(fpu_get(insn.modrm().register_index())); + } else { + auto new_f32 = insn.modrm().read32(*this, insn); + // FIXME: Respect shadow values + fpu_push(bit_cast<float>(new_f32.value())); + } +} + +void SoftCPU::FXCH(const X86::Instruction& insn) +{ + ASSERT(insn.modrm().is_register()); + auto tmp = fpu_get(0); + fpu_set(0, fpu_get(insn.modrm().register_index())); + fpu_set(insn.modrm().register_index(), tmp); +} + +void SoftCPU::FST_RM32(const X86::Instruction& insn) +{ + ASSERT(!insn.modrm().is_register()); + float f32 = (float)fpu_get(0); + // FIXME: Respect shadow values + insn.modrm().write32(*this, insn, shadow_wrap_as_initialized(bit_cast<u32>(f32))); +} + +void SoftCPU::FNOP(const X86::Instruction&) { TODO_INSN(); } + +void SoftCPU::FSTP_RM32(const X86::Instruction& insn) +{ + FST_RM32(insn); + fpu_pop(); +} + +void SoftCPU::FLDENV(const X86::Instruction&) { TODO_INSN(); } + +void SoftCPU::FCHS(const X86::Instruction&) +{ + fpu_set(0, -fpu_get(0)); +} + +void SoftCPU::FABS(const X86::Instruction&) +{ + fpu_set(0, __builtin_fabs(fpu_get(0))); +} + +void SoftCPU::FTST(const X86::Instruction&) { TODO_INSN(); } +void SoftCPU::FXAM(const X86::Instruction&) { TODO_INSN(); } + +void SoftCPU::FLDCW(const X86::Instruction& insn) +{ + m_fpu_cw = insn.modrm().read16(*this, insn); +} + +void SoftCPU::FLD1(const X86::Instruction&) +{ + fpu_push(1.0); +} + +void SoftCPU::FLDL2T(const X86::Instruction&) { TODO_INSN(); } +void SoftCPU::FLDL2E(const X86::Instruction&) { TODO_INSN(); } +void SoftCPU::FLDPI(const X86::Instruction&) { TODO_INSN(); } +void SoftCPU::FLDLG2(const X86::Instruction&) { TODO_INSN(); } +void SoftCPU::FLDLN2(const X86::Instruction&) { TODO_INSN(); } + +void SoftCPU::FLDZ(const X86::Instruction&) +{ + fpu_push(0.0); +} + +void SoftCPU::FNSTENV(const X86::Instruction&) { TODO_INSN(); } +void SoftCPU::F2XM1(const X86::Instruction&) { TODO_INSN(); } +void SoftCPU::FYL2X(const X86::Instruction&) { TODO_INSN(); } +void SoftCPU::FPTAN(const X86::Instruction&) { TODO_INSN(); } +void SoftCPU::FPATAN(const X86::Instruction&) { TODO_INSN(); } +void SoftCPU::FXTRACT(const X86::Instruction&) { TODO_INSN(); } +void SoftCPU::FPREM1(const X86::Instruction&) { TODO_INSN(); } +void SoftCPU::FDECSTP(const X86::Instruction&) { TODO_INSN(); } +void SoftCPU::FINCSTP(const X86::Instruction&) { TODO_INSN(); } + +void SoftCPU::FNSTCW(const X86::Instruction& insn) +{ + insn.modrm().write16(*this, insn, m_fpu_cw); +} + +void SoftCPU::FPREM(const X86::Instruction&) { TODO_INSN(); } +void SoftCPU::FYL2XP1(const X86::Instruction&) { TODO_INSN(); } + +void SoftCPU::FSQRT(const X86::Instruction&) +{ + fpu_set(0, sqrt(fpu_get(0))); +} + +void SoftCPU::FSINCOS(const X86::Instruction&) { TODO_INSN(); } +void SoftCPU::FRNDINT(const X86::Instruction&) { TODO_INSN(); } +void SoftCPU::FSCALE(const X86::Instruction&) { TODO_INSN(); } + +void SoftCPU::FSIN(const X86::Instruction&) +{ + fpu_set(0, sin(fpu_get(0))); +} + +void SoftCPU::FCOS(const X86::Instruction&) { TODO_INSN(); } + +void SoftCPU::FIADD_RM32(const X86::Instruction& insn) +{ + ASSERT(!insn.modrm().is_register()); + auto m32int = (i32)insn.modrm().read32(*this, insn).value(); + // FIXME: Respect shadow values + fpu_set(0, fpu_get(0) + (long double)m32int); +} + +void SoftCPU::FCMOVB(const X86::Instruction&) { TODO_INSN(); } + +void SoftCPU::FIMUL_RM32(const X86::Instruction& insn) +{ + ASSERT(!insn.modrm().is_register()); + auto m32int = (i32)insn.modrm().read32(*this, insn).value(); + // FIXME: Respect shadow values + fpu_set(0, fpu_get(0) * (long double)m32int); +} + +void SoftCPU::FCMOVE(const X86::Instruction&) { TODO_INSN(); } +void SoftCPU::FICOM_RM32(const X86::Instruction&) { TODO_INSN(); } + +void SoftCPU::FCMOVBE(const X86::Instruction& insn) +{ + if (evaluate_condition(6)) + fpu_set(0, fpu_get(insn.rm() & 7)); +} + +void SoftCPU::FICOMP_RM32(const X86::Instruction&) { TODO_INSN(); } +void SoftCPU::FCMOVU(const X86::Instruction&) { TODO_INSN(); } + +void SoftCPU::FISUB_RM32(const X86::Instruction& insn) +{ + ASSERT(!insn.modrm().is_register()); + auto m32int = (i32)insn.modrm().read32(*this, insn).value(); + // FIXME: Respect shadow values + fpu_set(0, fpu_get(0) - (long double)m32int); +} + +void SoftCPU::FISUBR_RM32(const X86::Instruction& insn) +{ + ASSERT(!insn.modrm().is_register()); + auto m32int = (i32)insn.modrm().read32(*this, insn).value(); + // FIXME: Respect shadow values + fpu_set(0, (long double)m32int - fpu_get(0)); +} + +void SoftCPU::FUCOMPP(const X86::Instruction&) { TODO_INSN(); } + +void SoftCPU::FIDIV_RM32(const X86::Instruction& insn) +{ + ASSERT(!insn.modrm().is_register()); + auto m32int = (i32)insn.modrm().read32(*this, insn).value(); + // FIXME: Respect shadow values + // FIXME: Raise IA on 0 / _=0, raise Z on finite / +-0 + fpu_set(0, fpu_get(0) / (long double)m32int); +} + +void SoftCPU::FIDIVR_RM32(const X86::Instruction& insn) +{ + ASSERT(!insn.modrm().is_register()); + auto m32int = (i32)insn.modrm().read32(*this, insn).value(); + // FIXME: Respect shadow values + // FIXME: Raise IA on 0 / _=0, raise Z on finite / +-0 + fpu_set(0, (long double)m32int / fpu_get(0)); +} + +void SoftCPU::FILD_RM32(const X86::Instruction& insn) +{ + ASSERT(!insn.modrm().is_register()); + auto m32int = (i32)insn.modrm().read32(*this, insn).value(); + // FIXME: Respect shadow values + fpu_push((long double)m32int); +} + +void SoftCPU::FCMOVNB(const X86::Instruction&) { TODO_INSN(); } +void SoftCPU::FISTTP_RM32(const X86::Instruction&) { TODO_INSN(); } +void SoftCPU::FCMOVNE(const X86::Instruction&) { TODO_INSN(); } + +void SoftCPU::FIST_RM32(const X86::Instruction& insn) +{ + ASSERT(!insn.modrm().is_register()); + auto f = fpu_get(0); + // FIXME: Respect rounding mode in m_fpu_cw. + auto i32 = static_cast<int32_t>(f); + // FIXME: Respect shadow values + insn.modrm().write32(*this, insn, shadow_wrap_as_initialized(bit_cast<u32>(i32))); +} + +void SoftCPU::FCMOVNBE(const X86::Instruction& insn) +{ + if (evaluate_condition(7)) + fpu_set(0, fpu_get(insn.rm() & 7)); +} + +void SoftCPU::FISTP_RM32(const X86::Instruction& insn) +{ + FIST_RM32(insn); + fpu_pop(); +} + +void SoftCPU::FCMOVNU(const X86::Instruction&) { TODO_INSN(); } +void SoftCPU::FNENI(const X86::Instruction&) { TODO_INSN(); } +void SoftCPU::FNDISI(const X86::Instruction&) { TODO_INSN(); } +void SoftCPU::FNCLEX(const X86::Instruction&) { TODO_INSN(); } +void SoftCPU::FNINIT(const X86::Instruction&) { TODO_INSN(); } +void SoftCPU::FNSETPM(const X86::Instruction&) { TODO_INSN(); } +void SoftCPU::FLD_RM80(const X86::Instruction&) { TODO_INSN(); } + +void SoftCPU::FUCOMI(const X86::Instruction& insn) +{ + auto i = insn.rm() & 7; + // FIXME: Unordered comparison checks. + // FIXME: QNaN / exception handling. + // FIXME: Set C0, C2, C3 in FPU status word. + if (__builtin_isnan(fpu_get(0)) || __builtin_isnan(fpu_get(i))) { + set_zf(true); + set_pf(true); + set_cf(true); + } else { + set_zf(fpu_get(0) == fpu_get(i)); + set_pf(false); + set_cf(fpu_get(0) < fpu_get(i)); + set_of(false); + } + + // FIXME: Taint should be based on ST(0) and ST(i) + m_flags_tainted = false; +} + +void SoftCPU::FCOMI(const X86::Instruction& insn) +{ + auto i = insn.rm() & 7; + // FIXME: QNaN / exception handling. + // FIXME: Set C0, C2, C3 in FPU status word. + set_zf(fpu_get(0) == fpu_get(i)); + set_pf(false); + set_cf(fpu_get(0) < fpu_get(i)); + set_of(false); + + // FIXME: Taint should be based on ST(0) and ST(i) + m_flags_tainted = false; +} + +void SoftCPU::FSTP_RM80(const X86::Instruction&) { TODO_INSN(); } + +void SoftCPU::FADD_RM64(const X86::Instruction& insn) +{ + // XXX look at ::INC_foo for how mem/reg stuff is handled, and use that here too to make sure this is only called for mem64 ops + if (insn.modrm().is_register()) { + fpu_set(insn.modrm().register_index(), fpu_get(insn.modrm().register_index()) + fpu_get(0)); + } else { + auto new_f64 = insn.modrm().read64(*this, insn); + // FIXME: Respect shadow values + auto f64 = bit_cast<double>(new_f64.value()); + fpu_set(0, fpu_get(0) + f64); + } +} + +void SoftCPU::FMUL_RM64(const X86::Instruction& insn) +{ + // XXX look at ::INC_foo for how mem/reg stuff is handled, and use that here too to make sure this is only called for mem64 ops + if (insn.modrm().is_register()) { + fpu_set(insn.modrm().register_index(), fpu_get(insn.modrm().register_index()) * fpu_get(0)); + } else { + auto new_f64 = insn.modrm().read64(*this, insn); + // FIXME: Respect shadow values + auto f64 = bit_cast<double>(new_f64.value()); + fpu_set(0, fpu_get(0) * f64); + } +} + +void SoftCPU::FCOM_RM64(const X86::Instruction&) { TODO_INSN(); } +void SoftCPU::FCOMP_RM64(const X86::Instruction&) { TODO_INSN(); } + +void SoftCPU::FSUB_RM64(const X86::Instruction& insn) +{ + if (insn.modrm().is_register()) { + fpu_set(insn.modrm().register_index(), fpu_get(insn.modrm().register_index()) - fpu_get(0)); + } else { + auto new_f64 = insn.modrm().read64(*this, insn); + // FIXME: Respect shadow values + auto f64 = bit_cast<double>(new_f64.value()); + fpu_set(0, fpu_get(0) - f64); + } +} + +void SoftCPU::FSUBR_RM64(const X86::Instruction& insn) +{ + if (insn.modrm().is_register()) { + fpu_set(insn.modrm().register_index(), fpu_get(insn.modrm().register_index()) - fpu_get(0)); + } else { + auto new_f64 = insn.modrm().read64(*this, insn); + // FIXME: Respect shadow values + auto f64 = bit_cast<double>(new_f64.value()); + fpu_set(0, f64 - fpu_get(0)); + } +} + +void SoftCPU::FDIV_RM64(const X86::Instruction& insn) +{ + if (insn.modrm().is_register()) { + fpu_set(insn.modrm().register_index(), fpu_get(insn.modrm().register_index()) / fpu_get(0)); + } else { + auto new_f64 = insn.modrm().read64(*this, insn); + // FIXME: Respect shadow values + auto f64 = bit_cast<double>(new_f64.value()); + // FIXME: Raise IA on + infinity / +-infinitiy, +-0 / +-0, raise Z on finite / +-0 + fpu_set(0, fpu_get(0) / f64); + } +} + +void SoftCPU::FDIVR_RM64(const X86::Instruction& insn) +{ + if (insn.modrm().is_register()) { + // XXX this is FDIVR, Instruction decodes this weirdly + //fpu_set(insn.modrm().register_index(), fpu_get(0) / fpu_get(insn.modrm().register_index())); + fpu_set(insn.modrm().register_index(), fpu_get(insn.modrm().register_index()) / fpu_get(0)); + } else { + auto new_f64 = insn.modrm().read64(*this, insn); + // FIXME: Respect shadow values + auto f64 = bit_cast<double>(new_f64.value()); + // FIXME: Raise IA on + infinity / +-infinitiy, +-0 / +-0, raise Z on finite / +-0 + fpu_set(0, f64 / fpu_get(0)); + } +} + +void SoftCPU::FLD_RM64(const X86::Instruction& insn) +{ + ASSERT(!insn.modrm().is_register()); + auto new_f64 = insn.modrm().read64(*this, insn); + // FIXME: Respect shadow values + fpu_push(bit_cast<double>(new_f64.value())); +} + +void SoftCPU::FFREE(const X86::Instruction&) { TODO_INSN(); } +void SoftCPU::FISTTP_RM64(const X86::Instruction&) { TODO_INSN(); } + +void SoftCPU::FST_RM64(const X86::Instruction& insn) +{ + if (insn.modrm().is_register()) { + fpu_set(insn.modrm().register_index(), fpu_get(0)); + } else { + // FIXME: Respect shadow values + double f64 = (double)fpu_get(0); + insn.modrm().write64(*this, insn, shadow_wrap_as_initialized(bit_cast<u64>(f64))); + } +} + +void SoftCPU::FSTP_RM64(const X86::Instruction& insn) +{ + FST_RM64(insn); + fpu_pop(); +} + +void SoftCPU::FRSTOR(const X86::Instruction&) { TODO_INSN(); } +void SoftCPU::FUCOM(const X86::Instruction&) { TODO_INSN(); } +void SoftCPU::FUCOMP(const X86::Instruction&) { TODO_INSN(); } +void SoftCPU::FNSAVE(const X86::Instruction&) { TODO_INSN(); } +void SoftCPU::FNSTSW(const X86::Instruction&) { TODO_INSN(); } + +void SoftCPU::FIADD_RM16(const X86::Instruction& insn) +{ + ASSERT(!insn.modrm().is_register()); + auto m16int = (i16)insn.modrm().read16(*this, insn).value(); + // FIXME: Respect shadow values + fpu_set(0, fpu_get(0) + (long double)m16int); +} + +void SoftCPU::FADDP(const X86::Instruction& insn) +{ + ASSERT(insn.modrm().is_register()); + fpu_set(insn.modrm().register_index(), fpu_get(insn.modrm().register_index()) + fpu_get(0)); + fpu_pop(); +} + +void SoftCPU::FIMUL_RM16(const X86::Instruction& insn) +{ + ASSERT(!insn.modrm().is_register()); + auto m16int = (i16)insn.modrm().read16(*this, insn).value(); + // FIXME: Respect shadow values + fpu_set(0, fpu_get(0) * (long double)m16int); +} + +void SoftCPU::FMULP(const X86::Instruction& insn) +{ + ASSERT(insn.modrm().is_register()); + fpu_set(insn.modrm().register_index(), fpu_get(insn.modrm().register_index()) * fpu_get(0)); + fpu_pop(); +} + +void SoftCPU::FICOM_RM16(const X86::Instruction&) { TODO_INSN(); } +void SoftCPU::FICOMP_RM16(const X86::Instruction&) { TODO_INSN(); } +void SoftCPU::FCOMPP(const X86::Instruction&) { TODO_INSN(); } + +void SoftCPU::FISUB_RM16(const X86::Instruction& insn) +{ + ASSERT(!insn.modrm().is_register()); + auto m16int = (i16)insn.modrm().read16(*this, insn).value(); + // FIXME: Respect shadow values + fpu_set(0, fpu_get(0) - (long double)m16int); +} + +void SoftCPU::FSUBRP(const X86::Instruction& insn) +{ + ASSERT(insn.modrm().is_register()); + fpu_set(insn.modrm().register_index(), fpu_get(0) - fpu_get(insn.modrm().register_index())); + fpu_pop(); +} + +void SoftCPU::FISUBR_RM16(const X86::Instruction& insn) +{ + ASSERT(!insn.modrm().is_register()); + auto m16int = (i16)insn.modrm().read16(*this, insn).value(); + // FIXME: Respect shadow values + fpu_set(0, (long double)m16int - fpu_get(0)); +} + +void SoftCPU::FSUBP(const X86::Instruction& insn) +{ + ASSERT(insn.modrm().is_register()); + fpu_set(insn.modrm().register_index(), fpu_get(insn.modrm().register_index()) - fpu_get(0)); + fpu_pop(); +} + +void SoftCPU::FIDIV_RM16(const X86::Instruction& insn) +{ + ASSERT(!insn.modrm().is_register()); + auto m16int = (i16)insn.modrm().read16(*this, insn).value(); + // FIXME: Respect shadow values + // FIXME: Raise IA on 0 / _=0, raise Z on finite / +-0 + fpu_set(0, fpu_get(0) / (long double)m16int); +} + +void SoftCPU::FDIVRP(const X86::Instruction& insn) +{ + ASSERT(insn.modrm().is_register()); + // FIXME: Raise IA on + infinity / +-infinitiy, +-0 / +-0, raise Z on finite / +-0 + fpu_set(insn.modrm().register_index(), fpu_get(0) / fpu_get(insn.modrm().register_index())); + fpu_pop(); +} + +void SoftCPU::FIDIVR_RM16(const X86::Instruction& insn) +{ + ASSERT(!insn.modrm().is_register()); + auto m16int = (i16)insn.modrm().read16(*this, insn).value(); + // FIXME: Respect shadow values + // FIXME: Raise IA on 0 / _=0, raise Z on finite / +-0 + fpu_set(0, (long double)m16int / fpu_get(0)); +} + +void SoftCPU::FDIVP(const X86::Instruction& insn) +{ + ASSERT(insn.modrm().is_register()); + // FIXME: Raise IA on + infinity / +-infinitiy, +-0 / +-0, raise Z on finite / +-0 + fpu_set(insn.modrm().register_index(), fpu_get(insn.modrm().register_index()) / fpu_get(0)); + fpu_pop(); +} + +void SoftCPU::FILD_RM16(const X86::Instruction& insn) +{ + ASSERT(!insn.modrm().is_register()); + auto m16int = (i16)insn.modrm().read16(*this, insn).value(); + // FIXME: Respect shadow values + fpu_push((long double)m16int); +} + +void SoftCPU::FFREEP(const X86::Instruction&) { TODO_INSN(); } +void SoftCPU::FISTTP_RM16(const X86::Instruction&) { TODO_INSN(); } + +void SoftCPU::FIST_RM16(const X86::Instruction& insn) +{ + ASSERT(!insn.modrm().is_register()); + auto f = fpu_get(0); + // FIXME: Respect rounding mode in m_fpu_cw. + auto i16 = static_cast<int16_t>(f); + // FIXME: Respect shadow values + insn.modrm().write16(*this, insn, shadow_wrap_as_initialized(bit_cast<u16>(i16))); +} + +void SoftCPU::FISTP_RM16(const X86::Instruction& insn) +{ + FIST_RM16(insn); + fpu_pop(); +} + +void SoftCPU::FBLD_M80(const X86::Instruction&) { TODO_INSN(); } +void SoftCPU::FNSTSW_AX(const X86::Instruction&) { TODO_INSN(); } + +void SoftCPU::FILD_RM64(const X86::Instruction& insn) +{ + ASSERT(!insn.modrm().is_register()); + auto m64int = (i64)insn.modrm().read64(*this, insn).value(); + // FIXME: Respect shadow values + fpu_push((long double)m64int); +} + +void SoftCPU::FUCOMIP(const X86::Instruction& insn) +{ + FUCOMI(insn); + fpu_pop(); +} + +void SoftCPU::FBSTP_M80(const X86::Instruction&) { TODO_INSN(); } + +void SoftCPU::FCOMIP(const X86::Instruction& insn) +{ + FCOMI(insn); + fpu_pop(); +} + +void SoftCPU::FISTP_RM64(const X86::Instruction& insn) +{ + ASSERT(!insn.modrm().is_register()); + auto f = fpu_pop(); + // FIXME: Respect rounding mode in m_fpu_cw. + auto i64 = static_cast<int64_t>(f); + // FIXME: Respect shadow values + insn.modrm().write64(*this, insn, shadow_wrap_as_initialized(bit_cast<u64>(i64))); +} +void SoftCPU::HLT(const X86::Instruction&) { TODO_INSN(); } + +void SoftCPU::IDIV_RM16(const X86::Instruction& insn) +{ + auto divisor_with_shadow = insn.modrm().read16(*this, insn); + auto divisor = (i16)divisor_with_shadow.value(); + if (divisor == 0) { + reportln("Divide by zero"); + TODO(); + } + i32 dividend = (i32)(((u32)dx().value() << 16) | (u32)ax().value()); + i32 result = dividend / divisor; + if (result > NumericLimits<i16>::max() || result < NumericLimits<i16>::min()) { + reportln("Divide overflow"); + TODO(); + } + + auto original_ax = ax(); + set_ax(shadow_wrap_with_taint_from<u16>(result, original_ax, dx(), divisor_with_shadow)); + set_dx(shadow_wrap_with_taint_from<u16>(dividend % divisor, original_ax, dx(), divisor_with_shadow)); +} + +void SoftCPU::IDIV_RM32(const X86::Instruction& insn) +{ + auto divisor_with_shadow = insn.modrm().read32(*this, insn); + auto divisor = (i32)divisor_with_shadow.value(); + if (divisor == 0) { + reportln("Divide by zero"); + TODO(); + } + i64 dividend = (i64)(((u64)edx().value() << 32) | (u64)eax().value()); + i64 result = dividend / divisor; + if (result > NumericLimits<i32>::max() || result < NumericLimits<i32>::min()) { + reportln("Divide overflow"); + TODO(); + } + + auto original_eax = eax(); + set_eax(shadow_wrap_with_taint_from<u32>(result, original_eax, edx(), divisor_with_shadow)); + set_edx(shadow_wrap_with_taint_from<u32>(dividend % divisor, original_eax, edx(), divisor_with_shadow)); +} + +void SoftCPU::IDIV_RM8(const X86::Instruction& insn) +{ + auto divisor_with_shadow = insn.modrm().read8(*this, insn); + auto divisor = (i8)divisor_with_shadow.value(); + if (divisor == 0) { + reportln("Divide by zero"); + TODO(); + } + i16 dividend = ax().value(); + i16 result = dividend / divisor; + if (result > NumericLimits<i8>::max() || result < NumericLimits<i8>::min()) { + reportln("Divide overflow"); + TODO(); + } + + auto original_ax = ax(); + set_al(shadow_wrap_with_taint_from<u8>(result, divisor_with_shadow, original_ax)); + set_ah(shadow_wrap_with_taint_from<u8>(dividend % divisor, divisor_with_shadow, original_ax)); +} + +void SoftCPU::IMUL_RM16(const X86::Instruction& insn) +{ + i16 result_high; + i16 result_low; + auto src = insn.modrm().read16(*this, insn); + op_imul<i16>(*this, src.value(), ax().value(), result_high, result_low); + gpr16(X86::RegisterDX) = shadow_wrap_with_taint_from<u16>(result_high, src, ax()); + gpr16(X86::RegisterAX) = shadow_wrap_with_taint_from<u16>(result_low, src, ax()); +} + +void SoftCPU::IMUL_RM32(const X86::Instruction& insn) +{ + i32 result_high; + i32 result_low; + auto src = insn.modrm().read32(*this, insn); + op_imul<i32>(*this, src.value(), eax().value(), result_high, result_low); + gpr32(X86::RegisterEDX) = shadow_wrap_with_taint_from<u32>(result_high, src, eax()); + gpr32(X86::RegisterEAX) = shadow_wrap_with_taint_from<u32>(result_low, src, eax()); +} + +void SoftCPU::IMUL_RM8(const X86::Instruction& insn) +{ + i8 result_high; + i8 result_low; + auto src = insn.modrm().read8(*this, insn); + op_imul<i8>(*this, src.value(), al().value(), result_high, result_low); + gpr8(X86::RegisterAH) = shadow_wrap_with_taint_from<u8>(result_high, src, al()); + gpr8(X86::RegisterAL) = shadow_wrap_with_taint_from<u8>(result_low, src, al()); +} + +void SoftCPU::IMUL_reg16_RM16(const X86::Instruction& insn) +{ + i16 result_high; + i16 result_low; + auto src = insn.modrm().read16(*this, insn); + op_imul<i16>(*this, gpr16(insn.reg16()).value(), src.value(), result_high, result_low); + gpr16(insn.reg16()) = shadow_wrap_with_taint_from<u16>(result_low, src, gpr16(insn.reg16())); +} + +void SoftCPU::IMUL_reg16_RM16_imm16(const X86::Instruction& insn) +{ + i16 result_high; + i16 result_low; + auto src = insn.modrm().read16(*this, insn); + op_imul<i16>(*this, src.value(), insn.imm16(), result_high, result_low); + gpr16(insn.reg16()) = shadow_wrap_with_taint_from<u16>(result_low, src); +} + +void SoftCPU::IMUL_reg16_RM16_imm8(const X86::Instruction& insn) +{ + i16 result_high; + i16 result_low; + auto src = insn.modrm().read16(*this, insn); + op_imul<i16>(*this, src.value(), sign_extended_to<i16>(insn.imm8()), result_high, result_low); + gpr16(insn.reg16()) = shadow_wrap_with_taint_from<u16>(result_low, src); +} + +void SoftCPU::IMUL_reg32_RM32(const X86::Instruction& insn) +{ + i32 result_high; + i32 result_low; + auto src = insn.modrm().read32(*this, insn); + op_imul<i32>(*this, gpr32(insn.reg32()).value(), src.value(), result_high, result_low); + gpr32(insn.reg32()) = shadow_wrap_with_taint_from<u32>(result_low, src, gpr32(insn.reg32())); +} + +void SoftCPU::IMUL_reg32_RM32_imm32(const X86::Instruction& insn) +{ + i32 result_high; + i32 result_low; + auto src = insn.modrm().read32(*this, insn); + op_imul<i32>(*this, src.value(), insn.imm32(), result_high, result_low); + gpr32(insn.reg32()) = shadow_wrap_with_taint_from<u32>(result_low, src); +} + +void SoftCPU::IMUL_reg32_RM32_imm8(const X86::Instruction& insn) +{ + i32 result_high; + i32 result_low; + auto src = insn.modrm().read32(*this, insn); + op_imul<i32>(*this, src.value(), sign_extended_to<i32>(insn.imm8()), result_high, result_low); + gpr32(insn.reg32()) = shadow_wrap_with_taint_from<u32>(result_low, src); +} + +void SoftCPU::INC_RM16(const X86::Instruction& insn) +{ + insn.modrm().write16(*this, insn, op_inc(*this, insn.modrm().read16(*this, insn))); +} + +void SoftCPU::INC_RM32(const X86::Instruction& insn) +{ + insn.modrm().write32(*this, insn, op_inc(*this, insn.modrm().read32(*this, insn))); +} + +void SoftCPU::INC_RM8(const X86::Instruction& insn) +{ + insn.modrm().write8(*this, insn, op_inc(*this, insn.modrm().read8(*this, insn))); +} + +void SoftCPU::INC_reg16(const X86::Instruction& insn) +{ + gpr16(insn.reg16()) = op_inc(*this, const_gpr16(insn.reg16())); +} + +void SoftCPU::INC_reg32(const X86::Instruction& insn) +{ + gpr32(insn.reg32()) = op_inc(*this, const_gpr32(insn.reg32())); +} + +void SoftCPU::INSB(const X86::Instruction&) { TODO_INSN(); } +void SoftCPU::INSD(const X86::Instruction&) { TODO_INSN(); } +void SoftCPU::INSW(const X86::Instruction&) { TODO_INSN(); } +void SoftCPU::INT3(const X86::Instruction&) { TODO_INSN(); } +void SoftCPU::INTO(const X86::Instruction&) { TODO_INSN(); } + +void SoftCPU::INT_imm8(const X86::Instruction& insn) +{ + ASSERT(insn.imm8() == 0x82); + // FIXME: virt_syscall should take ValueWithShadow and whine about uninitialized arguments + set_eax(shadow_wrap_as_initialized(m_emulator.virt_syscall(eax().value(), edx().value(), ecx().value(), ebx().value()))); +} + +void SoftCPU::INVLPG(const X86::Instruction&) { TODO_INSN(); } +void SoftCPU::IN_AL_DX(const X86::Instruction&) { TODO_INSN(); } +void SoftCPU::IN_AL_imm8(const X86::Instruction&) { TODO_INSN(); } +void SoftCPU::IN_AX_DX(const X86::Instruction&) { TODO_INSN(); } +void SoftCPU::IN_AX_imm8(const X86::Instruction&) { TODO_INSN(); } +void SoftCPU::IN_EAX_DX(const X86::Instruction&) { TODO_INSN(); } +void SoftCPU::IN_EAX_imm8(const X86::Instruction&) { TODO_INSN(); } +void SoftCPU::IRET(const X86::Instruction&) { TODO_INSN(); } + +void SoftCPU::JCXZ_imm8(const X86::Instruction& insn) +{ + if (insn.a32()) { + warn_if_uninitialized(ecx(), "jecxz imm8"); + if (ecx().value() == 0) + set_eip(eip() + (i8)insn.imm8()); + } else { + warn_if_uninitialized(cx(), "jcxz imm8"); + if (cx().value() == 0) + set_eip(eip() + (i8)insn.imm8()); + } +} + +void SoftCPU::JMP_FAR_mem16(const X86::Instruction&) { TODO_INSN(); } +void SoftCPU::JMP_FAR_mem32(const X86::Instruction&) { TODO_INSN(); } +void SoftCPU::JMP_RM16(const X86::Instruction&) { TODO_INSN(); } + +void SoftCPU::JMP_RM32(const X86::Instruction& insn) +{ + set_eip(insn.modrm().read32(*this, insn).value()); +} + +void SoftCPU::JMP_imm16(const X86::Instruction& insn) +{ + set_eip(eip() + (i16)insn.imm16()); +} + +void SoftCPU::JMP_imm16_imm16(const X86::Instruction&) { TODO_INSN(); } +void SoftCPU::JMP_imm16_imm32(const X86::Instruction&) { TODO_INSN(); } + +void SoftCPU::JMP_imm32(const X86::Instruction& insn) +{ + set_eip(eip() + (i32)insn.imm32()); +} + +void SoftCPU::JMP_short_imm8(const X86::Instruction& insn) +{ + set_eip(eip() + (i8)insn.imm8()); +} + +void SoftCPU::Jcc_NEAR_imm(const X86::Instruction& insn) +{ + warn_if_flags_tainted("jcc near imm32"); + if (evaluate_condition(insn.cc())) + set_eip(eip() + (i32)insn.imm32()); +} + +void SoftCPU::Jcc_imm8(const X86::Instruction& insn) +{ + warn_if_flags_tainted("jcc imm8"); + if (evaluate_condition(insn.cc())) + set_eip(eip() + (i8)insn.imm8()); +} + +void SoftCPU::LAHF(const X86::Instruction&) { TODO_INSN(); } +void SoftCPU::LAR_reg16_RM16(const X86::Instruction&) { TODO_INSN(); } +void SoftCPU::LAR_reg32_RM32(const X86::Instruction&) { TODO_INSN(); } +void SoftCPU::LDS_reg16_mem16(const X86::Instruction&) { TODO_INSN(); } +void SoftCPU::LDS_reg32_mem32(const X86::Instruction&) { TODO_INSN(); } +void SoftCPU::LEAVE16(const X86::Instruction&) { TODO_INSN(); } + +void SoftCPU::LEAVE32(const X86::Instruction&) +{ + auto new_ebp = read_memory32({ ss(), ebp().value() }); + set_esp({ ebp().value() + 4, ebp().shadow() }); + set_ebp(new_ebp); +} + +void SoftCPU::LEA_reg16_mem16(const X86::Instruction& insn) +{ + // FIXME: Respect shadow values + gpr16(insn.reg16()) = shadow_wrap_as_initialized<u16>(insn.modrm().resolve(*this, insn).offset()); +} + +void SoftCPU::LEA_reg32_mem32(const X86::Instruction& insn) +{ + // FIXME: Respect shadow values + gpr32(insn.reg32()) = shadow_wrap_as_initialized<u32>(insn.modrm().resolve(*this, insn).offset()); +} + +void SoftCPU::LES_reg16_mem16(const X86::Instruction&) { TODO_INSN(); } +void SoftCPU::LES_reg32_mem32(const X86::Instruction&) { TODO_INSN(); } +void SoftCPU::LFS_reg16_mem16(const X86::Instruction&) { TODO_INSN(); } +void SoftCPU::LFS_reg32_mem32(const X86::Instruction&) { TODO_INSN(); } +void SoftCPU::LGDT(const X86::Instruction&) { TODO_INSN(); } +void SoftCPU::LGS_reg16_mem16(const X86::Instruction&) { TODO_INSN(); } +void SoftCPU::LGS_reg32_mem32(const X86::Instruction&) { TODO_INSN(); } +void SoftCPU::LIDT(const X86::Instruction&) { TODO_INSN(); } +void SoftCPU::LLDT_RM16(const X86::Instruction&) { TODO_INSN(); } +void SoftCPU::LMSW_RM16(const X86::Instruction&) { TODO_INSN(); } + +template<typename T> +ALWAYS_INLINE static void do_lods(SoftCPU& cpu, const X86::Instruction& insn) +{ + auto src_segment = cpu.segment(insn.segment_prefix().value_or(X86::SegmentRegister::DS)); + cpu.do_once_or_repeat<true>(insn, [&] { + auto src = cpu.read_memory<T>({ src_segment, cpu.source_index(insn.a32()).value() }); + cpu.gpr<T>(X86::RegisterAL) = src; + cpu.step_source_index(insn.a32(), sizeof(T)); + }); +} + +void SoftCPU::LODSB(const X86::Instruction& insn) +{ + do_lods<u8>(*this, insn); +} + +void SoftCPU::LODSD(const X86::Instruction& insn) +{ + do_lods<u32>(*this, insn); +} + +void SoftCPU::LODSW(const X86::Instruction& insn) +{ + do_lods<u16>(*this, insn); +} + +void SoftCPU::LOOPNZ_imm8(const X86::Instruction& insn) +{ + warn_if_flags_tainted("loopnz"); + if (insn.a32()) { + set_ecx({ ecx().value() - 1, ecx().shadow() }); + if (ecx().value() != 0 && !zf()) + set_eip(eip() + (i8)insn.imm8()); + } else { + set_cx({ (u16)(cx().value() - 1), cx().shadow() }); + if (cx().value() != 0 && !zf()) + set_eip(eip() + (i8)insn.imm8()); + } +} +void SoftCPU::LOOPZ_imm8(const X86::Instruction& insn) +{ + warn_if_flags_tainted("loopz"); + if (insn.a32()) { + set_ecx({ ecx().value() - 1, ecx().shadow() }); + if (ecx().value() != 0 && zf()) + set_eip(eip() + (i8)insn.imm8()); + } else { + set_cx({ (u16)(cx().value() - 1), cx().shadow() }); + if (cx().value() != 0 && zf()) + set_eip(eip() + (i8)insn.imm8()); + } +} + +void SoftCPU::LOOP_imm8(const X86::Instruction& insn) +{ + if (insn.a32()) { + set_ecx({ ecx().value() - 1, ecx().shadow() }); + if (ecx().value() != 0) + set_eip(eip() + (i8)insn.imm8()); + } else { + set_cx({ (u16)(cx().value() - 1), cx().shadow() }); + if (cx().value() != 0) + set_eip(eip() + (i8)insn.imm8()); + } +} + +void SoftCPU::LSL_reg16_RM16(const X86::Instruction&) { TODO_INSN(); } +void SoftCPU::LSL_reg32_RM32(const X86::Instruction&) { TODO_INSN(); } +void SoftCPU::LSS_reg16_mem16(const X86::Instruction&) { TODO_INSN(); } +void SoftCPU::LSS_reg32_mem32(const X86::Instruction&) { TODO_INSN(); } +void SoftCPU::LTR_RM16(const X86::Instruction&) { TODO_INSN(); } + +template<typename T> +ALWAYS_INLINE static void do_movs(SoftCPU& cpu, const X86::Instruction& insn) +{ + auto src_segment = cpu.segment(insn.segment_prefix().value_or(X86::SegmentRegister::DS)); + cpu.do_once_or_repeat<false>(insn, [&] { + auto src = cpu.read_memory<T>({ src_segment, cpu.source_index(insn.a32()).value() }); + cpu.write_memory<T>({ cpu.es(), cpu.destination_index(insn.a32()).value() }, src); + cpu.step_source_index(insn.a32(), sizeof(T)); + cpu.step_destination_index(insn.a32(), sizeof(T)); + }); +} + +void SoftCPU::MOVSB(const X86::Instruction& insn) +{ + do_movs<u8>(*this, insn); +} + +void SoftCPU::MOVSD(const X86::Instruction& insn) +{ + do_movs<u32>(*this, insn); +} + +void SoftCPU::MOVSW(const X86::Instruction& insn) +{ + do_movs<u16>(*this, insn); +} + +void SoftCPU::MOVSX_reg16_RM8(const X86::Instruction& insn) +{ + auto src = insn.modrm().read8(*this, insn); + gpr16(insn.reg16()) = shadow_wrap_with_taint_from<u16>(sign_extended_to<u16>(src.value()), src); +} + +void SoftCPU::MOVSX_reg32_RM16(const X86::Instruction& insn) +{ + auto src = insn.modrm().read16(*this, insn); + gpr32(insn.reg32()) = shadow_wrap_with_taint_from<u32>(sign_extended_to<u32>(src.value()), src); +} + +void SoftCPU::MOVSX_reg32_RM8(const X86::Instruction& insn) +{ + auto src = insn.modrm().read8(*this, insn); + gpr32(insn.reg32()) = shadow_wrap_with_taint_from<u32>(sign_extended_to<u32>(src.value()), src); +} + +void SoftCPU::MOVZX_reg16_RM8(const X86::Instruction& insn) +{ + auto src = insn.modrm().read8(*this, insn); + gpr16(insn.reg16()) = ValueWithShadow<u16>(src.value(), 0x0100 | (src.shadow() & 0xff)); +} + +void SoftCPU::MOVZX_reg32_RM16(const X86::Instruction& insn) +{ + auto src = insn.modrm().read16(*this, insn); + gpr32(insn.reg32()) = ValueWithShadow<u32>(src.value(), 0x01010000 | (src.shadow() & 0xffff)); +} + +void SoftCPU::MOVZX_reg32_RM8(const X86::Instruction& insn) +{ + auto src = insn.modrm().read8(*this, insn); + gpr32(insn.reg32()) = ValueWithShadow<u32>(src.value(), 0x01010100 | (src.shadow() & 0xff)); +} + +void SoftCPU::MOV_AL_moff8(const X86::Instruction& insn) +{ + set_al(read_memory8({ segment(insn.segment_prefix().value_or(X86::SegmentRegister::DS)), insn.imm_address() })); +} + +void SoftCPU::MOV_AX_moff16(const X86::Instruction& insn) +{ + set_ax(read_memory16({ segment(insn.segment_prefix().value_or(X86::SegmentRegister::DS)), insn.imm_address() })); +} + +void SoftCPU::MOV_CR_reg32(const X86::Instruction&) { TODO_INSN(); } +void SoftCPU::MOV_DR_reg32(const X86::Instruction&) { TODO_INSN(); } + +void SoftCPU::MOV_EAX_moff32(const X86::Instruction& insn) +{ + set_eax(read_memory32({ segment(insn.segment_prefix().value_or(X86::SegmentRegister::DS)), insn.imm_address() })); +} + +void SoftCPU::MOV_RM16_imm16(const X86::Instruction& insn) +{ + insn.modrm().write16(*this, insn, shadow_wrap_as_initialized(insn.imm16())); +} + +void SoftCPU::MOV_RM16_reg16(const X86::Instruction& insn) +{ + insn.modrm().write16(*this, insn, const_gpr16(insn.reg16())); +} + +void SoftCPU::MOV_RM16_seg(const X86::Instruction&) { TODO_INSN(); } + +void SoftCPU::MOV_RM32_imm32(const X86::Instruction& insn) +{ + insn.modrm().write32(*this, insn, shadow_wrap_as_initialized(insn.imm32())); +} + +void SoftCPU::MOV_RM32_reg32(const X86::Instruction& insn) +{ + insn.modrm().write32(*this, insn, const_gpr32(insn.reg32())); +} + +void SoftCPU::MOV_RM8_imm8(const X86::Instruction& insn) +{ + insn.modrm().write8(*this, insn, shadow_wrap_as_initialized(insn.imm8())); +} + +void SoftCPU::MOV_RM8_reg8(const X86::Instruction& insn) +{ + insn.modrm().write8(*this, insn, const_gpr8(insn.reg8())); +} + +void SoftCPU::MOV_moff16_AX(const X86::Instruction& insn) +{ + write_memory16({ segment(insn.segment_prefix().value_or(X86::SegmentRegister::DS)), insn.imm_address() }, ax()); +} + +void SoftCPU::MOV_moff32_EAX(const X86::Instruction& insn) +{ + write_memory32({ segment(insn.segment_prefix().value_or(X86::SegmentRegister::DS)), insn.imm_address() }, eax()); +} + +void SoftCPU::MOV_moff8_AL(const X86::Instruction& insn) +{ + write_memory8({ segment(insn.segment_prefix().value_or(X86::SegmentRegister::DS)), insn.imm_address() }, al()); +} + +void SoftCPU::MOV_reg16_RM16(const X86::Instruction& insn) +{ + gpr16(insn.reg16()) = insn.modrm().read16(*this, insn); +} + +void SoftCPU::MOV_reg16_imm16(const X86::Instruction& insn) +{ + gpr16(insn.reg16()) = shadow_wrap_as_initialized(insn.imm16()); +} + +void SoftCPU::MOV_reg32_CR(const X86::Instruction&) { TODO_INSN(); } +void SoftCPU::MOV_reg32_DR(const X86::Instruction&) { TODO_INSN(); } + +void SoftCPU::MOV_reg32_RM32(const X86::Instruction& insn) +{ + gpr32(insn.reg32()) = insn.modrm().read32(*this, insn); +} + +void SoftCPU::MOV_reg32_imm32(const X86::Instruction& insn) +{ + gpr32(insn.reg32()) = shadow_wrap_as_initialized(insn.imm32()); +} + +void SoftCPU::MOV_reg8_RM8(const X86::Instruction& insn) +{ + gpr8(insn.reg8()) = insn.modrm().read8(*this, insn); +} + +void SoftCPU::MOV_reg8_imm8(const X86::Instruction& insn) +{ + gpr8(insn.reg8()) = shadow_wrap_as_initialized(insn.imm8()); +} + +void SoftCPU::MOV_seg_RM16(const X86::Instruction&) { TODO_INSN(); } +void SoftCPU::MOV_seg_RM32(const X86::Instruction&) { TODO_INSN(); } + +void SoftCPU::MUL_RM16(const X86::Instruction& insn) +{ + auto src = insn.modrm().read16(*this, insn); + u32 result = (u32)ax().value() * (u32)src.value(); + auto original_ax = ax(); + set_ax(shadow_wrap_with_taint_from<u16>(result & 0xffff, src, original_ax)); + set_dx(shadow_wrap_with_taint_from<u16>(result >> 16, src, original_ax)); + taint_flags_from(src, original_ax); + + set_cf(dx().value() != 0); + set_of(dx().value() != 0); +} + +void SoftCPU::MUL_RM32(const X86::Instruction& insn) +{ + auto src = insn.modrm().read32(*this, insn); + u64 result = (u64)eax().value() * (u64)src.value(); + auto original_eax = eax(); + set_eax(shadow_wrap_with_taint_from<u32>(result, src, original_eax)); + set_edx(shadow_wrap_with_taint_from<u32>(result >> 32, src, original_eax)); + taint_flags_from(src, original_eax); + + set_cf(edx().value() != 0); + set_of(edx().value() != 0); +} + +void SoftCPU::MUL_RM8(const X86::Instruction& insn) +{ + auto src = insn.modrm().read8(*this, insn); + u16 result = (u16)al().value() * src.value(); + auto original_al = al(); + set_ax(shadow_wrap_with_taint_from(result, src, original_al)); + taint_flags_from(src, original_al); + + set_cf((result & 0xff00) != 0); + set_of((result & 0xff00) != 0); +} + +void SoftCPU::NEG_RM16(const X86::Instruction& insn) +{ + insn.modrm().write16(*this, insn, op_sub<ValueWithShadow<u16>>(*this, shadow_wrap_as_initialized<u16>(0), insn.modrm().read16(*this, insn))); +} + +void SoftCPU::NEG_RM32(const X86::Instruction& insn) +{ + insn.modrm().write32(*this, insn, op_sub<ValueWithShadow<u32>>(*this, shadow_wrap_as_initialized<u32>(0), insn.modrm().read32(*this, insn))); +} + +void SoftCPU::NEG_RM8(const X86::Instruction& insn) +{ + insn.modrm().write8(*this, insn, op_sub<ValueWithShadow<u8>>(*this, shadow_wrap_as_initialized<u8>(0), insn.modrm().read8(*this, insn))); +} + +void SoftCPU::NOP(const X86::Instruction&) +{ +} + +void SoftCPU::NOT_RM16(const X86::Instruction& insn) +{ + auto data = insn.modrm().read16(*this, insn); + insn.modrm().write16(*this, insn, ValueWithShadow<u16>(~data.value(), data.shadow())); +} + +void SoftCPU::NOT_RM32(const X86::Instruction& insn) +{ + auto data = insn.modrm().read32(*this, insn); + insn.modrm().write32(*this, insn, ValueWithShadow<u32>(~data.value(), data.shadow())); +} + +void SoftCPU::NOT_RM8(const X86::Instruction& insn) +{ + auto data = insn.modrm().read8(*this, insn); + insn.modrm().write8(*this, insn, ValueWithShadow<u8>(~data.value(), data.shadow())); +} + +void SoftCPU::OUTSB(const X86::Instruction&) { TODO_INSN(); } +void SoftCPU::OUTSD(const X86::Instruction&) { TODO_INSN(); } +void SoftCPU::OUTSW(const X86::Instruction&) { TODO_INSN(); } +void SoftCPU::OUT_DX_AL(const X86::Instruction&) { TODO_INSN(); } +void SoftCPU::OUT_DX_AX(const X86::Instruction&) { TODO_INSN(); } +void SoftCPU::OUT_DX_EAX(const X86::Instruction&) { TODO_INSN(); } +void SoftCPU::OUT_imm8_AL(const X86::Instruction&) { TODO_INSN(); } +void SoftCPU::OUT_imm8_AX(const X86::Instruction&) { TODO_INSN(); } +void SoftCPU::OUT_imm8_EAX(const X86::Instruction&) { TODO_INSN(); } +void SoftCPU::PADDB_mm1_mm2m64(const X86::Instruction&) { TODO_INSN(); } +void SoftCPU::PADDW_mm1_mm2m64(const X86::Instruction&) { TODO_INSN(); } +void SoftCPU::PADDD_mm1_mm2m64(const X86::Instruction&) { TODO_INSN(); } +void SoftCPU::POPA(const X86::Instruction&) { TODO_INSN(); } +void SoftCPU::POPAD(const X86::Instruction&) { TODO_INSN(); } +void SoftCPU::POPF(const X86::Instruction&) { TODO_INSN(); } + +void SoftCPU::POPFD(const X86::Instruction&) +{ + auto popped_value = pop32(); + m_eflags &= ~0x00fcffff; + m_eflags |= popped_value.value() & 0x00fcffff; + taint_flags_from(popped_value); +} + +void SoftCPU::POP_DS(const X86::Instruction&) { TODO_INSN(); } +void SoftCPU::POP_ES(const X86::Instruction&) { TODO_INSN(); } +void SoftCPU::POP_FS(const X86::Instruction&) { TODO_INSN(); } +void SoftCPU::POP_GS(const X86::Instruction&) { TODO_INSN(); } + +void SoftCPU::POP_RM16(const X86::Instruction& insn) +{ + insn.modrm().write16(*this, insn, pop16()); +} + +void SoftCPU::POP_RM32(const X86::Instruction& insn) +{ + insn.modrm().write32(*this, insn, pop32()); +} + +void SoftCPU::POP_SS(const X86::Instruction&) { TODO_INSN(); } + +void SoftCPU::POP_reg16(const X86::Instruction& insn) +{ + gpr16(insn.reg16()) = pop16(); +} + +void SoftCPU::POP_reg32(const X86::Instruction& insn) +{ + gpr32(insn.reg32()) = pop32(); +} + +void SoftCPU::PUSHA(const X86::Instruction&) { TODO_INSN(); } +void SoftCPU::PUSHAD(const X86::Instruction&) { TODO_INSN(); } +void SoftCPU::PUSHF(const X86::Instruction&) { TODO_INSN(); } + +void SoftCPU::PUSHFD(const X86::Instruction&) +{ + // FIXME: Respect shadow flags when they exist! + push32(shadow_wrap_as_initialized(m_eflags & 0x00fcffff)); +} + +void SoftCPU::PUSH_CS(const X86::Instruction&) { TODO_INSN(); } +void SoftCPU::PUSH_DS(const X86::Instruction&) { TODO_INSN(); } +void SoftCPU::PUSH_ES(const X86::Instruction&) { TODO_INSN(); } +void SoftCPU::PUSH_FS(const X86::Instruction&) { TODO_INSN(); } +void SoftCPU::PUSH_GS(const X86::Instruction&) { TODO_INSN(); } +void SoftCPU::PUSH_RM16(const X86::Instruction&) { TODO_INSN(); } + +void SoftCPU::PUSH_RM32(const X86::Instruction& insn) +{ + push32(insn.modrm().read32(*this, insn)); +} + +void SoftCPU::PUSH_SP_8086_80186(const X86::Instruction&) { TODO_INSN(); } +void SoftCPU::PUSH_SS(const X86::Instruction&) { TODO_INSN(); } + +void SoftCPU::PUSH_imm16(const X86::Instruction& insn) +{ + push16(shadow_wrap_as_initialized(insn.imm16())); +} + +void SoftCPU::PUSH_imm32(const X86::Instruction& insn) +{ + push32(shadow_wrap_as_initialized(insn.imm32())); +} + +void SoftCPU::PUSH_imm8(const X86::Instruction& insn) +{ + ASSERT(!insn.has_operand_size_override_prefix()); + push32(shadow_wrap_as_initialized<u32>(sign_extended_to<i32>(insn.imm8()))); +} + +void SoftCPU::PUSH_reg16(const X86::Instruction& insn) +{ + push16(gpr16(insn.reg16())); +} + +void SoftCPU::PUSH_reg32(const X86::Instruction& insn) +{ + push32(gpr32(insn.reg32())); + + if (m_secret_handshake_state == 2) { + m_secret_data[0] = gpr32(insn.reg32()).value(); + ++m_secret_handshake_state; + } else if (m_secret_handshake_state == 3) { + m_secret_data[1] = gpr32(insn.reg32()).value(); + ++m_secret_handshake_state; + } else if (m_secret_handshake_state == 4) { + m_secret_data[2] = gpr32(insn.reg32()).value(); + m_secret_handshake_state = 0; + did_receive_secret_data(); + } +} + +template<typename T, bool cf> +ALWAYS_INLINE static T op_rcl_impl(SoftCPU& cpu, T data, ValueWithShadow<u8> steps) +{ + if (steps.value() == 0) + return shadow_wrap_with_taint_from(data.value(), data, steps); + + u32 result = 0; + u32 new_flags = 0; + + if constexpr (cf) + asm volatile("stc"); + else + asm volatile("clc"); + + if constexpr (sizeof(typename T::ValueType) == 4) { + asm volatile("rcll %%cl, %%eax\n" + : "=a"(result) + : "a"(data.value()), "c"(steps.value())); + } else if constexpr (sizeof(typename T::ValueType) == 2) { + asm volatile("rclw %%cl, %%ax\n" + : "=a"(result) + : "a"(data.value()), "c"(steps.value())); + } else if constexpr (sizeof(typename T::ValueType) == 1) { + asm volatile("rclb %%cl, %%al\n" + : "=a"(result) + : "a"(data.value()), "c"(steps.value())); + } + + asm volatile( + "pushf\n" + "pop %%ebx" + : "=b"(new_flags)); + + cpu.set_flags_oc(new_flags); + cpu.taint_flags_from(data, steps); + return shadow_wrap_with_taint_from<typename T::ValueType>(result, data, steps); +} + +template<typename T> +ALWAYS_INLINE static T op_rcl(SoftCPU& cpu, T data, ValueWithShadow<u8> steps) +{ + cpu.warn_if_flags_tainted("rcl"); + if (cpu.cf()) + return op_rcl_impl<T, true>(cpu, data, steps); + return op_rcl_impl<T, false>(cpu, data, steps); +} + +DEFINE_GENERIC_SHIFT_ROTATE_INSN_HANDLERS(RCL, op_rcl) + +template<typename T, bool cf> +ALWAYS_INLINE static T op_rcr_impl(SoftCPU& cpu, T data, ValueWithShadow<u8> steps) +{ + if (steps.value() == 0) + return shadow_wrap_with_taint_from(data.value(), data, steps); + + u32 result = 0; + u32 new_flags = 0; + + if constexpr (cf) + asm volatile("stc"); + else + asm volatile("clc"); + + if constexpr (sizeof(typename T::ValueType) == 4) { + asm volatile("rcrl %%cl, %%eax\n" + : "=a"(result) + : "a"(data.value()), "c"(steps.value())); + } else if constexpr (sizeof(typename T::ValueType) == 2) { + asm volatile("rcrw %%cl, %%ax\n" + : "=a"(result) + : "a"(data.value()), "c"(steps.value())); + } else if constexpr (sizeof(typename T::ValueType) == 1) { + asm volatile("rcrb %%cl, %%al\n" + : "=a"(result) + : "a"(data.value()), "c"(steps.value())); + } + + asm volatile( + "pushf\n" + "pop %%ebx" + : "=b"(new_flags)); + + cpu.set_flags_oc(new_flags); + return shadow_wrap_with_taint_from<typename T::ValueType>(result, data, steps); +} + +template<typename T> +ALWAYS_INLINE static T op_rcr(SoftCPU& cpu, T data, ValueWithShadow<u8> steps) +{ + cpu.warn_if_flags_tainted("rcr"); + if (cpu.cf()) + return op_rcr_impl<T, true>(cpu, data, steps); + return op_rcr_impl<T, false>(cpu, data, steps); +} + +DEFINE_GENERIC_SHIFT_ROTATE_INSN_HANDLERS(RCR, op_rcr) + +void SoftCPU::RDTSC(const X86::Instruction&) { TODO_INSN(); } + +void SoftCPU::RET(const X86::Instruction& insn) +{ + ASSERT(!insn.has_operand_size_override_prefix()); + auto ret_address = pop32(); + warn_if_uninitialized(ret_address, "ret"); + set_eip(ret_address.value()); +} + +void SoftCPU::RETF(const X86::Instruction&) { TODO_INSN(); } +void SoftCPU::RETF_imm16(const X86::Instruction&) { TODO_INSN(); } + +void SoftCPU::RET_imm16(const X86::Instruction& insn) +{ + ASSERT(!insn.has_operand_size_override_prefix()); + auto ret_address = pop32(); + warn_if_uninitialized(ret_address, "ret imm16"); + set_eip(ret_address.value()); + set_esp({ esp().value() + insn.imm16(), esp().shadow() }); +} + +template<typename T> +ALWAYS_INLINE static T op_rol(SoftCPU& cpu, T data, ValueWithShadow<u8> steps) +{ + if (steps.value() == 0) + return shadow_wrap_with_taint_from(data.value(), data, steps); + + u32 result = 0; + u32 new_flags = 0; + + if constexpr (sizeof(typename T::ValueType) == 4) { + asm volatile("roll %%cl, %%eax\n" + : "=a"(result) + : "a"(data.value()), "c"(steps.value())); + } else if constexpr (sizeof(typename T::ValueType) == 2) { + asm volatile("rolw %%cl, %%ax\n" + : "=a"(result) + : "a"(data.value()), "c"(steps.value())); + } else if constexpr (sizeof(typename T::ValueType) == 1) { + asm volatile("rolb %%cl, %%al\n" + : "=a"(result) + : "a"(data.value()), "c"(steps.value())); + } + + asm volatile( + "pushf\n" + "pop %%ebx" + : "=b"(new_flags)); + + cpu.set_flags_oc(new_flags); + return shadow_wrap_with_taint_from<typename T::ValueType>(result, data, steps); +} + +DEFINE_GENERIC_SHIFT_ROTATE_INSN_HANDLERS(ROL, op_rol) + +template<typename T> +ALWAYS_INLINE static T op_ror(SoftCPU& cpu, T data, ValueWithShadow<u8> steps) +{ + if (steps.value() == 0) + return shadow_wrap_with_taint_from(data.value(), data, steps); + + u32 result = 0; + u32 new_flags = 0; + + if constexpr (sizeof(typename T::ValueType) == 4) { + asm volatile("rorl %%cl, %%eax\n" + : "=a"(result) + : "a"(data.value()), "c"(steps.value())); + } else if constexpr (sizeof(typename T::ValueType) == 2) { + asm volatile("rorw %%cl, %%ax\n" + : "=a"(result) + : "a"(data.value()), "c"(steps.value())); + } else if constexpr (sizeof(typename T::ValueType) == 1) { + asm volatile("rorb %%cl, %%al\n" + : "=a"(result) + : "a"(data.value()), "c"(steps.value())); + } + + asm volatile( + "pushf\n" + "pop %%ebx" + : "=b"(new_flags)); + + cpu.set_flags_oc(new_flags); + return shadow_wrap_with_taint_from<typename T::ValueType>(result, data, steps); +} + +DEFINE_GENERIC_SHIFT_ROTATE_INSN_HANDLERS(ROR, op_ror) + +void SoftCPU::SAHF(const X86::Instruction&) { TODO_INSN(); } + +void SoftCPU::SALC(const X86::Instruction&) +{ + // FIXME: Respect shadow flags once they exists! + set_al(shadow_wrap_as_initialized<u8>(cf() ? 0xff : 0x00)); + + if (m_secret_handshake_state < 2) + ++m_secret_handshake_state; + else + m_secret_handshake_state = 0; +} + +template<typename T> +static T op_sar(SoftCPU& cpu, T data, ValueWithShadow<u8> steps) +{ + if (steps.value() == 0) + return shadow_wrap_with_taint_from(data.value(), data, steps); + + u32 result = 0; + u32 new_flags = 0; + + if constexpr (sizeof(typename T::ValueType) == 4) { + asm volatile("sarl %%cl, %%eax\n" + : "=a"(result) + : "a"(data.value()), "c"(steps.value())); + } else if constexpr (sizeof(typename T::ValueType) == 2) { + asm volatile("sarw %%cl, %%ax\n" + : "=a"(result) + : "a"(data.value()), "c"(steps.value())); + } else if constexpr (sizeof(typename T::ValueType) == 1) { + asm volatile("sarb %%cl, %%al\n" + : "=a"(result) + : "a"(data.value()), "c"(steps.value())); + } + + asm volatile( + "pushf\n" + "pop %%ebx" + : "=b"(new_flags)); + + cpu.set_flags_oszapc(new_flags); + return shadow_wrap_with_taint_from<typename T::ValueType>(result, data, steps); +} + +DEFINE_GENERIC_SHIFT_ROTATE_INSN_HANDLERS(SAR, op_sar) + +template<typename T> +ALWAYS_INLINE static void do_scas(SoftCPU& cpu, const X86::Instruction& insn) +{ + cpu.do_once_or_repeat<true>(insn, [&] { + auto src = cpu.const_gpr<T>(X86::RegisterAL); + auto dest = cpu.read_memory<T>({ cpu.es(), cpu.destination_index(insn.a32()).value() }); + op_sub(cpu, dest, src); + cpu.step_destination_index(insn.a32(), sizeof(T)); + }); +} + +void SoftCPU::SCASB(const X86::Instruction& insn) +{ + do_scas<u8>(*this, insn); +} + +void SoftCPU::SCASD(const X86::Instruction& insn) +{ + do_scas<u32>(*this, insn); +} + +void SoftCPU::SCASW(const X86::Instruction& insn) +{ + do_scas<u16>(*this, insn); +} + +void SoftCPU::SETcc_RM8(const X86::Instruction& insn) +{ + warn_if_flags_tainted("setcc"); + insn.modrm().write8(*this, insn, shadow_wrap_as_initialized<u8>(evaluate_condition(insn.cc()))); +} + +void SoftCPU::SGDT(const X86::Instruction&) { TODO_INSN(); } + +void SoftCPU::SHLD_RM16_reg16_CL(const X86::Instruction& insn) +{ + insn.modrm().write16(*this, insn, op_shld(*this, insn.modrm().read16(*this, insn), const_gpr16(insn.reg16()), cl())); +} + +void SoftCPU::SHLD_RM16_reg16_imm8(const X86::Instruction& insn) +{ + insn.modrm().write16(*this, insn, op_shld(*this, insn.modrm().read16(*this, insn), const_gpr16(insn.reg16()), shadow_wrap_as_initialized(insn.imm8()))); +} + +void SoftCPU::SHLD_RM32_reg32_CL(const X86::Instruction& insn) +{ + insn.modrm().write32(*this, insn, op_shld(*this, insn.modrm().read32(*this, insn), const_gpr32(insn.reg32()), cl())); +} + +void SoftCPU::SHLD_RM32_reg32_imm8(const X86::Instruction& insn) +{ + insn.modrm().write32(*this, insn, op_shld(*this, insn.modrm().read32(*this, insn), const_gpr32(insn.reg32()), shadow_wrap_as_initialized(insn.imm8()))); +} + +DEFINE_GENERIC_SHIFT_ROTATE_INSN_HANDLERS(SHL, op_shl) + +void SoftCPU::SHRD_RM16_reg16_CL(const X86::Instruction& insn) +{ + insn.modrm().write16(*this, insn, op_shrd(*this, insn.modrm().read16(*this, insn), const_gpr16(insn.reg16()), cl())); +} + +void SoftCPU::SHRD_RM16_reg16_imm8(const X86::Instruction& insn) +{ + insn.modrm().write16(*this, insn, op_shrd(*this, insn.modrm().read16(*this, insn), const_gpr16(insn.reg16()), shadow_wrap_as_initialized(insn.imm8()))); +} + +void SoftCPU::SHRD_RM32_reg32_CL(const X86::Instruction& insn) +{ + insn.modrm().write32(*this, insn, op_shrd(*this, insn.modrm().read32(*this, insn), const_gpr32(insn.reg32()), cl())); +} + +void SoftCPU::SHRD_RM32_reg32_imm8(const X86::Instruction& insn) +{ + insn.modrm().write32(*this, insn, op_shrd(*this, insn.modrm().read32(*this, insn), const_gpr32(insn.reg32()), shadow_wrap_as_initialized(insn.imm8()))); +} + +DEFINE_GENERIC_SHIFT_ROTATE_INSN_HANDLERS(SHR, op_shr) + +void SoftCPU::SIDT(const X86::Instruction&) { TODO_INSN(); } +void SoftCPU::SLDT_RM16(const X86::Instruction&) { TODO_INSN(); } +void SoftCPU::SMSW_RM16(const X86::Instruction&) { TODO_INSN(); } + +void SoftCPU::STC(const X86::Instruction&) +{ + set_cf(true); +} + +void SoftCPU::STD(const X86::Instruction&) +{ + set_df(true); +} + +void SoftCPU::STI(const X86::Instruction&) { TODO_INSN(); } + +void SoftCPU::STOSB(const X86::Instruction& insn) +{ + if (insn.has_rep_prefix() && !df()) { + // Fast path for 8-bit forward memory fill. + if (m_emulator.mmu().fast_fill_memory8({ es(), destination_index(insn.a32()).value() }, ecx().value(), al())) { + if (insn.a32()) { + // FIXME: Should an uninitialized ECX taint EDI here? + set_edi({ (u32)(edi().value() + ecx().value()), edi().shadow() }); + set_ecx(shadow_wrap_as_initialized<u32>(0)); + } else { + // FIXME: Should an uninitialized CX taint DI here? + set_di({ (u16)(di().value() + cx().value()), di().shadow() }); + set_cx(shadow_wrap_as_initialized<u16>(0)); + } + return; + } + } + + do_once_or_repeat<false>(insn, [&] { + write_memory8({ es(), destination_index(insn.a32()).value() }, al()); + step_destination_index(insn.a32(), 1); + }); +} + +void SoftCPU::STOSD(const X86::Instruction& insn) +{ + if (insn.has_rep_prefix() && !df()) { + // Fast path for 32-bit forward memory fill. + if (m_emulator.mmu().fast_fill_memory32({ es(), destination_index(insn.a32()).value() }, ecx().value(), eax())) { + if (insn.a32()) { + // FIXME: Should an uninitialized ECX taint EDI here? + set_edi({ (u32)(edi().value() + (ecx().value() * sizeof(u32))), edi().shadow() }); + set_ecx(shadow_wrap_as_initialized<u32>(0)); + } else { + // FIXME: Should an uninitialized CX taint DI here? + set_di({ (u16)(di().value() + (cx().value() * sizeof(u32))), di().shadow() }); + set_cx(shadow_wrap_as_initialized<u16>(0)); + } + return; + } + } + + do_once_or_repeat<false>(insn, [&] { + write_memory32({ es(), destination_index(insn.a32()).value() }, eax()); + step_destination_index(insn.a32(), 4); + }); +} + +void SoftCPU::STOSW(const X86::Instruction& insn) +{ + do_once_or_repeat<false>(insn, [&] { + write_memory16({ es(), destination_index(insn.a32()).value() }, ax()); + step_destination_index(insn.a32(), 2); + }); +} + +void SoftCPU::STR_RM16(const X86::Instruction&) { TODO_INSN(); } +void SoftCPU::UD0(const X86::Instruction&) { TODO_INSN(); } +void SoftCPU::UD1(const X86::Instruction&) { TODO_INSN(); } +void SoftCPU::UD2(const X86::Instruction&) { TODO_INSN(); } +void SoftCPU::VERR_RM16(const X86::Instruction&) { TODO_INSN(); } +void SoftCPU::VERW_RM16(const X86::Instruction&) { TODO_INSN(); } +void SoftCPU::WAIT(const X86::Instruction&) { TODO_INSN(); } +void SoftCPU::WBINVD(const X86::Instruction&) { TODO_INSN(); } + +void SoftCPU::XADD_RM16_reg16(const X86::Instruction& insn) +{ + auto dest = insn.modrm().read16(*this, insn); + auto src = const_gpr16(insn.reg16()); + auto result = op_add(*this, dest, src); + gpr16(insn.reg16()) = dest; + insn.modrm().write16(*this, insn, result); +} + +void SoftCPU::XADD_RM32_reg32(const X86::Instruction& insn) +{ + auto dest = insn.modrm().read32(*this, insn); + auto src = const_gpr32(insn.reg32()); + auto result = op_add(*this, dest, src); + gpr32(insn.reg32()) = dest; + insn.modrm().write32(*this, insn, result); +} + +void SoftCPU::XADD_RM8_reg8(const X86::Instruction& insn) +{ + auto dest = insn.modrm().read8(*this, insn); + auto src = const_gpr8(insn.reg8()); + auto result = op_add(*this, dest, src); + gpr8(insn.reg8()) = dest; + insn.modrm().write8(*this, insn, result); +} + +void SoftCPU::XCHG_AX_reg16(const X86::Instruction& insn) +{ + auto temp = gpr16(insn.reg16()); + gpr16(insn.reg16()) = ax(); + set_ax(temp); +} + +void SoftCPU::XCHG_EAX_reg32(const X86::Instruction& insn) +{ + auto temp = gpr32(insn.reg32()); + gpr32(insn.reg32()) = eax(); + set_eax(temp); +} + +void SoftCPU::XCHG_reg16_RM16(const X86::Instruction& insn) +{ + auto temp = insn.modrm().read16(*this, insn); + insn.modrm().write16(*this, insn, const_gpr16(insn.reg16())); + gpr16(insn.reg16()) = temp; +} + +void SoftCPU::XCHG_reg32_RM32(const X86::Instruction& insn) +{ + auto temp = insn.modrm().read32(*this, insn); + insn.modrm().write32(*this, insn, const_gpr32(insn.reg32())); + gpr32(insn.reg32()) = temp; +} + +void SoftCPU::XCHG_reg8_RM8(const X86::Instruction& insn) +{ + auto temp = insn.modrm().read8(*this, insn); + insn.modrm().write8(*this, insn, const_gpr8(insn.reg8())); + gpr8(insn.reg8()) = temp; +} + +void SoftCPU::XLAT(const X86::Instruction& insn) +{ + if (insn.a32()) + warn_if_uninitialized(ebx(), "xlat ebx"); + else + warn_if_uninitialized(bx(), "xlat bx"); + warn_if_uninitialized(al(), "xlat al"); + u32 offset = (insn.a32() ? ebx().value() : bx().value()) + al().value(); + set_al(read_memory8({ segment(insn.segment_prefix().value_or(X86::SegmentRegister::DS)), offset })); +} + +#define DEFINE_GENERIC_INSN_HANDLERS_PARTIAL(mnemonic, op, update_dest, is_zero_idiom_if_both_operands_same, is_or) \ + void SoftCPU::mnemonic##_AL_imm8(const X86::Instruction& insn) { generic_AL_imm8<update_dest, is_or>(op<ValueWithShadow<u8>>, insn); } \ + void SoftCPU::mnemonic##_AX_imm16(const X86::Instruction& insn) { generic_AX_imm16<update_dest, is_or>(op<ValueWithShadow<u16>>, insn); } \ + void SoftCPU::mnemonic##_EAX_imm32(const X86::Instruction& insn) { generic_EAX_imm32<update_dest, is_or>(op<ValueWithShadow<u32>>, insn); } \ + void SoftCPU::mnemonic##_RM16_imm16(const X86::Instruction& insn) { generic_RM16_imm16<update_dest, is_or>(op<ValueWithShadow<u16>>, insn); } \ + void SoftCPU::mnemonic##_RM16_reg16(const X86::Instruction& insn) { generic_RM16_reg16<update_dest, is_zero_idiom_if_both_operands_same>(op<ValueWithShadow<u16>>, insn); } \ + void SoftCPU::mnemonic##_RM32_imm32(const X86::Instruction& insn) { generic_RM32_imm32<update_dest, is_or>(op<ValueWithShadow<u32>>, insn); } \ + void SoftCPU::mnemonic##_RM32_reg32(const X86::Instruction& insn) { generic_RM32_reg32<update_dest, is_zero_idiom_if_both_operands_same>(op<ValueWithShadow<u32>>, insn); } \ + void SoftCPU::mnemonic##_RM8_imm8(const X86::Instruction& insn) { generic_RM8_imm8<update_dest, is_or>(op<ValueWithShadow<u8>>, insn); } \ + void SoftCPU::mnemonic##_RM8_reg8(const X86::Instruction& insn) { generic_RM8_reg8<update_dest, is_zero_idiom_if_both_operands_same>(op<ValueWithShadow<u8>>, insn); } + +#define DEFINE_GENERIC_INSN_HANDLERS(mnemonic, op, update_dest, is_zero_idiom_if_both_operands_same, is_or) \ + DEFINE_GENERIC_INSN_HANDLERS_PARTIAL(mnemonic, op, update_dest, is_zero_idiom_if_both_operands_same, is_or) \ + void SoftCPU::mnemonic##_RM16_imm8(const X86::Instruction& insn) { generic_RM16_imm8<update_dest, is_or>(op<ValueWithShadow<u16>>, insn); } \ + void SoftCPU::mnemonic##_RM32_imm8(const X86::Instruction& insn) { generic_RM32_imm8<update_dest, is_or>(op<ValueWithShadow<u32>>, insn); } \ + void SoftCPU::mnemonic##_reg16_RM16(const X86::Instruction& insn) { generic_reg16_RM16<update_dest, is_zero_idiom_if_both_operands_same>(op<ValueWithShadow<u16>>, insn); } \ + void SoftCPU::mnemonic##_reg32_RM32(const X86::Instruction& insn) { generic_reg32_RM32<update_dest, is_zero_idiom_if_both_operands_same>(op<ValueWithShadow<u32>>, insn); } \ + void SoftCPU::mnemonic##_reg8_RM8(const X86::Instruction& insn) { generic_reg8_RM8<update_dest, is_zero_idiom_if_both_operands_same>(op<ValueWithShadow<u8>>, insn); } + +DEFINE_GENERIC_INSN_HANDLERS(XOR, op_xor, true, true, false) +DEFINE_GENERIC_INSN_HANDLERS(OR, op_or, true, false, true) +DEFINE_GENERIC_INSN_HANDLERS(ADD, op_add, true, false, false) +DEFINE_GENERIC_INSN_HANDLERS(ADC, op_adc, true, false, false) +DEFINE_GENERIC_INSN_HANDLERS(SUB, op_sub, true, true, false) +DEFINE_GENERIC_INSN_HANDLERS(SBB, op_sbb, true, false, false) +DEFINE_GENERIC_INSN_HANDLERS(AND, op_and, true, false, false) +DEFINE_GENERIC_INSN_HANDLERS(CMP, op_sub, false, false, false) +DEFINE_GENERIC_INSN_HANDLERS_PARTIAL(TEST, op_and, false, false, false) + +void SoftCPU::MOVQ_mm1_mm2m64(const X86::Instruction&) { TODO_INSN(); } +void SoftCPU::EMMS(const X86::Instruction&) { TODO_INSN(); } +void SoftCPU::MOVQ_mm1_m64_mm2(const X86::Instruction&) { TODO_INSN(); } +void SoftCPU::wrap_0xC0(const X86::Instruction&) { TODO_INSN(); } +void SoftCPU::wrap_0xC1_16(const X86::Instruction&) { TODO_INSN(); } +void SoftCPU::wrap_0xC1_32(const X86::Instruction&) { TODO_INSN(); } +void SoftCPU::wrap_0xD0(const X86::Instruction&) { TODO_INSN(); } +void SoftCPU::wrap_0xD1_16(const X86::Instruction&) { TODO_INSN(); } +void SoftCPU::wrap_0xD1_32(const X86::Instruction&) { TODO_INSN(); } +void SoftCPU::wrap_0xD2(const X86::Instruction&) { TODO_INSN(); } +void SoftCPU::wrap_0xD3_16(const X86::Instruction&) { TODO_INSN(); } +void SoftCPU::wrap_0xD3_32(const X86::Instruction&) { TODO_INSN(); } +} diff --git a/Userland/DevTools/UserspaceEmulator/SoftCPU.h b/Userland/DevTools/UserspaceEmulator/SoftCPU.h new file mode 100644 index 0000000000..89ece8976d --- /dev/null +++ b/Userland/DevTools/UserspaceEmulator/SoftCPU.h @@ -0,0 +1,1202 @@ +/* + * Copyright (c) 2020, Andreas Kling <kling@serenityos.org> + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * 1. Redistributions of source code must retain the above copyright notice, this + * list of conditions and the following disclaimer. + * + * 2. Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE + * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL + * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR + * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER + * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, + * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE + * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +#pragma once + +#include "Region.h" +#include "ValueWithShadow.h" +#include <LibX86/Instruction.h> +#include <LibX86/Interpreter.h> + +namespace UserspaceEmulator { + +class Emulator; +class Region; + +union PartAddressableRegister { + struct { + u32 full_u32; + }; + struct { + u16 low_u16; + u16 high_u16; + }; + struct { + u8 low_u8; + u8 high_u8; + u16 also_high_u16; + }; +}; + +class SoftCPU final + : public X86::Interpreter + , public X86::InstructionStream { +public: + using ValueWithShadowType8 = ValueWithShadow<u8>; + using ValueWithShadowType16 = ValueWithShadow<u16>; + using ValueWithShadowType32 = ValueWithShadow<u32>; + using ValueWithShadowType64 = ValueWithShadow<u64>; + + explicit SoftCPU(Emulator&); + void dump() const; + + u32 base_eip() const { return m_base_eip; } + void save_base_eip() { m_base_eip = m_eip; } + + u32 eip() const { return m_eip; } + void set_eip(u32 eip) + { + m_eip = eip; + } + + struct Flags { + enum Flag { + CF = 0x0001, + PF = 0x0004, + AF = 0x0010, + ZF = 0x0040, + SF = 0x0080, + TF = 0x0100, + IF = 0x0200, + DF = 0x0400, + OF = 0x0800, + }; + }; + + void push32(ValueWithShadow<u32>); + ValueWithShadow<u32> pop32(); + + void push16(ValueWithShadow<u16>); + ValueWithShadow<u16> pop16(); + + void push_string(const StringView&); + void push_buffer(const u8* data, size_t); + + u16 segment(X86::SegmentRegister seg) const { return m_segment[(int)seg]; } + u16& segment(X86::SegmentRegister seg) { return m_segment[(int)seg]; } + + ValueAndShadowReference<u8> gpr8(X86::RegisterIndex8 reg) + { + switch (reg) { + case X86::RegisterAL: + return { m_gpr[X86::RegisterEAX].low_u8, m_gpr_shadow[X86::RegisterEAX].low_u8 }; + case X86::RegisterAH: + return { m_gpr[X86::RegisterEAX].high_u8, m_gpr_shadow[X86::RegisterEAX].high_u8 }; + case X86::RegisterBL: + return { m_gpr[X86::RegisterEBX].low_u8, m_gpr_shadow[X86::RegisterEBX].low_u8 }; + case X86::RegisterBH: + return { m_gpr[X86::RegisterEBX].high_u8, m_gpr_shadow[X86::RegisterEBX].high_u8 }; + case X86::RegisterCL: + return { m_gpr[X86::RegisterECX].low_u8, m_gpr_shadow[X86::RegisterECX].low_u8 }; + case X86::RegisterCH: + return { m_gpr[X86::RegisterECX].high_u8, m_gpr_shadow[X86::RegisterECX].high_u8 }; + case X86::RegisterDL: + return { m_gpr[X86::RegisterEDX].low_u8, m_gpr_shadow[X86::RegisterEDX].low_u8 }; + case X86::RegisterDH: + return { m_gpr[X86::RegisterEDX].high_u8, m_gpr_shadow[X86::RegisterEDX].high_u8 }; + } + ASSERT_NOT_REACHED(); + } + + ValueWithShadow<u8> const_gpr8(X86::RegisterIndex8 reg) const + { + switch (reg) { + case X86::RegisterAL: + return { m_gpr[X86::RegisterEAX].low_u8, m_gpr_shadow[X86::RegisterEAX].low_u8 }; + case X86::RegisterAH: + return { m_gpr[X86::RegisterEAX].high_u8, m_gpr_shadow[X86::RegisterEAX].high_u8 }; + case X86::RegisterBL: + return { m_gpr[X86::RegisterEBX].low_u8, m_gpr_shadow[X86::RegisterEBX].low_u8 }; + case X86::RegisterBH: + return { m_gpr[X86::RegisterEBX].high_u8, m_gpr_shadow[X86::RegisterEBX].high_u8 }; + case X86::RegisterCL: + return { m_gpr[X86::RegisterECX].low_u8, m_gpr_shadow[X86::RegisterECX].low_u8 }; + case X86::RegisterCH: + return { m_gpr[X86::RegisterECX].high_u8, m_gpr_shadow[X86::RegisterECX].high_u8 }; + case X86::RegisterDL: + return { m_gpr[X86::RegisterEDX].low_u8, m_gpr_shadow[X86::RegisterEDX].low_u8 }; + case X86::RegisterDH: + return { m_gpr[X86::RegisterEDX].high_u8, m_gpr_shadow[X86::RegisterEDX].high_u8 }; + } + ASSERT_NOT_REACHED(); + } + + ValueWithShadow<u16> const_gpr16(X86::RegisterIndex16 reg) const + { + return { m_gpr[reg].low_u16, m_gpr_shadow[reg].low_u16 }; + } + + ValueAndShadowReference<u16> gpr16(X86::RegisterIndex16 reg) + { + return { m_gpr[reg].low_u16, m_gpr_shadow[reg].low_u16 }; + } + + ValueWithShadow<u32> const_gpr32(X86::RegisterIndex32 reg) const + { + return { m_gpr[reg].full_u32, m_gpr_shadow[reg].full_u32 }; + } + + ValueAndShadowReference<u32> gpr32(X86::RegisterIndex32 reg) + { + return { m_gpr[reg].full_u32, m_gpr_shadow[reg].full_u32 }; + } + + template<typename T> + ValueWithShadow<T> const_gpr(unsigned register_index) const + { + if constexpr (sizeof(T) == 1) + return const_gpr8((X86::RegisterIndex8)register_index); + if constexpr (sizeof(T) == 2) + return const_gpr16((X86::RegisterIndex16)register_index); + if constexpr (sizeof(T) == 4) + return const_gpr32((X86::RegisterIndex32)register_index); + } + + template<typename T> + ValueAndShadowReference<T> gpr(unsigned register_index) + { + if constexpr (sizeof(T) == 1) + return gpr8((X86::RegisterIndex8)register_index); + if constexpr (sizeof(T) == 2) + return gpr16((X86::RegisterIndex16)register_index); + if constexpr (sizeof(T) == 4) + return gpr32((X86::RegisterIndex32)register_index); + } + + ValueWithShadow<u32> source_index(bool a32) const + { + if (a32) + return esi(); + return { si().value(), (u32)si().shadow() & 0xffff }; + } + + ValueWithShadow<u32> destination_index(bool a32) const + { + if (a32) + return edi(); + return { di().value(), (u32)di().shadow() & 0xffff }; + } + + ValueWithShadow<u32> loop_index(bool a32) const + { + if (a32) + return ecx(); + return { cx().value(), (u32)cx().shadow() & 0xffff }; + } + + bool decrement_loop_index(bool a32) + { + if (a32) { + set_ecx({ ecx().value() - 1, ecx().shadow() }); + return ecx().value() == 0; + } + set_cx(ValueWithShadow<u16>(cx().value() - 1, cx().shadow())); + return cx().value() == 0; + } + + ALWAYS_INLINE void step_source_index(bool a32, u32 step) + { + if (a32) { + if (df()) + set_esi({ esi().value() - step, esi().shadow() }); + else + set_esi({ esi().value() + step, esi().shadow() }); + } else { + if (df()) + set_si(ValueWithShadow<u16>(si().value() - step, si().shadow())); + else + set_si(ValueWithShadow<u16>(si().value() + step, si().shadow())); + } + } + + ALWAYS_INLINE void step_destination_index(bool a32, u32 step) + { + if (a32) { + if (df()) + set_edi({ edi().value() - step, edi().shadow() }); + else + set_edi({ edi().value() + step, edi().shadow() }); + } else { + if (df()) + set_di(ValueWithShadow<u16>(di().value() - step, di().shadow())); + else + set_di(ValueWithShadow<u16>(di().value() + step, di().shadow())); + } + } + + u32 eflags() const { return m_eflags; } + void set_eflags(ValueWithShadow<u32> eflags) + { + m_eflags = eflags.value(); + m_flags_tainted = eflags.is_uninitialized(); + } + + ValueWithShadow<u32> eax() const { return const_gpr32(X86::RegisterEAX); } + ValueWithShadow<u32> ebx() const { return const_gpr32(X86::RegisterEBX); } + ValueWithShadow<u32> ecx() const { return const_gpr32(X86::RegisterECX); } + ValueWithShadow<u32> edx() const { return const_gpr32(X86::RegisterEDX); } + ValueWithShadow<u32> esp() const { return const_gpr32(X86::RegisterESP); } + ValueWithShadow<u32> ebp() const { return const_gpr32(X86::RegisterEBP); } + ValueWithShadow<u32> esi() const { return const_gpr32(X86::RegisterESI); } + ValueWithShadow<u32> edi() const { return const_gpr32(X86::RegisterEDI); } + + ValueWithShadow<u16> ax() const { return const_gpr16(X86::RegisterAX); } + ValueWithShadow<u16> bx() const { return const_gpr16(X86::RegisterBX); } + ValueWithShadow<u16> cx() const { return const_gpr16(X86::RegisterCX); } + ValueWithShadow<u16> dx() const { return const_gpr16(X86::RegisterDX); } + ValueWithShadow<u16> sp() const { return const_gpr16(X86::RegisterSP); } + ValueWithShadow<u16> bp() const { return const_gpr16(X86::RegisterBP); } + ValueWithShadow<u16> si() const { return const_gpr16(X86::RegisterSI); } + ValueWithShadow<u16> di() const { return const_gpr16(X86::RegisterDI); } + + ValueWithShadow<u8> al() const { return const_gpr8(X86::RegisterAL); } + ValueWithShadow<u8> ah() const { return const_gpr8(X86::RegisterAH); } + ValueWithShadow<u8> bl() const { return const_gpr8(X86::RegisterBL); } + ValueWithShadow<u8> bh() const { return const_gpr8(X86::RegisterBH); } + ValueWithShadow<u8> cl() const { return const_gpr8(X86::RegisterCL); } + ValueWithShadow<u8> ch() const { return const_gpr8(X86::RegisterCH); } + ValueWithShadow<u8> dl() const { return const_gpr8(X86::RegisterDL); } + ValueWithShadow<u8> dh() const { return const_gpr8(X86::RegisterDH); } + + void set_eax(ValueWithShadow<u32> value) { gpr32(X86::RegisterEAX) = value; } + void set_ebx(ValueWithShadow<u32> value) { gpr32(X86::RegisterEBX) = value; } + void set_ecx(ValueWithShadow<u32> value) { gpr32(X86::RegisterECX) = value; } + void set_edx(ValueWithShadow<u32> value) { gpr32(X86::RegisterEDX) = value; } + void set_esp(ValueWithShadow<u32> value) { gpr32(X86::RegisterESP) = value; } + void set_ebp(ValueWithShadow<u32> value) { gpr32(X86::RegisterEBP) = value; } + void set_esi(ValueWithShadow<u32> value) { gpr32(X86::RegisterESI) = value; } + void set_edi(ValueWithShadow<u32> value) { gpr32(X86::RegisterEDI) = value; } + + void set_ax(ValueWithShadow<u16> value) { gpr16(X86::RegisterAX) = value; } + void set_bx(ValueWithShadow<u16> value) { gpr16(X86::RegisterBX) = value; } + void set_cx(ValueWithShadow<u16> value) { gpr16(X86::RegisterCX) = value; } + void set_dx(ValueWithShadow<u16> value) { gpr16(X86::RegisterDX) = value; } + void set_sp(ValueWithShadow<u16> value) { gpr16(X86::RegisterSP) = value; } + void set_bp(ValueWithShadow<u16> value) { gpr16(X86::RegisterBP) = value; } + void set_si(ValueWithShadow<u16> value) { gpr16(X86::RegisterSI) = value; } + void set_di(ValueWithShadow<u16> value) { gpr16(X86::RegisterDI) = value; } + + void set_al(ValueWithShadow<u8> value) { gpr8(X86::RegisterAL) = value; } + void set_ah(ValueWithShadow<u8> value) { gpr8(X86::RegisterAH) = value; } + void set_bl(ValueWithShadow<u8> value) { gpr8(X86::RegisterBL) = value; } + void set_bh(ValueWithShadow<u8> value) { gpr8(X86::RegisterBH) = value; } + void set_cl(ValueWithShadow<u8> value) { gpr8(X86::RegisterCL) = value; } + void set_ch(ValueWithShadow<u8> value) { gpr8(X86::RegisterCH) = value; } + void set_dl(ValueWithShadow<u8> value) { gpr8(X86::RegisterDL) = value; } + void set_dh(ValueWithShadow<u8> value) { gpr8(X86::RegisterDH) = value; } + + bool of() const { return m_eflags & Flags::OF; } + bool sf() const { return m_eflags & Flags::SF; } + bool zf() const { return m_eflags & Flags::ZF; } + bool af() const { return m_eflags & Flags::AF; } + bool pf() const { return m_eflags & Flags::PF; } + bool cf() const { return m_eflags & Flags::CF; } + bool df() const { return m_eflags & Flags::DF; } + + void set_flag(Flags::Flag flag, bool value) + { + if (value) + m_eflags |= flag; + else + m_eflags &= ~flag; + } + + void set_of(bool value) { set_flag(Flags::OF, value); } + void set_sf(bool value) { set_flag(Flags::SF, value); } + void set_zf(bool value) { set_flag(Flags::ZF, value); } + void set_af(bool value) { set_flag(Flags::AF, value); } + void set_pf(bool value) { set_flag(Flags::PF, value); } + void set_cf(bool value) { set_flag(Flags::CF, value); } + void set_df(bool value) { set_flag(Flags::DF, value); } + + void set_flags_with_mask(u32 new_flags, u32 mask) + { + m_eflags &= ~mask; + m_eflags |= new_flags & mask; + } + + void set_flags_oszapc(u32 new_flags) + { + set_flags_with_mask(new_flags, Flags::OF | Flags::SF | Flags::ZF | Flags::AF | Flags::PF | Flags::CF); + } + + void set_flags_oszap(u32 new_flags) + { + set_flags_with_mask(new_flags, Flags::OF | Flags::SF | Flags::ZF | Flags::AF | Flags::PF); + } + + void set_flags_oszpc(u32 new_flags) + { + set_flags_with_mask(new_flags, Flags::OF | Flags::SF | Flags::ZF | Flags::PF | Flags::CF); + } + + void set_flags_oc(u32 new_flags) + { + set_flags_with_mask(new_flags, Flags::OF | Flags::CF); + } + + u16 cs() const { return m_segment[(int)X86::SegmentRegister::CS]; } + u16 ds() const { return m_segment[(int)X86::SegmentRegister::DS]; } + u16 es() const { return m_segment[(int)X86::SegmentRegister::ES]; } + u16 ss() const { return m_segment[(int)X86::SegmentRegister::SS]; } + + ValueWithShadow<u8> read_memory8(X86::LogicalAddress); + ValueWithShadow<u16> read_memory16(X86::LogicalAddress); + ValueWithShadow<u32> read_memory32(X86::LogicalAddress); + ValueWithShadow<u64> read_memory64(X86::LogicalAddress); + + template<typename T> + ValueWithShadow<T> read_memory(X86::LogicalAddress address) + { + if constexpr (sizeof(T) == 1) + return read_memory8(address); + if constexpr (sizeof(T) == 2) + return read_memory16(address); + if constexpr (sizeof(T) == 4) + return read_memory32(address); + } + + void write_memory8(X86::LogicalAddress, ValueWithShadow<u8>); + void write_memory16(X86::LogicalAddress, ValueWithShadow<u16>); + void write_memory32(X86::LogicalAddress, ValueWithShadow<u32>); + void write_memory64(X86::LogicalAddress, ValueWithShadow<u64>); + + template<typename T> + void write_memory(X86::LogicalAddress address, ValueWithShadow<T> data) + { + if constexpr (sizeof(T) == 1) + return write_memory8(address, data); + if constexpr (sizeof(T) == 2) + return write_memory16(address, data); + if constexpr (sizeof(T) == 4) + return write_memory32(address, data); + } + + bool evaluate_condition(u8 condition) const + { + switch (condition) { + case 0: + return of(); // O + case 1: + return !of(); // NO + case 2: + return cf(); // B, C, NAE + case 3: + return !cf(); // NB, NC, AE + case 4: + return zf(); // E, Z + case 5: + return !zf(); // NE, NZ + case 6: + return (cf() | zf()); // BE, NA + case 7: + return !(cf() | zf()); // NBE, A + case 8: + return sf(); // S + case 9: + return !sf(); // NS + case 10: + return pf(); // P, PE + case 11: + return !pf(); // NP, PO + case 12: + return sf() ^ of(); // L, NGE + case 13: + return !(sf() ^ of()); // NL, GE + case 14: + return (sf() ^ of()) | zf(); // LE, NG + case 15: + return !((sf() ^ of()) | zf()); // NLE, G + default: + ASSERT_NOT_REACHED(); + } + return 0; + } + + template<bool check_zf, typename Callback> + void do_once_or_repeat(const X86::Instruction& insn, Callback); + + template<typename A> + void taint_flags_from(const A& a) + { + m_flags_tainted = a.is_uninitialized(); + } + + template<typename A, typename B> + void taint_flags_from(const A& a, const B& b) + { + m_flags_tainted = a.is_uninitialized() || b.is_uninitialized(); + } + + template<typename A, typename B, typename C> + void taint_flags_from(const A& a, const B& b, const C& c) + { + m_flags_tainted = a.is_uninitialized() || b.is_uninitialized() || c.is_uninitialized(); + } + + void warn_if_flags_tainted(const char* message) const; + + // ^X86::InstructionStream + virtual bool can_read() override { return false; } + virtual u8 read8() override; + virtual u16 read16() override; + virtual u32 read32() override; + virtual u64 read64() override; + +private: + // ^X86::Interpreter + virtual void AAA(const X86::Instruction&) override; + virtual void AAD(const X86::Instruction&) override; + virtual void AAM(const X86::Instruction&) override; + virtual void AAS(const X86::Instruction&) override; + virtual void ADC_AL_imm8(const X86::Instruction&) override; + virtual void ADC_AX_imm16(const X86::Instruction&) override; + virtual void ADC_EAX_imm32(const X86::Instruction&) override; + virtual void ADC_RM16_imm16(const X86::Instruction&) override; + virtual void ADC_RM16_imm8(const X86::Instruction&) override; + virtual void ADC_RM16_reg16(const X86::Instruction&) override; + virtual void ADC_RM32_imm32(const X86::Instruction&) override; + virtual void ADC_RM32_imm8(const X86::Instruction&) override; + virtual void ADC_RM32_reg32(const X86::Instruction&) override; + virtual void ADC_RM8_imm8(const X86::Instruction&) override; + virtual void ADC_RM8_reg8(const X86::Instruction&) override; + virtual void ADC_reg16_RM16(const X86::Instruction&) override; + virtual void ADC_reg32_RM32(const X86::Instruction&) override; + virtual void ADC_reg8_RM8(const X86::Instruction&) override; + virtual void ADD_AL_imm8(const X86::Instruction&) override; + virtual void ADD_AX_imm16(const X86::Instruction&) override; + virtual void ADD_EAX_imm32(const X86::Instruction&) override; + virtual void ADD_RM16_imm16(const X86::Instruction&) override; + virtual void ADD_RM16_imm8(const X86::Instruction&) override; + virtual void ADD_RM16_reg16(const X86::Instruction&) override; + virtual void ADD_RM32_imm32(const X86::Instruction&) override; + virtual void ADD_RM32_imm8(const X86::Instruction&) override; + virtual void ADD_RM32_reg32(const X86::Instruction&) override; + virtual void ADD_RM8_imm8(const X86::Instruction&) override; + virtual void ADD_RM8_reg8(const X86::Instruction&) override; + virtual void ADD_reg16_RM16(const X86::Instruction&) override; + virtual void ADD_reg32_RM32(const X86::Instruction&) override; + virtual void ADD_reg8_RM8(const X86::Instruction&) override; + virtual void AND_AL_imm8(const X86::Instruction&) override; + virtual void AND_AX_imm16(const X86::Instruction&) override; + virtual void AND_EAX_imm32(const X86::Instruction&) override; + virtual void AND_RM16_imm16(const X86::Instruction&) override; + virtual void AND_RM16_imm8(const X86::Instruction&) override; + virtual void AND_RM16_reg16(const X86::Instruction&) override; + virtual void AND_RM32_imm32(const X86::Instruction&) override; + virtual void AND_RM32_imm8(const X86::Instruction&) override; + virtual void AND_RM32_reg32(const X86::Instruction&) override; + virtual void AND_RM8_imm8(const X86::Instruction&) override; + virtual void AND_RM8_reg8(const X86::Instruction&) override; + virtual void AND_reg16_RM16(const X86::Instruction&) override; + virtual void AND_reg32_RM32(const X86::Instruction&) override; + virtual void AND_reg8_RM8(const X86::Instruction&) override; + virtual void ARPL(const X86::Instruction&) override; + virtual void BOUND(const X86::Instruction&) override; + virtual void BSF_reg16_RM16(const X86::Instruction&) override; + virtual void BSF_reg32_RM32(const X86::Instruction&) override; + virtual void BSR_reg16_RM16(const X86::Instruction&) override; + virtual void BSR_reg32_RM32(const X86::Instruction&) override; + virtual void BSWAP_reg32(const X86::Instruction&) override; + virtual void BTC_RM16_imm8(const X86::Instruction&) override; + virtual void BTC_RM16_reg16(const X86::Instruction&) override; + virtual void BTC_RM32_imm8(const X86::Instruction&) override; + virtual void BTC_RM32_reg32(const X86::Instruction&) override; + virtual void BTR_RM16_imm8(const X86::Instruction&) override; + virtual void BTR_RM16_reg16(const X86::Instruction&) override; + virtual void BTR_RM32_imm8(const X86::Instruction&) override; + virtual void BTR_RM32_reg32(const X86::Instruction&) override; + virtual void BTS_RM16_imm8(const X86::Instruction&) override; + virtual void BTS_RM16_reg16(const X86::Instruction&) override; + virtual void BTS_RM32_imm8(const X86::Instruction&) override; + virtual void BTS_RM32_reg32(const X86::Instruction&) override; + virtual void BT_RM16_imm8(const X86::Instruction&) override; + virtual void BT_RM16_reg16(const X86::Instruction&) override; + virtual void BT_RM32_imm8(const X86::Instruction&) override; + virtual void BT_RM32_reg32(const X86::Instruction&) override; + virtual void CALL_FAR_mem16(const X86::Instruction&) override; + virtual void CALL_FAR_mem32(const X86::Instruction&) override; + virtual void CALL_RM16(const X86::Instruction&) override; + virtual void CALL_RM32(const X86::Instruction&) override; + virtual void CALL_imm16(const X86::Instruction&) override; + virtual void CALL_imm16_imm16(const X86::Instruction&) override; + virtual void CALL_imm16_imm32(const X86::Instruction&) override; + virtual void CALL_imm32(const X86::Instruction&) override; + virtual void CBW(const X86::Instruction&) override; + virtual void CDQ(const X86::Instruction&) override; + virtual void CLC(const X86::Instruction&) override; + virtual void CLD(const X86::Instruction&) override; + virtual void CLI(const X86::Instruction&) override; + virtual void CLTS(const X86::Instruction&) override; + virtual void CMC(const X86::Instruction&) override; + virtual void CMOVcc_reg16_RM16(const X86::Instruction&) override; + virtual void CMOVcc_reg32_RM32(const X86::Instruction&) override; + virtual void CMPSB(const X86::Instruction&) override; + virtual void CMPSD(const X86::Instruction&) override; + virtual void CMPSW(const X86::Instruction&) override; + virtual void CMPXCHG_RM16_reg16(const X86::Instruction&) override; + virtual void CMPXCHG_RM32_reg32(const X86::Instruction&) override; + virtual void CMPXCHG_RM8_reg8(const X86::Instruction&) override; + virtual void CMP_AL_imm8(const X86::Instruction&) override; + virtual void CMP_AX_imm16(const X86::Instruction&) override; + virtual void CMP_EAX_imm32(const X86::Instruction&) override; + virtual void CMP_RM16_imm16(const X86::Instruction&) override; + virtual void CMP_RM16_imm8(const X86::Instruction&) override; + virtual void CMP_RM16_reg16(const X86::Instruction&) override; + virtual void CMP_RM32_imm32(const X86::Instruction&) override; + virtual void CMP_RM32_imm8(const X86::Instruction&) override; + virtual void CMP_RM32_reg32(const X86::Instruction&) override; + virtual void CMP_RM8_imm8(const X86::Instruction&) override; + virtual void CMP_RM8_reg8(const X86::Instruction&) override; + virtual void CMP_reg16_RM16(const X86::Instruction&) override; + virtual void CMP_reg32_RM32(const X86::Instruction&) override; + virtual void CMP_reg8_RM8(const X86::Instruction&) override; + virtual void CPUID(const X86::Instruction&) override; + virtual void CWD(const X86::Instruction&) override; + virtual void CWDE(const X86::Instruction&) override; + virtual void DAA(const X86::Instruction&) override; + virtual void DAS(const X86::Instruction&) override; + virtual void DEC_RM16(const X86::Instruction&) override; + virtual void DEC_RM32(const X86::Instruction&) override; + virtual void DEC_RM8(const X86::Instruction&) override; + virtual void DEC_reg16(const X86::Instruction&) override; + virtual void DEC_reg32(const X86::Instruction&) override; + virtual void DIV_RM16(const X86::Instruction&) override; + virtual void DIV_RM32(const X86::Instruction&) override; + virtual void DIV_RM8(const X86::Instruction&) override; + virtual void ENTER16(const X86::Instruction&) override; + virtual void ENTER32(const X86::Instruction&) override; + virtual void ESCAPE(const X86::Instruction&) override; + virtual void FADD_RM32(const X86::Instruction&) override; + virtual void FMUL_RM32(const X86::Instruction&) override; + virtual void FCOM_RM32(const X86::Instruction&) override; + virtual void FCOMP_RM32(const X86::Instruction&) override; + virtual void FSUB_RM32(const X86::Instruction&) override; + virtual void FSUBR_RM32(const X86::Instruction&) override; + virtual void FDIV_RM32(const X86::Instruction&) override; + virtual void FDIVR_RM32(const X86::Instruction&) override; + virtual void FLD_RM32(const X86::Instruction&) override; + virtual void FXCH(const X86::Instruction&) override; + virtual void FST_RM32(const X86::Instruction&) override; + virtual void FNOP(const X86::Instruction&) override; + virtual void FSTP_RM32(const X86::Instruction&) override; + virtual void FLDENV(const X86::Instruction&) override; + virtual void FCHS(const X86::Instruction&) override; + virtual void FABS(const X86::Instruction&) override; + virtual void FTST(const X86::Instruction&) override; + virtual void FXAM(const X86::Instruction&) override; + virtual void FLDCW(const X86::Instruction&) override; + virtual void FLD1(const X86::Instruction&) override; + virtual void FLDL2T(const X86::Instruction&) override; + virtual void FLDL2E(const X86::Instruction&) override; + virtual void FLDPI(const X86::Instruction&) override; + virtual void FLDLG2(const X86::Instruction&) override; + virtual void FLDLN2(const X86::Instruction&) override; + virtual void FLDZ(const X86::Instruction&) override; + virtual void FNSTENV(const X86::Instruction&) override; + virtual void F2XM1(const X86::Instruction&) override; + virtual void FYL2X(const X86::Instruction&) override; + virtual void FPTAN(const X86::Instruction&) override; + virtual void FPATAN(const X86::Instruction&) override; + virtual void FXTRACT(const X86::Instruction&) override; + virtual void FPREM1(const X86::Instruction&) override; + virtual void FDECSTP(const X86::Instruction&) override; + virtual void FINCSTP(const X86::Instruction&) override; + virtual void FNSTCW(const X86::Instruction&) override; + virtual void FPREM(const X86::Instruction&) override; + virtual void FYL2XP1(const X86::Instruction&) override; + virtual void FSQRT(const X86::Instruction&) override; + virtual void FSINCOS(const X86::Instruction&) override; + virtual void FRNDINT(const X86::Instruction&) override; + virtual void FSCALE(const X86::Instruction&) override; + virtual void FSIN(const X86::Instruction&) override; + virtual void FCOS(const X86::Instruction&) override; + virtual void FIADD_RM32(const X86::Instruction&) override; + virtual void FCMOVB(const X86::Instruction&) override; + virtual void FIMUL_RM32(const X86::Instruction&) override; + virtual void FCMOVE(const X86::Instruction&) override; + virtual void FICOM_RM32(const X86::Instruction&) override; + virtual void FCMOVBE(const X86::Instruction&) override; + virtual void FICOMP_RM32(const X86::Instruction&) override; + virtual void FCMOVU(const X86::Instruction&) override; + virtual void FISUB_RM32(const X86::Instruction&) override; + virtual void FISUBR_RM32(const X86::Instruction&) override; + virtual void FUCOMPP(const X86::Instruction&) override; + virtual void FIDIV_RM32(const X86::Instruction&) override; + virtual void FIDIVR_RM32(const X86::Instruction&) override; + virtual void FILD_RM32(const X86::Instruction&) override; + virtual void FCMOVNB(const X86::Instruction&) override; + virtual void FISTTP_RM32(const X86::Instruction&) override; + virtual void FCMOVNE(const X86::Instruction&) override; + virtual void FIST_RM32(const X86::Instruction&) override; + virtual void FCMOVNBE(const X86::Instruction&) override; + virtual void FISTP_RM32(const X86::Instruction&) override; + virtual void FCMOVNU(const X86::Instruction&) override; + virtual void FNENI(const X86::Instruction&) override; + virtual void FNDISI(const X86::Instruction&) override; + virtual void FNCLEX(const X86::Instruction&) override; + virtual void FNINIT(const X86::Instruction&) override; + virtual void FNSETPM(const X86::Instruction&) override; + virtual void FLD_RM80(const X86::Instruction&) override; + virtual void FUCOMI(const X86::Instruction&) override; + virtual void FCOMI(const X86::Instruction&) override; + virtual void FSTP_RM80(const X86::Instruction&) override; + virtual void FADD_RM64(const X86::Instruction&) override; + virtual void FMUL_RM64(const X86::Instruction&) override; + virtual void FCOM_RM64(const X86::Instruction&) override; + virtual void FCOMP_RM64(const X86::Instruction&) override; + virtual void FSUB_RM64(const X86::Instruction&) override; + virtual void FSUBR_RM64(const X86::Instruction&) override; + virtual void FDIV_RM64(const X86::Instruction&) override; + virtual void FDIVR_RM64(const X86::Instruction&) override; + virtual void FLD_RM64(const X86::Instruction&) override; + virtual void FFREE(const X86::Instruction&) override; + virtual void FISTTP_RM64(const X86::Instruction&) override; + virtual void FST_RM64(const X86::Instruction&) override; + virtual void FSTP_RM64(const X86::Instruction&) override; + virtual void FRSTOR(const X86::Instruction&) override; + virtual void FUCOM(const X86::Instruction&) override; + virtual void FUCOMP(const X86::Instruction&) override; + virtual void FNSAVE(const X86::Instruction&) override; + virtual void FNSTSW(const X86::Instruction&) override; + virtual void FIADD_RM16(const X86::Instruction&) override; + virtual void FADDP(const X86::Instruction&) override; + virtual void FIMUL_RM16(const X86::Instruction&) override; + virtual void FMULP(const X86::Instruction&) override; + virtual void FICOM_RM16(const X86::Instruction&) override; + virtual void FICOMP_RM16(const X86::Instruction&) override; + virtual void FCOMPP(const X86::Instruction&) override; + virtual void FISUB_RM16(const X86::Instruction&) override; + virtual void FSUBRP(const X86::Instruction&) override; + virtual void FISUBR_RM16(const X86::Instruction&) override; + virtual void FSUBP(const X86::Instruction&) override; + virtual void FIDIV_RM16(const X86::Instruction&) override; + virtual void FDIVRP(const X86::Instruction&) override; + virtual void FIDIVR_RM16(const X86::Instruction&) override; + virtual void FDIVP(const X86::Instruction&) override; + virtual void FILD_RM16(const X86::Instruction&) override; + virtual void FFREEP(const X86::Instruction&) override; + virtual void FISTTP_RM16(const X86::Instruction&) override; + virtual void FIST_RM16(const X86::Instruction&) override; + virtual void FISTP_RM16(const X86::Instruction&) override; + virtual void FBLD_M80(const X86::Instruction&) override; + virtual void FNSTSW_AX(const X86::Instruction&) override; + virtual void FILD_RM64(const X86::Instruction&) override; + virtual void FUCOMIP(const X86::Instruction&) override; + virtual void FBSTP_M80(const X86::Instruction&) override; + virtual void FCOMIP(const X86::Instruction&) override; + virtual void FISTP_RM64(const X86::Instruction&) override; + virtual void HLT(const X86::Instruction&) override; + virtual void IDIV_RM16(const X86::Instruction&) override; + virtual void IDIV_RM32(const X86::Instruction&) override; + virtual void IDIV_RM8(const X86::Instruction&) override; + virtual void IMUL_RM16(const X86::Instruction&) override; + virtual void IMUL_RM32(const X86::Instruction&) override; + virtual void IMUL_RM8(const X86::Instruction&) override; + virtual void IMUL_reg16_RM16(const X86::Instruction&) override; + virtual void IMUL_reg16_RM16_imm16(const X86::Instruction&) override; + virtual void IMUL_reg16_RM16_imm8(const X86::Instruction&) override; + virtual void IMUL_reg32_RM32(const X86::Instruction&) override; + virtual void IMUL_reg32_RM32_imm32(const X86::Instruction&) override; + virtual void IMUL_reg32_RM32_imm8(const X86::Instruction&) override; + virtual void INC_RM16(const X86::Instruction&) override; + virtual void INC_RM32(const X86::Instruction&) override; + virtual void INC_RM8(const X86::Instruction&) override; + virtual void INC_reg16(const X86::Instruction&) override; + virtual void INC_reg32(const X86::Instruction&) override; + virtual void INSB(const X86::Instruction&) override; + virtual void INSD(const X86::Instruction&) override; + virtual void INSW(const X86::Instruction&) override; + virtual void INT3(const X86::Instruction&) override; + virtual void INTO(const X86::Instruction&) override; + virtual void INT_imm8(const X86::Instruction&) override; + virtual void INVLPG(const X86::Instruction&) override; + virtual void IN_AL_DX(const X86::Instruction&) override; + virtual void IN_AL_imm8(const X86::Instruction&) override; + virtual void IN_AX_DX(const X86::Instruction&) override; + virtual void IN_AX_imm8(const X86::Instruction&) override; + virtual void IN_EAX_DX(const X86::Instruction&) override; + virtual void IN_EAX_imm8(const X86::Instruction&) override; + virtual void IRET(const X86::Instruction&) override; + virtual void JCXZ_imm8(const X86::Instruction&) override; + virtual void JMP_FAR_mem16(const X86::Instruction&) override; + virtual void JMP_FAR_mem32(const X86::Instruction&) override; + virtual void JMP_RM16(const X86::Instruction&) override; + virtual void JMP_RM32(const X86::Instruction&) override; + virtual void JMP_imm16(const X86::Instruction&) override; + virtual void JMP_imm16_imm16(const X86::Instruction&) override; + virtual void JMP_imm16_imm32(const X86::Instruction&) override; + virtual void JMP_imm32(const X86::Instruction&) override; + virtual void JMP_short_imm8(const X86::Instruction&) override; + virtual void Jcc_NEAR_imm(const X86::Instruction&) override; + virtual void Jcc_imm8(const X86::Instruction&) override; + virtual void LAHF(const X86::Instruction&) override; + virtual void LAR_reg16_RM16(const X86::Instruction&) override; + virtual void LAR_reg32_RM32(const X86::Instruction&) override; + virtual void LDS_reg16_mem16(const X86::Instruction&) override; + virtual void LDS_reg32_mem32(const X86::Instruction&) override; + virtual void LEAVE16(const X86::Instruction&) override; + virtual void LEAVE32(const X86::Instruction&) override; + virtual void LEA_reg16_mem16(const X86::Instruction&) override; + virtual void LEA_reg32_mem32(const X86::Instruction&) override; + virtual void LES_reg16_mem16(const X86::Instruction&) override; + virtual void LES_reg32_mem32(const X86::Instruction&) override; + virtual void LFS_reg16_mem16(const X86::Instruction&) override; + virtual void LFS_reg32_mem32(const X86::Instruction&) override; + virtual void LGDT(const X86::Instruction&) override; + virtual void LGS_reg16_mem16(const X86::Instruction&) override; + virtual void LGS_reg32_mem32(const X86::Instruction&) override; + virtual void LIDT(const X86::Instruction&) override; + virtual void LLDT_RM16(const X86::Instruction&) override; + virtual void LMSW_RM16(const X86::Instruction&) override; + virtual void LODSB(const X86::Instruction&) override; + virtual void LODSD(const X86::Instruction&) override; + virtual void LODSW(const X86::Instruction&) override; + virtual void LOOPNZ_imm8(const X86::Instruction&) override; + virtual void LOOPZ_imm8(const X86::Instruction&) override; + virtual void LOOP_imm8(const X86::Instruction&) override; + virtual void LSL_reg16_RM16(const X86::Instruction&) override; + virtual void LSL_reg32_RM32(const X86::Instruction&) override; + virtual void LSS_reg16_mem16(const X86::Instruction&) override; + virtual void LSS_reg32_mem32(const X86::Instruction&) override; + virtual void LTR_RM16(const X86::Instruction&) override; + virtual void MOVSB(const X86::Instruction&) override; + virtual void MOVSD(const X86::Instruction&) override; + virtual void MOVSW(const X86::Instruction&) override; + virtual void MOVSX_reg16_RM8(const X86::Instruction&) override; + virtual void MOVSX_reg32_RM16(const X86::Instruction&) override; + virtual void MOVSX_reg32_RM8(const X86::Instruction&) override; + virtual void MOVZX_reg16_RM8(const X86::Instruction&) override; + virtual void MOVZX_reg32_RM16(const X86::Instruction&) override; + virtual void MOVZX_reg32_RM8(const X86::Instruction&) override; + virtual void MOV_AL_moff8(const X86::Instruction&) override; + virtual void MOV_AX_moff16(const X86::Instruction&) override; + virtual void MOV_CR_reg32(const X86::Instruction&) override; + virtual void MOV_DR_reg32(const X86::Instruction&) override; + virtual void MOV_EAX_moff32(const X86::Instruction&) override; + virtual void MOV_RM16_imm16(const X86::Instruction&) override; + virtual void MOV_RM16_reg16(const X86::Instruction&) override; + virtual void MOV_RM16_seg(const X86::Instruction&) override; + virtual void MOV_RM32_imm32(const X86::Instruction&) override; + virtual void MOV_RM32_reg32(const X86::Instruction&) override; + virtual void MOV_RM8_imm8(const X86::Instruction&) override; + virtual void MOV_RM8_reg8(const X86::Instruction&) override; + virtual void MOV_moff16_AX(const X86::Instruction&) override; + virtual void MOV_moff32_EAX(const X86::Instruction&) override; + virtual void MOV_moff8_AL(const X86::Instruction&) override; + virtual void MOV_reg16_RM16(const X86::Instruction&) override; + virtual void MOV_reg16_imm16(const X86::Instruction&) override; + virtual void MOV_reg32_CR(const X86::Instruction&) override; + virtual void MOV_reg32_DR(const X86::Instruction&) override; + virtual void MOV_reg32_RM32(const X86::Instruction&) override; + virtual void MOV_reg32_imm32(const X86::Instruction&) override; + virtual void MOV_reg8_RM8(const X86::Instruction&) override; + virtual void MOV_reg8_imm8(const X86::Instruction&) override; + virtual void MOV_seg_RM16(const X86::Instruction&) override; + virtual void MOV_seg_RM32(const X86::Instruction&) override; + virtual void MUL_RM16(const X86::Instruction&) override; + virtual void MUL_RM32(const X86::Instruction&) override; + virtual void MUL_RM8(const X86::Instruction&) override; + virtual void NEG_RM16(const X86::Instruction&) override; + virtual void NEG_RM32(const X86::Instruction&) override; + virtual void NEG_RM8(const X86::Instruction&) override; + virtual void NOP(const X86::Instruction&) override; + virtual void NOT_RM16(const X86::Instruction&) override; + virtual void NOT_RM32(const X86::Instruction&) override; + virtual void NOT_RM8(const X86::Instruction&) override; + virtual void OR_AL_imm8(const X86::Instruction&) override; + virtual void OR_AX_imm16(const X86::Instruction&) override; + virtual void OR_EAX_imm32(const X86::Instruction&) override; + virtual void OR_RM16_imm16(const X86::Instruction&) override; + virtual void OR_RM16_imm8(const X86::Instruction&) override; + virtual void OR_RM16_reg16(const X86::Instruction&) override; + virtual void OR_RM32_imm32(const X86::Instruction&) override; + virtual void OR_RM32_imm8(const X86::Instruction&) override; + virtual void OR_RM32_reg32(const X86::Instruction&) override; + virtual void OR_RM8_imm8(const X86::Instruction&) override; + virtual void OR_RM8_reg8(const X86::Instruction&) override; + virtual void OR_reg16_RM16(const X86::Instruction&) override; + virtual void OR_reg32_RM32(const X86::Instruction&) override; + virtual void OR_reg8_RM8(const X86::Instruction&) override; + virtual void OUTSB(const X86::Instruction&) override; + virtual void OUTSD(const X86::Instruction&) override; + virtual void OUTSW(const X86::Instruction&) override; + virtual void OUT_DX_AL(const X86::Instruction&) override; + virtual void OUT_DX_AX(const X86::Instruction&) override; + virtual void OUT_DX_EAX(const X86::Instruction&) override; + virtual void OUT_imm8_AL(const X86::Instruction&) override; + virtual void OUT_imm8_AX(const X86::Instruction&) override; + virtual void OUT_imm8_EAX(const X86::Instruction&) override; + virtual void PADDB_mm1_mm2m64(const X86::Instruction&) override; + virtual void PADDW_mm1_mm2m64(const X86::Instruction&) override; + virtual void PADDD_mm1_mm2m64(const X86::Instruction&) override; + virtual void POPA(const X86::Instruction&) override; + virtual void POPAD(const X86::Instruction&) override; + virtual void POPF(const X86::Instruction&) override; + virtual void POPFD(const X86::Instruction&) override; + virtual void POP_DS(const X86::Instruction&) override; + virtual void POP_ES(const X86::Instruction&) override; + virtual void POP_FS(const X86::Instruction&) override; + virtual void POP_GS(const X86::Instruction&) override; + virtual void POP_RM16(const X86::Instruction&) override; + virtual void POP_RM32(const X86::Instruction&) override; + virtual void POP_SS(const X86::Instruction&) override; + virtual void POP_reg16(const X86::Instruction&) override; + virtual void POP_reg32(const X86::Instruction&) override; + virtual void PUSHA(const X86::Instruction&) override; + virtual void PUSHAD(const X86::Instruction&) override; + virtual void PUSHF(const X86::Instruction&) override; + virtual void PUSHFD(const X86::Instruction&) override; + virtual void PUSH_CS(const X86::Instruction&) override; + virtual void PUSH_DS(const X86::Instruction&) override; + virtual void PUSH_ES(const X86::Instruction&) override; + virtual void PUSH_FS(const X86::Instruction&) override; + virtual void PUSH_GS(const X86::Instruction&) override; + virtual void PUSH_RM16(const X86::Instruction&) override; + virtual void PUSH_RM32(const X86::Instruction&) override; + virtual void PUSH_SP_8086_80186(const X86::Instruction&) override; + virtual void PUSH_SS(const X86::Instruction&) override; + virtual void PUSH_imm16(const X86::Instruction&) override; + virtual void PUSH_imm32(const X86::Instruction&) override; + virtual void PUSH_imm8(const X86::Instruction&) override; + virtual void PUSH_reg16(const X86::Instruction&) override; + virtual void PUSH_reg32(const X86::Instruction&) override; + virtual void RCL_RM16_1(const X86::Instruction&) override; + virtual void RCL_RM16_CL(const X86::Instruction&) override; + virtual void RCL_RM16_imm8(const X86::Instruction&) override; + virtual void RCL_RM32_1(const X86::Instruction&) override; + virtual void RCL_RM32_CL(const X86::Instruction&) override; + virtual void RCL_RM32_imm8(const X86::Instruction&) override; + virtual void RCL_RM8_1(const X86::Instruction&) override; + virtual void RCL_RM8_CL(const X86::Instruction&) override; + virtual void RCL_RM8_imm8(const X86::Instruction&) override; + virtual void RCR_RM16_1(const X86::Instruction&) override; + virtual void RCR_RM16_CL(const X86::Instruction&) override; + virtual void RCR_RM16_imm8(const X86::Instruction&) override; + virtual void RCR_RM32_1(const X86::Instruction&) override; + virtual void RCR_RM32_CL(const X86::Instruction&) override; + virtual void RCR_RM32_imm8(const X86::Instruction&) override; + virtual void RCR_RM8_1(const X86::Instruction&) override; + virtual void RCR_RM8_CL(const X86::Instruction&) override; + virtual void RCR_RM8_imm8(const X86::Instruction&) override; + virtual void RDTSC(const X86::Instruction&) override; + virtual void RET(const X86::Instruction&) override; + virtual void RETF(const X86::Instruction&) override; + virtual void RETF_imm16(const X86::Instruction&) override; + virtual void RET_imm16(const X86::Instruction&) override; + virtual void ROL_RM16_1(const X86::Instruction&) override; + virtual void ROL_RM16_CL(const X86::Instruction&) override; + virtual void ROL_RM16_imm8(const X86::Instruction&) override; + virtual void ROL_RM32_1(const X86::Instruction&) override; + virtual void ROL_RM32_CL(const X86::Instruction&) override; + virtual void ROL_RM32_imm8(const X86::Instruction&) override; + virtual void ROL_RM8_1(const X86::Instruction&) override; + virtual void ROL_RM8_CL(const X86::Instruction&) override; + virtual void ROL_RM8_imm8(const X86::Instruction&) override; + virtual void ROR_RM16_1(const X86::Instruction&) override; + virtual void ROR_RM16_CL(const X86::Instruction&) override; + virtual void ROR_RM16_imm8(const X86::Instruction&) override; + virtual void ROR_RM32_1(const X86::Instruction&) override; + virtual void ROR_RM32_CL(const X86::Instruction&) override; + virtual void ROR_RM32_imm8(const X86::Instruction&) override; + virtual void ROR_RM8_1(const X86::Instruction&) override; + virtual void ROR_RM8_CL(const X86::Instruction&) override; + virtual void ROR_RM8_imm8(const X86::Instruction&) override; + virtual void SAHF(const X86::Instruction&) override; + virtual void SALC(const X86::Instruction&) override; + virtual void SAR_RM16_1(const X86::Instruction&) override; + virtual void SAR_RM16_CL(const X86::Instruction&) override; + virtual void SAR_RM16_imm8(const X86::Instruction&) override; + virtual void SAR_RM32_1(const X86::Instruction&) override; + virtual void SAR_RM32_CL(const X86::Instruction&) override; + virtual void SAR_RM32_imm8(const X86::Instruction&) override; + virtual void SAR_RM8_1(const X86::Instruction&) override; + virtual void SAR_RM8_CL(const X86::Instruction&) override; + virtual void SAR_RM8_imm8(const X86::Instruction&) override; + virtual void SBB_AL_imm8(const X86::Instruction&) override; + virtual void SBB_AX_imm16(const X86::Instruction&) override; + virtual void SBB_EAX_imm32(const X86::Instruction&) override; + virtual void SBB_RM16_imm16(const X86::Instruction&) override; + virtual void SBB_RM16_imm8(const X86::Instruction&) override; + virtual void SBB_RM16_reg16(const X86::Instruction&) override; + virtual void SBB_RM32_imm32(const X86::Instruction&) override; + virtual void SBB_RM32_imm8(const X86::Instruction&) override; + virtual void SBB_RM32_reg32(const X86::Instruction&) override; + virtual void SBB_RM8_imm8(const X86::Instruction&) override; + virtual void SBB_RM8_reg8(const X86::Instruction&) override; + virtual void SBB_reg16_RM16(const X86::Instruction&) override; + virtual void SBB_reg32_RM32(const X86::Instruction&) override; + virtual void SBB_reg8_RM8(const X86::Instruction&) override; + virtual void SCASB(const X86::Instruction&) override; + virtual void SCASD(const X86::Instruction&) override; + virtual void SCASW(const X86::Instruction&) override; + virtual void SETcc_RM8(const X86::Instruction&) override; + virtual void SGDT(const X86::Instruction&) override; + virtual void SHLD_RM16_reg16_CL(const X86::Instruction&) override; + virtual void SHLD_RM16_reg16_imm8(const X86::Instruction&) override; + virtual void SHLD_RM32_reg32_CL(const X86::Instruction&) override; + virtual void SHLD_RM32_reg32_imm8(const X86::Instruction&) override; + virtual void SHL_RM16_1(const X86::Instruction&) override; + virtual void SHL_RM16_CL(const X86::Instruction&) override; + virtual void SHL_RM16_imm8(const X86::Instruction&) override; + virtual void SHL_RM32_1(const X86::Instruction&) override; + virtual void SHL_RM32_CL(const X86::Instruction&) override; + virtual void SHL_RM32_imm8(const X86::Instruction&) override; + virtual void SHL_RM8_1(const X86::Instruction&) override; + virtual void SHL_RM8_CL(const X86::Instruction&) override; + virtual void SHL_RM8_imm8(const X86::Instruction&) override; + virtual void SHRD_RM16_reg16_CL(const X86::Instruction&) override; + virtual void SHRD_RM16_reg16_imm8(const X86::Instruction&) override; + virtual void SHRD_RM32_reg32_CL(const X86::Instruction&) override; + virtual void SHRD_RM32_reg32_imm8(const X86::Instruction&) override; + virtual void SHR_RM16_1(const X86::Instruction&) override; + virtual void SHR_RM16_CL(const X86::Instruction&) override; + virtual void SHR_RM16_imm8(const X86::Instruction&) override; + virtual void SHR_RM32_1(const X86::Instruction&) override; + virtual void SHR_RM32_CL(const X86::Instruction&) override; + virtual void SHR_RM32_imm8(const X86::Instruction&) override; + virtual void SHR_RM8_1(const X86::Instruction&) override; + virtual void SHR_RM8_CL(const X86::Instruction&) override; + virtual void SHR_RM8_imm8(const X86::Instruction&) override; + virtual void SIDT(const X86::Instruction&) override; + virtual void SLDT_RM16(const X86::Instruction&) override; + virtual void SMSW_RM16(const X86::Instruction&) override; + virtual void STC(const X86::Instruction&) override; + virtual void STD(const X86::Instruction&) override; + virtual void STI(const X86::Instruction&) override; + virtual void STOSB(const X86::Instruction&) override; + virtual void STOSD(const X86::Instruction&) override; + virtual void STOSW(const X86::Instruction&) override; + virtual void STR_RM16(const X86::Instruction&) override; + virtual void SUB_AL_imm8(const X86::Instruction&) override; + virtual void SUB_AX_imm16(const X86::Instruction&) override; + virtual void SUB_EAX_imm32(const X86::Instruction&) override; + virtual void SUB_RM16_imm16(const X86::Instruction&) override; + virtual void SUB_RM16_imm8(const X86::Instruction&) override; + virtual void SUB_RM16_reg16(const X86::Instruction&) override; + virtual void SUB_RM32_imm32(const X86::Instruction&) override; + virtual void SUB_RM32_imm8(const X86::Instruction&) override; + virtual void SUB_RM32_reg32(const X86::Instruction&) override; + virtual void SUB_RM8_imm8(const X86::Instruction&) override; + virtual void SUB_RM8_reg8(const X86::Instruction&) override; + virtual void SUB_reg16_RM16(const X86::Instruction&) override; + virtual void SUB_reg32_RM32(const X86::Instruction&) override; + virtual void SUB_reg8_RM8(const X86::Instruction&) override; + virtual void TEST_AL_imm8(const X86::Instruction&) override; + virtual void TEST_AX_imm16(const X86::Instruction&) override; + virtual void TEST_EAX_imm32(const X86::Instruction&) override; + virtual void TEST_RM16_imm16(const X86::Instruction&) override; + virtual void TEST_RM16_reg16(const X86::Instruction&) override; + virtual void TEST_RM32_imm32(const X86::Instruction&) override; + virtual void TEST_RM32_reg32(const X86::Instruction&) override; + virtual void TEST_RM8_imm8(const X86::Instruction&) override; + virtual void TEST_RM8_reg8(const X86::Instruction&) override; + virtual void UD0(const X86::Instruction&) override; + virtual void UD1(const X86::Instruction&) override; + virtual void UD2(const X86::Instruction&) override; + virtual void VERR_RM16(const X86::Instruction&) override; + virtual void VERW_RM16(const X86::Instruction&) override; + virtual void WAIT(const X86::Instruction&) override; + virtual void WBINVD(const X86::Instruction&) override; + virtual void XADD_RM16_reg16(const X86::Instruction&) override; + virtual void XADD_RM32_reg32(const X86::Instruction&) override; + virtual void XADD_RM8_reg8(const X86::Instruction&) override; + virtual void XCHG_AX_reg16(const X86::Instruction&) override; + virtual void XCHG_EAX_reg32(const X86::Instruction&) override; + virtual void XCHG_reg16_RM16(const X86::Instruction&) override; + virtual void XCHG_reg32_RM32(const X86::Instruction&) override; + virtual void XCHG_reg8_RM8(const X86::Instruction&) override; + virtual void XLAT(const X86::Instruction&) override; + virtual void XOR_AL_imm8(const X86::Instruction&) override; + virtual void XOR_AX_imm16(const X86::Instruction&) override; + virtual void XOR_EAX_imm32(const X86::Instruction&) override; + virtual void XOR_RM16_imm16(const X86::Instruction&) override; + virtual void XOR_RM16_imm8(const X86::Instruction&) override; + virtual void XOR_RM16_reg16(const X86::Instruction&) override; + virtual void XOR_RM32_imm32(const X86::Instruction&) override; + virtual void XOR_RM32_imm8(const X86::Instruction&) override; + virtual void XOR_RM32_reg32(const X86::Instruction&) override; + virtual void XOR_RM8_imm8(const X86::Instruction&) override; + virtual void XOR_RM8_reg8(const X86::Instruction&) override; + virtual void XOR_reg16_RM16(const X86::Instruction&) override; + virtual void XOR_reg32_RM32(const X86::Instruction&) override; + virtual void XOR_reg8_RM8(const X86::Instruction&) override; + virtual void MOVQ_mm1_mm2m64(const X86::Instruction&) override; + virtual void EMMS(const X86::Instruction&) override; + virtual void MOVQ_mm1_m64_mm2(const X86::Instruction&) override; + virtual void wrap_0xC0(const X86::Instruction&) override; + virtual void wrap_0xC1_16(const X86::Instruction&) override; + virtual void wrap_0xC1_32(const X86::Instruction&) override; + virtual void wrap_0xD0(const X86::Instruction&) override; + virtual void wrap_0xD1_16(const X86::Instruction&) override; + virtual void wrap_0xD1_32(const X86::Instruction&) override; + virtual void wrap_0xD2(const X86::Instruction&) override; + virtual void wrap_0xD3_16(const X86::Instruction&) override; + virtual void wrap_0xD3_32(const X86::Instruction&) override; + + template<bool update_dest, bool is_or, typename Op> + void generic_AL_imm8(Op, const X86::Instruction&); + template<bool update_dest, bool is_or, typename Op> + void generic_AX_imm16(Op, const X86::Instruction&); + template<bool update_dest, bool is_or, typename Op> + void generic_EAX_imm32(Op, const X86::Instruction&); + template<bool update_dest, bool is_or, typename Op> + void generic_RM16_imm16(Op, const X86::Instruction&); + template<bool update_dest, bool is_or, typename Op> + void generic_RM16_imm8(Op, const X86::Instruction&); + template<bool update_dest, typename Op> + void generic_RM16_unsigned_imm8(Op, const X86::Instruction&); + template<bool update_dest, bool is_zero_idiom_if_both_operands_same, typename Op> + void generic_RM16_reg16(Op, const X86::Instruction&); + template<bool update_dest, bool is_or, typename Op> + void generic_RM32_imm32(Op, const X86::Instruction&); + template<bool update_dest, bool is_or, typename Op> + void generic_RM32_imm8(Op, const X86::Instruction&); + template<bool update_dest, typename Op> + void generic_RM32_unsigned_imm8(Op, const X86::Instruction&); + template<bool update_dest, bool is_zero_idiom_if_both_operands_same, typename Op> + void generic_RM32_reg32(Op, const X86::Instruction&); + template<bool update_dest, bool is_or, typename Op> + void generic_RM8_imm8(Op, const X86::Instruction&); + template<bool update_dest, bool is_zero_idiom_if_both_operands_same, typename Op> + void generic_RM8_reg8(Op, const X86::Instruction&); + template<bool update_dest, bool is_zero_idiom_if_both_operands_same, typename Op> + void generic_reg16_RM16(Op, const X86::Instruction&); + template<bool update_dest, bool is_zero_idiom_if_both_operands_same, typename Op> + void generic_reg32_RM32(Op, const X86::Instruction&); + template<bool update_dest, bool is_zero_idiom_if_both_operands_same, typename Op> + void generic_reg8_RM8(Op, const X86::Instruction&); + + template<typename Op> + void generic_RM8_1(Op, const X86::Instruction&); + template<typename Op> + void generic_RM8_CL(Op, const X86::Instruction&); + template<typename Op> + void generic_RM16_1(Op, const X86::Instruction&); + template<typename Op> + void generic_RM16_CL(Op, const X86::Instruction&); + template<typename Op> + void generic_RM32_1(Op, const X86::Instruction&); + template<typename Op> + void generic_RM32_CL(Op, const X86::Instruction&); + + void update_code_cache(); + + void did_receive_secret_data(); + +private: + Emulator& m_emulator; + + PartAddressableRegister m_gpr[8]; + PartAddressableRegister m_gpr_shadow[8]; + + u16 m_segment[8] { 0 }; + u32 m_eflags { 0 }; + + bool m_flags_tainted { false }; + + u32 m_eip { 0 }; + u32 m_base_eip { 0 }; + + long double m_fpu[8]; + // FIXME: Shadow for m_fpu. + + // FIXME: Use bits 11 to 13 in the FPU status word for this. + int m_fpu_top { -1 }; + + void fpu_push(long double n) + { + ++m_fpu_top; + fpu_set(0, n); + } + long double fpu_pop() + { + auto n = fpu_get(0); + m_fpu_top--; + return n; + } + long double fpu_get(int i) + { + ASSERT(i >= 0 && i <= m_fpu_top); + return m_fpu[m_fpu_top - i]; + } + void fpu_set(int i, long double n) + { + ASSERT(i >= 0 && i <= m_fpu_top); + m_fpu[m_fpu_top - i] = n; + } + + // FIXME: Or just something like m_flags_tainted? + ValueWithShadow<u16> m_fpu_cw { 0, 0 }; + + Region* m_cached_code_region { nullptr }; + u8* m_cached_code_base_ptr { nullptr }; + + u32 m_secret_handshake_state { 0 }; + u32 m_secret_data[3]; +}; + +ALWAYS_INLINE u8 SoftCPU::read8() +{ + if (!m_cached_code_region || !m_cached_code_region->contains(m_eip)) + update_code_cache(); + + u8 value = m_cached_code_base_ptr[m_eip - m_cached_code_region->base()]; + m_eip += 1; + return value; +} + +ALWAYS_INLINE u16 SoftCPU::read16() +{ + if (!m_cached_code_region || !m_cached_code_region->contains(m_eip)) + update_code_cache(); + + u16 value = *reinterpret_cast<const u16*>(&m_cached_code_base_ptr[m_eip - m_cached_code_region->base()]); + m_eip += 2; + return value; +} + +ALWAYS_INLINE u32 SoftCPU::read32() +{ + if (!m_cached_code_region || !m_cached_code_region->contains(m_eip)) + update_code_cache(); + + u32 value = *reinterpret_cast<const u32*>(&m_cached_code_base_ptr[m_eip - m_cached_code_region->base()]); + m_eip += 4; + return value; +} + +ALWAYS_INLINE u64 SoftCPU::read64() +{ + if (!m_cached_code_region || !m_cached_code_region->contains(m_eip)) + update_code_cache(); + + auto value = *reinterpret_cast<const u64*>(&m_cached_code_base_ptr[m_eip - m_cached_code_region->base()]); + m_eip += 8; + return value; +} + +} diff --git a/Userland/DevTools/UserspaceEmulator/SoftMMU.cpp b/Userland/DevTools/UserspaceEmulator/SoftMMU.cpp new file mode 100644 index 0000000000..50da131f1c --- /dev/null +++ b/Userland/DevTools/UserspaceEmulator/SoftMMU.cpp @@ -0,0 +1,296 @@ +/* + * Copyright (c) 2020, Andreas Kling <kling@serenityos.org> + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * 1. Redistributions of source code must retain the above copyright notice, this + * list of conditions and the following disclaimer. + * + * 2. Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE + * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL + * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR + * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER + * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, + * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE + * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +#include "SoftMMU.h" +#include "Emulator.h" +#include "MmapRegion.h" +#include "Report.h" +#include "SharedBufferRegion.h" +#include <AK/ByteBuffer.h> +#include <AK/Memory.h> + +namespace UserspaceEmulator { + +SoftMMU::SoftMMU(Emulator& emulator) + : m_emulator(emulator) +{ +} + +void SoftMMU::add_region(NonnullOwnPtr<Region> region) +{ + ASSERT(!find_region({ 0x23, region->base() })); + + // FIXME: More sanity checks pls + if (is<SharedBufferRegion>(*region)) + m_shbuf_regions.set(static_cast<SharedBufferRegion*>(region.ptr())->shbuf_id(), region.ptr()); + + size_t first_page_in_region = region->base() / PAGE_SIZE; + size_t last_page_in_region = (region->base() + region->size() - 1) / PAGE_SIZE; + for (size_t page = first_page_in_region; page <= last_page_in_region; ++page) { + m_page_to_region_map[page] = region.ptr(); + } + + m_regions.append(move(region)); +} + +void SoftMMU::remove_region(Region& region) +{ + size_t first_page_in_region = region.base() / PAGE_SIZE; + for (size_t i = 0; i < ceil_div(region.size(), PAGE_SIZE); ++i) { + m_page_to_region_map[first_page_in_region + i] = nullptr; + } + + if (is<SharedBufferRegion>(region)) + m_shbuf_regions.remove(static_cast<SharedBufferRegion&>(region).shbuf_id()); + m_regions.remove_first_matching([&](auto& entry) { return entry.ptr() == ®ion; }); +} + +void SoftMMU::set_tls_region(NonnullOwnPtr<Region> region) +{ + ASSERT(!m_tls_region); + m_tls_region = move(region); +} + +ValueWithShadow<u8> SoftMMU::read8(X86::LogicalAddress address) +{ + auto* region = find_region(address); + if (!region) { + reportln("SoftMMU::read8: No region for @ {:p}", address.offset()); + m_emulator.dump_backtrace(); + TODO(); + } + + if (!region->is_readable()) { + reportln("SoftMMU::read8: Non-readable region @ {:p}", address.offset()); + m_emulator.dump_backtrace(); + TODO(); + } + + return region->read8(address.offset() - region->base()); +} + +ValueWithShadow<u16> SoftMMU::read16(X86::LogicalAddress address) +{ + auto* region = find_region(address); + if (!region) { + reportln("SoftMMU::read16: No region for @ {:p}", address.offset()); + m_emulator.dump_backtrace(); + TODO(); + } + + if (!region->is_readable()) { + reportln("SoftMMU::read16: Non-readable region @ {:p}", address.offset()); + m_emulator.dump_backtrace(); + TODO(); + } + + return region->read16(address.offset() - region->base()); +} + +ValueWithShadow<u32> SoftMMU::read32(X86::LogicalAddress address) +{ + auto* region = find_region(address); + if (!region) { + reportln("SoftMMU::read32: No region for @ {:04x}:{:p}", address.selector(), address.offset()); + m_emulator.dump_backtrace(); + TODO(); + } + + if (!region->is_readable()) { + reportln("SoftMMU::read32: Non-readable region @ {:p}", address.offset()); + m_emulator.dump_backtrace(); + TODO(); + } + + return region->read32(address.offset() - region->base()); +} + +ValueWithShadow<u64> SoftMMU::read64(X86::LogicalAddress address) +{ + auto* region = find_region(address); + if (!region) { + reportln("SoftMMU::read64: No region for @ {:p}", address.offset()); + m_emulator.dump_backtrace(); + TODO(); + } + + if (!region->is_readable()) { + reportln("SoftMMU::read64: Non-readable region @ {:p}", address.offset()); + m_emulator.dump_backtrace(); + TODO(); + } + + return region->read64(address.offset() - region->base()); +} + +void SoftMMU::write8(X86::LogicalAddress address, ValueWithShadow<u8> value) +{ + auto* region = find_region(address); + if (!region) { + reportln("SoftMMU::write8: No region for @ {:p}", address.offset()); + m_emulator.dump_backtrace(); + TODO(); + } + + if (!region->is_writable()) { + reportln("SoftMMU::write8: Non-writable region @ {:p}", address.offset()); + m_emulator.dump_backtrace(); + TODO(); + } + region->write8(address.offset() - region->base(), value); +} + +void SoftMMU::write16(X86::LogicalAddress address, ValueWithShadow<u16> value) +{ + auto* region = find_region(address); + if (!region) { + reportln("SoftMMU::write16: No region for @ {:p}", address.offset()); + m_emulator.dump_backtrace(); + TODO(); + } + + if (!region->is_writable()) { + reportln("SoftMMU::write16: Non-writable region @ {:p}", address.offset()); + m_emulator.dump_backtrace(); + TODO(); + } + + region->write16(address.offset() - region->base(), value); +} + +void SoftMMU::write32(X86::LogicalAddress address, ValueWithShadow<u32> value) +{ + auto* region = find_region(address); + if (!region) { + reportln("SoftMMU::write32: No region for @ {:p}", address.offset()); + m_emulator.dump_backtrace(); + TODO(); + } + + if (!region->is_writable()) { + reportln("SoftMMU::write32: Non-writable region @ {:p}", address.offset()); + m_emulator.dump_backtrace(); + TODO(); + } + + region->write32(address.offset() - region->base(), value); +} + +void SoftMMU::write64(X86::LogicalAddress address, ValueWithShadow<u64> value) +{ + auto* region = find_region(address); + if (!region) { + reportln("SoftMMU::write64: No region for @ {:p}", address.offset()); + m_emulator.dump_backtrace(); + TODO(); + } + + if (!region->is_writable()) { + reportln("SoftMMU::write64: Non-writable region @ {:p}", address.offset()); + m_emulator.dump_backtrace(); + TODO(); + } + + region->write64(address.offset() - region->base(), value); +} + +void SoftMMU::copy_to_vm(FlatPtr destination, const void* source, size_t size) +{ + // FIXME: We should have a way to preserve the shadow data here as well. + for (size_t i = 0; i < size; ++i) + write8({ 0x23, destination + i }, shadow_wrap_as_initialized(((const u8*)source)[i])); +} + +void SoftMMU::copy_from_vm(void* destination, const FlatPtr source, size_t size) +{ + // FIXME: We should have a way to preserve the shadow data here as well. + for (size_t i = 0; i < size; ++i) + ((u8*)destination)[i] = read8({ 0x23, source + i }).value(); +} + +ByteBuffer SoftMMU::copy_buffer_from_vm(const FlatPtr source, size_t size) +{ + auto buffer = ByteBuffer::create_uninitialized(size); + copy_from_vm(buffer.data(), source, size); + return buffer; +} + +SharedBufferRegion* SoftMMU::shbuf_region(int shbuf_id) +{ + return (SharedBufferRegion*)m_shbuf_regions.get(shbuf_id).value_or(nullptr); +} + +bool SoftMMU::fast_fill_memory8(X86::LogicalAddress address, size_t size, ValueWithShadow<u8> value) +{ + if (!size) + return true; + auto* region = find_region(address); + if (!region) + return false; + if (!region->contains(address.offset() + size - 1)) + return false; + + if (is<MmapRegion>(*region) && static_cast<const MmapRegion&>(*region).is_malloc_block()) { + if (auto* tracer = m_emulator.malloc_tracer()) { + // FIXME: Add a way to audit an entire range of memory instead of looping here! + for (size_t i = 0; i < size; ++i) { + tracer->audit_write(*region, address.offset() + (i * sizeof(u8)), sizeof(u8)); + } + } + } + + size_t offset_in_region = address.offset() - region->base(); + memset(region->data() + offset_in_region, value.value(), size); + memset(region->shadow_data() + offset_in_region, value.shadow(), size); + return true; +} + +bool SoftMMU::fast_fill_memory32(X86::LogicalAddress address, size_t count, ValueWithShadow<u32> value) +{ + if (!count) + return true; + auto* region = find_region(address); + if (!region) + return false; + if (!region->contains(address.offset() + (count * sizeof(u32)) - 1)) + return false; + + if (is<MmapRegion>(*region) && static_cast<const MmapRegion&>(*region).is_malloc_block()) { + if (auto* tracer = m_emulator.malloc_tracer()) { + // FIXME: Add a way to audit an entire range of memory instead of looping here! + for (size_t i = 0; i < count; ++i) { + tracer->audit_write(*region, address.offset() + (i * sizeof(u32)), sizeof(u32)); + } + } + } + + size_t offset_in_region = address.offset() - region->base(); + fast_u32_fill((u32*)(region->data() + offset_in_region), value.value(), count); + fast_u32_fill((u32*)(region->shadow_data() + offset_in_region), value.shadow(), count); + return true; +} + +} diff --git a/Userland/DevTools/UserspaceEmulator/SoftMMU.h b/Userland/DevTools/UserspaceEmulator/SoftMMU.h new file mode 100644 index 0000000000..33351ff57b --- /dev/null +++ b/Userland/DevTools/UserspaceEmulator/SoftMMU.h @@ -0,0 +1,102 @@ +/* + * Copyright (c) 2020, Andreas Kling <kling@serenityos.org> + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * 1. Redistributions of source code must retain the above copyright notice, this + * list of conditions and the following disclaimer. + * + * 2. Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE + * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL + * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR + * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER + * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, + * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE + * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +#pragma once + +#include "Region.h" +#include "ValueWithShadow.h" +#include <AK/HashMap.h> +#include <AK/NonnullOwnPtrVector.h> +#include <AK/OwnPtr.h> +#include <AK/Types.h> +#include <LibX86/Instruction.h> + +namespace UserspaceEmulator { + +class Emulator; +class SharedBufferRegion; + +class SoftMMU { +public: + explicit SoftMMU(Emulator&); + + ValueWithShadow<u8> read8(X86::LogicalAddress); + ValueWithShadow<u16> read16(X86::LogicalAddress); + ValueWithShadow<u32> read32(X86::LogicalAddress); + ValueWithShadow<u64> read64(X86::LogicalAddress); + + void write8(X86::LogicalAddress, ValueWithShadow<u8>); + void write16(X86::LogicalAddress, ValueWithShadow<u16>); + void write32(X86::LogicalAddress, ValueWithShadow<u32>); + void write64(X86::LogicalAddress, ValueWithShadow<u64>); + + ALWAYS_INLINE Region* find_region(X86::LogicalAddress address) + { + if (address.selector() == 0x2b) + return m_tls_region.ptr(); + + size_t page_index = (address.offset() & ~(PAGE_SIZE - 1)) / PAGE_SIZE; + return m_page_to_region_map[page_index]; + } + + void add_region(NonnullOwnPtr<Region>); + void remove_region(Region&); + + void set_tls_region(NonnullOwnPtr<Region>); + + bool fast_fill_memory8(X86::LogicalAddress, size_t size, ValueWithShadow<u8>); + bool fast_fill_memory32(X86::LogicalAddress, size_t size, ValueWithShadow<u32>); + + void copy_to_vm(FlatPtr destination, const void* source, size_t); + void copy_from_vm(void* destination, const FlatPtr source, size_t); + ByteBuffer copy_buffer_from_vm(const FlatPtr source, size_t); + + SharedBufferRegion* shbuf_region(int shbuf_id); + + template<typename Callback> + void for_each_region(Callback callback) + { + if (m_tls_region) { + if (callback(*m_tls_region) == IterationDecision::Break) + return; + } + for (auto& region : m_regions) { + if (callback(region) == IterationDecision::Break) + return; + } + } + +private: + Emulator& m_emulator; + + Region* m_page_to_region_map[786432]; + + OwnPtr<Region> m_tls_region; + NonnullOwnPtrVector<Region> m_regions; + HashMap<int, Region*> m_shbuf_regions; +}; + +} diff --git a/Userland/DevTools/UserspaceEmulator/ValueWithShadow.h b/Userland/DevTools/UserspaceEmulator/ValueWithShadow.h new file mode 100644 index 0000000000..3684482642 --- /dev/null +++ b/Userland/DevTools/UserspaceEmulator/ValueWithShadow.h @@ -0,0 +1,171 @@ +/* + * Copyright (c) 2020, Andreas Kling <kling@serenityos.org> + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * 1. Redistributions of source code must retain the above copyright notice, this + * list of conditions and the following disclaimer. + * + * 2. Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE + * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL + * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR + * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER + * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, + * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE + * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +#include <AK/Format.h> +#include <AK/Platform.h> + +#pragma once + +namespace UserspaceEmulator { + +template<typename T> +class ValueAndShadowReference; + +template<typename T> +class ValueWithShadow { +public: + using ValueType = T; + + ValueWithShadow(T value, T shadow) + : m_value(value) + , m_shadow(shadow) + { + } + + ValueWithShadow(const ValueAndShadowReference<T>&); + + T value() const { return m_value; } + T shadow() const { return m_shadow; } + + bool is_uninitialized() const + { + if constexpr (sizeof(T) == 4) + return (m_shadow & 0x01010101) != 0x01010101; + if constexpr (sizeof(T) == 2) + return (m_shadow & 0x0101) != 0x0101; + if constexpr (sizeof(T) == 1) + return (m_shadow & 0x01) != 0x01; + } + + void set_initialized() + { + if constexpr (sizeof(T) == 4) + m_shadow = 0x01010101; + if constexpr (sizeof(T) == 2) + m_shadow = 0x0101; + if constexpr (sizeof(T) == 1) + m_shadow = 0x01; + } + +private: + T m_value; + T m_shadow; +}; + +template<typename T> +class ValueAndShadowReference { +public: + using ValueType = T; + + ValueAndShadowReference(T& value, T& shadow) + : m_value(value) + , m_shadow(shadow) + { + } + + bool is_uninitialized() const + { + if constexpr (sizeof(T) == 4) + return (m_shadow & 0x01010101) != 0x01010101; + if constexpr (sizeof(T) == 2) + return (m_shadow & 0x0101) != 0x0101; + if constexpr (sizeof(T) == 1) + return (m_shadow & 0x01) != 0x01; + } + + void operator=(const ValueWithShadow<T>&); + + T& value() { return m_value; } + T& shadow() { return m_shadow; } + + const T& value() const { return m_value; } + const T& shadow() const { return m_shadow; } + +private: + T& m_value; + T& m_shadow; +}; + +template<typename T> +ALWAYS_INLINE ValueWithShadow<T> shadow_wrap_as_initialized(T value) +{ + if constexpr (sizeof(T) == 8) + return { value, 0x01010101'01010101LLU }; + if constexpr (sizeof(T) == 4) + return { value, 0x01010101 }; + if constexpr (sizeof(T) == 2) + return { value, 0x0101 }; + if constexpr (sizeof(T) == 1) + return { value, 0x01 }; +} + +template<typename T, typename U> +ALWAYS_INLINE ValueWithShadow<T> shadow_wrap_with_taint_from(T value, const U& taint_a) +{ + if (taint_a.is_uninitialized()) + return { value, 0 }; + return shadow_wrap_as_initialized(value); +} + +template<typename T, typename U, typename V> +ALWAYS_INLINE ValueWithShadow<T> shadow_wrap_with_taint_from(T value, const U& taint_a, const V& taint_b) +{ + if (taint_a.is_uninitialized() || taint_b.is_uninitialized()) + return { value, 0 }; + return shadow_wrap_as_initialized(value); +} + +template<typename T, typename U, typename V, typename X> +ALWAYS_INLINE ValueWithShadow<T> shadow_wrap_with_taint_from(T value, const U& taint_a, const V& taint_b, const X& taint_c) +{ + if (taint_a.is_uninitialized() || taint_b.is_uninitialized() || taint_c.is_uninitialized()) + return { value, 0 }; + return shadow_wrap_as_initialized(value); +} + +template<typename T> +inline ValueWithShadow<T>::ValueWithShadow(const ValueAndShadowReference<T>& other) + : m_value(other.value()) + , m_shadow(other.shadow()) +{ +} + +template<typename T> +inline void ValueAndShadowReference<T>::operator=(const ValueWithShadow<T>& other) +{ + m_value = other.value(); + m_shadow = other.shadow(); +} + +} + +template<typename T> +struct AK::Formatter<UserspaceEmulator::ValueWithShadow<T>> : AK::Formatter<T> { + void format(FormatBuilder& builder, UserspaceEmulator::ValueWithShadow<T> value) + { + return Formatter<T>::format(builder, value.value()); + } +}; diff --git a/Userland/DevTools/UserspaceEmulator/main.cpp b/Userland/DevTools/UserspaceEmulator/main.cpp new file mode 100644 index 0000000000..432c9a4a75 --- /dev/null +++ b/Userland/DevTools/UserspaceEmulator/main.cpp @@ -0,0 +1,81 @@ +/* + * Copyright (c) 2020, Andreas Kling <kling@serenityos.org> + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * 1. Redistributions of source code must retain the above copyright notice, this + * list of conditions and the following disclaimer. + * + * 2. Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE + * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL + * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR + * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER + * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, + * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE + * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +#include "Emulator.h" +#include "SoftCPU.h" +#include <AK/Format.h> +#include <AK/LexicalPath.h> +#include <AK/MappedFile.h> +#include <AK/StringBuilder.h> +#include <LibCore/ArgsParser.h> +#include <LibCore/DirIterator.h> +#include <LibELF/Image.h> +#include <getopt.h> +#include <pthread.h> +#include <string.h> + +bool g_report_to_debug = false; + +int main(int argc, char** argv, char** env) +{ + Vector<const char*> command; + + Core::ArgsParser parser; + parser.add_option(g_report_to_debug, "Write reports to the debug log", "report-to-debug", 0); + parser.add_positional_argument(command, "Command to emulate", "command"); + parser.parse(argc, argv); + + auto executable_path = Core::find_executable_in_path(command[0]); + + Vector<String> arguments; + for (auto arg : command) { + arguments.append(arg); + } + + Vector<String> environment; + for (int i = 0; env[i]; ++i) { + environment.append(env[i]); + } + + // FIXME: It might be nice to tear down the emulator properly. + auto& emulator = *new UserspaceEmulator::Emulator(executable_path, arguments, environment); + if (!emulator.load_elf()) + return 1; + + StringBuilder builder; + builder.append("(UE) "); + builder.append(LexicalPath(arguments[0]).basename()); + if (set_process_name(builder.string_view().characters_without_null_termination(), builder.string_view().length()) < 0) { + perror("set_process_name"); + return 1; + } + int rc = pthread_setname_np(pthread_self(), builder.to_string().characters()); + if (rc != 0) { + reportln("pthread_setname_np: {}", strerror(rc)); + return 1; + } + return emulator.exec(); +} |