tor-browser

The Tor Browser
git clone https://git.dasho.dev/tor-browser.git
Log | Files | Refs | README | LICENSE

commit e7537a61ee03cb2d27f85197280b8c1565816fe7
parent c0fdda3a1e0a663e96f403e1db42d06f2d0689cf
Author: Alexandru Marc <amarc@mozilla.com>
Date:   Fri, 12 Dec 2025 01:02:03 +0200

Revert "Bug 1959738 - part 3 - Add a few SanitizeFileName tests with unpaired surrogates. r=Gijs" for causing bc failures @ browser_save_filenames

This reverts commit e9790fa9ac1ce7e43b60fb9d4586080d0866bc1c.

Revert "Bug 1959738 - part 2 - Rewrite SanitizeFileName for better readability and correctness. r=Gijs"

This reverts commit d5b61ae19884f6ae1fa4714c747876a8905fca5d.

Revert "Bug 1959738 - Use a standard "_files" suffix for associated files folder in all locales. r=Gijs,mak"

This reverts commit 6eb038cba94d164438c21207527bd0c24cdf036e.

Diffstat:
Mtoolkit/content/contentAreaUtils.js | 9++++-----
Mtoolkit/locales/en-US/chrome/global/contentAreaCommands.properties | 6++++++
Muriloader/exthandler/nsExternalHelperAppService.cpp | 396++++++++++++++++++++++++++++++++++++++-----------------------------------------
Muriloader/exthandler/tests/unit/test_filename_sanitize.js | 82++++++++++++++++++++-----------------------------------------------------------
Mxpcom/io/nsLocalFileWin.cpp | 2+-
Mxpcom/io/nsLocalFileWin.h | 2+-
6 files changed, 222 insertions(+), 275 deletions(-)

diff --git a/toolkit/content/contentAreaUtils.js b/toolkit/content/contentAreaUtils.js @@ -507,11 +507,10 @@ function internalPersist(persistArgs) { filesFolder = persistArgs.targetFile.clone(); var nameWithoutExtension = getFileBaseName(filesFolder.leafName); - // Given the minimal benefits, the "_files" suffix is intentionally not - // localized. 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. - var filesFolderLeafName = nameWithoutExtension + "_files"; + var filesFolderLeafName = + ContentAreaUtils.stringBundle.formatStringFromName("filesFolder", [ + nameWithoutExtension, + ]); filesFolder.leafName = filesFolderLeafName; } diff --git a/toolkit/locales/en-US/chrome/global/contentAreaCommands.properties b/toolkit/locales/en-US/chrome/global/contentAreaCommands.properties @@ -20,3 +20,9 @@ WebPageXMLOnlyFilter=Web Page, XML only # not be determined or if a filename was invalid. A period and file # extension may be appended to this string. UntitledSaveFileName=Untitled + +# LOCALIZATION NOTE (filesFolder): +# This is the name of the folder that is created parallel to a HTML file +# when it is saved "With Images". The %S section is replaced with the +# leaf name of the file being saved (minus extension). +filesFolder=%S_files diff --git a/uriloader/exthandler/nsExternalHelperAppService.cpp b/uriloader/exthandler/nsExternalHelperAppService.cpp @@ -17,7 +17,6 @@ #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" @@ -802,7 +801,7 @@ NS_IMETHODIMP nsExternalHelperAppService::CreateListener( } nsAutoString extension; - int32_t dotidx = fileName.RFindChar(u'.'); + int32_t dotidx = fileName.RFind(u"."); if (dotidx != -1) { extension = Substring(fileName, dotidx + 1); } @@ -3545,250 +3544,233 @@ void nsExternalHelperAppService::CheckDefaultFileName(nsAString& aFileName, void nsExternalHelperAppService::SanitizeFileName(nsAString& aFileName, uint32_t aFlags) { - // 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. + 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. + const uint32_t maxBytes = + (aFlags & VALIDATE_DONT_TRUNCATE) ? 0 : kDefaultMaxFileNameLength; + + // True if the last character added 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; - } - // Remember where this character begins. - const char16_t* charStart = cp; - // Get the full character code, and advance cp past it. + // 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; + + // 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) { bool err = false; - 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; + char32_t nextChar = UTF16CharEnumerator::NextChar(&startStr, endStr, &err); + if (err) { + break; } - // Skip control characters and line/paragraph separators. + // nulls are already stripped out above. + MOZ_ASSERT(nextChar != char16_t(0)); + 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') { - if (dest == aFileName.BeginWriting() || - (collapseWhitespace && lastWasWhitespace)) { + // 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; 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 = '_'; + } - if ((nextChar == u'.' || nextChar == u'\u180e')) { - // Trim dot and vowel separator at beginning. - if (dest == aFileName.BeginWriting()) { - continue; + // Don't truncate surrogate pairs in the middle. + lastNonTrimmable = + int32_t(outFileName.Length()) + + (NS_IS_HIGH_SURROGATE(H_SURROGATE(nextChar)) ? 2 : 1); } - } else if (unicodeCategory == HB_UNICODE_GENERAL_CATEGORY_FORMAT) { - // Replace other formatting characters with underscore. - *dest++ = u'_'; - continue; } - // Copy the current character (potentially a surrogate pair) to dest. - while (charStart < cp) { - *dest++ = *charStart++; - } - } - - // 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'; - }; + 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()); + } + } - // Strip any trailing whitespace/period/vowel-separator. - while (dest > aFileName.BeginWriting()) { - char16_t ch = *(dest - 1); - if (trimIfTrailing(ch)) { - dest--; - } else { - break; + // 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; + } } - } - - // 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)) { - ext.StripWhitespace(); - } + AppendUCS4ToUTF16(nextChar, outFileName); + } + + // If the filename is longer than the maximum allowed filename size, + // truncate it, but preserve the desired extension that is currently + // on the filename. + if (bytesLength > maxBytes && !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 -= extensionBytesLength; + 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)); - // 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; - } - } + // Now that the filename has been truncated, re-append the extension + // again. + if (!extension.IsEmpty()) { + if (outFileName.Last() != '.') { + outFileName.AppendLiteral("."); + } - // Filename finalization helper, applied once we have determined the - // (possibly-truncated) sanitized base-name and extension components. - auto finalizeName = MakeScopeExit([&]() { -#ifdef XP_WIN - // 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); - } -#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); + outFileName.Append(extension); + } } - 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; + } 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); } - // 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); + 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; } - fileNameBytes = utf8Length(aFileName); - ext.Truncate(); - extBytes = 0; } - if (fileNameBytes + extBytes <= byteLimit && - fileNameBytes + kFiles.Length() <= byteLimit) { - return; +#ifdef XP_WIN + if (nsLocalFile::CheckForReservedFileName(outFileName)) { + int32_t dotidx = outFileName.RFind(u"."); + if (dotidx == -1) { + outFileName.Truncate(); + } else { + outFileName = Substring(outFileName, dotidx); + } + CheckDefaultFileName(outFileName, aFlags); } +#endif - // 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())); - - // 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; + if (!(aFlags & VALIDATE_ALLOW_INVALID_FILENAMES)) { + // If the extension is one these types, replace it with .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(outFileName, u".lnk"_ns, + nsCaseInsensitiveStringComparator) || + StringEndsWith(outFileName, u".local"_ns, + nsCaseInsensitiveStringComparator) || + StringEndsWith(outFileName, u".url"_ns, + nsCaseInsensitiveStringComparator) || + StringEndsWith(outFileName, u".scf"_ns, + nsCaseInsensitiveStringComparator) || + StringEndsWith(outFileName, u".desktop"_ns, + nsCaseInsensitiveStringComparator)) { + outFileName.AppendLiteral(".download"); } - AppendUCS4ToUTF16(ch, aFileName); } - // Trim any trailing space/vowel-separator/dots at the truncation point. - while (!aFileName.IsEmpty() && trimIfTrailing(aFileName.Last())) { - aFileName.Truncate(aFileName.Length() - 1); - } + aFileName = outFileName; } nsExternalHelperAppService::ModifyExtensionType diff --git a/uriloader/exthandler/tests/unit/test_filename_sanitize.js b/uriloader/exthandler/tests/unit/test_filename_sanitize.js @@ -68,14 +68,10 @@ add_task(async function validate_filename_method() { "whit\u180ee.png" ); Assert.equal(checkFilename("簡単簡単簡単", 0), "簡単簡単簡単.png"); - Assert.equal( - checkFilename("\u3000簡単\u3000\u3000簡単簡単\u3000\u3000.png\u3000", 0), - "簡単\u3000簡単簡単\u3000.png" - ); Assert.equal(checkFilename(" happy\u061c\u2069.png", 0), "happy__.png"); Assert.equal( - checkFilename("12345678".repeat(30) + "abcdefghijk.png", 0), - "12345678".repeat(30) + "abcdefgh.png" + checkFilename("12345678".repeat(31) + "abcdefgh.png", 0), + "12345678".repeat(31) + "ab.png" ); Assert.equal( checkFilename("簡単".repeat(41) + ".png", 0), @@ -83,20 +79,16 @@ add_task(async function validate_filename_method() { ); Assert.equal( checkFilename("a" + "簡単".repeat(42) + ".png", 0), - "a" + "簡単".repeat(41) + ".png" - ); - Assert.equal( - checkFilename("ab" + "簡単".repeat(42) + ".png", 0), - "ab" + "簡単".repeat(41) + ".png" + "a" + "簡単".repeat(40) + "簡.png" ); Assert.equal( - checkFilename("abc" + "簡単".repeat(42) + ".png", 0), - "abc" + "簡単".repeat(40) + "簡.png" + checkFilename("a" + "簡単".repeat(56) + ".png", 0), + "a" + "簡単".repeat(40) + ".png" ); Assert.equal(checkFilename("café.png", 0), "café.png"); Assert.equal( - checkFilename("café".repeat(49) + "caf.png", 0), - "café".repeat(49) + "caf.png" + checkFilename("café".repeat(50) + ".png", 0), + "café".repeat(50) + ".png" ); Assert.equal( checkFilename("café".repeat(51) + ".png", 0), @@ -113,7 +105,7 @@ add_task(async function validate_filename_method() { ); Assert.equal( checkFilename("\u{100001}\u{100002}".repeat(32) + ".png", 0), - "\u{100001}\u{100002}".repeat(31) + ".png" + "\u{100001}\u{100002}".repeat(30) + "\u{100001}.png" ); Assert.equal( @@ -122,11 +114,11 @@ add_task(async function validate_filename_method() { ); Assert.equal( checkFilename("noextensionfile".repeat(17), 0), - "noextensionfile".repeat(16) + "noextens.png" + "noextensionfile".repeat(16) + "noextensio.png" ); Assert.equal( checkFilename("noextensionfile".repeat(16) + "noextensionfil.", 0), - "noextensionfile".repeat(16) + "noextens.png" + "noextensionfile".repeat(16) + "noextensio.png" ); Assert.equal(checkFilename(" first .png ", 0), "first .png"); @@ -158,17 +150,17 @@ add_task(async function validate_filename_method() { ); Assert.equal(checkFilename("sixth.j pe/*g", 0), "sixth.png"); - let repeatStr = "12345678".repeat(30); + let repeatStr = "12345678".repeat(31); Assert.equal( checkFilename( - repeatStr + "seventeenth.png", + repeatStr + "seventh.png", mimeService.VALIDATE_DONT_TRUNCATE ), - repeatStr + "seventeenth.png" + repeatStr + "seventh.png" ); Assert.equal( - checkFilename(repeatStr + "seventeenth.png", 0), - repeatStr + "seventee.png" + checkFilename(repeatStr + "seventh.png", 0), + repeatStr + "se.png" ); // no filename, so index is used by default. @@ -198,13 +190,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_gi" + repeatStr + "lo#_n_" ); ext = ".long/invalid#? ch\\ars"; Assert.equal( checkFilename(repeatStr + ext, mimeService.VALIDATE_SANITIZE_ONLY), - repeatStr.substring(0, 233) + ".long_invalid#_ch_ars" + repeatStr.substring(0, 232) + ".long_invalid#_ch_ars" ); Assert.equal( @@ -220,26 +212,6 @@ add_task(async function validate_filename_method() { Assert.equal(checkFilename("test😀", 0, ""), "test😀"); Assert.equal(checkFilename("test😀😀", 0, ""), "test😀😀"); - // Some examples with unpaired surrogate code units. - Assert.equal( - checkFilename( - "file\uD800name with <unpaired surrogate> and invalid chars.png", - 0 - ), - "file\uFFFDname with _unpaired surrogate_ and invalid chars.png" - ); - Assert.equal( - checkFilename( - "name with <unpaired surrogate> in.exten\uDFFFsion", - mimeService.VALIDATE_SANITIZE_ONLY - ), - "name with _unpaired surrogate_ in.exten\uFFFDsion" - ); - Assert.equal( - checkFilename("." + "\uDC00\uDC01".repeat(4) + repeatStr, 0), - "\uFFFD".repeat(8) + repeatStr.substring(0, 224) + ".png" - ); - // Now check some media types Assert.equal( mimeService.validateFileNameForSaving("video.ogg", "video/ogg", 0), @@ -316,22 +288,10 @@ 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" ); - // Trailing ideographic spaces left at the truncation position should be trimmed. - Assert.equal( - mimeService.validateFileNameForSaving( - "라이브9.9만 시청컬처렐 다이제스티브 3박스 - 3박스 더 (뚱랑이 굿즈 증정) - 선물용 쇼핑백 2장컬처렐 다이제스티브 3박스 - 3박스 더 (뚱랑이 굿즈 증정) - 선물용\u3000\u3000\u3000\u3000\u3000.등-유산균-컬처렐-특가!", - "text/unknown", - mimeService.VALIDATE_SANITIZE_ONLY | - mimeService.VALIDATE_DONT_COLLAPSE_WHITESPACE - ), - "라이브9.9만 시청컬처렐 다이제스티브 3박스 - 3박스 더 (뚱랑이 굿즈 증정) - 선물용 쇼핑백 2장컬처렐 다이제스티브 3박스 - 3박스 더 (뚱랑이 굿즈 증정) - 선물용.등-유산균-컬처렐-특가!", - "very long filename with extension truncated among spaces" - ); - // This filename has a very long extension, almost the entire filename. Assert.equal( mimeService.validateFileNameForSaving( @@ -343,14 +303,14 @@ add_task(async function validate_filename_method() { "another very long filename with long extension" ); - // This filename is cropped at <=248 bytes (so there would be room to append "_files"). + // This filename is cropped at 254 bytes. 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" ); @@ -434,7 +394,7 @@ add_task(async function validate_filename_method() { 0 ), "filename.local.download", - "filename.local with vowel separators" + "filename.lnk 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 nsAString& aFileName) { +bool nsLocalFile::CheckForReservedFileName(const nsString& 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 nsAString& aFileName); + static bool CheckForReservedFileName(const nsString& aFileName); /** * Checks whether the inherited ACEs in aChildDacl only come from the parent.