/*
* Copyright (C) 2010 Google Inc. All rights reserved.
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions are
* met:
*
* * Redistributions of source code must retain the above copyright
* notice, this list of conditions and the following disclaimer.
* * Redistributions in binary form must reproduce the above
* copyright notice, this list of conditions and the following disclaimer
* in the documentation and/or other materials provided with the
* distribution.
* * Neither the name of Google Inc. nor the names of its
* contributors may be used to endorse or promote products derived from
* this software without specific prior written permission.
*
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
* "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
* LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
* A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
* OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
* SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
* LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
* DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
* THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
* (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
* OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*/
#include "config.h"
#if ENABLE(FILE_READER)
#include "FileReader.h"
#include "Base64.h"
#include "Blob.h"
#include "File.h"
#include "FileStreamProxy.h"
#include "Logging.h"
#include "ProgressEvent.h"
#include "ScriptExecutionContext.h"
#include "TextResourceDecoder.h"
#include <wtf/CurrentTime.h>
namespace WebCore {
const unsigned bufferSize = 1024;
const double progressNotificationIntervalMS = 50;
FileReader::FileReader(ScriptExecutionContext* context)
: ActiveDOMObject(context, this)
, m_state(None)
, m_readType(ReadFileAsBinaryString)
, m_result("")
, m_isRawDataConverted(false)
, m_bytesLoaded(0)
, m_totalBytes(0)
, m_lastProgressNotificationTimeMS(0)
{
m_buffer.resize(bufferSize);
}
FileReader::~FileReader()
{
terminate();
}
bool FileReader::hasPendingActivity() const
{
return (m_state != None && m_state != Completed) || ActiveDOMObject::hasPendingActivity();
}
bool FileReader::canSuspend() const
{
// FIXME: It is not currently possible to suspend a FileReader, so pages with FileReader can not go into page cache.
return false;
}
void FileReader::stop()
{
terminate();
}
void FileReader::readAsBinaryString(Blob* fileBlob)
{
if (!fileBlob)
return;
// FIXME: needs to handle non-file blobs.
LOG(FileAPI, "FileReader: reading as binary: %s\n", fileBlob->path().utf8().data());
readInternal(fileBlob, ReadFileAsBinaryString);
}
void FileReader::readAsText(Blob* fileBlob, const String& encoding)
{
if (!fileBlob)
return;
// FIXME: needs to handle non-file blobs.
LOG(FileAPI, "FileReader: reading as text: %s\n", fileBlob->path().utf8().data());
if (!encoding.isEmpty())
m_encoding = TextEncoding(encoding);
readInternal(fileBlob, ReadFileAsText);
}
void FileReader::readAsDataURL(File* file)
{
if (!file)
return;
LOG(FileAPI, "FileReader: reading as data URL: %s\n", file->path().utf8().data());
m_fileType = file->type();
readInternal(file, ReadFileAsDataURL);
}
void FileReader::readInternal(Blob* fileBlob, ReadType type)
{
// readAs*** methods() can be called multiple times. Only the last call before the actual reading happens is processed.
if (m_state != None && m_state != Starting)
return;
m_fileBlob = fileBlob;
m_readType = type;
m_state = Starting;
// When FileStreamProxy is created, FileReader::didStart() will get notified on the File thread and we will start
// opening and reading the file since then.
if (!m_streamProxy.get())
m_streamProxy = FileStreamProxy::create(scriptExecutionContext(), this);
}
void FileReader::abort()
{
LOG(FileAPI, "FileReader: aborting\n");
terminate();
m_result = "";
m_error = FileError::create(ABORT_ERR);
fireEvent(eventNames().errorEvent);
fireEvent(eventNames().abortEvent);
fireEvent(eventNames().loadendEvent);
}
void FileReader::terminate()
{
if (m_streamProxy) {
m_streamProxy->stop();
m_streamProxy = 0;
}
m_state = Completed;
}
void FileReader::didStart()
{
m_state = Opening;
m_streamProxy->openForRead(m_fileBlob.get());
}
void FileReader::didGetSize(long long size)
{
m_state = Reading;
fireEvent(eventNames().loadstartEvent);
m_totalBytes = size;
m_streamProxy->read(&m_buffer.at(0), m_buffer.size());
}
void FileReader::didRead(const char* data, int bytesRead)
{
ASSERT(data && bytesRead);
// Bail out if we have aborted the reading.
if (m_state == Completed)
return;
switch (m_readType) {
case ReadFileAsBinaryString:
m_result += String(data, static_cast<unsigned>(bytesRead));
break;
case ReadFileAsText:
case ReadFileAsDataURL:
m_rawData.append(data, static_cast<unsigned>(bytesRead));
m_isRawDataConverted = false;
break;
default:
ASSERT_NOT_REACHED();
}
m_bytesLoaded += bytesRead;
// Fire the progress event at least every 50ms.
double now = WTF::currentTimeMS();
if (!m_lastProgressNotificationTimeMS)
m_lastProgressNotificationTimeMS = now;
else if (now - m_lastProgressNotificationTimeMS > progressNotificationIntervalMS) {
fireEvent(eventNames().progressEvent);
m_lastProgressNotificationTimeMS = now;
}
// Continue reading.
m_streamProxy->read(&m_buffer.at(0), m_buffer.size());
}
void FileReader::didFinish()
{
m_state = Completed;
m_streamProxy->close();
fireEvent(eventNames().loadEvent);
fireEvent(eventNames().loadendEvent);
}
void FileReader::didFail(ExceptionCode ec)
{
m_state = Completed;
m_error = FileError::create(ec);
m_streamProxy->close();
fireEvent(eventNames().errorEvent);
fireEvent(eventNames().loadendEvent);
}
void FileReader::fireEvent(const AtomicString& type)
{
// FIXME: the current ProgressEvent uses "unsigned long" for total and loaded attributes. Need to talk with the spec writer to resolve the issue.
dispatchEvent(ProgressEvent::create(type, true, static_cast<unsigned>(m_bytesLoaded), static_cast<unsigned>(m_totalBytes)));
}
FileReader::ReadyState FileReader::readyState() const
{
switch (m_state) {
case None:
case Starting:
return Empty;
case Opening:
case Reading:
return Loading;
case Completed:
return Done;
}
ASSERT_NOT_REACHED();
return Empty;
}
const ScriptString& FileReader::result()
{
// If reading as binary string, we can return the result immediately.
if (m_readType == ReadFileAsBinaryString)
return m_result;
// If we already convert the raw data received so far, we can return the result now.
if (m_isRawDataConverted)
return m_result;
m_isRawDataConverted = true;
if (m_readType == ReadFileAsText)
convertToText();
// For data URL, we only do the coversion until we receive all the raw data.
else if (m_readType == ReadFileAsDataURL && m_state == Completed)
convertToDataURL();
return m_result;
}
void FileReader::convertToText()
{
if (!m_rawData.size()) {
m_result = "";
return;
}
// Decode the data.
// The File API spec says that we should use the supplied encoding if it is valid. However, we choose to ignore this
// requirement in order to be consistent with how WebKit decodes the web content: always has the BOM override the
// provided encoding.
// FIXME: consider supporting incremental decoding to improve the perf.
if (!m_decoder)
m_decoder = TextResourceDecoder::create("text/plain", m_encoding.isValid() ? m_encoding : UTF8Encoding());
m_result = m_decoder->decode(&m_rawData.at(0), m_rawData.size());
if (m_state == Completed && !m_error)
m_result += m_decoder->flush();
}
void FileReader::convertToDataURL()
{
m_result = "data:";
if (!m_rawData.size())
return;
m_result += m_fileType;
if (!m_fileType.isEmpty())
m_result += ";";
m_result += "base64,";
Vector<char> out;
base64Encode(m_rawData, out);
out.append('\0');
m_result += out.data();
}
} // namespace WebCore
#endif // ENABLE(FILE_READER)