#include "branding_bundle.hpp" #include #include #include #include namespace branding { namespace { void put16(std::vector& out, std::uint16_t v) { out.push_back(static_cast(v)); out.push_back(static_cast(v >> 8)); } void put32(std::vector& out, std::uint32_t v) { put16(out, static_cast(v)); put16(out, static_cast(v >> 16)); } std::uint16_t get16(const std::vector& in, std::size_t p) { return static_cast(in[p] | (static_cast(in[p + 1]) << 8)); } std::uint32_t get32(const std::vector& in, std::size_t p) { return static_cast(get16(in, p)) | (static_cast(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(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(c)); } out.push_back('"'); return out; } void appendUtf8(std::string& out, std::uint32_t c) { if (c <= 0x7f) out.push_back(static_cast(c)); else if (c <= 0x7ff) { out.push_back(static_cast(0xc0 | (c >> 6))); out.push_back(static_cast(0x80 | (c & 63))); } else { out.push_back(static_cast(0xe0 | (c >> 12))); out.push_back(static_cast(0x80 | ((c >> 6) & 63))); out.push_back(static_cast(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(json[p++]); if (c == '"') return true; if (c != '\\') { value.push_back(static_cast(c)); continue; } if (p >= json.size()) return false; c = static_cast(json[p++]); switch (c) { case '"': case '\\': case '/': value.push_back(static_cast(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(h - '0'); else if (h >= 'a' && h <= 'f') cp |= static_cast(h - 'a' + 10); else if (h >= 'A' && h <= 'F') cp |= static_cast(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(jsonOwned.data()), bundle.logo.data()}; const std::uint32_t sizes[2] = {static_cast(jsonOwned.size()), static_cast(bundle.logo.size())}; std::vector 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(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(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(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(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(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(archive.data()), static_cast(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 bytes(static_cast(length)); in.seekg(0); in.read(reinterpret_cast(bytes.data()), length); if (!in) { error = "Could not read branding config."; return false; } std::string json; std::vector 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(nameAt), bytes.begin() + static_cast(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(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