// Copyright (c) 2010 The Chromium Authors. All rights reserved. // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. #include "chrome/browser/download/download_util.h" #if defined(OS_POSIX) && !defined(OS_MACOSX) #include #endif #include "base/string_util.h" #include "base/test/test_file_util.h" #include "googleurl/src/gurl.h" #include "testing/gtest/include/gtest/gtest.h" #if defined(OS_WIN) #define JPEG_EXT L".jpg" #define HTML_EXT L".htm" #define TXT_EXT L".txt" #define TAR_EXT L".tar" #elif defined(OS_MACOSX) #define JPEG_EXT L".jpeg" #define HTML_EXT L".html" #define TXT_EXT L".txt" #define TAR_EXT L".tar" #else #define JPEG_EXT L".jpg" #define HTML_EXT L".html" #define TXT_EXT L".txt" #define TAR_EXT L".tar" #endif namespace { const struct { const char* disposition; const char* url; const char* mime_type; const wchar_t* expected_name; } kGenerateFileNameTestCases[] = { // No 'filename' keyword in the disposition, use the URL {"a_file_name.txt", "http://www.evil.com/my_download.txt", "text/plain", L"my_download.txt"}, // Disposition has relative paths, remove directory separators {"filename=../../../../././../a_file_name.txt", "http://www.evil.com/my_download.txt", "text/plain", L"_.._.._.._._._.._a_file_name.txt"}, // Disposition has parent directories, remove directory separators {"filename=dir1/dir2/a_file_name.txt", "http://www.evil.com/my_download.txt", "text/plain", L"dir1_dir2_a_file_name.txt"}, // Disposition has relative paths, remove directory separators {"filename=..\\..\\..\\..\\.\\.\\..\\a_file_name.txt", "http://www.evil.com/my_download.txt", "text/plain", L"_.._.._.._._._.._a_file_name.txt"}, // Disposition has parent directories, remove directory separators {"filename=dir1\\dir2\\a_file_name.txt", "http://www.evil.com/my_download.txt", "text/plain", L"dir1_dir2_a_file_name.txt"}, // No useful information in disposition or URL, use default {"", "http://www.truncated.com/path/", "text/plain", L"download" TXT_EXT }, // A normal avi should get .avi and not .avi.avi {"", "https://blah.google.com/misc/2.avi", "video/x-msvideo", L"2.avi"}, // Spaces in the disposition file name {"filename=My Downloaded File.exe", "http://www.frontpagehacker.com/a_download.exe", "application/octet-stream", L"My Downloaded File.exe"}, {"filename=my-cat", "http://www.example.com/my-cat", "image/jpeg", L"my-cat" JPEG_EXT }, {"filename=my-cat", "http://www.example.com/my-cat", "text/plain", L"my-cat.txt"}, {"filename=my-cat", "http://www.example.com/my-cat", "text/html", L"my-cat" HTML_EXT }, {"filename=my-cat", "http://www.example.com/my-cat", "dance/party", L"my-cat"}, {"filename=my-cat.jpg", "http://www.example.com/my-cat.jpg", "text/plain", L"my-cat.jpg"}, // .exe tests. #if defined(OS_WIN) {"filename=evil.exe", "http://www.goodguy.com/evil.exe", "image/jpeg", L"evil.exe"}, {"filename=ok.exe", "http://www.goodguy.com/ok.exe", "binary/octet-stream", L"ok.exe"}, {"filename=evil.dll", "http://www.goodguy.com/evil.dll", "dance/party", L"evil.dll"}, {"filename=evil", "http://www.goodguy.com/evil.exe", "application/rss+xml", L"evil"}, // Test truncation of trailing dots and spaces {"filename=evil.exe ", "http://www.goodguy.com/evil.exe ", "binary/octet-stream", L"evil.exe"}, {"filename=evil.exe.", "http://www.goodguy.com/evil.exe.", "binary/octet-stream", L"evil.exe"}, {"filename=evil.exe. . .", "http://www.goodguy.com/evil.exe. . .", "binary/octet-stream", L"evil.exe"}, {"filename=evil.", "http://www.goodguy.com/evil.", "binary/octet-stream", L"evil"}, {"filename=. . . . .", "http://www.goodguy.com/. . . . .", "binary/octet-stream", L"download"}, #endif // OS_WIN {"filename=utils.js", "http://www.goodguy.com/utils.js", "application/x-javascript", L"utils.js"}, {"filename=contacts.js", "http://www.goodguy.com/contacts.js", "application/json", L"contacts.js"}, {"filename=utils.js", "http://www.goodguy.com/utils.js", "text/javascript", L"utils.js"}, {"filename=utils.js", "http://www.goodguy.com/utils.js", "text/javascript;version=2", L"utils.js"}, {"filename=utils.js", "http://www.goodguy.com/utils.js", "application/ecmascript", L"utils.js"}, {"filename=utils.js", "http://www.goodguy.com/utils.js", "application/ecmascript;version=4", L"utils.js"}, {"filename=program.exe", "http://www.goodguy.com/program.exe", "application/foo-bar", L"program.exe"}, {"filename=../foo.txt", "http://www.evil.com/../foo.txt", "text/plain", L"_foo.txt"}, {"filename=..\\foo.txt", "http://www.evil.com/..\\foo.txt", "text/plain", L"_foo.txt" }, {"filename=.hidden", "http://www.evil.com/.hidden", "text/plain", L"hidden" TXT_EXT }, {"filename=trailing.", "http://www.evil.com/trailing.", "dance/party", L"trailing" }, {"filename=trailing.", "http://www.evil.com/trailing.", "text/plain", L"trailing" TXT_EXT }, {"filename=.", "http://www.evil.com/.", "dance/party", L"download"}, {"filename=..", "http://www.evil.com/..", "dance/party", L"download"}, {"filename=...", "http://www.evil.com/...", "dance/party", L"download"}, // Note that this one doesn't have "filename=" on it. {"a_file_name.txt", "http://www.evil.com/", "image/jpeg", L"download" JPEG_EXT }, {"filename=", "http://www.evil.com/", "image/jpeg", L"download" JPEG_EXT }, {"filename=simple", "http://www.example.com/simple", "application/octet-stream", L"simple"}, {"filename=COM1", "http://www.goodguy.com/COM1", "application/foo-bar", #if defined(OS_WIN) L"_COM1" #else L"COM1" #endif }, {"filename=COM4.txt", "http://www.goodguy.com/COM4.txt", "text/plain", #if defined(OS_WIN) L"_COM4.txt" #else L"COM4.txt" #endif }, {"filename=lpt1.TXT", "http://www.goodguy.com/lpt1.TXT", "text/plain", #if defined(OS_WIN) L"_lpt1.TXT" #else L"lpt1.TXT" #endif }, {"filename=clock$.txt", "http://www.goodguy.com/clock$.txt", "text/plain", #if defined(OS_WIN) L"_clock$.txt" #else L"clock$.txt" #endif }, {"filename=mycom1.foo", "http://www.goodguy.com/mycom1.foo", "text/plain", L"mycom1.foo"}, {"filename=Setup.exe.local", "http://www.badguy.com/Setup.exe.local", "application/foo-bar", #if defined(OS_WIN) L"Setup.exe.download" #else L"Setup.exe.local" #endif }, {"filename=Setup.exe.local.local", "http://www.badguy.com/Setup.exe.local", "application/foo-bar", #if defined(OS_WIN) L"Setup.exe.local.download" #else L"Setup.exe.local.local" #endif }, {"filename=Setup.exe.lnk", "http://www.badguy.com/Setup.exe.lnk", "application/foo-bar", #if defined(OS_WIN) L"Setup.exe.download" #else L"Setup.exe.lnk" #endif }, {"filename=Desktop.ini", "http://www.badguy.com/Desktop.ini", "application/foo-bar", #if defined(OS_WIN) L"_Desktop.ini" #else L"Desktop.ini" #endif }, {"filename=Thumbs.db", "http://www.badguy.com/Thumbs.db", "application/foo-bar", #if defined(OS_WIN) L"_Thumbs.db" #else L"Thumbs.db" #endif }, {"filename=source.jpg", "http://www.hotmail.com", "application/x-javascript", L"source.jpg" }, // NetUtilTest.{GetSuggestedFilename, GetFileNameFromCD} test these // more thoroughly. Tested below are a small set of samples. {"attachment; filename=\"%EC%98%88%EC%88%A0%20%EC%98%88%EC%88%A0.jpg\"", "http://www.examples.com/", "image/jpeg", L"\uc608\uc220 \uc608\uc220.jpg"}, {"attachment; name=abc de.pdf", "http://www.examples.com/q.cgi?id=abc", "application/octet-stream", L"abc de.pdf"}, {"filename=\"=?EUC-JP?Q?=B7=DD=BD=D13=2Epng?=\"", "http://www.example.com/path", "image/png", L"\x82b8\x8853" L"3.png"}, // The following two have invalid CD headers and filenames come // from the URL. {"attachment; filename==?iiso88591?Q?caf=EG?=", "http://www.example.com/test%20123", "image/jpeg", L"test 123" JPEG_EXT }, {"malformed_disposition", "http://www.google.com/%EC%98%88%EC%88%A0%20%EC%98%88%EC%88%A0.jpg", "image/jpeg", L"\uc608\uc220 \uc608\uc220.jpg"}, // Invalid C-D. No filename from URL. Falls back to 'download'. {"attachment; filename==?iso88591?Q?caf=E3?", "http://www.google.com/path1/path2/", "image/jpeg", L"download" JPEG_EXT }, // Issue=5772. {"", "http://www.example.com/foo.tar.gz", "application/x-tar", L"foo.tar.gz"}, // Issue=52250. {"", "http://www.example.com/foo.tgz", "application/x-tar", L"foo.tgz"}, // Issue=7337. {"", "http://maged.lordaeron.org/blank.reg", "text/x-registry", L"blank.reg"}, {"", "http://www.example.com/bar.tar", "application/x-tar", L"bar.tar"}, {"", "http://www.example.com/bar.bogus", "application/x-tar", L"bar.bogus" }, // http://code.google.com/p/chromium/issues/detail?id=20337 {"filename=.download.txt", "http://www.example.com/.download.txt", "text/plain", L"download.txt"}, // Issue=56855. {"", "http://www.example.com/bar.sh", "application/x-sh", L"bar.sh" }, }; // Tests to ensure that the file names we generate from hints from the server // (content-disposition, URL name, etc) don't cause security holes. TEST(DownloadUtilTest, GenerateFileName) { #if defined(OS_POSIX) && !defined(OS_MACOSX) // This test doesn't run when the locale is not UTF-8 because some of the // string conversions fail. This is OK (we have the default value) but they // don't match our expectations. std::string locale = setlocale(LC_CTYPE, NULL); StringToLowerASCII(&locale); EXPECT_NE(std::string::npos, locale.find("utf-8")) << "Your locale (" << locale << ") must be set to UTF-8 " << "for this test to pass!"; #endif for (size_t i = 0; i < ARRAYSIZE_UNSAFE(kGenerateFileNameTestCases); ++i) { FilePath generated_name; download_util::GenerateFileName(GURL(kGenerateFileNameTestCases[i].url), kGenerateFileNameTestCases[i].disposition, "", kGenerateFileNameTestCases[i].mime_type, &generated_name); EXPECT_EQ(kGenerateFileNameTestCases[i].expected_name, file_util::FilePathAsWString(generated_name)) << i; } for (size_t i = 0; i < ARRAYSIZE_UNSAFE(kGenerateFileNameTestCases); ++i) { FilePath generated_name; download_util::GenerateFileName(GURL(kGenerateFileNameTestCases[i].url), kGenerateFileNameTestCases[i].disposition, "GBK", kGenerateFileNameTestCases[i].mime_type, &generated_name); EXPECT_EQ(kGenerateFileNameTestCases[i].expected_name, file_util::FilePathAsWString(generated_name)) << i; } // A couple of cases with raw 8bit characters in C-D. { FilePath generated_name; download_util::GenerateFileName(GURL("http://www.example.com/images?id=3"), "attachment; filename=caf\xc3\xa9.png", "iso-8859-1", "image/png", &generated_name); EXPECT_EQ(L"caf\u00e9.png", file_util::FilePathAsWString(generated_name)); } { FilePath generated_name; download_util::GenerateFileName(GURL("http://www.example.com/images?id=3"), "attachment; filename=caf\xe5.png", "windows-1253", "image/png", &generated_name); EXPECT_EQ(L"caf\u03b5.png", file_util::FilePathAsWString(generated_name)); } } const struct { const FilePath::CharType* path; const char* mime_type; const FilePath::CharType* expected_path; } kSafeFilenameCases[] = { #if defined(OS_WIN) { FILE_PATH_LITERAL("C:\\foo\\bar.htm"), "text/html", FILE_PATH_LITERAL("C:\\foo\\bar.htm") }, { FILE_PATH_LITERAL("C:\\foo\\bar.html"), "text/html", FILE_PATH_LITERAL("C:\\foo\\bar.html") }, { FILE_PATH_LITERAL("C:\\foo\\bar"), "text/html", FILE_PATH_LITERAL("C:\\foo\\bar.htm") }, { FILE_PATH_LITERAL("C:\\bar.html"), "image/png", FILE_PATH_LITERAL("C:\\bar.html") }, { FILE_PATH_LITERAL("C:\\bar"), "image/png", FILE_PATH_LITERAL("C:\\bar.png") }, { FILE_PATH_LITERAL("C:\\foo\\bar.exe"), "text/html", FILE_PATH_LITERAL("C:\\foo\\bar.exe") }, { FILE_PATH_LITERAL("C:\\foo\\bar.exe"), "image/gif", FILE_PATH_LITERAL("C:\\foo\\bar.exe") }, { FILE_PATH_LITERAL("C:\\foo\\google.com"), "text/html", FILE_PATH_LITERAL("C:\\foo\\google.com") }, { FILE_PATH_LITERAL("C:\\foo\\con.htm"), "text/html", FILE_PATH_LITERAL("C:\\foo\\_con.htm") }, { FILE_PATH_LITERAL("C:\\foo\\con"), "text/html", FILE_PATH_LITERAL("C:\\foo\\_con.htm") }, #else // !defined(OS_WIN) { FILE_PATH_LITERAL("/foo/bar.htm"), "text/html", FILE_PATH_LITERAL("/foo/bar.htm") }, { FILE_PATH_LITERAL("/foo/bar.html"), "text/html", FILE_PATH_LITERAL("/foo/bar.html") }, { FILE_PATH_LITERAL("/foo/bar"), "text/html", FILE_PATH_LITERAL("/foo/bar.html") }, { FILE_PATH_LITERAL("/bar.html"), "image/png", FILE_PATH_LITERAL("/bar.html") }, { FILE_PATH_LITERAL("/bar"), "image/png", FILE_PATH_LITERAL("/bar.png") }, { FILE_PATH_LITERAL("/foo/bar.exe"), "image/gif", FILE_PATH_LITERAL("/foo/bar.exe") }, { FILE_PATH_LITERAL("/foo/google.com"), "text/html", FILE_PATH_LITERAL("/foo/google.com") }, { FILE_PATH_LITERAL("/foo/con.htm"), "text/html", FILE_PATH_LITERAL("/foo/con.htm") }, { FILE_PATH_LITERAL("/foo/con"), "text/html", FILE_PATH_LITERAL("/foo/con.html") }, #endif // !defined(OS_WIN) }; TEST(DownloadUtilTest, GenerateSafeFileName) { for (size_t i = 0; i < ARRAYSIZE_UNSAFE(kSafeFilenameCases); ++i) { FilePath path(kSafeFilenameCases[i].path); download_util::GenerateSafeFileName(kSafeFilenameCases[i].mime_type, &path); EXPECT_EQ(kSafeFilenameCases[i].expected_path, path.value()) << i; } } } // namespace