#include "sppm.hpp" #include #include #include #include #include #include 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& 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(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(i); for (std::size_t i = 0; i < fingerprintIdentity.deviceTag.size(); ++i) fingerprintIdentity.deviceTag[i] = static_cast(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 secret{}; std::array nonce{}; for (std::size_t i = 0; i < secret.size(); ++i) secret[i] = static_cast(i); for (std::size_t i = 0; i < nonce.size(); ++i) nonce[i] = static_cast(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 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(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{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 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 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(audioStart + 2)); oldFormat[4] = 1; oldFormat[28] &= static_cast(~4U); const std::uint64_t fileSize = oldFormat.size(); for (int i = 0; i < 8; ++i) oldFormat[8 + i] = static_cast(fileSize >> (8 * i)); const std::uint32_t checksum = crc32(oldFormat, 36); for (int i = 0; i < 4; ++i) oldFormat[16 + i] = static_cast(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 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 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 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 signedBytes; { std::ifstream signedInput(path, std::ios::binary | std::ios::ate); const auto signedSize = signedInput.tellg(); signedBytes.resize(static_cast(signedSize)); signedInput.seekg(0); signedInput.read(reinterpret_cast(signedBytes.data()), signedSize); } if (!signedBytes.empty()) signedBytes[40] ^= 0x01; check(!sppm::deserializeSigned(signedBytes, fromDisk, signingKey, error), "tampered signed file is rejected"); std::array 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(publicSize)); publicInput.seekg(0); publicInput.read(reinterpret_cast(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; }