diff --git a/16kb/CopyEfsTest/Android.bp b/16kb/CopyEfsTest/Android.bp
new file mode 100644
index 0000000..e599215
--- /dev/null
+++ b/16kb/CopyEfsTest/Android.bp
@@ -0,0 +1,38 @@
+// Copyright (C) 2025 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package {
+ default_applicable_licenses: ["device_google_gs-common_license"],
+}
+
+java_test_host {
+ name: "CopyEfsTest",
+ // Include all test java files
+ srcs: ["src/**/*.java"],
+ static_libs: [
+ "junit",
+ "platform-test-annotations",
+ "truth",
+ ],
+ libs: [
+ "tradefed",
+ "compatibility-host-util",
+ "compatibility-tradefed",
+ ],
+ test_suites: [
+ "device-tests",
+ "device-pixel-tests",
+ ],
+ test_config: "AndroidTest.xml",
+}
diff --git a/16kb/CopyEfsTest/AndroidTest.xml b/16kb/CopyEfsTest/AndroidTest.xml
new file mode 100644
index 0000000..2b18b04
--- /dev/null
+++ b/16kb/CopyEfsTest/AndroidTest.xml
@@ -0,0 +1,40 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/16kb/CopyEfsTest/src/com/android/test/CopyEfsTest.java b/16kb/CopyEfsTest/src/com/android/test/CopyEfsTest.java
new file mode 100644
index 0000000..0f87513
--- /dev/null
+++ b/16kb/CopyEfsTest/src/com/android/test/CopyEfsTest.java
@@ -0,0 +1,130 @@
+/*
+ * Copyright (C) 2025 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.test;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertTrue;
+import static org.junit.Assume.assumeTrue;
+import org.junit.Before;
+import org.junit.After;
+
+import android.platform.test.annotations.AppModeFull;
+
+import com.android.tradefed.device.DeviceNotAvailableException;
+import com.android.tradefed.testtype.DeviceJUnit4ClassRunner;
+import com.android.tradefed.testtype.junit4.BaseHostJUnit4Test;
+import com.android.tradefed.testtype.junit4.DeviceTestRunOptions;
+import com.android.tradefed.util.CommandResult;
+import com.android.tradefed.util.RunUtil;
+
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+import java.io.BufferedReader;
+import java.io.StringReader;
+
+@RunWith(DeviceJUnit4ClassRunner.class)
+public class CopyEfsTest extends BaseHostJUnit4Test {
+
+ @Before
+ public void setUp() throws Exception {
+ getDevice().enableAdbRoot();
+
+ getDevice().executeShellCommand("rm -rf /data/local/tmp/efs_test");
+ getDevice().executeShellCommand("mkdir -p /data/local/tmp/efs_test/mnt");
+ getDevice().executeShellCommand("mkdir -p /data/local/tmp/efs_test/dump");
+ }
+
+ @Test
+ @AppModeFull
+ public void copyEfsTest() throws Exception {
+ assumeTrue(getDevice().executeShellCommand("getconf PAGESIZE").trim().equals("4096"));
+
+ testDumpF2FS("efs");
+ testDumpF2FS("efs_backup");
+ testDumpF2FS("modem_userdata");
+ testDumpF2FS("persist");
+ }
+
+ private CommandResult RunAndCheckAdbCmd(String cmd) throws DeviceNotAvailableException {
+ CommandResult r = getDevice().executeShellV2Command(cmd);
+ assertEquals("Failed to run " + cmd, Integer.valueOf(0), r.getExitCode());
+ return r;
+ }
+
+ // Remove timestamps because ls on device does not support --time-style.
+ // Format is [permissions] [links] [uid] [gid] [size] time [name/symlink]
+ // time may vary greatly in formatting
+ // symlinks will be of the form a -> b
+ // So we can check for -> in the second to last spot to determine what position the timestamp ends at
+ // Remove totals because on disk block usage may change depending on filesystem
+ private String removeTimestamps(String input) {
+ StringBuilder output = new StringBuilder();
+ for (String line : input.split("\n")) {
+ String[] tokens = line.split("(?"))
+ name_offset = 3;
+ for (int i=0; i= 5 && i < tokens.length - name_offset)
+ continue;
+ if (i != 0)
+ output.append(" ");
+ output.append(tokens[i]);
+ }
+ output.append("\n");
+ }
+ return output.toString();
+ }
+
+ private void testDumpF2FS(String name) throws Exception {
+ RunAndCheckAdbCmd(String.format("cp /dev/block/by-name/%s /data/local/tmp/efs_test/%s.img", name, name));
+
+ // The device was mounted r/w. To get a clean image, we run fsck, and then mount to allow mount time fixes to happen.
+ // We can then dump and mount read only to ensure the contents should be the same.
+ RunAndCheckAdbCmd(String.format("fsck.f2fs -f /data/local/tmp/efs_test/%s.img", name));
+ RunAndCheckAdbCmd(String.format("mount /data/local/tmp/efs_test/%s.img /data/local/tmp/efs_test/mnt", name));
+ RunAndCheckAdbCmd("umount /data/local/tmp/efs_test/mnt");
+
+ RunAndCheckAdbCmd(String.format("dump.f2fs -rfPLo /data/local/tmp/efs_test/dump /data/local/tmp/efs_test/%s.img", name));
+ RunAndCheckAdbCmd(String.format("mount -r /data/local/tmp/efs_test/%s.img /data/local/tmp/efs_test/mnt", name));
+
+ CommandResult r = RunAndCheckAdbCmd("diff -rq --no-dereference /data/local/tmp/efs_test/mnt /data/local/tmp/efs_test/dump");
+ assertEquals(r.getStdout(), "");
+
+ String ls_cmd = "cd /data/local/tmp/efs_test/%s;ls -AlnR .";
+ CommandResult mnt_ls = RunAndCheckAdbCmd(String.format(ls_cmd, "mnt"));
+ CommandResult dump_ls = RunAndCheckAdbCmd(String.format(ls_cmd, "dump"));
+ assertEquals(removeTimestamps(mnt_ls.getStdout()), removeTimestamps(dump_ls.getStdout()));
+
+ getDevice().executeShellCommand("umount /data/local/tmp/efs_test/mnt");
+ getDevice().executeShellCommand("rm -rf /data/local/tmp/efs_test/dump/*");
+ getDevice().executeShellCommand("rm /data/local/tmp/efs_test/" + name + ".img");
+ }
+
+ @After
+ public void tearDown() throws Exception {
+ getDevice().executeShellCommand("umount /data/local/tmp/efs_test/mnt");
+ getDevice().executeShellCommand("rm -rf /data/local/tmp/efs_test");
+ }
+}