[Ravenwood] Make Os.stat() family available

Add Ravenwood support for stat, lstat, and fstat.

Bug: 350811257
Test: atest RavenwoodRuntimeTest
Flag: TEST_ONLY (ravenwood)
Change-Id: I7c13021f539a38b3eb493ddbf3c04eadd5d8ef81
diff --git a/ravenwood/runtime-helper-src/libcore-fake/android/system/Os.java b/ravenwood/runtime-helper-src/libcore-fake/android/system/Os.java
index e031eb2..8a1fe62 100644
--- a/ravenwood/runtime-helper-src/libcore-fake/android/system/Os.java
+++ b/ravenwood/runtime-helper-src/libcore-fake/android/system/Os.java
@@ -30,7 +30,6 @@
         return RavenwoodRuntimeNative.lseek(fd, offset, whence);
     }
 
-
     public static FileDescriptor[] pipe2(int flags) throws ErrnoException {
         return RavenwoodRuntimeNative.pipe2(flags);
     }
@@ -42,4 +41,16 @@
     public static int fcntlInt(FileDescriptor fd, int cmd, int arg) throws ErrnoException {
         return RavenwoodRuntimeNative.fcntlInt(fd, cmd, arg);
     }
+
+    public static StructStat fstat(FileDescriptor fd) throws ErrnoException {
+        return RavenwoodRuntimeNative.fstat(fd);
+    }
+
+    public static StructStat lstat(String path) throws ErrnoException {
+        return RavenwoodRuntimeNative.lstat(path);
+    }
+
+    public static StructStat stat(String path) throws ErrnoException {
+        return RavenwoodRuntimeNative.stat(path);
+    }
 }
diff --git a/ravenwood/runtime-helper-src/libcore-fake/android/system/StructStat.java b/ravenwood/runtime-helper-src/libcore-fake/android/system/StructStat.java
new file mode 100644
index 0000000..a8b1fca
--- /dev/null
+++ b/ravenwood/runtime-helper-src/libcore-fake/android/system/StructStat.java
@@ -0,0 +1,121 @@
+/*
+ * Copyright (C) 2011 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 android.system;
+
+import libcore.util.Objects;
+
+/**
+ * File information returned by {@link Os#fstat}, {@link Os#lstat}, and {@link Os#stat}.
+ * Corresponds to C's {@code struct stat} from {@code <stat.h>}.
+ */
+public final class StructStat {
+    /** Device ID of device containing file. */
+    public final long st_dev; /*dev_t*/
+
+    /** File serial number (inode). */
+    public final long st_ino; /*ino_t*/
+
+    /** Mode (permissions) of file. */
+    public final int st_mode; /*mode_t*/
+
+    /** Number of hard links to the file. */
+    public final long st_nlink; /*nlink_t*/
+
+    /** User ID of file. */
+    public final int st_uid; /*uid_t*/
+
+    /** Group ID of file. */
+    public final int st_gid; /*gid_t*/
+
+    /** Device ID (if file is character or block special). */
+    public final long st_rdev; /*dev_t*/
+
+    /**
+     * For regular files, the file size in bytes.
+     * For symbolic links, the length in bytes of the pathname contained in the symbolic link.
+     * For a shared memory object, the length in bytes.
+     * For a typed memory object, the length in bytes.
+     * For other file types, the use of this field is unspecified.
+     */
+    public final long st_size; /*off_t*/
+
+    /** Seconds part of time of last access. */
+    public final long st_atime; /*time_t*/
+
+    /** StructTimespec with time of last access. */
+    public final StructTimespec st_atim;
+
+    /** Seconds part of time of last data modification. */
+    public final long st_mtime; /*time_t*/
+
+    /** StructTimespec with time of last modification. */
+    public final StructTimespec st_mtim;
+
+    /** Seconds part of time of last status change */
+    public final long st_ctime; /*time_t*/
+
+    /** StructTimespec with time of last status change. */
+    public final StructTimespec st_ctim;
+
+    /**
+     * A file system-specific preferred I/O block size for this object.
+     * For some file system types, this may vary from file to file.
+     */
+    public final long st_blksize; /*blksize_t*/
+
+    /** Number of blocks allocated for this object. */
+    public final long st_blocks; /*blkcnt_t*/
+
+    /**
+     * Constructs an instance with the given field values.
+     */
+    public StructStat(long st_dev, long st_ino, int st_mode, long st_nlink, int st_uid, int st_gid,
+            long st_rdev, long st_size, long st_atime, long st_mtime, long st_ctime,
+            long st_blksize, long st_blocks) {
+        this(st_dev, st_ino, st_mode, st_nlink, st_uid, st_gid,
+                st_rdev, st_size, new StructTimespec(st_atime, 0L), new StructTimespec(st_mtime, 0L),
+                new StructTimespec(st_ctime, 0L), st_blksize, st_blocks);
+    }
+
+    /**
+     * Constructs an instance with the given field values.
+     */
+    public StructStat(long st_dev, long st_ino, int st_mode, long st_nlink, int st_uid, int st_gid,
+            long st_rdev, long st_size, StructTimespec st_atim, StructTimespec st_mtim,
+            StructTimespec st_ctim, long st_blksize, long st_blocks) {
+        this.st_dev = st_dev;
+        this.st_ino = st_ino;
+        this.st_mode = st_mode;
+        this.st_nlink = st_nlink;
+        this.st_uid = st_uid;
+        this.st_gid = st_gid;
+        this.st_rdev = st_rdev;
+        this.st_size = st_size;
+        this.st_atime = st_atim.tv_sec;
+        this.st_mtime = st_mtim.tv_sec;
+        this.st_ctime = st_ctim.tv_sec;
+        this.st_atim = st_atim;
+        this.st_mtim = st_mtim;
+        this.st_ctim = st_ctim;
+        this.st_blksize = st_blksize;
+        this.st_blocks = st_blocks;
+    }
+
+    @Override public String toString() {
+        return Objects.toString(this);
+    }
+}
diff --git a/ravenwood/runtime-helper-src/libcore-fake/android/system/StructTimespec.java b/ravenwood/runtime-helper-src/libcore-fake/android/system/StructTimespec.java
new file mode 100644
index 0000000..c106780
--- /dev/null
+++ b/ravenwood/runtime-helper-src/libcore-fake/android/system/StructTimespec.java
@@ -0,0 +1,79 @@
+/*
+ * Copyright (C) 2017 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 android.system;
+
+import libcore.util.Objects;;
+
+/**
+ * Corresponds to C's {@code struct timespec} from {@code <time.h>}.
+ */
+public final class StructTimespec implements Comparable<StructTimespec> {
+    /** Seconds part of time of last data modification. */
+    public final long tv_sec; /*time_t*/
+
+    /** Nanoseconds (values are [0, 999999999]). */
+    public final long tv_nsec;
+
+    public StructTimespec(long tv_sec, long tv_nsec) {
+        this.tv_sec = tv_sec;
+        this.tv_nsec = tv_nsec;
+        if (tv_nsec < 0 || tv_nsec > 999_999_999) {
+            throw new IllegalArgumentException(
+                    "tv_nsec value " + tv_nsec + " is not in [0, 999999999]");
+        }
+    }
+
+    @Override
+    public int compareTo(StructTimespec other) {
+        if (tv_sec > other.tv_sec) {
+            return 1;
+        }
+        if (tv_sec < other.tv_sec) {
+            return -1;
+        }
+        if (tv_nsec > other.tv_nsec) {
+            return 1;
+        }
+        if (tv_nsec < other.tv_nsec) {
+            return -1;
+        }
+        return 0;
+    }
+
+    @Override
+    public boolean equals(Object o) {
+        if (this == o) return true;
+        if (o == null || getClass() != o.getClass()) return false;
+
+        StructTimespec that = (StructTimespec) o;
+
+        if (tv_sec != that.tv_sec) return false;
+        return tv_nsec == that.tv_nsec;
+    }
+
+    @Override
+    public int hashCode() {
+        int result = (int) (tv_sec ^ (tv_sec >>> 32));
+        result = 31 * result + (int) (tv_nsec ^ (tv_nsec >>> 32));
+        return result;
+    }
+
+    @Override
+    public String toString() {
+        return Objects.toString(this);
+    }
+}
diff --git a/ravenwood/runtime-common-src/com/android/ravenwood/common/RavenwoodRuntimeNative.java b/ravenwood/runtime-helper-src/libcore-fake/com/android/ravenwood/common/RavenwoodRuntimeNative.java
similarity index 65%
rename from ravenwood/runtime-common-src/com/android/ravenwood/common/RavenwoodRuntimeNative.java
rename to ravenwood/runtime-helper-src/libcore-fake/com/android/ravenwood/common/RavenwoodRuntimeNative.java
index 6540221..e9b305e 100644
--- a/ravenwood/runtime-common-src/com/android/ravenwood/common/RavenwoodRuntimeNative.java
+++ b/ravenwood/runtime-helper-src/libcore-fake/com/android/ravenwood/common/RavenwoodRuntimeNative.java
@@ -15,6 +15,9 @@
  */
 package com.android.ravenwood.common;
 
+import android.system.ErrnoException;
+import android.system.StructStat;
+
 import java.io.FileDescriptor;
 
 /**
@@ -31,19 +34,25 @@
 
     public static native void applyFreeFunction(long freeFunction, long nativePtr);
 
-    public static native long nLseek(int fd, long offset, int whence);
+    private static native long nLseek(int fd, long offset, int whence) throws ErrnoException;
 
-    public static native int[] nPipe2(int flags);
+    private static native int[] nPipe2(int flags) throws ErrnoException;
 
-    public static native int nDup(int oldfd);
+    private static native int nDup(int oldfd) throws ErrnoException;
 
-    public static native int nFcntlInt(int fd, int cmd, int arg);
+    private static native int nFcntlInt(int fd, int cmd, int arg) throws ErrnoException;
 
-    public static long lseek(FileDescriptor fd, long offset, int whence) {
+    private static native StructStat nFstat(int fd) throws ErrnoException;
+
+    public static native StructStat lstat(String path) throws ErrnoException;
+
+    public static native StructStat stat(String path) throws ErrnoException;
+
+    public static long lseek(FileDescriptor fd, long offset, int whence) throws ErrnoException {
         return nLseek(JvmWorkaround.getInstance().getFdInt(fd), offset, whence);
     }
 
-    public static FileDescriptor[] pipe2(int flags) {
+    public static FileDescriptor[] pipe2(int flags) throws ErrnoException {
         var fds = nPipe2(flags);
         var ret = new FileDescriptor[] {
                 new FileDescriptor(),
@@ -55,7 +64,7 @@
         return ret;
     }
 
-    public static FileDescriptor dup(FileDescriptor fd) {
+    public static FileDescriptor dup(FileDescriptor fd) throws ErrnoException {
         var fdInt = nDup(JvmWorkaround.getInstance().getFdInt(fd));
 
         var retFd = new java.io.FileDescriptor();
@@ -63,9 +72,15 @@
         return retFd;
     }
 
-    public static int fcntlInt(FileDescriptor fd, int cmd, int arg) {
+    public static int fcntlInt(FileDescriptor fd, int cmd, int arg) throws ErrnoException {
         var fdInt = JvmWorkaround.getInstance().getFdInt(fd);
 
         return nFcntlInt(fdInt, cmd, arg);
     }
+
+    public static StructStat fstat(FileDescriptor fd) throws ErrnoException {
+        var fdInt = JvmWorkaround.getInstance().getFdInt(fd);
+
+        return nFstat(fdInt);
+    }
 }
diff --git a/ravenwood/runtime-helper-src/libcore-fake/libcore/util/Objects.java b/ravenwood/runtime-helper-src/libcore-fake/libcore/util/Objects.java
new file mode 100644
index 0000000..3781fcf
--- /dev/null
+++ b/ravenwood/runtime-helper-src/libcore-fake/libcore/util/Objects.java
@@ -0,0 +1,85 @@
+/*
+ * Copyright (C) 2010 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 libcore.util;
+
+import java.lang.reflect.Field;
+import java.lang.reflect.Modifier;
+import java.util.Arrays;
+
+public final class Objects {
+    private Objects() {}
+
+    /**
+     * Returns a string reporting the value of each declared field, via reflection.
+     * Static and transient fields are automatically skipped. Produces output like
+     * "SimpleClassName[integer=1234,string="hello",character='c',intArray=[1,2,3]]".
+     */
+    public static String toString(Object o) {
+        Class<?> c = o.getClass();
+        StringBuilder sb = new StringBuilder();
+        sb.append(c.getSimpleName()).append('[');
+        int i = 0;
+        for (Field f : c.getDeclaredFields()) {
+            if ((f.getModifiers() & (Modifier.STATIC | Modifier.TRANSIENT)) != 0) {
+                continue;
+            }
+            f.setAccessible(true);
+            try {
+                Object value = f.get(o);
+
+                if (i++ > 0) {
+                    sb.append(',');
+                }
+
+                sb.append(f.getName());
+                sb.append('=');
+
+                if (value.getClass().isArray()) {
+                    if (value.getClass() == boolean[].class) {
+                        sb.append(Arrays.toString((boolean[]) value));
+                    } else if (value.getClass() == byte[].class) {
+                        sb.append(Arrays.toString((byte[]) value));
+                    } else if (value.getClass() == char[].class) {
+                        sb.append(Arrays.toString((char[]) value));
+                    } else if (value.getClass() == double[].class) {
+                        sb.append(Arrays.toString((double[]) value));
+                    } else if (value.getClass() == float[].class) {
+                        sb.append(Arrays.toString((float[]) value));
+                    } else if (value.getClass() == int[].class) {
+                        sb.append(Arrays.toString((int[]) value));
+                    } else if (value.getClass() == long[].class) {
+                        sb.append(Arrays.toString((long[]) value));
+                    } else if (value.getClass() == short[].class) {
+                        sb.append(Arrays.toString((short[]) value));
+                    } else {
+                        sb.append(Arrays.toString((Object[]) value));
+                    }
+                } else if (value.getClass() == Character.class) {
+                    sb.append('\'').append(value).append('\'');
+                } else if (value.getClass() == String.class) {
+                    sb.append('"').append(value).append('"');
+                } else {
+                    sb.append(value);
+                }
+            } catch (IllegalAccessException unexpected) {
+                throw new AssertionError(unexpected);
+            }
+        }
+        sb.append("]");
+        return sb.toString();
+    }
+}
diff --git a/ravenwood/runtime-jni/ravenwood_runtime.cpp b/ravenwood/runtime-jni/ravenwood_runtime.cpp
index 34cf9f9..e0a3e1c 100644
--- a/ravenwood/runtime-jni/ravenwood_runtime.cpp
+++ b/ravenwood/runtime-jni/ravenwood_runtime.cpp
@@ -19,6 +19,9 @@
 #include <string.h>
 #include <unistd.h>
 #include <nativehelper/JNIHelp.h>
+#include <nativehelper/ScopedLocalRef.h>
+#include <nativehelper/ScopedUtfChars.h>
+
 #include "jni.h"
 #include "utils/Log.h"
 #include "utils/misc.h"
@@ -41,6 +44,75 @@
     return rc;
 }
 
+// ---- Helper functions ---
+
+static jclass g_StructStat;
+static jclass g_StructTimespecClass;
+
+static jclass findClass(JNIEnv* env, const char* name) {
+    ScopedLocalRef<jclass> localClass(env, env->FindClass(name));
+    jclass result = reinterpret_cast<jclass>(env->NewGlobalRef(localClass.get()));
+    if (result == NULL) {
+        ALOGE("failed to find class '%s'", name);
+        abort();
+    }
+    return result;
+}
+
+static jobject makeStructTimespec(JNIEnv* env, const struct timespec& ts) {
+    static jmethodID ctor = env->GetMethodID(g_StructTimespecClass, "<init>",
+            "(JJ)V");
+    if (ctor == NULL) {
+        return NULL;
+    }
+    return env->NewObject(g_StructTimespecClass, ctor,
+            static_cast<jlong>(ts.tv_sec), static_cast<jlong>(ts.tv_nsec));
+}
+
+static jobject makeStructStat(JNIEnv* env, const struct stat64& sb) {
+    static jmethodID ctor = env->GetMethodID(g_StructStat, "<init>",
+            "(JJIJIIJJLandroid/system/StructTimespec;Landroid/system/StructTimespec;Landroid/system/StructTimespec;JJ)V");
+    if (ctor == NULL) {
+        return NULL;
+    }
+
+    jobject atim_timespec = makeStructTimespec(env, sb.st_atim);
+    if (atim_timespec == NULL) {
+        return NULL;
+    }
+    jobject mtim_timespec = makeStructTimespec(env, sb.st_mtim);
+    if (mtim_timespec == NULL) {
+        return NULL;
+    }
+    jobject ctim_timespec = makeStructTimespec(env, sb.st_ctim);
+    if (ctim_timespec == NULL) {
+        return NULL;
+    }
+
+    return env->NewObject(g_StructStat, ctor,
+            static_cast<jlong>(sb.st_dev), static_cast<jlong>(sb.st_ino),
+            static_cast<jint>(sb.st_mode), static_cast<jlong>(sb.st_nlink),
+            static_cast<jint>(sb.st_uid), static_cast<jint>(sb.st_gid),
+            static_cast<jlong>(sb.st_rdev), static_cast<jlong>(sb.st_size),
+            atim_timespec, mtim_timespec, ctim_timespec,
+            static_cast<jlong>(sb.st_blksize), static_cast<jlong>(sb.st_blocks));
+}
+
+static jobject doStat(JNIEnv* env, jstring javaPath, bool isLstat) {
+    ScopedUtfChars path(env, javaPath);
+    if (path.c_str() == NULL) {
+        return NULL;
+    }
+    struct stat64 sb;
+    int rc = isLstat ? TEMP_FAILURE_RETRY(lstat64(path.c_str(), &sb))
+                     : TEMP_FAILURE_RETRY(stat64(path.c_str(), &sb));
+    if (rc == -1) {
+        throwErrnoException(env, isLstat ? "lstat" : "stat");
+        return NULL;
+    }
+    return makeStructStat(env, sb);
+}
+
 // ---- JNI methods ----
 
 typedef void (*FreeFunction)(void*);
@@ -77,6 +149,24 @@
     return throwIfMinusOne(env, "fcntl", TEMP_FAILURE_RETRY(fcntl(fd, F_DUPFD_CLOEXEC, 0)));
 }
 
+static jobject nFstat(JNIEnv* env, jobject, jint fd) {
+    struct stat64 sb;
+    int rc = TEMP_FAILURE_RETRY(fstat64(fd, &sb));
+    if (rc == -1) {
+        throwErrnoException(env, "fstat");
+        return NULL;
+    }
+    return makeStructStat(env, sb);
+}
+
+static jobject Linux_lstat(JNIEnv* env, jobject, jstring javaPath) {
+    return doStat(env, javaPath, true);
+}
+
+static jobject Linux_stat(JNIEnv* env, jobject, jstring javaPath) {
+    return doStat(env, javaPath, false);
+}
+
 // ---- Registration ----
 
 static const JNINativeMethod sMethods[] =
@@ -86,6 +176,9 @@
     { "nLseek", "(IJI)J", (void*)nLseek },
     { "nPipe2", "(I)[I", (void*)nPipe2 },
     { "nDup", "(I)I", (void*)nDup },
+    { "nFstat", "(I)Landroid/system/StructStat;", (void*)nFstat },
+    { "lstat", "(Ljava/lang/String;)Landroid/system/StructStat;", (void*)Linux_lstat },
+    { "stat", "(Ljava/lang/String;)Landroid/system/StructStat;", (void*)Linux_stat },
 };
 
 extern "C" jint JNI_OnLoad(JavaVM* vm, void* /* reserved */)
@@ -101,6 +194,9 @@
 
     ALOGI("%s: JNI_OnLoad", __FILE__);
 
+    g_StructStat = findClass(env, "android/system/StructStat");
+    g_StructTimespecClass = findClass(env, "android/system/StructTimespec");
+
     jint res = jniRegisterNativeMethods(env, "com/android/ravenwood/common/RavenwoodRuntimeNative",
             sMethods, NELEM(sMethods));
     if (res < 0) {
diff --git a/ravenwood/runtime-test/test/com/android/ravenwood/runtimetest/OsTest.java b/ravenwood/runtime-test/test/com/android/ravenwood/runtimetest/OsTest.java
index b5038e6..05275b2 100644
--- a/ravenwood/runtime-test/test/com/android/ravenwood/runtimetest/OsTest.java
+++ b/ravenwood/runtime-test/test/com/android/ravenwood/runtimetest/OsTest.java
@@ -15,15 +15,26 @@
  */
 package com.android.ravenwood.runtimetest;
 
+import static android.system.OsConstants.S_ISBLK;
+import static android.system.OsConstants.S_ISCHR;
+import static android.system.OsConstants.S_ISDIR;
+import static android.system.OsConstants.S_ISFIFO;
+import static android.system.OsConstants.S_ISLNK;
+import static android.system.OsConstants.S_ISREG;
+import static android.system.OsConstants.S_ISSOCK;
+
 import static org.junit.Assert.assertEquals;
 
+import static java.nio.file.LinkOption.NOFOLLOW_LINKS;
+
 import android.system.Os;
 import android.system.OsConstants;
+import android.system.StructStat;
+import android.system.StructTimespec;
 
 import androidx.test.ext.junit.runners.AndroidJUnit4;
 
 import com.android.ravenwood.common.JvmWorkaround;
-import com.android.ravenwood.common.RavenwoodRuntimeNative;
 
 import org.junit.Test;
 import org.junit.runner.RunWith;
@@ -32,8 +43,15 @@
 import java.io.FileDescriptor;
 import java.io.FileInputStream;
 import java.io.FileOutputStream;
-import java.io.IOException;
 import java.io.RandomAccessFile;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.nio.file.attribute.FileTime;
+import java.nio.file.attribute.PosixFileAttributes;
+import java.nio.file.attribute.PosixFilePermission;
+import java.util.HashSet;
+import java.util.Set;
+import java.util.concurrent.TimeUnit;
 
 @RunWith(AndroidJUnit4.class)
 public class OsTest {
@@ -41,7 +59,7 @@
         void accept(T var1) throws Exception;
     }
 
-    private void withTestFile(ConsumerWithThrow<FileDescriptor> consumer) throws Exception {
+    private void withTestFileFD(ConsumerWithThrow<FileDescriptor> consumer) throws Exception {
         File file = File.createTempFile("osTest", "bin");
         try (var raf = new RandomAccessFile(file, "rw")) {
             var fd = raf.getFD();
@@ -57,9 +75,20 @@
         }
     }
 
+    private void withTestFile(ConsumerWithThrow<Path> consumer) throws Exception {
+        var path = Files.createTempFile("osTest", "bin");
+        try (var os = Files.newOutputStream(path)) {
+            os.write(1);
+            os.write(2);
+            os.write(3);
+            os.write(4);
+        }
+        consumer.accept(path);
+    }
+
     @Test
     public void testLseek() throws Exception {
-        withTestFile((fd) -> {
+        withTestFileFD((fd) -> {
             assertEquals(4, Os.lseek(fd, 4, OsConstants.SEEK_SET));
             assertEquals(4, Os.lseek(fd, 0, OsConstants.SEEK_CUR));
             assertEquals(6, Os.lseek(fd, 2, OsConstants.SEEK_CUR));
@@ -68,7 +97,7 @@
 
     @Test
     public void testDup() throws Exception {
-        withTestFile((fd) -> {
+        withTestFileFD((fd) -> {
             var dup = Os.dup(fd);
 
             checkAreDup(fd, dup);
@@ -85,7 +114,7 @@
 
     @Test
     public void testFcntlInt() throws Exception {
-        withTestFile((fd) -> {
+        withTestFileFD((fd) -> {
             var dupInt = Os.fcntlInt(fd, 0, 0);
 
             var dup = new FileDescriptor();
@@ -95,16 +124,90 @@
         });
     }
 
-    private static void write(FileDescriptor fd, int oneByte)  throws IOException {
+    @Test
+    public void testStat() throws Exception {
+        withTestFile(path -> {
+            var attr = Files.readAttributes(path, PosixFileAttributes.class);
+            var stat = Os.stat(path.toAbsolutePath().toString());
+            assertAttributesEqual(attr, stat);
+        });
+    }
+
+    @Test
+    public void testLstat() throws Exception {
+        withTestFile(path -> {
+            // Create a symbolic link
+            var lnk = Files.createTempFile("osTest", "lnk");
+            Files.delete(lnk);
+            Files.createSymbolicLink(lnk, path);
+
+            // Test lstat
+            var attr = Files.readAttributes(lnk, PosixFileAttributes.class, NOFOLLOW_LINKS);
+            var stat = Os.lstat(lnk.toAbsolutePath().toString());
+            assertAttributesEqual(attr, stat);
+
+            // Test stat
+            var followAttr = Files.readAttributes(lnk, PosixFileAttributes.class);
+            var followStat = Os.stat(lnk.toAbsolutePath().toString());
+            assertAttributesEqual(followAttr, followStat);
+        });
+    }
+
+    @Test
+    public void testFstat() throws Exception {
+        withTestFile(path -> {
+            var attr = Files.readAttributes(path, PosixFileAttributes.class);
+            try (var raf = new RandomAccessFile(path.toFile(), "r")) {
+                var fd = raf.getFD();
+                var stat = Os.fstat(fd);
+                assertAttributesEqual(attr, stat);
+            }
+        });
+    }
+
+    // Verify StructStat values from libcore against native JVM PosixFileAttributes
+    private static void assertAttributesEqual(PosixFileAttributes attr, StructStat stat) {
+        assertEquals(attr.lastModifiedTime(), convertTimespecToFileTime(stat.st_mtim));
+        assertEquals(attr.size(), stat.st_size);
+        assertEquals(attr.isDirectory(), S_ISDIR(stat.st_mode));
+        assertEquals(attr.isRegularFile(), S_ISREG(stat.st_mode));
+        assertEquals(attr.isSymbolicLink(), S_ISLNK(stat.st_mode));
+        assertEquals(attr.isOther(), S_ISCHR(stat.st_mode)
+                || S_ISBLK(stat.st_mode) || S_ISFIFO(stat.st_mode) || S_ISSOCK(stat.st_mode));
+        assertEquals(attr.permissions(), convertModeToPosixPerms(stat.st_mode));
+
+    }
+
+    private static FileTime convertTimespecToFileTime(StructTimespec ts) {
+        var nanos = TimeUnit.SECONDS.toNanos(ts.tv_sec);
+        nanos += ts.tv_nsec;
+        return FileTime.from(nanos, TimeUnit.NANOSECONDS);
+    }
+
+    private static Set<PosixFilePermission> convertModeToPosixPerms(int mode) {
+        var set = new HashSet<PosixFilePermission>();
+        if ((mode & OsConstants.S_IRUSR) != 0) set.add(PosixFilePermission.OWNER_READ);
+        if ((mode & OsConstants.S_IWUSR) != 0) set.add(PosixFilePermission.OWNER_WRITE);
+        if ((mode & OsConstants.S_IXUSR) != 0) set.add(PosixFilePermission.OWNER_EXECUTE);
+        if ((mode & OsConstants.S_IRGRP) != 0) set.add(PosixFilePermission.GROUP_READ);
+        if ((mode & OsConstants.S_IWGRP) != 0) set.add(PosixFilePermission.GROUP_WRITE);
+        if ((mode & OsConstants.S_IXGRP) != 0) set.add(PosixFilePermission.GROUP_EXECUTE);
+        if ((mode & OsConstants.S_IROTH) != 0) set.add(PosixFilePermission.OTHERS_READ);
+        if ((mode & OsConstants.S_IWOTH) != 0) set.add(PosixFilePermission.OTHERS_WRITE);
+        if ((mode & OsConstants.S_IXOTH) != 0) set.add(PosixFilePermission.OTHERS_EXECUTE);
+        return set;
+    }
+
+    private static void write(FileDescriptor fd, int oneByte) throws Exception {
         // Create a dup to avoid closing the FD.
-        try (var dup = new FileOutputStream(RavenwoodRuntimeNative.dup(fd))) {
+        try (var dup = new FileOutputStream(Os.dup(fd))) {
             dup.write(oneByte);
         }
     }
 
-    private static int read(FileDescriptor fd) throws IOException {
+    private static int read(FileDescriptor fd) throws Exception {
         // Create a dup to avoid closing the FD.
-        try (var dup = new FileInputStream(RavenwoodRuntimeNative.dup(fd))) {
+        try (var dup = new FileInputStream(Os.dup(fd))) {
             return dup.read();
         }
     }