| Index: native_client_sdk/src/libraries/nacl_io/mount_node_http.cc
|
| diff --git a/native_client_sdk/src/libraries/nacl_io/mount_node_http.cc b/native_client_sdk/src/libraries/nacl_io/mount_node_http.cc
|
| new file mode 100644
|
| index 0000000000000000000000000000000000000000..5005ff48d30c292b81b345f80164497f10c55b49
|
| --- /dev/null
|
| +++ b/native_client_sdk/src/libraries/nacl_io/mount_node_http.cc
|
| @@ -0,0 +1,523 @@
|
| +/* Copyright (c) 2013 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 "nacl_io/mount_node_http.h"
|
| +
|
| +#include <assert.h>
|
| +#include <errno.h>
|
| +#include <stdio.h>
|
| +#include <string.h>
|
| +
|
| +#include <ppapi/c/pp_errors.h>
|
| +
|
| +#include "nacl_io/mount_http.h"
|
| +#include "nacl_io/osinttypes.h"
|
| +
|
| +#if defined(WIN32)
|
| +#define snprintf _snprintf
|
| +#endif
|
| +
|
| +namespace {
|
| +
|
| +// If we're attempting to read a partial request, but the server returns a full
|
| +// request, we need to read all of the data up to the start of our partial
|
| +// request into a dummy buffer. This is the maximum size of that buffer.
|
| +const size_t MAX_READ_BUFFER_SIZE = 64 * 1024;
|
| +const int32_t STATUSCODE_OK = 200;
|
| +const int32_t STATUSCODE_PARTIAL_CONTENT = 206;
|
| +
|
| +StringMap_t ParseHeaders(const char* headers, int32_t headers_length) {
|
| + enum State {
|
| + FINDING_KEY,
|
| + SKIPPING_WHITESPACE,
|
| + FINDING_VALUE,
|
| + };
|
| +
|
| + StringMap_t result;
|
| + std::string key;
|
| + std::string value;
|
| +
|
| + State state = FINDING_KEY;
|
| + const char* start = headers;
|
| + for (int i = 0; i < headers_length; ++i) {
|
| + switch (state) {
|
| + case FINDING_KEY:
|
| + if (headers[i] == ':') {
|
| + // Found key.
|
| + key.assign(start, &headers[i] - start);
|
| + key = NormalizeHeaderKey(key);
|
| + state = SKIPPING_WHITESPACE;
|
| + }
|
| + break;
|
| +
|
| + case SKIPPING_WHITESPACE:
|
| + if (headers[i] == ' ') {
|
| + // Found whitespace, keep going...
|
| + break;
|
| + }
|
| +
|
| + // Found a non-whitespace, mark this as the start of the value.
|
| + start = &headers[i];
|
| + state = FINDING_VALUE;
|
| + // Fallthrough to start processing value without incrementing i.
|
| +
|
| + case FINDING_VALUE:
|
| + if (headers[i] == '\n') {
|
| + // Found value.
|
| + value.assign(start, &headers[i] - start);
|
| + result[key] = value;
|
| + start = &headers[i + 1];
|
| + state = FINDING_KEY;
|
| + }
|
| + break;
|
| + }
|
| + }
|
| +
|
| + return result;
|
| +}
|
| +
|
| +bool ParseContentLength(const StringMap_t& headers, size_t* content_length) {
|
| + StringMap_t::const_iterator iter = headers.find("Content-Length");
|
| + if (iter == headers.end())
|
| + return false;
|
| +
|
| + *content_length = strtoul(iter->second.c_str(), NULL, 10);
|
| + return true;
|
| +}
|
| +
|
| +bool ParseContentRange(const StringMap_t& headers,
|
| + size_t* read_start,
|
| + size_t* read_end,
|
| + size_t* entity_length) {
|
| + StringMap_t::const_iterator iter = headers.find("Content-Range");
|
| + if (iter == headers.end())
|
| + return false;
|
| +
|
| + // The key should look like "bytes ##-##/##" or "bytes ##-##/*". The last
|
| + // value is the entity length, which can potentially be * (i.e. unknown).
|
| + int read_start_int;
|
| + int read_end_int;
|
| + int entity_length_int;
|
| + int result = sscanf(iter->second.c_str(),
|
| + "bytes %" SCNuS "-%" SCNuS "/%" SCNuS,
|
| + &read_start_int,
|
| + &read_end_int,
|
| + &entity_length_int);
|
| +
|
| + // The Content-Range header specifies an inclusive range: e.g. the first ten
|
| + // bytes is "bytes 0-9/*". Convert it to a half-open range by incrementing
|
| + // read_end.
|
| + if (result == 2) {
|
| + *read_start = read_start_int;
|
| + *read_end = read_end_int + 1;
|
| + *entity_length = 0;
|
| + return true;
|
| + } else if (result == 3) {
|
| + *read_start = read_start_int;
|
| + *read_end = read_end_int + 1;
|
| + *entity_length = entity_length_int;
|
| + return true;
|
| + }
|
| +
|
| + return false;
|
| +}
|
| +
|
| +} // namespace
|
| +
|
| +void MountNodeHttp::SetCachedSize(off_t size) {
|
| + has_cached_size_ = true;
|
| + stat_.st_size = size;
|
| +}
|
| +
|
| +Error MountNodeHttp::FSync() { return ENOSYS; }
|
| +
|
| +Error MountNodeHttp::GetDents(size_t offs,
|
| + struct dirent* pdir,
|
| + size_t count,
|
| + int* out_bytes) {
|
| + *out_bytes = 0;
|
| + return ENOSYS;
|
| +}
|
| +
|
| +Error MountNodeHttp::GetStat(struct stat* stat) {
|
| + AutoLock lock(&lock_);
|
| +
|
| + // Assume we need to 'HEAD' if we do not know the size, otherwise, assume
|
| + // that the information is constant. We can add a timeout if needed.
|
| + MountHttp* mount = static_cast<MountHttp*>(mount_);
|
| + if (stat_.st_size == 0 || !mount->cache_stat_) {
|
| + StringMap_t headers;
|
| + PP_Resource loader;
|
| + PP_Resource request;
|
| + PP_Resource response;
|
| + int32_t statuscode;
|
| + StringMap_t response_headers;
|
| + Error error = OpenUrl("HEAD",
|
| + &headers,
|
| + &loader,
|
| + &request,
|
| + &response,
|
| + &statuscode,
|
| + &response_headers);
|
| + if (error)
|
| + return error;
|
| +
|
| + ScopedResource scoped_loader(mount_->ppapi(), loader);
|
| + ScopedResource scoped_request(mount_->ppapi(), request);
|
| + ScopedResource scoped_response(mount_->ppapi(), response);
|
| +
|
| + size_t entity_length;
|
| + if (ParseContentLength(response_headers, &entity_length)) {
|
| + SetCachedSize(static_cast<off_t>(entity_length));
|
| + } else if (cache_content_ && !has_cached_size_) {
|
| + error = DownloadToCache();
|
| + // TODO(binji): this error should not be dropped, but it requires a bit
|
| + // of a refactor of the tests. See crbug.com/245431
|
| + // if (error)
|
| + // return error;
|
| + } else {
|
| + // Don't use SetCachedSize here -- it is actually unknown.
|
| + stat_.st_size = 0;
|
| + }
|
| +
|
| + stat_.st_atime = 0; // TODO(binji): Use "Last-Modified".
|
| + stat_.st_mtime = 0;
|
| + stat_.st_ctime = 0;
|
| + }
|
| +
|
| + // Fill the stat structure if provided
|
| + if (stat)
|
| + memcpy(stat, &stat_, sizeof(stat_));
|
| +
|
| + return 0;
|
| +}
|
| +
|
| +Error MountNodeHttp::Read(size_t offs,
|
| + void* buf,
|
| + size_t count,
|
| + int* out_bytes) {
|
| + *out_bytes = 0;
|
| +
|
| + AutoLock lock(&lock_);
|
| + if (cache_content_) {
|
| + if (cached_data_.empty()) {
|
| + Error error = DownloadToCache();
|
| + if (error)
|
| + return error;
|
| + }
|
| +
|
| + return ReadPartialFromCache(offs, buf, count, out_bytes);
|
| + }
|
| +
|
| + return DownloadPartial(offs, buf, count, out_bytes);
|
| +}
|
| +
|
| +Error MountNodeHttp::FTruncate(off_t size) { return ENOSYS; }
|
| +
|
| +Error MountNodeHttp::Write(size_t offs,
|
| + const void* buf,
|
| + size_t count,
|
| + int* out_bytes) {
|
| + // TODO(binji): support POST?
|
| + *out_bytes = 0;
|
| + return ENOSYS;
|
| +}
|
| +
|
| +Error MountNodeHttp::GetSize(size_t* out_size) {
|
| + *out_size = 0;
|
| +
|
| + // TODO(binji): This value should be cached properly; i.e. obey the caching
|
| + // headers returned by the server.
|
| + AutoLock lock(&lock_);
|
| + if (!has_cached_size_) {
|
| + // Even if DownloadToCache fails, the best result we can return is what
|
| + // was written to stat_.st_size.
|
| + if (cache_content_) {
|
| + Error error = DownloadToCache();
|
| + if (error)
|
| + return error;
|
| + }
|
| + }
|
| +
|
| + *out_size = stat_.st_size;
|
| + return 0;
|
| +}
|
| +
|
| +MountNodeHttp::MountNodeHttp(Mount* mount,
|
| + const std::string& url,
|
| + bool cache_content)
|
| + : MountNode(mount),
|
| + url_(url),
|
| + cache_content_(cache_content),
|
| + has_cached_size_(false) {}
|
| +
|
| +Error MountNodeHttp::OpenUrl(const char* method,
|
| + StringMap_t* request_headers,
|
| + PP_Resource* out_loader,
|
| + PP_Resource* out_request,
|
| + PP_Resource* out_response,
|
| + int32_t* out_statuscode,
|
| + StringMap_t* out_response_headers) {
|
| + // Assume lock_ is already held.
|
| + PepperInterface* ppapi = mount_->ppapi();
|
| +
|
| + MountHttp* mount_http = static_cast<MountHttp*>(mount_);
|
| + ScopedResource request(
|
| + ppapi, mount_http->MakeUrlRequestInfo(url_, method, request_headers));
|
| + if (!request.pp_resource())
|
| + return EINVAL;
|
| +
|
| + URLLoaderInterface* loader_interface = ppapi->GetURLLoaderInterface();
|
| + URLResponseInfoInterface* response_interface =
|
| + ppapi->GetURLResponseInfoInterface();
|
| + VarInterface* var_interface = ppapi->GetVarInterface();
|
| +
|
| + ScopedResource loader(ppapi, loader_interface->Create(ppapi->GetInstance()));
|
| + if (!loader.pp_resource())
|
| + return EINVAL;
|
| +
|
| + int32_t result = loader_interface->Open(
|
| + loader.pp_resource(), request.pp_resource(), PP_BlockUntilComplete());
|
| + if (result != PP_OK)
|
| + return PPErrorToErrno(result);
|
| +
|
| + ScopedResource response(
|
| + ppapi, loader_interface->GetResponseInfo(loader.pp_resource()));
|
| + if (!response.pp_resource())
|
| + return EINVAL;
|
| +
|
| + // Get response statuscode.
|
| + PP_Var statuscode = response_interface->GetProperty(
|
| + response.pp_resource(), PP_URLRESPONSEPROPERTY_STATUSCODE);
|
| +
|
| + if (statuscode.type != PP_VARTYPE_INT32)
|
| + return EINVAL;
|
| +
|
| + *out_statuscode = statuscode.value.as_int;
|
| +
|
| + // Only accept OK or Partial Content.
|
| + if (*out_statuscode != STATUSCODE_OK &&
|
| + *out_statuscode != STATUSCODE_PARTIAL_CONTENT) {
|
| + return EINVAL;
|
| + }
|
| +
|
| + // Get response headers.
|
| + PP_Var response_headers_var = response_interface->GetProperty(
|
| + response.pp_resource(), PP_URLRESPONSEPROPERTY_HEADERS);
|
| +
|
| + uint32_t response_headers_length;
|
| + const char* response_headers_str =
|
| + var_interface->VarToUtf8(response_headers_var, &response_headers_length);
|
| +
|
| + *out_loader = loader.Release();
|
| + *out_request = request.Release();
|
| + *out_response = response.Release();
|
| + *out_response_headers =
|
| + ParseHeaders(response_headers_str, response_headers_length);
|
| +
|
| + return 0;
|
| +}
|
| +
|
| +Error MountNodeHttp::DownloadToCache() {
|
| + StringMap_t headers;
|
| + PP_Resource loader;
|
| + PP_Resource request;
|
| + PP_Resource response;
|
| + int32_t statuscode;
|
| + StringMap_t response_headers;
|
| + Error error = OpenUrl("GET",
|
| + &headers,
|
| + &loader,
|
| + &request,
|
| + &response,
|
| + &statuscode,
|
| + &response_headers);
|
| + if (error)
|
| + return error;
|
| +
|
| + PepperInterface* ppapi = mount_->ppapi();
|
| + ScopedResource scoped_loader(ppapi, loader);
|
| + ScopedResource scoped_request(ppapi, request);
|
| + ScopedResource scoped_response(ppapi, response);
|
| +
|
| + size_t content_length = 0;
|
| + if (ParseContentLength(response_headers, &content_length)) {
|
| + cached_data_.resize(content_length);
|
| + int real_size;
|
| + error = DownloadToBuffer(
|
| + loader, cached_data_.data(), content_length, &real_size);
|
| + if (error)
|
| + return error;
|
| +
|
| + SetCachedSize(real_size);
|
| + cached_data_.resize(real_size);
|
| + return 0;
|
| + }
|
| +
|
| + // We don't know how big the file is. Read in chunks.
|
| + cached_data_.resize(MAX_READ_BUFFER_SIZE);
|
| + size_t total_bytes_read = 0;
|
| + size_t bytes_to_read = MAX_READ_BUFFER_SIZE;
|
| + while (true) {
|
| + char* buf = cached_data_.data() + total_bytes_read;
|
| + int bytes_read;
|
| + error = DownloadToBuffer(loader, buf, bytes_to_read, &bytes_read);
|
| + if (error)
|
| + return error;
|
| +
|
| + total_bytes_read += bytes_read;
|
| +
|
| + if (bytes_read < bytes_to_read) {
|
| + SetCachedSize(total_bytes_read);
|
| + cached_data_.resize(total_bytes_read);
|
| + return 0;
|
| + }
|
| +
|
| + cached_data_.resize(total_bytes_read + bytes_to_read);
|
| + }
|
| +}
|
| +
|
| +Error MountNodeHttp::ReadPartialFromCache(size_t offs,
|
| + void* buf,
|
| + size_t count,
|
| + int* out_bytes) {
|
| + *out_bytes = 0;
|
| +
|
| + if (offs > cached_data_.size())
|
| + return EINVAL;
|
| +
|
| + count = std::min(count, cached_data_.size() - offs);
|
| + memcpy(buf, &cached_data_.data()[offs], count);
|
| +
|
| + *out_bytes = count;
|
| + return 0;
|
| +}
|
| +
|
| +Error MountNodeHttp::DownloadPartial(size_t offs,
|
| + void* buf,
|
| + size_t count,
|
| + int* out_bytes) {
|
| + *out_bytes = 0;
|
| +
|
| + StringMap_t headers;
|
| +
|
| + char buffer[100];
|
| + // Range request is inclusive: 0-99 returns 100 bytes.
|
| + snprintf(&buffer[0],
|
| + sizeof(buffer),
|
| + "bytes=%" PRIuS "-%" PRIuS,
|
| + offs,
|
| + offs + count - 1);
|
| + headers["Range"] = buffer;
|
| +
|
| + PP_Resource loader;
|
| + PP_Resource request;
|
| + PP_Resource response;
|
| + int32_t statuscode;
|
| + StringMap_t response_headers;
|
| + Error error = OpenUrl("GET",
|
| + &headers,
|
| + &loader,
|
| + &request,
|
| + &response,
|
| + &statuscode,
|
| + &response_headers);
|
| + if (error)
|
| + return error;
|
| +
|
| + PepperInterface* ppapi = mount_->ppapi();
|
| + ScopedResource scoped_loader(ppapi, loader);
|
| + ScopedResource scoped_request(ppapi, request);
|
| + ScopedResource scoped_response(ppapi, response);
|
| +
|
| + size_t read_start = 0;
|
| + if (statuscode == STATUSCODE_OK) {
|
| + // No partial result, read everything starting from the part we care about.
|
| + size_t content_length;
|
| + if (ParseContentLength(response_headers, &content_length)) {
|
| + if (offs >= content_length)
|
| + return EINVAL;
|
| +
|
| + // Clamp count, if trying to read past the end of the file.
|
| + if (offs + count > content_length) {
|
| + count = content_length - offs;
|
| + }
|
| + }
|
| + } else if (statuscode == STATUSCODE_PARTIAL_CONTENT) {
|
| + // Determine from the headers where we are reading.
|
| + size_t read_end;
|
| + size_t entity_length;
|
| + if (ParseContentRange(
|
| + response_headers, &read_start, &read_end, &entity_length)) {
|
| + if (read_start > offs || read_start > read_end) {
|
| + // If this error occurs, the server is returning bogus values.
|
| + return EINVAL;
|
| + }
|
| +
|
| + // Clamp count, if trying to read past the end of the file.
|
| + count = std::min(read_end - read_start, count);
|
| + } else {
|
| + // Partial Content without Content-Range. Assume that the server gave us
|
| + // exactly what we asked for. This can happen even when the server
|
| + // returns 200 -- the cache may return 206 in this case, but not modify
|
| + // the headers.
|
| + read_start = offs;
|
| + }
|
| + }
|
| +
|
| + if (read_start < offs) {
|
| + // We aren't yet at the location where we want to start reading. Read into
|
| + // our dummy buffer until then.
|
| + size_t bytes_to_read = offs - read_start;
|
| + if (buffer_.size() < bytes_to_read)
|
| + buffer_.resize(std::min(bytes_to_read, MAX_READ_BUFFER_SIZE));
|
| +
|
| + while (bytes_to_read > 0) {
|
| + int32_t bytes_read;
|
| + Error error =
|
| + DownloadToBuffer(loader, buffer_.data(), buffer_.size(), &bytes_read);
|
| + if (error)
|
| + return error;
|
| +
|
| + bytes_to_read -= bytes_read;
|
| + }
|
| + }
|
| +
|
| + return DownloadToBuffer(loader, buf, count, out_bytes);
|
| +}
|
| +
|
| +Error MountNodeHttp::DownloadToBuffer(PP_Resource loader,
|
| + void* buf,
|
| + size_t count,
|
| + int* out_bytes) {
|
| + *out_bytes = 0;
|
| +
|
| + PepperInterface* ppapi = mount_->ppapi();
|
| + URLLoaderInterface* loader_interface = ppapi->GetURLLoaderInterface();
|
| +
|
| + char* out_buffer = static_cast<char*>(buf);
|
| + size_t bytes_to_read = count;
|
| + while (bytes_to_read > 0) {
|
| + int32_t bytes_read = loader_interface->ReadResponseBody(
|
| + loader, out_buffer, bytes_to_read, PP_BlockUntilComplete());
|
| +
|
| + if (bytes_read == 0) {
|
| + // This is not an error -- it may just be that we were trying to read
|
| + // more data than exists.
|
| + *out_bytes = count - bytes_to_read;
|
| + return 0;
|
| + }
|
| +
|
| + if (bytes_read < 0)
|
| + return PPErrorToErrno(bytes_read);
|
| +
|
| + assert(bytes_read <= bytes_to_read);
|
| + bytes_to_read -= bytes_read;
|
| + out_buffer += bytes_read;
|
| + }
|
| +
|
| + *out_bytes = count;
|
| + return 0;
|
| +}
|
|
|