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,6 +1,8 @@
|
||||
#include "core/package/package_format.h"
|
||||
|
||||
#include <cctype>
|
||||
#include <cstddef>
|
||||
|
||||
#include "core/util/relative_path.h"
|
||||
|
||||
namespace reasampler::package {
|
||||
|
||||
@@ -14,21 +16,64 @@ PackageReadability classifyPackageVersion(std::uint32_t formatVersion,
|
||||
|
||||
namespace {
|
||||
|
||||
// Hand-rolled rather than std::tolower: that fold is locale-dependent, so two
|
||||
// machines reading the same package could disagree on which names collide.
|
||||
char lowerAscii(unsigned char c) {
|
||||
return (c >= 'A' && c <= 'Z') ? static_cast<char>(c - 'A' + 'a') : static_cast<char>(c);
|
||||
}
|
||||
|
||||
// Windows device names claim the whole entry regardless of extension
|
||||
// (CON, CON.wav, con.WAV are all the same reserved device) — checked against
|
||||
// the portion before the first dot only.
|
||||
// the portion before the first dot only. The trailing three pairs are the UTF-8
|
||||
// spellings of COM¹/COM²/COM³/LPT¹/LPT²/LPT³: Windows reads those ISO 8859-1
|
||||
// superscripts as digits in a device name. COM0/LPT0 are NOT reserved.
|
||||
bool isDosDeviceName(const std::string& name) {
|
||||
std::string base = name.substr(0, name.find('.'));
|
||||
for (char& c : base) c = static_cast<char>(std::toupper(static_cast<unsigned char>(c)));
|
||||
for (char& c : base) c = lowerAscii(static_cast<unsigned char>(c));
|
||||
static const std::string kReserved[] = {
|
||||
"CON", "PRN", "AUX", "NUL",
|
||||
"COM1", "COM2", "COM3", "COM4", "COM5", "COM6", "COM7", "COM8", "COM9",
|
||||
"LPT1", "LPT2", "LPT3", "LPT4", "LPT5", "LPT6", "LPT7", "LPT8", "LPT9",
|
||||
"con", "prn", "aux", "nul",
|
||||
"com1", "com2", "com3", "com4", "com5", "com6", "com7", "com8", "com9",
|
||||
"lpt1", "lpt2", "lpt3", "lpt4", "lpt5", "lpt6", "lpt7", "lpt8", "lpt9",
|
||||
"com\xC2\xB9", "com\xC2\xB2", "com\xC2\xB3",
|
||||
"lpt\xC2\xB9", "lpt\xC2\xB2", "lpt\xC2\xB3",
|
||||
};
|
||||
for (const auto& r : kReserved) if (base == r) return true;
|
||||
return false;
|
||||
}
|
||||
|
||||
// Table-free UTF-8 well-formedness. Overlong encodings, surrogate halves and
|
||||
// code points past U+10FFFF are rejected as hard as a structural length error:
|
||||
// the UTF-8 -> UTF-16 conversion a host must perform maps an ill-formed
|
||||
// sequence to U+FFFD unless it opts into failing, so two names differing only
|
||||
// in invalid bytes would otherwise collapse onto one destination file.
|
||||
bool isWellFormedUtf8(const std::string& s) {
|
||||
const auto* p = reinterpret_cast<const unsigned char*>(s.data());
|
||||
const std::size_t n = s.size();
|
||||
for (std::size_t i = 0; i < n;) {
|
||||
const unsigned char c = p[i];
|
||||
std::size_t extra = 0;
|
||||
std::uint32_t cp = 0;
|
||||
if (c < 0x80) { ++i; continue; }
|
||||
else if ((c & 0xE0) == 0xC0) { extra = 1; cp = c & 0x1Fu; }
|
||||
else if ((c & 0xF0) == 0xE0) { extra = 2; cp = c & 0x0Fu; }
|
||||
else if ((c & 0xF8) == 0xF0) { extra = 3; cp = c & 0x07u; }
|
||||
else return false; // a stray continuation byte, or a 5/6-byte lead
|
||||
if (i + extra >= n) return false;
|
||||
for (std::size_t k = 1; k <= extra; ++k) {
|
||||
const unsigned char cont = p[i + k];
|
||||
if ((cont & 0xC0) != 0x80) return false;
|
||||
cp = (cp << 6) | (cont & 0x3Fu);
|
||||
}
|
||||
if (extra == 1 && cp < 0x80) return false;
|
||||
if (extra == 2 && cp < 0x800) return false;
|
||||
if (extra == 3 && cp < 0x10000) return false;
|
||||
if (cp > 0x10FFFF) return false;
|
||||
if (cp >= 0xD800 && cp <= 0xDFFF) return false;
|
||||
i += extra + 1;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
bool isValidEntryName(const std::string& name) {
|
||||
@@ -46,7 +91,29 @@ bool isValidEntryName(const std::string& name) {
|
||||
if (c == '*' || c == '?' || c == '|' || c == '<' || c == '>' || c == '"') return false;
|
||||
}
|
||||
if (isDosDeviceName(name)) return false;
|
||||
return isWellFormedUtf8(name);
|
||||
}
|
||||
|
||||
bool sameEntryName(const std::string& a, const std::string& b) {
|
||||
if (a.size() != b.size()) return false;
|
||||
for (std::size_t i = 0; i < a.size(); ++i)
|
||||
if (lowerAscii(static_cast<unsigned char>(a[i])) !=
|
||||
lowerAscii(static_cast<unsigned char>(b[i])))
|
||||
return false;
|
||||
return true;
|
||||
}
|
||||
|
||||
bool isValidNestedSamplePath(const std::string& path) {
|
||||
if (util::isAbsolutePath(path)) return false;
|
||||
// Component-wise, not a substring scan: "take..final/a.wav" is a legal
|
||||
// relative path, "bank/../evil.wav" is not.
|
||||
for (std::size_t start = 0;; ) {
|
||||
const std::size_t sep = path.find_first_of("/\\", start);
|
||||
const std::size_t end = (sep == std::string::npos) ? path.size() : sep;
|
||||
if (path.compare(start, end - start, "..") == 0) return false;
|
||||
if (sep == std::string::npos) return true;
|
||||
start = sep + 1;
|
||||
}
|
||||
}
|
||||
|
||||
} // namespace reasampler::package
|
||||
|
||||
Reference in New Issue
Block a user