| /* |
| * 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(BLOB) |
| |
| #include "BlobResourceHandle.h" |
| |
| #include "AsyncFileStream.h" |
| #include "BlobStorageData.h" |
| #include "FileStream.h" |
| #include "FileSystem.h" |
| #include "HTTPParsers.h" |
| #include "URL.h" |
| #include "ResourceError.h" |
| #include "ResourceHandleClient.h" |
| #include "ResourceRequest.h" |
| #include "ResourceResponse.h" |
| #include "SharedBuffer.h" |
| #include <wtf/MainThread.h> |
| #include <wtf/Ref.h> |
| |
| namespace WebCore { |
| |
| static const unsigned bufferSize = 512 * 1024; |
| static const long long positionNotSpecified = -1; |
| |
| static const int httpOK = 200; |
| static const int httpPartialContent = 206; |
| static const int httpNotAllowed = 403; |
| static const int httpNotFound = 404; |
| static const int httpRequestedRangeNotSatisfiable = 416; |
| static const int httpInternalError = 500; |
| static const char* httpOKText = "OK"; |
| static const char* httpPartialContentText = "Partial Content"; |
| static const char* httpNotAllowedText = "Not Allowed"; |
| static const char* httpNotFoundText = "Not Found"; |
| static const char* httpRequestedRangeNotSatisfiableText = "Requested Range Not Satisfiable"; |
| static const char* httpInternalErrorText = "Internal Server Error"; |
| |
| static const char* const webKitBlobResourceDomain = "WebKitBlobResource"; |
| enum { |
| notFoundError = 1, |
| securityError = 2, |
| rangeError = 3, |
| notReadableError = 4, |
| methodNotAllowed = 5 |
| }; |
| |
| /////////////////////////////////////////////////////////////////////////////// |
| // BlobResourceSynchronousLoader |
| |
| namespace { |
| |
| class BlobResourceSynchronousLoader : public ResourceHandleClient { |
| public: |
| BlobResourceSynchronousLoader(ResourceError&, ResourceResponse&, Vector<char>&); |
| |
| virtual void didReceiveResponse(ResourceHandle*, const ResourceResponse&) override; |
| virtual void didReceiveData(ResourceHandle*, const char*, unsigned, int /*encodedDataLength*/) override; |
| virtual void didFinishLoading(ResourceHandle*, double /*finishTime*/) override; |
| virtual void didFail(ResourceHandle*, const ResourceError&) override; |
| |
| private: |
| ResourceError& m_error; |
| ResourceResponse& m_response; |
| Vector<char>& m_data; |
| }; |
| |
| BlobResourceSynchronousLoader::BlobResourceSynchronousLoader(ResourceError& error, ResourceResponse& response, Vector<char>& data) |
| : m_error(error) |
| , m_response(response) |
| , m_data(data) |
| { |
| } |
| |
| void BlobResourceSynchronousLoader::didReceiveResponse(ResourceHandle* handle, const ResourceResponse& response) |
| { |
| // We cannot handle the size that is more than maximum integer. |
| if (response.expectedContentLength() > INT_MAX) { |
| m_error = ResourceError(webKitBlobResourceDomain, notReadableError, response.url(), "File is too large"); |
| return; |
| } |
| |
| m_response = response; |
| |
| // Read all the data. |
| m_data.resize(static_cast<size_t>(response.expectedContentLength())); |
| static_cast<BlobResourceHandle*>(handle)->readSync(m_data.data(), static_cast<int>(m_data.size())); |
| } |
| |
| void BlobResourceSynchronousLoader::didReceiveData(ResourceHandle*, const char*, unsigned, int) |
| { |
| } |
| |
| void BlobResourceSynchronousLoader::didFinishLoading(ResourceHandle*, double) |
| { |
| } |
| |
| void BlobResourceSynchronousLoader::didFail(ResourceHandle*, const ResourceError& error) |
| { |
| m_error = error; |
| } |
| |
| } |
| |
| /////////////////////////////////////////////////////////////////////////////// |
| // BlobResourceHandle |
| |
| PassRefPtr<BlobResourceHandle> BlobResourceHandle::createAsync(BlobStorageData* blobData, const ResourceRequest& request, ResourceHandleClient* client) |
| { |
| // FIXME: Should probably call didFail() instead of blocking the load without explanation. |
| if (!equalIgnoringCase(request.httpMethod(), "GET")) |
| return 0; |
| |
| return adoptRef(new BlobResourceHandle(blobData, request, client, true)); |
| } |
| |
| void BlobResourceHandle::loadResourceSynchronously(BlobStorageData* blobData, const ResourceRequest& request, ResourceError& error, ResourceResponse& response, Vector<char>& data) |
| { |
| if (!equalIgnoringCase(request.httpMethod(), "GET")) { |
| error = ResourceError(webKitBlobResourceDomain, methodNotAllowed, response.url(), "Request method must be GET"); |
| return; |
| } |
| |
| BlobResourceSynchronousLoader loader(error, response, data); |
| RefPtr<BlobResourceHandle> handle = adoptRef(new BlobResourceHandle(blobData, request, &loader, false)); |
| handle->start(); |
| } |
| |
| BlobResourceHandle::BlobResourceHandle(PassRefPtr<BlobStorageData> blobData, const ResourceRequest& request, ResourceHandleClient* client, bool async) |
| : ResourceHandle(0, request, client, false, false) |
| , m_blobData(blobData) |
| , m_async(async) |
| , m_errorCode(0) |
| , m_aborted(false) |
| , m_rangeOffset(positionNotSpecified) |
| , m_rangeEnd(positionNotSpecified) |
| , m_rangeSuffixLength(positionNotSpecified) |
| , m_totalRemainingSize(0) |
| , m_currentItemReadSize(0) |
| , m_sizeItemCount(0) |
| , m_readItemCount(0) |
| , m_fileOpened(false) |
| { |
| if (m_async) |
| m_asyncStream = AsyncFileStream::create(this); |
| else |
| m_stream = FileStream::create(); |
| } |
| |
| BlobResourceHandle::~BlobResourceHandle() |
| { |
| if (m_async) { |
| if (m_asyncStream) |
| m_asyncStream->stop(); |
| } else { |
| if (m_stream) |
| m_stream->stop(); |
| } |
| } |
| |
| void BlobResourceHandle::cancel() |
| { |
| if (m_async) { |
| if (m_asyncStream) { |
| m_asyncStream->stop(); |
| m_asyncStream = 0; |
| } |
| } |
| |
| m_aborted = true; |
| |
| ResourceHandle::cancel(); |
| } |
| |
| void delayedStartBlobResourceHandle(void* context) |
| { |
| RefPtr<BlobResourceHandle> handle = adoptRef(static_cast<BlobResourceHandle*>(context)); |
| handle->doStart(); |
| } |
| |
| void BlobResourceHandle::start() |
| { |
| if (m_async) { |
| // Keep BlobResourceHandle alive until delayedStartBlobResourceHandle runs. |
| ref(); |
| |
| // Finish this async call quickly and return. |
| callOnMainThread(delayedStartBlobResourceHandle, this); |
| return; |
| } |
| |
| doStart(); |
| } |
| |
| void BlobResourceHandle::doStart() |
| { |
| // Do not continue if the request is aborted or an error occurs. |
| if (m_aborted || m_errorCode) |
| return; |
| |
| // If the blob data is not found, fail now. |
| if (!m_blobData) { |
| m_errorCode = notFoundError; |
| notifyResponse(); |
| return; |
| } |
| |
| // Parse the "Range" header we care about. |
| String range = firstRequest().httpHeaderField("Range"); |
| if (!range.isEmpty() && !parseRange(range, m_rangeOffset, m_rangeEnd, m_rangeSuffixLength)) { |
| m_errorCode = rangeError; |
| notifyResponse(); |
| return; |
| } |
| |
| if (m_async) |
| getSizeForNext(); |
| else { |
| Ref<BlobResourceHandle> protect(*this); // getSizeForNext calls the client |
| for (size_t i = 0; i < m_blobData->items().size() && !m_aborted && !m_errorCode; ++i) |
| getSizeForNext(); |
| notifyResponse(); |
| } |
| } |
| |
| void BlobResourceHandle::getSizeForNext() |
| { |
| // Do we finish validating and counting size for all items? |
| if (m_sizeItemCount >= m_blobData->items().size()) { |
| seek(); |
| |
| // Start reading if in asynchronous mode. |
| if (m_async) { |
| Ref<BlobResourceHandle> protect(*this); |
| notifyResponse(); |
| m_buffer.resize(bufferSize); |
| readAsync(); |
| } |
| return; |
| } |
| |
| const BlobDataItem& item = m_blobData->items().at(m_sizeItemCount); |
| switch (item.type) { |
| case BlobDataItem::Data: |
| didGetSize(item.length); |
| break; |
| case BlobDataItem::File: |
| if (m_async) |
| m_asyncStream->getSize(item.path, item.expectedModificationTime); |
| else |
| didGetSize(m_stream->getSize(item.path, item.expectedModificationTime)); |
| break; |
| default: |
| ASSERT_NOT_REACHED(); |
| } |
| } |
| |
| void BlobResourceHandle::didGetSize(long long size) |
| { |
| // Do not continue if the request is aborted or an error occurs. |
| if (m_aborted || m_errorCode) |
| return; |
| |
| // If the size is -1, it means the file has been moved or changed. Fail now. |
| if (size == -1) { |
| m_errorCode = notFoundError; |
| notifyResponse(); |
| return; |
| } |
| |
| // The size passed back is the size of the whole file. If the underlying item is a sliced file, we need to use the slice length. |
| const BlobDataItem& item = m_blobData->items().at(m_sizeItemCount); |
| if (item.type == BlobDataItem::File && item.length != BlobDataItem::toEndOfFile) |
| size = item.length; |
| |
| // Cache the size. |
| m_itemLengthList.append(size); |
| |
| // Count the size. |
| m_totalRemainingSize += size; |
| m_sizeItemCount++; |
| |
| // Continue with the next item. |
| getSizeForNext(); |
| } |
| |
| void BlobResourceHandle::seek() |
| { |
| // Convert from the suffix length to the range. |
| if (m_rangeSuffixLength != positionNotSpecified) { |
| m_rangeOffset = m_totalRemainingSize - m_rangeSuffixLength; |
| m_rangeEnd = m_rangeOffset + m_rangeSuffixLength - 1; |
| } |
| |
| // Bail out if the range is not provided. |
| if (m_rangeOffset == positionNotSpecified) |
| return; |
| |
| // Skip the initial items that are not in the range. |
| long long offset = m_rangeOffset; |
| for (m_readItemCount = 0; m_readItemCount < m_blobData->items().size() && offset >= m_itemLengthList[m_readItemCount]; ++m_readItemCount) |
| offset -= m_itemLengthList[m_readItemCount]; |
| |
| // Set the offset that need to jump to for the first item in the range. |
| m_currentItemReadSize = offset; |
| |
| // Adjust the total remaining size in order not to go beyond the range. |
| if (m_rangeEnd != positionNotSpecified) { |
| long long rangeSize = m_rangeEnd - m_rangeOffset + 1; |
| if (m_totalRemainingSize > rangeSize) |
| m_totalRemainingSize = rangeSize; |
| } else |
| m_totalRemainingSize -= m_rangeOffset; |
| } |
| |
| int BlobResourceHandle::readSync(char* buf, int length) |
| { |
| ASSERT(!m_async); |
| Ref<BlobResourceHandle> protect(*this); |
| |
| int offset = 0; |
| int remaining = length; |
| while (remaining) { |
| // Do not continue if the request is aborted or an error occurs. |
| if (m_aborted || m_errorCode) |
| break; |
| |
| // If there is no more remaining data to read, we are done. |
| if (!m_totalRemainingSize || m_readItemCount >= m_blobData->items().size()) |
| break; |
| |
| const BlobDataItem& item = m_blobData->items().at(m_readItemCount); |
| int bytesRead = 0; |
| if (item.type == BlobDataItem::Data) |
| bytesRead = readDataSync(item, buf + offset, remaining); |
| else if (item.type == BlobDataItem::File) |
| bytesRead = readFileSync(item, buf + offset, remaining); |
| else |
| ASSERT_NOT_REACHED(); |
| |
| if (bytesRead > 0) { |
| offset += bytesRead; |
| remaining -= bytesRead; |
| } |
| } |
| |
| int result; |
| if (m_aborted || m_errorCode) |
| result = -1; |
| else |
| result = length - remaining; |
| |
| if (result > 0) |
| notifyReceiveData(buf, result); |
| |
| if (!result) |
| notifyFinish(); |
| |
| return result; |
| } |
| |
| int BlobResourceHandle::readDataSync(const BlobDataItem& item, char* buf, int length) |
| { |
| ASSERT(!m_async); |
| |
| long long remaining = item.length - m_currentItemReadSize; |
| int bytesToRead = (length > remaining) ? static_cast<int>(remaining) : length; |
| if (bytesToRead > m_totalRemainingSize) |
| bytesToRead = static_cast<int>(m_totalRemainingSize); |
| memcpy(buf, item.data->data() + item.offset + m_currentItemReadSize, bytesToRead); |
| m_totalRemainingSize -= bytesToRead; |
| |
| m_currentItemReadSize += bytesToRead; |
| if (m_currentItemReadSize == item.length) { |
| m_readItemCount++; |
| m_currentItemReadSize = 0; |
| } |
| |
| return bytesToRead; |
| } |
| |
| int BlobResourceHandle::readFileSync(const BlobDataItem& item, char* buf, int length) |
| { |
| ASSERT(!m_async); |
| |
| if (!m_fileOpened) { |
| long long bytesToRead = m_itemLengthList[m_readItemCount] - m_currentItemReadSize; |
| if (bytesToRead > m_totalRemainingSize) |
| bytesToRead = m_totalRemainingSize; |
| bool success = m_stream->openForRead(item.path, item.offset + m_currentItemReadSize, bytesToRead); |
| m_currentItemReadSize = 0; |
| if (!success) { |
| m_errorCode = notReadableError; |
| return 0; |
| } |
| |
| m_fileOpened = true; |
| } |
| |
| int bytesRead = m_stream->read(buf, length); |
| if (bytesRead < 0) { |
| m_errorCode = notReadableError; |
| return 0; |
| } |
| if (!bytesRead) { |
| m_stream->close(); |
| m_fileOpened = false; |
| m_readItemCount++; |
| } else |
| m_totalRemainingSize -= bytesRead; |
| |
| return bytesRead; |
| } |
| |
| void BlobResourceHandle::readAsync() |
| { |
| ASSERT(m_async); |
| |
| // Do not continue if the request is aborted or an error occurs. |
| if (m_aborted || m_errorCode) |
| return; |
| |
| // If there is no more remaining data to read, we are done. |
| if (!m_totalRemainingSize || m_readItemCount >= m_blobData->items().size()) { |
| notifyFinish(); |
| return; |
| } |
| |
| const BlobDataItem& item = m_blobData->items().at(m_readItemCount); |
| if (item.type == BlobDataItem::Data) |
| readDataAsync(item); |
| else if (item.type == BlobDataItem::File) |
| readFileAsync(item); |
| else |
| ASSERT_NOT_REACHED(); |
| } |
| |
| void BlobResourceHandle::readDataAsync(const BlobDataItem& item) |
| { |
| ASSERT(m_async); |
| Ref<BlobResourceHandle> protect(*this); |
| |
| long long bytesToRead = item.length - m_currentItemReadSize; |
| if (bytesToRead > m_totalRemainingSize) |
| bytesToRead = m_totalRemainingSize; |
| consumeData(item.data->data() + item.offset + m_currentItemReadSize, static_cast<int>(bytesToRead)); |
| m_currentItemReadSize = 0; |
| } |
| |
| void BlobResourceHandle::readFileAsync(const BlobDataItem& item) |
| { |
| ASSERT(m_async); |
| |
| if (m_fileOpened) { |
| m_asyncStream->read(m_buffer.data(), m_buffer.size()); |
| return; |
| } |
| |
| long long bytesToRead = m_itemLengthList[m_readItemCount] - m_currentItemReadSize; |
| if (bytesToRead > m_totalRemainingSize) |
| bytesToRead = static_cast<int>(m_totalRemainingSize); |
| m_asyncStream->openForRead(item.path, item.offset + m_currentItemReadSize, bytesToRead); |
| m_fileOpened = true; |
| m_currentItemReadSize = 0; |
| } |
| |
| void BlobResourceHandle::didOpen(bool success) |
| { |
| ASSERT(m_async); |
| |
| if (!success) { |
| failed(notReadableError); |
| return; |
| } |
| |
| // Continue the reading. |
| readAsync(); |
| } |
| |
| void BlobResourceHandle::didRead(int bytesRead) |
| { |
| if (bytesRead < 0) { |
| failed(notReadableError); |
| return; |
| } |
| |
| consumeData(m_buffer.data(), bytesRead); |
| } |
| |
| void BlobResourceHandle::consumeData(const char* data, int bytesRead) |
| { |
| ASSERT(m_async); |
| Ref<BlobResourceHandle> protect(*this); |
| |
| m_totalRemainingSize -= bytesRead; |
| |
| // Notify the client. |
| if (bytesRead) |
| notifyReceiveData(data, bytesRead); |
| |
| if (m_fileOpened) { |
| // When the current item is a file item, the reading is completed only if bytesRead is 0. |
| if (!bytesRead) { |
| // Close the file. |
| m_fileOpened = false; |
| m_asyncStream->close(); |
| |
| // Move to the next item. |
| m_readItemCount++; |
| } |
| } else { |
| // Otherwise, we read the current text item as a whole and move to the next item. |
| m_readItemCount++; |
| } |
| |
| // Continue the reading. |
| readAsync(); |
| } |
| |
| void BlobResourceHandle::failed(int errorCode) |
| { |
| ASSERT(m_async); |
| Ref<BlobResourceHandle> protect(*this); |
| |
| // Notify the client. |
| notifyFail(errorCode); |
| |
| // Close the file if needed. |
| if (m_fileOpened) { |
| m_fileOpened = false; |
| m_asyncStream->close(); |
| } |
| } |
| |
| void BlobResourceHandle::notifyResponse() |
| { |
| if (!client()) |
| return; |
| |
| if (m_errorCode) { |
| Ref<BlobResourceHandle> protect(*this); |
| notifyResponseOnError(); |
| notifyFinish(); |
| } else |
| notifyResponseOnSuccess(); |
| } |
| |
| void BlobResourceHandle::notifyResponseOnSuccess() |
| { |
| bool isRangeRequest = m_rangeOffset != positionNotSpecified; |
| ResourceResponse response(firstRequest().url(), m_blobData->contentType(), m_totalRemainingSize, String(), String()); |
| response.setExpectedContentLength(m_totalRemainingSize); |
| response.setHTTPStatusCode(isRangeRequest ? httpPartialContent : httpOK); |
| response.setHTTPStatusText(isRangeRequest ? httpPartialContentText : httpOKText); |
| if (!m_blobData->contentDisposition().isEmpty()) |
| response.setHTTPHeaderField("Content-Disposition", m_blobData->contentDisposition()); |
| |
| // BlobResourceHandle cannot be used with downloading, and doesn't even wait for continueDidReceiveResponse. |
| // It's currently client's responsibility to know that didReceiveResponseAsync cannot be used to convert a |
| // load into a download or blobs. |
| if (client()->usesAsyncCallbacks()) |
| client()->didReceiveResponseAsync(this, response); |
| else |
| client()->didReceiveResponse(this, response); |
| } |
| |
| void BlobResourceHandle::notifyResponseOnError() |
| { |
| ASSERT(m_errorCode); |
| |
| ResourceResponse response(firstRequest().url(), "text/plain", 0, String(), String()); |
| switch (m_errorCode) { |
| case rangeError: |
| response.setHTTPStatusCode(httpRequestedRangeNotSatisfiable); |
| response.setHTTPStatusText(httpRequestedRangeNotSatisfiableText); |
| break; |
| case notFoundError: |
| response.setHTTPStatusCode(httpNotFound); |
| response.setHTTPStatusText(httpNotFoundText); |
| break; |
| case securityError: |
| response.setHTTPStatusCode(httpNotAllowed); |
| response.setHTTPStatusText(httpNotAllowedText); |
| break; |
| default: |
| response.setHTTPStatusCode(httpInternalError); |
| response.setHTTPStatusText(httpInternalErrorText); |
| break; |
| } |
| |
| // Note that we don't wait for continueDidReceiveResponse when using didReceiveResponseAsync. |
| // This is not formally correct, but the client has to be a no-op anyway, because blobs can't be downloaded. |
| if (client()->usesAsyncCallbacks()) |
| client()->didReceiveResponseAsync(this, response); |
| else |
| client()->didReceiveResponse(this, response); |
| } |
| |
| void BlobResourceHandle::notifyReceiveData(const char* data, int bytesRead) |
| { |
| if (client()) |
| client()->didReceiveBuffer(this, SharedBuffer::create(data, bytesRead), bytesRead); |
| } |
| |
| void BlobResourceHandle::notifyFail(int errorCode) |
| { |
| if (client()) |
| client()->didFail(this, ResourceError(webKitBlobResourceDomain, errorCode, firstRequest().url(), String())); |
| } |
| |
| static void doNotifyFinish(void* context) |
| { |
| BlobResourceHandle* handle = static_cast<BlobResourceHandle*>(context); |
| if (!handle->aborted() && handle->client()) |
| handle->client()->didFinishLoading(handle, 0); |
| |
| // Balance the ref() in BlobResourceHandle::notfyFinish(). |
| handle->deref(); |
| } |
| |
| void BlobResourceHandle::notifyFinish() |
| { |
| // Balanced in doNotifyFinish(). |
| ref(); |
| |
| if (m_async) { |
| // Schedule to notify the client from a standalone function because the client might dispose the handle immediately from the callback function |
| // while we still have BlobResourceHandle calls in the stack. |
| callOnMainThread(doNotifyFinish, this); |
| return; |
| } |
| |
| doNotifyFinish(this); |
| } |
| |
| } // namespace WebCore |
| |
| #endif // ENABLE(BLOB) |