Add files via upload

This commit is contained in:
Rekushi authored and GitHub committed 2026-10-29 03:55:40 +00:00
commit 558a387b6e
35 files changed
+14482

No files matched your search

+289
View File
@@ -0,0 +1,289 @@
#include "sppm.hpp"
#include <array>
#include <cstdio>
#include <filesystem>
#include <fstream>
#include <string>
#include <vector>
namespace {
int failures = 0;
void check(bool condition, const char* description) {
if (!condition) {
std::fprintf(stderr, "FAIL: %s\n", description);
++failures;
}
}
std::uint32_t crc32(const std::vector<std::uint8_t>& bytes, std::size_t start) {
std::uint32_t crc = 0xFFFFFFFFU;
for (std::size_t i = start; i < bytes.size(); ++i) {
crc ^= bytes[i];
for (int bit = 0; bit < 8; ++bit) crc = (crc >> 1) ^ ((crc & 1U) ? 0xEDB88320U : 0U);
}
return crc ^ 0xFFFFFFFFU;
}
sppm::Document example() {
sppm::Document note;
auto& m = note.metadata;
m.title = "A small Switchnote";
m.description = "Two frames and a sound effect";
m.authorName = "Ada";
m.originalAuthorName = "Ada";
m.lastEditorName = "Ada";
m.authorFsid = "0123456789ABCDEF";
m.originalAuthorFsid = m.authorFsid;
m.lastEditorFsid = m.authorFsid;
m.currentId = "1111111111111111";
m.rootId = m.currentId;
m.createdUnixSeconds = 1000;
m.modifiedUnixSeconds = 1100;
m.fps = 12;
note.frames.emplace_back();
note.frames.emplace_back();
note.frames[0].layers[0][1] = 2;
note.frames[0].layers[2][10000] = 15;
note.frames[1].layers[3][sppm::kPixelsPerLayer - 1] = 6;
for (std::size_t i = 0; i < sppm::kPixelsPerLayer; ++i) {
note.frames[1].layers[1][i] = static_cast<std::uint8_t>(i % 251);
}
note.palette[6] = {12, 34, 56, 255};
sppm::AudioTrack sound;
sound.name = "BGM";
sound.sampleRate = 8000;
sound.channels = 1;
sound.referenceFps = 8;
sound.pcm = {0, 100, -100, 32767, -32768};
note.audioTracks.push_back(sound);
sound.name = "SE1";
sound.referenceFps = 0;
sound.frameTriggers = {1};
sound.pcm = {-3, 2};
note.audioTracks.push_back(sound);
return note;
}
} // namespace
int main() {
sppm::PublicIdentity fingerprintIdentity{};
for (std::size_t i = 0; i < fingerprintIdentity.key.size(); ++i)
fingerprintIdentity.key[i] = static_cast<std::uint8_t>(i);
for (std::size_t i = 0; i < fingerprintIdentity.deviceTag.size(); ++i)
fingerprintIdentity.deviceTag[i] = static_cast<std::uint8_t>(i + 32);
check(sppm::identityFingerprint(fingerprintIdentity) ==
"c27a4648f0825f8e69be9f507bcfa3dee5f614565a2d847c8445caebc19345df",
"Gallery ownership fingerprint is bound to the signing key");
const sppm::Palette defaults = sppm::defaultPalette();
check(defaults[6].r == 239 && defaults[6].g == 68 && defaults[6].b == 133 &&
defaults[10].r == 132 && defaults[10].g == 66 && defaults[10].b == 183,
"default Switchnote palette uses pink and purple replacements");
std::array<std::uint8_t, 32> secret{};
std::array<std::uint8_t, 16> nonce{};
for (std::size_t i = 0; i < secret.size(); ++i) secret[i] = static_cast<std::uint8_t>(i);
for (std::size_t i = 0; i < nonce.size(); ++i) nonce[i] = static_cast<std::uint8_t>(i);
check(sppm::deriveFsid(secret, nonce, "note") == "0305B0DFF9F8600F",
"HMAC-SHA256 FSID matches independent test vector");
check(sppm::deriveFsid(secret, nonce, "note") != sppm::deriveFsid(secret, nonce, "creator"),
"FSID domains are separated");
check(sppm::isValidFsid("0123456789abcdef"), "lowercase hex IDs are accepted");
check(!sppm::isValidFsid("0000000000000000"), "null lineage ID is reserved");
sppm::Document original = example();
std::string error;
std::vector<std::uint8_t> encoded;
check(sppm::serialize(original, encoded, error), "serialize valid Switchnote");
check(encoded.size() > 36, "serialized file contains payload");
sppm::Document loaded;
check(sppm::deserialize(encoded, loaded, error), "deserialize valid Switchnote");
check(loaded.metadata.title == original.metadata.title &&
loaded.metadata.currentId == original.metadata.currentId &&
loaded.metadata.createdUnixSeconds == original.metadata.createdUnixSeconds,
"metadata survives round trip");
check(loaded.frames.size() == 2 && loaded.frames[0].layers[0][1] == 2 &&
loaded.frames[0].layers[2][10000] == 15 &&
loaded.frames[1].layers[1][123456] == static_cast<std::uint8_t>(123456 % 251) &&
loaded.frames[1].layers[3][sppm::kPixelsPerLayer - 1] == 6,
"all frames and layers survive round trip");
check(loaded.palette[6].r == 12 && loaded.palette[6].g == 34 &&
loaded.audioTracks.size() == 2 && loaded.audioTracks[0].pcm[4] == -32768 &&
loaded.audioTracks[1].frameTriggers == std::vector<std::uint16_t>{1},
"palette, PCM audio, and SE timing survive round trip");
check(loaded.metadata.bgmFollowsSpeed && loaded.audioTracks[0].referenceFps == 8,
"BGM timing reference survives round trip");
check(loaded.metadata.loopPlayback, "existing Switchnotes loop by default");
sppm::Document speedOne = original;
speedOne.metadata.speed = 1;
speedOne.metadata.fps = 1;
speedOne.audioTracks[0].referenceFps = 0;
speedOne.audioTracks[0].referenceSpeed = 1;
std::vector<std::uint8_t> speedBytes;
sppm::Document speedLoaded;
check(sppm::serialize(speedOne, speedBytes, error) && sppm::deserialize(speedBytes, speedLoaded, error) &&
speedLoaded.metadata.speed == 1 && speedLoaded.audioTracks[0].referenceSpeed == 1 &&
speedLoaded.audioTracks[0].referenceFps == 0 && sppm::frameRate(speedLoaded.metadata) == 0.4985,
"DSi speed 1 and its BGM reference survive v3 round trip at half FPS");
original.metadata.bgmFollowsSpeed = false;
check(sppm::serialize(original, encoded, error) && sppm::deserialize(encoded, loaded, error) &&
!loaded.metadata.bgmFollowsSpeed && loaded.audioTracks[0].referenceFps == 8,
"loose BGM timing survives round trip");
original.metadata.loopPlayback = false;
check(sppm::serialize(original, encoded, error) && sppm::deserialize(encoded, loaded, error) &&
!loaded.metadata.loopPlayback,
"disabled playback loop survives round trip");
original = example();
check(sppm::serialize(original, encoded, error), "restore encoded sample for corruption tests");
// Version 1 had no BGM reference-speed byte. Keep already-saved notes
// readable after adding explicit speed modes in version 2.
sppm::Document oneTrack = example();
oneTrack.audioTracks.resize(1);
std::vector<std::uint8_t> oldFormat, noAudioBytes;
check(sppm::serialize(oneTrack, oldFormat, error), "make one-track compatibility fixture");
oneTrack.audioTracks.clear();
check(sppm::serialize(oneTrack, noAudioBytes, error), "find audio section boundary");
const std::size_t audioStart = noAudioBytes.size();
if (oldFormat.size() > audioStart + 2) {
oldFormat.erase(oldFormat.begin() + static_cast<std::ptrdiff_t>(audioStart + 2));
oldFormat[4] = 1;
oldFormat[28] &= static_cast<std::uint8_t>(~4U);
const std::uint64_t fileSize = oldFormat.size();
for (int i = 0; i < 8; ++i) oldFormat[8 + i] = static_cast<std::uint8_t>(fileSize >> (8 * i));
const std::uint32_t checksum = crc32(oldFormat, 36);
for (int i = 0; i < 4; ++i) oldFormat[16 + i] = static_cast<std::uint8_t>(checksum >> (8 * i));
sppm::Document v1Loaded;
check(sppm::deserialize(oldFormat, v1Loaded, error) &&
v1Loaded.metadata.bgmFollowsSpeed && v1Loaded.metadata.loopPlayback &&
v1Loaded.audioTracks[0].referenceFps == 12,
"version 1 SPPM audio remains playable");
} else check(false, "version 1 compatibility fixture has an audio section");
sppm::Document legacyPalette = example();
legacyPalette.palette[6] = {92, 224, 36, 255};
legacyPalette.palette[10] = {26, 40, 72, 255};
legacyPalette.frames[0].layers[0][20] = 6;
legacyPalette.frames[0].layers[0][21] = 10;
std::vector<std::uint8_t> oldPaletteBytes;
sppm::Document migratedPalette;
check(sppm::serialize(legacyPalette, oldPaletteBytes, error) &&
sppm::deserialize(oldPaletteBytes, migratedPalette, error) &&
migratedPalette.palette[6].r == 239 && migratedPalette.palette[6].g == 68 &&
migratedPalette.palette[10].r == 132 && migratedPalette.palette[10].b == 183 &&
migratedPalette.frames[0].layers[0][20] == 6 &&
migratedPalette.frames[0].layers[0][21] == 10,
"legacy SPPM colors migrate without changing indexed drawing data");
std::vector<std::uint8_t> broken = encoded;
broken.back() ^= 0x55;
loaded.metadata.title = "untouched";
check(!sppm::deserialize(broken, loaded, error), "corrupt file is rejected");
check(loaded.metadata.title == "untouched", "failed load does not replace document");
broken = encoded;
broken.resize(broken.size() - 1);
check(!sppm::deserialize(broken, loaded, error), "truncated file is rejected");
broken = encoded;
broken[28] = 0xFF;
check(!sppm::deserialize(broken, loaded, error), "unknown flags are rejected");
sppm::Document fork;
check(sppm::createSpinoff(original, "Ben", "ABCDEF0123456789",
"2222222222222222", 2000, fork, error),
"allowed spin-off is created");
check(fork.metadata.authorName == "Ben" && fork.metadata.originalAuthorName == "Ada" &&
fork.metadata.originalAuthorFsid == original.metadata.originalAuthorFsid &&
fork.metadata.rootId == original.metadata.rootId &&
fork.metadata.parentId == original.metadata.currentId &&
fork.metadata.currentId == "2222222222222222" && !fork.metadata.locked,
"spin-off maintains immutable lineage and new ownership");
fork.metadata.title = "fork edit";
check(original.metadata.title != fork.metadata.title, "spin-off content is independent");
original.metadata.locked = true;
check(!sppm::createSpinoff(original, "Ben", "ABCDEF0123456789",
"3333333333333333", 2000, fork, error),
"locked Switchnote rejects spin-off");
original.metadata.locked = false;
original.metadata.allowSpinoffs = false;
check(!sppm::createSpinoff(original, "Ben", "ABCDEF0123456789",
"3333333333333333", 2000, fork, error),
"author spin-off permission is enforced");
original = example();
original.audioTracks[1].frameTriggers = {2};
check(!sppm::validate(original, error), "audio cue outside frame range is rejected");
original = example();
original.metadata.title = std::string("bad\xC0\x80", 5);
check(!sppm::validate(original, error), "invalid UTF-8 metadata is rejected");
original = example();
const auto path = std::filesystem::temp_directory_path() / "switchnote-sppm-test.sppm";
check(sppm::saveFile(path.string(), original, error), "atomic save creates a file");
sppm::Document fromDisk;
check(sppm::loadFile(path.string(), fromDisk, error), "load reads saved file");
check(fromDisk.metadata.title == original.metadata.title, "disk round trip preserves title");
original.metadata.title = "Replaced safely";
check(sppm::saveFile(path.string(), original, error), "atomic save replaces existing file");
check(sppm::loadFile(path.string(), fromDisk, error) &&
fromDisk.metadata.title == "Replaced safely", "replacement file is complete");
std::array<std::uint8_t, 32> signingKey{};
signingKey.fill(0x5A);
check(sppm::saveSignedFile(path.string(), original, signingKey, error),
"signed save creates an authenticated file");
check(sppm::loadSignedFile(path.string(), fromDisk, signingKey, error) &&
fromDisk.metadata.title == "Replaced safely", "signed file verifies and round-trips");
std::vector<std::uint8_t> signedBytes;
{
std::ifstream signedInput(path, std::ios::binary | std::ios::ate);
const auto signedSize = signedInput.tellg();
signedBytes.resize(static_cast<std::size_t>(signedSize));
signedInput.seekg(0);
signedInput.read(reinterpret_cast<char*>(signedBytes.data()), signedSize);
}
if (!signedBytes.empty()) signedBytes[40] ^= 0x01;
check(!sppm::deserializeSigned(signedBytes, fromDisk, signingKey, error),
"tampered signed file is rejected");
std::array<std::uint8_t, 16> deviceTag{};
deviceTag.fill(0x42);
sppm::SigningIdentity identity{};
check(sppm::createSigningIdentity(deviceTag, identity, error),
"public signing identity can be created");
sppm::Document publicNote = example();
const std::string publicFsid = sppm::deriveFsid(identity.publicIdentity);
publicNote.metadata.authorFsid = publicFsid;
publicNote.metadata.originalAuthorFsid = publicFsid;
publicNote.metadata.lastEditorFsid = publicFsid;
publicNote.metadata.authorIdentity = identity.publicIdentity;
publicNote.metadata.originalAuthorIdentity = identity.publicIdentity;
publicNote.metadata.lastEditorIdentity = identity.publicIdentity;
check(sppm::savePublicSignedFile(path.string(), publicNote, identity, error),
"public-key signed save creates an SPPM file");
check(sppm::loadSignedFile(path.string(), fromDisk, signingKey, error) &&
fromDisk.metadata.title == publicNote.metadata.title &&
fromDisk.frames.size() == publicNote.frames.size() &&
fromDisk.frames[1].layers[3][sppm::kPixelsPerLayer - 1] == 6,
"public-key signature verifies and the saved Switchnote reopens");
signedBytes.clear();
{
std::ifstream publicInput(path, std::ios::binary | std::ios::ate);
const auto publicSize = publicInput.tellg();
if (publicSize >= 0) {
signedBytes.resize(static_cast<std::size_t>(publicSize));
publicInput.seekg(0);
publicInput.read(reinterpret_cast<char*>(signedBytes.data()), publicSize);
}
}
if (!signedBytes.empty()) signedBytes[40] ^= 0x01;
check(!sppm::deserializeSigned(signedBytes, fromDisk, signingKey, error),
"tampered public-key signed Switchnote is rejected");
std::error_code ignored;
std::filesystem::remove(path, ignored);
if (failures == 0) std::puts("SPPM tests passed");
return failures == 0 ? 0 : 1;
}