blob: 25c2434d383f01362ab08b18893f15b6b55153a1 [file] [log] [blame]
// Copyright (c) 2011 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 "content/browser/download/base_file.h"
#include "base/file_util.h"
#include "base/format_macros.h"
#include "base/logging.h"
#include "base/stringprintf.h"
#include "base/threading/thread_restrictions.h"
#include "base/utf_string_conversions.h"
#include "content/browser/download/download_stats.h"
#include "content/public/browser/browser_thread.h"
#include "content/public/browser/content_browser_client.h"
#include "crypto/secure_hash.h"
#include "net/base/file_stream.h"
#include "net/base/net_errors.h"
#if defined(OS_WIN)
#include <windows.h>
#include <shellapi.h>
#include "content/browser/safe_util_win.h"
#elif defined(OS_MACOSX)
#include "content/browser/file_metadata_mac.h"
#endif
namespace {
#define LOG_ERROR(o, e) LogError(__FILE__, __LINE__, __FUNCTION__, o, e)
// Logs the value and passes error on through, converting to a |net::Error|.
// Returns |ERR_UNEXPECTED| if the value is not in the enum.
net::Error LogError(const char* file, int line, const char* func,
const char* operation, int error) {
const char* err_string = "";
net::Error net_error = net::OK;
#define NET_ERROR(label, value) \
case net::ERR_##label: \
err_string = #label; \
net_error = net::ERR_##label; \
break;
switch (error) {
case net::OK:
return net::OK;
#include "net/base/net_error_list.h"
default:
err_string = "Unexpected enum value";
net_error = net::ERR_UNEXPECTED;
break;
}
#undef NET_ERROR
DVLOG(1) << " " << func << "(): " << operation
<< "() returned error " << error << " (" << err_string << ")";
return net_error;
}
#if defined(OS_WIN)
#define SHFILE_TO_NET_ERROR(symbol, value, mapping, description) \
case value: return net::ERR_##mapping;
// Maps the result of a call to |SHFileOperation()| onto a |net::Error|.
//
// These return codes are *old* (as in, DOS era), and specific to
// |SHFileOperation()|.
// They do not appear in any windows header.
//
// See http://msdn.microsoft.com/en-us/library/bb762164(VS.85).aspx.
net::Error MapShFileOperationCodes(int code) {
// Check these pre-Win32 error codes first, then check for matches
// in Winerror.h.
switch (code) {
// Error Code, Value, Platform Error Mapping, Meaning
SHFILE_TO_NET_ERROR(DE_SAMEFILE, 0x71, FILE_EXISTS,
"The source and destination files are the same file.")
SHFILE_TO_NET_ERROR(DE_OPCANCELLED, 0x75, ABORTED,
"The operation was canceled by the user, or silently canceled if "
"the appropriate flags were supplied to SHFileOperation.")
SHFILE_TO_NET_ERROR(DE_ACCESSDENIEDSRC, 0x78, ACCESS_DENIED,
"Security settings denied access to the source.")
SHFILE_TO_NET_ERROR(DE_PATHTOODEEP, 0x79, FILE_PATH_TOO_LONG,
"The source or destination path exceeded or would exceed MAX_PATH.")
SHFILE_TO_NET_ERROR(DE_INVALIDFILES, 0x7C, FILE_NOT_FOUND,
"The path in the source or destination or both was invalid.")
SHFILE_TO_NET_ERROR(DE_FLDDESTISFILE, 0x7E, FILE_EXISTS,
"The destination path is an existing file.")
SHFILE_TO_NET_ERROR(DE_FILEDESTISFLD, 0x80, FILE_EXISTS,
"The destination path is an existing folder.")
SHFILE_TO_NET_ERROR(DE_FILENAMETOOLONG, 0x81, FILE_PATH_TOO_LONG,
"The name of the file exceeds MAX_PATH.")
SHFILE_TO_NET_ERROR(DE_DEST_IS_CDROM, 0x82, ACCESS_DENIED,
"The destination is a read-only CD-ROM, possibly unformatted.")
SHFILE_TO_NET_ERROR(DE_DEST_IS_DVD, 0x83, ACCESS_DENIED,
"The destination is a read-only DVD, possibly unformatted.")
SHFILE_TO_NET_ERROR(DE_DEST_IS_CDRECORD, 0x84, ACCESS_DENIED,
"The destination is a writable CD-ROM, possibly unformatted.")
SHFILE_TO_NET_ERROR(DE_FILE_TOO_LARGE, 0x85, FILE_TOO_BIG,
"The file involved in the operation is too large for the destination "
"media or file system.")
SHFILE_TO_NET_ERROR(DE_SRC_IS_CDROM, 0x86, ACCESS_DENIED,
"The source is a read-only CD-ROM, possibly unformatted.")
SHFILE_TO_NET_ERROR(DE_SRC_IS_DVD, 0x87, ACCESS_DENIED,
"The source is a read-only DVD, possibly unformatted.")
SHFILE_TO_NET_ERROR(DE_SRC_IS_CDRECORD, 0x88, ACCESS_DENIED,
"The source is a writable CD-ROM, possibly unformatted.")
SHFILE_TO_NET_ERROR(DE_ERROR_MAX, 0xB7, FILE_PATH_TOO_LONG,
"MAX_PATH was exceeded during the operation.")
SHFILE_TO_NET_ERROR(XE_ERRORONDEST, 0x10000, UNEXPECTED,
"An unspecified error occurred on the destination.")
// These are not expected to occur for in our usage.
SHFILE_TO_NET_ERROR(DE_MANYSRC1DEST, 0x72, FAILED,
"Multiple file paths were specified in the source buffer, "
"but only one destination file path.")
SHFILE_TO_NET_ERROR(DE_DIFFDIR, 0x73, FAILED,
"Rename operation was specified but the destination path is "
"a different directory. Use the move operation instead.")
SHFILE_TO_NET_ERROR(DE_ROOTDIR, 0x74, FAILED,
"The source is a root directory, which cannot be moved or renamed.")
SHFILE_TO_NET_ERROR(DE_DESTSUBTREE, 0x76, FAILED,
"The destination is a subtree of the source.")
SHFILE_TO_NET_ERROR(DE_MANYDEST, 0x7A, FAILED,
"The operation involved multiple destination paths, "
"which can fail in the case of a move operation.")
SHFILE_TO_NET_ERROR(DE_DESTSAMETREE, 0x7D, FAILED,
"The source and destination have the same parent folder.")
SHFILE_TO_NET_ERROR(DE_UNKNOWN_ERROR, 0x402, FAILED,
"An unknown error occurred. "
"This is typically due to an invalid path in the source or destination."
" This error does not occur on Windows Vista and later.")
SHFILE_TO_NET_ERROR(DE_ROOTDIR | ERRORONDEST, 0x10074, FAILED,
"Destination is a root directory and cannot be renamed.")
default:
break;
}
// If not one of the above codes, it should be a standard Windows error code.
return static_cast<net::Error>(net::MapSystemError(code));
}
#undef SHFILE_TO_NET_ERROR
// Renames a file using the SHFileOperation API to ensure that the target file
// gets the correct default security descriptor in the new path.
// Returns a network error, or net::OK for success.
net::Error RenameFileAndResetSecurityDescriptor(
const FilePath& source_file_path,
const FilePath& target_file_path) {
base::ThreadRestrictions::AssertIOAllowed();
// The parameters to SHFileOperation must be terminated with 2 NULL chars.
std::wstring source = source_file_path.value();
std::wstring target = target_file_path.value();
source.append(1, L'\0');
target.append(1, L'\0');
SHFILEOPSTRUCT move_info = {0};
move_info.wFunc = FO_MOVE;
move_info.pFrom = source.c_str();
move_info.pTo = target.c_str();
move_info.fFlags = FOF_SILENT | FOF_NOCONFIRMATION | FOF_NOERRORUI |
FOF_NOCONFIRMMKDIR | FOF_NOCOPYSECURITYATTRIBS;
int result = SHFileOperation(&move_info);
if (result == 0)
return net::OK;
return MapShFileOperationCodes(result);
}
#endif
} // namespace
BaseFile::BaseFile(const FilePath& full_path,
const GURL& source_url,
const GURL& referrer_url,
int64 received_bytes,
const linked_ptr<net::FileStream>& file_stream)
: full_path_(full_path),
source_url_(source_url),
referrer_url_(referrer_url),
file_stream_(file_stream),
bytes_so_far_(received_bytes),
power_save_blocker_(PowerSaveBlocker::kPowerSaveBlockPreventSystemSleep),
calculate_hash_(false),
detached_(false) {
DCHECK(BrowserThread::CurrentlyOn(BrowserThread::FILE));
memset(sha256_hash_, 0, sizeof(sha256_hash_));
if (file_stream_.get())
file_stream_->EnableErrorStatistics();
}
BaseFile::~BaseFile() {
DCHECK(BrowserThread::CurrentlyOn(BrowserThread::FILE));
if (detached_)
Close();
else
Cancel(); // Will delete the file.
}
net::Error BaseFile::Initialize(bool calculate_hash) {
DCHECK(BrowserThread::CurrentlyOn(BrowserThread::FILE));
DCHECK(!detached_);
calculate_hash_ = calculate_hash;
if (calculate_hash_)
secure_hash_.reset(crypto::SecureHash::Create(crypto::SecureHash::SHA256));
if (full_path_.empty()) {
FilePath temp_file;
FilePath download_dir =
content::GetContentClient()->browser()->GetDefaultDownloadDirectory();
if (!file_util::CreateTemporaryFileInDir(download_dir, &temp_file) &&
!file_util::CreateTemporaryFile(&temp_file)) {
return LOG_ERROR("unable to create", net::ERR_FILE_NOT_FOUND);
}
full_path_ = temp_file;
}
return Open();
}
net::Error BaseFile::AppendDataToFile(const char* data, size_t data_len) {
DCHECK(BrowserThread::CurrentlyOn(BrowserThread::FILE));
DCHECK(!detached_);
if (!file_stream_.get())
return LOG_ERROR("get", net::ERR_INVALID_HANDLE);
// TODO(phajdan.jr): get rid of this check.
if (data_len == 0)
return net::OK;
// The Write call below is not guaranteed to write all the data.
size_t write_count = 0;
size_t len = data_len;
const char* current_data = data;
while (len > 0) {
write_count++;
int write_result =
file_stream_->Write(current_data, len, net::CompletionCallback());
DCHECK_NE(0, write_result);
// Check for errors.
if (static_cast<size_t>(write_result) != data_len) {
// We should never get ERR_IO_PENDING, as the Write above is synchronous.
DCHECK_NE(net::ERR_IO_PENDING, write_result);
// Report errors on file writes.
if (write_result < 0)
return LOG_ERROR("Write", write_result);
}
// Update status.
size_t write_size = static_cast<size_t>(write_result);
DCHECK_LE(write_size, len);
len -= write_size;
current_data += write_size;
bytes_so_far_ += write_size;
}
// TODO(ahendrickson) -- Uncomment these when the functions are available.
download_stats::RecordDownloadWriteSize(data_len);
download_stats::RecordDownloadWriteLoopCount(write_count);
if (calculate_hash_)
secure_hash_->Update(data, data_len);
return net::OK;
}
net::Error BaseFile::Rename(const FilePath& new_path) {
DCHECK(BrowserThread::CurrentlyOn(BrowserThread::FILE));
// Save the information whether the download is in progress because
// it will be overwritten by closing the file.
bool saved_in_progress = in_progress();
// If the new path is same as the old one, there is no need to perform the
// following renaming logic.
if (new_path == full_path_) {
// Don't close the file if we're not done (finished or canceled).
if (!saved_in_progress)
Close();
return net::OK;
}
Close();
file_util::CreateDirectory(new_path.DirName());
#if defined(OS_WIN)
// We cannot rename because rename will keep the same security descriptor
// on the destination file. We want to recreate the security descriptor
// with the security that makes sense in the new path.
// |RenameFileAndResetSecurityDescriptor| returns a windows-specific
// error, whch we must translate into a net::Error.
net::Error rename_err =
RenameFileAndResetSecurityDescriptor(full_path_, new_path);
if (rename_err != net::OK)
return LOG_ERROR("RenameFileAndResetSecurityDescriptor", rename_err);
#elif defined(OS_POSIX)
{
// Similarly, on Unix, we're moving a temp file created with permissions
// 600 to |new_path|. Here, we try to fix up the destination file with
// appropriate permissions.
struct stat st;
// First check the file existence and create an empty file if it doesn't
// exist.
if (!file_util::PathExists(new_path)) {
int write_error = file_util::WriteFile(new_path, "", 0);
if (write_error < 0)
return LOG_ERROR("WriteFile", net::MapSystemError(errno));
}
int stat_error = stat(new_path.value().c_str(), &st);
bool stat_succeeded = (stat_error == 0);
if (!stat_succeeded)
return LOG_ERROR("stat", net::MapSystemError(errno));
// TODO(estade): Move() falls back to copying and deleting when a simple
// rename fails. Copying sucks for large downloads. crbug.com/8737
if (!file_util::Move(full_path_, new_path))
return LOG_ERROR("Move", net::MapSystemError(errno));
int chmod_error = chmod(new_path.value().c_str(), st.st_mode);
if (chmod_error < 0)
return LOG_ERROR("chmod", net::MapSystemError(errno));
}
#endif
full_path_ = new_path;
// We don't need to re-open the file if we're done (finished or canceled).
if (!saved_in_progress)
return net::OK;
return Open();
}
void BaseFile::Detach() {
detached_ = true;
}
void BaseFile::Cancel() {
DCHECK(BrowserThread::CurrentlyOn(BrowserThread::FILE));
DCHECK(!detached_);
Close();
if (!full_path_.empty())
file_util::Delete(full_path_, false);
}
void BaseFile::Finish() {
DCHECK(BrowserThread::CurrentlyOn(BrowserThread::FILE));
if (calculate_hash_)
secure_hash_->Finish(sha256_hash_, kSha256HashLen);
Close();
}
bool BaseFile::GetSha256Hash(std::string* hash) {
DCHECK(!detached_);
if (!calculate_hash_ || in_progress())
return false;
hash->assign(reinterpret_cast<const char*>(sha256_hash_),
sizeof(sha256_hash_));
return true;
}
void BaseFile::AnnotateWithSourceInformation() {
DCHECK(BrowserThread::CurrentlyOn(BrowserThread::FILE));
DCHECK(!detached_);
#if defined(OS_WIN)
// Sets the Zone to tell Windows that this file comes from the internet.
// We ignore the return value because a failure is not fatal.
win_util::SetInternetZoneIdentifier(full_path_,
UTF8ToWide(source_url_.spec()));
#elif defined(OS_MACOSX)
file_metadata::AddQuarantineMetadataToFile(full_path_, source_url_,
referrer_url_);
file_metadata::AddOriginMetadataToFile(full_path_, source_url_,
referrer_url_);
#endif
}
void BaseFile::CreateFileStream() {
file_stream_.reset(new net::FileStream);
}
net::Error BaseFile::Open() {
DCHECK(BrowserThread::CurrentlyOn(BrowserThread::FILE));
DCHECK(!detached_);
DCHECK(!full_path_.empty());
// Create a new file stream if it is not provided.
if (!file_stream_.get()) {
CreateFileStream();
file_stream_->EnableErrorStatistics();
int open_result = file_stream_->Open(
full_path_,
base::PLATFORM_FILE_OPEN_ALWAYS | base::PLATFORM_FILE_WRITE);
if (open_result != net::OK) {
file_stream_.reset();
return LOG_ERROR("Open", open_result);
}
// We may be re-opening the file after rename. Always make sure we're
// writing at the end of the file.
int64 seek_result = file_stream_->Seek(net::FROM_END, 0);
if (seek_result < 0) {
file_stream_.reset();
return LOG_ERROR("Seek", seek_result);
}
}
#if defined(OS_WIN)
AnnotateWithSourceInformation();
#endif
return net::OK;
}
void BaseFile::Close() {
DCHECK(BrowserThread::CurrentlyOn(BrowserThread::FILE));
if (file_stream_.get()) {
#if defined(OS_CHROMEOS)
// Currently we don't really care about the return value, since if it fails
// theres not much we can do. But we might in the future.
file_stream_->Flush();
#endif
file_stream_->Close();
file_stream_.reset();
}
}
std::string BaseFile::DebugString() const {
return base::StringPrintf("{ source_url_ = \"%s\""
" full_path_ = \"%" PRFilePath "\""
" bytes_so_far_ = %" PRId64
" detached_ = %c }",
source_url_.spec().c_str(),
full_path_.value().c_str(),
bytes_so_far_,
detached_ ? 'T' : 'F');
}