adb: add dry-run option to push/sync.

Make it easier to benchmark file sync performance by ignoring the file
system.

Bug: https://issuetracker.google.com/150827486
Test: test_device.py
Change-Id: Icfa4b28eb5206f1914c0c163833d070a3748c3ea
diff --git a/adb/client/adb_install.cpp b/adb/client/adb_install.cpp
index f022b8b..da3154e 100644
--- a/adb/client/adb_install.cpp
+++ b/adb/client/adb_install.cpp
@@ -290,7 +290,7 @@
         }
     }
 
-    if (do_sync_push(apk_file, apk_dest.c_str(), false, CompressionType::Any)) {
+    if (do_sync_push(apk_file, apk_dest.c_str(), false, CompressionType::Any, false)) {
         result = pm_command(argc, argv);
         delete_device_file(apk_dest);
     }
diff --git a/adb/client/commandline.cpp b/adb/client/commandline.cpp
index 02f6e9c..ceb21d5 100644
--- a/adb/client/commandline.cpp
+++ b/adb/client/commandline.cpp
@@ -132,6 +132,7 @@
         " push [--sync] [-z ALGORITHM] [-Z] LOCAL... REMOTE\n"
         "     copy local files/directories to device\n"
         "     --sync: only push files that are newer on the host than the device\n"
+        "     -n: dry run: push files to device without storing to the filesystem\n"
         "     -z: enable compression with a specified algorithm (any, none, brotli)\n"
         "     -Z: disable compression\n"
         " pull [-a] [-z ALGORITHM] [-Z] REMOTE... LOCAL\n"
@@ -141,6 +142,7 @@
         "     -Z: disable compression\n"
         " sync [-l] [-z ALGORITHM] [-Z] [all|data|odm|oem|product|system|system_ext|vendor]\n"
         "     sync a local build from $ANDROID_PRODUCT_OUT to the device (default all)\n"
+        "     -n: dry run: push files to device without storing to the filesystem\n"
         "     -l: list files that would be copied, but don't copy them\n"
         "     -z: enable compression with a specified algorithm (any, none, brotli)\n"
         "     -Z: disable compression\n"
@@ -1340,7 +1342,7 @@
 
 static void parse_push_pull_args(const char** arg, int narg, std::vector<const char*>* srcs,
                                  const char** dst, bool* copy_attrs, bool* sync,
-                                 CompressionType* compression) {
+                                 CompressionType* compression, bool* dry_run) {
     *copy_attrs = false;
     if (const char* adb_compression = getenv("ADB_COMPRESSION")) {
         *compression = parse_compression_type(adb_compression, true);
@@ -1364,6 +1366,8 @@
                 --narg;
             } else if (!strcmp(*arg, "-Z")) {
                 *compression = CompressionType::None;
+            } else if (dry_run && !strcmp(*arg, "-n")) {
+                *dry_run = true;
             } else if (!strcmp(*arg, "--sync")) {
                 if (sync != nullptr) {
                     *sync = true;
@@ -1918,20 +1922,23 @@
     } else if (!strcmp(argv[0], "push")) {
         bool copy_attrs = false;
         bool sync = false;
+        bool dry_run = false;
         CompressionType compression = CompressionType::Any;
         std::vector<const char*> srcs;
         const char* dst = nullptr;
 
-        parse_push_pull_args(&argv[1], argc - 1, &srcs, &dst, &copy_attrs, &sync, &compression);
+        parse_push_pull_args(&argv[1], argc - 1, &srcs, &dst, &copy_attrs, &sync, &compression,
+                             &dry_run);
         if (srcs.empty() || !dst) error_exit("push requires an argument");
-        return do_sync_push(srcs, dst, sync, compression) ? 0 : 1;
+        return do_sync_push(srcs, dst, sync, compression, dry_run) ? 0 : 1;
     } else if (!strcmp(argv[0], "pull")) {
         bool copy_attrs = false;
         CompressionType compression = CompressionType::Any;
         std::vector<const char*> srcs;
         const char* dst = ".";
 
-        parse_push_pull_args(&argv[1], argc - 1, &srcs, &dst, &copy_attrs, nullptr, &compression);
+        parse_push_pull_args(&argv[1], argc - 1, &srcs, &dst, &copy_attrs, nullptr, &compression,
+                             nullptr);
         if (srcs.empty()) error_exit("pull requires an argument");
         return do_sync_pull(srcs, dst, copy_attrs, compression) ? 0 : 1;
     } else if (!strcmp(argv[0], "install")) {
@@ -1949,6 +1956,7 @@
     } else if (!strcmp(argv[0], "sync")) {
         std::string src;
         bool list_only = false;
+        bool dry_run = false;
         CompressionType compression = CompressionType::Any;
 
         if (const char* adb_compression = getenv("ADB_COMPRESSION"); adb_compression) {
@@ -1956,11 +1964,14 @@
         }
 
         int opt;
-        while ((opt = getopt(argc, const_cast<char**>(argv), "lz:Z")) != -1) {
+        while ((opt = getopt(argc, const_cast<char**>(argv), "lnz:Z")) != -1) {
             switch (opt) {
                 case 'l':
                     list_only = true;
                     break;
+                case 'n':
+                    dry_run = true;
+                    break;
                 case 'z':
                     compression = parse_compression_type(optarg, false);
                     break;
@@ -1968,7 +1979,7 @@
                     compression = CompressionType::None;
                     break;
                 default:
-                    error_exit("usage: adb sync [-l] [-z ALGORITHM] [-Z] [PARTITION]");
+                    error_exit("usage: adb sync [-l] [-n]  [-z ALGORITHM] [-Z] [PARTITION]");
             }
         }
 
@@ -1977,7 +1988,7 @@
         } else if (optind + 1 == argc) {
             src = argv[optind];
         } else {
-            error_exit("usage: adb sync [-l] [-z ALGORITHM] [-Z] [PARTITION]");
+            error_exit("usage: adb sync [-l] [-n] [-z ALGORITHM] [-Z] [PARTITION]");
         }
 
         std::vector<std::string> partitions{"data",   "odm",        "oem",   "product",
@@ -1988,7 +1999,9 @@
                 std::string src_dir{product_file(partition)};
                 if (!directory_exists(src_dir)) continue;
                 found = true;
-                if (!do_sync_sync(src_dir, "/" + partition, list_only, compression)) return 1;
+                if (!do_sync_sync(src_dir, "/" + partition, list_only, compression, dry_run)) {
+                    return 1;
+                }
             }
         }
         if (!found) error_exit("don't know how to sync %s partition", src.c_str());
diff --git a/adb/client/fastdeploy.cpp b/adb/client/fastdeploy.cpp
index 37f1a90..bc4b91b 100644
--- a/adb/client/fastdeploy.cpp
+++ b/adb/client/fastdeploy.cpp
@@ -112,7 +112,7 @@
     // but can't be removed until after the push.
     unix_close(tf.release());
 
-    if (!do_sync_push(srcs, dst, sync, CompressionType::Any)) {
+    if (!do_sync_push(srcs, dst, sync, CompressionType::Any, false)) {
         error_exit("Failed to push fastdeploy agent to device.");
     }
 }
diff --git a/adb/client/file_sync_client.cpp b/adb/client/file_sync_client.cpp
index 75334d7..6816734 100644
--- a/adb/client/file_sync_client.cpp
+++ b/adb/client/file_sync_client.cpp
@@ -238,6 +238,7 @@
             have_sendrecv_v2_ = CanUseFeature(features_, kFeatureSendRecv2);
             have_sendrecv_v2_brotli_ = CanUseFeature(features_, kFeatureSendRecv2Brotli);
             have_sendrecv_v2_lz4_ = CanUseFeature(features_, kFeatureSendRecv2LZ4);
+            have_sendrecv_v2_dry_run_send_ = CanUseFeature(features_, kFeatureSendRecv2DryRunSend);
             fd.reset(adb_connect("sync:", &error));
             if (fd < 0) {
                 Error("connect failed: %s", error.c_str());
@@ -264,6 +265,7 @@
     bool HaveSendRecv2() const { return have_sendrecv_v2_; }
     bool HaveSendRecv2Brotli() const { return have_sendrecv_v2_brotli_; }
     bool HaveSendRecv2LZ4() const { return have_sendrecv_v2_lz4_; }
+    bool HaveSendRecv2DryRunSend() const { return have_sendrecv_v2_dry_run_send_; }
 
     // Resolve a compression type which might be CompressionType::Any to a specific compression
     // algorithm.
@@ -340,7 +342,7 @@
         return WriteFdExactly(fd, buf.data(), buf.size());
     }
 
-    bool SendSend2(std::string_view path, mode_t mode, CompressionType compression) {
+    bool SendSend2(std::string_view path, mode_t mode, CompressionType compression, bool dry_run) {
         if (path.length() > 1024) {
             Error("SendRequest failed: path too long: %zu", path.length());
             errno = ENAMETOOLONG;
@@ -373,6 +375,10 @@
                 LOG(FATAL) << "unexpected CompressionType::Any";
         }
 
+        if (dry_run) {
+            msg.send_v2_setup.flags |= kSyncFlagDryRun;
+        }
+
         buf.resize(sizeof(SyncRequest) + path.length() + sizeof(msg.send_v2_setup));
 
         void* p = buf.data();
@@ -541,7 +547,12 @@
     // difference to "adb sync" performance.
     bool SendSmallFile(const std::string& path, mode_t mode, const std::string& lpath,
                        const std::string& rpath, unsigned mtime, const char* data,
-                       size_t data_length) {
+                       size_t data_length, bool dry_run) {
+        if (dry_run) {
+            // We need to use send v2 for dry run.
+            return SendLargeFile(path, mode, lpath, rpath, mtime, CompressionType::None, dry_run);
+        }
+
         std::string path_and_mode = android::base::StringPrintf("%s,%d", path.c_str(), mode);
         if (path_and_mode.length() > 1024) {
             Error("SendSmallFile failed: path too long: %zu", path_and_mode.length());
@@ -581,14 +592,20 @@
     }
 
     bool SendLargeFile(const std::string& path, mode_t mode, const std::string& lpath,
-                       const std::string& rpath, unsigned mtime, CompressionType compression) {
+                       const std::string& rpath, unsigned mtime, CompressionType compression,
+                       bool dry_run) {
+        if (dry_run && !HaveSendRecv2DryRunSend()) {
+            Error("dry-run not supported by the device");
+            return false;
+        }
+
         if (!HaveSendRecv2()) {
             return SendLargeFileLegacy(path, mode, lpath, rpath, mtime);
         }
 
         compression = ResolveCompressionType(compression);
 
-        if (!SendSend2(path, mode, compression)) {
+        if (!SendSend2(path, mode, compression, dry_run)) {
             Error("failed to send ID_SEND_V2 message '%s': %s", path.c_str(), strerror(errno));
             return false;
         }
@@ -908,6 +925,7 @@
     bool have_sendrecv_v2_;
     bool have_sendrecv_v2_brotli_;
     bool have_sendrecv_v2_lz4_;
+    bool have_sendrecv_v2_dry_run_send_;
 
     TransferLedger global_ledger_;
     TransferLedger current_ledger_;
@@ -989,7 +1007,8 @@
 }
 
 static bool sync_send(SyncConnection& sc, const std::string& lpath, const std::string& rpath,
-                      unsigned mtime, mode_t mode, bool sync, CompressionType compression) {
+                      unsigned mtime, mode_t mode, bool sync, CompressionType compression,
+                      bool dry_run) {
     if (sync) {
         struct stat st;
         if (sync_lstat(sc, rpath, &st)) {
@@ -1010,7 +1029,7 @@
         }
         buf[data_length++] = '\0';
 
-        if (!sc.SendSmallFile(rpath, mode, lpath, rpath, mtime, buf, data_length)) {
+        if (!sc.SendSmallFile(rpath, mode, lpath, rpath, mtime, buf, data_length, dry_run)) {
             return false;
         }
         return sc.ReadAcknowledgements();
@@ -1028,11 +1047,12 @@
             sc.Error("failed to read all of '%s': %s", lpath.c_str(), strerror(errno));
             return false;
         }
-        if (!sc.SendSmallFile(rpath, mode, lpath, rpath, mtime, data.data(), data.size())) {
+        if (!sc.SendSmallFile(rpath, mode, lpath, rpath, mtime, data.data(), data.size(),
+                              dry_run)) {
             return false;
         }
     } else {
-        if (!sc.SendLargeFile(rpath, mode, lpath, rpath, mtime, compression)) {
+        if (!sc.SendLargeFile(rpath, mode, lpath, rpath, mtime, compression, dry_run)) {
             return false;
         }
     }
@@ -1284,7 +1304,7 @@
 
 static bool copy_local_dir_remote(SyncConnection& sc, std::string lpath, std::string rpath,
                                   bool check_timestamps, bool list_only,
-                                  CompressionType compression) {
+                                  CompressionType compression, bool dry_run) {
     sc.NewTransfer();
 
     // Make sure that both directory paths end in a slash.
@@ -1366,7 +1386,8 @@
             if (list_only) {
                 sc.Println("would push: %s -> %s", ci.lpath.c_str(), ci.rpath.c_str());
             } else {
-                if (!sync_send(sc, ci.lpath, ci.rpath, ci.time, ci.mode, false, compression)) {
+                if (!sync_send(sc, ci.lpath, ci.rpath, ci.time, ci.mode, false, compression,
+                               dry_run)) {
                     return false;
                 }
             }
@@ -1382,7 +1403,7 @@
 }
 
 bool do_sync_push(const std::vector<const char*>& srcs, const char* dst, bool sync,
-                  CompressionType compression) {
+                  CompressionType compression, bool dry_run) {
     SyncConnection sc;
     if (!sc.IsValid()) return false;
 
@@ -1447,7 +1468,8 @@
                 dst_dir.append(android::base::Basename(src_path));
             }
 
-            success &= copy_local_dir_remote(sc, src_path, dst_dir, sync, false, compression);
+            success &=
+                    copy_local_dir_remote(sc, src_path, dst_dir, sync, false, compression, dry_run);
             continue;
         } else if (!should_push_file(st.st_mode)) {
             sc.Warning("skipping special file '%s' (mode = 0o%o)", src_path, st.st_mode);
@@ -1468,7 +1490,8 @@
 
         sc.NewTransfer();
         sc.SetExpectedTotalBytes(st.st_size);
-        success &= sync_send(sc, src_path, dst_path, st.st_mtime, st.st_mode, sync, compression);
+        success &= sync_send(sc, src_path, dst_path, st.st_mtime, st.st_mode, sync, compression,
+                             dry_run);
         sc.ReportTransferRate(src_path, TransferDirection::push);
     }
 
@@ -1712,11 +1735,11 @@
 }
 
 bool do_sync_sync(const std::string& lpath, const std::string& rpath, bool list_only,
-                  CompressionType compression) {
+                  CompressionType compression, bool dry_run) {
     SyncConnection sc;
     if (!sc.IsValid()) return false;
 
-    bool success = copy_local_dir_remote(sc, lpath, rpath, true, list_only, compression);
+    bool success = copy_local_dir_remote(sc, lpath, rpath, true, list_only, compression, dry_run);
     if (!list_only) {
         sc.ReportOverallTransferRate(TransferDirection::push);
     }
diff --git a/adb/client/file_sync_client.h b/adb/client/file_sync_client.h
index aab2e3f..cb8ca93 100644
--- a/adb/client/file_sync_client.h
+++ b/adb/client/file_sync_client.h
@@ -23,9 +23,9 @@
 
 bool do_sync_ls(const char* path);
 bool do_sync_push(const std::vector<const char*>& srcs, const char* dst, bool sync,
-                  CompressionType compression);
+                  CompressionType compression, bool dry_run);
 bool do_sync_pull(const std::vector<const char*>& srcs, const char* dst, bool copy_attrs,
                   CompressionType compression, const char* name = nullptr);
 
 bool do_sync_sync(const std::string& lpath, const std::string& rpath, bool list_only,
-                  CompressionType compression);
+                  CompressionType compression, bool dry_run);
diff --git a/adb/daemon/file_sync_service.cpp b/adb/daemon/file_sync_service.cpp
index 3436e32..d58131e 100644
--- a/adb/daemon/file_sync_service.cpp
+++ b/adb/daemon/file_sync_service.cpp
@@ -315,9 +315,12 @@
                 return false;
             }
 
-            if (!WriteFdExactly(fd, output.data(), output.size())) {
-                SendSyncFailErrno(s, "write failed");
-                return false;
+            // fd is -1 if the client is pushing with --dry-run.
+            if (fd != -1) {
+                if (!WriteFdExactly(fd, output.data(), output.size())) {
+                    SendSyncFailErrno(s, "write failed");
+                    return false;
+                }
             }
 
             if (result == DecodeResult::NeedInput) {
@@ -337,66 +340,65 @@
 
 static bool handle_send_file(borrowed_fd s, const char* path, uint32_t* timestamp, uid_t uid,
                              gid_t gid, uint64_t capabilities, mode_t mode,
-                             CompressionType compression, std::vector<char>& buffer,
+                             CompressionType compression, bool dry_run, std::vector<char>& buffer,
                              bool do_unlink) {
-    int rc;
     syncmsg msg;
+    unique_fd fd;
 
-    __android_log_security_bswrite(SEC_TAG_ADB_SEND_FILE, path);
-
-    unique_fd fd(adb_open_mode(path, O_WRONLY | O_CREAT | O_EXCL | O_CLOEXEC, mode));
-
-    if (fd < 0 && errno == ENOENT) {
-        if (!secure_mkdirs(Dirname(path))) {
-            SendSyncFailErrno(s, "secure_mkdirs failed");
-            goto fail;
-        }
+    if (!dry_run) {
+        __android_log_security_bswrite(SEC_TAG_ADB_SEND_FILE, path);
         fd.reset(adb_open_mode(path, O_WRONLY | O_CREAT | O_EXCL | O_CLOEXEC, mode));
-    }
-    if (fd < 0 && errno == EEXIST) {
-        fd.reset(adb_open_mode(path, O_WRONLY | O_CLOEXEC, mode));
-    }
-    if (fd < 0) {
-        SendSyncFailErrno(s, "couldn't create file");
-        goto fail;
-    } else {
-        if (fchown(fd.get(), uid, gid) == -1) {
-            SendSyncFailErrno(s, "fchown failed");
-            goto fail;
+
+        if (fd < 0 && errno == ENOENT) {
+            if (!secure_mkdirs(Dirname(path))) {
+                SendSyncFailErrno(s, "secure_mkdirs failed");
+                goto fail;
+            }
+            fd.reset(adb_open_mode(path, O_WRONLY | O_CREAT | O_EXCL | O_CLOEXEC, mode));
         }
+        if (fd < 0 && errno == EEXIST) {
+            fd.reset(adb_open_mode(path, O_WRONLY | O_CLOEXEC, mode));
+        }
+        if (fd < 0) {
+            SendSyncFailErrno(s, "couldn't create file");
+            goto fail;
+        } else {
+            if (fchown(fd.get(), uid, gid) == -1) {
+                SendSyncFailErrno(s, "fchown failed");
+                goto fail;
+            }
 
 #if defined(__ANDROID__)
-        // Not all filesystems support setting SELinux labels. http://b/23530370.
-        selinux_android_restorecon(path, 0);
+            // Not all filesystems support setting SELinux labels. http://b/23530370.
+            selinux_android_restorecon(path, 0);
 #endif
 
-        // fchown clears the setuid bit - restore it if present.
-        // Ignore the result of calling fchmod. It's not supported
-        // by all filesystems, so we don't check for success. b/12441485
-        fchmod(fd.get(), mode);
-    }
+            // fchown clears the setuid bit - restore it if present.
+            // Ignore the result of calling fchmod. It's not supported
+            // by all filesystems, so we don't check for success. b/12441485
+            fchmod(fd.get(), mode);
+        }
 
-    {
-        rc = posix_fadvise(fd.get(), 0, 0,
-                           POSIX_FADV_SEQUENTIAL | POSIX_FADV_NOREUSE | POSIX_FADV_WILLNEED);
+        int rc = posix_fadvise(fd.get(), 0, 0,
+                               POSIX_FADV_SEQUENTIAL | POSIX_FADV_NOREUSE | POSIX_FADV_WILLNEED);
         if (rc != 0) {
             D("[ Failed to fadvise: %s ]", strerror(rc));
         }
-
-        if (!handle_send_file_data(s, std::move(fd), timestamp, compression)) {
-            goto fail;
-        }
-
-        if (!update_capabilities(path, capabilities)) {
-            SendSyncFailErrno(s, "update_capabilities failed");
-            goto fail;
-        }
-
-        msg.status.id = ID_OKAY;
-        msg.status.msglen = 0;
-        return WriteFdExactly(s, &msg.status, sizeof(msg.status));
     }
 
+    if (!handle_send_file_data(s, std::move(fd), timestamp, compression)) {
+        goto fail;
+    }
+
+    if (!update_capabilities(path, capabilities)) {
+        SendSyncFailErrno(s, "update_capabilities failed");
+        goto fail;
+    }
+
+    msg.status.id = ID_OKAY;
+    msg.status.msglen = 0;
+    return WriteFdExactly(s, &msg.status, sizeof(msg.status));
+
 fail:
     // If there's a problem on the device, we'll send an ID_FAIL message and
     // close the socket. Unfortunately the kernel will sometimes throw that
@@ -435,7 +437,7 @@
                              uint32_t* timestamp, std::vector<char>& buffer)
         __attribute__((error("no symlinks on Windows")));
 #else
-static bool handle_send_link(int s, const std::string& path, uint32_t* timestamp,
+static bool handle_send_link(int s, const std::string& path, uint32_t* timestamp, bool dry_run,
                              std::vector<char>& buffer) {
     syncmsg msg;
 
@@ -454,19 +456,21 @@
     if (!ReadFdExactly(s, &buffer[0], len)) return false;
 
     std::string buf_link;
-    if (!android::base::Readlink(path, &buf_link) || (buf_link != &buffer[0])) {
-        adb_unlink(path.c_str());
-        auto ret = symlink(&buffer[0], path.c_str());
-        if (ret && errno == ENOENT) {
-            if (!secure_mkdirs(Dirname(path))) {
-                SendSyncFailErrno(s, "secure_mkdirs failed");
+    if (!dry_run) {
+        if (!android::base::Readlink(path, &buf_link) || (buf_link != &buffer[0])) {
+            adb_unlink(path.c_str());
+            auto ret = symlink(&buffer[0], path.c_str());
+            if (ret && errno == ENOENT) {
+                if (!secure_mkdirs(Dirname(path))) {
+                    SendSyncFailErrno(s, "secure_mkdirs failed");
+                    return false;
+                }
+                ret = symlink(&buffer[0], path.c_str());
+            }
+            if (ret) {
+                SendSyncFailErrno(s, "symlink failed");
                 return false;
             }
-            ret = symlink(&buffer[0], path.c_str());
-        }
-        if (ret) {
-            SendSyncFailErrno(s, "symlink failed");
-            return false;
         }
     }
 
@@ -487,11 +491,14 @@
 #endif
 
 static bool send_impl(int s, const std::string& path, mode_t mode, CompressionType compression,
-                      std::vector<char>& buffer) {
+                      bool dry_run, std::vector<char>& buffer) {
     // Don't delete files before copying if they are not "regular" or symlinks.
     struct stat st;
-    bool do_unlink = (lstat(path.c_str(), &st) == -1) || S_ISREG(st.st_mode) ||
-                     (S_ISLNK(st.st_mode) && !S_ISLNK(mode));
+    bool do_unlink = false;
+    if (!dry_run) {
+        do_unlink = (lstat(path.c_str(), &st) == -1) || S_ISREG(st.st_mode) ||
+                    (S_ISLNK(st.st_mode) && !S_ISLNK(mode));
+    }
     if (do_unlink) {
         adb_unlink(path.c_str());
     }
@@ -499,7 +506,7 @@
     bool result;
     uint32_t timestamp;
     if (S_ISLNK(mode)) {
-        result = handle_send_link(s, path, &timestamp, buffer);
+        result = handle_send_link(s, path, &timestamp, dry_run, buffer);
     } else {
         // Copy user permission bits to "group" and "other" permissions.
         mode &= 0777;
@@ -509,12 +516,12 @@
         uid_t uid = -1;
         gid_t gid = -1;
         uint64_t capabilities = 0;
-        if (should_use_fs_config(path)) {
+        if (should_use_fs_config(path) && !dry_run) {
             adbd_fs_config(path.c_str(), 0, nullptr, &uid, &gid, &mode, &capabilities);
         }
 
         result = handle_send_file(s, path.c_str(), &timestamp, uid, gid, capabilities, mode,
-                                  compression, buffer, do_unlink);
+                                  compression, dry_run, buffer, do_unlink);
     }
 
     if (!result) {
@@ -547,7 +554,7 @@
         return false;
     }
 
-    return send_impl(s, path, mode, CompressionType::None, buffer);
+    return send_impl(s, path, mode, CompressionType::None, false, buffer);
 }
 
 static bool do_send_v2(int s, const std::string& path, std::vector<char>& buffer) {
@@ -561,6 +568,7 @@
         PLOG(ERROR) << "failed to read send_v2 setup packet";
     }
 
+    bool dry_run = false;
     std::optional<CompressionType> compression;
 
     uint32_t orig_flags = msg.send_v2_setup.flags;
@@ -582,6 +590,10 @@
         }
         compression = CompressionType::LZ4;
     }
+    if (msg.send_v2_setup.flags & kSyncFlagDryRun) {
+        msg.send_v2_setup.flags &= ~kSyncFlagDryRun;
+        dry_run = true;
+    }
 
     if (msg.send_v2_setup.flags) {
         SendSyncFail(s, android::base::StringPrintf("unknown flags: %d", msg.send_v2_setup.flags));
@@ -590,7 +602,7 @@
 
     errno = 0;
     return send_impl(s, path, msg.send_v2_setup.mode, compression.value_or(CompressionType::None),
-                     buffer);
+                     dry_run, buffer);
 }
 
 static bool recv_impl(borrowed_fd s, const char* path, CompressionType compression,
diff --git a/adb/file_sync_protocol.h b/adb/file_sync_protocol.h
index 90bd763..8f8f85f 100644
--- a/adb/file_sync_protocol.h
+++ b/adb/file_sync_protocol.h
@@ -93,6 +93,7 @@
     kSyncFlagNone = 0,
     kSyncFlagBrotli = 1,
     kSyncFlagLZ4 = 2,
+    kSyncFlagDryRun = 0x8000'0000U,
 };
 
 enum class CompressionType {
diff --git a/adb/test_device.py b/adb/test_device.py
index 3be7c9a..f5e4cbb 100755
--- a/adb/test_device.py
+++ b/adb/test_device.py
@@ -1268,6 +1268,59 @@
                 if temp_dir is not None:
                     shutil.rmtree(temp_dir)
 
+        def test_push_dry_run_nonexistent_file(self):
+            """Push with dry run."""
+
+            for file_size in [8, 1024 * 1024]:
+                try:
+                    device_dir = posixpath.join(self.DEVICE_TEMP_DIR, 'push_dry_run')
+                    device_file = posixpath.join(device_dir, 'file')
+
+                    self.device.shell(['rm', '-rf', self.DEVICE_TEMP_DIR])
+                    self.device.shell(['mkdir', '-p', device_dir])
+
+                    host_dir = tempfile.mkdtemp()
+                    host_file = posixpath.join(host_dir, 'file')
+
+                    with open(host_file, "w") as f:
+                        f.write('x' * file_size)
+
+                    self.device._simple_call(['push', '-n', host_file, device_file])
+                    rc, _, _ = self.device.shell_nocheck(['[', '-e', device_file, ']'])
+                    self.assertNotEqual(0, rc)
+
+                    self.device.shell(['rm', '-rf', self.DEVICE_TEMP_DIR])
+                finally:
+                    if host_dir is not None:
+                        shutil.rmtree(host_dir)
+
+        def test_push_dry_run_existent_file(self):
+            """Push with dry run."""
+
+            for file_size in [8, 1024 * 1024]:
+                try:
+                    device_dir = posixpath.join(self.DEVICE_TEMP_DIR, 'push_dry_run')
+                    device_file = posixpath.join(device_dir, 'file')
+
+                    self.device.shell(['rm', '-rf', self.DEVICE_TEMP_DIR])
+                    self.device.shell(['mkdir', '-p', device_dir])
+                    self.device.shell(['echo', 'foo', '>', device_file])
+
+                    host_dir = tempfile.mkdtemp()
+                    host_file = posixpath.join(host_dir, 'file')
+
+                    with open(host_file, "w") as f:
+                        f.write('x' * file_size)
+
+                    self.device._simple_call(['push', '-n', host_file, device_file])
+                    stdout, stderr = self.device.shell(['cat', device_file])
+                    self.assertEqual(stdout.strip(), "foo")
+
+                    self.device.shell(['rm', '-rf', self.DEVICE_TEMP_DIR])
+                finally:
+                    if host_dir is not None:
+                        shutil.rmtree(host_dir)
+
         def test_unicode_paths(self):
             """Ensure that we can support non-ASCII paths, even on Windows."""
             name = u'로보카 폴리'
diff --git a/adb/transport.cpp b/adb/transport.cpp
index cef3850..cc2e0e3 100644
--- a/adb/transport.cpp
+++ b/adb/transport.cpp
@@ -85,6 +85,7 @@
 const char* const kFeatureSendRecv2 = "sendrecv_v2";
 const char* const kFeatureSendRecv2Brotli = "sendrecv_v2_brotli";
 const char* const kFeatureSendRecv2LZ4 = "sendrecv_v2_lz4";
+const char* const kFeatureSendRecv2DryRunSend = "sendrecv_v2_dry_run_send";
 
 namespace {
 
@@ -1185,6 +1186,7 @@
             kFeatureSendRecv2,
             kFeatureSendRecv2Brotli,
             kFeatureSendRecv2LZ4,
+            kFeatureSendRecv2DryRunSend,
             // Increment ADB_SERVER_VERSION when adding a feature that adbd needs
             // to know about. Otherwise, the client can be stuck running an old
             // version of the server even after upgrading their copy of adb.
diff --git a/adb/transport.h b/adb/transport.h
index 12803b5..4b2e000 100644
--- a/adb/transport.h
+++ b/adb/transport.h
@@ -90,6 +90,8 @@
 extern const char* const kFeatureSendRecv2Brotli;
 // adbd supports LZ4 for send/recv v2.
 extern const char* const kFeatureSendRecv2LZ4;
+// adbd supports dry-run send for send/recv v2.
+extern const char* const kFeatureSendRecv2DryRunSend;
 
 TransportId NextTransportId();