Skip to content

Commit

Permalink
add clipboard handling
Browse files Browse the repository at this point in the history
  • Loading branch information
slowriot committed Dec 9, 2024
1 parent e9682b7 commit 240f669
Show file tree
Hide file tree
Showing 7 changed files with 235 additions and 0 deletions.
1 change: 1 addition & 0 deletions CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -125,6 +125,7 @@ add_compile_definitions(
add_executable(client
# project-specific:
main.cpp
gui/clipboard.cpp
gui/gui_renderer.cpp
render/webgpu_renderer.cpp
# shared libraries:
Expand Down
96 changes: 96 additions & 0 deletions emscripten_browser_clipboard.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,96 @@
#pragma once

#include <string>
#include <emscripten.h>

#define _EM_JS_INLINE(ret, c_name, js_name, params, code) \
extern "C" { \
ret c_name params EM_IMPORT(js_name); \
EMSCRIPTEN_KEEPALIVE \
__attribute__((section("em_js"), aligned(1))) inline char __em_js__##js_name[] = \
#params "<::>" code; \
}

#define EM_JS_INLINE(ret, name, params, ...) _EM_JS_INLINE(ret, name, name, params, #__VA_ARGS__)

namespace emscripten_browser_clipboard {

/////////////////////////////////// Interface //////////////////////////////////

using paste_handler = void(*)(std::string&&, void*);
using copy_handler = char const*(*)(void*);

inline void paste(paste_handler callback, void *callback_data = nullptr);
inline void copy(copy_handler callback, void *callback_data = nullptr);
inline void copy(std::string const &content);

///////////////////////////////// Implementation ///////////////////////////////

namespace detail {

EM_JS_INLINE(void, paste_js, (paste_handler callback, void *callback_data), {
/// Register the given callback to handle paste events. Callback data pointer is passed through to the callback.
/// Paste handler callback signature is:
/// void my_handler(std::string const &paste_data, void *callback_data = nullptr);
document.addEventListener('paste', (event) => {
Module["ccall"]('emscripten_browser_clipboard_detail_paste_return', 'number', ['string', 'number', 'number'], [event.clipboardData.getData('text/plain'), callback, callback_data]);
});
});

EM_JS_INLINE(void, copy_js, (copy_handler callback, void *callback_data), {
/// Register the given callback to handle copy events. Callback data pointer is passed through to the callback.
/// Copy handler callback signature is:
/// char const *my_handler(void *callback_data = nullptr);
document.addEventListener('copy', (event) => {
const content_ptr = Module["ccall"]('emscripten_browser_clipboard_detail_copy_return', 'number', ['number', 'number'], [callback, callback_data]);
event.clipboardData.setData('text/plain', UTF8ToString(content_ptr));
event.preventDefault();
});
});

EM_JS_INLINE(void, copy_async_js, (char const *content_ptr), {
/// Attempt to copy the provided text asynchronously
navigator.clipboard.writeText(UTF8ToString(content_ptr));
});

} // namespace detail

inline void paste(paste_handler callback, void *callback_data) {
/// C++ wrapper for javascript paste call
detail::paste_js(callback, callback_data);
}

inline void copy(copy_handler callback, void *callback_data) {
/// C++ wrapper for javascript copy call
detail::copy_js(callback, callback_data);
}

inline void copy(std::string const &content) {
/// C++ wrapper for javascript copy call
detail::copy_async_js(content.c_str());
}

namespace detail {

extern "C" {

EMSCRIPTEN_KEEPALIVE inline int emscripten_browser_clipboard_detail_paste_return(char const *paste_data, paste_handler callback, void *callback_data);

EMSCRIPTEN_KEEPALIVE inline int emscripten_browser_clipboard_detail_paste_return(char const *paste_data, paste_handler callback, void *callback_data) {
/// Call paste callback - this function is called from javascript when the paste event occurs
callback(paste_data, callback_data);
return 1;
}

EMSCRIPTEN_KEEPALIVE inline char const *emscripten_browser_clipboard_detail_copy_return(copy_handler callback, void *callback_data);

EMSCRIPTEN_KEEPALIVE inline char const *emscripten_browser_clipboard_detail_copy_return(copy_handler callback, void *callback_data) {
/// Call paste callback - this function is called from javascript when the paste event occurs
return callback(callback_data);
}

}

} // namespace detail

} // namespace emscripten_browser_clipboard
88 changes: 88 additions & 0 deletions gui/clipboard.cpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,88 @@
#include "clipboard.h"
#include <imgui/imgui.h>
#include <imgui/imgui_internal.h>
#include "emscripten_browser_clipboard.h"
#include "secure_cleanse.h"

#define DEBUG_CLIPBOARD

#ifdef NDEBUG
// clipboard debugging must always be disabled in release builds for security
#undef DEBUG_CLIPBOARD
#endif // NDEBUG

#ifdef DEBUG_CLIPBOARD
#include <iostream>
#include <iomanip>
#endif // DEBUG_CLIPBOARD

namespace gui {

clipboard::clipboard() {
/// Default constructor
emscripten_browser_clipboard::paste([](std::string &&paste_data, void *callback_data){
/// Callback to handle clipboard paste from browser
auto &parent{*static_cast<clipboard*>(callback_data)};
#ifdef DEBUG_CLIPBOARD
std::cout << "DEBUG: Clipboard: browser paste event with data " << std::quoted(paste_data) << std::endl;
#endif // DEBUG_CLIPBOARD
parent.content = std::move(paste_data);
}, this);

/*
emscripten_browser_clipboard::copy([](void *callback_data){
/// Callback to offer data for clipboard copy to browser
auto &parent{*static_cast<clipboard*>(callback_data)};
#ifdef DEBUG_CLIPBOARD
std::cout << "DEBUG: Clipboard: browser copy event, sending data " << std::quoted(parent.content) << std::endl;
#endif // DEBUG_CLIPBOARD
return parent.content.c_str();
});
*/
}

clipboard::~clipboard() {
/// Default destructor
scrub(); // clean up memory for security reasons on destruction
}

void clipboard::set_imgui_callbacks() {
/// Set imgui clipboard callbacks - must be done after imgui context is initialised
#ifdef DEBUG_CLIPBOARD
std::cout << "DEBUG: Clipboard: Setting imgui callbacks" << std::endl;
#endif // DEBUG_CLIPBOARD

ImGuiPlatformIO &imgui_platform_io{ImGui::GetPlatformIO()};
imgui_platform_io.Platform_ClipboardUserData = this;

imgui_platform_io.Platform_GetClipboardTextFn = [](ImGuiContext *ctx){
/// Callback for imgui, to return clipboard content
auto &parent{*static_cast<clipboard*>(ctx->PlatformIO.Platform_ClipboardUserData)};

#ifdef DEBUG_CLIPBOARD
std::cout << "DEBUG: Clipboard: imgui requested content, returning " << std::quoted(parent.content) << std::endl;
#endif // DEBUG_CLIPBOARD
return parent.content.c_str();
};

imgui_platform_io.Platform_SetClipboardTextFn = [](ImGuiContext *ctx, char const *text){
/// Callback for imgui, to set clipboard content
auto &parent{*static_cast<clipboard*>(ctx->PlatformIO.Platform_ClipboardUserData)};

parent.content = text;
#ifdef DEBUG_CLIPBOARD
std::cout << "DEBUG: Clipboard: imgui set content, now " << std::quoted(parent.content) << std::endl;
#endif // DEBUG_CLIPBOARD
emscripten_browser_clipboard::copy(parent.content);
};
}

void clipboard::scrub() {
/// Wipe memory securely
#ifdef DEBUG_CLIPBOARD
std::cout << "DEBUG: Clipboard: Scrubbing" << std::endl;
#endif // DEBUG_CLIPBOARD
secure_cleanse(content);
}

}
19 changes: 19 additions & 0 deletions gui/clipboard.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
#pragma once

#include <string>

namespace gui {

class clipboard {
std::string content;

public:
clipboard();
~clipboard();

void set_imgui_callbacks();

void scrub();
};

}
2 changes: 2 additions & 0 deletions gui/gui_renderer.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,8 @@ void gui_renderer::init(ImGui_ImplWGPU_InitInfo &imgui_wgpu_info) {
/// Any additional initialisation that needs to occur after WebGPU has been initialised
ImGui_ImplWGPU_Init(&imgui_wgpu_info);
ImGui_ImplEmscripten_Init();

clipboard.set_imgui_callbacks();
}

void gui_renderer::draw() {
Expand Down
3 changes: 3 additions & 0 deletions gui/gui_renderer.h
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
#pragma once
#include <string>
#include "clipboard.h"
#include "logstorm/logstorm_forward.h"

class ImGui_ImplWGPU_InitInfo;
Expand All @@ -9,6 +10,8 @@ namespace gui {
class gui_renderer {
logstorm::manager &logger;

clipboard clipboard;

public:
std::string shader_code;
bool shader_code_updated{false};
Expand Down
26 changes: 26 additions & 0 deletions secure_cleanse.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
#pragma once

namespace secure_cleanse_impl {

// Pointer to memset is volatile so that compiler must de-reference
// the pointer and can't assume that it points to any function in
// particular (such as memset, which it then might further "optimize")
typedef void* (*memset_t)(void*, int, size_t);
static volatile memset_t memset_func = memset;

}

inline void secure_cleanse(void *ptr, size_t len);
inline void secure_cleanse(std::string &target);

inline void secure_cleanse(void *ptr, size_t len) {
/// Securely erase a block of memory
secure_cleanse_impl::memset_func(ptr, 0, len);
}

inline void secure_cleanse(std::string &target) {
/// Securely erase a string
target.resize(target.capacity(), 0);
secure_cleanse(&target[0], target.size());
target.clear();
}

0 comments on commit 240f669

Please sign in to comment.