1
Fork 0
mirror of https://github.com/RGBCube/serenity synced 2025-07-26 07:07:45 +00:00

HexEditor: Stream input files instead of keeping them in memory

To support editing of large files it is an advantage to not load the
entire file into memory but only load whatever is needed for display at
the moment. To make it work, file access is abstracted into a socalled
HexDocument, of which there two: a memory based and a file based one.
The former can be used for newly created documents, the latter for file
based editing.

Hex documents now do track changes instead of the HexEditor. HexEditor
only sets new values. This frees HexEditor of some responsibility.
This commit is contained in:
Arne Elster 2021-11-10 01:37:10 +01:00 committed by Brian Gianforcaro
parent 60c3ad9ae8
commit fd66dda1d7
7 changed files with 362 additions and 117 deletions

View file

@ -28,6 +28,7 @@
HexEditor::HexEditor()
: m_blink_timer(Core::Timer::construct())
, m_document(make<HexDocumentMemory>(ByteBuffer::create_zeroed(0).release_value()))
{
set_should_hide_unnecessary_scrollbars(true);
set_focus_policy(GUI::FocusPolicy::StrongFocus);
@ -56,11 +57,27 @@ void HexEditor::set_readonly(bool readonly)
m_readonly = readonly;
}
void HexEditor::set_buffer(const ByteBuffer& buffer)
bool HexEditor::open_new_file(size_t size)
{
m_buffer = buffer;
set_content_length(buffer.size());
m_tracked_changes.clear();
auto maybe_buffer = ByteBuffer::create_zeroed(size);
if (!maybe_buffer.has_value()) {
return false;
}
m_document = make<HexDocumentMemory>(maybe_buffer.release_value());
set_content_length(m_document->size());
m_position = 0;
m_cursor_at_low_nibble = false;
update();
update_status();
return true;
}
void HexEditor::open_file(NonnullRefPtr<Core::File> file)
{
m_document = make<HexDocumentFile>(file);
set_content_length(m_document->size());
m_position = 0;
m_cursor_at_low_nibble = false;
update();
@ -72,9 +89,8 @@ void HexEditor::fill_selection(u8 fill_byte)
if (!has_selection())
return;
for (size_t i = m_selection_start; i < m_selection_end; i++) {
m_tracked_changes.set(i, m_buffer.data()[i]);
m_buffer.data()[i] = fill_byte;
for (size_t i = m_selection_start; i <= m_selection_end; i++) {
m_document->set(i, fill_byte);
}
update();
@ -83,7 +99,7 @@ void HexEditor::fill_selection(u8 fill_byte)
void HexEditor::set_position(size_t position)
{
if (position > m_buffer.size())
if (position > m_document->size())
return;
m_position = position;
@ -93,42 +109,37 @@ void HexEditor::set_position(size_t position)
update_status();
}
bool HexEditor::write_to_file(const String& path)
bool HexEditor::save_as(int fd)
{
if (m_buffer.is_empty())
return true;
int fd = open(path.characters(), O_WRONLY | O_CREAT | O_TRUNC, 0666);
if (fd < 0) {
perror("open");
auto new_file = Core::File::construct();
if (!new_file->open(fd, Core::OpenMode::ReadWrite, Core::File::ShouldCloseFileDescriptor::Yes)) {
return false;
}
return write_to_file(fd);
if (m_document->type() == HexDocument::Type::File) {
HexDocumentFile* fileDoc = static_cast<HexDocumentFile*>(m_document.ptr());
if (!fileDoc->write_to_file(new_file))
return false;
fileDoc->set_file(new_file);
} else {
HexDocumentMemory* memDoc = static_cast<HexDocumentMemory*>(m_document.ptr());
if (!memDoc->write_to_file(new_file))
return false;
m_document = make<HexDocumentFile>(new_file);
}
update();
return true;
}
bool HexEditor::write_to_file(int fd)
bool HexEditor::save()
{
ScopeGuard fd_guard = [fd] { close(fd); };
int rc = ftruncate(fd, m_buffer.size());
if (rc < 0) {
perror("ftruncate");
if (m_document->type() != HexDocument::Type::File) {
return false;
}
ssize_t nwritten = write(fd, m_buffer.data(), m_buffer.size());
if (nwritten < 0) {
perror("write");
close(fd);
return false;
}
if (static_cast<size_t>(nwritten) == m_buffer.size()) {
m_tracked_changes.clear();
update();
}
static_cast<HexDocumentFile*>(m_document.ptr())->write_to_file();
return true;
}
@ -145,8 +156,8 @@ bool HexEditor::copy_selected_hex_to_clipboard()
return false;
StringBuilder output_string_builder;
for (size_t i = m_selection_start; i < m_selection_end; i++)
output_string_builder.appendff("{:02X} ", m_buffer.data()[i]);
for (size_t i = m_selection_start; i <= m_selection_end; i++)
output_string_builder.appendff("{:02X} ", m_document->get(i).value);
GUI::Clipboard::the().set_plain_text(output_string_builder.to_string());
return true;
@ -158,8 +169,8 @@ bool HexEditor::copy_selected_text_to_clipboard()
return false;
StringBuilder output_string_builder;
for (size_t i = m_selection_start; i < m_selection_end; i++)
output_string_builder.append(isprint(m_buffer.data()[i]) ? m_buffer[i] : '.');
for (size_t i = m_selection_start; i <= m_selection_end; i++)
output_string_builder.append(isprint(m_document->get(i).value) ? m_document->get(i).value : '.');
GUI::Clipboard::the().set_plain_text(output_string_builder.to_string());
return true;
@ -173,8 +184,8 @@ bool HexEditor::copy_selected_hex_to_clipboard_as_c_code()
StringBuilder output_string_builder;
output_string_builder.appendff("unsigned char raw_data[{}] = {{\n", (m_selection_end - m_selection_start) + 1);
output_string_builder.append(" ");
for (size_t i = m_selection_start, j = 1; i < m_selection_end; i++, j++) {
output_string_builder.appendff("{:#02X}", m_buffer.data()[i]);
for (size_t i = m_selection_start, j = 1; i <= m_selection_end; i++, j++) {
output_string_builder.appendff("{:#02X}", m_document->get(i).value);
if (i != m_selection_end)
output_string_builder.append(", ");
if ((j % 12) == 0) {
@ -234,7 +245,7 @@ void HexEditor::mousedown_event(GUI::MouseEvent& event)
auto byte_y = (absolute_y - hex_start_y) / line_height();
auto offset = (byte_y * m_bytes_per_row) + byte_x;
if (offset >= m_buffer.size())
if (offset >= m_document->size())
return;
dbgln_if(HEX_DEBUG, "HexEditor::mousedown_event(hex): offset={}", offset);
@ -257,7 +268,7 @@ void HexEditor::mousedown_event(GUI::MouseEvent& event)
auto byte_y = (absolute_y - text_start_y) / line_height();
auto offset = (byte_y * m_bytes_per_row) + byte_x;
if (offset >= m_buffer.size())
if (offset >= m_document->size())
return;
dbgln_if(HEX_DEBUG, "HexEditor::mousedown_event(text): offset={}", offset);
@ -306,7 +317,7 @@ void HexEditor::mousemove_event(GUI::MouseEvent& event)
auto byte_y = (absolute_y - hex_start_y) / line_height();
auto offset = (byte_y * m_bytes_per_row) + byte_x;
if (offset > m_buffer.size())
if (offset > m_document->size())
return;
m_selection_end = offset;
@ -321,8 +332,7 @@ void HexEditor::mousemove_event(GUI::MouseEvent& event)
auto byte_x = (absolute_x - text_start_x) / character_width();
auto byte_y = (absolute_y - text_start_y) / line_height();
auto offset = (byte_y * m_bytes_per_row) + byte_x;
if (offset > m_buffer.size())
if (offset > m_document->size())
return;
m_selection_end = offset;
@ -383,7 +393,7 @@ void HexEditor::keydown_event(GUI::KeyEvent& event)
}
if (event.key() == KeyCode::Key_Down) {
if (m_position + bytes_per_row() < m_buffer.size()) {
if (m_position + bytes_per_row() < m_document->size()) {
m_position += bytes_per_row();
m_selection_start = m_selection_end = m_position;
m_cursor_at_low_nibble = false;
@ -409,7 +419,7 @@ void HexEditor::keydown_event(GUI::KeyEvent& event)
}
if (event.key() == KeyCode::Key_Right) {
if (m_position + 1 < m_buffer.size()) {
if (m_position + 1 < m_document->size()) {
m_position++;
m_selection_start = m_selection_end = m_position;
m_cursor_at_low_nibble = false;
@ -446,12 +456,10 @@ void HexEditor::keydown_event(GUI::KeyEvent& event)
void HexEditor::hex_mode_keydown_event(GUI::KeyEvent& event)
{
if ((event.key() >= KeyCode::Key_0 && event.key() <= KeyCode::Key_9) || (event.key() >= KeyCode::Key_A && event.key() <= KeyCode::Key_F)) {
if (m_buffer.is_empty())
return;
if (m_position == m_buffer.size())
if (m_document->size() == 0)
return;
VERIFY(m_position <= m_buffer.size());
VERIFY(m_position <= m_document->size());
// yes, this is terrible... but it works.
auto value = (event.key() >= KeyCode::Key_0 && event.key() <= KeyCode::Key_9)
@ -459,12 +467,13 @@ void HexEditor::hex_mode_keydown_event(GUI::KeyEvent& event)
: (event.key() - KeyCode::Key_A) + 0xA;
if (!m_cursor_at_low_nibble) {
m_tracked_changes.set(m_position, m_buffer.data()[m_position]);
m_buffer.data()[m_position] = value << 4 | (m_buffer.data()[m_position] & 0xF); // shift new value left 4 bits, OR with existing last 4 bits
u8 existing_change = m_document->get(m_position).value;
existing_change = value << 4 | (existing_change & 0xF); // shift new value left 4 bits, OR with existing last 4 bits
m_document->set(m_position, existing_change);
m_cursor_at_low_nibble = true;
} else {
m_buffer.data()[m_position] = (m_buffer.data()[m_position] & 0xF0) | value; // save the first 4 bits, OR the new value in the last 4
if (m_position + 1 < m_buffer.size())
m_document->set(m_position, (m_document->get(m_position).value & 0xF0) | value); // save the first 4 bits, OR the new value in the last 4
if (m_position + 1 < m_document->size())
m_position++;
m_cursor_at_low_nibble = false;
}
@ -478,16 +487,15 @@ void HexEditor::hex_mode_keydown_event(GUI::KeyEvent& event)
void HexEditor::text_mode_keydown_event(GUI::KeyEvent& event)
{
if (m_buffer.is_empty())
if (m_document->size() == 0)
return;
VERIFY(m_position < m_buffer.size());
VERIFY(m_position < m_document->size());
if (event.code_point() == 0) // This is a control key
return;
m_tracked_changes.set(m_position, m_buffer.data()[m_position]);
m_buffer.data()[m_position] = event.code_point();
if (m_position + 1 < m_buffer.size())
m_document->set(m_position, event.code_point());
if (m_position + 1 < m_document->size())
m_position++;
m_cursor_at_low_nibble = false;
@ -518,7 +526,7 @@ void HexEditor::paint_event(GUI::PaintEvent& event)
painter.add_clip_rect(event.rect());
painter.fill_rect(event.rect(), palette().color(background_role()));
if (m_buffer.is_empty())
if (m_document->size() == 0)
return;
painter.translate(frame_thickness(), frame_thickness());
@ -564,10 +572,10 @@ void HexEditor::paint_event(GUI::PaintEvent& event)
for (size_t i = min_row; i < max_row; i++) {
for (size_t j = 0; j < bytes_per_row(); j++) {
auto byte_position = (i * bytes_per_row()) + j;
if (byte_position >= m_buffer.size())
if (byte_position >= m_document->size())
return;
const bool edited_flag = m_tracked_changes.contains(byte_position);
const bool edited_flag = m_document->get(byte_position).modified;
const bool selection_inbetween_start_end = byte_position >= m_selection_start && byte_position < m_selection_end;
const bool selection_inbetween_end_start = byte_position >= m_selection_end && byte_position < m_selection_start;
@ -592,7 +600,8 @@ void HexEditor::paint_event(GUI::PaintEvent& event)
}
painter.fill_rect(hex_display_rect, background_color);
auto line = String::formatted("{:02X}", m_buffer[byte_position]);
const u8 cell_value = m_document->get(byte_position).value;
auto line = String::formatted("{:02X}", cell_value);
painter.draw_text(hex_display_rect, line, Gfx::TextAlignment::TopLeft, text_color);
if (m_edit_mode == EditMode::Hex) {
@ -626,7 +635,7 @@ void HexEditor::paint_event(GUI::PaintEvent& event)
}
painter.fill_rect(text_display_rect, background_color);
painter.draw_text(text_display_rect, String::formatted("{:c}", isprint(m_buffer[byte_position]) ? m_buffer[byte_position] : '.'), Gfx::TextAlignment::TopLeft, text_color);
painter.draw_text(text_display_rect, String::formatted("{:c}", isprint(cell_value) ? cell_value : '.'), Gfx::TextAlignment::TopLeft, text_color);
if (m_edit_mode == EditMode::Text) {
if (byte_position == m_position && m_cursor_blink_active) {
@ -642,7 +651,7 @@ void HexEditor::paint_event(GUI::PaintEvent& event)
void HexEditor::select_all()
{
highlight(0, m_buffer.size() - 1);
highlight(0, m_document->size() - 1);
set_position(0);
}
@ -664,38 +673,50 @@ Optional<size_t> HexEditor::find_and_highlight(ByteBuffer& needle, size_t start)
Optional<size_t> HexEditor::find(ByteBuffer& needle, size_t start)
{
if (m_buffer.is_empty())
if (m_document->size() == 0)
return {};
auto raw_offset = memmem(m_buffer.data() + start, m_buffer.size() - start, needle.data(), needle.size());
if (raw_offset == NULL)
return {};
for (size_t i = start; i < m_document->size(); i++) {
if (m_document->get(i).value == *needle.data()) {
bool found = true;
for (size_t j = 1; j < needle.size(); j++) {
if (m_document->get(i + j).value != needle.data()[j]) {
found = false;
break;
}
}
if (found) {
auto end_of_match = i + needle.size();
return end_of_match;
}
}
}
int relative_offset = static_cast<const u8*>(raw_offset) - m_buffer.data();
dbgln("find: start={} raw_offset={} relative_offset={}", start, raw_offset, relative_offset);
auto end_of_match = relative_offset + needle.size();
return end_of_match;
return {};
}
Vector<Match> HexEditor::find_all(ByteBuffer& needle, size_t start)
{
if (m_buffer.is_empty())
if (m_document->size() == 0 || needle.size() == 0)
return {};
Vector<Match> matches;
size_t i = start;
while (i < m_buffer.size()) {
auto raw_offset = memmem(m_buffer.data() + i, m_buffer.size() - i, needle.data(), needle.size());
if (raw_offset == NULL)
break;
int relative_offset = static_cast<const u8*>(raw_offset) - m_buffer.data();
dbgln("find_all: needle={} start={} raw_offset={} relative_offset={}", needle.data(), i, raw_offset, relative_offset);
matches.append({ relative_offset, String::formatted("{}", StringView { needle }.to_string().characters()) });
i = relative_offset + needle.size();
for (; i < m_document->size(); i++) {
if (m_document->get(i).value == *needle.data()) {
bool found = true;
for (size_t j = 1; j < needle.size(); j++) {
if (m_document->get(i + j).value != needle.data()[j]) {
found = false;
break;
}
}
if (found) {
matches.append({ i, String::formatted("{}", StringView { needle }.to_string().characters()) });
i += needle.size() - 1;
}
}
}
if (matches.is_empty())
@ -709,18 +730,21 @@ Vector<Match> HexEditor::find_all(ByteBuffer& needle, size_t start)
Vector<Match> HexEditor::find_all_strings(size_t min_length)
{
if (m_buffer.is_empty())
if (m_document->size() == 0)
return {};
Vector<Match> matches;
int offset = -1;
bool found_string = false;
size_t offset = 0;
StringBuilder builder;
for (size_t i = 0; i < m_buffer.size(); i++) {
char c = m_buffer.bytes().at(i);
for (size_t i = 0; i < m_document->size(); i++) {
char c = m_document->get(i).value;
if (isprint(c)) {
if (offset == -1)
if (!found_string) {
offset = i;
found_string = true;
}
builder.append(c);
} else {
if (builder.length() >= min_length) {
@ -728,7 +752,7 @@ Vector<Match> HexEditor::find_all_strings(size_t min_length)
matches.append({ offset, builder.to_string() });
}
builder.clear();
offset = -1;
found_string = false;
}
}