commit d58bd4d89a5cbb2f2aac7a8292473d8ed980d60a
parent 9fd2574b8db46543153a6d45c85bc76e203f46e7
Author: Jonathan Kew <jkew@mozilla.com>
Date: Fri, 12 Dec 2025 23:10:13 +0000
Bug 1959738 - part 2 - Rewrite SanitizeFileName for better readability and correctness. r=Gijs
Differential Revision: https://phabricator.services.mozilla.com/D274484
Diffstat:
5 files changed, 240 insertions(+), 240 deletions(-)
diff --git a/uriloader/exthandler/nsExternalHelperAppService.cpp b/uriloader/exthandler/nsExternalHelperAppService.cpp
@@ -17,6 +17,7 @@
#include "mozilla/dom/Element.h"
#include "mozilla/dom/WindowGlobalParent.h"
#include "mozilla/RandomNum.h"
+#include "mozilla/ScopeExit.h"
#include "mozilla/StaticPrefs_dom.h"
#include "mozilla/StaticPrefs_security.h"
#include "mozilla/StaticPtr.h"
@@ -801,7 +802,7 @@ NS_IMETHODIMP nsExternalHelperAppService::CreateListener(
}
nsAutoString extension;
- int32_t dotidx = fileName.RFind(u".");
+ int32_t dotidx = fileName.RFindChar(u'.');
if (dotidx != -1) {
extension = Substring(fileName, dotidx + 1);
}
@@ -3544,255 +3545,250 @@ void nsExternalHelperAppService::CheckDefaultFileName(nsAString& aFileName,
void nsExternalHelperAppService::SanitizeFileName(nsAString& aFileName,
uint32_t aFlags) {
- nsAutoString fileName(aFileName);
-
- // Replace known invalid characters.
- fileName.ReplaceChar(u"" KNOWN_PATH_SEPARATORS FILE_ILLEGAL_CHARACTERS "%",
- u'_');
- fileName.StripChar(char16_t(0));
-
- const char16_t *startStr, *endStr;
- fileName.BeginReading(startStr);
- fileName.EndReading(endStr);
-
- // True if multiple consecutive whitespace characters should
- // be replaced by single space ' '.
- bool collapseWhitespace = !(aFlags & VALIDATE_DONT_COLLAPSE_WHITESPACE);
-
- // The maximum filename length differs based on the platform:
- // Windows (FAT/NTFS) stores filenames as a maximum of 255 UTF-16 code units.
- // Mac (APFS) stores filenames with a maximum 255 of UTF-8 code units.
- // Linux (ext3/ext4...) stores filenames with a maximum 255 bytes.
- // So here we just use the maximum of 255 bytes.
- // 0 means don't truncate at a maximum size.
- uint32_t maxBytes =
- (aFlags & VALIDATE_DONT_TRUNCATE) ? 0 : kDefaultMaxFileNameLength;
-
- const auto downloadSuffix = u".download"_ns;
- bool appendDownloadSuffix = false;
-
- if (!(aFlags & VALIDATE_ALLOW_INVALID_FILENAMES)) {
- // If the extension is one these types, we append .download, as these
- // types of files can have significance on Windows or Linux.
- // This happens for any file, not just those with the shortcut mime type.
- if (StringEndsWith(fileName, u".lnk"_ns,
- nsCaseInsensitiveStringComparator) ||
- StringEndsWith(fileName, u".local"_ns,
- nsCaseInsensitiveStringComparator) ||
- StringEndsWith(fileName, u".url"_ns,
- nsCaseInsensitiveStringComparator) ||
- StringEndsWith(fileName, u".scf"_ns,
- nsCaseInsensitiveStringComparator) ||
- StringEndsWith(fileName, u".desktop"_ns,
- nsCaseInsensitiveStringComparator)) {
- appendDownloadSuffix = true;
-
- // To ensure this will not make the final name too long, adjust maxBytes.
- if (maxBytes) {
- maxBytes -= downloadSuffix.Length();
- }
- }
- }
-
- // True if the last character added was whitespace.
+ // True if multiple consecutive whitespace characters should be replaced by
+ // single space ' '.
+ const bool collapseWhitespace = !(aFlags & VALIDATE_DONT_COLLAPSE_WHITESPACE);
+
+ // Scan the filename in-place, stripping control and separator characters;
+ // collapse runs of whitespace if required, and convert formatting chars
+ // (except ZWNBSP, which is treated as whitespace) to underscore.
+ char16_t* dest = aFileName.BeginWriting();
+ // Known-invalid chars that will be replaced by '_'.
+ const auto kInvalidChars =
+ u"" KNOWN_PATH_SEPARATORS FILE_ILLEGAL_CHARACTERS "%"_ns;
+ // True if the last character seen was whitespace.
bool lastWasWhitespace = false;
+ // Accumulator for all bits seen in characters; this gives us a trivial way
+ // to confirm if the name is pure ASCII.
+ char32_t allBits = 0;
+ const char16_t* end = aFileName.EndReading();
+ for (const char16_t* cp = aFileName.BeginReading(); cp < end;) {
+ // Replace known-invalid characters with underscore.
+ if (kInvalidChars.Contains(*cp)) {
+ *dest++ = u'_';
+ cp++;
+ lastWasWhitespace = false;
+ continue;
+ }
- // Length of the filename that fits into the maximum size excluding the
- // extension and period.
- int32_t longFileNameEnd = -1;
-
- // Index of the last character added that was not a character that can be
- // trimmed off of the end of the string. Trimmable characters are whitespace,
- // periods and the vowel separator u'\u180e'. If all the characters after this
- // point are trimmable characters, truncate the string to this point after
- // iterating over the filename.
- int32_t lastNonTrimmable = -1;
-
- // The number of bytes that the string would occupy if encoded in UTF-8.
- uint32_t bytesLength = 0;
-
- // The length of the extension in bytes.
- uint32_t extensionBytesLength = 0;
-
- // Given the minimal benefits, the "_files" suffix is intentionally not
- // localized (see also contentAreaUtils.js). Localizing it introduces
- // complexity in handling OS filename length limits (e.g. bug 1959738)
- // and risks breaking the folder-linking feature if an unsupported suffix
- // is used.
- constexpr uint32_t filesFolderLength = "_files"_ns.Length();
-
- // This algorithm iterates over each character in the string and appends it
- // or a replacement character if needed to outFileName.
- nsAutoString outFileName;
- while (startStr < endStr) {
+ // Remember where this character begins.
+ const char16_t* charStart = cp;
+ // Get the full character code, and advance cp past it.
bool err = false;
- char32_t nextChar = UTF16CharEnumerator::NextChar(&startStr, endStr, &err);
- if (err) {
- break;
+ char32_t nextChar = UTF16CharEnumerator::NextChar(&cp, end, &err);
+ allBits |= nextChar;
+ if (NS_WARN_IF(err)) {
+ // Invalid (unpaired) surrogate: replace with REPLACEMENT CHARACTER,
+ // and continue processing the remainder of the name.
+ MOZ_ASSERT(nextChar == u'\uFFFD');
+ *dest++ = nextChar;
+ lastWasWhitespace = false;
+ continue;
}
- // nulls are already stripped out above.
- MOZ_ASSERT(nextChar != char16_t(0));
-
+ // Skip control characters and line/paragraph separators.
auto unicodeCategory = unicode::GetGeneralCategory(nextChar);
if (unicodeCategory == HB_UNICODE_GENERAL_CATEGORY_CONTROL ||
unicodeCategory == HB_UNICODE_GENERAL_CATEGORY_LINE_SEPARATOR ||
unicodeCategory == HB_UNICODE_GENERAL_CATEGORY_PARAGRAPH_SEPARATOR) {
- // Skip over any control characters and separators.
continue;
}
+ // Convert whitespace to ASCII spaces, and handle whitespace collapsing.
if (unicodeCategory == HB_UNICODE_GENERAL_CATEGORY_SPACE_SEPARATOR ||
nextChar == u'\ufeff') {
- // Trim out any whitespace characters at the beginning of the filename,
- // and only add whitespace in the middle of the filename if the last
- // character was not whitespace or if we are not collapsing whitespace.
- if (!outFileName.IsEmpty() &&
- (!lastWasWhitespace || !collapseWhitespace)) {
- // Allow the ideographic space if it is present, otherwise replace with
- // ' '.
- if (nextChar != u'\u3000') {
- nextChar = ' ';
- }
- lastWasWhitespace = true;
- } else {
- lastWasWhitespace = true;
+ if (dest == aFileName.BeginWriting() ||
+ (collapseWhitespace && lastWasWhitespace)) {
continue;
}
+ lastWasWhitespace = true;
+ if (nextChar != u'\u3000') {
+ nextChar = u' ';
+ }
+ *dest++ = nextChar;
+ continue;
} else {
lastWasWhitespace = false;
- if (nextChar == '.' || nextChar == u'\u180e') {
- // Don't add any periods or vowel separators at the beginning of the
- // string. Note also that lastNonTrimmable is not adjusted in this
- // case, because periods and vowel separators are included in the
- // set of characters to trim at the end of the filename.
- if (outFileName.IsEmpty()) {
- continue;
- }
- } else {
- if (unicodeCategory == HB_UNICODE_GENERAL_CATEGORY_FORMAT) {
- // Replace formatting characters with an underscore.
- nextChar = '_';
- }
-
- // Don't truncate surrogate pairs in the middle.
- lastNonTrimmable =
- int32_t(outFileName.Length()) +
- (NS_IS_HIGH_SURROGATE(H_SURROGATE(nextChar)) ? 2 : 1);
- }
}
- if (maxBytes) {
- // UTF16CharEnumerator already converts surrogate pairs, so we can use
- // a simple computation of byte length here.
- uint32_t charBytesLength = nextChar < 0x80 ? 1
- : nextChar < 0x800 ? 2
- : nextChar < 0x10000 ? 3
- : 4;
- bytesLength += charBytesLength;
- if (bytesLength > maxBytes) {
- if (longFileNameEnd == -1) {
- longFileNameEnd = int32_t(outFileName.Length());
- }
- }
-
- // If we encounter a period, it could be the start of an extension, so
- // start counting the number of bytes in the extension. If another period
- // is found, start again since we want to use the last extension found.
- if (nextChar == u'.') {
- extensionBytesLength = 1; // 1 byte for the period.
- } else if (extensionBytesLength) {
- extensionBytesLength += charBytesLength;
+ if ((nextChar == u'.' || nextChar == u'\u180e')) {
+ // Trim dot and vowel separator at beginning.
+ if (dest == aFileName.BeginWriting()) {
+ continue;
}
+ } else if (unicodeCategory == HB_UNICODE_GENERAL_CATEGORY_FORMAT) {
+ // Replace other formatting characters with underscore.
+ *dest++ = u'_';
+ continue;
}
- AppendUCS4ToUTF16(nextChar, outFileName);
- }
-
- // If the filename is longer than the maximum allowed filename size, or
- // if replacing the extension with the filesFolder name would make it so,
- // truncate it, but preserve the desired extension that is currently
- // on the filename.
- if ((bytesLength > std::min(maxBytes, maxBytes - filesFolderLength +
- extensionBytesLength)) &&
- !outFileName.IsEmpty()) {
- // Get the sanitized extension from the filename without the dot.
- nsAutoString extension;
- int32_t dotidx = outFileName.RFind(u".");
- if (dotidx != -1) {
- extension = Substring(outFileName, dotidx + 1);
- }
-
- // There are two ways in which the filename should be truncated:
- // - If the filename was too long, truncate the name at the length
- // of the filename.
- // This position is indicated by longFileNameEnd.
- // - lastNonTrimmable will indicate the last character that was not
- // whitespace, a period, or a vowel separator at the end of the
- // the string, so the string should be truncated there as well.
- // If both apply, use the earliest position.
- if (lastNonTrimmable >= 0) {
- // Subtract off the amount for the extension and the period.
- // Note that the extension length is in bytes but longFileNameEnd is in
- // characters, but if they don't match, it just means we crop off
- // more than is necessary. This is OK since it is better than cropping
- // off too little.
- longFileNameEnd -= std::max(extensionBytesLength, filesFolderLength);
- if (longFileNameEnd <= 0) {
- // This is extremely unlikely, but if the extension is larger than the
- // maximum size, just get rid of it. In this case, the extension
- // wouldn't have been an ordinary one we would want to preserve (such
- // as .html or .png) so just truncate off the file wherever the first
- // period appears.
- int32_t dotidx = outFileName.Find(u".");
- outFileName.Truncate(dotidx > 0 ? dotidx : 1);
- } else {
- outFileName.Truncate(std::min(longFileNameEnd, lastNonTrimmable));
+ // Copy the current character (potentially a surrogate pair) to dest.
+ while (charStart < cp) {
+ *dest++ = *charStart++;
+ }
+ }
- // Now that the filename has been truncated, re-append the extension
- // again.
- if (!extension.IsEmpty()) {
- if (outFileName.Last() != '.') {
- outFileName.AppendLiteral(".");
- }
+ // UTF-16 code units we will trim if they're left at the end of the name,
+ // or at the end of the base name after removing an extension.
+ auto trimIfTrailing = [](char16_t aCh) -> bool {
+ return aCh == u' ' || aCh == u'\u3000' || aCh == u'.' || aCh == u'\u180e';
+ };
- outFileName.Append(extension);
- }
- }
+ // Strip any trailing whitespace/period/vowel-separator.
+ while (dest > aFileName.BeginWriting()) {
+ char16_t ch = *(dest - 1);
+ if (trimIfTrailing(ch)) {
+ dest--;
+ } else {
+ break;
}
- } else if (lastNonTrimmable >= 0) {
- // Otherwise, the filename wasn't too long, so just trim off the
- // extra whitespace and periods at the end.
- outFileName.Truncate(lastNonTrimmable);
+ }
+
+ // Update the string length to account for trimmed/skipped characters.
+ aFileName.SetLength(dest - aFileName.BeginWriting());
+
+ // Get the sanitized extension from the filename (including its dot).
+ nsAutoString ext;
+ int32_t dotidx = aFileName.RFindChar(u'.');
+ if (dotidx != -1) {
+ ext = Substring(aFileName, dotidx);
+ aFileName.Truncate(dotidx);
}
if (!(aFlags & VALIDATE_ALLOW_DIRECTORY_NAMES)) {
- nsAutoString extension;
- int32_t dotidx = outFileName.RFind(u".");
- if (dotidx != -1) {
- extension = Substring(outFileName, dotidx + 1);
- extension.StripWhitespace();
- outFileName = Substring(outFileName, 0, dotidx + 1) + extension;
+ ext.StripWhitespace();
+ }
+
+ // Determine if we need to add a ".download" suffix.
+ nsAutoString downloadSuffix;
+ if (!(aFlags & VALIDATE_ALLOW_INVALID_FILENAMES)) {
+ // If the extension is one these types, we append .download, as these
+ // types of files can have significance on Windows or Linux.
+ // This happens for any file, not just those with the shortcut mime type.
+ if (nsContentUtils::EqualsIgnoreASCIICase(ext, u".lnk"_ns) ||
+ nsContentUtils::EqualsIgnoreASCIICase(ext, u".local"_ns) ||
+ nsContentUtils::EqualsIgnoreASCIICase(ext, u".url"_ns) ||
+ nsContentUtils::EqualsIgnoreASCIICase(ext, u".scf"_ns) ||
+ nsContentUtils::EqualsIgnoreASCIICase(ext, u".desktop"_ns)) {
+ downloadSuffix = u".download"_ns;
}
}
+ // Filename finalization helper, applied once we have determined the
+ // (possibly-truncated) sanitized base-name and extension components.
+ auto finalizeName = MakeScopeExit([&]() {
#ifdef XP_WIN
- if (nsLocalFile::CheckForReservedFileName(outFileName)) {
- int32_t dotidx = outFileName.RFind(u".");
- if (dotidx == -1) {
- outFileName.Truncate();
- } else {
- outFileName = Substring(outFileName, dotidx);
+ // If aFileName is one of the Windows reserved names, replace it with our
+ // default ("Untitled") name.
+ if (nsLocalFile::CheckForReservedFileName(aFileName)) {
+ aFileName.Truncate();
+ CheckDefaultFileName(aFileName, aFlags);
}
- CheckDefaultFileName(outFileName, aFlags);
- }
#endif
+ aFileName.Append(ext);
+ aFileName.Append(downloadSuffix);
+#ifdef DEBUG
+ if (!(aFlags & VALIDATE_DONT_TRUNCATE)) {
+ // Verify that the final name, when converted to UTF-8, does not exceed
+ // the allowed length in bytes.
+ NS_ConvertUTF16toUTF8 utf8name(aFileName);
+ MOZ_ASSERT(utf8name.Length() <= kDefaultMaxFileNameLength);
+ // Verify that replacing the extension with "_files" will also not exceed
+ // the allowed length.
+ int32_t dotidx = utf8name.RFindChar('.');
+ if (dotidx >= 0) {
+ utf8name.Truncate(dotidx);
+ }
+ utf8name.Append("_files");
+ MOZ_ASSERT(utf8name.Length() <= kDefaultMaxFileNameLength);
+ }
+#endif
+ });
+
+ // Depending whether the name contained any non-ASCII chars, or any chars
+ // above U+07FF, we can determine a safe UTF-16 length for which detailed
+ // UTF-8 length checking is unnecessary.
+ uint32_t safeUtf16Length = (allBits & ~0x7f) == 0 ? kDefaultMaxFileNameLength
+ : (allBits & ~0x7ff) == 0
+ ? kDefaultMaxFileNameLength / 2
+ : kDefaultMaxFileNameLength / 3;
+ safeUtf16Length -= downloadSuffix.Length();
+
+ // Check if the name is short enough that it is guaranteed to fit (or
+ // truncation is disabled), without needing to count the exact utf-8 byte
+ // length and figure out the preferred truncation position.
+ const auto kFiles = u"_files"_ns;
+ if ((aFlags & VALIDATE_DONT_TRUNCATE) ||
+ (aFileName.Length() + ext.Length() <= safeUtf16Length &&
+ aFileName.Length() + kFiles.Length() <= safeUtf16Length)) {
+ return;
+ }
+
+ // The name might exceed the UTF-8 byte limit, so we need to actually compute
+ // its length in UTF-8 code units and truncate appropriately.
+
+ uint32_t byteLimit = kDefaultMaxFileNameLength;
+ // downloadSuffix is pure ASCII, so its UTF-8 length == UTF-16 length.
+ byteLimit -= downloadSuffix.Length();
+
+ // Helper to compute the UTF-8 code unit length of a UTF-16 string.
+ auto utf8Length = [](const nsAString& aString) -> size_t {
+ size_t result = 0;
+ const char16_t* end = aString.EndReading();
+ for (const char16_t* cp = aString.BeginReading(); cp < end;) {
+ bool err = false;
+ char32_t ch = UTF16CharEnumerator::NextChar(&cp, end, &err);
+ MOZ_ASSERT(!err, "unexpected lone surrogate");
+ result += ch < 0x80 ? 1 : ch < 0x800 ? 2 : ch < 0x10000 ? 3 : 4;
+ }
+ return result;
+ };
+
+ size_t fileNameBytes = utf8Length(aFileName);
+ size_t extBytes = utf8Length(ext);
+
+ if (extBytes >= byteLimit) {
+ // This is extremely unlikely, but if the extension is larger than the
+ // maximum size, just get rid of it. In this case, the extension
+ // wouldn't have been an ordinary one we would want to preserve (such
+ // as .html or .png) so just truncate off the file wherever the first
+ // period appears.
+ int32_t dotidx = aFileName.FindChar(u'.');
+ if (dotidx > 0) {
+ aFileName.Truncate(dotidx);
+ }
+ fileNameBytes = utf8Length(aFileName);
+ ext.Truncate();
+ extBytes = 0;
+ }
+
+ if (fileNameBytes + extBytes <= byteLimit &&
+ fileNameBytes + kFiles.Length() <= byteLimit) {
+ return;
+ }
+
+ // Convert to UTF-8 and truncate at the byte-length limit. This may leave
+ // an incomplete UTF-8 sequence at the end of the string.
+ NS_ConvertUTF16toUTF8 truncated(aFileName);
+ truncated.Truncate(byteLimit - std::max(extBytes, kFiles.Length()));
- if (appendDownloadSuffix) {
- outFileName.Append(downloadSuffix);
+ // Convert back to UTF-16, discarding any trailing incomplete character.
+ aFileName.Truncate();
+ const char* endUtf8 = truncated.EndReading();
+ for (const char* cp = truncated.BeginReading(); cp < endUtf8;) {
+ bool err = false;
+ char32_t ch = UTF8CharEnumerator::NextChar(&cp, endUtf8, &err);
+ if (err) {
+ // Discard a possible broken final character.
+ break;
+ }
+ AppendUCS4ToUTF16(ch, aFileName);
}
- aFileName = outFileName;
+ // Trim any trailing space/vowel-separator/dots at the truncation point.
+ while (!aFileName.IsEmpty() && trimIfTrailing(aFileName.Last())) {
+ aFileName.Truncate(aFileName.Length() - 1);
+ }
}
nsExternalHelperAppService::ModifyExtensionType
diff --git a/uriloader/exthandler/tests/mochitest/save_filenames.html b/uriloader/exthandler/tests/mochitest/save_filenames.html
@@ -37,15 +37,15 @@
<!-- long filename -->
<img id="i8" src="http://localhost:8000/save_filename.sjs?type=png&filename=averylongfilenameverylongfilenameverylongfilenameverylongfilenameverylongfilenameverylongfilenameverylongfilenameverylongfilenameverylongfilenameverylongfilenameverylongfilenameverylongfilenameverylongfilenameverylongfilenameverylongfilenameverylongfilename.png"
- data-filename="averylongfilenameverylongfilenameverylongfilenameverylongfilenameverylongfilenameverylongfilenameverylongfilenameverylongfilenameverylongfilenameverylongfilenameverylongfilenameverylongfilenameverylongfilenameverylongfilenameverylongfilenameverylongf.png">
+ data-filename="averylongfilenameverylongfilenameverylongfilenameverylongfilenameverylongfilenameverylongfilenameverylongfilenameverylongfilenameverylongfilenameverylongfilenameverylongfilenameverylongfilenameverylongfilenameverylongfilenameverylongfilenameverylon.png">
<!-- long filename with invalid extension -->
<img id="i9" src="http://localhost:8000/save_filename.sjs?type=png&filename=bverylongfilenameverylongfilenameverylongfilenameverylongfilenameverylongfilenameverylongfilenameverylongfilenameverylongfilenameverylongfilenameverylongfilenameverylongfilenameverylongfilenameverylongfilenameverylongfilenameverylongfilenameverylongfilename.exe"
- data-filename="bverylongfilenameverylongfilenameverylongfilenameverylongfilenameverylongfilenameverylongfilenameverylongfilenameverylongfilenameverylongfilenameverylongfilenameverylongfilenameverylongfilenameverylongfilenameverylongfilenameverylongfilenameverylongf.png">
+ data-filename="bverylongfilenameverylongfilenameverylongfilenameverylongfilenameverylongfilenameverylongfilenameverylongfilenameverylongfilenameverylongfilenameverylongfilenameverylongfilenameverylongfilenameverylongfilenameverylongfilenameverylongfilenameverylon.png">
<!-- long filename with invalid extension -->
<img id="i10" src="http://localhost:8000/save_filename.sjs?type=png&filename=cverylongfilenameverylongfilenameverylongfilenameverylongfilenameverylongfilenameverylongfilenameverylongfilenameverylongfilenameverylongfilenameverylongfilenameverylongfilenameverylongfilenameverylongfilenameverylongfilenameverylongfilenameverylongfilename.exe.jpg"
- data-filename="cverylongfilenameverylongfilenameverylongfilenameverylongfilenameverylongfilenameverylongfilenameverylongfilenameverylongfilenameverylongfilenameverylongfilenameverylongfilenameverylongfilenameverylongfilenameverylongfilenameverylongfilenameverylongf.png">
+ data-filename="cverylongfilenameverylongfilenameverylongfilenameverylongfilenameverylongfilenameverylongfilenameverylongfilenameverylongfilenameverylongfilenameverylongfilenameverylongfilenameverylongfilenameverylongfilenameverylongfilenameverylongfilenameverylon.png">
<!-- jpeg with jpg extension -->
<img id="i11" src="http://localhost:8000/save_filename.sjs?type=jpeg&filename=thejpg.jpg" data-filename="thejpg.jpg">
@@ -97,7 +97,7 @@
<!-- script with long filename -->
<script id="i25" src="http://localhost:8000/save_filename.sjs?type=js&filename=script123456789script123456789script123456789script123456789script123456789script123456789script123456789script123456789script123456789script123456789script123456789script123456789script123456789script123456789script123456789script123456789script123456789script123456789.js"
- data-filename="script123456789script123456789script123456789script123456789script123456789script123456789script123456789script123456789script123456789script123456789script123456789script123456789script123456789script123456789script123456789script123456789script12345.js"></script>
+ data-filename="script123456789script123456789script123456789script123456789script123456789script123456789script123456789script123456789script123456789script123456789script123456789script123456789script123456789script123456789script123456789script123456789script12.js"></script>
<!-- binary with exe extension -->
<object id="i26" data="http://localhost:8000/save_filename.sjs?type=binary&filename=download1.exe"
@@ -188,7 +188,7 @@
data-nodrag="true" data-filename="text3.gonk">
<!-- text with long filename -->
-<img id="i50" src="http://localhost:8000/text3.gonk?type=text&filename=text0123456789zztext0123456789zztext0123456789zztext0123456789zztext0123456789zztext0123456789zztext0123456789zztext0123456789zztext0123456789zztext0123456789zztext0123456789zztext0123456789zztext0123456789zztext0123456789zztext0123456789zztext0123456789text0123456789zztext0123456789zztext0123456789zztext01234567.exe.txt" data-nodrag="true" data-filename="text0123456789zztext0123456789zztext0123456789zztext0123456789zztext0123456789zztext0123456789zztext0123456789zztext0123456789zztext0123456789zztext0123456789zztext0123456789zztext0123456789zztext0123456789zztext0123456789zztext0123456789zztext012345.txt">
+<img id="i50" src="http://localhost:8000/text3.gonk?type=text&filename=text0123456789zztext0123456789zztext0123456789zztext0123456789zztext0123456789zztext0123456789zztext0123456789zztext0123456789zztext0123456789zztext0123456789zztext0123456789zztext0123456789zztext0123456789zztext0123456789zztext0123456789zztext0123456789text0123456789zztext0123456789zztext0123456789zztext01234567.exe.txt" data-nodrag="true" data-filename="text0123456789zztext0123456789zztext0123456789zztext0123456789zztext0123456789zztext0123456789zztext0123456789zztext0123456789zztext0123456789zztext0123456789zztext0123456789zztext0123456789zztext0123456789zztext0123456789zztext0123456789zztext0123.txt">
<!-- webp image -->
<img id="i51" src="http://localhost:8000/save_filename.sjs?type=webp&filename=webpimage.webp"
diff --git a/uriloader/exthandler/tests/unit/test_filename_sanitize.js b/uriloader/exthandler/tests/unit/test_filename_sanitize.js
@@ -70,8 +70,8 @@ add_task(async function validate_filename_method() {
Assert.equal(checkFilename("簡単簡単簡単", 0), "簡単簡単簡単.png");
Assert.equal(checkFilename(" happy\u061c\u2069.png", 0), "happy__.png");
Assert.equal(
- checkFilename("12345678".repeat(31) + "abcdefgh.png", 0),
- "12345678".repeat(31) + "ab.png"
+ checkFilename("12345678".repeat(30) + "abcdefghijk.png", 0),
+ "12345678".repeat(30) + "abcdefgh.png"
);
Assert.equal(
checkFilename("簡単".repeat(41) + ".png", 0),
@@ -79,16 +79,20 @@ add_task(async function validate_filename_method() {
);
Assert.equal(
checkFilename("a" + "簡単".repeat(42) + ".png", 0),
- "a" + "簡単".repeat(40) + "簡.png"
+ "a" + "簡単".repeat(41) + ".png"
);
Assert.equal(
- checkFilename("a" + "簡単".repeat(56) + ".png", 0),
- "a" + "簡単".repeat(40) + ".png"
+ checkFilename("ab" + "簡単".repeat(42) + ".png", 0),
+ "ab" + "簡単".repeat(41) + ".png"
+ );
+ Assert.equal(
+ checkFilename("abc" + "簡単".repeat(42) + ".png", 0),
+ "abc" + "簡単".repeat(40) + "簡.png"
);
Assert.equal(checkFilename("café.png", 0), "café.png");
Assert.equal(
- checkFilename("café".repeat(50) + ".png", 0),
- "café".repeat(50) + ".png"
+ checkFilename("café".repeat(49) + "caf.png", 0),
+ "café".repeat(49) + "caf.png"
);
Assert.equal(
checkFilename("café".repeat(51) + ".png", 0),
@@ -105,7 +109,7 @@ add_task(async function validate_filename_method() {
);
Assert.equal(
checkFilename("\u{100001}\u{100002}".repeat(32) + ".png", 0),
- "\u{100001}\u{100002}".repeat(30) + "\u{100001}.png"
+ "\u{100001}\u{100002}".repeat(31) + ".png"
);
Assert.equal(
@@ -114,11 +118,11 @@ add_task(async function validate_filename_method() {
);
Assert.equal(
checkFilename("noextensionfile".repeat(17), 0),
- "noextensionfile".repeat(16) + "noextensio.png"
+ "noextensionfile".repeat(16) + "noextens.png"
);
Assert.equal(
checkFilename("noextensionfile".repeat(16) + "noextensionfil.", 0),
- "noextensionfile".repeat(16) + "noextensio.png"
+ "noextensionfile".repeat(16) + "noextens.png"
);
Assert.equal(checkFilename(" first .png ", 0), "first .png");
@@ -150,17 +154,17 @@ add_task(async function validate_filename_method() {
);
Assert.equal(checkFilename("sixth.j pe/*g", 0), "sixth.png");
- let repeatStr = "12345678".repeat(31);
+ let repeatStr = "12345678".repeat(30);
Assert.equal(
checkFilename(
- repeatStr + "seventh.png",
+ repeatStr + "seventeenth.png",
mimeService.VALIDATE_DONT_TRUNCATE
),
- repeatStr + "seventh.png"
+ repeatStr + "seventeenth.png"
);
Assert.equal(
- checkFilename(repeatStr + "seventh.png", 0),
- repeatStr + "se.png"
+ checkFilename(repeatStr + "seventeenth.png", 0),
+ repeatStr + "seventee.png"
);
// no filename, so index is used by default.
@@ -190,13 +194,13 @@ add_task(async function validate_filename_method() {
ext = "lo#?n/ginvalid? ch\\ars";
Assert.equal(
checkFilename(repeatStr + ext, mimeService.VALIDATE_SANITIZE_ONLY),
- repeatStr + "lo#_n_"
+ repeatStr + "lo#_n_gi"
);
ext = ".long/invalid#? ch\\ars";
Assert.equal(
checkFilename(repeatStr + ext, mimeService.VALIDATE_SANITIZE_ONLY),
- repeatStr.substring(0, 232) + ".long_invalid#_ch_ars"
+ repeatStr.substring(0, 233) + ".long_invalid#_ch_ars"
);
Assert.equal(
@@ -288,7 +292,7 @@ add_task(async function validate_filename_method() {
"text/unknown",
mimeService.VALIDATE_SANITIZE_ONLY
),
- "라이브9.9만 시청컬처렐 다이제스티브 3박스 - 3박스 더 (뚱랑이 굿즈 증정) - 선물용 쇼핑백 2장컬처렐 다이제스티브 3박스 - 3박스 더 (뚱랑이 굿즈 .등-유산균-컬처렐-특가!",
+ "라이브9.9만 시청컬처렐 다이제스티브 3박스 - 3박스 더 (뚱랑이 굿즈 증정) - 선물용 쇼핑백 2장컬처렐 다이제스티브 3박스 - 3박스 더 (뚱랑이 굿즈 증정) - 선물용 쇼핑.등-유산균-컬처렐-특가!",
"very long filename with extension"
);
@@ -303,14 +307,14 @@ add_task(async function validate_filename_method() {
"another very long filename with long extension"
);
- // This filename is cropped at 254 bytes.
+ // This filename is cropped at <=248 bytes (so there would be room to append "_files").
Assert.equal(
mimeService.validateFileNameForSaving(
".라이브99만 시청컬처렐 다이제스티브 3박스 - 3박스 더 (뚱랑이 굿즈 증정) - 선물용 쇼핑백 2장컬처렐 다이제스티브 3박스 - 3박스 더 (뚱랑이 굿즈 증정) - 선물용 쇼핑백 2장24_102 000원 브랜드데이 앵콜 🎁 1등 유산균 컬처렐 특가!",
"text/unknown",
mimeService.VALIDATE_SANITIZE_ONLY
),
- "라이브99만 시청컬처렐 다이제스티브 3박스 - 3박스 더 (뚱랑이 굿즈 증정) - 선물용 쇼핑백 2장컬처렐 다이제스티브 3박스 - 3박스 더 (뚱랑이 굿즈 증정) - 선물용 쇼핑백 2장24_102 000원 브랜드데",
+ "라이브99만 시청컬처렐 다이제스티브 3박스 - 3박스 더 (뚱랑이 굿즈 증정) - 선물용 쇼핑백 2장컬처렐 다이제스티브 3박스 - 3박스 더 (뚱랑이 굿즈 증정) - 선물용 쇼핑백 2장24_102 000원 브랜",
"very filename with extension only"
);
@@ -394,7 +398,7 @@ add_task(async function validate_filename_method() {
0
),
"filename.local.download",
- "filename.lnk with vowel separators"
+ "filename.local with vowel separators"
);
Assert.equal(
diff --git a/xpcom/io/nsLocalFileWin.cpp b/xpcom/io/nsLocalFileWin.cpp
@@ -182,7 +182,7 @@ nsresult nsLocalFile::RevealFile(const nsString& aResolvedPath) {
}
// static
-bool nsLocalFile::CheckForReservedFileName(const nsString& aFileName) {
+bool nsLocalFile::CheckForReservedFileName(const nsAString& aFileName) {
static const nsLiteralString forbiddenNames[] = {
u"COM1"_ns, u"COM2"_ns, u"COM3"_ns, u"COM4"_ns, u"COM5"_ns, u"COM6"_ns,
u"COM7"_ns, u"COM8"_ns, u"COM9"_ns, u"LPT1"_ns, u"LPT2"_ns, u"LPT3"_ns,
diff --git a/xpcom/io/nsLocalFileWin.h b/xpcom/io/nsLocalFileWin.h
@@ -49,7 +49,7 @@ class nsLocalFile final : public nsILocalFileWin {
// Checks if the filename is one of the windows reserved filenames
// (com1, com2, etc...) and returns true if so.
- static bool CheckForReservedFileName(const nsString& aFileName);
+ static bool CheckForReservedFileName(const nsAString& aFileName);
/**
* Checks whether the inherited ACEs in aChildDacl only come from the parent.