Blink Clipboard: Implement Raw Clipboard API write.

- Adds ClipboardItem property raw, and plumbing/support for this property in
  `navigator.clipboard.write` (ClipboardPromise and ClipboardWriter).
- Adds a `RawClipboard` flag, to ensure this feature isn't accidentally
  activated via experimental flags or otherwise.
- Adds tests, including VirtualTestSuite and tentative tests, to verify that
  this works with the flag enabled, but is correctly disabled without the
  flag enabled.


Bug: 897289
Change-Id: I453bb0ad18d9e52a4f2054d0f124fc3e8d5b430d
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/1927003
Commit-Queue: Darwin Huang <huangdarwin@chromium.org>
Reviewed-by: Matthew Denton <mpdenton@chromium.org>
Reviewed-by: Kentaro Hara <haraken@chromium.org>
Reviewed-by: Victor Costan <pwnall@chromium.org>
Cr-Commit-Position: refs/heads/master@{#724804}
diff --git a/chrome/browser/about_flags.cc b/chrome/browser/about_flags.cc
index ce3cd59..1c1bbc6 100644
--- a/chrome/browser/about_flags.cc
+++ b/chrome/browser/about_flags.cc
@@ -4839,6 +4839,11 @@
      FEATURE_VALUE_TYPE(switches::kAccountIdMigration)},
 #endif  // defined(OS_CHROMEOS)
 
+    // TODO(https://crbug.com/1032161): Implement and enable for ChromeOS.
+    {"raw-clipboard", flag_descriptions::kRawClipboardName,
+     flag_descriptions::kRawClipboardDescription, kOsMac | kOsWin | kOsLinux,
+     FEATURE_VALUE_TYPE(blink::features::kRawClipboard)},
+
     // NOTE: Adding a new flag requires adding a corresponding entry to enum
     // "LoginCustomFlags" in tools/metrics/histograms/enums.xml. See "Flag
     // Histograms" in tools/metrics/histograms/README.md (run the
diff --git a/chrome/browser/flag-metadata.json b/chrome/browser/flag-metadata.json
index 399a1ff1..64d6d9e 100644
--- a/chrome/browser/flag-metadata.json
+++ b/chrome/browser/flag-metadata.json
@@ -3109,6 +3109,11 @@
     "expiry_milestone": 83
   },
   {
+    "name": "raw-clipboard",
+    "owners": ["huangdarwin", "pwnall"],
+    "expiry_milestone": 84
+  },
+  {
     "name": "reader-mode-heuristics",
     "owners": [ "mdjones", "wychen" ],
     // This flag is a utility for testing Reader Mode heuristics or force
diff --git a/chrome/browser/flag_descriptions.cc b/chrome/browser/flag_descriptions.cc
index cc0e88aea5..a485ba3 100644
--- a/chrome/browser/flag_descriptions.cc
+++ b/chrome/browser/flag_descriptions.cc
@@ -1727,6 +1727,11 @@
     "When a site wishes to show notifications, the usual modal dialog is "
     "replaced with a quieter version.";
 
+const char kRawClipboardName[] = "Raw Clipboard";
+const char kRawClipboardDescription[] =
+    "Allows raw / unsanitized clipboard content to be read and written. "
+    "See https://github.com/WICG/raw-clipboard-access.";
+
 const char kReducedReferrerGranularityName[] =
     "Reduce default 'referer' header granularity.";
 const char kReducedReferrerGranularityDescription[] =
diff --git a/chrome/browser/flag_descriptions.h b/chrome/browser/flag_descriptions.h
index 7e2f5195..4a2deb2e 100644
--- a/chrome/browser/flag_descriptions.h
+++ b/chrome/browser/flag_descriptions.h
@@ -1027,6 +1027,9 @@
 extern const char kQuietNotificationPromptsName[];
 extern const char kQuietNotificationPromptsDescription[];
 
+extern const char kRawClipboardName[];
+extern const char kRawClipboardDescription[];
+
 extern const char kReducedReferrerGranularityName[];
 extern const char kReducedReferrerGranularityDescription[];
 
diff --git a/third_party/blink/common/features.cc b/third_party/blink/common/features.cc
index f445217..80e5d05 100644
--- a/third_party/blink/common/features.cc
+++ b/third_party/blink/common/features.cc
@@ -150,6 +150,10 @@
 const base::Feature kCSSOMViewScrollCoordinates{
     "CSSOMViewScrollCoordinates", base::FEATURE_DISABLED_BY_DEFAULT};
 
+// Enables Raw Clipboard. https://crbug.com/897289.
+const base::Feature kRawClipboard{"RawClipboard",
+                                  base::FEATURE_DISABLED_BY_DEFAULT};
+
 // Enables usage of getDisplayMedia() that allows capture of web content, see
 // https://crbug.com/865060.
 const base::Feature kRTCGetDisplayMedia{"RTCGetDisplayMedia",
diff --git a/third_party/blink/public/common/features.h b/third_party/blink/public/common/features.h
index 3ad1f5b4..9a8d054 100644
--- a/third_party/blink/public/common/features.h
+++ b/third_party/blink/public/common/features.h
@@ -45,6 +45,7 @@
     kPreviewsResourceLoadingHintsSpecificResourceTypes;
 BLINK_COMMON_EXPORT extern const base::Feature
     kPurgeRendererMemoryWhenBackgrounded;
+BLINK_COMMON_EXPORT extern const base::Feature kRawClipboard;
 BLINK_COMMON_EXPORT extern const base::Feature kRTCGetDisplayMedia;
 BLINK_COMMON_EXPORT extern const base::Feature kRTCUnifiedPlanByDefault;
 BLINK_COMMON_EXPORT extern const base::Feature kRTCOfferExtmapAllowMixed;
diff --git a/third_party/blink/public/mojom/clipboard/clipboard.mojom b/third_party/blink/public/mojom/clipboard/clipboard.mojom
index a1f0d0f2..bfbad07d 100644
--- a/third_party/blink/public/mojom/clipboard/clipboard.mojom
+++ b/third_party/blink/public/mojom/clipboard/clipboard.mojom
@@ -69,7 +69,6 @@
   // Chrome-specific pickled data.
   WriteCustomData(map<mojo_base.mojom.String16, mojo_base.mojom.BigString16> data);
   // Arbitrary unsanitized data from renderer.
-  // NOTE: This has no callers, but will with Raw Clipboard Access.
   WriteRawData(mojo_base.mojom.String16 format, mojo_base.mojom.BigBuffer data);
 
   // TODO(dcheng): The |url| parameter should really be a GURL, but <canvas>'s
diff --git a/third_party/blink/renderer/core/clipboard/system_clipboard.cc b/third_party/blink/renderer/core/clipboard/system_clipboard.cc
index 77dfff3..75f3077 100644
--- a/third_party/blink/renderer/core/clipboard/system_clipboard.cc
+++ b/third_party/blink/renderer/core/clipboard/system_clipboard.cc
@@ -188,6 +188,11 @@
   clipboard_->WriteImage(bitmap);
 }
 
+void SystemClipboard::WriteRawData(const String& type,
+                                   mojo_base::BigBuffer data) {
+  clipboard_->WriteRawData(type, std::move(data));
+}
+
 String SystemClipboard::ReadCustomData(const String& type) {
   if (!IsValidBufferType(buffer_))
     return String();
diff --git a/third_party/blink/renderer/core/clipboard/system_clipboard.h b/third_party/blink/renderer/core/clipboard/system_clipboard.h
index 5b9e6cd..8420c1a 100644
--- a/third_party/blink/renderer/core/clipboard/system_clipboard.h
+++ b/third_party/blink/renderer/core/clipboard/system_clipboard.h
@@ -61,6 +61,9 @@
   // Write the image only.
   void WriteImage(const SkBitmap&);
 
+  // Arbitrary unsanitized data from renderer.
+  void WriteRawData(const String& type, mojo_base::BigBuffer data);
+
   String ReadCustomData(const String& type);
   void WriteDataObject(DataObject*);
 
diff --git a/third_party/blink/renderer/modules/clipboard/clipboard_item.cc b/third_party/blink/renderer/modules/clipboard/clipboard_item.cc
index d032f794..a20f82d2 100644
--- a/third_party/blink/renderer/modules/clipboard/clipboard_item.cc
+++ b/third_party/blink/renderer/modules/clipboard/clipboard_item.cc
@@ -4,7 +4,9 @@
 
 #include "third_party/blink/renderer/modules/clipboard/clipboard_item.h"
 
+#include "third_party/blink/public/common/features.h"
 #include "third_party/blink/renderer/core/dom/dom_exception.h"
+#include "third_party/blink/renderer/modules/clipboard/clipboard_item_options.h"
 #include "third_party/blink/renderer/platform/bindings/script_state.h"
 #include "third_party/blink/renderer/platform/heap/heap.h"
 
@@ -13,7 +15,9 @@
 // static
 ClipboardItem* ClipboardItem::Create(
     const HeapVector<std::pair<String, Member<Blob>>>& items,
+    const ClipboardItemOptions* options,
     ExceptionState& exception_state) {
+  DCHECK(options);
   // Check that incoming dictionary isn't empty. If it is, it's possible that
   // Javascript bindings implicitly converted an Object (like a Blob) into {},
   // an empty dictionary.
@@ -21,12 +25,17 @@
     exception_state.ThrowTypeError("Empty dictionary argument");
     return nullptr;
   }
-  return MakeGarbageCollected<ClipboardItem>(items);
+  return MakeGarbageCollected<ClipboardItem>(items, options);
 }
 
 ClipboardItem::ClipboardItem(
-    const HeapVector<std::pair<String, Member<Blob>>>& items)
-    : items_(items) {}
+    const HeapVector<std::pair<String, Member<Blob>>>& items,
+    const ClipboardItemOptions* options)
+    : items_(items),
+      is_raw_(base::FeatureList::IsEnabled(blink::features::kRawClipboard) &&
+              options->raw()) {
+  DCHECK(items_.size());
+}
 
 Vector<String> ClipboardItem::types() const {
   Vector<String> types;
@@ -37,6 +46,10 @@
   return types;
 }
 
+bool ClipboardItem::raw() const {
+  return is_raw_;
+}
+
 ScriptPromise ClipboardItem::getType(ScriptState* script_state,
                                      const String& type) const {
   auto* resolver = MakeGarbageCollected<ScriptPromiseResolver>(script_state);
diff --git a/third_party/blink/renderer/modules/clipboard/clipboard_item.h b/third_party/blink/renderer/modules/clipboard/clipboard_item.h
index 10e71ed..ed3f9c1b 100644
--- a/third_party/blink/renderer/modules/clipboard/clipboard_item.h
+++ b/third_party/blink/renderer/modules/clipboard/clipboard_item.h
@@ -13,6 +13,7 @@
 namespace blink {
 
 class ScriptState;
+class ClipboardItemOptions;
 
 class ClipboardItem final : public ScriptWrappable {
   DEFINE_WRAPPERTYPEINFO();
@@ -20,10 +21,14 @@
  public:
   static ClipboardItem* Create(
       const HeapVector<std::pair<String, Member<Blob>>>& items,
+      const ClipboardItemOptions* options,
       ExceptionState& exception_state);
+
   explicit ClipboardItem(
-      const HeapVector<std::pair<String, Member<Blob>>>& items);
+      const HeapVector<std::pair<String, Member<Blob>>>& items,
+      const ClipboardItemOptions* options);
   Vector<String> types() const;
+  bool raw() const;
   ScriptPromise getType(ScriptState* script_state, const String& type) const;
 
   const HeapVector<std::pair<String, Member<Blob>>>& GetItems() const {
@@ -34,6 +39,7 @@
 
  private:
   HeapVector<std::pair<String, Member<Blob>>> items_;
+  const bool is_raw_;
 };
 
 }  // namespace blink
diff --git a/third_party/blink/renderer/modules/clipboard/clipboard_item.idl b/third_party/blink/renderer/modules/clipboard/clipboard_item.idl
index 63a4e9c..b2444e1 100644
--- a/third_party/blink/renderer/modules/clipboard/clipboard_item.idl
+++ b/third_party/blink/renderer/modules/clipboard/clipboard_item.idl
@@ -5,13 +5,17 @@
 // https://w3c.github.io/clipboard-apis/#clipboard-interface
 
 [
-  Constructor(record<DOMString, Blob> items),
+  Constructor(record<DOMString, Blob> items,
+              optional ClipboardItemOptions options),
   RaisesException=Constructor,
   Exposed=Window,
   RuntimeEnabled=AsyncClipboard
 ] interface ClipboardItem {
   readonly attribute FrozenArray<DOMString> types;
 
+  [RuntimeEnabled=RawClipboard]
+  readonly attribute boolean raw;
+
   [
     CallWith=ScriptState
   ] Promise<Blob> getType(DOMString type);
diff --git a/third_party/blink/renderer/modules/clipboard/clipboard_item_options.idl b/third_party/blink/renderer/modules/clipboard/clipboard_item_options.idl
new file mode 100644
index 0000000..eefa3ed
--- /dev/null
+++ b/third_party/blink/renderer/modules/clipboard/clipboard_item_options.idl
@@ -0,0 +1,9 @@
+// Copyright 2019 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.
+
+// https://w3c.github.io/clipboard-apis/#clipboard-interface
+
+dictionary ClipboardItemOptions {
+  boolean raw = false;
+};
\ No newline at end of file
diff --git a/third_party/blink/renderer/modules/clipboard/clipboard_promise.cc b/third_party/blink/renderer/modules/clipboard/clipboard_promise.cc
index 82b1bcd..8f2d9e86 100644
--- a/third_party/blink/renderer/modules/clipboard/clipboard_promise.cc
+++ b/third_party/blink/renderer/modules/clipboard/clipboard_promise.cc
@@ -9,6 +9,7 @@
 
 #include "base/single_thread_task_runner.h"
 #include "base/task/post_task.h"
+#include "third_party/blink/public/common/features.h"
 #include "third_party/blink/public/platform/task_type.h"
 #include "third_party/blink/renderer/bindings/core/v8/script_promise_resolver.h"
 #include "third_party/blink/renderer/core/clipboard/clipboard_mime_types.h"
@@ -17,6 +18,7 @@
 #include "third_party/blink/renderer/core/dom/dom_exception.h"
 #include "third_party/blink/renderer/core/execution_context/execution_context.h"
 #include "third_party/blink/renderer/core/frame/local_frame.h"
+#include "third_party/blink/renderer/modules/clipboard/clipboard_item_options.h"
 #include "third_party/blink/renderer/modules/clipboard/clipboard_reader.h"
 #include "third_party/blink/renderer/modules/permissions/permission_utils.h"
 #include "third_party/blink/renderer/platform/heap/heap.h"
@@ -28,9 +30,9 @@
 // * clipboard-write
 // See https://w3c.github.io/clipboard-apis/#clipboard-permissions
 //
-// Write access is granted by default, whereas read access is gated behind a
-// permission prompt. Both read and write require the tab to be focused (and
-// Chrome must be the foreground app) for the operation to be allowed.
+// These permissions map to these ContentSettings:
+// * CLIPBOARD_READ_WRITE, for sanitized read, and unsanitized read/write.
+// * CLIPBOARD_SANITIZED_WRITE, for sanitized write only.
 
 namespace blink {
 
@@ -115,7 +117,7 @@
       clipboard_item_data_[clipboard_representation_index_].second;
 
   DCHECK(!clipboard_writer_);
-  clipboard_writer_ = ClipboardWriter::Create(type, this);
+  clipboard_writer_ = ClipboardWriter::Create(type, is_raw_, this);
   clipboard_writer_->WriteToSystem(blob);
 }
 
@@ -161,8 +163,12 @@
   // For now, we only process the first ClipboardItem.
   ClipboardItem* clipboard_item = (*clipboard_items)[0];
   clipboard_item_data_ = clipboard_item->GetItems();
+  is_raw_ = clipboard_item->raw();
 
-  RequestPermission(mojom::blink::PermissionName::CLIPBOARD_WRITE, false,
+  DCHECK(base::FeatureList::IsEnabled(blink::features::kRawClipboard) ||
+         !is_raw_);
+
+  RequestPermission(mojom::blink::PermissionName::CLIPBOARD_WRITE, is_raw_,
                     WTF::Bind(&ClipboardPromise::HandleWriteWithPermission,
                               WrapPersistent(this)));
 }
@@ -200,8 +206,11 @@
     return;
   }
 
+  ClipboardItemOptions* options = ClipboardItemOptions::Create();
+  options->setRaw(false);
+
   HeapVector<Member<ClipboardItem>> clipboard_items = {
-      MakeGarbageCollected<ClipboardItem>(items)};
+      MakeGarbageCollected<ClipboardItem>(items, options)};
   script_promise_resolver_->Resolve(clipboard_items);
 }
 
@@ -232,10 +241,10 @@
   for (const auto& type_and_blob : clipboard_item_data_) {
     String type = type_and_blob.first;
     String type_with_args = type_and_blob.second->type();
-    if (!ClipboardWriter::IsValidType(type)) {
+    if (!is_raw_ && !ClipboardWriter::IsValidType(type)) {
       script_promise_resolver_->Reject(MakeGarbageCollected<DOMException>(
           DOMExceptionCode::kNotAllowedError,
-          "Write type " + type + " not supported."));
+          "Sanitized MIME type " + type + " not supported on write."));
       return;
     }
     if (!type_with_args.Contains(type)) {
diff --git a/third_party/blink/renderer/modules/clipboard/clipboard_promise.h b/third_party/blink/renderer/modules/clipboard/clipboard_promise.h
index 096e00a..5a41a5932 100644
--- a/third_party/blink/renderer/modules/clipboard/clipboard_promise.h
+++ b/third_party/blink/renderer/modules/clipboard/clipboard_promise.h
@@ -78,6 +78,7 @@
   // Only for use in writeText().
   String plain_text_;
   HeapVector<std::pair<String, Member<Blob>>> clipboard_item_data_;
+  bool is_raw_;  // Corresponds to allowWithoutSanitization in ClipboardItem.
   // Index of clipboard representation currently being processed.
   wtf_size_t clipboard_representation_index_;
 
diff --git a/third_party/blink/renderer/modules/clipboard/clipboard_writer.cc b/third_party/blink/renderer/modules/clipboard/clipboard_writer.cc
index 6244ac5..34ccc22 100644
--- a/third_party/blink/renderer/modules/clipboard/clipboard_writer.cc
+++ b/third_party/blink/renderer/modules/clipboard/clipboard_writer.cc
@@ -4,6 +4,7 @@
 
 #include "third_party/blink/renderer/modules/clipboard/clipboard_writer.h"
 
+#include "third_party/blink/public/common/features.h"
 #include "third_party/blink/public/mojom/clipboard/clipboard.mojom-blink.h"
 #include "third_party/blink/renderer/core/clipboard/clipboard_mime_types.h"
 #include "third_party/blink/renderer/core/clipboard/system_clipboard.h"
@@ -95,6 +96,45 @@
   }
 };
 
+// Writes a blob with arbitrary, unsanitized content to the System Clipboard.
+class ClipboardRawDataWriter final : public ClipboardWriter {
+ public:
+  ClipboardRawDataWriter(ClipboardPromise* promise, String mime_type)
+      : ClipboardWriter(promise), mime_type_(mime_type) {}
+  ~ClipboardRawDataWriter() override = default;
+
+ private:
+  // Unfortunately, in order to use the same ClipboardWriter base,
+  // ClipboardRawDataWriter does need to have these extra 2 thread hops.
+  void DecodeOnBackgroundThread(
+      scoped_refptr<base::SingleThreadTaskRunner> task_runner,
+      DOMArrayBuffer* raw_data) override {
+    DCHECK(!IsMainThread());
+
+    PostCrossThreadTask(
+        *task_runner, FROM_HERE,
+        CrossThreadBindOnce(
+            &ClipboardRawDataWriter::Write,
+            /* This unretained is safe because the ClipboardRawDataWriter
+              must remain alive when returning to its main thread. */
+            CrossThreadUnretained(this), WrapCrossThreadPersistent(raw_data)));
+  }
+
+  void Write(DOMArrayBuffer* raw_data) {
+    DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
+
+    uint8_t* raw_data_pointer = static_cast<uint8_t*>(raw_data->Data());
+    mojo_base::BigBuffer buffer(std::vector<uint8_t>(
+        raw_data_pointer, raw_data_pointer + raw_data->ByteLengthAsSizeT()));
+
+    SystemClipboard::GetInstance().WriteRawData(mime_type_, std::move(buffer));
+
+    promise_->CompleteWriteRepresentation();
+  }
+
+  String mime_type_;
+};
+
 }  // anonymous namespace
 
 // ClipboardWriter functions.
@@ -102,7 +142,12 @@
 // static
 std::unique_ptr<ClipboardWriter> ClipboardWriter::Create(
     const String& mime_type,
+    bool is_raw,
     ClipboardPromise* promise) {
+  DCHECK(base::FeatureList::IsEnabled(blink::features::kRawClipboard) ||
+         !is_raw);
+  if (is_raw)
+    return std::make_unique<ClipboardRawDataWriter>(promise, mime_type);
   if (mime_type == kMimeTypeImagePng)
     return std::make_unique<ClipboardImageWriter>(promise);
   if (mime_type == kMimeTypeTextPlain)
diff --git a/third_party/blink/renderer/modules/clipboard/clipboard_writer.h b/third_party/blink/renderer/modules/clipboard/clipboard_writer.h
index 475e182..63eac11 100644
--- a/third_party/blink/renderer/modules/clipboard/clipboard_writer.h
+++ b/third_party/blink/renderer/modules/clipboard/clipboard_writer.h
@@ -25,8 +25,10 @@
 // (3) Writing the blob's decoded contents to the system clipboard.
 class ClipboardWriter : public FileReaderLoaderClient {
  public:
-  static std::unique_ptr<ClipboardWriter> Create(const String& mime_type,
-                                                 ClipboardPromise* promise);
+  static std::unique_ptr<ClipboardWriter> Create(
+      const String& mime_type,
+      bool allow_without_sanitization,
+      ClipboardPromise* promise);
   ~ClipboardWriter() override;
 
   static bool IsValidType(const String& type);
diff --git a/third_party/blink/renderer/modules/modules_idl_files.gni b/third_party/blink/renderer/modules/modules_idl_files.gni
index 5224345..4a6022c 100644
--- a/third_party/blink/renderer/modules/modules_idl_files.gni
+++ b/third_party/blink/renderer/modules/modules_idl_files.gni
@@ -583,6 +583,7 @@
           "canvas/canvas2d/canvas_rendering_context_2d_settings.idl",
           "canvas/canvas2d/hit_region_options.idl",
           "canvas/htmlcanvas/canvas_context_creation_attributes_module.idl",
+          "clipboard/clipboard_item_options.idl",
           "contacts_picker/contact_info.idl",
           "contacts_picker/contacts_select_options.idl",
           "content_index/content_description.idl",
diff --git a/third_party/blink/renderer/platform/runtime_enabled_features.json5 b/third_party/blink/renderer/platform/runtime_enabled_features.json5
index e3b8873..e00da9dd 100644
--- a/third_party/blink/renderer/platform/runtime_enabled_features.json5
+++ b/third_party/blink/renderer/platform/runtime_enabled_features.json5
@@ -1392,6 +1392,10 @@
       status: "experimental",
     },
     {
+      // Enabled when blink::features::kRawClipboard is enabled.
+      name: "RawClipboard",
+    },
+    {
       name: "ReducedReferrerGranularity",
     },
     {
diff --git a/third_party/blink/web_tests/NeverFixTests b/third_party/blink/web_tests/NeverFixTests
index e36b0e9..c527fdf 100644
--- a/third_party/blink/web_tests/NeverFixTests
+++ b/third_party/blink/web_tests/NeverFixTests
@@ -1423,6 +1423,7 @@
 external/wpt/battery-status/battery-plugging-in-manual.https.html [ Skip ]
 external/wpt/battery-status/battery-unplugging-manual.https.html [ Skip ]
 external/wpt/clipboard-apis/async-navigator-clipboard-basics.https.html [ Skip ]
+external/wpt/clipboard-apis/async-raw-write-read.tentative.https.html [ Skip ]
 external/wpt/clipboard-apis/async-write-blobs-read-blobs-manual.https.html [ Skip ]
 external/wpt/clipboard-apis/async-write-image-read-image-manual.https.html [ Skip ]
 external/wpt/clipboard-apis/events/copy-event-manual.html [ Skip ]
diff --git a/third_party/blink/web_tests/VirtualTestSuites b/third_party/blink/web_tests/VirtualTestSuites
index 598ad42..da206d2d 100644
--- a/third_party/blink/web_tests/VirtualTestSuites
+++ b/third_party/blink/web_tests/VirtualTestSuites
@@ -654,5 +654,11 @@
               "fast/html",
               "fast/parser"],
     "args": ["--disable-blink-features=ShadowDOMV0,CustomElementsV0,HTMLImports"]
+  },
+  {
+    "prefix": "raw-clipboard",
+    "bases": ["clipboard/async-clipboard",
+              "external/wpt/clipboard-apis/clipboard-item.https.html"],
+    "args": ["--enable-features=RawClipboard"]
   }
 ]
diff --git a/third_party/blink/web_tests/clipboard/async-clipboard/async-raw-write-read.tentative.https-expected.txt b/third_party/blink/web_tests/clipboard/async-clipboard/async-raw-write-read.tentative.https-expected.txt
new file mode 100644
index 0000000..17c9fc2
--- /dev/null
+++ b/third_party/blink/web_tests/clipboard/async-clipboard/async-raw-write-read.tentative.https-expected.txt
@@ -0,0 +1,4 @@
+This is a testharness.js-based test.
+FAIL Verify write and read clipboard given arbitrary raw input: Async Clipboard raw write -> Async Clipboard raw read tests promise_test: Unhandled rejection with value: object "NotAllowedError: Sanitized MIME type chromium/x-test-format not supported on write."
+Harness: the test ran to completion.
+
diff --git a/third_party/blink/web_tests/clipboard/async-clipboard/async-raw-write-read.tentative.https.html b/third_party/blink/web_tests/clipboard/async-clipboard/async-raw-write-read.tentative.https.html
new file mode 100644
index 0000000..e40ccc0
--- /dev/null
+++ b/third_party/blink/web_tests/clipboard/async-clipboard/async-raw-write-read.tentative.https.html
@@ -0,0 +1,26 @@
+<!DOCTYPE html>
+<meta charset="utf-8">
+<title> Async Clipboard raw write -> Async Clipboard raw read tests </title>
+<link rel="help" href="https://w3c.github.io/clipboard-apis/#async-clipboard-api">
+<script src="../../resources/testharness.js"></script>
+<script src="../../resources/testharnessreport.js"></script>
+<script src="../../http/tests/resources/permissions-helper.js"></script>
+<script>
+async function readWriteTest(rawInput) {
+  promise_test(async t => {
+    await PermissionsHelper.setPermission('clipboard-read-write', 'granted');
+    await PermissionsHelper.setPermission('clipboard-sanitized-write',
+                                          'granted');
+
+    const blobInput = new Blob([rawInput], {type: 'chromium/x-test-format'});
+    const clipboardItem = new ClipboardItem(
+        {'chromium/x-test-format': blobInput}, {raw: true});
+
+    await navigator.clipboard.write([clipboardItem]);
+    // TODO(https://crbug.com/897289): Implement raw clipboard read.
+
+  }, 'Verify write and read clipboard given arbitrary raw input: ' + rawInput);
+}
+
+readWriteTest('Async Clipboard raw write -> Async Clipboard raw read tests');
+</script>
diff --git a/third_party/blink/web_tests/external/wpt/clipboard-apis/async-raw-write-read.tentative.https.html b/third_party/blink/web_tests/external/wpt/clipboard-apis/async-raw-write-read.tentative.https.html
new file mode 100644
index 0000000..6cfa2db
--- /dev/null
+++ b/third_party/blink/web_tests/external/wpt/clipboard-apis/async-raw-write-read.tentative.https.html
@@ -0,0 +1,26 @@
+<!DOCTYPE html>
+<meta charset="utf-8">
+<title> Async Clipboard raw write -> Async Clipboard raw read tests </title>
+<link rel="help" href="https://w3c.github.io/clipboard-apis/#async-clipboard-api">
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script>
+async function readWriteTest(rawInput) {
+  promise_test(async t => {
+    const blobInput = new Blob([rawInput], {type: 'chromium/x-test-format'});
+    const clipboardItem = new ClipboardItem(
+        {'chromium/x-test-format': blobInput}, {raw: true});
+
+    await navigator.clipboard.write([clipboardItem]);
+    // TODO(https://crbug.com/897289): Implement raw clipboard read.
+
+  }, 'Verify write and read clipboard given arbitrary raw input: ' + rawInput);
+}
+
+readWriteTest('Async Clipboard raw write -> Async Clipboard raw read tests');
+</script>
+<p>
+  Note: This is a manual test because it writes/reads to the shared system
+  clipboard and thus cannot be run async with other tests that might interact
+  with the clipboard.
+</p>
diff --git a/third_party/blink/web_tests/external/wpt/clipboard-apis/clipboard-item.https-expected.txt b/third_party/blink/web_tests/external/wpt/clipboard-apis/clipboard-item.https-expected.txt
new file mode 100644
index 0000000..ad40535
--- /dev/null
+++ b/third_party/blink/web_tests/external/wpt/clipboard-apis/clipboard-item.https-expected.txt
@@ -0,0 +1,14 @@
+This is a testharness.js-based test.
+PASS ClipboardItem({string, Blob}) succeeds with different types
+PASS ClipboardItem() succeeds with empty options
+PASS ClipboardItem({}) fails with empty dictionary input
+PASS ClipboardItem(Blob) fails
+PASS ClipboardItem() fails with null input
+PASS ClipboardItem() fails with no input
+PASS types() returns correct values
+FAIL raw() returns correct values, defaulting to false assert_equals: expected (boolean) false but got (undefined) undefined
+PASS getType(DOMString valid type) succeeds with correct output
+PASS getType(DOMString invalid type) succeeds with correct output
+PASS getType(DOMString type) rejects correctly when querying for missing type
+Harness: the test ran to completion.
+
diff --git a/third_party/blink/web_tests/external/wpt/clipboard-apis/clipboard-item.https.html b/third_party/blink/web_tests/external/wpt/clipboard-apis/clipboard-item.https.html
index 9218ee29..fe5e76c 100644
--- a/third_party/blink/web_tests/external/wpt/clipboard-apis/clipboard-item.https.html
+++ b/third_party/blink/web_tests/external/wpt/clipboard-apis/clipboard-item.https.html
@@ -47,6 +47,20 @@
   assert_equals(types2[1], 'not a/real type');
 }, "types() returns correct values");
 
+test(() => {
+  const item = new ClipboardItem({'text/plain': blob});
+  assert_equals(item.raw, false);
+
+  const item2 = new ClipboardItem({'text/plain': blob}, {});
+  assert_equals(item2.raw, false);
+
+  const item3 = new ClipboardItem({'text/plain': blob}, {raw: false});
+  assert_equals(item3.raw, false);
+
+  const item4 = new ClipboardItem({'text/plain': blob}, {raw: true});
+  assert_equals(item4.raw, true);
+}, "raw() returns correct values, defaulting to false");
+
 promise_test(async () => {
   const item =
       new ClipboardItem({'text/plain': blob, 'not a/real type': blob2});
diff --git a/third_party/blink/web_tests/virtual/raw-clipboard/README.md b/third_party/blink/web_tests/virtual/raw-clipboard/README.md
new file mode 100644
index 0000000..af2c60d
--- /dev/null
+++ b/third_party/blink/web_tests/virtual/raw-clipboard/README.md
@@ -0,0 +1,4 @@
+# Clipboard
+This directory contains (tentative) tests for the [Raw Clipboard](https://github.com/WICG/raw-clipboard-access/blob/master/explainer.md) proposal.
+
+**This suite runs the tests with** `--enable-features=RawClipboard`
\ No newline at end of file
diff --git a/third_party/blink/web_tests/virtual/raw-clipboard/clipboard/async-clipboard/README.txt b/third_party/blink/web_tests/virtual/raw-clipboard/clipboard/async-clipboard/README.txt
new file mode 100644
index 0000000..af2c60d
--- /dev/null
+++ b/third_party/blink/web_tests/virtual/raw-clipboard/clipboard/async-clipboard/README.txt
@@ -0,0 +1,4 @@
+# Clipboard
+This directory contains (tentative) tests for the [Raw Clipboard](https://github.com/WICG/raw-clipboard-access/blob/master/explainer.md) proposal.
+
+**This suite runs the tests with** `--enable-features=RawClipboard`
\ No newline at end of file
diff --git a/third_party/blink/web_tests/virtual/raw-clipboard/clipboard/async-clipboard/async-raw-write-read.tentative.https-expected.txt b/third_party/blink/web_tests/virtual/raw-clipboard/clipboard/async-clipboard/async-raw-write-read.tentative.https-expected.txt
new file mode 100644
index 0000000..d78b92b
--- /dev/null
+++ b/third_party/blink/web_tests/virtual/raw-clipboard/clipboard/async-clipboard/async-raw-write-read.tentative.https-expected.txt
@@ -0,0 +1,4 @@
+This is a testharness.js-based test.
+PASS Verify write and read clipboard given arbitrary raw input: Async Clipboard raw write -> Async Clipboard raw read tests
+Harness: the test ran to completion.
+
diff --git a/tools/metrics/histograms/enums.xml b/tools/metrics/histograms/enums.xml
index e2c45ef..e36d4b1 100644
--- a/tools/metrics/histograms/enums.xml
+++ b/tools/metrics/histograms/enums.xml
@@ -38223,6 +38223,7 @@
   <int value="649111851" label="MidiManagerCros:enabled"/>
   <int value="649508040" label="AutofillEnableCompanyName:enabled"/>
   <int value="651421878" label="VideoRotateToFullscreen:enabled"/>
+  <int value="651562604" label="RawClipboard:enabled"/>
   <int value="651844675" label="EasyUnlockPromotions:enabled"/>
   <int value="652561231" label="CustomContextMenu:enabled"/>
   <int value="654199907" label="AllowSyncXHRInPageDismissal:disabled"/>
@@ -38625,6 +38626,7 @@
   <int value="1185424279" label="enable-media-router"/>
   <int value="1188109510" label="AssistantAudioEraser:enabled"/>
   <int value="1190035852" label="MediaRemoting:enabled"/>
+  <int value="1191531211" label="RawClipboard:disabled"/>
   <int value="1192302892" label="gesture-typing"/>
   <int value="1192913630" label="OfflinePagesBackgroundLoading:disabled"/>
   <int value="1193385506" label="OmniboxLooseMaxLimitOnDedicatedRows:enabled"/>