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:
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.