mirror of
https://github.com/eried/portapack-mayhem.git
synced 2024-12-28 00:39:44 -05:00
Move file_wrapper and make testable. (#1085)
* WIP Move file_wrapper and make testable. * More tests, get text_editor compiling * Back to working * Run formatter --------- Co-authored-by: kallanreed <kallanreed@outlook.com>
This commit is contained in:
parent
23c24355ab
commit
e50d8dc148
@ -39,216 +39,6 @@ namespace {
|
||||
|
||||
namespace ui {
|
||||
|
||||
/* FileWrapper ******************************************************/
|
||||
|
||||
FileWrapper::FileWrapper() {
|
||||
}
|
||||
|
||||
Optional<FileWrapper::Error> FileWrapper::open(const fs::path& path) {
|
||||
file_ = File();
|
||||
auto error = file_.open(path);
|
||||
|
||||
if (!error)
|
||||
initialize();
|
||||
|
||||
return error;
|
||||
}
|
||||
|
||||
std::string FileWrapper::get_text(Offset line, Offset col, Offset length) {
|
||||
// TODO: better way to return errors.
|
||||
auto range = line_range(line);
|
||||
int32_t to_read = length;
|
||||
|
||||
if (!range)
|
||||
return "[UNCACHED LINE]";
|
||||
|
||||
// Don't read past end of line.
|
||||
if (range->start + col + to_read >= range->end)
|
||||
to_read = range->end - col - range->start;
|
||||
|
||||
if (to_read <= 0)
|
||||
return {};
|
||||
|
||||
return read(range->start + col, to_read);
|
||||
}
|
||||
|
||||
Optional<FileWrapper::Range> FileWrapper::line_range(Line line) {
|
||||
ensure_cached(line);
|
||||
|
||||
auto offset = offset_for_line(line);
|
||||
if (!offset)
|
||||
return {};
|
||||
|
||||
auto start = *offset == 0 ? start_offset_ : (newlines_[*offset - 1] + 1);
|
||||
auto end = newlines_[*offset] + 1;
|
||||
|
||||
return {Range{start, end}};
|
||||
}
|
||||
|
||||
FileWrapper::Offset FileWrapper::line_length(Line line) {
|
||||
auto range = line_range(line);
|
||||
|
||||
if (range)
|
||||
return range->end - range->start;
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
void FileWrapper::initialize() {
|
||||
start_offset_ = 0;
|
||||
start_line_ = 0;
|
||||
line_count_ = 0;
|
||||
newlines_.clear();
|
||||
line_ending_ = LineEnding::LF;
|
||||
|
||||
Offset offset = 0;
|
||||
auto result = next_newline(offset);
|
||||
|
||||
while (result) {
|
||||
++line_count_;
|
||||
if (newlines_.size() < max_newlines)
|
||||
newlines_.push_back(*result);
|
||||
offset = *result + 1;
|
||||
result = next_newline(offset);
|
||||
}
|
||||
}
|
||||
|
||||
std::string FileWrapper::read(Offset offset, Offset length) {
|
||||
// TODO: better way to return errors.
|
||||
if (offset + length > file_.size())
|
||||
return {"[BAD OFFSET]"};
|
||||
|
||||
std::string buffer(length, '\0');
|
||||
file_.seek(offset);
|
||||
|
||||
auto result = file_.read(&buffer[0], length);
|
||||
if (result.is_ok())
|
||||
buffer.resize(*result);
|
||||
else
|
||||
return result.error().what();
|
||||
|
||||
return buffer;
|
||||
}
|
||||
|
||||
Optional<FileWrapper::Offset> FileWrapper::offset_for_line(Line line) const {
|
||||
if (line >= line_count_)
|
||||
return {};
|
||||
|
||||
Offset actual = line - start_line_;
|
||||
if (actual < newlines_.size()) // NB: underflow wrap.
|
||||
return {actual};
|
||||
|
||||
return {};
|
||||
}
|
||||
|
||||
void FileWrapper::ensure_cached(Line line) {
|
||||
if (line >= line_count_)
|
||||
return;
|
||||
|
||||
auto result = offset_for_line(line);
|
||||
if (result)
|
||||
return;
|
||||
|
||||
if (line < start_line_) {
|
||||
while (line < start_line_ && start_offset_ >= 2) {
|
||||
// start_offset_ - 1 should be a newline. Need to
|
||||
// find the new value for start_offset_. start_line_
|
||||
// has to be > 0 to get into this block so there should
|
||||
// always be one newline before start_offset_.
|
||||
auto offset = previous_newline(start_offset_ - 2);
|
||||
newlines_.push_front(start_offset_ - 1);
|
||||
|
||||
if (!offset) {
|
||||
// Must be at beginning.
|
||||
start_line_ = 0;
|
||||
start_offset_ = 0;
|
||||
} else {
|
||||
// Found an previous newline, the new start_line_
|
||||
// starts at the newline offset + 1.
|
||||
start_line_--;
|
||||
start_offset_ = *offset + 1;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
while (line >= start_line_ + newlines_.size()) {
|
||||
auto offset = next_newline(newlines_.back() + 1);
|
||||
if (offset) {
|
||||
start_line_++;
|
||||
start_offset_ = newlines_.front() + 1;
|
||||
newlines_.push_back(*offset);
|
||||
} /* else at the EOF. */
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Optional<FileWrapper::Offset> FileWrapper::previous_newline(Offset start) {
|
||||
char buffer[buffer_size];
|
||||
Offset offset = start;
|
||||
auto to_read = buffer_size;
|
||||
|
||||
do {
|
||||
if (offset < to_read) {
|
||||
// NB: Char at 'offset' was read in the previous iteration.
|
||||
to_read = offset;
|
||||
offset = 0;
|
||||
} else
|
||||
offset -= to_read;
|
||||
|
||||
file_.seek(offset);
|
||||
|
||||
auto result = file_.read(buffer, to_read);
|
||||
if (result.is_error())
|
||||
break;
|
||||
|
||||
// Find newlines in the buffer backwards.
|
||||
for (int32_t i = *result - 1; i >= 0; --i) {
|
||||
switch (buffer[i]) {
|
||||
case '\n':
|
||||
return {offset + i};
|
||||
}
|
||||
}
|
||||
|
||||
if (offset == 0)
|
||||
break;
|
||||
|
||||
} while (true);
|
||||
|
||||
return {}; // Didn't find one.
|
||||
}
|
||||
|
||||
Optional<FileWrapper::Offset> FileWrapper::next_newline(Offset start) {
|
||||
char buffer[buffer_size];
|
||||
Offset offset = start;
|
||||
|
||||
// EOF, nothing to do.
|
||||
if (start >= size())
|
||||
return {};
|
||||
|
||||
file_.seek(offset);
|
||||
|
||||
while (true) {
|
||||
auto result = file_.read(buffer, buffer_size);
|
||||
if (result.is_error())
|
||||
return {};
|
||||
|
||||
// Find newlines in the buffer.
|
||||
for (Offset i = 0; i < *result; ++i) {
|
||||
switch (buffer[i]) {
|
||||
case '\n':
|
||||
return {offset + i};
|
||||
}
|
||||
}
|
||||
|
||||
offset += *result;
|
||||
|
||||
if (*result < buffer_size)
|
||||
break;
|
||||
}
|
||||
|
||||
// Fake a newline at the end for consistency.
|
||||
return {offset};
|
||||
}
|
||||
|
||||
/* TextViewer *******************************************************/
|
||||
|
||||
TextViewer::TextViewer(Rect parent_rect)
|
||||
@ -391,9 +181,6 @@ void TextViewer::redraw(bool redraw_text) {
|
||||
}
|
||||
|
||||
void TextViewer::paint_text(Painter& painter, uint32_t line, uint16_t col) {
|
||||
// CONSIDER: A line cache would use more memory but save a lot of IO.
|
||||
// Only the new lines/characters would need to be refetched.
|
||||
|
||||
auto r = screen_rect();
|
||||
|
||||
// Draw the lines from the file
|
||||
@ -403,14 +190,13 @@ void TextViewer::paint_text(Painter& painter, uint32_t line, uint16_t col) {
|
||||
|
||||
auto str = file_->get_text(line + i, col, max_col);
|
||||
|
||||
// Draw text.
|
||||
if (str.length() > 0)
|
||||
if (str && str->length() > 0)
|
||||
painter.draw_string(
|
||||
{0, r.top() + (int)i * char_height},
|
||||
style_text, str);
|
||||
style_text, *str);
|
||||
|
||||
// Clear empty line sections.
|
||||
int32_t clear_width = max_col - str.length();
|
||||
// Clear empty line sections. This is less visually jarring than full clear.
|
||||
int32_t clear_width = max_col - (str ? str->length() : 0);
|
||||
if (clear_width > 0)
|
||||
painter.fill_rectangle(
|
||||
{(max_col - clear_width) * char_width,
|
||||
@ -567,15 +353,14 @@ void TextEditorView::on_show() {
|
||||
}
|
||||
|
||||
void TextEditorView::open_file(const fs::path& path) {
|
||||
auto file = std::make_unique<FileWrapper>();
|
||||
auto error = file->open(path);
|
||||
auto result = FileWrapper::open(path);
|
||||
|
||||
if (error) {
|
||||
nav_.display_modal("Read Error", "Cannot open file:\n" + error->what());
|
||||
if (!result) {
|
||||
nav_.display_modal("Read Error", "Cannot open file:\n" + result.error().what());
|
||||
file_.reset();
|
||||
viewer.clear_file();
|
||||
} else {
|
||||
file_ = std::move(file);
|
||||
file_ = result.take();
|
||||
viewer.set_file(*file_);
|
||||
}
|
||||
|
||||
|
@ -28,90 +28,19 @@
|
||||
#include "ui_painter.hpp"
|
||||
#include "ui_widget.hpp"
|
||||
|
||||
#include "circular_buffer.hpp"
|
||||
#include "file.hpp"
|
||||
#include "file_wrapper.hpp"
|
||||
#include "optional.hpp"
|
||||
|
||||
#include <memory>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
namespace ui {
|
||||
|
||||
/* TODO:
|
||||
* - Copy on write into temp file so startup is fast.
|
||||
*/
|
||||
|
||||
enum class LineEnding : uint8_t {
|
||||
LF,
|
||||
CRLF
|
||||
};
|
||||
|
||||
enum class ScrollDirection : uint8_t {
|
||||
Vertical,
|
||||
Horizontal
|
||||
};
|
||||
|
||||
/* Wraps a file and provides an API for accessing lines efficiently. */
|
||||
class FileWrapper {
|
||||
public:
|
||||
using Error = std::filesystem::filesystem_error;
|
||||
using Offset = uint32_t; // TODO: make enums?
|
||||
using Line = uint32_t;
|
||||
using Column = uint32_t;
|
||||
using Range = struct {
|
||||
// Offset of the line start.
|
||||
Offset start;
|
||||
// Offset of one past the line end.
|
||||
Offset end;
|
||||
};
|
||||
|
||||
FileWrapper();
|
||||
|
||||
/* Prevent copies. */
|
||||
FileWrapper(const FileWrapper&) = delete;
|
||||
FileWrapper& operator=(const FileWrapper&) = delete;
|
||||
|
||||
Optional<Error> open(const std::filesystem::path& path);
|
||||
std::string get_text(Line line, Column col, Offset length);
|
||||
|
||||
File::Size size() const { return file_.size(); }
|
||||
uint32_t line_count() const { return line_count_; }
|
||||
|
||||
Optional<Range> line_range(Line line);
|
||||
Offset line_length(Line line);
|
||||
|
||||
private:
|
||||
/* Number of newline offsets to cache. */
|
||||
static constexpr Offset max_newlines = 64;
|
||||
static constexpr size_t buffer_size = 512;
|
||||
|
||||
void initialize();
|
||||
std::string read(Offset offset, Offset length = 30);
|
||||
|
||||
/* Returns the offset into the newline cache if valid. */
|
||||
Optional<Offset> offset_for_line(Line line) const;
|
||||
|
||||
/* Ensure specified line is in the newline cache. */
|
||||
void ensure_cached(Line line);
|
||||
|
||||
/* Helpers for finding the prev/next newline. */
|
||||
Optional<Offset> previous_newline(Offset start);
|
||||
Optional<Offset> next_newline(Offset start);
|
||||
|
||||
File file_{};
|
||||
|
||||
/* Total number of lines in the file. */
|
||||
Offset line_count_{0};
|
||||
|
||||
/* The offset and line of the newlines cache. */
|
||||
Offset start_offset_{0};
|
||||
Offset start_line_{0};
|
||||
|
||||
LineEnding line_ending_{LineEnding::LF};
|
||||
CircularBuffer<Offset, max_newlines + 1> newlines_{};
|
||||
};
|
||||
|
||||
/* Control that renders a text file. */
|
||||
class TextViewer : public Widget {
|
||||
public:
|
||||
|
@ -60,7 +60,7 @@ File::~File() {
|
||||
f_close(&f);
|
||||
}
|
||||
|
||||
File::Result<File::Size> File::read(void* const data, const Size bytes_to_read) {
|
||||
File::Result<File::Size> File::read(void* data, Size bytes_to_read) {
|
||||
UINT bytes_read = 0;
|
||||
const auto result = f_read(&f, data, bytes_to_read, &bytes_read);
|
||||
if (result == FR_OK) {
|
||||
@ -70,7 +70,7 @@ File::Result<File::Size> File::read(void* const data, const Size bytes_to_read)
|
||||
}
|
||||
}
|
||||
|
||||
File::Result<File::Size> File::write(const void* const data, const Size bytes_to_write) {
|
||||
File::Result<File::Size> File::write(const void* data, Size bytes_to_write) {
|
||||
UINT bytes_written = 0;
|
||||
const auto result = f_write(&f, data, bytes_to_write, &bytes_written);
|
||||
if (result == FR_OK) {
|
||||
@ -84,7 +84,7 @@ File::Result<File::Size> File::write(const void* const data, const Size bytes_to
|
||||
}
|
||||
}
|
||||
|
||||
File::Result<File::Offset> File::seek(const Offset new_position) {
|
||||
File::Result<File::Offset> File::seek(Offset new_position) {
|
||||
/* NOTE: Returns *old* position, not new position */
|
||||
const auto old_position = f_tell(&f);
|
||||
const auto result = f_lseek(&f, new_position);
|
||||
|
@ -288,6 +288,7 @@ class File {
|
||||
using Timestamp = uint32_t;
|
||||
using Error = std::filesystem::filesystem_error;
|
||||
|
||||
// TODO: move to common.
|
||||
template <typename T>
|
||||
struct Result {
|
||||
enum class Type {
|
||||
@ -303,11 +304,15 @@ class File {
|
||||
return type == Type::Success;
|
||||
}
|
||||
|
||||
operator bool() const {
|
||||
return is_ok();
|
||||
}
|
||||
|
||||
bool is_error() const {
|
||||
return type == Type::Error;
|
||||
}
|
||||
|
||||
const T& value() const {
|
||||
const T& value() const& {
|
||||
return value_;
|
||||
}
|
||||
|
||||
@ -315,6 +320,15 @@ class File {
|
||||
return value_;
|
||||
}
|
||||
|
||||
/* Allows value to be moved out of the Result. */
|
||||
T take() {
|
||||
if (is_error())
|
||||
return {};
|
||||
T temp;
|
||||
std::swap(temp, value_);
|
||||
return temp;
|
||||
}
|
||||
|
||||
Error error() const {
|
||||
return error_;
|
||||
}
|
||||
@ -322,9 +336,9 @@ class File {
|
||||
Result() = delete;
|
||||
|
||||
constexpr Result(
|
||||
T value)
|
||||
T&& value)
|
||||
: type{Type::Success},
|
||||
value_{value} {
|
||||
value_{std::forward<T>(value)} {
|
||||
}
|
||||
|
||||
constexpr Result(
|
||||
@ -334,17 +348,21 @@ class File {
|
||||
}
|
||||
|
||||
~Result() {
|
||||
if (type == Type::Success) {
|
||||
if (is_ok())
|
||||
value_.~T();
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
File(){};
|
||||
~File();
|
||||
|
||||
File(File&&) = default;
|
||||
File& operator=(File&&) = default;
|
||||
File(File&& other) {
|
||||
std::swap(f, other.f);
|
||||
}
|
||||
File& operator=(File&& other) {
|
||||
std::swap(f, other.f);
|
||||
return *this;
|
||||
}
|
||||
|
||||
/* Prevent copies */
|
||||
File(const File&) = delete;
|
||||
@ -355,10 +373,10 @@ class File {
|
||||
Optional<Error> append(const std::filesystem::path& filename);
|
||||
Optional<Error> create(const std::filesystem::path& filename);
|
||||
|
||||
Result<Size> read(void* const data, const Size bytes_to_read);
|
||||
Result<Size> write(const void* const data, const Size bytes_to_write);
|
||||
Result<Size> read(void* data, const Size bytes_to_read);
|
||||
Result<Size> write(const void* data, Size bytes_to_write);
|
||||
|
||||
Result<Offset> seek(const uint64_t Offset);
|
||||
Result<Offset> seek(uint64_t Offset);
|
||||
// Timestamp created_date() const;
|
||||
Size size() const;
|
||||
|
||||
|
342
firmware/application/file_wrapper.hpp
Normal file
342
firmware/application/file_wrapper.hpp
Normal file
@ -0,0 +1,342 @@
|
||||
/*
|
||||
* Copyright (C) 2023 Kyle Reed
|
||||
*
|
||||
* This file is part of PortaPack.
|
||||
*
|
||||
* This program is free software; you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation; either version 2, or (at your option)
|
||||
* any later version.
|
||||
*
|
||||
* 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 for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program; see the file COPYING. If not, write to
|
||||
* the Free Software Foundation, Inc., 51 Franklin Street,
|
||||
* Boston, MA 02110-1301, USA.
|
||||
*/
|
||||
|
||||
#ifndef __FILE_WRAPPER_HPP__
|
||||
#define __FILE_WRAPPER_HPP__
|
||||
|
||||
#include "circular_buffer.hpp"
|
||||
#include "file.hpp"
|
||||
#include "optional.hpp"
|
||||
|
||||
#include <memory>
|
||||
#include <string>
|
||||
|
||||
enum class LineEnding : uint8_t {
|
||||
LF,
|
||||
CRLF
|
||||
};
|
||||
|
||||
/* TODO:
|
||||
* - CRLF handling.
|
||||
*/
|
||||
|
||||
/* BufferType requires the following members
|
||||
* Size size()
|
||||
* Result<Size> read(void* data, Size bytes_to_read)
|
||||
* Result<Offset> seek(uint32_t offset)
|
||||
*/
|
||||
|
||||
/* Wraps a buffer and provides an API for accessing lines efficiently. */
|
||||
template <typename BufferType, uint32_t CacheSize>
|
||||
class BufferWrapper {
|
||||
public:
|
||||
using Offset = uint32_t;
|
||||
using Line = uint32_t;
|
||||
using Column = uint32_t;
|
||||
using Range = struct {
|
||||
// Offset of the line start.
|
||||
Offset start;
|
||||
// Offset of one past the line end.
|
||||
Offset end;
|
||||
};
|
||||
|
||||
BufferWrapper(BufferType* buffer)
|
||||
: wrapped_{buffer} {
|
||||
initialize();
|
||||
}
|
||||
virtual ~BufferWrapper() {}
|
||||
|
||||
/* Prevent copies */
|
||||
BufferWrapper(const BufferWrapper&) = delete;
|
||||
BufferWrapper& operator=(const BufferWrapper&) = delete;
|
||||
|
||||
Optional<std::string> get_text(Line line, Column col, Offset length) {
|
||||
auto range = line_range(line);
|
||||
int32_t to_read = length;
|
||||
|
||||
if (!range)
|
||||
return {};
|
||||
|
||||
// Don't read past end of line.
|
||||
if (range->start + col + to_read >= range->end)
|
||||
to_read = range->end - col - range->start;
|
||||
|
||||
if (to_read <= 0)
|
||||
return {};
|
||||
|
||||
return read(range->start + col, to_read);
|
||||
}
|
||||
|
||||
/* Gets the size of the buffer in bytes. */
|
||||
File::Size size() const { return wrapped_->size(); }
|
||||
|
||||
/* Get the count of the lines in the buffer. */
|
||||
uint32_t line_count() const { return line_count_; }
|
||||
|
||||
/* Gets the range of the line if valid. */
|
||||
Optional<Range> line_range(Line line) {
|
||||
ensure_cached(line);
|
||||
|
||||
auto offset = offset_for_line(line);
|
||||
if (!offset)
|
||||
return {};
|
||||
|
||||
auto start = *offset == 0 ? start_offset_ : (newlines_[*offset - 1] + 1);
|
||||
auto end = newlines_[*offset] + 1;
|
||||
|
||||
return Range{start, end};
|
||||
}
|
||||
|
||||
/* Gets the length of the line, or 0 if invalid. */
|
||||
Offset line_length(Line line) {
|
||||
auto range = line_range(line);
|
||||
|
||||
if (range)
|
||||
return range->end - range->start;
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
/* Gets the index of the first line in the cache.
|
||||
* Only really useful for unit testing or diagnostics. */
|
||||
Offset start_line() { return start_line_; };
|
||||
|
||||
protected:
|
||||
BufferWrapper() {}
|
||||
|
||||
void set_buffer(BufferType* buffer) {
|
||||
wrapped_ = buffer;
|
||||
initialize();
|
||||
}
|
||||
|
||||
private:
|
||||
/* Number of newline offsets to cache. */
|
||||
static constexpr Offset max_newlines = CacheSize;
|
||||
|
||||
/* Size of stack buffer used for reading/writing. */
|
||||
static constexpr size_t buffer_size = 512;
|
||||
|
||||
void initialize() {
|
||||
start_offset_ = 0;
|
||||
start_line_ = 0;
|
||||
line_count_ = 0;
|
||||
newlines_.clear();
|
||||
|
||||
// Special case for empty files to keep them consistent.
|
||||
if (size() == 0) {
|
||||
line_count_ = 1;
|
||||
newlines_.push_back(0);
|
||||
return;
|
||||
}
|
||||
|
||||
Offset offset = 0;
|
||||
auto result = next_newline(offset);
|
||||
|
||||
while (result) {
|
||||
++line_count_;
|
||||
if (newlines_.size() < max_newlines)
|
||||
newlines_.push_back(*result);
|
||||
offset = *result + 1;
|
||||
result = next_newline(offset);
|
||||
}
|
||||
}
|
||||
|
||||
Optional<std::string> read(Offset offset, Offset length) {
|
||||
if (offset + length > size())
|
||||
return {};
|
||||
|
||||
std::string buffer;
|
||||
buffer.resize(length);
|
||||
wrapped_->seek(offset);
|
||||
|
||||
auto result = wrapped_->read(&buffer[0], length);
|
||||
if (result.is_error())
|
||||
// TODO: better error handling.
|
||||
return std::string{"[Bad Read]"};
|
||||
|
||||
buffer.resize(*result);
|
||||
return buffer;
|
||||
}
|
||||
|
||||
/* Returns the offset of the line in the newline cache if valid. */
|
||||
Optional<Offset> offset_for_line(Line line) const {
|
||||
if (line >= line_count_)
|
||||
return {};
|
||||
|
||||
Offset actual = line - start_line_;
|
||||
if (actual >= newlines_.size()) // NB: underflow wrap.
|
||||
return {};
|
||||
|
||||
return actual;
|
||||
}
|
||||
|
||||
/* Ensure specified line is in the newline cache. */
|
||||
void ensure_cached(Line line) {
|
||||
if (line >= line_count_)
|
||||
return;
|
||||
|
||||
auto result = offset_for_line(line);
|
||||
if (result)
|
||||
return;
|
||||
|
||||
if (line < start_line_) {
|
||||
while (line < start_line_ && start_offset_ >= 2) {
|
||||
// start_offset_ - 1 should be a newline. Need to
|
||||
// find the new value for start_offset_. start_line_
|
||||
// has to be > 0 to get into this block so there should
|
||||
// always be one newline before start_offset_.
|
||||
auto offset = previous_newline(start_offset_ - 2);
|
||||
newlines_.push_front(start_offset_ - 1);
|
||||
|
||||
if (!offset) {
|
||||
// Must be at beginning.
|
||||
start_line_ = 0;
|
||||
start_offset_ = 0;
|
||||
} else {
|
||||
// Found an previous newline, the new start_line_
|
||||
// starts at the newline offset + 1.
|
||||
start_line_--;
|
||||
start_offset_ = *offset + 1;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
while (line >= start_line_ + newlines_.size()) {
|
||||
auto offset = next_newline(newlines_.back() + 1);
|
||||
if (offset) {
|
||||
start_line_++;
|
||||
start_offset_ = newlines_.front() + 1;
|
||||
newlines_.push_back(*offset);
|
||||
} /* else at the EOF. */
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/* Helpers for finding the prev/next newline. */
|
||||
Optional<Offset> previous_newline(Offset offset) {
|
||||
char buffer[buffer_size];
|
||||
auto to_read = buffer_size;
|
||||
|
||||
do {
|
||||
if (offset < to_read) {
|
||||
// NB: Char at 'offset' was read in the previous iteration.
|
||||
to_read = offset;
|
||||
offset = 0;
|
||||
} else
|
||||
offset -= to_read;
|
||||
|
||||
wrapped_->seek(offset);
|
||||
|
||||
auto result = wrapped_->read(buffer, to_read);
|
||||
if (result.is_error())
|
||||
break;
|
||||
|
||||
// Find newlines in the buffer backwards.
|
||||
for (int32_t i = *result - 1; i >= 0; --i) {
|
||||
switch (buffer[i]) {
|
||||
case '\n':
|
||||
return offset + i;
|
||||
}
|
||||
}
|
||||
|
||||
if (offset == 0)
|
||||
break;
|
||||
|
||||
} while (true);
|
||||
|
||||
return {}; // Didn't find one.
|
||||
}
|
||||
|
||||
Optional<Offset> next_newline(Offset offset) {
|
||||
// EOF, no more newlines to find.
|
||||
if (offset >= size())
|
||||
return {};
|
||||
|
||||
char buffer[buffer_size];
|
||||
wrapped_->seek(offset);
|
||||
|
||||
while (true) {
|
||||
auto result = wrapped_->read(buffer, buffer_size);
|
||||
if (result.is_error())
|
||||
return {};
|
||||
|
||||
// Find newlines in the buffer.
|
||||
for (Offset i = 0; i < *result; ++i) {
|
||||
switch (buffer[i]) {
|
||||
case '\n':
|
||||
return offset + i;
|
||||
}
|
||||
}
|
||||
|
||||
offset += *result;
|
||||
|
||||
if (*result < buffer_size)
|
||||
break;
|
||||
}
|
||||
|
||||
// For consistency, treat the end of the file as a "newline".
|
||||
return size() - 1;
|
||||
}
|
||||
|
||||
BufferType* wrapped_{};
|
||||
|
||||
/* Total number of lines in the buffer. */
|
||||
Offset line_count_{0};
|
||||
|
||||
/* The offset and line of the newlines cache. */
|
||||
Offset start_offset_{0};
|
||||
Offset start_line_{0};
|
||||
|
||||
LineEnding line_ending_{LineEnding::LF};
|
||||
CircularBuffer<Offset, max_newlines + 1> newlines_{};
|
||||
};
|
||||
|
||||
/* A BufferWrapper over a file. */
|
||||
class FileWrapper : public BufferWrapper<File, 64> {
|
||||
public:
|
||||
template <typename T>
|
||||
using Result = File::Result<T>;
|
||||
using Error = File::Error;
|
||||
static Result<std::unique_ptr<FileWrapper>> open(const std::filesystem::path& path) {
|
||||
auto fw = std::unique_ptr<FileWrapper>(new FileWrapper());
|
||||
auto error = fw->file_.open(path);
|
||||
|
||||
if (error)
|
||||
return *error;
|
||||
|
||||
fw->initialize();
|
||||
return fw;
|
||||
}
|
||||
|
||||
private:
|
||||
FileWrapper() {}
|
||||
void initialize() {
|
||||
set_buffer(&file_);
|
||||
}
|
||||
|
||||
File file_{};
|
||||
};
|
||||
|
||||
template <uint32_t CacheSize = 64, typename T>
|
||||
BufferWrapper<T, CacheSize> wrap_buffer(T& buffer) {
|
||||
return {&buffer};
|
||||
}
|
||||
|
||||
#endif // __FILE_WRAPPER_HPP__
|
@ -36,11 +36,13 @@ add_executable(application_test EXCLUDE_FROM_ALL
|
||||
${PROJECT_SOURCE_DIR}/main.cpp
|
||||
${PROJECT_SOURCE_DIR}/test_basics.cpp
|
||||
${PROJECT_SOURCE_DIR}/test_circular_buffer.cpp
|
||||
${PROJECT_SOURCE_DIR}/test_file_wrapper.cpp
|
||||
${PROJECT_SOURCE_DIR}/test_optional.cpp
|
||||
)
|
||||
|
||||
target_include_directories(application_test PRIVATE
|
||||
${DOCTESTINC}
|
||||
${PROJECT_SOURCE_DIR}/../../application
|
||||
${COMMON}
|
||||
${PORTINC}
|
||||
${KERNINC}
|
||||
@ -49,6 +51,7 @@ target_include_directories(application_test PRIVATE
|
||||
${PLATFORMINC}
|
||||
${BOARDINC}
|
||||
${CHIBIOS}/os/various
|
||||
${FATFSINC}
|
||||
${BASEBAND}
|
||||
)
|
||||
|
||||
|
346
firmware/test/application/test_file_wrapper.cpp
Normal file
346
firmware/test/application/test_file_wrapper.cpp
Normal file
@ -0,0 +1,346 @@
|
||||
/*
|
||||
* Copyright (C) 2023
|
||||
*
|
||||
* This file is part of PortaPack.
|
||||
*
|
||||
* This program is free software; you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation; either version 2, or (at your option)
|
||||
* any later version.
|
||||
*
|
||||
* 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 for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program; see the file COPYING. If not, write to
|
||||
* the Free Software Foundation, Inc., 51 Franklin Street,
|
||||
* Boston, MA 02110-1301, USA.
|
||||
*/
|
||||
|
||||
#include "doctest.h"
|
||||
#include "file.hpp"
|
||||
#include "file_wrapper.hpp"
|
||||
|
||||
#include <cstring>
|
||||
#include <cstdio>
|
||||
#include <string>
|
||||
|
||||
/* Mocks the File interface with a backing string. */
|
||||
class MockFile {
|
||||
public:
|
||||
using Error = File::Error;
|
||||
using Offset = File::Offset;
|
||||
using Size = File::Size;
|
||||
template <typename T>
|
||||
using Result = File::Result<T>;
|
||||
|
||||
MockFile(std::string data)
|
||||
: data_{std::move(data)} {}
|
||||
|
||||
Size size() { return data_.size(); }
|
||||
|
||||
Result<Offset> seek(uint32_t offset) {
|
||||
if (offset >= size())
|
||||
return {static_cast<Error>(FR_BAD_SEEK)};
|
||||
|
||||
auto previous = offset_;
|
||||
offset_ = offset;
|
||||
return previous;
|
||||
}
|
||||
|
||||
Result<Size> read(void* data, Size bytes_to_read) {
|
||||
if (offset_ + bytes_to_read > size())
|
||||
bytes_to_read = size() - offset_;
|
||||
|
||||
if (bytes_to_read == 0 || bytes_to_read > size()) // NB: underflow wrap
|
||||
return 0;
|
||||
|
||||
memcpy(data, &data_[offset_], bytes_to_read);
|
||||
offset_ += bytes_to_read;
|
||||
return bytes_to_read;
|
||||
}
|
||||
|
||||
std::string data_;
|
||||
uint32_t offset_{0};
|
||||
};
|
||||
|
||||
/* Verifies correctness of MockFile. */
|
||||
TEST_SUITE("Test MockFile") {
|
||||
SCENARIO("File size") {
|
||||
GIVEN("Empty string") {
|
||||
MockFile f{""};
|
||||
|
||||
THEN("size() should be 0.") {
|
||||
CHECK_EQ(f.size(), 0);
|
||||
}
|
||||
}
|
||||
|
||||
GIVEN("Not empty string") {
|
||||
MockFile f{"abc"};
|
||||
|
||||
THEN("size() should be string length.") {
|
||||
CHECK_EQ(f.size(), 3);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
SCENARIO("File seek") {
|
||||
GIVEN("Valid file") {
|
||||
MockFile f{"abc\ndef"};
|
||||
|
||||
WHEN("seek() negative offset") {
|
||||
auto r = f.seek(-1);
|
||||
|
||||
THEN("Result should be bad_seek") {
|
||||
CHECK(r.is_error());
|
||||
CHECK_EQ(r.error().code(), FR_BAD_SEEK);
|
||||
}
|
||||
}
|
||||
|
||||
WHEN("seek() offset is size()") {
|
||||
auto r = f.seek(f.size());
|
||||
|
||||
THEN("Result should be bad_seek") {
|
||||
CHECK(r.is_error());
|
||||
CHECK_EQ(r.error().code(), FR_BAD_SEEK);
|
||||
}
|
||||
}
|
||||
|
||||
WHEN("seek() offset > size()") {
|
||||
auto r = f.seek(f.size() + 1);
|
||||
|
||||
THEN("Result should be bad_seek") {
|
||||
CHECK(r.is_error());
|
||||
CHECK_EQ(r.error().code(), FR_BAD_SEEK);
|
||||
}
|
||||
}
|
||||
|
||||
WHEN("seek() offset < size()") {
|
||||
auto r = f.seek(1);
|
||||
|
||||
THEN("Result should be ok") {
|
||||
CHECK(r.is_ok());
|
||||
}
|
||||
|
||||
r = f.seek(3);
|
||||
|
||||
THEN("Result should be previous offset") {
|
||||
CHECK(r);
|
||||
CHECK_EQ(*r, 1);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
SCENARIO("File read") {
|
||||
GIVEN("Valid file") {
|
||||
MockFile f{"abc\ndef"};
|
||||
|
||||
const auto buf_len = 10;
|
||||
std::string buf;
|
||||
buf.resize(buf_len);
|
||||
|
||||
WHEN("Reading") {
|
||||
auto r = f.read(&buf[0], 3);
|
||||
|
||||
THEN("Result should be number of bytes read") {
|
||||
CHECK(r);
|
||||
CHECK_EQ(*r, 3);
|
||||
}
|
||||
|
||||
buf.resize(*r);
|
||||
THEN("Buffer should contain read data") {
|
||||
CHECK_EQ(buf.length(), 3);
|
||||
CHECK_EQ(buf, "abc");
|
||||
}
|
||||
|
||||
r = f.read(&buf[0], 3);
|
||||
THEN("Reading should continue where it left off") {
|
||||
CHECK_EQ(buf.length(), 3);
|
||||
CHECK_EQ(buf, "\nde");
|
||||
}
|
||||
|
||||
r = f.read(&buf[0], 3);
|
||||
THEN("Reading should stop at the end of the file") {
|
||||
CHECK(r);
|
||||
CHECK_EQ(*r, 1);
|
||||
|
||||
buf.resize(*r);
|
||||
CHECK_EQ(buf.length(), 1);
|
||||
CHECK_EQ(buf, "f");
|
||||
}
|
||||
}
|
||||
|
||||
WHEN("Reading block larger than file size") {
|
||||
auto r = f.read(&buf[0], buf_len);
|
||||
buf.resize(*r);
|
||||
|
||||
THEN("It should read to file end.") {
|
||||
CHECK(r);
|
||||
CHECK_EQ(*r, 7);
|
||||
CHECK_EQ(buf, f.data_);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
TEST_SUITE_BEGIN("Test BufferWrapper");
|
||||
|
||||
TEST_CASE("It can wrap a MockFile.") {
|
||||
MockFile f{""};
|
||||
auto w = wrap_buffer(f);
|
||||
}
|
||||
|
||||
SCENARIO("Empty file") {
|
||||
GIVEN("An empty file") {
|
||||
MockFile f{""};
|
||||
auto w = wrap_buffer(f);
|
||||
|
||||
WHEN("Initializing") {
|
||||
CHECK_EQ(w.size(), 0);
|
||||
REQUIRE_EQ(w.line_count(), 1);
|
||||
}
|
||||
|
||||
WHEN("Getting line_range()") {
|
||||
auto r = w.line_range(0);
|
||||
CHECK(r);
|
||||
CHECK_EQ(r->start, 0);
|
||||
CHECK_EQ(r->end, 1);
|
||||
}
|
||||
|
||||
WHEN("Getting line_length()") {
|
||||
CHECK_EQ(w.line_length(0), 1);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
SCENARIO("Basic file") {
|
||||
GIVEN("A file") {
|
||||
MockFile f{"abc\ndef"};
|
||||
auto w = wrap_buffer(f);
|
||||
|
||||
WHEN("Initializing") {
|
||||
CHECK_EQ(w.size(), 7);
|
||||
REQUIRE_EQ(w.line_count(), 2);
|
||||
}
|
||||
|
||||
WHEN("Getting line_range()") {
|
||||
auto r = w.line_range(0);
|
||||
CHECK(r);
|
||||
CHECK_EQ(r->start, 0);
|
||||
CHECK_EQ(r->end, 4);
|
||||
|
||||
r = w.line_range(1);
|
||||
CHECK(r);
|
||||
CHECK_EQ(r->start, 4);
|
||||
CHECK_EQ(r->end, 7);
|
||||
}
|
||||
|
||||
WHEN("Getting line_length()") {
|
||||
CHECK_EQ(w.line_length(0), 4);
|
||||
CHECK_EQ(w.line_length(1), 3);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
SCENARIO("Reading file lines.") {
|
||||
GIVEN("A valid file") {
|
||||
MockFile f{"abc\ndef"};
|
||||
auto w = wrap_buffer(f);
|
||||
|
||||
WHEN("Reading a line") {
|
||||
auto str = w.get_text(0, 0, 10);
|
||||
|
||||
THEN("It should read exactly one line.") {
|
||||
REQUIRE(str);
|
||||
CHECK_EQ(str->length(), 4); // Includes '\n'
|
||||
CHECK_EQ(*str, "abc\n");
|
||||
}
|
||||
}
|
||||
|
||||
WHEN("Reading the last line") {
|
||||
auto str = w.get_text(w.line_count() - 1, 0, 10);
|
||||
|
||||
THEN("It should read exactly one line.") {
|
||||
REQUIRE(str);
|
||||
CHECK_EQ(str->length(), 3);
|
||||
CHECK_EQ(*str, "def");
|
||||
}
|
||||
}
|
||||
|
||||
WHEN("Reading past the last line") {
|
||||
auto str = w.get_text(w.line_count(), 0, 10);
|
||||
|
||||
THEN("It should return empty value.") {
|
||||
REQUIRE(!str);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
SCENARIO("Reading with cache miss.") {
|
||||
GIVEN("A valid file") {
|
||||
MockFile f{"abc\ndef\nghi\njkl\nmno"};
|
||||
constexpr uint32_t cache_size = 2;
|
||||
auto w = wrap_buffer<cache_size>(f);
|
||||
|
||||
CHECK_EQ(w.start_line(), 0);
|
||||
|
||||
WHEN("Reading a cached line") {
|
||||
auto str = w.get_text(0, 0, 10);
|
||||
|
||||
THEN("It should read exactly one line.") {
|
||||
REQUIRE(str);
|
||||
CHECK_EQ(*str, "abc\n");
|
||||
}
|
||||
}
|
||||
|
||||
WHEN("Reading line after last cached line.") {
|
||||
auto str = w.get_text(w.line_count() - 1, 0, 10);
|
||||
|
||||
THEN("It should read exactly one line.") {
|
||||
REQUIRE(str);
|
||||
CHECK_EQ(*str, "mno");
|
||||
}
|
||||
|
||||
THEN("It should push cache window forward to include line.") {
|
||||
CHECK_EQ(w.start_line(), w.line_count() - cache_size);
|
||||
}
|
||||
}
|
||||
|
||||
WHEN("Reading line before first cached line.") {
|
||||
// First move cache forward to end.
|
||||
w.get_text(w.line_count() - 1, 0, 10);
|
||||
auto str = w.get_text(1, 0, 10);
|
||||
|
||||
THEN("It should read exactly one line.") {
|
||||
REQUIRE(str);
|
||||
CHECK_EQ(*str, "def\n");
|
||||
}
|
||||
|
||||
THEN("It should push cache window backward to include line.") {
|
||||
CHECK_EQ(w.start_line(), 1);
|
||||
}
|
||||
}
|
||||
|
||||
WHEN("Reading line 0 before first cached line.") {
|
||||
// First move cache forward to end, then back to beginning.
|
||||
w.get_text(w.line_count() - 1, 0, 10);
|
||||
auto str = w.get_text(0, 0, 10);
|
||||
|
||||
THEN("It should read exactly one line.") {
|
||||
REQUIRE(str);
|
||||
CHECK_EQ(*str, "abc\n");
|
||||
}
|
||||
|
||||
THEN("It should push cache window backward to include line.") {
|
||||
CHECK_EQ(w.start_line(), 0);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
TEST_SUITE_END();
|
Loading…
Reference in New Issue
Block a user