Add files via upload
This commit is contained in:
35 files changed
+14482
No files matched your search
@@ -0,0 +1,9 @@
|
||||
cmake_minimum_required(VERSION 3.16)
|
||||
project(sppm_test LANGUAGES C CXX)
|
||||
|
||||
add_executable(sppm_test ../source/sppm.cpp ../third_party/tweetnacl.c sppm_test.cpp)
|
||||
target_include_directories(sppm_test PRIVATE ../include ../third_party)
|
||||
target_compile_features(sppm_test PRIVATE cxx_std_17)
|
||||
|
||||
enable_testing()
|
||||
add_test(NAME sppm_roundtrip COMMAND sppm_test)
|
||||
@@ -0,0 +1,70 @@
|
||||
'use strict';
|
||||
|
||||
const assert = require('node:assert/strict');
|
||||
const fs = require('node:fs');
|
||||
const path = require('node:path');
|
||||
const vm = require('node:vm');
|
||||
|
||||
async function main() {
|
||||
const source = fs.readFileSync(path.join(__dirname, '..', 'source', 'audio_upload_server.cpp'), 'utf8');
|
||||
const script = source.match(/<script>([\s\S]*?)<\/script>/)?.[1];
|
||||
assert.ok(script, 'embedded upload script exists');
|
||||
|
||||
const handlers = {};
|
||||
const elements = new Map();
|
||||
for (const id of ['code', 'pair', 'pairStatus', 'connected', 'uploadPanel', 'track', 'file', 'send', 'status']) {
|
||||
elements.set(id, {
|
||||
value: '', hidden: id === 'connected' || id === 'uploadPanel',
|
||||
className: 'status', textContent: '', files: [], disabled: false,
|
||||
addEventListener(event, handler) { handlers[id + ':' + event] = handler; },
|
||||
});
|
||||
}
|
||||
elements.get('track').value = '0';
|
||||
elements.get('file').files = [{
|
||||
name: 'short.wav', size: 4, arrayBuffer: async () => new ArrayBuffer(4),
|
||||
}];
|
||||
|
||||
const token = 'a'.repeat(64);
|
||||
const requests = [];
|
||||
const fetch = async (url, options) => {
|
||||
requests.push({url, options});
|
||||
if (url === '/api/connect') return {ok: true, text: async () => token};
|
||||
if (url === '/api/upload') return {ok: true, text: async () => 'Received'};
|
||||
if (url === '/api/status') return {ok: true, text: async () => 'imported'};
|
||||
throw new Error('Unexpected request: ' + url);
|
||||
};
|
||||
class AudioContext {
|
||||
async decodeAudioData() {
|
||||
return {
|
||||
length: 1, sampleRate: 48000, numberOfChannels: 1,
|
||||
getChannelData: () => new Float32Array([0]),
|
||||
};
|
||||
}
|
||||
async close() {}
|
||||
}
|
||||
vm.runInNewContext(script, {
|
||||
document: {getElementById: id => elements.get(id)},
|
||||
window: {AudioContext}, fetch, Blob, ArrayBuffer, DataView,
|
||||
setTimeout: callback => callback(),
|
||||
});
|
||||
|
||||
elements.get('code').value = '123456';
|
||||
handlers['code:input']();
|
||||
await new Promise(setImmediate);
|
||||
assert.equal(elements.get('code').value, '123-456');
|
||||
assert.equal(requests[0].url, '/api/connect');
|
||||
assert.equal(requests[0].options.headers.Authorization, 'Bearer 123-456');
|
||||
assert.equal(elements.get('pair').hidden, true);
|
||||
assert.equal(elements.get('connected').hidden, false);
|
||||
assert.match(source, /id="connected"[^>]*>Connected to Nintendo Switch<\/div>/);
|
||||
assert.equal(elements.get('uploadPanel').hidden, false);
|
||||
|
||||
await handlers['send:click']();
|
||||
assert.equal(requests[1].url, '/api/upload');
|
||||
assert.equal(requests[1].options.headers.Authorization, 'Bearer ' + token);
|
||||
assert.equal(requests[2].url, '/api/status');
|
||||
assert.equal(requests[2].options.headers.Authorization, 'Bearer ' + token);
|
||||
assert.equal(elements.get('status').className, 'status ok');
|
||||
}
|
||||
|
||||
main().catch(error => { console.error(error); process.exitCode = 1; });
|
||||
@@ -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
