Add files via upload
This commit is contained in:
35 files changed
+14482
No files matched your search
@@ -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;
|
||||
}
|
||||
Reference in new issue
Block a user
