Files
2026-10-29 03:55:40 +00:00

1056 lines
50 KiB
C++

#include "sppm.hpp"
#include <algorithm>
#include <array>
#include <atomic>
#include <chrono>
#include <cstdio>
#include <cstdlib>
#include <cstring>
#include <filesystem>
#include <fstream>
#include <limits>
#include <random>
#include <system_error>
#ifdef __SWITCH__
#include <switch.h>
#endif
#ifdef _WIN32
#define NOMINMAX
#include <windows.h>
#endif
extern "C" {
int crypto_sign_ed25519_tweet(unsigned char*, unsigned long long*, const unsigned char*, unsigned long long, const unsigned char*);
int crypto_sign_ed25519_tweet_open(unsigned char*, unsigned long long*, const unsigned char*, unsigned long long, const unsigned char*);
int crypto_sign_ed25519_tweet_keypair(unsigned char*, unsigned char*);
int crypto_secretbox_xsalsa20poly1305_tweet(unsigned char*, const unsigned char*, unsigned long long, const unsigned char*, const unsigned char*);
int crypto_secretbox_xsalsa20poly1305_tweet_open(unsigned char*, const unsigned char*, unsigned long long, const unsigned char*, const unsigned char*);
void randombytes(unsigned char* out, unsigned long long length) {
#ifdef __SWITCH__
if (R_FAILED(csrngGetRandomBytes(out, static_cast<std::size_t>(length)))) std::abort();
#else
std::random_device random;
for (unsigned long long i = 0; i < length; ++i) out[i] = static_cast<unsigned char>(random());
#endif
}
}
namespace sppm {
namespace {
constexpr std::uint16_t kVersion = 3;
constexpr std::uint16_t kLegacyVersion = 1;
constexpr std::uint16_t kHeaderBytes = 36;
constexpr std::uint64_t kMaxFileBytes = 512ULL * 1024ULL * 1024ULL;
constexpr std::size_t kMaxAudioSamples = 32ULL * 1024ULL * 1024ULL;
constexpr std::size_t kMaxLayerRleBytes = kPixelsPerLayer * 2;
constexpr std::size_t kSignatureBytes = 36;
constexpr std::size_t kPublicSignatureBytes = 4 + 3 * (32 + 16) + 64;
constexpr char kNullId[] = "0000000000000000";
std::string upperId(std::string_view id) {
std::string out(id);
for (char& c : out) {
if (c >= 'a' && c <= 'f') c = static_cast<char>(c - 'a' + 'A');
}
return out;
}
bool sameId(std::string_view a, std::string_view b) {
return upperId(a) == upperId(b);
}
bool validUtf8(std::string_view text) {
for (std::size_t i = 0; i < text.size();) {
const auto first = static_cast<std::uint8_t>(text[i]);
if (first < 0x80) { ++i; continue; }
std::size_t length;
if (first >= 0xC2 && first <= 0xDF) length = 2;
else if (first >= 0xE0 && first <= 0xEF) length = 3;
else if (first >= 0xF0 && first <= 0xF4) length = 4;
else return false;
if (length > text.size() - i) return false;
const auto second = static_cast<std::uint8_t>(text[i + 1]);
if (second < 0x80 || second > 0xBF ||
(first == 0xE0 && second < 0xA0) ||
(first == 0xED && second > 0x9F) ||
(first == 0xF0 && second < 0x90) ||
(first == 0xF4 && second > 0x8F)) return false;
for (std::size_t j = 2; j < length; ++j) {
const auto continuation = static_cast<std::uint8_t>(text[i + j]);
if (continuation < 0x80 || continuation > 0xBF) return false;
}
i += length;
}
return true;
}
std::uint32_t crc32(const std::uint8_t* bytes, std::size_t length) {
static const auto table = [] {
std::array<std::uint32_t, 256> entries{};
for (std::uint32_t i = 0; i < entries.size(); ++i) {
std::uint32_t c = i;
for (int j = 0; j < 8; ++j) c = (c >> 1) ^ ((c & 1) ? 0xEDB88320U : 0U);
entries[i] = c;
}
return entries;
}();
std::uint32_t c = 0xFFFFFFFFU;
for (std::size_t i = 0; i < length; ++i) c = table[(c ^ bytes[i]) & 0xFFU] ^ (c >> 8);
return c ^ 0xFFFFFFFFU;
}
class Writer {
public:
std::vector<std::uint8_t> bytes;
void u8(std::uint8_t v) { bytes.push_back(v); }
void u16(std::uint16_t v) { u8(static_cast<std::uint8_t>(v)); u8(static_cast<std::uint8_t>(v >> 8)); }
void u32(std::uint32_t v) { for (int i = 0; i < 4; ++i) u8(static_cast<std::uint8_t>(v >> (i * 8))); }
void u64(std::uint64_t v) { for (int i = 0; i < 8; ++i) u8(static_cast<std::uint8_t>(v >> (i * 8))); }
void raw(const std::uint8_t* p, std::size_t n) { if (n) bytes.insert(bytes.end(), p, p + n); }
void text(std::string_view value) { u16(static_cast<std::uint16_t>(value.size())); raw(reinterpret_cast<const std::uint8_t*>(value.data()), value.size()); }
void id(std::string_view value) {
const std::string normal = upperId(value.empty() ? std::string_view(kNullId) : value);
raw(reinterpret_cast<const std::uint8_t*>(normal.data()), normal.size());
}
void setU32(std::size_t at, std::uint32_t v) { for (int i = 0; i < 4; ++i) bytes[at + i] = static_cast<std::uint8_t>(v >> (i * 8)); }
void setU64(std::size_t at, std::uint64_t v) { for (int i = 0; i < 8; ++i) bytes[at + i] = static_cast<std::uint8_t>(v >> (i * 8)); }
};
class Reader {
public:
explicit Reader(const std::vector<std::uint8_t>& b) : bytes(b) {}
const std::vector<std::uint8_t>& bytes;
std::size_t at = 0;
bool take(std::size_t n, const std::uint8_t*& p) {
if (n > bytes.size() - at) return false;
p = bytes.data() + at;
at += n;
return true;
}
bool u8(std::uint8_t& v) { const std::uint8_t* p; if (!take(1, p)) return false; v = *p; return true; }
bool u16(std::uint16_t& v) { const std::uint8_t* p; if (!take(2, p)) return false; v = static_cast<std::uint16_t>(p[0] | (p[1] << 8)); return true; }
bool u32(std::uint32_t& v) { const std::uint8_t* p; if (!take(4, p)) return false; v = 0; for (int i = 0; i < 4; ++i) v |= static_cast<std::uint32_t>(p[i]) << (i * 8); return true; }
bool u64(std::uint64_t& v) { const std::uint8_t* p; if (!take(8, p)) return false; v = 0; for (int i = 0; i < 8; ++i) v |= static_cast<std::uint64_t>(p[i]) << (i * 8); return true; }
bool text(std::string& out, std::size_t maxLength) {
std::uint16_t n;
const std::uint8_t* p;
if (!u16(n) || n > maxLength || !take(n, p)) return false;
out.assign(reinterpret_cast<const char*>(p), n);
return out.find('\0') == std::string::npos && validUtf8(out);
}
bool id(std::string& out, bool nullable) {
const std::uint8_t* p;
if (!take(16, p)) return false;
out.assign(reinterpret_cast<const char*>(p), 16);
if (nullable && out == kNullId) { out.clear(); return true; }
if (!isValidFsid(out) || out == kNullId) return false;
out = upperId(out);
return true;
}
};
std::vector<std::uint8_t> encodeLayer(const std::vector<std::uint8_t>& pixels) {
std::vector<std::uint8_t> out;
out.reserve(pixels.size() / 2);
std::size_t at = 0;
while (at < pixels.size()) {
std::size_t run = 1;
while (run < 128 && at + run < pixels.size() && pixels[at + run] == pixels[at]) ++run;
if (run >= 3) {
out.push_back(static_cast<std::uint8_t>(0x80U | (run - 1)));
out.push_back(pixels[at]);
at += run;
continue;
}
const std::size_t start = at;
at += run;
while (at - start < 128 && at < pixels.size()) {
run = 1;
while (run < 3 && at + run < pixels.size() && pixels[at + run] == pixels[at]) ++run;
if (run >= 3 || at + run - start > 128) break;
at += run;
}
out.push_back(static_cast<std::uint8_t>(at - start - 1));
out.insert(out.end(), pixels.begin() + static_cast<std::ptrdiff_t>(start),
pixels.begin() + static_cast<std::ptrdiff_t>(at));
}
return out;
}
bool decodeLayer(const std::uint8_t* p, std::size_t n, std::vector<std::uint8_t>& out) {
out.clear();
out.reserve(kPixelsPerLayer);
std::size_t at = 0;
while (at < n) {
const std::uint8_t command = p[at++];
const std::size_t count = static_cast<std::size_t>(command & 0x7FU) + 1;
if (count > kPixelsPerLayer - out.size()) return false;
if (command & 0x80U) {
if (at == n) return false;
out.insert(out.end(), count, p[at++]);
} else {
if (count > n - at) return false;
out.insert(out.end(), p + at, p + at + count);
at += count;
}
}
return out.size() == kPixelsPerLayer;
}
std::uint32_t rightRotate(std::uint32_t x, unsigned n) { return (x >> n) | (x << (32 - n)); }
class Sha256 {
public:
void update(const std::uint8_t* p, std::size_t n) {
total += n;
while (n) {
const std::size_t step = std::min(n, block.size() - used);
std::memcpy(block.data() + used, p, step);
used += step;
p += step;
n -= step;
if (used == block.size()) { transform(); used = 0; }
}
}
std::array<std::uint8_t, 32> finish() {
const std::uint64_t bitLength = static_cast<std::uint64_t>(total) * 8;
const std::uint8_t marker = 0x80;
update(&marker, 1);
const std::uint8_t zero = 0;
while (used != 56) update(&zero, 1);
std::uint8_t length[8];
for (int i = 0; i < 8; ++i) length[i] = static_cast<std::uint8_t>(bitLength >> ((7 - i) * 8));
update(length, 8);
std::array<std::uint8_t, 32> result{};
for (int i = 0; i < 8; ++i) {
result[i * 4] = static_cast<std::uint8_t>(state[i] >> 24);
result[i * 4 + 1] = static_cast<std::uint8_t>(state[i] >> 16);
result[i * 4 + 2] = static_cast<std::uint8_t>(state[i] >> 8);
result[i * 4 + 3] = static_cast<std::uint8_t>(state[i]);
}
return result;
}
private:
static constexpr std::uint32_t K[64] = {
0x428a2f98U,0x71374491U,0xb5c0fbcfU,0xe9b5dba5U,0x3956c25bU,0x59f111f1U,0x923f82a4U,0xab1c5ed5U,
0xd807aa98U,0x12835b01U,0x243185beU,0x550c7dc3U,0x72be5d74U,0x80deb1feU,0x9bdc06a7U,0xc19bf174U,
0xe49b69c1U,0xefbe4786U,0x0fc19dc6U,0x240ca1ccU,0x2de92c6fU,0x4a7484aaU,0x5cb0a9dcU,0x76f988daU,
0x983e5152U,0xa831c66dU,0xb00327c8U,0xbf597fc7U,0xc6e00bf3U,0xd5a79147U,0x06ca6351U,0x14292967U,
0x27b70a85U,0x2e1b2138U,0x4d2c6dfcU,0x53380d13U,0x650a7354U,0x766a0abbU,0x81c2c92eU,0x92722c85U,
0xa2bfe8a1U,0xa81a664bU,0xc24b8b70U,0xc76c51a3U,0xd192e819U,0xd6990624U,0xf40e3585U,0x106aa070U,
0x19a4c116U,0x1e376c08U,0x2748774cU,0x34b0bcb5U,0x391c0cb3U,0x4ed8aa4aU,0x5b9cca4fU,0x682e6ff3U,
0x748f82eeU,0x78a5636fU,0x84c87814U,0x8cc70208U,0x90befffaU,0xa4506cebU,0xbef9a3f7U,0xc67178f2U
};
std::array<std::uint32_t, 8> state{0x6a09e667U,0xbb67ae85U,0x3c6ef372U,0xa54ff53aU,0x510e527fU,0x9b05688cU,0x1f83d9abU,0x5be0cd19U};
std::array<std::uint8_t, 64> block{};
std::size_t used = 0;
std::size_t total = 0;
void transform() {
std::uint32_t w[64];
for (int i = 0; i < 16; ++i) {
const int j = i * 4;
w[i] = (static_cast<std::uint32_t>(block[j]) << 24) | (static_cast<std::uint32_t>(block[j+1]) << 16) |
(static_cast<std::uint32_t>(block[j+2]) << 8) | block[j+3];
}
for (int i = 16; i < 64; ++i) {
const std::uint32_t s0 = rightRotate(w[i-15], 7) ^ rightRotate(w[i-15], 18) ^ (w[i-15] >> 3);
const std::uint32_t s1 = rightRotate(w[i-2], 17) ^ rightRotate(w[i-2], 19) ^ (w[i-2] >> 10);
w[i] = w[i-16] + s0 + w[i-7] + s1;
}
std::uint32_t a=state[0], b=state[1], c=state[2], d=state[3], e=state[4], f=state[5], g=state[6], h=state[7];
for (int i = 0; i < 64; ++i) {
const std::uint32_t s1 = rightRotate(e, 6) ^ rightRotate(e, 11) ^ rightRotate(e, 25);
const std::uint32_t choose = (e & f) ^ (~e & g);
const std::uint32_t t1 = h + s1 + choose + K[i] + w[i];
const std::uint32_t s0 = rightRotate(a, 2) ^ rightRotate(a, 13) ^ rightRotate(a, 22);
const std::uint32_t majority = (a & b) ^ (a & c) ^ (b & c);
const std::uint32_t t2 = s0 + majority;
h=g; g=f; f=e; e=d+t1; d=c; c=b; b=a; a=t1+t2;
}
state[0]+=a; state[1]+=b; state[2]+=c; state[3]+=d;
state[4]+=e; state[5]+=f; state[6]+=g; state[7]+=h;
}
};
std::array<std::uint8_t, 32> hmacSha256(const std::array<std::uint8_t, 32>& secret,
const std::uint8_t* message, std::size_t length) {
std::array<std::uint8_t, 64> innerKey{}, outerKey{};
for (std::size_t i = 0; i < 64; ++i) {
const std::uint8_t key = i < secret.size() ? secret[i] : 0;
innerKey[i] = key ^ 0x36U;
outerKey[i] = key ^ 0x5cU;
}
Sha256 inner;
inner.update(innerKey.data(), innerKey.size());
inner.update(message, length);
const auto middle = inner.finish();
Sha256 outer;
outer.update(outerKey.data(), outerKey.size());
outer.update(middle.data(), middle.size());
return outer.finish();
}
bool commitBytes(const std::string& path, const std::vector<std::uint8_t>& bytes,
std::string& error) {
if (path.empty()) { error = "empty save path"; return false; }
static std::atomic<std::uint64_t> sequence{0};
const auto stamp = std::chrono::steady_clock::now().time_since_epoch().count();
const std::uint64_t serial = sequence.fetch_add(1);
std::filesystem::path target(path);
std::filesystem::path temporary = target;
temporary += ".tmp-" + std::to_string(stamp) + "-" + std::to_string(serial);
{
std::ofstream output(temporary, std::ios::binary | std::ios::trunc);
if (!output) { error = "could not create temporary save file"; return false; }
output.write(reinterpret_cast<const char*>(bytes.data()), static_cast<std::streamsize>(bytes.size()));
output.flush();
if (!output) {
output.close();
std::error_code ignored;
std::filesystem::remove(temporary, ignored);
error = "could not write complete temporary save file";
return false;
}
}
#ifdef _WIN32
if (!MoveFileExW(temporary.c_str(), target.c_str(), MOVEFILE_REPLACE_EXISTING | MOVEFILE_WRITE_THROUGH)) {
std::error_code ignored;
std::filesystem::remove(temporary, ignored);
error = "could not replace save file";
return false;
}
#else
std::error_code ec;
std::filesystem::rename(temporary, target, ec);
if (ec) {
// Some SD/FAT rename implementations refuse to replace an existing
// file. Keep the old note recoverable while installing the new one.
std::error_code existsError;
const bool targetExists = std::filesystem::exists(target, existsError);
if (!targetExists || existsError) {
std::error_code ignored;
std::filesystem::remove(temporary, ignored);
error = "could not replace save file: " + ec.message();
return false;
}
std::filesystem::path backup = target;
backup += ".bak-" + std::to_string(stamp) + "-" + std::to_string(serial);
std::error_code backupError;
std::filesystem::rename(target, backup, backupError);
if (backupError) {
std::error_code ignored;
std::filesystem::remove(temporary, ignored);
error = "could not preserve previous save file: " + backupError.message();
return false;
}
ec.clear();
std::filesystem::rename(temporary, target, ec);
if (ec) {
std::error_code restoreError;
std::filesystem::rename(backup, target, restoreError);
std::error_code ignored;
std::filesystem::remove(temporary, ignored);
error = "could not install new save file: " + ec.message();
if (restoreError) error += "; previous save preserved at " + backup.string();
return false;
}
std::error_code ignored;
std::filesystem::remove(backup, ignored);
}
#endif
error.clear();
return true;
}
} // namespace
Palette defaultPalette() {
Palette palette{};
palette[0] = {0, 0, 0, 0};
constexpr std::array<Color, 15> initial = {{{255,255,255,255}, {0,0,0,255}, {255,52,63,255},
{255,119,28,255}, {255,221,0,255}, {239,68,133,255}, {0,172,102,255},
{0,214,231,255}, {21,82,151,255}, {132,66,183,255}, {132,66,183,255},
{239,68,133,255}, {144,94,62,255}, {115,122,131,255}, {210,219,230,255}}};
std::copy(initial.begin(), initial.end(), palette.begin() + 1);
for (std::size_t i = 16; i < palette.size(); ++i) {
const std::uint8_t shade = static_cast<std::uint8_t>((i - 16) * 255 / (palette.size() - 17));
palette[i] = {shade, shade, shade, 255};
}
return palette;
}
Frame::Frame() {
for (auto& layer : layers) layer.assign(kPixelsPerLayer, 0);
}
bool isValidFsid(std::string_view fsid) {
if (fsid.size() != 16) return false;
for (char c : fsid) {
if (!((c >= '0' && c <= '9') || (c >= 'A' && c <= 'F') || (c >= 'a' && c <= 'f'))) return false;
}
return fsid != kNullId;
}
std::string deriveFsid(const std::array<std::uint8_t, 32>& secret,
const std::array<std::uint8_t, 16>& nonce,
std::string_view domain) {
constexpr char prefix[] = "FlipnoteNX-FSID-v1";
std::vector<std::uint8_t> message;
message.insert(message.end(), prefix, prefix + sizeof(prefix)); // includes separator NUL
message.insert(message.end(), domain.begin(), domain.end());
message.push_back(0);
message.insert(message.end(), nonce.begin(), nonce.end());
const auto digest = hmacSha256(secret, message.data(), message.size());
constexpr char hex[] = "0123456789ABCDEF";
std::string result(16, '0');
for (int i = 0; i < 8; ++i) {
result[i * 2] = hex[digest[i] >> 4];
result[i * 2 + 1] = hex[digest[i] & 15];
}
return result;
}
bool hasPublicIdentity(const PublicIdentity& identity) {
return std::any_of(identity.key.begin(), identity.key.end(), [](std::uint8_t value) { return value != 0; }) &&
std::any_of(identity.deviceTag.begin(), identity.deviceTag.end(), [](std::uint8_t value) { return value != 0; });
}
std::array<std::uint8_t, 16> deriveDeviceTag(const std::vector<std::uint8_t>& deviceMaterial) {
constexpr char domain[] = "FlipnoteNX-device-tag-v1";
Sha256 hash;
hash.update(reinterpret_cast<const std::uint8_t*>(domain), sizeof(domain));
if (!deviceMaterial.empty()) hash.update(deviceMaterial.data(), deviceMaterial.size());
const auto digest = hash.finish();
std::array<std::uint8_t, 16> tag{};
std::copy_n(digest.begin(), tag.size(), tag.begin());
return tag;
}
std::array<std::uint8_t, 32> deriveDeviceWrapKey(const std::vector<std::uint8_t>& deviceMaterial,
const std::array<std::uint8_t, 16>& salt) {
constexpr char domain[] = "FlipnoteNX-identity-wrap-v1";
Sha256 materialHash;
materialHash.update(reinterpret_cast<const std::uint8_t*>(domain), sizeof(domain));
if (!deviceMaterial.empty()) materialHash.update(deviceMaterial.data(), deviceMaterial.size());
const auto key = materialHash.finish();
std::vector<std::uint8_t> context(domain, domain + sizeof(domain));
context.insert(context.end(), salt.begin(), salt.end());
return hmacSha256(key, context.data(), context.size());
}
std::string deriveFsid(const PublicIdentity& identity) {
if (!hasPublicIdentity(identity)) return {};
constexpr char domain[] = "FlipnoteNX-public-fsid-v2";
Sha256 hash;
hash.update(reinterpret_cast<const std::uint8_t*>(domain), sizeof(domain));
hash.update(identity.key.data(), identity.key.size());
hash.update(identity.deviceTag.data(), identity.deviceTag.size());
const auto digest = hash.finish();
constexpr char hex[] = "0123456789ABCDEF";
std::string result(16, '0');
for (int i = 0; i < 8; ++i) {
result[i * 2] = hex[digest[static_cast<std::size_t>(i)] >> 4];
result[i * 2 + 1] = hex[digest[static_cast<std::size_t>(i)] & 15];
}
return result;
}
std::string identityFingerprint(const PublicIdentity& identity) {
if (!hasPublicIdentity(identity)) return {};
constexpr char domain[] = "FlipnoteNX-gallery-key-v2";
Sha256 hash;
hash.update(reinterpret_cast<const std::uint8_t*>(domain), sizeof(domain));
hash.update(identity.key.data(), identity.key.size());
const auto digest = hash.finish();
constexpr char hex[] = "0123456789abcdef";
std::string result;
result.reserve(digest.size() * 2);
for (std::uint8_t byte : digest) {
result.push_back(hex[byte >> 4]);
result.push_back(hex[byte & 15]);
}
return result;
}
bool createSigningIdentity(const std::array<std::uint8_t, 16>& deviceTag,
SigningIdentity& identity, std::string& error) {
if (!std::any_of(deviceTag.begin(), deviceTag.end(), [](std::uint8_t value) { return value != 0; }) ||
crypto_sign_ed25519_tweet_keypair(identity.publicIdentity.key.data(), identity.secretKey.data()) != 0) {
error = "could not create Ed25519 signing identity";
return false;
}
identity.publicIdentity.deviceTag = deviceTag;
error.clear();
return true;
}
bool sealSigningSecret(const SigningIdentity& identity,
const std::array<std::uint8_t, 32>& wrapKey,
const std::array<std::uint8_t, 24>& nonce,
std::array<std::uint8_t, 128>& sealed, std::string& error) {
std::array<std::uint8_t, 128> input{};
std::copy(identity.secretKey.begin(), identity.secretKey.end(), input.begin() + 32);
std::copy(identity.legacyHmacKey.begin(), identity.legacyHmacKey.end(), input.begin() + 96);
if (crypto_secretbox_xsalsa20poly1305_tweet(sealed.data(), input.data(), input.size(), nonce.data(), wrapKey.data()) != 0) {
error = "could not encrypt signing key";
return false;
}
error.clear();
return true;
}
bool unsealSigningSecret(const std::array<std::uint8_t, 128>& sealed,
const std::array<std::uint8_t, 32>& wrapKey,
const std::array<std::uint8_t, 24>& nonce,
SigningIdentity& identity, std::string& error) {
std::array<std::uint8_t, 128> output{};
if (crypto_secretbox_xsalsa20poly1305_tweet_open(output.data(), sealed.data(), sealed.size(), nonce.data(), wrapKey.data()) != 0 ||
std::any_of(output.begin(), output.begin() + 32, [](std::uint8_t value) { return value != 0; })) {
error = "identity key cannot be opened on this console";
return false;
}
std::copy(output.begin() + 32, output.begin() + 96, identity.secretKey.begin());
std::copy(output.begin() + 96, output.end(), identity.legacyHmacKey.begin());
error.clear();
return true;
}
double frameRate(const Metadata& metadata) {
constexpr std::array<double, 8> rates{{0.4985, 1.0, 2.0, 4.0, 5.9972, 11.986, 19.9459, 29.9275}};
if (metadata.speed >= 1 && metadata.speed <= rates.size()) return rates[metadata.speed - 1];
return std::max(1, static_cast<int>(metadata.fps));
}
bool validate(const Document& document, std::string& error) {
const Metadata& m = document.metadata;
if (document.frames.empty() || document.frames.size() > kMaxFrames) {
error = "frame count must be between 1 and 9999"; return false;
}
if (document.audioTracks.size() > kMaxAudioTracks) {
error = "too many audio tracks"; return false;
}
if (m.title.size() > 256 || m.description.size() > 4096 ||
m.authorName.size() > 128 || m.originalAuthorName.size() > 128 ||
m.lastEditorName.size() > 128) {
error = "metadata text is too long"; return false;
}
for (const std::string* text : {&m.title, &m.description, &m.authorName,
&m.originalAuthorName, &m.lastEditorName}) {
if (text->find('\0') != std::string::npos || !validUtf8(*text)) {
error = "metadata text is not valid UTF-8"; return false;
}
}
if (!isValidFsid(m.authorFsid) || !isValidFsid(m.originalAuthorFsid) ||
!isValidFsid(m.lastEditorFsid) || !isValidFsid(m.currentId) ||
!isValidFsid(m.rootId) || (!m.parentId.empty() && !isValidFsid(m.parentId))) {
error = "one or more IDs are not 16 hexadecimal characters"; return false;
}
const auto matchesIdentity = [](const PublicIdentity& identity, const std::string& fsid) {
return !hasPublicIdentity(identity) || sameId(deriveFsid(identity), fsid);
};
if (!matchesIdentity(m.authorIdentity, m.authorFsid) ||
!matchesIdentity(m.originalAuthorIdentity, m.originalAuthorFsid) ||
!matchesIdentity(m.lastEditorIdentity, m.lastEditorFsid)) {
error = "creator FSID does not match its public identity"; return false;
}
if (m.parentId.empty()) {
if (!sameId(m.currentId, m.rootId) || !sameId(m.authorFsid, m.originalAuthorFsid)) {
error = "an original Switchnote must have its own root ID and original author"; return false;
}
} else if (sameId(m.parentId, m.currentId) || sameId(m.rootId, m.currentId)) {
error = "a spin-off must have a new ID distinct from its parent and root"; return false;
}
if (m.fps < 1 || m.fps > 30 || m.speed > 8 || m.coverFrame >= document.frames.size() ||
m.createdUnixSeconds > m.modifiedUnixSeconds) {
error = "invalid playback or date metadata"; return false;
}
if (document.palette[0].a != 0) {
error = "palette index 0 must be transparent"; return false;
}
for (const Frame& frame : document.frames) {
for (const auto& layer : frame.layers) {
if (layer.size() != kPixelsPerLayer) {
error = "a layer has the wrong pixel count"; return false;
}
}
}
std::size_t audioSamples = 0;
for (std::size_t i = 0; i < document.audioTracks.size(); ++i) {
const AudioTrack& track = document.audioTracks[i];
if (track.name.size() > 64 || track.name.find('\0') != std::string::npos ||
!validUtf8(track.name) ||
track.sampleRate < 8000 || track.sampleRate > 48000 ||
(i == 0 && track.referenceFps != 0 && (track.referenceFps < 1 || track.referenceFps > 30)) ||
(i == 0 && track.referenceSpeed > 8) ||
(i == 0 && track.referenceSpeed != 0 && track.referenceFps != 0) ||
(i > 0 && (track.referenceFps != 0 || track.referenceSpeed != 0)) ||
(track.channels != 1 && track.channels != 2) ||
track.pcm.size() % track.channels != 0 ||
track.pcm.size() > kMaxAudioSamples - audioSamples ||
track.frameTriggers.size() > document.frames.size() ||
(i == 0 && !track.frameTriggers.empty())) {
error = "invalid audio track"; return false;
}
std::uint16_t previous = 0;
for (std::size_t cue = 0; cue < track.frameTriggers.size(); ++cue) {
const std::uint16_t frame = track.frameTriggers[cue];
if (frame >= document.frames.size() || (cue != 0 && frame <= previous)) {
error = "audio frame triggers must be sorted, unique and in range"; return false;
}
previous = frame;
}
audioSamples += track.pcm.size();
}
error.clear();
return true;
}
bool serialize(const Document& document, std::vector<std::uint8_t>& bytes,
std::string& error) {
if (!validate(document, error)) return false;
Writer w;
w.bytes.reserve(kHeaderBytes + kPaletteSize * 4 + document.frames.size() * 256);
w.raw(reinterpret_cast<const std::uint8_t*>("SPPM"), 4);
w.u16(kVersion);
w.u16(kHeaderBytes);
w.u64(0); // total size, patched below
w.u32(0); // CRC-32 of everything after the header
w.u16(kWidth);
w.u16(kHeight);
w.u8(static_cast<std::uint8_t>(kLayerCount));
w.u16(static_cast<std::uint16_t>(kPaletteSize));
w.u8(document.metadata.fps);
w.u8(static_cast<std::uint8_t>((document.metadata.locked ? 1 : 0) |
(document.metadata.allowSpinoffs ? 2 : 0) |
(document.metadata.bgmFollowsSpeed ? 4 : 0) |
(document.metadata.loopPlayback ? 0 : 8)));
w.u32(static_cast<std::uint32_t>(document.frames.size()));
w.u8(static_cast<std::uint8_t>(document.audioTracks.size()));
w.u16(document.metadata.speed); // v3 playback speed index; 0 retains legacy fps behavior
if (w.bytes.size() != kHeaderBytes) { error = "internal header size error"; return false; }
for (const Color& c : document.palette) { w.u8(c.r); w.u8(c.g); w.u8(c.b); w.u8(c.a); }
const Metadata& m = document.metadata;
w.text(m.title); w.text(m.description); w.text(m.authorName);
w.text(m.originalAuthorName); w.text(m.lastEditorName);
w.id(m.authorFsid); w.id(m.originalAuthorFsid); w.id(m.lastEditorFsid);
w.id(m.currentId); w.id(m.rootId); w.id(m.parentId);
w.u64(m.createdUnixSeconds); w.u64(m.modifiedUnixSeconds); w.u32(m.coverFrame);
for (const Frame& frame : document.frames) {
for (const auto& layer : frame.layers) {
const std::vector<std::uint8_t> encoded = encodeLayer(layer);
if (encoded.size() > kMaxLayerRleBytes) { error = "layer compression exceeded limit"; return false; }
w.u8(1); // RLE codec
w.u32(static_cast<std::uint32_t>(encoded.size()));
w.u32(crc32(encoded.data(), encoded.size()));
w.raw(encoded.data(), encoded.size());
}
}
for (std::size_t i = 0; i < document.audioTracks.size(); ++i) {
const AudioTrack& track = document.audioTracks[i];
w.u8(static_cast<std::uint8_t>(i));
w.u8(track.channels);
w.u8(track.referenceSpeed ? track.referenceSpeed : track.referenceFps);
w.u32(track.sampleRate);
w.text(track.name);
w.u16(static_cast<std::uint16_t>(track.frameTriggers.size()));
for (std::uint16_t frame : track.frameTriggers) w.u16(frame);
w.u32(static_cast<std::uint32_t>(track.pcm.size()));
std::vector<std::uint8_t> pcmBytes;
pcmBytes.reserve(track.pcm.size() * 2);
for (std::int16_t sample : track.pcm) {
const auto bits = static_cast<std::uint16_t>(sample);
pcmBytes.push_back(static_cast<std::uint8_t>(bits));
pcmBytes.push_back(static_cast<std::uint8_t>(bits >> 8));
}
w.u32(crc32(pcmBytes.data(), pcmBytes.size()));
w.raw(pcmBytes.data(), pcmBytes.size());
}
if (w.bytes.size() > kMaxFileBytes) { error = "file is too large"; return false; }
w.setU64(8, w.bytes.size());
w.setU32(16, crc32(w.bytes.data() + kHeaderBytes, w.bytes.size() - kHeaderBytes));
bytes.swap(w.bytes);
error.clear();
return true;
}
bool deserialize(const std::vector<std::uint8_t>& bytes, Document& document,
std::string& error) {
if (bytes.size() < kHeaderBytes || bytes.size() > kMaxFileBytes) {
error = "file size is outside supported bounds"; return false;
}
Reader r(bytes);
const std::uint8_t* magic;
std::uint16_t version, headerBytes, width, height, paletteCount, reserved;
std::uint64_t totalBytes;
std::uint32_t payloadCrc, frameCount;
std::uint8_t layerCount, fps, flags, audioCount;
if (!r.take(4, magic) || std::memcmp(magic, "SPPM", 4) != 0 ||
!r.u16(version) || !r.u16(headerBytes) || !r.u64(totalBytes) ||
!r.u32(payloadCrc) || !r.u16(width) || !r.u16(height) ||
!r.u8(layerCount) || !r.u16(paletteCount) || !r.u8(fps) ||
!r.u8(flags) || !r.u32(frameCount) || !r.u8(audioCount) || !r.u16(reserved)) {
error = "invalid or truncated SPPM header"; return false;
}
if ((version < 1 || version > kVersion) || headerBytes != kHeaderBytes || totalBytes != bytes.size() ||
width != kWidth || height != kHeight || layerCount != kLayerCount ||
paletteCount != kPaletteSize || fps < 1 || fps > 30 ||
(version == kLegacyVersion ? (flags & ~3U) != 0 : (flags & ~15U) != 0) ||
frameCount == 0 || frameCount > kMaxFrames || audioCount > kMaxAudioTracks ||
(version < 3 ? reserved != 0 : reserved > 8) ||
crc32(bytes.data() + kHeaderBytes, bytes.size() - kHeaderBytes) != payloadCrc) {
error = "unsupported SPPM layout or failed file CRC"; return false;
}
Document parsed;
parsed.metadata.fps = fps;
parsed.metadata.speed = version >= 3 ? static_cast<std::uint8_t>(reserved) : 0;
parsed.metadata.locked = (flags & 1U) != 0;
parsed.metadata.allowSpinoffs = (flags & 2U) != 0;
parsed.metadata.bgmFollowsSpeed = version == kLegacyVersion || (flags & 4U) != 0;
parsed.metadata.loopPlayback = version == kLegacyVersion || (flags & 8U) == 0;
for (Color& c : parsed.palette) {
if (!r.u8(c.r) || !r.u8(c.g) || !r.u8(c.b) || !r.u8(c.a)) {
error = "truncated palette"; return false;
}
// Migrate the two original default swatches to the requested SPPM
// palette: dark navy becomes purple and lime becomes pink. Palette
// indices remain stable, so existing drawings keep their structure.
if (c.a && c.r == 26 && c.g == 40 && c.b == 72) c = {132, 66, 183, c.a};
else if (c.a && c.r == 92 && c.g == 224 && c.b == 36) c = {239, 68, 133, c.a};
}
Metadata& m = parsed.metadata;
if (!r.text(m.title, 256) || !r.text(m.description, 4096) ||
!r.text(m.authorName, 128) || !r.text(m.originalAuthorName, 128) ||
!r.text(m.lastEditorName, 128) || !r.id(m.authorFsid, false) ||
!r.id(m.originalAuthorFsid, false) || !r.id(m.lastEditorFsid, false) ||
!r.id(m.currentId, false) || !r.id(m.rootId, false) ||
!r.id(m.parentId, true) || !r.u64(m.createdUnixSeconds) ||
!r.u64(m.modifiedUnixSeconds) || !r.u32(m.coverFrame)) {
error = "invalid or truncated metadata"; return false;
}
parsed.frames.reserve(frameCount);
for (std::uint32_t frameIndex = 0; frameIndex < frameCount; ++frameIndex) {
Frame frame;
for (auto& layer : frame.layers) {
std::uint8_t codec;
std::uint32_t length, layerCrc;
const std::uint8_t* compressed;
if (!r.u8(codec) || !r.u32(length) || !r.u32(layerCrc) || codec != 1 ||
length == 0 || length > kMaxLayerRleBytes || !r.take(length, compressed) ||
crc32(compressed, length) != layerCrc || !decodeLayer(compressed, length, layer)) {
error = "invalid frame layer or failed layer CRC"; return false;
}
}
parsed.frames.push_back(std::move(frame));
}
std::size_t totalSamples = 0;
for (std::uint8_t i = 0; i < audioCount; ++i) {
AudioTrack track;
std::uint8_t trackIndex;
std::uint16_t triggerCount;
std::uint32_t sampleCount, pcmCrc;
if (!r.u8(trackIndex) || !r.u8(track.channels) ||
(version >= 2 && !r.u8(track.referenceFps)) || !r.u32(track.sampleRate) ||
!r.text(track.name, 64) || !r.u16(triggerCount) ||
trackIndex != i || triggerCount > frameCount || (i == 0 && triggerCount != 0)) {
error = "invalid audio metadata"; return false;
}
if (version == kLegacyVersion && i == 0) track.referenceFps = parsed.metadata.fps;
if (version >= 3 && parsed.metadata.speed != 0) {
if (i == 0) { track.referenceSpeed = track.referenceFps; track.referenceFps = 0; }
else if (track.referenceFps != 0) { error = "invalid sound-effect speed metadata"; return false; }
}
track.frameTriggers.resize(triggerCount);
for (std::uint16_t& frame : track.frameTriggers) {
if (!r.u16(frame)) { error = "truncated audio triggers"; return false; }
}
if (!r.u32(sampleCount) || !r.u32(pcmCrc) ||
sampleCount > kMaxAudioSamples - totalSamples ||
(track.channels != 1 && track.channels != 2) ||
sampleCount % track.channels != 0 ||
track.sampleRate < 8000 || track.sampleRate > 48000) {
error = "invalid audio metadata"; return false;
}
const std::uint8_t* pcmBytes;
if (!r.take(static_cast<std::size_t>(sampleCount) * 2, pcmBytes) ||
crc32(pcmBytes, static_cast<std::size_t>(sampleCount) * 2) != pcmCrc) {
error = "truncated audio or failed audio CRC"; return false;
}
track.pcm.resize(sampleCount);
for (std::size_t n = 0; n < sampleCount; ++n) {
const std::uint16_t bits = static_cast<std::uint16_t>(pcmBytes[n * 2] | (pcmBytes[n * 2 + 1] << 8));
track.pcm[n] = static_cast<std::int16_t>(bits);
}
totalSamples += sampleCount;
parsed.audioTracks.push_back(std::move(track));
}
if (r.at != bytes.size()) { error = "unexpected trailing data"; return false; }
if (!validate(parsed, error)) return false;
document = std::move(parsed);
error.clear();
return true;
}
bool loadFile(const std::string& path, Document& document, std::string& error) {
std::ifstream input(std::filesystem::path(path), std::ios::binary | std::ios::ate);
if (!input) { error = "could not open file"; return false; }
const std::streamoff size = input.tellg();
if (size < 0 || static_cast<std::uint64_t>(size) > kMaxFileBytes) {
error = "file is too large"; return false;
}
std::vector<std::uint8_t> bytes(static_cast<std::size_t>(size));
input.seekg(0, std::ios::beg);
if (!bytes.empty()) input.read(reinterpret_cast<char*>(bytes.data()), size);
if (!input && size != 0) { error = "could not read complete file"; return false; }
return deserialize(bytes, document, error);
}
bool saveFile(const std::string& path, const Document& document, std::string& error) {
std::vector<std::uint8_t> bytes;
if (!serialize(document, bytes, error)) return false;
return commitBytes(path, bytes, error);
}
namespace {
bool deserializePublicSigned(const std::vector<std::uint8_t>& bytes, Document& document, std::string& error);
}
bool deserializeSigned(const std::vector<std::uint8_t>& bytes, Document& document,
const std::array<std::uint8_t, 32>& signingKey,
std::string& error) {
if (bytes.size() >= kPublicSignatureBytes &&
std::memcmp(bytes.data() + bytes.size() - kPublicSignatureBytes, "SPP3", 4) == 0) {
return deserializePublicSigned(bytes, document, error);
}
if (bytes.size() < kSignatureBytes || std::memcmp(bytes.data() + bytes.size() - kSignatureBytes, "SPPS", 4) != 0) {
// Read-only compatibility for v1 notes; the next save upgrades them.
if (bytes.size() >= 6 && (bytes[4] != 1 || bytes[5] != 0)) {
error = "signed Switchnote is missing its signature";
return false;
}
return deserialize(bytes, document, error);
}
const std::size_t bodySize = bytes.size() - kSignatureBytes;
const auto expected = hmacSha256(signingKey, bytes.data(), bodySize);
unsigned difference = 0;
for (std::size_t i = 0; i < expected.size(); ++i) difference |= expected[i] ^ bytes[bodySize + 4 + i];
if (difference != 0) { error = "Switchnote signature is invalid"; return false; }
std::vector<std::uint8_t> body(bytes.begin(), bytes.begin() + static_cast<std::ptrdiff_t>(bodySize));
if (body.size() < 6 || (body[4] != 2 && body[4] != 3) || body[5] != 0) {
error = "unsupported signed Switchnote version";
return false;
}
// Earlier signed notes used a v1 payload with the same envelope marker.
// Authenticate first, then prefer the current layout and fall back to v1.
if (body[4] == 3 && deserialize(body, document, error)) return true;
body[4] = kVersion;
if (deserialize(body, document, error)) return true;
body[4] = kLegacyVersion;
return deserialize(body, document, error);
}
bool loadSignedFile(const std::string& path, Document& document,
const std::array<std::uint8_t, 32>& signingKey,
std::string& error) {
std::ifstream input(std::filesystem::path(path), std::ios::binary | std::ios::ate);
if (!input) { error = "could not open file"; return false; }
const std::streamoff size = input.tellg();
if (size < 0 || static_cast<std::uint64_t>(size) > kMaxFileBytes + kSignatureBytes) {
error = "file is too large"; return false;
}
std::vector<std::uint8_t> bytes(static_cast<std::size_t>(size));
input.seekg(0, std::ios::beg);
if (!bytes.empty()) input.read(reinterpret_cast<char*>(bytes.data()), size);
if (!input && size != 0) { error = "could not read complete file"; return false; }
return deserializeSigned(bytes, document, signingKey, error);
}
bool saveSignedFile(const std::string& path, const Document& document,
const std::array<std::uint8_t, 32>& signingKey,
std::string& error) {
std::vector<std::uint8_t> bytes;
if (!serialize(document, bytes, error)) return false;
bytes[4] = kVersion;
const auto signature = hmacSha256(signingKey, bytes.data(), bytes.size());
bytes.insert(bytes.end(), {'S', 'P', 'P', 'S'});
bytes.insert(bytes.end(), signature.begin(), signature.end());
return commitBytes(path, bytes, error);
}
namespace {
void appendIdentity(std::vector<std::uint8_t>& out, const PublicIdentity& identity) {
out.insert(out.end(), identity.key.begin(), identity.key.end());
out.insert(out.end(), identity.deviceTag.begin(), identity.deviceTag.end());
}
bool readIdentity(const std::vector<std::uint8_t>& bytes, std::size_t& offset, PublicIdentity& identity) {
if (offset > bytes.size() || bytes.size() - offset < identity.key.size() + identity.deviceTag.size()) return false;
std::copy_n(bytes.begin() + static_cast<std::ptrdiff_t>(offset), identity.key.size(), identity.key.begin());
offset += identity.key.size();
std::copy_n(bytes.begin() + static_cast<std::ptrdiff_t>(offset), identity.deviceTag.size(), identity.deviceTag.begin());
offset += identity.deviceTag.size();
return true;
}
std::array<std::uint8_t, 32> publicSignatureDigest(const std::vector<std::uint8_t>& body,
const PublicIdentity& author,
const PublicIdentity& original,
const PublicIdentity& editor) {
constexpr char domain[] = "FlipnoteNX-SPPM-public-signature-v3";
Sha256 hash;
hash.update(reinterpret_cast<const std::uint8_t*>(domain), sizeof(domain));
hash.update(body.data(), body.size());
hash.update(author.key.data(), author.key.size());
hash.update(author.deviceTag.data(), author.deviceTag.size());
hash.update(original.key.data(), original.key.size());
hash.update(original.deviceTag.data(), original.deviceTag.size());
hash.update(editor.key.data(), editor.key.size());
hash.update(editor.deviceTag.data(), editor.deviceTag.size());
return hash.finish();
}
bool deserializePublicSigned(const std::vector<std::uint8_t>& bytes, Document& document, std::string& error) {
if (bytes.size() < kPublicSignatureBytes) { error = "public signature is truncated"; return false; }
const std::size_t footer = bytes.size() - kPublicSignatureBytes;
if (std::memcmp(bytes.data() + footer, "SPP3", 4) != 0) { error = "public signature marker is invalid"; return false; }
std::size_t offset = footer + 4;
PublicIdentity author, original, editor;
if (!readIdentity(bytes, offset, author) || !readIdentity(bytes, offset, original) || !readIdentity(bytes, offset, editor) ||
bytes.size() - offset != 64 || !hasPublicIdentity(editor)) {
error = "public identity footer is invalid"; return false;
}
std::vector<std::uint8_t> body(bytes.begin(), bytes.begin() + static_cast<std::ptrdiff_t>(footer));
if (body.size() < 6 || body[4] != 3 || body[5] != 0) { error = "unsupported public signed Switchnote version"; return false; }
const auto digest = publicSignatureDigest(body, author, original, editor);
std::array<std::uint8_t, 96> signedDigest{};
std::copy_n(bytes.begin() + static_cast<std::ptrdiff_t>(offset), 64, signedDigest.begin());
std::copy(digest.begin(), digest.end(), signedDigest.begin() + 64);
// crypto_sign_open copies the complete message into its output buffer
// before checking the signature. The signed payload here is 64 bytes of
// Ed25519 signature plus the 32-byte digest, so the destination must be
// the full 96 bytes even though only the digest is compared below.
std::array<std::uint8_t, 96> verifiedMessage{};
unsigned long long verifiedLength = 0;
if (crypto_sign_ed25519_tweet_open(verifiedMessage.data(), &verifiedLength, signedDigest.data(), signedDigest.size(), editor.key.data()) != 0 ||
verifiedLength != digest.size() || !std::equal(digest.begin(), digest.end(), verifiedMessage.begin())) {
error = "Switchnote public signature is invalid"; return false;
}
Document parsed;
body[4] = kVersion;
if (!deserialize(body, parsed, error)) {
body[4] = kLegacyVersion;
if (!deserialize(body, parsed, error)) return false;
}
parsed.metadata.authorIdentity = author;
parsed.metadata.originalAuthorIdentity = original;
parsed.metadata.lastEditorIdentity = editor;
if (!validate(parsed, error)) return false;
document = std::move(parsed);
error.clear();
return true;
}
} // namespace
bool savePublicSignedFile(const std::string& path, const Document& document,
const SigningIdentity& identity, std::string& error) {
if (!hasPublicIdentity(identity.publicIdentity) ||
!sameId(deriveFsid(identity.publicIdentity), document.metadata.lastEditorFsid) ||
!hasPublicIdentity(document.metadata.lastEditorIdentity) ||
document.metadata.lastEditorIdentity.key != identity.publicIdentity.key ||
document.metadata.lastEditorIdentity.deviceTag != identity.publicIdentity.deviceTag) {
error = "document last editor does not match signing identity";
return false;
}
std::vector<std::uint8_t> body;
if (!serialize(document, body, error)) return false;
body[4] = 3;
const auto digest = publicSignatureDigest(body, document.metadata.authorIdentity,
document.metadata.originalAuthorIdentity,
document.metadata.lastEditorIdentity);
std::array<std::uint8_t, 96> signedDigest{};
unsigned long long signedLength = 0;
if (crypto_sign_ed25519_tweet(signedDigest.data(), &signedLength, digest.data(), digest.size(), identity.secretKey.data()) != 0 ||
signedLength != signedDigest.size()) {
error = "could not sign Switchnote";
return false;
}
body.insert(body.end(), {'S', 'P', 'P', '3'});
appendIdentity(body, document.metadata.authorIdentity);
appendIdentity(body, document.metadata.originalAuthorIdentity);
appendIdentity(body, document.metadata.lastEditorIdentity);
body.insert(body.end(), signedDigest.begin(), signedDigest.begin() + 64);
return commitBytes(path, body, error);
}
bool signMessage(const SigningIdentity& identity, const std::uint8_t* message,
std::size_t messageSize, std::array<std::uint8_t, 64>& signature,
std::string& error) {
if (!hasPublicIdentity(identity.publicIdentity) || (messageSize && !message) ||
!std::equal(identity.publicIdentity.key.begin(), identity.publicIdentity.key.end(),
identity.secretKey.begin() + 32) ||
messageSize > std::numeric_limits<std::size_t>::max() - 64 ||
messageSize > std::numeric_limits<unsigned long long>::max() - 64) {
error = "invalid signing identity or request message";
return false;
}
std::vector<unsigned char> signedMessage(messageSize + 64);
unsigned long long signedLength = 0;
if (crypto_sign_ed25519_tweet(signedMessage.data(), &signedLength, message,
static_cast<unsigned long long>(messageSize),
identity.secretKey.data()) != 0 ||
signedLength != signedMessage.size()) {
std::fill(signedMessage.begin(), signedMessage.end(), 0);
error = "could not sign Gallery request";
return false;
}
std::copy_n(signedMessage.begin(), signature.size(), signature.begin());
std::fill(signedMessage.begin(), signedMessage.end(), 0);
error.clear();
return true;
}
bool createSpinoff(const Document& parent, std::string_view newAuthorName,
std::string_view newAuthorFsid, std::string_view newCurrentId,
std::uint64_t nowUnixSeconds, Document& child,
std::string& error) {
if (!validate(parent, error)) return false;
if (parent.metadata.locked || !parent.metadata.allowSpinoffs) {
error = "this Switchnote does not permit spin-offs"; return false;
}
if (newAuthorName.size() > 128 || newAuthorName.find('\0') != std::string_view::npos ||
!validUtf8(newAuthorName) ||
!isValidFsid(newAuthorFsid) || !isValidFsid(newCurrentId) ||
sameId(newCurrentId, parent.metadata.currentId) ||
sameId(newCurrentId, parent.metadata.rootId) ||
nowUnixSeconds < parent.metadata.createdUnixSeconds) {
error = "invalid spin-off identity or creation time"; return false;
}
Document fork = parent;
Metadata& m = fork.metadata;
m.authorName.assign(newAuthorName);
m.authorFsid = upperId(newAuthorFsid);
m.authorIdentity = {};
m.lastEditorName = m.authorName;
m.lastEditorFsid = m.authorFsid;
m.lastEditorIdentity = {};
m.parentId = upperId(parent.metadata.currentId);
m.currentId = upperId(newCurrentId);
m.createdUnixSeconds = nowUnixSeconds;
m.modifiedUnixSeconds = nowUnixSeconds;
m.locked = false;
if (!validate(fork, error)) return false;
child = std::move(fork);
error.clear();
return true;
}
} // namespace sppm