diff options
| author | 2024-07-09 23:24:06 +0000 | |
|---|---|---|
| committer | 2024-07-15 21:17:10 +0000 | |
| commit | d017c334ac7834a034131f088f08682ff04477dd (patch) | |
| tree | 976560507e00b2c27c11f1ea39e1db72cbdf6b47 | |
| parent | 7aea47ead67a6a525f0bbb609573535317960060 (diff) | |
[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
| -rw-r--r-- | ravenwood/runtime-helper-src/libcore-fake/android/system/Os.java | 13 | ||||
| -rw-r--r-- | ravenwood/runtime-helper-src/libcore-fake/android/system/StructStat.java | 121 | ||||
| -rw-r--r-- | ravenwood/runtime-helper-src/libcore-fake/android/system/StructTimespec.java | 79 | ||||
| -rw-r--r-- | ravenwood/runtime-helper-src/libcore-fake/com/android/ravenwood/common/RavenwoodRuntimeNative.java (renamed from ravenwood/runtime-common-src/com/android/ravenwood/common/RavenwoodRuntimeNative.java) | 31 | ||||
| -rw-r--r-- | ravenwood/runtime-helper-src/libcore-fake/libcore/util/Objects.java | 85 | ||||
| -rw-r--r-- | ravenwood/runtime-jni/ravenwood_runtime.cpp | 96 | ||||
| -rw-r--r-- | ravenwood/runtime-test/test/com/android/ravenwood/runtimetest/OsTest.java | 123 | 
7 files changed, 529 insertions, 19 deletions
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 e031eb27513b..8a1fe62db4e1 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 @@ public final class Os {          return RavenwoodRuntimeNative.lseek(fd, offset, whence);      } -      public static FileDescriptor[] pipe2(int flags) throws ErrnoException {          return RavenwoodRuntimeNative.pipe2(flags);      } @@ -42,4 +41,16 @@ public final class Os {      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 000000000000..a8b1fca464dd --- /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 000000000000..c10678057eeb --- /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 index 65402219ebee..e9b305e5d789 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 class RavenwoodRuntimeNative {      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; + +    private static native int[] nPipe2(int flags) throws ErrnoException; + +    private static native int nDup(int oldfd) throws ErrnoException; + +    private static native int nFcntlInt(int fd, int cmd, int arg) throws ErrnoException; -    public static native int[] nPipe2(int flags); +    private static native StructStat nFstat(int fd) throws ErrnoException; -    public static native int nDup(int oldfd); +    public static native StructStat lstat(String path) throws ErrnoException; -    public static native int nFcntlInt(int fd, int cmd, int arg); +    public static native StructStat stat(String path) throws ErrnoException; -    public static long lseek(FileDescriptor fd, long offset, int whence) { +    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 @@ public class RavenwoodRuntimeNative {          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 @@ public class RavenwoodRuntimeNative {          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 000000000000..3781fcf4e891 --- /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 34cf9f915677..e0a3e1c9edf6 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 @@ static rc_t throwIfMinusOne(JNIEnv* env, const char* name, rc_t rc) {      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 @@ static jlong nDup(JNIEnv* env, jclass, jint fd) {      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 @@ static const JNINativeMethod sMethods[] =      { "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 @@ extern "C" jint JNI_OnLoad(JavaVM* vm, void* /* reserved */)      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 b5038e68516d..05275b29e48b 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.File;  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 @@ public class OsTest {          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 @@ public class OsTest {          }      } +    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 @@ public class OsTest {      @Test      public void testDup() throws Exception { -        withTestFile((fd) -> { +        withTestFileFD((fd) -> {              var dup = Os.dup(fd);              checkAreDup(fd, dup); @@ -85,7 +114,7 @@ public class OsTest {      @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 @@ public class OsTest {          });      } -    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();          }      }  |