222 lines
7.8 KiB
C++
222 lines
7.8 KiB
C++
#pragma once
|
|
|
|
#include <algorithm>
|
|
#include <array>
|
|
#include <cassert>
|
|
#include <cstdint>
|
|
#include <cstring>
|
|
#include <limits>
|
|
#include <map>
|
|
#include <string>
|
|
#include <vector>
|
|
#include <Arduino.h>
|
|
enum { FILE_O_READ = 0, FILE_O_WRITE = 1 };
|
|
|
|
class FakeFilesystem;
|
|
class File {
|
|
FakeFilesystem* _fs = nullptr;
|
|
std::string _path;
|
|
size_t _position = 0;
|
|
bool _write = false;
|
|
unsigned _read_generation = 0;
|
|
std::vector<uint8_t> _read_inode;
|
|
size_t _stdio_size = 4096, _backend_position = 0;
|
|
std::vector<uint8_t> _stdio_buffer;
|
|
size_t _read_buffer_position = 0;
|
|
void flushWriteBuffer();
|
|
public:
|
|
File() = default;
|
|
explicit File(FakeFilesystem& fs) : _fs(&fs) {}
|
|
File(FakeFilesystem* fs, const char* path, bool write);
|
|
bool open(const char* path, uint8_t mode);
|
|
explicit operator bool() const { return _fs != nullptr; }
|
|
size_t read(uint8_t* bytes, size_t length);
|
|
size_t write(const uint8_t* bytes, size_t length);
|
|
size_t size() const;
|
|
bool seek(size_t position);
|
|
bool setBufferSize(size_t size);
|
|
void flush();
|
|
void close();
|
|
};
|
|
|
|
class FakeFilesystem {
|
|
struct Config { uint32_t block_count = 256, block_size = 4096; } _config;
|
|
struct Lfs { const Config* cfg; } _lfs{&_config};
|
|
public:
|
|
using Files = std::map<std::string, std::vector<uint8_t>>;
|
|
Files files;
|
|
size_t capacity = 1024 * 1024;
|
|
size_t max_write = std::numeric_limits<size_t>::max();
|
|
size_t largest_read = 0, largest_write = 0;
|
|
// ESP's File wrapper is a buffered FILE*. Enable its 4 KiB default in
|
|
// production transaction tests, and count backend I/O separately from
|
|
// fwrite/fread calls so logical record/chunk bounds cannot hide batching.
|
|
bool emulate_stdio = false;
|
|
size_t largest_backend_read = 0, largest_backend_write = 0;
|
|
unsigned backend_reads = 0, backend_writes = 0;
|
|
unsigned buffer_config_calls = 0, fail_buffer_config = 0;
|
|
std::string fail_read;
|
|
bool metadata_error = false, fail_create = false, fail_remove = false;
|
|
bool partial_create_failure = false;
|
|
unsigned metadata_probes = 0, fail_metadata_probe = 0;
|
|
unsigned removes = 0, missing_remove_logs = 0;
|
|
unsigned fail_rename = 0, renames = 0, writes = 0, reads = 0, opens = 0;
|
|
std::vector<Files> rename_snapshots;
|
|
std::map<std::string, unsigned> generations;
|
|
bool exists(const char* path) const { return files.count(path) != 0; }
|
|
bool mkdir(const char*) { return true; }
|
|
bool remove(const char* path) {
|
|
++removes;
|
|
if (!exists(path)) { ++missing_remove_logs; return false; }
|
|
if (fail_remove) return false;
|
|
++generations[path];
|
|
return files.erase(path) != 0;
|
|
}
|
|
File open(const char* path, const char* mode = "r", bool = false) {
|
|
++opens;
|
|
if (*mode != 'r') {
|
|
if (fail_create) {
|
|
if (partial_create_failure) files[path].clear();
|
|
return File();
|
|
}
|
|
files[path].clear();
|
|
return File(this, path, true);
|
|
}
|
|
if (fail_read == path || !exists(path)) return File();
|
|
return File(this, path, false);
|
|
}
|
|
unsigned startupOperations() const {
|
|
return metadata_probes + opens + removes + renames + buffer_config_calls;
|
|
}
|
|
bool rename(const char* from, const char* to) {
|
|
++renames;
|
|
if (renames == fail_rename || !exists(from)) return false;
|
|
#if defined(ESP32_PLATFORM)
|
|
if (exists(to)) return false;
|
|
#endif
|
|
files[to] = files.at(from);
|
|
files.erase(from);
|
|
++generations[from]; ++generations[to];
|
|
rename_snapshots.push_back(files);
|
|
return true;
|
|
}
|
|
size_t totalBytes() const { return capacity; }
|
|
void setBlockCount(uint32_t count) { _config.block_count = count; }
|
|
Lfs* _getFS() { _lfs.cfg = &_config; return &_lfs; }
|
|
size_t usedBytes() const {
|
|
size_t total = 0;
|
|
for (const auto& f : files) total += f.second.size();
|
|
return total;
|
|
}
|
|
} SPIFFS;
|
|
|
|
int _getLfsUsedBlockCount(FakeFilesystem* fs) {
|
|
unsigned used = 2; // directory metadata pair
|
|
for (const auto& file : fs->files) used += (file.second.size() + 4095) / 4096;
|
|
return used;
|
|
}
|
|
|
|
File::File(FakeFilesystem* fs, const char* path, bool write)
|
|
: _fs(fs), _path(path), _write(write) {
|
|
if (!write) {
|
|
_read_generation = fs->generations[path];
|
|
_read_inode = fs->files.at(path);
|
|
}
|
|
}
|
|
bool File::open(const char* path, uint8_t mode) {
|
|
if (!_fs) return false;
|
|
*this = _fs->open(path, mode == FILE_O_WRITE ? "w" : "r");
|
|
return static_cast<bool>(*this);
|
|
}
|
|
size_t File::read(uint8_t* bytes, size_t length) {
|
|
if (!_fs || _write || _fs->fail_read == _path) return 0;
|
|
++_fs->reads;
|
|
_fs->largest_read = std::max(_fs->largest_read, length);
|
|
// An open file keeps its old inode across a rename/unlink. A later File
|
|
// opened on the same name observes the replacement, as SPIFFS/POSIX do.
|
|
auto& data = _fs->generations[_path] == _read_generation
|
|
? _fs->files.at(_path) : _read_inode;
|
|
if (_fs->emulate_stdio) {
|
|
size_t copied = 0;
|
|
while (copied < length) {
|
|
if (_read_buffer_position == _stdio_buffer.size()) {
|
|
const size_t available = data.size() - std::min(data.size(), _backend_position);
|
|
const size_t count = std::min(_stdio_size, available);
|
|
if (count == 0) break;
|
|
++_fs->backend_reads;
|
|
_fs->largest_backend_read = std::max(_fs->largest_backend_read, count);
|
|
_stdio_buffer.assign(data.begin() + _backend_position,
|
|
data.begin() + _backend_position + count);
|
|
_backend_position += count;
|
|
_read_buffer_position = 0;
|
|
}
|
|
const size_t count = std::min(length - copied,
|
|
_stdio_buffer.size() - _read_buffer_position);
|
|
memcpy(bytes + copied, _stdio_buffer.data() + _read_buffer_position, count);
|
|
_read_buffer_position += count;
|
|
copied += count;
|
|
}
|
|
_position += copied;
|
|
return copied;
|
|
}
|
|
const size_t count = std::min(length, data.size() - std::min(data.size(), _position));
|
|
if (count) memcpy(bytes, data.data() + _position, count);
|
|
_position += count;
|
|
return count;
|
|
}
|
|
size_t File::write(const uint8_t* bytes, size_t length) {
|
|
if (!_fs || !_write) return 0;
|
|
++_fs->writes;
|
|
_fs->largest_write = std::max(_fs->largest_write, length);
|
|
const size_t count = std::min(length, _fs->max_write);
|
|
if (_fs->emulate_stdio) {
|
|
size_t consumed = 0;
|
|
while (consumed < count) {
|
|
if (_stdio_buffer.size() == _stdio_size) flushWriteBuffer();
|
|
const size_t part = std::min(count - consumed, _stdio_size - _stdio_buffer.size());
|
|
_stdio_buffer.insert(_stdio_buffer.end(), bytes + consumed, bytes + consumed + part);
|
|
consumed += part;
|
|
if (_stdio_buffer.size() == _stdio_size) flushWriteBuffer();
|
|
}
|
|
_position += count;
|
|
return count;
|
|
}
|
|
auto& data = _fs->files[_path];
|
|
data.resize(_position + count);
|
|
memcpy(data.data() + _position, bytes, count);
|
|
_position += count;
|
|
return count;
|
|
}
|
|
size_t File::size() const {
|
|
if (!_fs) return 0;
|
|
return !_write && _fs->generations[_path] != _read_generation
|
|
? _read_inode.size() : _fs->files.at(_path).size();
|
|
}
|
|
bool File::seek(size_t pos) {
|
|
if (!_fs || pos > size()) return false;
|
|
_position = pos;
|
|
_backend_position = pos;
|
|
_stdio_buffer.clear();
|
|
_read_buffer_position = 0;
|
|
return true;
|
|
}
|
|
bool File::setBufferSize(size_t size) {
|
|
assert(_position == 0 && _stdio_buffer.empty()); // before the stream's first I/O
|
|
if (!_fs || size == 0) return false;
|
|
if (++_fs->buffer_config_calls == _fs->fail_buffer_config) return false;
|
|
_stdio_size = size;
|
|
return true;
|
|
}
|
|
void File::flushWriteBuffer() {
|
|
if (!_fs || !_write || _stdio_buffer.empty()) return;
|
|
++_fs->backend_writes;
|
|
_fs->largest_backend_write = std::max(_fs->largest_backend_write, _stdio_buffer.size());
|
|
auto& data = _fs->files[_path];
|
|
data.resize(_backend_position + _stdio_buffer.size());
|
|
memcpy(data.data() + _backend_position, _stdio_buffer.data(), _stdio_buffer.size());
|
|
_backend_position += _stdio_buffer.size();
|
|
_stdio_buffer.clear();
|
|
}
|
|
void File::flush() { if (_fs && _fs->emulate_stdio && _write) flushWriteBuffer(); }
|
|
void File::close() { flush(); _fs = nullptr; }
|