Public Access
lib/files: path parts; names checked for rename and new folder; why something can't be renamed, moved or deleted (the Gemini cache, a file being written or a folder holding one, the firmware's top-level folders), and why it can't go into a folder; the viewer for a file by its name, with a sniff for text. FileList keeps a folder's entries packed, 256 at most, the first 256 by name whatever order the card lists them in, and sorts by name, date or size with folders first. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01EhqxQ49eCju4CzKYNjZzwT
100 lines
3.9 KiB
C++
100 lines
3.9 KiB
C++
#include "file_names.h"
|
|
|
|
#include <cctype>
|
|
|
|
namespace roro::files {
|
|
|
|
const char* const kFirmwareFolders[] = {"/irc", "/wifi", "/updates", "/gnss", "/gemini", "/captures", "/notes"};
|
|
const size_t kFirmwareFolderCount = sizeof kFirmwareFolders / sizeof kFirmwareFolders[0];
|
|
|
|
std::string parentOf(const std::string& path) {
|
|
size_t slash = path.rfind('/');
|
|
return slash == std::string::npos || slash == 0 ? "/" : path.substr(0, slash);
|
|
}
|
|
|
|
std::string baseName(const std::string& path) {
|
|
size_t slash = path.rfind('/');
|
|
return slash == std::string::npos ? path : path.substr(slash + 1);
|
|
}
|
|
|
|
std::string joinPath(const std::string& folder, const std::string& name) {
|
|
return folder == "/" ? "/" + name : folder + "/" + name;
|
|
}
|
|
|
|
std::string extensionOf(const std::string& name) {
|
|
size_t dot = name.rfind('.');
|
|
if (dot == std::string::npos || dot == 0) return "";
|
|
std::string ext = name.substr(dot + 1);
|
|
for (char& c : ext) c = static_cast<char>(std::tolower(static_cast<unsigned char>(c)));
|
|
return ext;
|
|
}
|
|
|
|
bool isInside(const std::string& path, const std::string& folder) {
|
|
if (folder == "/") return true;
|
|
if (path.compare(0, folder.size(), folder) != 0) return false;
|
|
return path.size() == folder.size() || path[folder.size()] == '/';
|
|
}
|
|
|
|
std::string checkName(const std::string& name) {
|
|
if (name.empty()) return "A name can't be empty";
|
|
if (name == "." || name == "..") return "\".\" and \"..\" aren't names";
|
|
if (name.size() > 64) return "A name can be 64 characters at most";
|
|
for (char c : name) {
|
|
if (static_cast<unsigned char>(c) < 0x20) return "A name can't contain control characters";
|
|
for (char bad : std::string("/\\:*?\"<>|"))
|
|
if (c == bad) return std::string("A name can't contain ") + c;
|
|
}
|
|
if (name.back() == '.' || name.back() == ' ') return "A name can't end with a dot or a space";
|
|
return "";
|
|
}
|
|
|
|
std::string whyReadOnly(const std::string& path, const std::vector<std::string>& inUse) {
|
|
if (path == "/" || path.empty()) return "That's the card itself";
|
|
if (isInside(path, kGeminiCache)) return std::string(kGeminiCache) + " is the Gemini App's working space";
|
|
for (const std::string& open : inUse) {
|
|
if (open == path) return "It's being written right now";
|
|
if (isInside(open, path)) return "It holds a file that's being written right now";
|
|
}
|
|
for (size_t i = 0; i < kFirmwareFolderCount; i++)
|
|
if (path == kFirmwareFolders[i]) return std::string("The firmware keeps its files in ") + path;
|
|
return "";
|
|
}
|
|
|
|
std::string whyNotInto(const std::string& source, const std::string& into) {
|
|
if (isInside(into, kGeminiCache)) return std::string(kGeminiCache) + " is the Gemini App's working space";
|
|
if (isInside(into, source)) return "A folder can't go inside itself";
|
|
if (parentOf(source) == into) return "It's already there";
|
|
return "";
|
|
}
|
|
|
|
std::string copyName(const std::string& name, int n) {
|
|
size_t dot = name.rfind('.');
|
|
std::string suffix = " (" + std::to_string(n) + ")";
|
|
if (dot == std::string::npos || dot == 0) return name + suffix;
|
|
return name.substr(0, dot) + suffix + name.substr(dot);
|
|
}
|
|
|
|
FileKind kindOf(const std::string& name) {
|
|
std::string ext = extensionOf(name);
|
|
if (ext == "gpx") return FileKind::Gpx;
|
|
if (ext == "pcap") return FileKind::Pcap;
|
|
if (ext == "ota") return FileKind::Ota;
|
|
for (const char* text : {"txt", "log", "gmi", "csv", "md", "json", "ini", "conf", "ir"})
|
|
if (ext == text) return FileKind::Text;
|
|
return FileKind::Unknown;
|
|
}
|
|
|
|
bool opensAtEnd(const std::string& name) { return extensionOf(name) == "log"; }
|
|
|
|
bool looksLikeText(const uint8_t* data, size_t len) {
|
|
size_t odd = 0;
|
|
for (size_t i = 0; i < len; i++) {
|
|
uint8_t b = data[i];
|
|
if (b == 0) return false;
|
|
if (b < 0x20 && b != '\n' && b != '\r' && b != '\t') odd++;
|
|
}
|
|
return odd * 20 <= len; // a stray control character or two is still text
|
|
}
|
|
|
|
} // namespace roro::files
|