Allow serialization of ArrayBuffer params in extension/apps API methods

This changes the V8ValueConverter to do ArrayBuffer<->BinaryValue conversions,
as well as supporting ArrayBufferView subclasses as request parameters (but
not response ones, since that's unnecessary).

Also adds an experimental API for testing ArrayBuffers in request/response
parameters.

(This is re-landing https://chromiumcodereview.appspot.com/10161038, which
ran into some test failures fixed by crrev.com/135933)

[email protected]

BUG=122675
TEST=Included browser tests should pass

Review URL: https://chromiumcodereview.appspot.com/10377063

git-svn-id: svn://svn.chromium.org/chrome/trunk/src@136114 0039d316-1c4b-4281-b951-d872f2087c98
diff --git a/chrome/browser/extensions/api/idltest/idltest_api.cc b/chrome/browser/extensions/api/idltest/idltest_api.cc
new file mode 100644
index 0000000..1feff0a
--- /dev/null
+++ b/chrome/browser/extensions/api/idltest/idltest_api.cc
@@ -0,0 +1,44 @@
+// Copyright (c) 2012 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 "chrome/browser/extensions/api/idltest/idltest_api.h"
+
+#include "base/values.h"
+
+using base::BinaryValue;
+
+namespace {
+
+ListValue* CopyBinaryValueToIntegerList(const BinaryValue* input) {
+  ListValue* output = new ListValue();
+  const char* input_buffer = input->GetBuffer();
+  for (size_t i = 0; i < input->GetSize(); i++) {
+    output->Append(Value::CreateIntegerValue(input_buffer[i]));
+  }
+  return output;
+}
+
+}
+
+bool IdltestSendArrayBufferFunction::RunImpl() {
+  BinaryValue* input = NULL;
+  EXTENSION_FUNCTION_VALIDATE(args_ != NULL && args_->GetBinary(0, &input));
+  result_.reset(CopyBinaryValueToIntegerList(input));
+  return true;
+}
+
+bool IdltestSendArrayBufferViewFunction::RunImpl() {
+  BinaryValue* input = NULL;
+  EXTENSION_FUNCTION_VALIDATE(args_ != NULL && args_->GetBinary(0, &input));
+  result_.reset(CopyBinaryValueToIntegerList(input));
+  return true;
+}
+
+bool IdltestGetArrayBufferFunction::RunImpl() {
+  std::string hello = "hello world";
+  BinaryValue* output =
+      BinaryValue::CreateWithCopiedBuffer(hello.c_str(), hello.size());
+  result_.reset(output);
+  return true;
+}
diff --git a/chrome/browser/extensions/api/idltest/idltest_api.h b/chrome/browser/extensions/api/idltest/idltest_api.h
new file mode 100644
index 0000000..a8a6a51d
--- /dev/null
+++ b/chrome/browser/extensions/api/idltest/idltest_api.h
@@ -0,0 +1,32 @@
+// Copyright (c) 2012 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.
+
+#ifndef CHROME_BROWSER_EXTENSIONS_API_IDLTEST_IDLTEST_API_H_
+#define CHROME_BROWSER_EXTENSIONS_API_IDLTEST_IDLTEST_API_H_
+#pragma once
+
+#include "chrome/browser/extensions/extension_function.h"
+
+class IdltestSendArrayBufferFunction : public SyncExtensionFunction {
+ public:
+  virtual bool RunImpl() OVERRIDE;
+ private:
+  DECLARE_EXTENSION_FUNCTION_NAME("experimental.idltest.sendArrayBuffer")
+};
+
+class IdltestSendArrayBufferViewFunction : public SyncExtensionFunction {
+ public:
+  virtual bool RunImpl() OVERRIDE;
+ private:
+  DECLARE_EXTENSION_FUNCTION_NAME("experimental.idltest.sendArrayBufferView")
+};
+
+class IdltestGetArrayBufferFunction : public SyncExtensionFunction {
+ public:
+  virtual bool RunImpl() OVERRIDE;
+ private:
+  DECLARE_EXTENSION_FUNCTION_NAME("experimental.idltest.getArrayBuffer")
+};
+
+#endif  // CHROME_BROWSER_EXTENSIONS_API_IDLTEST_IDLTEST_API_H_
diff --git a/chrome/browser/extensions/api/idltest/idltest_apitest.cc b/chrome/browser/extensions/api/idltest/idltest_apitest.cc
new file mode 100644
index 0000000..eecfb63
--- /dev/null
+++ b/chrome/browser/extensions/api/idltest/idltest_apitest.cc
@@ -0,0 +1,21 @@
+// Copyright (c) 2012 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 "chrome/browser/extensions/extension_apitest.h"
+#include "chrome/common/chrome_switches.h"
+
+class ExtensionIdltestApiTest : public ExtensionApiTest {
+ public:
+  ExtensionIdltestApiTest() {}
+  virtual ~ExtensionIdltestApiTest() {}
+
+  virtual void SetUpCommandLine(CommandLine* command_line) OVERRIDE {
+    ExtensionApiTest::SetUpCommandLine(command_line);
+    command_line->AppendSwitch(switches::kEnableExperimentalExtensionApis);
+  }
+};
+
+IN_PROC_BROWSER_TEST_F(ExtensionIdltestApiTest, BinaryData) {
+  EXPECT_TRUE(RunExtensionSubtest("idltest/binary_data", "binary.html"));
+};
diff --git a/chrome/chrome_browser_extensions.gypi b/chrome/chrome_browser_extensions.gypi
index 27af60a..bece4a8 100644
--- a/chrome/chrome_browser_extensions.gypi
+++ b/chrome/chrome_browser_extensions.gypi
@@ -108,6 +108,8 @@
         'browser/extensions/api/extension_action/extension_page_actions_api.h',
         'browser/extensions/api/extension_action/extension_page_actions_api_constants.cc',
         'browser/extensions/api/extension_action/extension_page_actions_api_constants.h',
+        'browser/extensions/api/idltest/idltest_api.cc',
+        'browser/extensions/api/idltest/idltest_api.h',
         'browser/extensions/api/identity/identity_api.cc',
         'browser/extensions/api/identity/identity_api.h',
         'browser/extensions/api/offscreen_tabs/offscreen_tabs_api.cc',
diff --git a/chrome/chrome_tests.gypi b/chrome/chrome_tests.gypi
index ec51b3e8..72c6dda 100644
--- a/chrome/chrome_tests.gypi
+++ b/chrome/chrome_tests.gypi
@@ -2703,6 +2703,7 @@
         'browser/extensions/api/extension_action/page_action_apitest.cc',
         'browser/extensions/api/extension_action/page_as_browser_action_apitest.cc',
         'browser/extensions/api/identity/identity_apitest.cc',
+        'browser/extensions/api/idltest/idltest_apitest.cc',
         'browser/extensions/api/offscreen_tabs/offscreen_tabs_apitest.cc',
         'browser/extensions/api/omnibox/omnibox_apitest.cc',
         'browser/extensions/api/permissions/permissions_apitest.cc',
diff --git a/chrome/common/extensions/api/api.gyp b/chrome/common/extensions/api/api.gyp
index bf23f22..76c69bba 100644
--- a/chrome/common/extensions/api/api.gyp
+++ b/chrome/common/extensions/api/api.gyp
@@ -31,6 +31,7 @@
           'alarms.idl',
           'experimental.bluetooth.idl',
           'experimental.dns.idl',
+          'experimental.idltest.idl',
           'experimental.serial.idl',
           'experimental.socket.idl',
           'experimental.usb.idl',
diff --git a/chrome/common/extensions/api/experimental.idltest.idl b/chrome/common/extensions/api/experimental.idltest.idl
new file mode 100644
index 0000000..4e496c1
--- /dev/null
+++ b/chrome/common/extensions/api/experimental.idltest.idl
@@ -0,0 +1,26 @@
+// Copyright (c) 2012 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.
+
+// An API to test IDL schema specifications.
+
+[nodoc] namespace experimental.idltest {
+
+  callback LongArrayCallback = void(long[] array);
+  callback ArrayBufferCallback = void([instanceOf=ArrayBuffer] object buffer);
+
+  interface Functions {
+    // Functions for testing binary data request/response parameters. The first
+    // two just return back the bytes they were passed in an array.
+    static void sendArrayBuffer([instanceOf=ArrayBuffer] object input,
+                                LongArrayCallback cb);
+    // TODO(asargent) - we currently can't have [instanceOf=ArrayBufferView],
+    // I think because ArrayBufferView isn't an instantiable type. The best
+    // we might be able to do is have a 'choices' list including all the
+    // typed array subclasses like Uint8Array, Uint16Array, Float32Array, etc.
+    static void sendArrayBufferView([instanceOf=Uint8Array] object input,
+                                    LongArrayCallback cb);
+    static void getArrayBuffer(ArrayBufferCallback cb);
+  };
+
+};
diff --git a/chrome/renderer/extensions/send_request_natives.cc b/chrome/renderer/extensions/send_request_natives.cc
index e5377ce3..17e0ba9 100644
--- a/chrome/renderer/extensions/send_request_natives.cc
+++ b/chrome/renderer/extensions/send_request_natives.cc
@@ -5,8 +5,11 @@
 #include "chrome/renderer/extensions/send_request_natives.h"
 
 #include "base/json/json_reader.h"
+#include "content/public/renderer/v8_value_converter.h"
 #include "chrome/renderer/extensions/extension_request_sender.h"
 
+using content::V8ValueConverter;
+
 namespace extensions {
 
 SendRequestNatives::SendRequestNatives(
@@ -32,21 +35,19 @@
 // callback will be dispatched to EventBindings::HandleResponse.
 v8::Handle<v8::Value> SendRequestNatives::StartRequest(
     const v8::Arguments& args) {
-  std::string str_args = *v8::String::Utf8Value(args[1]);
-  scoped_ptr<Value> value_args(base::JSONReader::Read(str_args));
-
-  // Since we do the serialization in the v8 extension, we should always get
-  // valid JSON.
-  if (!value_args.get() || !value_args->IsType(Value::TYPE_LIST)) {
-    NOTREACHED() << "Invalid JSON passed to StartRequest.";
-    return v8::Undefined();
-  }
-
   std::string name = *v8::String::AsciiValue(args[0]);
   int request_id = args[2]->Int32Value();
   bool has_callback = args[3]->BooleanValue();
   bool for_io_thread = args[4]->BooleanValue();
 
+  scoped_ptr<V8ValueConverter> converter(V8ValueConverter::create());
+  scoped_ptr<Value> value_args(
+      converter->FromV8Value(args[1], v8::Context::GetCurrent()));
+  if (!value_args.get() || !value_args->IsType(Value::TYPE_LIST)) {
+    NOTREACHED() << "Unable to convert args passed to StartRequest";
+    return v8::Undefined();
+  }
+
   request_sender_->StartRequest(name, request_id, has_callback, for_io_thread,
                                 static_cast<ListValue*>(value_args.get()));
   return v8::Undefined();
diff --git a/chrome/renderer/resources/extensions/send_request.js b/chrome/renderer/resources/extensions/send_request.js
index 045e270..2e79b8a 100644
--- a/chrome/renderer/resources/extensions/send_request.js
+++ b/chrome/renderer/resources/extensions/send_request.js
@@ -107,8 +107,13 @@
   if (request.args === undefined)
     request.args = null;
 
-  var sargs = opt_args.noStringify ?
-      request.args : chromeHidden.JSON.stringify(request.args);
+  // TODO(asargent) - convert all optional native functions to accept raw
+  // v8 values instead of expecting JSON strings.
+  var doStringify = false;
+  if (opt_args.nativeFunction && !opt_args.noStringify)
+    doStringify = true;
+  var requestArgs = doStringify ?
+      chromeHidden.JSON.stringify(request.args) : request.args;
   var nativeFunction = opt_args.nativeFunction || natives.StartRequest;
 
   var requestId = natives.GetNextRequestId();
@@ -116,7 +121,7 @@
   requests[requestId] = request;
   var hasCallback =
       (request.callback || opt_args.customCallback) ? true : false;
-  return nativeFunction(functionName, sargs, requestId, hasCallback,
+  return nativeFunction(functionName, requestArgs, requestId, hasCallback,
                         opt_args.forIOThread);
 }
 
diff --git a/chrome/test/data/extensions/api_test/idltest/binary_data/binary.html b/chrome/test/data/extensions/api_test/idltest/binary_data/binary.html
new file mode 100644
index 0000000..0f52baaa
--- /dev/null
+++ b/chrome/test/data/extensions/api_test/idltest/binary_data/binary.html
@@ -0,0 +1 @@
+<script src="binary.js"></script>
diff --git a/chrome/test/data/extensions/api_test/idltest/binary_data/binary.js b/chrome/test/data/extensions/api_test/idltest/binary_data/binary.js
new file mode 100644
index 0000000..950db1c
--- /dev/null
+++ b/chrome/test/data/extensions/api_test/idltest/binary_data/binary.js
@@ -0,0 +1,62 @@
+// Copyright (c) 2012 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.
+
+var assertEq = chrome.test.assertEq;
+var assertTrue = chrome.test.assertTrue;
+var callbackPass = chrome.test.callbackPass;
+
+function makeCompareCallback(buf) {
+  return function(array) {
+    assertEq(buf.byteLength, array.length);
+    for (var i = 0; i < buf.length; i++) {
+      assertEq(buf[i], array[i]);
+    }
+  };
+}
+
+function makeBuffer() {
+  var bufferSize = 128;
+  var ab = new ArrayBuffer(bufferSize);
+  var view = new Uint8Array(ab);
+  for (var i = 0; i < bufferSize; i++) {
+    view[i] = i+3;
+  }
+  return view;
+}
+
+var tests = [
+  function sendBuffer() {
+    var view = makeBuffer();
+    chrome.experimental.idltest.sendArrayBuffer(
+        view.buffer, callbackPass(makeCompareCallback(view.buffer)));
+  },
+
+  function sendBufferView() {
+    var view = makeBuffer();
+    chrome.experimental.idltest.sendArrayBufferView(
+        view, callbackPass(makeCompareCallback(view.buffer)));
+  },
+
+  function sendBufferSlice() {
+    var view = makeBuffer();
+    var bufferSlice = view.buffer.slice(64);
+    assertEq(64, bufferSlice.byteLength);
+    chrome.experimental.idltest.sendArrayBuffer(
+        bufferSlice, callbackPass(makeCompareCallback(bufferSlice)));
+  },
+
+  function getBuffer() {
+    chrome.experimental.idltest.getArrayBuffer(callbackPass(function(buffer) {
+      assertTrue(buffer.__proto__ == (new ArrayBuffer()).__proto__);
+      var view = new Uint8Array(buffer);
+      var expected = "hello world";
+      assertEq(view.byteLength, expected.length);
+      for (var i = 0; i < view.byteLength; i++) {
+        assertTrue(expected[i] == String.fromCharCode(view[i]));
+      }
+    }));
+  }
+];
+
+chrome.test.runTests(tests);
diff --git a/chrome/test/data/extensions/api_test/idltest/binary_data/manifest.json b/chrome/test/data/extensions/api_test/idltest/binary_data/manifest.json
new file mode 100644
index 0000000..b6b7767
--- /dev/null
+++ b/chrome/test/data/extensions/api_test/idltest/binary_data/manifest.json
@@ -0,0 +1,6 @@
+{
+  "name": "IDLTest API test extension",
+  "version": "0.5",
+  "permissions": ["experimental"],
+  "manifest_version": 2
+}
diff --git a/content/public/renderer/v8_value_converter.h b/content/public/renderer/v8_value_converter.h
index e8fad2a..6e785c1 100644
--- a/content/public/renderer/v8_value_converter.h
+++ b/content/public/renderer/v8_value_converter.h
@@ -1,4 +1,4 @@
-// Copyright (c) 2011 The Chromium Authors. All rights reserved.
+// Copyright (c) 2012 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.
 
@@ -18,8 +18,10 @@
 // values (from base/values.h). Lists and dictionaries are converted
 // recursively.
 //
-// Only the JSON types (null, boolean, string, number, array, and object) are
-// supported.
+// The JSON types (null, boolean, string, number, array, and object) as well as
+// binary values are supported. For binary values, we convert to WebKit
+// ArrayBuffers, and support converting from an ArrayBuffer or any of the
+// ArrayBufferView subclasses (Uint8Array, etc.).
 class CONTENT_EXPORT V8ValueConverter {
  public:
   static V8ValueConverter* create();
diff --git a/content/renderer/v8_value_converter_impl.cc b/content/renderer/v8_value_converter_impl.cc
index b0762ef1..c457377 100644
--- a/content/renderer/v8_value_converter_impl.cc
+++ b/content/renderer/v8_value_converter_impl.cc
@@ -1,4 +1,4 @@
-// Copyright (c) 2011 The Chromium Authors. All rights reserved.
+// Copyright (c) 2012 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.
 
@@ -9,8 +9,17 @@
 #include "base/logging.h"
 #include "base/memory/scoped_ptr.h"
 #include "base/values.h"
+#include "third_party/WebKit/Source/WebKit/chromium/public/WebArrayBuffer.h"
+#include "third_party/WebKit/Source/WebKit/chromium/public/platform/WebArrayBufferView.h"
 #include "v8/include/v8.h"
 
+using base::BinaryValue;
+using base::DictionaryValue;
+using base::ListValue;
+using base::StringValue;
+using base::Value;
+
+
 namespace content {
 
 V8ValueConverter* V8ValueConverter::create() {
@@ -77,6 +86,9 @@
     case Value::TYPE_DICTIONARY:
       return ToV8Object(static_cast<const DictionaryValue*>(value));
 
+    case Value::TYPE_BINARY:
+      return ToArrayBuffer(static_cast<const BinaryValue*>(value));
+
     default:
       LOG(ERROR) << "Unexpected value type: " << value->GetType();
       return v8::Null();
@@ -127,6 +139,14 @@
   return result;
 }
 
+v8::Handle<v8::Value> V8ValueConverterImpl::ToArrayBuffer(
+    const BinaryValue* value) const {
+  WebKit::WebArrayBuffer buffer =
+      WebKit::WebArrayBuffer::create(value->GetSize(), 1);
+  memcpy(buffer.data(), value->GetBuffer(), value->GetSize());
+  return buffer.toV8Value();
+}
+
 Value* V8ValueConverterImpl::FromV8ValueImpl(v8::Handle<v8::Value> val) const {
   CHECK(!val.IsEmpty());
 
@@ -164,9 +184,14 @@
   if (val->IsArray())
     return FromV8Array(val.As<v8::Array>());
 
-  if (val->IsObject())
-    return FromV8Object(val->ToObject());
-
+  if (val->IsObject()) {
+    BinaryValue* binary_value = FromV8Buffer(val);
+    if (binary_value) {
+      return binary_value;
+    } else {
+      return FromV8Object(val->ToObject());
+    }
+  }
   LOG(ERROR) << "Unexpected v8 value type encountered.";
   return Value::CreateNullValue();
 }
@@ -194,6 +219,31 @@
   return result;
 }
 
+base::BinaryValue* V8ValueConverterImpl::FromV8Buffer(
+    v8::Handle<v8::Value> val) const {
+  char* data = NULL;
+  size_t length = 0;
+
+  WebKit::WebArrayBuffer* array_buffer =
+      WebKit::WebArrayBuffer::createFromV8Value(val);
+  if (array_buffer) {
+    data = reinterpret_cast<char*>(array_buffer->data());
+    length = array_buffer->byteLength();
+  } else {
+    WebKit::WebArrayBufferView* view =
+        WebKit::WebArrayBufferView::createFromV8Value(val);
+    if (view) {
+      data = reinterpret_cast<char*>(view->baseAddress()) + view->byteOffset();
+      length = view->byteLength();
+    }
+  }
+
+  if (data)
+    return base::BinaryValue::CreateWithCopiedBuffer(data, length);
+  else
+    return NULL;
+}
+
 DictionaryValue* V8ValueConverterImpl::FromV8Object(
     v8::Handle<v8::Object> val) const {
   DictionaryValue* result = new DictionaryValue();
diff --git a/content/renderer/v8_value_converter_impl.h b/content/renderer/v8_value_converter_impl.h
index ac6e7b7..c43da46 100644
--- a/content/renderer/v8_value_converter_impl.h
+++ b/content/renderer/v8_value_converter_impl.h
@@ -1,4 +1,4 @@
-// Copyright (c) 2011 The Chromium Authors. All rights reserved.
+// Copyright (c) 2012 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.
 
@@ -10,6 +10,7 @@
 #include "content/public/renderer/v8_value_converter.h"
 
 namespace base {
+class BinaryValue;
 class DictionaryValue;
 class ListValue;
 class Value;
@@ -43,9 +44,16 @@
   v8::Handle<v8::Value> ToV8Array(const base::ListValue* list) const;
   v8::Handle<v8::Value> ToV8Object(
       const base::DictionaryValue* dictionary) const;
+  v8::Handle<v8::Value> ToArrayBuffer(const base::BinaryValue* value) const;
 
   base::Value* FromV8ValueImpl(v8::Handle<v8::Value> value) const;
   base::ListValue* FromV8Array(v8::Handle<v8::Array> array) const;
+
+  // This will convert objects of type ArrayBuffer or any of the
+  // ArrayBufferView subclasses. The return value will be NULL if |value| is
+  // not one of these types.
+  base::BinaryValue* FromV8Buffer(v8::Handle<v8::Value> value) const;
+
   base::DictionaryValue* FromV8Object(v8::Handle<v8::Object> object) const;
 
   // If true, we will convert undefined JavaScript values to null.