Files
switchnote/source/branding_bundle.cpp
T
2026-10-29 03:55:40 +00:00

163 lines
9.5 KiB
C++

#include "branding_bundle.hpp"
#include <cstdint>
#include <fstream>
#include <iterator>
#include <utility>
namespace branding {
namespace {
void put16(std::vector<unsigned char>& out, std::uint16_t v) {
out.push_back(static_cast<unsigned char>(v));
out.push_back(static_cast<unsigned char>(v >> 8));
}
void put32(std::vector<unsigned char>& out, std::uint32_t v) {
put16(out, static_cast<std::uint16_t>(v));
put16(out, static_cast<std::uint16_t>(v >> 16));
}
std::uint16_t get16(const std::vector<unsigned char>& in, std::size_t p) {
return static_cast<std::uint16_t>(in[p] | (static_cast<std::uint16_t>(in[p + 1]) << 8));
}
std::uint32_t get32(const std::vector<unsigned char>& in, std::size_t p) {
return static_cast<std::uint32_t>(get16(in, p)) |
(static_cast<std::uint32_t>(get16(in, p + 2)) << 16);
}
std::uint32_t crc32(const unsigned char* data, std::size_t size) {
std::uint32_t crc = 0xffffffffU;
for (std::size_t i = 0; i < size; ++i) {
crc ^= data[i];
for (int bit = 0; bit < 8; ++bit)
crc = (crc >> 1) ^ (0xedb88320U & (0U - (crc & 1U)));
}
return ~crc;
}
std::string escape(const std::string& value) {
std::string out = "\"";
for (unsigned char c : value) {
if (c == '"' || c == '\\') { out.push_back('\\'); out.push_back(static_cast<char>(c)); }
else if (c < 0x20) {
static constexpr char hex[] = "0123456789abcdef";
out += "\\u00"; out.push_back(hex[c >> 4]); out.push_back(hex[c & 15]);
} else out.push_back(static_cast<char>(c));
}
out.push_back('"');
return out;
}
void appendUtf8(std::string& out, std::uint32_t c) {
if (c <= 0x7f) out.push_back(static_cast<char>(c));
else if (c <= 0x7ff) { out.push_back(static_cast<char>(0xc0 | (c >> 6))); out.push_back(static_cast<char>(0x80 | (c & 63))); }
else { out.push_back(static_cast<char>(0xe0 | (c >> 12))); out.push_back(static_cast<char>(0x80 | ((c >> 6) & 63))); out.push_back(static_cast<char>(0x80 | (c & 63))); }
}
bool jsonString(const std::string& json, const std::string& key, std::string& value) {
const std::string marker = "\"" + key + "\"";
std::size_t p = json.find(marker);
if (p == std::string::npos) return false;
p = json.find(':', p + marker.size());
if (p == std::string::npos) return false;
while (++p < json.size() && (json[p] == ' ' || json[p] == '\t' || json[p] == '\r' || json[p] == '\n')) {}
if (p >= json.size() || json[p++] != '"') return false;
value.clear();
while (p < json.size()) {
unsigned char c = static_cast<unsigned char>(json[p++]);
if (c == '"') return true;
if (c != '\\') { value.push_back(static_cast<char>(c)); continue; }
if (p >= json.size()) return false;
c = static_cast<unsigned char>(json[p++]);
switch (c) {
case '"': case '\\': case '/': value.push_back(static_cast<char>(c)); break;
case 'b': value.push_back('\b'); break; case 'f': value.push_back('\f'); break;
case 'n': value.push_back('\n'); break; case 'r': value.push_back('\r'); break; case 't': value.push_back('\t'); break;
case 'u': {
if (p + 4 > json.size()) return false;
std::uint32_t cp = 0;
for (int i = 0; i < 4; ++i) {
const char h = json[p++]; cp <<= 4;
if (h >= '0' && h <= '9') cp |= static_cast<unsigned>(h - '0');
else if (h >= 'a' && h <= 'f') cp |= static_cast<unsigned>(h - 'a' + 10);
else if (h >= 'A' && h <= 'F') cp |= static_cast<unsigned>(h - 'A' + 10);
else return false;
}
appendUtf8(value, cp); break;
}
default: return false;
}
}
return false;
}
} // namespace
bool writeConfig(const std::string& path, const Bundle& bundle, std::string& error) {
if (bundle.app.empty() || bundle.app.size() > 40 || bundle.singular.empty() || bundle.singular.size() > 24 ||
bundle.plural.empty() || bundle.plural.size() > 24 || bundle.logo.empty() || bundle.logo.size() > 8U * 1024U * 1024U) {
error = "Branding values or logo are outside supported limits."; return false;
}
const std::string json = "{\"app\":" + escape(bundle.app) + ",\"singular\":" + escape(bundle.singular) +
",\"plural\":" + escape(bundle.plural) + "}";
struct Entry { std::string name; const unsigned char* data; std::uint32_t size; std::uint32_t crc; std::uint32_t offset; };
const std::string names[2] = {"branding.json", "logo.png"};
const std::string jsonOwned = json;
const unsigned char* data[2] = {reinterpret_cast<const unsigned char*>(jsonOwned.data()), bundle.logo.data()};
const std::uint32_t sizes[2] = {static_cast<std::uint32_t>(jsonOwned.size()), static_cast<std::uint32_t>(bundle.logo.size())};
std::vector<unsigned char> archive;
Entry entries[2]{};
for (int i = 0; i < 2; ++i) {
Entry& e = entries[i]; e.name = names[i]; e.data = data[i]; e.size = sizes[i]; e.crc = crc32(e.data, e.size); e.offset = static_cast<std::uint32_t>(archive.size());
put32(archive, 0x04034b50U); put16(archive, 20); put16(archive, 0); put16(archive, 0);
put16(archive, 0); put16(archive, 0); put32(archive, e.crc); put32(archive, e.size); put32(archive, e.size);
put16(archive, static_cast<std::uint16_t>(e.name.size())); put16(archive, 0);
archive.insert(archive.end(), e.name.begin(), e.name.end()); archive.insert(archive.end(), e.data, e.data + e.size);
}
const std::uint32_t centralOffset = static_cast<std::uint32_t>(archive.size());
for (const Entry& e : entries) {
put32(archive, 0x02014b50U); put16(archive, 20); put16(archive, 20); put16(archive, 0); put16(archive, 0);
put16(archive, 0); put16(archive, 0); put32(archive, e.crc); put32(archive, e.size); put32(archive, e.size);
put16(archive, static_cast<std::uint16_t>(e.name.size())); put16(archive, 0); put16(archive, 0); put16(archive, 0);
put16(archive, 0); put32(archive, 0); put32(archive, e.offset);
archive.insert(archive.end(), e.name.begin(), e.name.end());
}
const std::uint32_t centralSize = static_cast<std::uint32_t>(archive.size()) - centralOffset;
put32(archive, 0x06054b50U); put16(archive, 0); put16(archive, 0); put16(archive, 2); put16(archive, 2);
put32(archive, centralSize); put32(archive, centralOffset); put16(archive, 0);
std::ofstream out(path, std::ios::binary | std::ios::trunc);
if (!out) { error = "Could not create branding.config."; return false; }
out.write(reinterpret_cast<const char*>(archive.data()), static_cast<std::streamsize>(archive.size()));
out.flush();
if (!out) { error = "Could not write branding.config."; return false; }
error.clear(); return true;
}
bool readConfig(const std::string& path, Bundle& bundle, std::string& error) {
std::ifstream in(path, std::ios::binary | std::ios::ate);
if (!in) { error = "Copy a .config file to the Switchnote folder first."; return false; }
const std::streamoff length = in.tellg();
if (length < 22 || length > 9 * 1024 * 1024) { error = "Branding config size is invalid."; return false; }
std::vector<unsigned char> bytes(static_cast<std::size_t>(length)); in.seekg(0); in.read(reinterpret_cast<char*>(bytes.data()), length);
if (!in) { error = "Could not read branding config."; return false; }
std::string json; std::vector<unsigned char> logo; std::size_t p = 0;
while (p + 4 <= bytes.size() && get32(bytes, p) == 0x04034b50U) {
if (p + 30 > bytes.size()) { error = "Branding config archive is truncated."; return false; }
const std::uint16_t flags = get16(bytes, p + 6), method = get16(bytes, p + 8), nameSize = get16(bytes, p + 26), extraSize = get16(bytes, p + 28);
const std::uint32_t expectedCrc = get32(bytes, p + 14), compressed = get32(bytes, p + 18), uncompressed = get32(bytes, p + 22);
const std::size_t nameAt = p + 30, dataAt = nameAt + nameSize + extraSize;
if (flags != 0 || method != 0 || compressed != uncompressed || dataAt > bytes.size() || compressed > bytes.size() - dataAt) { error = "Only standard uncompressed branding.config files are supported."; return false; }
const std::string name(bytes.begin() + static_cast<std::ptrdiff_t>(nameAt), bytes.begin() + static_cast<std::ptrdiff_t>(nameAt + nameSize));
const auto* data = bytes.data() + dataAt;
if (crc32(data, uncompressed) != expectedCrc) { error = "Branding config file checksum failed."; return false; }
if (name == "branding.json") json.assign(reinterpret_cast<const char*>(data), uncompressed);
else if (name == "logo.png") logo.assign(data, data + uncompressed);
else if (name.find('/') != std::string::npos || name.find('\\') != std::string::npos || name == "..") { error = "Branding config contains an unsafe file path."; return false; }
p = dataAt + compressed;
}
if (json.empty() || logo.empty() || logo.size() > 8U * 1024U * 1024U ||
!jsonString(json, "app", bundle.app) || !jsonString(json, "singular", bundle.singular) || !jsonString(json, "plural", bundle.plural) ||
bundle.app.empty() || bundle.app.size() > 40 || bundle.singular.empty() || bundle.singular.size() > 24 || bundle.plural.empty() || bundle.plural.size() > 24) {
error = "Branding config is missing valid names or a logo."; return false;
}
bundle.logo = std::move(logo);
error.clear(); return true;
}
} // namespace branding