Add support for attachments to generic crash report database

This is the beginning of support for attachments at the process level
being stored alongside a report. Attachments will be uploaded by key as
part of the multipart http upload. There's no interface at the client
level yet to pass these through.

As this is intended for Fuchsia, this is not yet implemented for the
Mac/Windows database implementations.

Bug: crashpad:196
Change-Id: Ieaf580675164b631d313193f97375710979ba2a9
Reviewed-on: https://chromium-review.googlesource.com/1060419
Commit-Queue: Scott Graham <scottmg@chromium.org>
Reviewed-by: Joshua Peraza <jperaza@chromium.org>
diff --git a/client/crash_report_database.cc b/client/crash_report_database.cc
index afd751d..aa365be 100644
--- a/client/crash_report_database.cc
+++ b/client/crash_report_database.cc
@@ -14,6 +14,7 @@
 
 #include "client/crash_report_database.h"
 
+#include "base/logging.h"
 #include "build/build_config.h"
 
 namespace crashpad {
@@ -29,13 +30,21 @@
       upload_explicitly_requested(false) {}
 
 CrashReportDatabase::NewReport::NewReport()
-    : writer_(std::make_unique<FileWriter>()), uuid_(), file_remover_() {}
+    : writer_(std::make_unique<FileWriter>()),
+      file_remover_(),
+      attachment_writers_(),
+      attachment_removers_(),
+      uuid_(),
+      database_() {}
 
 CrashReportDatabase::NewReport::~NewReport() = default;
 
 bool CrashReportDatabase::NewReport::Initialize(
+    CrashReportDatabase* database,
     const base::FilePath& directory,
     const base::FilePath::StringType& extension) {
+  database_ = database;
+
   if (!uuid_.InitializeWithNew()) {
     return false;
   }
@@ -56,7 +65,11 @@
 }
 
 CrashReportDatabase::UploadReport::UploadReport()
-    : Report(), reader_(std::make_unique<FileReader>()), database_(nullptr) {}
+    : Report(),
+      reader_(std::make_unique<FileReader>()),
+      database_(nullptr),
+      attachment_readers_(),
+      attachment_map_() {}
 
 CrashReportDatabase::UploadReport::~UploadReport() {
   if (database_) {
@@ -67,6 +80,7 @@
 bool CrashReportDatabase::UploadReport::Initialize(const base::FilePath path,
                                                    CrashReportDatabase* db) {
   database_ = db;
+  InitializeAttachments();
   return reader_->Open(path);
 }
 
diff --git a/client/crash_report_database.h b/client/crash_report_database.h
index efa7a77..9ceeddc 100644
--- a/client/crash_report_database.h
+++ b/client/crash_report_database.h
@@ -17,6 +17,7 @@
 
 #include <time.h>
 
+#include <map>
 #include <memory>
 #include <string>
 #include <vector>
@@ -112,19 +113,35 @@
 
     //! A unique identifier by which this report will always be known to the
     //! database.
-    const UUID& ReportID() { return uuid_; }
+    const UUID& ReportID() const { return uuid_; }
+
+    //! \brief Adds an attachment to the report.
+    //!
+    //! \note This function is not yet implemented on macOS or Windows.
+    //!
+    //! \param[in] name The key and name for the attachment, which will be
+    //!     included in the http upload. The attachment will not appear in the
+    //!     minidump report. \a name should only use characters from the set
+    //!     `[a-zA-Z0-9._-]`.
+    //! \return A FileWriter that the caller should use to write the contents of
+    //!     the attachment, or `nullptr` on failure with an error logged.
+    FileWriter* AddAttachment(const std::string& name);
 
    private:
     friend class CrashReportDatabaseGeneric;
     friend class CrashReportDatabaseMac;
     friend class CrashReportDatabaseWin;
 
-    bool Initialize(const base::FilePath& directory,
+    bool Initialize(CrashReportDatabase* database,
+                    const base::FilePath& directory,
                     const base::FilePath::StringType& extension);
 
     std::unique_ptr<FileWriter> writer_;
-    UUID uuid_;
     ScopedRemoveFile file_remover_;
+    std::vector<std::unique_ptr<FileWriter>> attachment_writers_;
+    std::vector<ScopedRemoveFile> attachment_removers_;
+    UUID uuid_;
+    CrashReportDatabase* database_;
 
     DISALLOW_COPY_AND_ASSIGN(NewReport);
   };
@@ -137,9 +154,17 @@
     UploadReport();
     virtual ~UploadReport();
 
-    // An open FileReader with which to read the report.
+    //! \brief An open FileReader with which to read the report.
     FileReader* Reader() const { return reader_.get(); }
 
+    //! \brief Obtains a mapping of names to file readers for any attachments
+    //!     for the report.
+    //!
+    //! This is not implemented on macOS or Windows.
+    std::map<std::string, FileReader*> GetAttachments() const {
+      return attachment_map_;
+    };
+
    private:
     friend class CrashReportDatabase;
     friend class CrashReportDatabaseGeneric;
@@ -147,9 +172,12 @@
     friend class CrashReportDatabaseWin;
 
     bool Initialize(const base::FilePath path, CrashReportDatabase* database);
+    void InitializeAttachments();
 
     std::unique_ptr<FileReader> reader_;
     CrashReportDatabase* database_;
+    std::vector<std::unique_ptr<FileReader>> attachment_readers_;
+    std::map<std::string, FileReader*> attachment_map_;
 
     DISALLOW_COPY_AND_ASSIGN(UploadReport);
   };
diff --git a/client/crash_report_database_generic.cc b/client/crash_report_database_generic.cc
index e10c24f..6dc8e9e 100644
--- a/client/crash_report_database_generic.cc
+++ b/client/crash_report_database_generic.cc
@@ -36,6 +36,20 @@
   return base::FilePath(path.RemoveFinalExtension().value() + extension);
 }
 
+UUID UUIDFromReportPath(const base::FilePath& path) {
+  UUID uuid;
+  uuid.InitializeFromString(path.RemoveFinalExtension().BaseName().value());
+  return uuid;
+}
+
+bool AttachmentNameIsOK(const std::string& name) {
+  for (const char c : name) {
+    if (c != '_' && c != '-' && c != '.' && !isalnum(c))
+      return false;
+  }
+  return true;
+}
+
 using OperationStatus = CrashReportDatabase::OperationStatus;
 
 constexpr base::FilePath::CharType kSettings[] =
@@ -53,6 +67,8 @@
     FILE_PATH_LITERAL("pending");
 constexpr base::FilePath::CharType kCompletedDirectory[] =
     FILE_PATH_LITERAL("completed");
+constexpr base::FilePath::CharType kAttachmentsDirectory[] =
+    FILE_PATH_LITERAL("attachments");
 
 constexpr const base::FilePath::CharType* kReportDirectories[] = {
     kNewDirectory,
@@ -171,6 +187,9 @@
   OperationStatus RequestUpload(const UUID& uuid) override;
   int CleanDatabase(time_t lockfile_ttl) override;
 
+  // Build a filepath for the directory for the report to hold attachments.
+  base::FilePath AttachmentsPath(const UUID& uuid);
+
  private:
   struct LockfileUploadReport : public UploadReport {
     ScopedLockFile lock_file;
@@ -225,11 +244,18 @@
   // Cleans lone metadata, reports, or expired locks in a particular state.
   int CleanReportsInState(ReportState state, time_t lockfile_ttl);
 
+  // Cleans any attachments that have no associated report in any state.
+  void CleanOrphanedAttachments();
+
+  // Attempt to remove any attachments associated with the given report UUID.
+  // There may not be any, so failing is not an error.
+  void RemoveAttachmentsByUUID(const UUID& uuid);
+
   // Reads the metadata for a report from path and returns it in report.
   static bool ReadMetadata(const base::FilePath& path, Report* report);
 
   // Wraps ReadMetadata and removes the report from the database on failure.
-  static bool CleaningReadMetadata(const base::FilePath& path, Report* report);
+  bool CleaningReadMetadata(const base::FilePath& path, Report* report);
 
   // Writes metadata for a new report to the filesystem at path.
   static bool WriteNewMetadata(const base::FilePath& path);
@@ -244,6 +270,59 @@
   DISALLOW_COPY_AND_ASSIGN(CrashReportDatabaseGeneric);
 };
 
+FileWriter* CrashReportDatabase::NewReport::AddAttachment(
+    const std::string& name) {
+  if (!AttachmentNameIsOK(name)) {
+    LOG(ERROR) << "invalid name for attachment " << name;
+    return nullptr;
+  }
+
+  base::FilePath attachments_dir =
+      static_cast<CrashReportDatabaseGeneric*>(database_)->AttachmentsPath(
+          uuid_);
+  if (!LoggingCreateDirectory(
+          attachments_dir, FilePermissions::kOwnerOnly, true)) {
+    return nullptr;
+  }
+
+  base::FilePath path = attachments_dir.Append(name);
+
+  auto writer = std::make_unique<FileWriter>();
+  if (!writer->Open(
+          path, FileWriteMode::kCreateOrFail, FilePermissions::kOwnerOnly)) {
+    LOG(ERROR) << "could not open " << path.value();
+    return nullptr;
+  }
+  attachment_writers_.emplace_back(std::move(writer));
+  attachment_removers_.emplace_back(ScopedRemoveFile(path));
+  return attachment_writers_.back().get();
+}
+
+void CrashReportDatabase::UploadReport::InitializeAttachments() {
+  base::FilePath attachments_dir =
+      static_cast<CrashReportDatabaseGeneric*>(database_)->AttachmentsPath(
+          uuid);
+  DirectoryReader reader;
+  if (!reader.Open(attachments_dir)) {
+    return;
+  }
+
+  base::FilePath filename;
+  DirectoryReader::Result dir_result;
+  while ((dir_result = reader.NextFile(&filename)) ==
+         DirectoryReader::Result::kSuccess) {
+    const base::FilePath filepath(attachments_dir.Append(filename));
+    std::unique_ptr<FileReader> reader(std::make_unique<FileReader>());
+    if (!reader->Open(filepath)) {
+      LOG(ERROR) << "attachment " << filepath.value()
+                 << " couldn't be opened, skipping";
+      continue;
+    }
+    attachment_readers_.emplace_back(std::move(reader));
+    attachment_map_[filename.value()] = attachment_readers_.back().get();
+  }
+}
+
 CrashReportDatabaseGeneric::CrashReportDatabaseGeneric() = default;
 
 CrashReportDatabaseGeneric::~CrashReportDatabaseGeneric() = default;
@@ -260,13 +339,18 @@
   }
 
   for (const base::FilePath::CharType* subdir : kReportDirectories) {
-    if (!LoggingCreateDirectory(base_dir_.Append(subdir),
-                                FilePermissions::kOwnerOnly,
-                                true)) {
+    if (!LoggingCreateDirectory(
+            base_dir_.Append(subdir), FilePermissions::kOwnerOnly, true)) {
       return false;
     }
   }
 
+  if (!LoggingCreateDirectory(base_dir_.Append(kAttachmentsDirectory),
+                              FilePermissions::kOwnerOnly,
+                              true)) {
+    return false;
+  }
+
   if (!settings_.Initialize(base_dir_.Append(kSettings))) {
     return false;
   }
@@ -299,8 +383,8 @@
   INITIALIZATION_STATE_DCHECK_VALID(initialized_);
 
   auto new_report = std::make_unique<NewReport>();
-  if (!new_report->Initialize(base_dir_.Append(kNewDirectory),
-                              kCrashReportExtension)) {
+  if (!new_report->Initialize(
+          this, base_dir_.Append(kNewDirectory), kCrashReportExtension)) {
     return kFileSystemError;
   }
 
@@ -332,6 +416,14 @@
   // We've moved the report to pending, so it no longer needs to be removed.
   ignore_result(report->file_remover_.release());
 
+  // Close all the attachments and disarm their removers too.
+  for (auto& writer : report->attachment_writers_) {
+    writer->Close();
+  }
+  for (auto& remover : report->attachment_removers_) {
+    ignore_result(remover.release());
+  }
+
   *uuid = report->ReportID();
 
   Metrics::CrashReportPending(Metrics::PendingReportReason::kNewlyCreated);
@@ -440,6 +532,8 @@
     return kDatabaseError;
   }
 
+  RemoveAttachmentsByUUID(uuid);
+
   return kNoError;
 }
 
@@ -505,6 +599,7 @@
 
   removed += CleanReportsInState(kPending, lockfile_ttl);
   removed += CleanReportsInState(kCompleted, lockfile_ttl);
+  CleanOrphanedAttachments();
   return removed;
 }
 
@@ -569,6 +664,16 @@
       .Append(uuid_string + kCrashReportExtension);
 }
 
+base::FilePath CrashReportDatabaseGeneric::AttachmentsPath(const UUID& uuid) {
+#if defined(OS_WIN)
+  const std::wstring uuid_string = uuid.ToString16();
+#else
+  const std::string uuid_string = uuid.ToString();
+#endif
+
+  return base_dir_.Append(kAttachmentsDirectory).Append(uuid_string);
+}
+
 OperationStatus CrashReportDatabaseGeneric::LocateAndLockReport(
     const UUID& uuid,
     ReportState desired_state,
@@ -688,6 +793,7 @@
       if (report_lock.ResetAcquire(filepath) && !IsRegularFile(metadata_path) &&
           LoggingRemoveFile(filepath)) {
         ++removed;
+        RemoveAttachmentsByUUID(UUIDFromReportPath(filepath));
       }
       continue;
     }
@@ -700,6 +806,7 @@
       if (report_lock.ResetAcquire(report_path) &&
           !IsRegularFile(report_path) && LoggingRemoveFile(filepath)) {
         ++removed;
+        RemoveAttachmentsByUUID(UUIDFromReportPath(filepath));
       }
       continue;
     }
@@ -717,6 +824,7 @@
 
       if (LoggingRemoveFile(filepath)) {
         ++removed;
+        RemoveAttachmentsByUUID(UUIDFromReportPath(filepath));
       }
       continue;
     }
@@ -725,6 +833,67 @@
   return removed;
 }
 
+void CrashReportDatabaseGeneric::CleanOrphanedAttachments() {
+  base::FilePath root_attachments_dir(base_dir_.Append(kAttachmentsDirectory));
+  DirectoryReader reader;
+  if (!reader.Open(root_attachments_dir)) {
+    LOG(ERROR) << "no attachments dir";
+    return;
+  }
+
+  base::FilePath filename;
+  DirectoryReader::Result result;
+  while ((result = reader.NextFile(&filename)) ==
+         DirectoryReader::Result::kSuccess) {
+    const base::FilePath path(root_attachments_dir.Append(filename));
+    if (IsDirectory(path, false)) {
+      UUID uuid;
+      if (!uuid.InitializeFromString(filename.value())) {
+        LOG(ERROR) << "unexpected attachment dir name " << filename.value();
+        continue;
+      }
+
+      // Check to see if the report is being created in "new".
+      base::FilePath new_dir_path =
+          base_dir_.Append(kNewDirectory)
+              .Append(uuid.ToString() + kCrashReportExtension);
+      if (IsRegularFile(new_dir_path)) {
+        continue;
+      }
+
+      // Check to see if the report is in "pending" or "completed".
+      ScopedLockFile local_lock;
+      base::FilePath local_path;
+      OperationStatus os =
+          LocateAndLockReport(uuid, kSearchable, &local_path, &local_lock);
+      if (os != kReportNotFound) {
+        continue;
+      }
+
+      // Couldn't find a report, assume these attachments are orphaned.
+      RemoveAttachmentsByUUID(uuid);
+    }
+  }
+}
+
+void CrashReportDatabaseGeneric::RemoveAttachmentsByUUID(const UUID& uuid) {
+  base::FilePath attachments_dir = AttachmentsPath(uuid);
+  DirectoryReader reader;
+  if (!reader.Open(attachments_dir)) {
+    return;
+  }
+
+  base::FilePath filename;
+  DirectoryReader::Result result;
+  while ((result = reader.NextFile(&filename)) ==
+         DirectoryReader::Result::kSuccess) {
+    const base::FilePath filepath(attachments_dir.Append(filename));
+    LoggingRemoveFile(filepath);
+  }
+
+  LoggingRemoveDirectory(attachments_dir);
+}
+
 // static
 bool CrashReportDatabaseGeneric::ReadMetadata(const base::FilePath& path,
                                               Report* report) {
@@ -766,7 +935,6 @@
   return true;
 }
 
-// static
 bool CrashReportDatabaseGeneric::CleaningReadMetadata(
     const base::FilePath& path,
     Report* report) {
@@ -776,6 +944,7 @@
 
   LoggingRemoveFile(path);
   LoggingRemoveFile(ReplaceFinalExtension(path, kMetadataExtension));
+  RemoveAttachmentsByUUID(report->uuid);
   return false;
 }
 
diff --git a/client/crash_report_database_mac.mm b/client/crash_report_database_mac.mm
index d0197fc..79b876d 100644
--- a/client/crash_report_database_mac.mm
+++ b/client/crash_report_database_mac.mm
@@ -245,6 +245,16 @@
   DISALLOW_COPY_AND_ASSIGN(CrashReportDatabaseMac);
 };
 
+FileWriter* CrashReportDatabase::NewReport::AddAttachment(
+    const std::string& name) {
+  // Attachments aren't implemented in the Mac database yet.
+  return nullptr;
+}
+
+void CrashReportDatabase::UploadReport::InitializeAttachments() {
+  // Attachments aren't implemented in the Mac database yet.
+}
+
 CrashReportDatabaseMac::CrashReportDatabaseMac(const base::FilePath& path)
     : CrashReportDatabase(),
       base_dir_(path),
@@ -311,7 +321,8 @@
   INITIALIZATION_STATE_DCHECK_VALID(initialized_);
 
   std::unique_ptr<NewReport> report(new NewReport());
-  if (!report->Initialize(base_dir_.Append(kWriteDirectory),
+  if (!report->Initialize(this,
+                          base_dir_.Append(kWriteDirectory),
                           std::string(".") + kCrashReportFileExtension)) {
     return kFileSystemError;
   }
diff --git a/client/crash_report_database_test.cc b/client/crash_report_database_test.cc
index 55bcf3c..2dbb4fc 100644
--- a/client/crash_report_database_test.cc
+++ b/client/crash_report_database_test.cc
@@ -20,6 +20,7 @@
 #include "test/errors.h"
 #include "test/file.h"
 #include "test/filesystem.h"
+#include "test/gtest_disabled.h"
 #include "test/scoped_temp_dir.h"
 #include "util/file/file_io.h"
 #include "util/file/filesystem.h"
@@ -669,6 +670,101 @@
             CrashReportDatabase::kCannotRequestUpload);
 }
 
+TEST_F(CrashReportDatabaseTest, Attachments) {
+#if defined(OS_MACOSX) || defined(OS_WIN)
+  // Attachments aren't supported on Mac and Windows yet.
+  DISABLED_TEST();
+#else
+  std::unique_ptr<CrashReportDatabase::NewReport> new_report;
+  ASSERT_EQ(db()->PrepareNewCrashReport(&new_report),
+            CrashReportDatabase::kNoError);
+
+  FileWriter* attach_some_file = new_report->AddAttachment("some_file");
+  ASSERT_NE(attach_some_file, nullptr);
+  static constexpr char test_data[] = "test data";
+  attach_some_file->Write(test_data, sizeof(test_data));
+
+  FileWriter* failed_attach = new_report->AddAttachment("not/a valid fi!e");
+  EXPECT_EQ(failed_attach, nullptr);
+
+  UUID expect_uuid = new_report->ReportID();
+  UUID uuid;
+  ASSERT_EQ(db()->FinishedWritingCrashReport(std::move(new_report), &uuid),
+            CrashReportDatabase::kNoError);
+  EXPECT_EQ(uuid, expect_uuid);
+
+  CrashReportDatabase::Report report;
+  EXPECT_EQ(db()->LookUpCrashReport(uuid, &report),
+            CrashReportDatabase::kNoError);
+  ExpectPreparedCrashReport(report);
+
+  std::vector<CrashReportDatabase::Report> reports;
+  EXPECT_EQ(db()->GetPendingReports(&reports), CrashReportDatabase::kNoError);
+  ASSERT_EQ(reports.size(), 1u);
+  EXPECT_EQ(reports[0].uuid, report.uuid);
+
+  std::unique_ptr<const CrashReportDatabase::UploadReport> upload_report;
+  ASSERT_EQ(db()->GetReportForUploading(reports[0].uuid, &upload_report),
+            CrashReportDatabase::kNoError);
+  std::map<std::string, FileReader*> result_attachments =
+      upload_report->GetAttachments();
+  EXPECT_EQ(result_attachments.size(), 1u);
+  EXPECT_NE(result_attachments.find("some_file"), result_attachments.end());
+  char result_buffer[sizeof(test_data)];
+  result_attachments["some_file"]->Read(result_buffer, sizeof(result_buffer));
+  EXPECT_EQ(memcmp(test_data, result_buffer, sizeof(test_data)), 0);
+#endif
+}
+
+TEST_F(CrashReportDatabaseTest, OrphanedAttachments) {
+#if defined(OS_MACOSX) || defined(OS_WIN)
+  // Attachments aren't supported on Mac and Windows yet.
+  DISABLED_TEST();
+#else
+  // TODO: This is using paths that are specific to the generic implementation
+  // and will need to be generalized for Mac and Windows.
+  std::unique_ptr<CrashReportDatabase::NewReport> new_report;
+  ASSERT_EQ(db()->PrepareNewCrashReport(&new_report),
+            CrashReportDatabase::kNoError);
+
+  FileWriter* file1 = new_report->AddAttachment("file1");
+  ASSERT_NE(file1, nullptr);
+  FileWriter* file2 = new_report->AddAttachment("file2");
+  ASSERT_NE(file2, nullptr);
+
+  UUID expect_uuid = new_report->ReportID();
+  UUID uuid;
+  ASSERT_EQ(db()->FinishedWritingCrashReport(std::move(new_report), &uuid),
+            CrashReportDatabase::kNoError);
+  EXPECT_EQ(uuid, expect_uuid);
+
+  CrashReportDatabase::Report report;
+  ASSERT_EQ(db()->LookUpCrashReport(uuid, &report),
+            CrashReportDatabase::kNoError);
+
+  ASSERT_TRUE(LoggingRemoveFile(report.file_path));
+
+  ASSERT_TRUE(LoggingRemoveFile(base::FilePath(
+      report.file_path.RemoveFinalExtension().value() + ".meta")));
+
+  ASSERT_EQ(db()->LookUpCrashReport(uuid, &report),
+            CrashReportDatabase::kReportNotFound);
+
+  base::FilePath report_attachments_dir(
+      path().Append("attachments").Append(uuid.ToString()));
+  base::FilePath file_path1(report_attachments_dir.Append("file1"));
+  base::FilePath file_path2(report_attachments_dir.Append("file2"));
+  EXPECT_TRUE(FileExists(file_path1));
+  EXPECT_TRUE(FileExists(file_path1));
+
+  EXPECT_EQ(db()->CleanDatabase(0), 0);
+
+  EXPECT_FALSE(FileExists(file_path1));
+  EXPECT_FALSE(FileExists(file_path2));
+  EXPECT_FALSE(FileExists(report_attachments_dir));
+#endif
+}
+
 // This test uses knowledge of the database format to break it, so it only
 // applies to the unfified database implementation.
 #if !defined(OS_MACOSX) && !defined(OS_WIN)
diff --git a/client/crash_report_database_win.cc b/client/crash_report_database_win.cc
index fb3cd8f..e19e605 100644
--- a/client/crash_report_database_win.cc
+++ b/client/crash_report_database_win.cc
@@ -615,6 +615,16 @@
   DISALLOW_COPY_AND_ASSIGN(CrashReportDatabaseWin);
 };
 
+FileWriter* CrashReportDatabase::NewReport::AddAttachment(
+    const std::string& name) {
+  // Attachments aren't implemented in the Windows database yet.
+  return nullptr;
+}
+
+void CrashReportDatabase::UploadReport::InitializeAttachments() {
+  // Attachments aren't implemented in the Windows database yet.
+}
+
 CrashReportDatabaseWin::CrashReportDatabaseWin(const base::FilePath& path)
     : CrashReportDatabase(), base_dir_(path), settings_(), initialized_() {}
 
@@ -653,7 +663,8 @@
   INITIALIZATION_STATE_DCHECK_VALID(initialized_);
 
   std::unique_ptr<NewReport> new_report(new NewReport());
-  if (!new_report->Initialize(base_dir_.Append(kReportsDirectory),
+  if (!new_report->Initialize(this,
+                              base_dir_.Append(kReportsDirectory),
                               std::wstring(L".") + kCrashReportFileExtension)) {
     return kFileSystemError;
   }
diff --git a/handler/crash_report_upload_thread.cc b/handler/crash_report_upload_thread.cc
index 715c533..4783ecb 100644
--- a/handler/crash_report_upload_thread.cc
+++ b/handler/crash_report_upload_thread.cc
@@ -284,6 +284,11 @@
     }
   }
 
+  for (const auto& it : report->GetAttachments()) {
+    http_multipart_builder.SetFileAttachment(
+        it.first, it.first, it.second, "application/octet-stream");
+  }
+
   http_multipart_builder.SetFileAttachment(kMinidumpKey,
                                            report->uuid.ToString() + ".dmp",
                                            reader,
diff --git a/handler/fuchsia/crash_report_exception_handler.cc b/handler/fuchsia/crash_report_exception_handler.cc
index 80b34a7..1da0985 100644
--- a/handler/fuchsia/crash_report_exception_handler.cc
+++ b/handler/fuchsia/crash_report_exception_handler.cc
@@ -52,10 +52,12 @@
     CrashReportDatabase* database,
     CrashReportUploadThread* upload_thread,
     const std::map<std::string, std::string>* process_annotations,
+    const std::map<std::string, base::FilePath>* process_attachments,
     const UserStreamDataSources* user_stream_data_sources)
     : database_(database),
       upload_thread_(upload_thread),
       process_annotations_(process_annotations),
+      process_attachments_(process_attachments),
       user_stream_data_sources_(user_stream_data_sources) {}
 
 CrashReportExceptionHandler::~CrashReportExceptionHandler() {}
@@ -144,6 +146,24 @@
       return false;
     }
 
+    if (process_attachments_) {
+      // Note that attachments are read at this point each time rather than once
+      // so that if the contents of the file has changed it will be re-read for
+      // each upload (e.g. in the case of a log file).
+      for (const auto& it : *process_attachments_) {
+        FileWriter* writer = new_report->AddAttachment(it.first);
+        if (writer) {
+          std::string contents;
+          if (!LoggingReadEntireFile(it.second, &contents)) {
+            // Not being able to read the file isn't considered fatal, and
+            // should not prevent the report from being processed.
+            continue;
+          }
+          writer->Write(contents.data(), contents.size());
+        }
+      }
+    }
+
     UUID uuid;
     database_status =
         database_->FinishedWritingCrashReport(std::move(new_report), &uuid);
diff --git a/handler/fuchsia/crash_report_exception_handler.h b/handler/fuchsia/crash_report_exception_handler.h
index 9f14051..5043d7e 100644
--- a/handler/fuchsia/crash_report_exception_handler.h
+++ b/handler/fuchsia/crash_report_exception_handler.h
@@ -21,6 +21,7 @@
 #include <map>
 #include <string>
 
+#include "base/files/file_path.h"
 #include "base/macros.h"
 #include "client/crash_report_database.h"
 #include "handler/crash_report_upload_thread.h"
@@ -47,6 +48,10 @@
   //!     To interoperate with Breakpad servers, the recommended practice is to
   //!     specify values for the `"prod"` and `"ver"` keys as process
   //!     annotations.
+  //! \param[in] process_attachments A map of file name keys to file paths to be
+  //!     included in the report. Each time a report is written, the file paths
+  //!     will be read in their entirety and included in the report using the
+  //!     file name key as the name in the http upload.
   //! \param[in] user_stream_data_sources Data sources to be used to extend
   //!     crash reports. For each crash report that is written, the data sources
   //!     are called in turn. These data sources may contribute additional
@@ -55,6 +60,7 @@
       CrashReportDatabase* database,
       CrashReportUploadThread* upload_thread,
       const std::map<std::string, std::string>* process_annotations,
+      const std::map<std::string, base::FilePath>* process_attachments,
       const UserStreamDataSources* user_stream_data_sources);
 
   ~CrashReportExceptionHandler();
@@ -88,6 +94,7 @@
   CrashReportDatabase* database_;  // weak
   CrashReportUploadThread* upload_thread_;  // weak
   const std::map<std::string, std::string>* process_annotations_;  // weak
+  const std::map<std::string, base::FilePath>* process_attachments_;  // weak
   const UserStreamDataSources* user_stream_data_sources_;  // weak
 
   DISALLOW_COPY_AND_ASSIGN(CrashReportExceptionHandler);
diff --git a/handler/handler_main.cc b/handler/handler_main.cc
index 70192cf..901ee22 100644
--- a/handler/handler_main.cc
+++ b/handler/handler_main.cc
@@ -836,6 +836,10 @@
       database.get(),
       static_cast<CrashReportUploadThread*>(upload_thread.Get()),
       &options.annotations,
+#if defined(OS_FUCHSIA)
+      // TODO(scottmg): Process level file attachments, and for all platforms.
+      nullptr,
+#endif
       user_stream_sources);
 
  #if defined(OS_LINUX) || defined(OS_ANDROID)