Bug 887425 - BackgroundFileSaver should support appending to an existing file. r=mcmanus, sr=biesi

This commit is contained in:
Paolo Amadini 2013-07-09 13:59:59 +02:00
parent b81ea1b6ba
commit 9c4fd821b1
4 changed files with 230 additions and 58 deletions

View File

@ -38,7 +38,7 @@ interface nsIFile;
* public methods of the interface may only be called from the main
* thread.
*/
[scriptable, uuid(17a2ff32-918f-11e2-8fc9-f9626188709b)]
[scriptable, uuid(581a99ca-dc8d-4cee-ac95-99156e7517ed)]
interface nsIBackgroundFileSaver : nsISupports
{
/**
@ -52,15 +52,8 @@ interface nsIBackgroundFileSaver : nsISupports
attribute nsIBackgroundFileSaverObserver observer;
/**
* The SHA256 hash in raw bytes associated with the file that was downloaded.
*
* @remarks Reading this will throw NS_ERROR_NOT_AVAILABLE unless
* sha256enabled is true and onSaveComplete has been called.
*/
readonly attribute ACString sha256Hash;
/**
* Compute SHA256.
* Instructs the component to compute the SHA-256 hash of the target file, and
* make it available in the sha256Hash property.
*
* @remarks This must be set on the main thread before the first call to
* setTarget.
@ -68,10 +61,38 @@ interface nsIBackgroundFileSaver : nsISupports
void enableSha256();
/**
* Sets the name of the output file to be written. The output file may
* already exist, in which case it will be overwritten. The target can be
* changed after data has already been fed, in which case the existing file
* will be moved to the new destination.
* The SHA-256 hash, in raw bytes, associated with the data that was saved.
*
* In case the enableAppend method has been called, the hash computation
* includes the contents of the existing file, if any.
*
* @throws NS_ERROR_NOT_AVAILABLE
* In case the enableSha256 method has not been called, or before the
* onSaveComplete method has been called to notify success.
*/
readonly attribute ACString sha256Hash;
/**
* Instructs the component to append data to the initial target file, that
* will be specified by the first call to the setTarget method, instead of
* overwriting the file.
*
* If the initial target file does not exist, this method has no effect.
*
* @remarks This must be set on the main thread before the first call to
* setTarget.
*/
void enableAppend();
/**
* Sets the name of the output file to be written. The target can be changed
* after data has already been fed, in which case the existing file will be
* moved to the new destination.
*
* In case the specified file already exists, and this method is called for
* the first time, the file may be either overwritten or appended to, based on
* whether the enableAppend method was called. Subsequent calls always
* overwrite the specified target file with the previously saved data.
*
* No file will be written until this function is called at least once. It's
* recommended not to feed any data until the output file is set.
@ -132,7 +153,7 @@ interface nsIBackgroundFileSaverObserver : nsISupports
* @param aSaver
* Reference to the object that raised the notification.
* @param aStatus
* Result code that determines whether the operation succeded or
* Result code that determines whether the operation succeeded or
* failed, as well as the failure reason.
*/
void onSaveComplete(in nsIBackgroundFileSaver aSaver, in nsresult aStatus);

View File

@ -25,9 +25,9 @@ namespace net {
//// Globals
/**
* Buffer size for writing to the output file.
* Buffer size for writing to the output file or reading from the input file.
*/
#define BUFFERED_OUTPUT_SIZE (1024 * 32)
#define BUFFERED_IO_SIZE (1024 * 32)
/**
* When this upper limit is reached, the original request is suspended.
@ -82,6 +82,7 @@ BackgroundFileSaver::BackgroundFileSaver()
, mFinishRequested(false)
, mComplete(false)
, mStatus(NS_OK)
, mAppend(false)
, mAssignedTarget(nullptr)
, mAssignedTargetKeepPartial(false)
, mAsyncCopyContext(nullptr)
@ -162,6 +163,18 @@ BackgroundFileSaver::SetObserver(nsIBackgroundFileSaverObserver *aObserver)
return NS_OK;
}
// Called on the control thread.
NS_IMETHODIMP
BackgroundFileSaver::EnableAppend()
{
MOZ_ASSERT(NS_IsMainThread(), "This should be called on the main thread");
MutexAutoLock lock(mLock);
mAppend = true;
return NS_OK;
}
// Called on the control thread.
NS_IMETHODIMP
BackgroundFileSaver::SetTarget(nsIFile *aTarget, bool aKeepPartial)
@ -364,16 +377,17 @@ BackgroundFileSaver::ProcessStateChange()
nsCOMPtr<nsIFile> target;
bool targetKeepPartial;
bool sha256Enabled = false;
bool append = false;
{
MutexAutoLock lock(mLock);
target = mAssignedTarget;
targetKeepPartial = mAssignedTargetKeepPartial;
sha256Enabled = mSha256Enabled;
append = mAppend;
// From now on, another attention event needs to be posted if state changes.
mWorkerThreadAttentionRequested = false;
sha256Enabled = mSha256Enabled;
}
// The target can only be null if it has never been assigned. In this case,
@ -382,13 +396,20 @@ BackgroundFileSaver::ProcessStateChange()
return NS_OK;
}
// We will append to the target file only if we already started writing it.
bool equalToCurrent = false;
int32_t creationIoFlags = PR_CREATE_FILE | PR_TRUNCATE;
if (mActualTarget) {
creationIoFlags = PR_APPEND;
bool isContinuation = !!mActualTarget;
// Verify whether we have actually been instructed to use a different file.
// We will append to the initial target file only if it was requested by the
// caller, but we'll always append on subsequent accesses to the target file.
int32_t creationIoFlags;
if (isContinuation) {
creationIoFlags = PR_APPEND;
} else {
creationIoFlags = (append ? PR_APPEND : PR_TRUNCATE) | PR_CREATE_FILE;
}
// Verify whether we have actually been instructed to use a different file.
bool equalToCurrent = false;
if (isContinuation) {
rv = mActualTarget->Equals(target, &equalToCurrent);
NS_ENSURE_SUCCESS(rv, rv);
if (!equalToCurrent)
@ -440,18 +461,20 @@ BackgroundFileSaver::ProcessStateChange()
NS_ENSURE_SUCCESS(rv, rv);
}
// The pending rename operation might be the last task before finishing.
if (CheckCompletion()) {
return NS_OK;
}
if (isContinuation) {
// The pending rename operation might be the last task before finishing. We
// may return here only if we have already created the target file.
if (CheckCompletion()) {
return NS_OK;
}
// Even if the operation did not complete, the pipe input stream may be empty
// and may have been closed already. We detect this case using the Available
// property, because it never returns an error if there is more data to be
// consumed. If the pipe input stream is closed, we just exit and wait for
// more calls like SetTarget or Finish to be invoked on the control thread.
// However, we still truncate the file if we are expected to do that.
if (creationIoFlags == PR_APPEND) {
// Even if the operation did not complete, the pipe input stream may be
// empty and may have been closed already. We detect this case using the
// Available property, because it never returns an error if there is more
// data to be consumed. If the pipe input stream is closed, we just exit
// and wait for more calls like SetTarget or Finish to be invoked on the
// control thread. However, we still truncate the file or create the
// initial digest context if we are expected to do that.
uint64_t available;
rv = mPipeInputStream->Available(&available);
if (NS_FAILED(rv)) {
@ -459,6 +482,53 @@ BackgroundFileSaver::ProcessStateChange()
}
}
// Create the digest context if requested and NSS hasn't been shut down.
if (sha256Enabled && !mDigestContext) {
nsNSSShutDownPreventionLock lock;
if (!isAlreadyShutDown()) {
mDigestContext =
PK11_CreateDigestContext(static_cast<SECOidTag>(SEC_OID_SHA256));
NS_ENSURE_TRUE(mDigestContext, NS_ERROR_OUT_OF_MEMORY);
}
}
// When we are requested to append to an existing file, we should read the
// existing data and ensure we include it as part of the final hash.
if (append && !isContinuation) {
nsCOMPtr<nsIInputStream> inputStream;
rv = NS_NewLocalFileInputStream(getter_AddRefs(inputStream),
mActualTarget,
PR_RDONLY | nsIFile::OS_READAHEAD);
if (rv != NS_ERROR_FILE_NOT_FOUND) {
NS_ENSURE_SUCCESS(rv, rv);
char buffer[BUFFERED_IO_SIZE];
while (true) {
uint32_t count;
rv = inputStream->Read(buffer, BUFFERED_IO_SIZE, &count);
NS_ENSURE_SUCCESS(rv, rv);
if (count == 0) {
// We reached the end of the file.
break;
}
nsNSSShutDownPreventionLock lock;
if (isAlreadyShutDown()) {
return NS_ERROR_NOT_AVAILABLE;
}
nsresult rv = MapSECStatus(PK11_DigestOp(mDigestContext,
uint8_t_ptr_cast(buffer),
count));
NS_ENSURE_SUCCESS(rv, rv);
}
rv = inputStream->Close();
NS_ENSURE_SUCCESS(rv, rv);
}
}
// Create the target file, or append to it if we already started writing it.
nsCOMPtr<nsIOutputStream> outputStream;
rv = NS_NewLocalFileOutputStream(getter_AddRefs(outputStream),
@ -466,24 +536,12 @@ BackgroundFileSaver::ProcessStateChange()
PR_WRONLY | creationIoFlags, 0600);
NS_ENSURE_SUCCESS(rv, rv);
outputStream = NS_BufferOutputStream(outputStream, BUFFERED_OUTPUT_SIZE);
outputStream = NS_BufferOutputStream(outputStream, BUFFERED_IO_SIZE);
if (!outputStream) {
return NS_ERROR_FAILURE;
}
// Wrap the output stream in a hashing stream if hashing is enabled and NSS
// hasn't been shut down.
bool isShutDown = false;
if (sha256Enabled && !mDigestContext) {
nsNSSShutDownPreventionLock lock;
if (!(isShutDown = isAlreadyShutDown())) {
mDigestContext =
PK11_CreateDigestContext(static_cast<SECOidTag>(SEC_OID_SHA256));
NS_ENSURE_TRUE(mDigestContext, NS_ERROR_OUT_OF_MEMORY);
}
}
MOZ_ASSERT(!sha256Enabled || mDigestContext || isShutDown,
"Hashing enabled but creating digest context didn't work");
// Wrap the output stream so that it feeds the digest context if needed.
if (mDigestContext) {
// No need to acquire the NSS lock here, DigestOutputStream must acquire it
// in any case before each asynchronous write. Constructing the
@ -511,7 +569,7 @@ BackgroundFileSaver::ProcessStateChange()
}
}
// If the operation succeded, we must ensure that we keep this object alive
// If the operation succeeded, we must ensure that we keep this object alive
// for the entire duration of the copy, since only the raw pointer will be
// provided as the argument of the AsyncCopyCallback function. We can add the
// reference now, after NS_AsyncCopy returned, because it always starts

View File

@ -154,6 +154,12 @@ private:
*/
nsresult mStatus;
/**
* True if we should append data to the initial target file, instead of
* overwriting it.
*/
bool mAppend;
/**
* Set by the control thread to the target file name that will be used by the
* worker thread, as soon as it is possible to update mActualTarget and open

View File

@ -45,13 +45,17 @@ const TEST_FILE_NAME_1 = "test-backgroundfilesaver-1.txt";
const TEST_FILE_NAME_2 = "test-backgroundfilesaver-2.txt";
const TEST_FILE_NAME_3 = "test-backgroundfilesaver-3.txt";
// A map of test data length to the expected hash
// A map of test data length to the expected SHA-256 hashes
const EXPECTED_HASHES = {
// SHA-256 hash of TEST_DATA_SHORT
// No data
0 : "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855",
// TEST_DATA_SHORT
40 : "f37176b690e8744ee990a206c086cba54d1502aa2456c3b0c84ef6345d72a192",
// SHA-256 hash of TEST_DATA_SHORT + TEST_DATA_SHORT
// TEST_DATA_SHORT + TEST_DATA_SHORT
80 : "780c0e91f50bb7ec922cc11e16859e6d5df283c0d9470f61772e3d79f41eeb58",
// SHA-256 hash of a bunch of dashes
// TEST_DATA_LONG
8388608 : "e3611a47714c42bdf326acfb2eb6ed9fa4cca65cb7d7be55217770a5bf5e7ff0",
// TEST_DATA_LONG + TEST_DATA_LONG
16777216 : "03a0db69a30140f307587ee746a539247c181bafd85b85c8516a3533c7d9ea1d"
};
@ -408,18 +412,18 @@ add_task(function test_setTarget_after_close_stream()
let saver = new BackgroundFileSaverOutputStream();
saver.enableSha256();
let completionPromise = promiseSaverComplete(saver);
// Copy some data to the output stream of the file saver. This data must
// be shorter than the internal component's pipe buffer for the test to
// succeed, because otherwise the test would block waiting for the write to
// complete.
yield promiseCopyToSaver(TEST_DATA_SHORT, saver, true);
// Set the target file and wait for the output to finish.
saver.setTarget(destFile, false);
saver.finish(Cr.NS_OK);
yield completionPromise;
// Verify results.
yield promiseVerifyContents(destFile, TEST_DATA_SHORT);
do_check_eq(EXPECTED_HASHES[TEST_DATA_SHORT.length],
@ -455,6 +459,38 @@ add_task(function test_setTarget_multiple()
destFile.remove(false);
});
add_task(function test_enableAppend_hash()
{
// This test checks append mode, also verifying that the computed hash
// includes the contents of the existing data.
let destFile = getTempFile(TEST_FILE_NAME_1);
// Test the case where the file does not already exists first, then the case
// where the file already exists.
for (let i = 0; i < 2; i++) {
let saver = new BackgroundFileSaverOutputStream();
saver.enableAppend();
saver.enableSha256();
let completionPromise = promiseSaverComplete(saver);
saver.setTarget(destFile, false);
yield promiseCopyToSaver(TEST_DATA_LONG, saver, true);
saver.finish(Cr.NS_OK);
yield completionPromise;
// Verify results.
let expectedContents = (i == 0 ? TEST_DATA_LONG
: TEST_DATA_LONG + TEST_DATA_LONG);
yield promiseVerifyContents(destFile, expectedContents);
do_check_eq(EXPECTED_HASHES[expectedContents.length],
toHex(saver.sha256Hash));
}
// Clean up.
destFile.remove(false);
});
add_task(function test_finish_only()
{
// This test checks creating the object and doing nothing.
@ -468,6 +504,57 @@ add_task(function test_finish_only()
yield completionPromise;
});
add_task(function test_empty()
{
// This test checks we still create an empty file when no data is fed.
let destFile = getTempFile(TEST_FILE_NAME_1);
let saver = new BackgroundFileSaverOutputStream();
let completionPromise = promiseSaverComplete(saver);
saver.setTarget(destFile, false);
yield promiseCopyToSaver("", saver, true);
saver.finish(Cr.NS_OK);
yield completionPromise;
// Verify results.
do_check_true(destFile.exists());
do_check_eq(destFile.fileSize, 0);
// Clean up.
destFile.remove(false);
});
add_task(function test_empty_hash()
{
// This test checks the hash of an empty file, both in normal and append mode.
let destFile = getTempFile(TEST_FILE_NAME_1);
// Test normal mode first, then append mode.
for (let i = 0; i < 2; i++) {
let saver = new BackgroundFileSaverOutputStream();
if (i == 1) {
saver.enableAppend();
}
saver.enableSha256();
let completionPromise = promiseSaverComplete(saver);
saver.setTarget(destFile, false);
yield promiseCopyToSaver("", saver, true);
saver.finish(Cr.NS_OK);
yield completionPromise;
// Verify results.
do_check_eq(destFile.fileSize, 0);
do_check_eq(EXPECTED_HASHES[0], toHex(saver.sha256Hash));
}
// Clean up.
destFile.remove(false);
});
add_task(function test_invalid_hash()
{
let saver = new BackgroundFileSaverStreamListener();