Close the RSBK name-collision class: ASCII case folding, UTF-8 well-formedness, nested-path traversal
All three are format-locked and validated on encode and decode. Repeated known keys now reject at the root and inside an entry rather than last-wins.
This commit is contained in:
@@ -1,7 +1,7 @@
|
||||
// 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.
|
||||
// branch) and the three naming rules: the entry-name rule, the ASCII-folding
|
||||
// name equivalence, and the nested-path traversal guard.
|
||||
|
||||
#include "../src/core/package/package_format.h"
|
||||
|
||||
@@ -111,8 +111,110 @@ static void testEntryNameRejectsWindowsHostileNames() {
|
||||
CHECK(!isValidEntryName("COM1"));
|
||||
CHECK(!isValidEntryName("com1.txt"));
|
||||
CHECK(!isValidEntryName("LPT1"));
|
||||
// Not a device name: a real filename that merely starts with one.
|
||||
// The superscript device forms (COM¹ COM² COM³ LPT¹ LPT² LPT³ in UTF-8):
|
||||
// Windows reads those as digits in a device name, so "COM².wav" is COM2.
|
||||
CHECK(!isValidEntryName("COM\xC2\xB9.wav"));
|
||||
CHECK(!isValidEntryName("com\xC2\xB2"));
|
||||
CHECK(!isValidEntryName("COM\xC2\xB3.wav"));
|
||||
CHECK(!isValidEntryName("LPT\xC2\xB9"));
|
||||
CHECK(!isValidEntryName("lpt\xC2\xB2.txt"));
|
||||
CHECK(!isValidEntryName("LPT\xC2\xB3.wav"));
|
||||
// Not a device name: a real filename that merely starts with one, and the
|
||||
// zero forms, which Windows does not reserve.
|
||||
CHECK(isValidEntryName("console.wav"));
|
||||
CHECK(isValidEntryName("COM0.wav"));
|
||||
CHECK(isValidEntryName("LPT0"));
|
||||
// Nor does a superscript past 3 name a device.
|
||||
CHECK(isValidEntryName("COM\xE2\x81\xB4.wav")); // U+2074 SUPERSCRIPT FOUR
|
||||
}
|
||||
|
||||
// --- isValidEntryName: UTF-8 well-formedness ---------------------------------
|
||||
|
||||
static void testEntryNameAcceptsWellFormedUtf8() {
|
||||
CHECK(isValidEntryName("caf\xC3\xA9.wav")); // 2-byte: é
|
||||
CHECK(isValidEntryName("\xE2\x99\xAA.wav")); // 3-byte: ♪
|
||||
CHECK(isValidEntryName("\xF0\x9F\x8E\xB5.wav")); // 4-byte: 🎵
|
||||
CHECK(isValidEntryName("\xEF\xBB\xBF.wav")); // U+FEFF, ugly but well-formed
|
||||
CHECK(isValidEntryName("\xF4\x8F\xBF\xBF.wav")); // U+10FFFF, the last code point
|
||||
}
|
||||
|
||||
static void testEntryNameRejectsIllFormedUtf8() {
|
||||
// Two names differing ONLY in their invalid bytes: a host converting to
|
||||
// UTF-16 substitutes U+FFFD for both by default, collapsing them onto one
|
||||
// file — the duplicate-name collision the manifest cannot otherwise see.
|
||||
CHECK(!isValidEntryName("a\x80.wav")); // stray continuation byte
|
||||
CHECK(!isValidEntryName("a\x81.wav"));
|
||||
// Structural: truncated sequences (a lead byte the name ends inside).
|
||||
CHECK(!isValidEntryName("a\xC3"));
|
||||
CHECK(!isValidEntryName("a\xE2\x99"));
|
||||
CHECK(!isValidEntryName("a\xF0\x9F\x8E"));
|
||||
// A lead byte followed by a non-continuation.
|
||||
CHECK(!isValidEntryName("a\xC3\x41.wav"));
|
||||
// Overlong encodings: an alternate spelling of an ASCII byte we ban.
|
||||
CHECK(!isValidEntryName("a\xC0\xAF.wav")); // overlong '/'
|
||||
CHECK(!isValidEntryName("a\xC0\x80.wav")); // overlong NUL
|
||||
CHECK(!isValidEntryName("a\xE0\x80\xAF.wav")); // overlong '/', 3-byte
|
||||
CHECK(!isValidEntryName("a\xF0\x80\x80\xAF.wav")); // overlong '/', 4-byte
|
||||
// Surrogate halves: no code point, and unrepresentable in UTF-16.
|
||||
CHECK(!isValidEntryName("a\xED\xA0\x80.wav")); // U+D800
|
||||
CHECK(!isValidEntryName("a\xED\xBF\xBF.wav")); // U+DFFF
|
||||
// Past U+10FFFF, and the 5/6-byte leads that never encode anything.
|
||||
CHECK(!isValidEntryName("a\xF4\x90\x80\x80.wav")); // U+110000
|
||||
CHECK(!isValidEntryName("a\xF5\x80\x80\x80.wav"));
|
||||
CHECK(!isValidEntryName("a\xFC\x80\x80\x80\x80\x80.wav"));
|
||||
CHECK(!isValidEntryName("a\xFF.wav"));
|
||||
}
|
||||
|
||||
// --- sameEntryName -----------------------------------------------------------
|
||||
|
||||
static void testSameEntryNameFoldsAsciiCase() {
|
||||
// A bank authored on a case-sensitive filesystem produces this pair
|
||||
// honestly; Windows and default APFS would extract both onto one file.
|
||||
CHECK(sameEntryName("Kick.wav", "kick.wav"));
|
||||
CHECK(sameEntryName("KICK.WAV", "kick.wav"));
|
||||
CHECK(sameEntryName("kick.wav", "kick.wav"));
|
||||
CHECK(!sameEntryName("kick.wav", "snare.wav"));
|
||||
CHECK(!sameEntryName("kick.wav", "kick.wave")); // length alone decides
|
||||
CHECK(!sameEntryName("", "a"));
|
||||
CHECK(sameEntryName("", ""));
|
||||
// ASCII only: "é" vs "É" are two names here (the NFC/NFD limitation this
|
||||
// shares — see this directory's CLAUDE.md).
|
||||
CHECK(!sameEntryName("caf\xC3\xA9.wav", "caf\xC3\x89.wav"));
|
||||
// Only the letters fold — the bytes flanking the ASCII range must not.
|
||||
CHECK(!sameEntryName("a[b", "a{b")); // 0x5B vs 0x7B, 'Z'+1 and 'z'+1
|
||||
CHECK(!sameEntryName("a@b", "a`b")); // 0x40 vs 0x60, 'A'-1 and 'a'-1
|
||||
}
|
||||
|
||||
// --- isValidNestedSamplePath -------------------------------------------------
|
||||
|
||||
static void testNestedSamplePathAcceptsRelative() {
|
||||
CHECK(isValidNestedSamplePath("a.wav"));
|
||||
CHECK(isValidNestedSamplePath("reasampler_bank/kick.wav"));
|
||||
CHECK(isValidNestedSamplePath("reasampler_bank\\kick.wav"));
|
||||
CHECK(isValidNestedSamplePath("deep/dir/tree/a.wav"));
|
||||
// A ".." that is not a whole component is an ordinary name.
|
||||
CHECK(isValidNestedSamplePath("take..final/a.wav"));
|
||||
CHECK(isValidNestedSamplePath("bank/..hidden"));
|
||||
CHECK(isValidNestedSamplePath("a..b"));
|
||||
}
|
||||
|
||||
static void testNestedSamplePathRejectsTraversalAndAbsolute() {
|
||||
// BankModel::add catches only the absolute forms, so traversal reaches the
|
||||
// format unless this rule stops it.
|
||||
CHECK(!isValidNestedSamplePath(".."));
|
||||
CHECK(!isValidNestedSamplePath("../evil.wav"));
|
||||
CHECK(!isValidNestedSamplePath("..\\evil.wav"));
|
||||
CHECK(!isValidNestedSamplePath("bank/../../evil.wav"));
|
||||
CHECK(!isValidNestedSamplePath("bank\\..\\evil.wav"));
|
||||
CHECK(!isValidNestedSamplePath("bank/.."));
|
||||
CHECK(!isValidNestedSamplePath("bank/../"));
|
||||
// Everything util::isAbsolutePath already catches.
|
||||
CHECK(!isValidNestedSamplePath("/rooted.wav"));
|
||||
CHECK(!isValidNestedSamplePath("\\rooted.wav"));
|
||||
CHECK(!isValidNestedSamplePath("C:/abs.wav"));
|
||||
CHECK(!isValidNestedSamplePath("C:\\abs.wav"));
|
||||
CHECK(!isValidNestedSamplePath("c:relative-to-drive.wav"));
|
||||
CHECK(!isValidNestedSamplePath("\\\\server\\share.wav"));
|
||||
}
|
||||
|
||||
int main() {
|
||||
@@ -123,6 +225,11 @@ int main() {
|
||||
testEntryNameRejectsSeparatorsAndDots();
|
||||
testEntryNameRejectsAbsolutePrefixes();
|
||||
testEntryNameRejectsWindowsHostileNames();
|
||||
testEntryNameAcceptsWellFormedUtf8();
|
||||
testEntryNameRejectsIllFormedUtf8();
|
||||
testSameEntryNameFoldsAsciiCase();
|
||||
testNestedSamplePathAcceptsRelative();
|
||||
testNestedSamplePathRejectsTraversalAndAbsolute();
|
||||
|
||||
if (g_fail == 0) {
|
||||
std::printf("package_format_tests: all passed\n");
|
||||
|
||||
Reference in New Issue
Block a user