backend/wbpublic/grtui/binary_data_editor.cpp (557 lines of code) (raw):

/* * Copyright (c) 2010, 2018, Oracle and/or its affiliates. All rights reserved. * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License, version 2.0, * as published by the Free Software Foundation. * * This program is designed to work with certain software (including * but not limited to OpenSSL) that is licensed under separate terms, as * designated in a particular file or component or in included license * documentation. The authors of MySQL hereby grant you an additional * permission to link the program and your derivative works with the * separately licensed software that they have either included with * the program or referenced in the documentation. * This program is distributed in the hope that it will be useful, but * WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See * the GNU General Public License, version 2.0, for more details. * * You should have received a copy of the GNU General Public License * along with this program; if not, write to the Free Software Foundation, Inc., * 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA */ #include "grt/grt_manager.h" #include "binary_data_editor.h" #include "geom_draw_box.h" #include "grt/spatial_handler.h" #include "base/log.h" #include "base/string_utilities.h" #include <glib/gstdio.h> #ifdef _MSC_VER #include <io.h> #endif DEFAULT_LOG_DOMAIN("BlobViewer"); #include "mforms/scrollpanel.h" #include "mforms/imagebox.h" #include "mforms/textbox.h" #include "mforms/selector.h" #include "mforms/treeview.h" #include "mforms/code_editor.h" #include "mforms/find_panel.h" #include "mforms/filechooser.h" #include "mforms/label.h" BinaryDataViewer::BinaryDataViewer(BinaryDataEditor *owner) : mforms::Box(false), _owner(owner) { } //-------------------------------------------------------------------------------- class ImageDataViewer : public BinaryDataViewer { public: ImageDataViewer(BinaryDataEditor *owner, bool read_only) : BinaryDataViewer(owner), _scroll(mforms::ScrollPanelNoFlags) { _image.set_scale_contents(false); add(&_scroll, true, true); _scroll.add(&_image); } virtual void data_changed() { _image.set_image_data(_owner->data(), _owner->length()); } static bool can_display(const char *data, size_t length) { if (length > 4) { if (data[0] == (char)0x89 && strncmp(data + 1, "PNG", 3) == 0) return true; if (data[0] == (char)0xff && data[1] == (char)0xd8) // jpeg return true; if (strncmp(data, "BM", 2) == 0) // bmp return true; if (strncmp(data, "GIF", 3) == 0) return true; if ((strncmp(data, "II", 2) == 0 || strncmp(data, "MM", 2) == 0) && data[2] == 42) // tiff return true; } return false; } private: mforms::ScrollPanel _scroll; mforms::ImageBox _image; }; //-------------------------------------------------------------------------------- class HexDataViewer : public BinaryDataViewer { public: HexDataViewer(BinaryDataEditor *owner, bool read_only) : BinaryDataViewer(owner), _tree(mforms::TreeShowColumnLines | mforms::TreeShowRowLines | mforms::TreeFlatList), _box(true) { _offset = 0; _block_size = 8 * 1024; add(&_tree, true, true); add(&_box, false, true); _box.set_spacing(8); _box.add(&_first, false, true); _box.add(&_back, false, true); _box.add(&_next, false, true); _box.add(&_last, false, true); _box.add(&_label, true, true); _label.set_text("Viewing Range 0 to 16KB"); _first.set_text("<< First"); _back.set_text("< Previous"); _next.set_text("Next >"); _last.set_text("Last >>"); scoped_connect(_first.signal_clicked(), std::bind(&HexDataViewer::go, this, -2)); scoped_connect(_back.signal_clicked(), std::bind(&HexDataViewer::go, this, -1)); scoped_connect(_next.signal_clicked(), std::bind(&HexDataViewer::go, this, 1)); scoped_connect(_last.signal_clicked(), std::bind(&HexDataViewer::go, this, 2)); _tree.add_column(mforms::StringColumnType, "Offset", 100, true); for (int i = 0; i < 16; i++) _tree.add_column(mforms::StringColumnType, base::strfmt("%X", i), 25, !read_only); _tree.end_columns(); _tree.set_cell_edit_handler(std::bind(&HexDataViewer::set_cell_value, this, std::placeholders::_1, std::placeholders::_2, std::placeholders::_3)); } virtual void data_changed() { if (_offset >= _owner->length()) _offset = (_owner->length() / _block_size) * _block_size; refresh(); } void go(int step) { switch (step) { case -2: _offset = 0; break; case -1: if (_block_size > _offset) _offset = 0; else _offset -= _block_size; break; case 1: _offset += _block_size; if (_offset >= _owner->length()) _offset = (_owner->length() / _block_size) * _block_size; break; case 2: _offset = (_owner->length() / _block_size) * _block_size; break; } refresh(); } void refresh() { suspend_layout(); unsigned char *ptr = (unsigned char *)_owner->data() + _offset; _tree.clear(); size_t offs; size_t length = std::min<size_t>(_offset + _block_size, _owner->length()); for (offs = _offset; offs < length; offs += 16) { mforms::TreeNodeRef row = _tree.add_node(); row->set_string(0, base::strfmt("0x%08x", (unsigned int)offs)); for (size_t i = offs, min = std::min<size_t>(offs + 16, length); i < min; ++i) { row->set_string((int)(i + 1 - offs), base::strfmt("%02x", *ptr)); ptr++; } } resume_layout(); _label.set_text(base::strfmt("Viewing Range %i to %i", (int)_offset, (int)(_offset + _block_size))); if (_offset == 0) { _back.set_enabled(false); _first.set_enabled(false); } else { _back.set_enabled(true); _first.set_enabled(true); } if (_offset + _block_size < _owner->length() - 1) { _next.set_enabled(true); _last.set_enabled(true); } else { _next.set_enabled(false); _last.set_enabled(false); } } private: mforms::TreeView _tree; mforms::Box _box; mforms::Button _first; mforms::Button _back; mforms::Label _label; mforms::Button _next; mforms::Button _last; size_t _offset; size_t _block_size; void set_cell_value(mforms::TreeNodeRef node, int column, const std::string &value) { size_t offset = _offset + _tree.row_for_node(node) * 16 + (column - 1); if (offset < _owner->length()) { int i; if (sscanf(value.c_str(), "%x", &i) != 1) return; if (i < 0 || i > 255) return; node->set_string(column, base::strfmt("%02x", i)); *(unsigned char *)(_owner->data() + offset) = i; _owner->notify_edit(); } } }; //-------------------------------------------------------------------------------- class TextDataViewer : public BinaryDataViewer { public: TextDataViewer(BinaryDataEditor *owner, const std::string &encoding, bool read_only) : BinaryDataViewer(owner), _text(), _encoding(encoding) { if (_encoding.empty()) _encoding = "UTF-8"; add(&_message, false, true); add_end(&_text, true, true); _text.set_language(mforms::LanguageNone); _text.set_features(mforms::FeatureWrapText, true); _text.set_features(mforms::FeatureReadOnly, read_only); scoped_connect(_text.signal_changed(), std::bind(&TextDataViewer::edited, this)); _text.set_show_find_panel_callback(std::bind(&TextDataViewer::embed_find_panel, this, std::placeholders::_2)); } virtual void data_changed() { GError *error = 0; gchar *converted = NULL; gsize bread, bwritten; if (!_owner->data() || !(converted = g_convert(_owner->data(), (gssize)_owner->length(), "UTF-8", _encoding.c_str(), &bread, &bwritten, &error)) || _owner->length() != bread) { std::string message = "Data could not be converted to UTF-8 text"; if (error) { message.append(": ").append(error->message); g_error_free(error); } g_free(converted); if (_owner->length()) { _message.set_text(message); _text.set_features(mforms::FeatureReadOnly, true); } else { _text.set_features(mforms::FeatureReadOnly, false); } _text.set_value(""); } else { _message.set_text(""); _text.set_features(mforms::FeatureReadOnly, false); _text.set_value(std::string(converted, bwritten)); if (_owner == NULL || _owner->read_only()) _text.set_features(mforms::FeatureReadOnly, true); } if (converted != NULL) g_free(converted); } private: mforms::CodeEditor _text; mforms::Label _message; std::string _encoding; void edited() { std::string data = _text.get_string_value(); gchar *converted; gsize bread, bwritten; GError *error = 0; if (_encoding != "utf8" && _encoding != "UTF8" && _encoding != "utf-8" && _encoding != "UTF-8") { converted = g_convert(data.data(), (gssize)data.length(), _encoding.c_str(), "UTF-8", &bread, &bwritten, &error); if (converted == NULL || data.length() != bread) { std::string message = base::strfmt("Data could not be converted back to %s", _encoding.c_str()); if (error) { message.append(": ").append(error->message); g_error_free(error); } _message.set_text(message); if (converted != NULL) g_free(converted); return; } _owner->assign_data(converted, bwritten); g_free(converted); _message.set_text(""); } else { _owner->assign_data(data.data(), data.length()); _message.set_text(""); } } void embed_find_panel(bool show) { mforms::View *panel = _text.get_find_panel(); if (show) { if (!panel->get_parent()) add(panel, false, true); } else { remove(panel); _text.focus(); } } }; //-------------------------------------------------------------------------------- class JsonDataViewer : public BinaryDataViewer { public: JsonDataViewer(BinaryDataEditor *owner, rapidjson::Value &value, const std::string &encoding) : BinaryDataViewer(owner), _encoding(encoding), _currentDelayTimer(nullptr){ set_spacing(8); _jsonView.setJson(value); add(&_jsonView, true, true); scoped_connect(_jsonView.editorDataChanged(), std::bind(&JsonDataViewer::edited, this, std::placeholders::_1)); _jsonView.setTextProcessingStopHandler([this]() { if (_currentDelayTimer != nullptr) { bec::GRTManager::get()->cancel_timer(_currentDelayTimer); _currentDelayTimer = nullptr; } }); _jsonView.setTextProcessingStartHandler([this](std::function<bool()> callback) { _currentDelayTimer = bec::GRTManager::get()->run_every([=]() -> bool { return callback(); }, 0.25); }); } virtual void data_changed() { if (!_owner->data()) { _jsonView.clear(); return; } GError *error = NULL; gsize bread = 0, bwritten = 0; char *converted = g_convert(_owner->data(), static_cast<gssize>(_owner->length()), "UTF-8", _encoding.c_str(), &bread, &bwritten, &error); if (!converted || _owner->length() != bread) { _jsonView.clear(); return; } std::string dataToTest = converted; size_t pos = dataToTest.find_first_not_of(SPACES); if (pos != std::string::npos && dataToTest.at(pos) != '{' && dataToTest.at(pos) != '[') { _jsonView.clear(); return; } rapidjson::Value value; rapidjson::Document d; d.Parse(converted); if (!d.HasParseError()) { value.CopyFrom(d, d.GetAllocator()); _jsonView.setJson(value); } else { _jsonView.setText(converted); } } virtual ~JsonDataViewer() { if (_currentDelayTimer) { bec::GRTManager::get()->cancel_timer(_currentDelayTimer); _currentDelayTimer = nullptr; } } private: void edited(const std::string &text) { _owner->assign_data(text.data(), text.length()); } mforms::JsonTabView _jsonView; std::string _encoding; bec::GRTManager::Timer *_currentDelayTimer; }; //-------------------------------------------------------------------------------- class GeomDataViewer : public BinaryDataViewer { public: GeomDataViewer(BinaryDataEditor *owner, bool read_only) : BinaryDataViewer(owner) { set_spacing(8); add(&_drawbox, true, true); } virtual void data_changed() { _drawbox.set_data(std::string(_owner->data(), _owner->length())); } private: GeomDrawBox _drawbox; }; //-------------------------------------------------------------------------------- class GeomTextDataViewer : public BinaryDataViewer { public: GeomTextDataViewer(BinaryDataEditor *owner, bool read_only) : BinaryDataViewer(owner), _text(mforms::VerticalScrollBar) { set_spacing(8); add(&_selector, false, true); add(&_text, true, true); add_end(&_srid, false, false); _text.set_read_only(read_only && false); // TODO: data editing (need to figure out a way to send WKT data to the server when saving) _selector.add_item("View as WKT"); _selector.add_item("View as GeoJSON"); _selector.add_item("View as GML"); _selector.add_item("View as KML"); _selector.signal_changed()->connect(std::bind(&GeomTextDataViewer::data_changed, this)); } virtual void data_changed() { std::string text; spatial::Importer importer; importer.import_from_mysql(std::string(_owner->data(), _owner->length())); switch (_selector.get_selected_index()) { case 0: text = importer.as_wkt(); break; case 1: text = importer.as_json(); break; case 2: text = importer.as_gml(); break; case 3: text = importer.as_kml(); break; } _text.set_value(text); _srid.set_text("SRID: " + std::to_string(importer.getSrid())); } private: mforms::TextBox _text; mforms::Selector _selector; mforms::Label _srid; std::string _encoding; }; //-------------------------------------------------------------------------------- BinaryDataEditor::BinaryDataEditor(const char *data, size_t length, bool read_only) : mforms::Form(0), _box(false), _hbox(true), _read_only(read_only) { set_name("BLOB Editor"); setInternalName("blob_editor"); _data = 0; _length = 0; grt::IntegerRef tab = grt::IntegerRef::cast_from(bec::GRTManager::get()->get_app_option("BlobViewer:DefaultTab")); setup(); assign_data(data, length); add_viewer(new HexDataViewer(this, read_only), "Binary"); add_viewer(new TextDataViewer(this, "LATIN1", read_only), "Text"); if (ImageDataViewer::can_display(data, length)) add_viewer(new ImageDataViewer(this, read_only), "Image"); int activeTab = 0; if (tab.is_valid()) activeTab = (int)*tab; if (tab.is_valid() && *tab >= _tab_view.page_count()) { grt::DictRef dict(grt::DictRef::cast_from(bec::GRTManager::get()->get_app_option(""))); if (dict.is_valid()) dict.gset("BlobViewer:DefaultTab", 0); activeTab = 0; } _tab_view.set_active_tab(activeTab); tab_changed(); } BinaryDataEditor::BinaryDataEditor(const char *data, size_t length, const std::string &text_encoding, const std::string &datatype, bool read_only) : mforms::Form(mforms::Form::main_form()), _type(datatype), _box(false), _hbox(true), _read_only(read_only) { set_name("BLOB Editor"); setInternalName("blob_editor"); _data = 0; _length = 0; _updating = false; grt::IntegerRef tab = grt::IntegerRef::cast_from(bec::GRTManager::get()->get_app_option("BlobViewer:DefaultTab")); setup(); add_viewer(new HexDataViewer(this, read_only), "Binary"); if (datatype == "GEOMETRY") { add_viewer(new GeomTextDataViewer(this, read_only), "Text"); add_viewer(new GeomDataViewer(this, read_only), "Image"); } else add_viewer(new TextDataViewer(this, text_encoding, read_only), "Text"); if (ImageDataViewer::can_display(data, length)) add_viewer(new ImageDataViewer(this, read_only), "Image"); assign_data(data, length); add_json_viewer(read_only, text_encoding, "JSON"); int activeTab = 0; if (tab.is_valid()) activeTab = (int)*tab; if (tab.is_valid() && *tab >= _tab_view.page_count()) { grt::DictRef dict(grt::DictRef::cast_from(bec::GRTManager::get()->get_app_option(""))); if (dict.is_valid()) dict.gset("BlobViewer:DefaultTab", 0); activeTab = 0; } _tab_view.set_active_tab(activeTab); tab_changed(); } BinaryDataEditor::~BinaryDataEditor() { g_free(_data); } void BinaryDataEditor::setup() { set_title("Edit Data"); set_content(&_box); _box.set_padding(12); _box.set_spacing(12); _box.add(&_tab_view, true, true); _box.add(&_length_text, false, true); _box.add(&_hbox, false, true); _hbox.add(&_export, false, true); if (!_read_only) _hbox.add(&_import, false, true); if (!_read_only) _hbox.add_end(&_save, false, true); _hbox.add_end(&_close, false, true); _hbox.set_spacing(12); _save.set_text("Apply"); _close.set_text("Close"); _export.set_text("Save..."); _import.set_text("Load..."); scoped_connect(_tab_view.signal_tab_changed(), std::bind(&BinaryDataEditor::tab_changed, this)); scoped_connect(_save.signal_clicked(), std::bind(&BinaryDataEditor::save, this)); scoped_connect(_close.signal_clicked(), std::bind(&BinaryDataEditor::close, this)); scoped_connect(_import.signal_clicked(), std::bind(&BinaryDataEditor::import_value, this)); scoped_connect(_export.signal_clicked(), std::bind(&BinaryDataEditor::export_value, this)); set_size(800, 500); // Golden ratio. center(); } void BinaryDataEditor::notify_edit() { _length_text.set_text(base::strfmt("Data Length: %i bytes", (int)_length)); } void BinaryDataEditor::assign_data(const char *data, size_t length, bool steal_pointer) { if (_updating) return; if (data != _data) { g_free(_data); if (steal_pointer) _data = (char *)data; else _data = (char *)g_memdup(data, (guint)length); for (size_t i = 0; i < _viewers.size(); i++) _pendingUpdates.insert(_viewers[i]); } _length = length; _length_text.set_text(base::strfmt("Data Length: %i bytes", (int)_length)); } void BinaryDataEditor::tab_changed() { int i = _tab_view.get_active_tab(); if (i < 0) i = 0; grt::DictRef dict(grt::DictRef::cast_from(bec::GRTManager::get()->get_app_option(""))); if (dict.is_valid()) dict.gset("BlobViewer:DefaultTab", i); if (i >= _tab_view.page_count()) { grt::DictRef dict(grt::DictRef::cast_from(bec::GRTManager::get()->get_app_option(""))); if (dict.is_valid()) dict.gset("BlobViewer:DefaultTab", 0); i = 0; } try { _updating = true; if (_pendingUpdates.count(_viewers[i]) > 0 && _data != NULL) _viewers[i]->data_changed(); _pendingUpdates.erase(_viewers[i]); _updating = false; } catch (std::exception &exc) { logError("Error displaying binary data: %s\n", exc.what()); } } void BinaryDataEditor::add_viewer(BinaryDataViewer *viewer, const std::string &title) { _viewers.push_back(viewer); _pendingUpdates.insert(viewer); _tab_view.add_page(mforms::manage(viewer), title); } void BinaryDataEditor::add_json_viewer(bool read_only, const std::string &text_encoding, const std::string &title) { if (!data()) return; GError *error = NULL; gsize bread = 0, bwritten = 0; char *converted = g_convert(data(), static_cast<gssize>(length()), "UTF-8", text_encoding.c_str(), &bread, &bwritten, &error); if (!converted || length() != bread) { // convert problem return; } std::string dataToTest = converted; size_t pos = dataToTest.find_first_not_of(SPACES); if (pos != std::string::npos && dataToTest.at(pos) != '{' && dataToTest.at(pos) != '[') return; rapidjson::Value value; rapidjson::Document d; d.Parse(converted); if (!d.HasParseError()) { value.CopyFrom(d, d.GetAllocator()); add_viewer(new JsonDataViewer(this, value, text_encoding), title.c_str()); _type = "JSON"; } } void BinaryDataEditor::save() { signal_saved(); close(); } void BinaryDataEditor::import_value() { mforms::FileChooser chooser(mforms::OpenFile); chooser.set_title("Import Field Data"); if (chooser.run_modal()) { std::string path = chooser.get_path(); GError *error = 0; char *data; gsize length; if (!g_file_get_contents(path.c_str(), &data, &length, &error)) { mforms::Utilities::show_error(base::strfmt("Could not import data from %s", path.c_str()), error->message, "OK"); g_error_free(error); } else { assign_data(data, length, true); tab_changed(); } } } void BinaryDataEditor::export_value() { mforms::FileChooser chooser(mforms::SaveFile); chooser.set_title("Export Field Data"); chooser.set_extensions("Text files (*.txt)|*.txt|All Files (*.*)|*.*", "txt"); if (chooser.run_modal()) { std::string path = chooser.get_path(); GError *error = 0; if (!g_file_set_contents(path.c_str(), _data, (gssize)_length, &error)) { mforms::Utilities::show_error(base::strfmt("Could not export data to %s", path.c_str()), error->message, "OK"); g_error_free(error); } } }