e0b4ec2e21
Reject NUL/control bytes and Windows-hostile names in entry names, relax the over-broad ".." substring ban to component-only, close the trailing-garbage gap on empty manifests, and relocate the package CMake subdirectory to its ladder home.
134 lines
5.6 KiB
C++
134 lines
5.6 KiB
C++
// Standalone tests for reasampler::package's format contract — no REAPER, no
|
|
// test framework. Pins the version-ladder classification (both integers, every
|
|
// branch) and the entry-name rule that makes path expression structurally
|
|
// impossible in a package.
|
|
|
|
#include "../src/core/package/package_format.h"
|
|
|
|
#include <cstdio>
|
|
#include <string>
|
|
|
|
using namespace reasampler::package;
|
|
|
|
static int g_fail = 0;
|
|
#define CHECK(cond) do { if(!(cond)) { \
|
|
std::printf("FAIL line %d: %s\n", __LINE__, #cond); ++g_fail; } } while(0)
|
|
|
|
// --- classifyPackageVersion --------------------------------------------------
|
|
|
|
static void testClassifyReadable() {
|
|
CHECK(classifyPackageVersion(kPackageFormatVersion, kPackageMinReaderVersion) ==
|
|
PackageReadability::Readable);
|
|
// The additive-forward-compat direction: a newer writer whose minReader
|
|
// still reaches back to this build reads fine.
|
|
CHECK(classifyPackageVersion(kPackageFormatVersion + 5, kPackageMinReaderVersion) ==
|
|
PackageReadability::Readable);
|
|
// Boundary: minReader exactly this build's format version.
|
|
CHECK(classifyPackageVersion(kPackageFormatVersion + 1, kPackageFormatVersion) ==
|
|
PackageReadability::Readable);
|
|
}
|
|
|
|
static void testClassifyTooNew() {
|
|
// Boundary: one past this build's format version refuses.
|
|
CHECK(classifyPackageVersion(kPackageFormatVersion + 1, kPackageFormatVersion + 1) ==
|
|
PackageReadability::TooNew);
|
|
CHECK(classifyPackageVersion(99, 42) == PackageReadability::TooNew);
|
|
}
|
|
|
|
static void testClassifyMalformed() {
|
|
// Zero versions: no honest writer emits them (the ladder starts at 1).
|
|
CHECK(classifyPackageVersion(0, 0) == PackageReadability::Malformed);
|
|
CHECK(classifyPackageVersion(1, 0) == PackageReadability::Malformed);
|
|
CHECK(classifyPackageVersion(0, 1) == PackageReadability::Malformed);
|
|
// A writer cannot require a reader newer than what it wrote.
|
|
CHECK(classifyPackageVersion(1, 2) == PackageReadability::Malformed);
|
|
// Incoherence outranks TooNew: even with both above this build, minReader >
|
|
// formatVersion is Malformed, not a refusal message.
|
|
CHECK(classifyPackageVersion(5, 9) == PackageReadability::Malformed);
|
|
}
|
|
|
|
// --- isValidEntryName --------------------------------------------------------
|
|
|
|
static void testEntryNameAccepts() {
|
|
CHECK(isValidEntryName("kick.wav"));
|
|
CHECK(isValidEntryName("Snare 03 (wet).wav"));
|
|
CHECK(isValidEntryName("no-extension"));
|
|
CHECK(isValidEntryName(".hidden")); // a leading dot is a bare name
|
|
CHECK(isValidEntryName("a.b.c.wav")); // single dots are fine
|
|
CHECK(isValidEntryName(std::string(kMaxEntryNameBytes, 'x'))); // at the cap
|
|
// Legal names containing a ".." substring that is not the whole name: a
|
|
// name can only ever be one path component (separators are banned), so
|
|
// ".." as a component is the only expressible traversal.
|
|
CHECK(isValidEntryName("take..final.wav"));
|
|
CHECK(isValidEntryName("loop...wav"));
|
|
CHECK(isValidEntryName("a..b"));
|
|
}
|
|
|
|
static void testEntryNameRejectsSeparatorsAndDots() {
|
|
CHECK(!isValidEntryName(""));
|
|
CHECK(!isValidEntryName("."));
|
|
CHECK(!isValidEntryName(".."));
|
|
CHECK(!isValidEntryName("..\\evil.wav"));
|
|
CHECK(!isValidEntryName("../evil.wav"));
|
|
CHECK(!isValidEntryName("dir/inner.wav"));
|
|
CHECK(!isValidEntryName("dir\\inner.wav"));
|
|
CHECK(!isValidEntryName("/rooted.wav"));
|
|
CHECK(!isValidEntryName("\\rooted.wav"));
|
|
// Embedded NUL: every plausible filesystem call (ofstream, fopen,
|
|
// CreateFileW off .c_str()) truncates at it, so two names differing only
|
|
// after the NUL would collide on one file.
|
|
CHECK(!isValidEntryName(std::string("a\0b.wav", 7)));
|
|
// Other control bytes (newline here) are equally hostile to logs/UI.
|
|
CHECK(!isValidEntryName("a\nb.wav"));
|
|
}
|
|
|
|
static void testEntryNameRejectsAbsolutePrefixes() {
|
|
CHECK(!isValidEntryName("C:\\abs.wav"));
|
|
CHECK(!isValidEntryName("C:/abs.wav"));
|
|
CHECK(!isValidEntryName("c:relative-to-drive.wav")); // ':' bans drive forms
|
|
CHECK(!isValidEntryName("\\\\server\\share.wav")); // UNC
|
|
CHECK(!isValidEntryName(std::string(kMaxEntryNameBytes + 1, 'x'))); // over cap
|
|
}
|
|
|
|
static void testEntryNameRejectsWindowsHostileNames() {
|
|
// Reserved characters.
|
|
CHECK(!isValidEntryName("a*b.wav"));
|
|
CHECK(!isValidEntryName("a?b.wav"));
|
|
CHECK(!isValidEntryName("a|b.wav"));
|
|
CHECK(!isValidEntryName("a<b>.wav"));
|
|
CHECK(!isValidEntryName("\"q\".wav"));
|
|
// Trailing dot or space (silently stripped at creation on Windows).
|
|
CHECK(!isValidEntryName("trailing "));
|
|
CHECK(!isValidEntryName("trailing."));
|
|
CHECK(!isValidEntryName(" "));
|
|
CHECK(!isValidEntryName(" "));
|
|
// DOS device names, case-insensitive, with and without an extension.
|
|
CHECK(!isValidEntryName("NUL"));
|
|
CHECK(!isValidEntryName("CON"));
|
|
CHECK(!isValidEntryName("con.wav"));
|
|
CHECK(!isValidEntryName("PRN"));
|
|
CHECK(!isValidEntryName("AUX"));
|
|
CHECK(!isValidEntryName("COM1"));
|
|
CHECK(!isValidEntryName("com1.txt"));
|
|
CHECK(!isValidEntryName("LPT1"));
|
|
// Not a device name: a real filename that merely starts with one.
|
|
CHECK(isValidEntryName("console.wav"));
|
|
}
|
|
|
|
int main() {
|
|
testClassifyReadable();
|
|
testClassifyTooNew();
|
|
testClassifyMalformed();
|
|
testEntryNameAccepts();
|
|
testEntryNameRejectsSeparatorsAndDots();
|
|
testEntryNameRejectsAbsolutePrefixes();
|
|
testEntryNameRejectsWindowsHostileNames();
|
|
|
|
if (g_fail == 0) {
|
|
std::printf("package_format_tests: all passed\n");
|
|
return 0;
|
|
}
|
|
std::printf("package_format_tests: %d failure(s)\n", g_fail);
|
|
return 1;
|
|
}
|