#include "core/package/package_format.h" #include #include "core/util/relative_path.h" namespace reasampler::package { PackageReadability classifyPackageVersion(std::uint32_t formatVersion, std::uint32_t minReaderVersion) { if (formatVersion == 0 || minReaderVersion == 0) return PackageReadability::Malformed; if (minReaderVersion > formatVersion) return PackageReadability::Malformed; if (minReaderVersion > kPackageFormatVersion) return PackageReadability::TooNew; return PackageReadability::Readable; } 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(c - 'A' + 'a') : static_cast(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 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 = lowerAscii(static_cast(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", "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(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) { if (name.empty() || name.size() > kMaxEntryNameBytes) return false; if (name == "." || name == "..") return false; // fopen/CreateFileW both silently strip a trailing dot or space at // creation, so "a.wav " and "a.wav" would collide on one file. if (name.back() == '.' || name.back() == ' ') return false; for (unsigned char c : name) { // NUL and other control bytes truncate at the first filesystem call // (std::ofstream, fopen, CreateFileW off .c_str()) — two names that // differ only after the NUL land on the same file. if (c < 0x20 || c == 0x7F) return false; if (c == '/' || c == '\\' || c == ':') return false; 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(a[i])) != lowerAscii(static_cast(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