smbprovider: Implement CopyEntry (nonrecursive)

This change implements non-recursive CopyEntry on
SmbProvider.

A follow up for recursive CopyEntry will build on this
by creating a PreDepthFirstIterator and calling CopyEntry
on each entry iteratred over.

TEST=none yet
BUG=chromium:757625
Change-Id: I1f1237709f665e79bc0a79cd02b86371bb055f65
Reviewed-on: https://chromium-review.googlesource.com/924623
Commit-Ready: Bailey Berro <baileyberro@chromium.org>
Tested-by: Bailey Berro <baileyberro@chromium.org>
Reviewed-by: Zentaro Kavanagh <zentaro@chromium.org>
diff --git a/smbprovider/smbprovider.cc b/smbprovider/smbprovider.cc
index 81d3732..510edd3 100644
--- a/smbprovider/smbprovider.cc
+++ b/smbprovider/smbprovider.cc
@@ -297,8 +297,17 @@
 }
 
 int32_t SmbProvider::CopyEntry(const ProtoBlob& options_blob) {
-  NOTIMPLEMENTED();
-  return 0;
+  int32_t error_code;
+  std::string source_path;
+  std::string target_path;
+  CopyEntryOptionsProto options;
+
+  const bool success =
+      ParseOptionsAndPaths(options_blob, &options, &source_path, &target_path,
+                           &error_code) &&
+      CopyEntry(options, source_path, target_path, &error_code);
+
+  return success ? static_cast<int32_t>(ERROR_OK) : error_code;
 }
 
 template <typename Proto>
@@ -449,17 +458,11 @@
 
   buffer->resize(options.length());
   size_t bytes_read;
-  int32_t result = samba_interface_->ReadFile(options.file_id(), buffer->data(),
-                                              buffer->size(), &bytes_read);
-  if (result != 0) {
-    LogAndSetError(options, GetErrorFromErrno(result), error_code);
+
+  if (!ReadToBuffer(options, options.file_id(), buffer, &bytes_read,
+                    error_code)) {
     return false;
   }
-
-  DCHECK_GE(bytes_read, 0);
-  DCHECK_LE(bytes_read, buffer->size());
-  // Make sure buffer is only as big as bytes_read.
-  buffer->resize(bytes_read);
   return true;
 }
 
@@ -694,4 +697,90 @@
   return true;
 }
 
+bool SmbProvider::CopyEntry(const CopyEntryOptionsProto& options,
+                            const std::string& source_path,
+                            const std::string& target_path,
+                            int32_t* error_code) {
+  DCHECK(error_code);
+  bool is_directory;
+  int32_t get_type_result;
+  if (!GetEntryType(source_path, &get_type_result, &is_directory)) {
+    LogAndSetError(options, GetErrorFromErrno(get_type_result), error_code);
+    return false;
+  }
+
+  if (is_directory) {
+    return CreateSingleDirectory(options, target_path,
+                                 false /* ignore_existing */, error_code);
+  }
+
+  return CopyFile(options, source_path, target_path, error_code);
+}
+
+bool SmbProvider::CopyFile(const CopyEntryOptionsProto& options,
+                           const std::string& source_path,
+                           const std::string& target_path,
+                           int32_t* error_code) {
+  DCHECK(error_code);
+
+  int32_t target_file_id;
+  int32_t source_file_id;
+  bool success =
+      CreateFile(options, target_path, &target_file_id, error_code) &&
+      OpenFile(options, source_path, error_code, &source_file_id) &&
+      CopyData(options, source_file_id, target_file_id, error_code) &&
+      CloseFile(options, source_file_id, error_code) &&
+      CloseFile(options, target_file_id, error_code);
+
+  return success;
+}
+
+bool SmbProvider::CopyData(const CopyEntryOptionsProto& options,
+                           int32_t source_fd,
+                           int32_t target_fd,
+                           int32_t* error_code) {
+  DCHECK(error_code);
+
+  std::vector<uint8_t> buffer;
+  buffer.resize(kBufferSize);
+
+  size_t bytes_read;
+  while (ReadToBuffer(options, source_fd, &buffer, &bytes_read, error_code)) {
+    if (bytes_read == 0) {
+      // reached end of file successfully.
+      return true;
+    }
+
+    if (!WriteFileFromBuffer(options, target_fd, buffer, error_code)) {
+      return false;
+    }
+  }
+
+  return false;
+}
+
+template <typename Proto>
+bool SmbProvider::ReadToBuffer(const Proto& options,
+                               int32_t file_id,
+                               std::vector<uint8_t>* buffer,
+                               size_t* bytes_read,
+                               int32_t* error_code) {
+  DCHECK(buffer);
+  DCHECK(bytes_read);
+  DCHECK(error_code);
+
+  int32_t result = samba_interface_->ReadFile(file_id, buffer->data(),
+                                              buffer->size(), bytes_read);
+  if (result != 0) {
+    LogAndSetError(options, GetErrorFromErrno(result), error_code);
+    return false;
+  }
+
+  DCHECK_GE(*bytes_read, 0);
+  DCHECK_LE(*bytes_read, buffer->size());
+  // Make sure buffer is only as big as bytes_read.
+  buffer->resize(*bytes_read);
+  return true;
+}
+
 }  // namespace smbprovider
diff --git a/smbprovider/smbprovider.h b/smbprovider/smbprovider.h
index eff79b6..db773f4 100644
--- a/smbprovider/smbprovider.h
+++ b/smbprovider/smbprovider.h
@@ -267,6 +267,37 @@
                   int32_t* file_id,
                   int32_t* error);
 
+  // Copies the entry at |source_path| to |target_path|. Returns true on
+  // success. Returns false and sets |error_code| on failure.
+  bool CopyEntry(const CopyEntryOptionsProto& options,
+                 const std::string& source_path,
+                 const std::string& target_path,
+                 int32_t* error_code);
+
+  // Copies the file at |source_path| to a created file at |target_path|.
+  // Returns true on success. Returns false and sets |error_code| on failure.
+  bool CopyFile(const CopyEntryOptionsProto& options,
+                const std::string& source_path,
+                const std::string& target_path,
+                int32_t* error_code);
+
+  // Copies the data at open file |source_fd| to open file |target_fd|. Returns
+  // true on success. Returns false and sets |error_code| on failure.
+  bool CopyData(const CopyEntryOptionsProto& options,
+                int32_t source_fd,
+                int32_t target_fd,
+                int32_t* error_code);
+
+  // Helper method that fills |buffer| by reading the file with handle
+  // |file_id|. Returns true and sets |bytes_read| on success. Returns false and
+  // sets |error_code| on failure. |options| is used for logging.
+  template <typename Proto>
+  bool ReadToBuffer(const Proto& options,
+                    int32_t file_id,
+                    std::vector<uint8_t>* buffer,
+                    size_t* bytes_read,
+                    int32_t* error_code);
+
   std::unique_ptr<SambaInterface> samba_interface_;
   std::unique_ptr<brillo::dbus_utils::DBusObject> dbus_object_;
   std::unique_ptr<MountManager> mount_manager_;
diff --git a/smbprovider/smbprovider_test.cc b/smbprovider/smbprovider_test.cc
index 12fc018..8fb421d 100644
--- a/smbprovider/smbprovider_test.cc
+++ b/smbprovider/smbprovider_test.cc
@@ -2029,4 +2029,84 @@
   EXPECT_EQ(ERROR_ACCESS_DENIED, CastError(smbprovider_->MoveEntry(move_blob)));
 }
 
+TEST_F(SmbProviderTest, CopyEntryFailsOnInvalidSource) {
+  const int32_t mount_id = PrepareMount();
+
+  fake_samba_->AddDirectory(GetDefaultFullPath("newdir"));
+
+  ProtoBlob copy_blob =
+      CreateCopyEntryOptionsBlob(mount_id, "/file.txt", "/newdir/file.txt");
+
+  EXPECT_EQ(ERROR_NOT_FOUND, CastError(smbprovider_->CopyEntry(copy_blob)));
+}
+
+TEST_F(SmbProviderTest, CopyEntryFailsOnFileWhenDestinationExists) {
+  const int32_t mount_id = PrepareMount();
+
+  fake_samba_->AddFile(GetDefaultFullPath("/file.txt"));
+  fake_samba_->AddDirectory(GetDefaultFullPath("/dir1"));
+  fake_samba_->AddFile(GetDefaultFullPath("/dir1/file.txt"));
+
+  ProtoBlob copy_blob =
+      CreateCopyEntryOptionsBlob(mount_id, "/file.txt", "/dir1/file.txt");
+
+  EXPECT_EQ(ERROR_EXISTS, CastError(smbprovider_->CopyEntry(copy_blob)));
+}
+
+TEST_F(SmbProviderTest, CopyEntryFailsOnDirectoryWhenDestinationExists) {
+  const int32_t mount_id = PrepareMount();
+
+  fake_samba_->AddDirectory(GetDefaultFullPath("/dogs"));
+  fake_samba_->AddDirectory(GetDefaultFullPath("/cats"));
+  fake_samba_->AddDirectory(GetDefaultFullPath("/cats/dogs"));
+
+  ProtoBlob copy_blob =
+      CreateCopyEntryOptionsBlob(mount_id, "/dogs", "/cats/dogs");
+
+  EXPECT_EQ(ERROR_EXISTS, CastError(smbprovider_->CopyEntry(copy_blob)));
+}
+
+TEST_F(SmbProviderTest, CopyEntryFailsWhenDestinationIsInALockedDir) {
+  const int32_t mount_id = PrepareMount();
+
+  fake_samba_->AddFile(GetDefaultFullPath("/dog.jpg"));
+  fake_samba_->AddLockedDirectory(GetDefaultFullPath("/cats"));
+
+  ProtoBlob copy_blob =
+      CreateCopyEntryOptionsBlob(mount_id, "/dog.jpg", "/cats/dog.jpg");
+
+  EXPECT_EQ(ERROR_ACCESS_DENIED, CastError(smbprovider_->CopyEntry(copy_blob)));
+}
+
+TEST_F(SmbProviderTest, CopyEntrySucceedsOnFile) {
+  const std::vector<uint8_t> file_data = {10, 11, 12, 13, 14, 15};
+  const int32_t mount_id = PrepareMount();
+
+  fake_samba_->AddFile(GetDefaultFullPath("/dog1.jpg"), kFileDate, file_data);
+  fake_samba_->AddDirectory(GetDefaultFullPath("/dogs"));
+
+  ProtoBlob copy_blob =
+      CreateCopyEntryOptionsBlob(mount_id, "/dog1.jpg", "/dogs/dog1.jpg");
+
+  EXPECT_EQ(ERROR_OK, CastError(smbprovider_->CopyEntry(copy_blob)));
+
+  EXPECT_TRUE(fake_samba_->EntryExists(GetDefaultFullPath("/dog1.jpg")));
+  EXPECT_TRUE(fake_samba_->EntryExists(GetDefaultFullPath("/dogs/dog1.jpg")));
+}
+
+TEST_F(SmbProviderTest, CopyEntrySucceedsOnDirectory) {
+  const int32_t mount_id = PrepareMount();
+
+  fake_samba_->AddDirectory(GetDefaultFullPath("/dogs"));
+  fake_samba_->AddDirectory(GetDefaultFullPath("/animals"));
+
+  ProtoBlob copy_blob =
+      CreateCopyEntryOptionsBlob(mount_id, "/dogs", "/animals/dogs");
+
+  EXPECT_EQ(ERROR_OK, CastError(smbprovider_->CopyEntry(copy_blob)));
+
+  EXPECT_TRUE(fake_samba_->EntryExists(GetDefaultFullPath("/dogs")));
+  EXPECT_TRUE(fake_samba_->EntryExists(GetDefaultFullPath("/animals/dogs")));
+}
+
 }  // namespace smbprovider