Files

230 lines
7.5 KiB
C++

#include "CardputerDataStore.h"
#include <SD.h> // Keep optional framework-SD discovery local to this variant.
#include <helpers/ContactFileTransaction.h>
#include <errno.h>
#include <math.h>
#include <sys/stat.h>
#include "globals.h"
static const char CHANNEL_HISTORY_ID_DIR[] = HISTORY_DIR "/chan2";
static void buf_to_hex_fast(const unsigned char *src, size_t src_len, char *dst) {
static const char hex_digits[] = "0123456789abcdef";
for (size_t i = 0; i < src_len; i++) {
dst[i * 2] = hex_digits[(src[i] >> 4) & 0x0F]; // High nibble
dst[i * 2 + 1] = hex_digits[src[i] & 0x0F]; // Low nibble
}
dst[src_len * 2] = '\0';
}
void CardputerDataStore::beginCustom(FILESYSTEM *history_fs) {
_history_fs = history_fs;
if (!_history_fs) {
return;
}
_history_fs->mkdir(CUSTOM_DATA_DIR);
_history_fs->mkdir(HISTORY_DIR);
// Leave ambiguous legacy PSK-only /chan histories intact for recovery.
_history_fs->mkdir(CHANNEL_HISTORY_ID_DIR);
_history_fs->mkdir(HISTORY_DIR_DIRECT);
}
static bool validCustomPrefs(const CustomNodePrefs &prefs) {
return isfinite(prefs.battery_correction) && prefs.battery_correction >= 0.5f
&& prefs.battery_correction <= 2.0f && prefs.text_level < 3;
}
static bool customPrefsPresence(FILESYSTEM *fs, const char *path, bool &present) {
(void)fs; // Custom preferences always use the default internal SPIFFS mount.
char vfs_path[96];
const int length = snprintf(vfs_path, sizeof(vfs_path), "/spiffs%s", path);
if (length < 0 || static_cast<size_t>(length) >= sizeof(vfs_path)) return false;
struct stat info;
const int result = ::stat(vfs_path, &info);
if (result != 0 && errno != ENOENT) return false;
present = result == 0;
return true;
}
static bool readCustomPrefs(FILESYSTEM *fs, const char *path, CustomNodePrefs &prefs) {
File file = fs->open(path, FILE_READ);
if (!file) return false;
CustomNodePrefs candidate;
const bool valid = !file.isDirectory() && file.size() == sizeof(candidate)
&& file.read(reinterpret_cast<uint8_t *>(&candidate), sizeof(candidate)) == sizeof(candidate)
&& validCustomPrefs(candidate);
file.close();
if (valid) prefs = candidate;
return valid;
}
bool CardputerDataStore::loadCustomPrefs(CustomNodePrefs &prefs) {
_custom_prefs_write_blocked = false;
bool present = false, backup_present = false;
if (!customPrefsPresence(_fs, CUSTOM_PREFS_FILE, present)
|| !customPrefsPresence(_fs, CUSTOM_PREFS_FILE ".bak", backup_present)) {
_custom_prefs_write_blocked = true;
return false;
}
if (present && readCustomPrefs(_fs, CUSTOM_PREFS_FILE, prefs)) return true;
CustomNodePrefs backup;
if (backup_present && readCustomPrefs(_fs, CUSTOM_PREFS_FILE ".bak", backup)) {
prefs = backup;
if (present && !_fs->remove(CUSTOM_PREFS_FILE)) {
_custom_prefs_write_blocked = true;
return true;
}
_fs->rename(CUSTOM_PREFS_FILE ".bak", CUSTOM_PREFS_FILE);
CustomNodePrefs verified;
_custom_prefs_write_blocked = !readCustomPrefs(_fs, CUSTOM_PREFS_FILE, verified)
|| memcmp(&verified, &backup, sizeof(backup)) != 0;
return true;
}
// An unreadable existing image is not permission to replace its only copy.
_custom_prefs_write_blocked = present || backup_present;
return false;
}
bool CardputerDataStore::saveCustomPrefs(const CustomNodePrefs &prefs) {
if (_custom_prefs_write_blocked || !validCustomPrefs(prefs)) return false;
mesh::ContactFileTransaction transaction(_fs, CUSTOM_PREFS_FILE, customPrefsPresence);
bool valid = transaction && transaction.write(reinterpret_cast<const uint8_t *>(&prefs), sizeof(prefs)) == sizeof(prefs);
mesh::ContactFileTransaction::CommitProgress progress;
do {
progress = transaction.serviceCommit(valid, 1, true);
} while (progress == mesh::ContactFileTransaction::CommitProgress::Pending);
if (progress != mesh::ContactFileTransaction::CommitProgress::Succeeded) {
_custom_prefs_write_blocked = transaction.outcome() == mesh::ContactFileTransaction::Outcome::Indeterminate;
return false;
}
CustomNodePrefs verified;
if (readCustomPrefs(_fs, CUSTOM_PREFS_FILE, verified) && memcmp(&verified, &prefs, sizeof(prefs)) == 0) {
_fs->remove(CUSTOM_PREFS_FILE ".bak");
return true;
}
// Keep the old verified image until the public candidate has been read back.
CustomNodePrefs backup;
bool backup_present = false;
if (!customPrefsPresence(_fs, CUSTOM_PREFS_FILE ".bak", backup_present)
|| (backup_present && !readCustomPrefs(_fs, CUSTOM_PREFS_FILE ".bak", backup))
|| !_fs->remove(CUSTOM_PREFS_FILE)) {
_custom_prefs_write_blocked = true;
return false;
}
if (backup_present) {
_fs->rename(CUSTOM_PREFS_FILE ".bak", CUSTOM_PREFS_FILE);
_custom_prefs_write_blocked = !readCustomPrefs(_fs, CUSTOM_PREFS_FILE, verified)
|| memcmp(&verified, &backup, sizeof(backup)) != 0;
} else {
bool present = false;
_custom_prefs_write_blocked = !customPrefsPresence(_fs, CUSTOM_PREFS_FILE, present) || present;
}
return false;
}
void CardputerDataStore::storeMessage(const uint8_t pkey[PUB_KEY_SIZE], const char *text, bool is_sent,
bool is_channel) {
if (!_history_fs) {
return;
}
char path[128];
char msg_copy[MAX_MESSAGE_LENGTH + 1] = { 0 };
char pkey_hex[PUB_KEY_SIZE * 2 + 1];
buf_to_hex_fast(pkey, PUB_KEY_SIZE, pkey_hex);
int msg_len;
for (msg_len = 0; msg_len < MAX_MESSAGE_LENGTH && text[msg_len] != '\0'; msg_len++) {
if (text[msg_len] == '\n' || text[msg_len] == '\r') {
msg_copy[msg_len] = ' '; // Swap newline with space
} else {
msg_copy[msg_len] = text[msg_len];
}
}
msg_copy[msg_len] = '\0';
snprintf(path, sizeof(path), "%s/%s", is_channel ? CHANNEL_HISTORY_ID_DIR : HISTORY_DIR_DIRECT, pkey_hex);
File file = _history_fs->open(path, FILE_APPEND, true);
if (!file) {
return;
}
file.print(is_sent ? ">> " : "<< ");
file.println(msg_copy);
file.close();
}
static void push_history_line(ChatHistory &history, char *buf) {
int len = strlen(buf);
if (len < 4 || (buf[0] != '>' && buf[0] != '<') || buf[0] != buf[1]) {
return;
}
HistoryMessage *msg = history.push_ref();
msg->out = buf[0] == '>';
strncpy(msg->text, buf + 3, sizeof(msg->text));
msg->text[sizeof(msg->text) - 1] = '\0';
}
void CardputerDataStore::loadMessages(const uint8_t pkey[PUB_KEY_SIZE], bool is_channel,
ChatHistory &history) {
history.clear();
if (!_history_fs) {
return;
}
char path[128];
char pkey_hex[PUB_KEY_SIZE * 2 + 1];
char line_buf[256] = { 0 };
uint8_t file_read_buf[1024];
buf_to_hex_fast(pkey, PUB_KEY_SIZE, pkey_hex);
snprintf(path, sizeof(path), "%s/%s", is_channel ? CHANNEL_HISTORY_ID_DIR : HISTORY_DIR_DIRECT, pkey_hex);
if (!_history_fs->exists(path)) {
return;
}
File file = _history_fs->open(path, FILE_READ, false);
if (!file) {
return;
}
size_t line_idx = 0;
while (file.available()) {
int bytesRead = file.read(file_read_buf, sizeof(file_read_buf));
if (bytesRead <= 0) {
break;
}
for (int b = 0; b < bytesRead; b++) {
char c = (char)file_read_buf[b];
if (c == '\n' || line_idx >= sizeof(line_buf) - 1) {
line_buf[line_idx] = '\0';
line_idx = 0;
push_history_line(history, line_buf);
} else if (c != '\r') {
line_buf[line_idx++] = c;
}
}
}
// Handle any remaining text if the file didn't end with a trailing LF
if (line_idx > 0) {
line_buf[line_idx] = '\0';
push_history_line(history, line_buf);
}
file.close();
}