diff --git a/src/base/utils/fs.cpp b/src/base/utils/fs.cpp index d0ac3bbc3..57d6e10c1 100644 --- a/src/base/utils/fs.cpp +++ b/src/base/utils/fs.cpp @@ -219,6 +219,60 @@ Path Utils::Fs::tempPath() return path; } +// Validates a file name, where "file" refers to both files and directories in Windows and Unix-like systems. +// Returns true if the name is valid, false if it contains empty/special names, exceeds platform-specific lengths, +// uses reserved names, or includes forbidden characters. +bool Utils::Fs::isValidName(const QString &name) +{ + // Reject empty names or special directory names (".", "..") + if (name.isEmpty() || (name == u"."_s) || (name == u".."_s)) + return false; + +#ifdef Q_OS_WIN + // Windows restricts file names to 255 characters and prohibits trailing dots + if ((name.length() > 255) || name.endsWith(u'.')) + return false; +#else + // Non-Windows systems limit file name lengths to 255 bytes in UTF-8 encoding + if (name.toUtf8().length() > 255) + return false; +#endif + +#ifdef Q_OS_WIN + // Windows reserves certain names for devices, which cannot be used as file names + const QSet reservedNames + { + u"CON"_s, u"PRN"_s, u"AUX"_s, u"NUL"_s, + u"COM1"_s, u"COM2"_s, u"COM3"_s, u"COM4"_s, + u"COM5"_s, u"COM6"_s, u"COM7"_s, u"COM8"_s, + u"COM9"_s, u"COM¹"_s, u"COM²"_s, u"COM³"_s, + u"LPT1"_s, u"LPT2"_s, u"LPT3"_s, u"LPT4"_s, + u"LPT5"_s, u"LPT6"_s, u"LPT7"_s, u"LPT8"_s, + u"LPT9"_s, u"LPT¹"_s, u"LPT²"_s, u"LPT³"_s + }; + const QString baseName = name.section(u'.', 0, 0).toUpper(); + if (reservedNames.contains(baseName)) + return false; +#endif + + // Check for control characters, delete character, and forward slash + for (const QChar &c : name) + { + const ushort unicode = c.unicode(); + if ((unicode < 32) || (unicode == 127) || (c == u'/')) + return false; +#ifdef Q_OS_WIN + // Windows forbids reserved characters in file names + if ((c == u'\\') || (c == u'<') || (c == u'>') || (c == u':') || (c == u'"') || + (c == u'|') || (c == u'?') || (c == u'*')) + return false; +#endif + } + + // If none of the invalid conditions are met, the name is valid + return true; +} + bool Utils::Fs::isRegularFile(const Path &path) { std::error_code ec; diff --git a/src/base/utils/fs.h b/src/base/utils/fs.h index eced8b072..a19fc524c 100644 --- a/src/base/utils/fs.h +++ b/src/base/utils/fs.h @@ -46,6 +46,7 @@ namespace Utils::Fs qint64 computePathSize(const Path &path); qint64 freeDiskSpaceOnPath(const Path &path); + bool isValidName(const QString &name); bool isRegularFile(const Path &path); bool isDir(const Path &path); bool isReadable(const Path &path); diff --git a/src/gui/torrentcontentmodel.cpp b/src/gui/torrentcontentmodel.cpp index 2df7b8176..73aa8f766 100644 --- a/src/gui/torrentcontentmodel.cpp +++ b/src/gui/torrentcontentmodel.cpp @@ -299,9 +299,16 @@ bool TorrentContentModel::setData(const QModelIndex &index, const QVariant &valu case TorrentContentModelItem::COL_NAME: { const QString currentName = item->name(); - const QString newName = value.toString(); + const QString newName = value.toString().trimmed(); + if (currentName != newName) { + if (!Utils::Fs::isValidName(newName)) + { + emit renameFailed(tr("The name is invalid: \"%1\"").arg(newName)); + return false; + } + try { const Path parentPath = getItemPath(index.parent()); diff --git a/src/webui/api/torrentscontroller.cpp b/src/webui/api/torrentscontroller.cpp index 9eeb183bc..10b7033a4 100644 --- a/src/webui/api/torrentscontroller.cpp +++ b/src/webui/api/torrentscontroller.cpp @@ -34,6 +34,7 @@ #include #include +#include #include #include #include @@ -1990,6 +1991,10 @@ void TorrentsController::renameFileAction() { requireParams({u"hash"_s, u"oldPath"_s, u"newPath"_s}); + const QString newFileName = QFileInfo(params()[u"newPath"_s]).fileName(); + if (!Utils::Fs::isValidName(newFileName)) + throw APIError(APIErrorType::Conflict, tr("File name has invalid characters")); + const auto id = BitTorrent::TorrentID::fromString(params()[u"hash"_s]); BitTorrent::Torrent *const torrent = BitTorrent::Session::instance()->getTorrent(id); if (!torrent) @@ -2014,6 +2019,10 @@ void TorrentsController::renameFolderAction() { requireParams({u"hash"_s, u"oldPath"_s, u"newPath"_s}); + const QString newFolderName = QFileInfo(params()[u"newPath"_s]).fileName(); + if (!Utils::Fs::isValidName(newFolderName)) + throw APIError(APIErrorType::Conflict, tr("Folder name has invalid characters")); + const auto id = BitTorrent::TorrentID::fromString(params()[u"hash"_s]); BitTorrent::Torrent *const torrent = BitTorrent::Session::instance()->getTorrent(id); if (!torrent)