Updated local storage
This commit is contained in:
1 parent
30db44eaa0
commit
2218aed839
10 files changed
+548
-5
No files matched your search
@@ -0,0 +1,248 @@
|
||||
// SPDX-License-Identifier: MIT
|
||||
// SPDX-FileCopyrightText: 2026 Martino Ferrari
|
||||
|
||||
#include "layout_store.hpp"
|
||||
|
||||
#include <cstdint>
|
||||
#include <cstdio>
|
||||
#include <fstream>
|
||||
#include <sstream>
|
||||
#include <sys/stat.h>
|
||||
#include <sys/types.h>
|
||||
|
||||
namespace udpscope {
|
||||
|
||||
namespace {
|
||||
|
||||
/// Anything outside a safe filename set becomes '_': the host is a dotted quad
|
||||
/// today, but the CLI takes any string and a '/' or ':' in it must not escape
|
||||
/// the directory.
|
||||
std::string sanitize(const std::string& host) {
|
||||
std::string out;
|
||||
out.reserve(host.size());
|
||||
for (char c : host) {
|
||||
const bool ok = (c >= 'a' && c <= 'z') || (c >= 'A' && c <= 'Z') ||
|
||||
(c >= '0' && c <= '9') || c == '.' || c == '_' || c == '-';
|
||||
out.push_back(ok ? c : '_');
|
||||
}
|
||||
return out.empty() ? "_" : out;
|
||||
}
|
||||
|
||||
std::string config_dir() {
|
||||
const char* xdg = std::getenv("XDG_CONFIG_HOME");
|
||||
if (xdg != nullptr && xdg[0] != '\0') return std::string(xdg) + "/udpscope";
|
||||
const char* home = std::getenv("HOME");
|
||||
if (home == nullptr || home[0] == '\0') return "/tmp/udpscope-config";
|
||||
return std::string(home) + "/.config/udpscope";
|
||||
}
|
||||
|
||||
void mkdirs(const std::string& path) {
|
||||
std::string cur;
|
||||
for (const char c : path) {
|
||||
cur.push_back(c);
|
||||
if (c == '/' || cur.size() == path.size()) {
|
||||
if (cur == "/" || cur.back() != '/') continue;
|
||||
::mkdir(cur.c_str(), 0755);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
std::string toml_escape(const std::string& s) {
|
||||
std::string out = "\"";
|
||||
for (char c : s) {
|
||||
if (c == '"' || c == '\\') out.push_back('\\');
|
||||
out.push_back(c);
|
||||
}
|
||||
return out + "\"";
|
||||
}
|
||||
|
||||
bool unescape(std::string& s) {
|
||||
std::string out;
|
||||
for (std::size_t i = 0; i < s.size(); ++i) {
|
||||
if (s[i] != '\\') {
|
||||
out.push_back(s[i]);
|
||||
continue;
|
||||
}
|
||||
if (++i >= s.size()) return false;
|
||||
if (s[i] == '"' || s[i] == '\\') {
|
||||
out.push_back(s[i]);
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
s = out;
|
||||
return true;
|
||||
}
|
||||
|
||||
std::string trim(const std::string& s) {
|
||||
const auto a = s.find_first_not_of(" \t\r");
|
||||
if (a == std::string::npos) return {};
|
||||
const auto b = s.find_last_not_of(" \t\r");
|
||||
return s.substr(a, b - a + 1);
|
||||
}
|
||||
|
||||
/// `"a", "b"` (with the brackets already stripped) -> ["a", "b"].
|
||||
bool parse_string_array(const std::string& in, std::vector<std::string>& out) {
|
||||
out.clear();
|
||||
std::size_t i = 0;
|
||||
while (i < in.size()) {
|
||||
while (i < in.size() && (in[i] == ' ' || in[i] == ',' || in[i] == '\t')) ++i;
|
||||
if (i >= in.size()) break;
|
||||
if (in[i] != '"') return false;
|
||||
++i;
|
||||
std::string tok;
|
||||
while (i < in.size() && in[i] != '"') {
|
||||
if (in[i] == '\\' && i + 1 < in.size()) {
|
||||
tok.push_back(in[i]);
|
||||
++i;
|
||||
}
|
||||
tok.push_back(in[i]);
|
||||
++i;
|
||||
}
|
||||
if (i >= in.size()) return false;
|
||||
++i; // closing quote
|
||||
if (!unescape(tok)) return false;
|
||||
out.push_back(tok);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
std::string layout_path(const std::string& host, std::uint16_t port) {
|
||||
const std::string dir = config_dir();
|
||||
mkdirs(dir);
|
||||
return dir + "/" + sanitize(host) + "_" + std::to_string(port) + ".toml";
|
||||
}
|
||||
|
||||
bool load_layout(const std::string& path, LayoutState& out) {
|
||||
std::ifstream in(path);
|
||||
if (!in) return false;
|
||||
|
||||
LayoutState st;
|
||||
st.window_s = 0.0;
|
||||
st.rows.clear();
|
||||
st.calibration.clear();
|
||||
|
||||
std::string line;
|
||||
// What the last `[[...]]` header opened, so keys land in the right record.
|
||||
enum class Section { Top, Plot, Calibration } section = Section::Top;
|
||||
int plot_count = 0;
|
||||
bool plot_section_seen = false;
|
||||
while (std::getline(in, line)) {
|
||||
const std::string t = trim(line);
|
||||
if (t.empty() || t[0] == '#') continue;
|
||||
if (t[0] == '[') {
|
||||
if (t == "[[plot]]") {
|
||||
section = Section::Plot;
|
||||
plot_section_seen = true;
|
||||
st.rows.emplace_back();
|
||||
} else if (t == "[[calibration]]") {
|
||||
section = Section::Calibration;
|
||||
st.calibration.emplace_back();
|
||||
} else {
|
||||
section = Section::Top; // an unknown table: keep its keys out of ours
|
||||
}
|
||||
continue;
|
||||
}
|
||||
const auto eq = t.find('=');
|
||||
if (eq == std::string::npos) continue;
|
||||
const std::string key = trim(t.substr(0, eq));
|
||||
const std::string value = trim(t.substr(eq + 1));
|
||||
auto strip_quote = [](const std::string& v) {
|
||||
if (v.size() >= 2 && v.front() == '"' && v.back() == '"') {
|
||||
std::string s = v.substr(1, v.size() - 2);
|
||||
std::string out;
|
||||
for (std::size_t i = 0; i < s.size(); ++i) {
|
||||
if (s[i] == '\\' && i + 1 < s.size()) ++i;
|
||||
out.push_back(s[i]);
|
||||
}
|
||||
return out;
|
||||
}
|
||||
return v;
|
||||
};
|
||||
|
||||
if (section == Section::Top) {
|
||||
if (key == "window_s") {
|
||||
try {
|
||||
st.window_s = std::stod(value);
|
||||
} catch (...) {
|
||||
}
|
||||
} else if (key == "plots") {
|
||||
// Only a fallback for a file with no [[plot]] sections: the writer emits
|
||||
// both, and honouring both appended the sections onto the pre-created
|
||||
// rows (a 2-plot file loaded as 4).
|
||||
try {
|
||||
plot_count = std::max(0, std::stoi(value));
|
||||
} catch (...) {
|
||||
}
|
||||
}
|
||||
} else if (section == Section::Plot && !st.rows.empty()) {
|
||||
if (key == "signals") {
|
||||
std::vector<std::string> names;
|
||||
if (value.size() >= 2 && value.front() == '[' && value.back() == ']') {
|
||||
parse_string_array(value.substr(1, value.size() - 2), names);
|
||||
}
|
||||
st.rows.back().signals = names;
|
||||
}
|
||||
} else if (section == Section::Calibration && !st.calibration.empty()) {
|
||||
if (key == "name") {
|
||||
st.calibration.back().name = strip_quote(value);
|
||||
} else if (key == "gain") {
|
||||
try {
|
||||
st.calibration.back().gain = std::stod(value);
|
||||
} catch (...) {
|
||||
}
|
||||
} else if (key == "offset") {
|
||||
try {
|
||||
st.calibration.back().offset = std::stod(value);
|
||||
} catch (...) {
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
if (!plot_section_seen && plot_count > 0) {
|
||||
st.rows.assign(static_cast<std::size_t>(plot_count), {});
|
||||
}
|
||||
out = std::move(st);
|
||||
return true;
|
||||
}
|
||||
|
||||
bool save_layout(const std::string& path, const LayoutState& state) {
|
||||
std::ostringstream o;
|
||||
o << "# udpscope session layout (written by the app; safe to edit)\n";
|
||||
if (state.window_s > 0.0) o << "window_s = " << state.window_s << "\n";
|
||||
o << "plots = " << state.rows.size() << "\n";
|
||||
for (const auto& row : state.rows) {
|
||||
o << "\n[[plot]]\nsignals = [";
|
||||
for (std::size_t i = 0; i < row.signals.size(); ++i) {
|
||||
if (i != 0) o << ", ";
|
||||
o << toml_escape(row.signals[i]);
|
||||
}
|
||||
o << "]\n";
|
||||
}
|
||||
for (const auto& cal : state.calibration) {
|
||||
o << "\n[[calibration]]\nname = " << toml_escape(cal.name) << "\ngain = " << cal.gain
|
||||
<< "\noffset = " << cal.offset << "\n";
|
||||
}
|
||||
|
||||
// A sibling temp file + rename: a crash mid-write leaves the old layout, not a
|
||||
// truncated one that would fail to parse next start.
|
||||
const std::string tmp = path + ".tmp";
|
||||
{
|
||||
std::ofstream f(tmp, std::ios::trunc);
|
||||
if (!f) return false;
|
||||
f << o.str();
|
||||
if (!f.good()) {
|
||||
::remove(tmp.c_str());
|
||||
return false;
|
||||
}
|
||||
}
|
||||
if (std::rename(tmp.c_str(), path.c_str()) != 0) {
|
||||
::remove(tmp.c_str());
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
} // namespace udpscope
|
||||
Reference in new issue
Block a user