summaryrefslogtreecommitdiff
path: root/file.c
diff options
context:
space:
mode:
Diffstat (limited to 'file.c')
-rw-r--r--file.c3331
1 files changed, 2323 insertions, 1008 deletions
diff --git a/file.c b/file.c
index ced2c1e4a2..c4a531d783 100644
--- a/file.c
+++ b/file.c
@@ -12,6 +12,7 @@
**********************************************************************/
#include "ruby/internal/config.h"
+#include "ruby/internal/attr/nonstring.h"
#ifdef _WIN32
# include "missing/file.h"
@@ -115,6 +116,8 @@ int flock(int, int);
# define link(f, t) rb_w32_ulink((f), (t))
# undef unlink
# define unlink(p) rb_w32_uunlink(p)
+# undef readlink
+# define readlink(f, t, l) rb_w32_ureadlink((f), (t), (l))
# undef rename
# define rename(f, t) rb_w32_urename((f), (t))
# undef symlink
@@ -127,6 +130,14 @@ int flock(int, int);
# endif
#else
# define STAT(p, s) stat((p), (s))
+#endif /* _WIN32 */
+
+#ifdef HAVE_STRUCT_STATX_STX_BTIME
+# define ST_(name) stx_ ## name
+typedef struct statx_timestamp stat_timestamp;
+#else
+# define ST_(name) st_ ## name
+typedef struct timespec stat_timestamp;
#endif
#if defined _WIN32 || defined __APPLE__
@@ -158,6 +169,7 @@ int flock(int, int);
#include "internal.h"
#include "internal/compilers.h"
#include "internal/dir.h"
+#include "internal/encoding.h"
#include "internal/error.h"
#include "internal/file.h"
#include "internal/io.h"
@@ -167,10 +179,16 @@ int flock(int, int);
#include "internal/thread.h"
#include "internal/vm.h"
#include "ruby/encoding.h"
-#include "ruby/io.h"
#include "ruby/thread.h"
#include "ruby/util.h"
+#define UIANY2NUM(x) \
+ ((sizeof(x) <= sizeof(unsigned int)) ? \
+ UINT2NUM((unsigned)(x)) : \
+ (sizeof(x) <= sizeof(unsigned long)) ? \
+ ULONG2NUM((unsigned long)(x)) : \
+ ULL2NUM((unsigned LONG_LONG)(x)))
+
VALUE rb_cFile;
VALUE rb_mFileTest;
VALUE rb_cStat;
@@ -196,15 +214,16 @@ file_path_convert(VALUE name)
return name;
}
-static rb_encoding *
+static void
check_path_encoding(VALUE str)
{
- rb_encoding *enc = rb_enc_get(str);
- if (!rb_enc_asciicompat(enc)) {
- rb_raise(rb_eEncCompatError, "path name must be ASCII-compatible (%s): %"PRIsVALUE,
- rb_enc_name(enc), rb_str_inspect(str));
+ if (RB_UNLIKELY(!rb_str_enc_fastpath(str))) {
+ rb_encoding *enc = rb_str_enc_get(str);
+ if (!rb_enc_asciicompat(enc)) {
+ rb_raise(rb_eEncCompatError, "path name must be ASCII-compatible (%s): %"PRIsVALUE,
+ rb_enc_name(enc), rb_str_inspect(str));
+ }
}
- return enc;
}
VALUE
@@ -226,13 +245,20 @@ VALUE
rb_get_path_check_convert(VALUE obj)
{
obj = file_path_convert(obj);
+ rb_get_path_check_no_convert(obj);
+ return rb_str_new_frozen(obj);
+}
+/* TODO: name */
+VALUE
+rb_get_path_check_no_convert(VALUE obj)
+{
check_path_encoding(obj);
if (!rb_str_to_cstr(obj)) {
rb_raise(rb_eArgError, "path name contains null byte");
}
- return rb_str_new4(obj);
+ return obj;
}
VALUE
@@ -247,6 +273,19 @@ rb_get_path(VALUE obj)
return rb_get_path_check_convert(rb_get_path_check_to_string(obj));
}
+static inline VALUE
+check_path(VALUE obj, const char **cstr)
+{
+ VALUE str = rb_get_path_check_convert(rb_get_path_check_to_string(obj));
+#if RUBY_DEBUG
+ str = rb_str_new_frozen(str);
+#endif
+ *cstr = RSTRING_PTR(str);
+ return str;
+}
+
+#define CheckPath(str, cstr) RB_GC_GUARD(str) = check_path(str, &cstr);
+
VALUE
rb_str_encode_ospath(VALUE path)
{
@@ -262,22 +301,73 @@ rb_str_encode_ospath(VALUE path)
rb_encoding *utf8 = rb_utf8_encoding();
path = rb_str_conv_enc(path, enc, utf8);
}
-#endif
+#endif /* USE_OSPATH */
return path;
}
#ifdef __APPLE__
# define NORMALIZE_UTF8PATH 1
+
+# ifdef HAVE_WORKING_FORK
+static CFMutableStringRef
+mutable_CFString_new(CFStringRef *s, const char *ptr, long len)
+{
+ const CFAllocatorRef alloc = kCFAllocatorDefault;
+ *s = CFStringCreateWithBytesNoCopy(alloc, (const UInt8 *)ptr, len,
+ kCFStringEncodingUTF8, FALSE,
+ kCFAllocatorNull);
+ return CFStringCreateMutableCopy(alloc, len, *s);
+}
+
+# define mutable_CFString_release(m, s) (CFRelease(m), CFRelease(s))
+
+static void
+rb_CFString_class_initialize_before_fork(void)
+{
+ /*
+ * Since macOS 13, CFString family API used in
+ * rb_str_append_normalized_ospath may internally use Objective-C classes
+ * (NSTaggedPointerString and NSPlaceholderMutableString) for small strings.
+ *
+ * On the other hand, Objective-C classes should not be used for the first
+ * time in a fork()'ed but not exec()'ed process. Violations for this rule
+ * can result deadlock during class initialization, so Objective-C runtime
+ * conservatively crashes on such cases by default.
+ *
+ * Therefore, we need to use CFString API to initialize Objective-C classes
+ * used internally *before* fork().
+ *
+ * For future changes, please note that this initialization process cannot
+ * be done in ctor because NSTaggedPointerString in CoreFoundation is enabled
+ * after CFStringInitializeTaggedStrings(), which is called during loading
+ * Objective-C runtime after ctor.
+ * For more details, see https://bugs.ruby-lang.org/issues/18912
+ */
+
+ /* Enough small but non-empty ASCII string to fit in NSTaggedPointerString. */
+ const char small_str[] = "/";
+ long len = sizeof(small_str) - 1;
+ CFStringRef s;
+ /*
+ * Touch `CFStringCreateWithBytesNoCopy` *twice* because the implementation
+ * shipped with macOS 15.0 24A5331b does not return `NSTaggedPointerString`
+ * instance for the first call (totally not sure why). CoreFoundation
+ * shipped with macOS 15.1 does not have this issue.
+ */
+ for (int i = 0; i < 2; i++) {
+ CFMutableStringRef m = mutable_CFString_new(&s, small_str, len);
+ mutable_CFString_release(m, s);
+ }
+}
+# endif /* HAVE_WORKING_FORK */
+
static VALUE
rb_str_append_normalized_ospath(VALUE str, const char *ptr, long len)
{
CFIndex buflen = 0;
CFRange all;
- CFStringRef s = CFStringCreateWithBytesNoCopy(kCFAllocatorDefault,
- (const UInt8 *)ptr, len,
- kCFStringEncodingUTF8, FALSE,
- kCFAllocatorNull);
- CFMutableStringRef m = CFStringCreateMutableCopy(kCFAllocatorDefault, len, s);
+ CFStringRef s;
+ CFMutableStringRef m = mutable_CFString_new(&s, ptr, len);
long oldlen = RSTRING_LEN(str);
CFStringNormalize(m, kCFStringNormalizationFormC);
@@ -287,8 +377,7 @@ rb_str_append_normalized_ospath(VALUE str, const char *ptr, long len)
CFStringGetBytes(m, all, kCFStringEncodingUTF8, '?', FALSE,
(UInt8 *)(RSTRING_PTR(str) + oldlen), buflen, &buflen);
rb_str_set_len(str, oldlen + buflen);
- CFRelease(m);
- CFRelease(s);
+ mutable_CFString_release(m, s);
return str;
}
@@ -298,16 +387,22 @@ rb_str_normalize_ospath(const char *ptr, long len)
const char *p = ptr;
const char *e = ptr + len;
const char *p1 = p;
- VALUE str = rb_str_buf_new(len);
rb_encoding *enc = rb_utf8_encoding();
- rb_enc_associate(str, enc);
+ VALUE str = rb_utf8_str_new(ptr, len);
+ if (RB_LIKELY(rb_enc_str_coderange(str) == ENC_CODERANGE_7BIT)) {
+ return str;
+ }
+ else {
+ str = rb_str_buf_new(len);
+ rb_enc_associate(str, enc);
+ }
while (p < e) {
int l, c;
int r = rb_enc_precise_mbclen(p, e, enc);
if (!MBCLEN_CHARFOUND_P(r)) {
/* invalid byte shall not happen but */
- static const char invalid[3] = "\xEF\xBF\xBD";
+ RBIMPL_ATTR_NONSTRING() static const char invalid[3] = "\xEF\xBF\xBD";
rb_str_append_normalized_ospath(str, p1, p-p1);
rb_str_cat(str, invalid, sizeof(invalid));
p += 1;
@@ -367,9 +462,9 @@ ignored_char_p(const char *p, const char *e, rb_encoding *enc)
}
return 0;
}
-#else
+#else /* !__APPLE__ */
# define NORMALIZE_UTF8PATH 0
-#endif
+#endif /* __APPLE__ */
#define apply2args(n) (rb_check_arity(argc, n, UNLIMITED_ARGUMENTS), argc-=n)
@@ -427,7 +522,7 @@ apply2files(int (*func)(const char *, void *), int argc, VALUE *argv, void *arg)
aa->fn[aa->i].path = path;
}
- rb_thread_call_without_gvl(no_gvl_apply2files, aa, RUBY_UBF_IO, 0);
+ IO_WITHOUT_GVL(no_gvl_apply2files, aa);
if (aa->errnum) {
#ifdef UTIME_EINVAL
if (func == utime_internal) {
@@ -442,105 +537,135 @@ apply2files(int (*func)(const char *, void *), int argc, VALUE *argv, void *arg)
return LONG2FIX(argc);
}
-/*
- * call-seq:
- * path -> filepath
- *
- * Returns the string filepath used to create +self+:
- *
- * f = File.new('t.txt') # => #<File:t.txt>
- f.path # => "t.txt"
- *
- * Does not normalize the returned filepath:
- *
- * f = File.new('../files/t.txt') # => #<File:../files/t.txt>
- f.path # => "../files/t.txt"
- *
- * Raises IOError for a file created using File::Constants::TMPFILE, because it has no filename.
- *
- * File#to_path is an alias for File#path.
- *
- */
-
-static VALUE
-rb_file_path(VALUE obj)
-{
- rb_io_t *fptr;
-
- fptr = RFILE(rb_io_taint_check(obj))->fptr;
- rb_io_check_initialized(fptr);
-
- if (NIL_P(fptr->pathv)) {
- rb_raise(rb_eIOError, "File is unnamed (TMPFILE?)");
- }
-
- return rb_str_dup(fptr->pathv);
-}
-
-static size_t
-stat_memsize(const void *p)
-{
- return sizeof(struct stat);
-}
+static stat_timestamp stat_atimespec(const struct stat *st);
+static stat_timestamp stat_mtimespec(const struct stat *st);
+static stat_timestamp stat_ctimespec(const struct stat *st);
static const rb_data_type_t stat_data_type = {
"stat",
- {NULL, RUBY_TYPED_DEFAULT_FREE, stat_memsize,},
- 0, 0, RUBY_TYPED_FREE_IMMEDIATELY
+ {
+ NULL,
+ RUBY_TYPED_DEFAULT_FREE,
+ NULL, // No external memory to report
+ },
+ 0, 0, RUBY_TYPED_FREE_IMMEDIATELY | RUBY_TYPED_WB_PROTECTED | RUBY_TYPED_EMBEDDABLE
};
-static VALUE
-stat_new_0(VALUE klass, const struct stat *st)
+struct rb_stat {
+ rb_io_stat_data stat;
+ bool initialized;
+};
+
+static struct rb_stat *
+stat_alloc(VALUE klass, VALUE *obj)
{
- struct stat *nst = 0;
- VALUE obj = TypedData_Wrap_Struct(klass, &stat_data_type, 0);
+ struct rb_stat *rb_st;
+ *obj = TypedData_Make_Struct(klass, struct rb_stat, &stat_data_type, rb_st);
+ return rb_st;
+}
+VALUE
+rb_stat_new(const struct stat *st)
+{
+ VALUE obj;
+ struct rb_stat *rb_st = stat_alloc(rb_cStat, &obj);
if (st) {
- nst = ALLOC(struct stat);
- *nst = *st;
- RTYPEDDATA_DATA(obj) = nst;
+#if RUBY_USE_STATX
+# define CP(m) .stx_ ## m = st->st_ ## m
+# define CP_32(m) .stx_ ## m = (uint32_t)st->st_ ## m
+# define CP_TS(m) .stx_ ## m = stat_ ## m ## spec(st)
+ rb_st->stat = (struct statx){
+ .stx_mask = STATX_BASIC_STATS,
+ CP(mode),
+ CP_32(nlink),
+ CP(uid),
+ CP(gid),
+ CP_TS(atime),
+ CP_TS(mtime),
+ CP_TS(ctime),
+ CP(ino),
+ CP(size),
+ CP(blocks),
+ };
+# undef CP
+# undef CP_TS
+#else
+ rb_st->stat = *st;
+#endif
+ rb_st->initialized = true;
}
+
return obj;
}
+#ifndef rb_statx_new
VALUE
-rb_stat_new(const struct stat *st)
+rb_statx_new(const rb_io_stat_data *st)
{
- return stat_new_0(rb_cStat, st);
+ VALUE obj;
+ struct rb_stat *rb_st = stat_alloc(rb_cStat, &obj);
+ if (st) {
+ rb_st->stat = *st;
+ rb_st->initialized = true;
+ }
+ return obj;
}
+#endif
-static struct stat*
+static rb_io_stat_data*
get_stat(VALUE self)
{
- struct stat* st;
- TypedData_Get_Struct(self, struct stat, &stat_data_type, st);
- if (!st) rb_raise(rb_eTypeError, "uninitialized File::Stat");
- return st;
+ struct rb_stat* rb_st;
+ TypedData_Get_Struct(self, struct rb_stat, &stat_data_type, rb_st);
+ if (!rb_st->initialized) rb_raise(rb_eTypeError, "uninitialized File::Stat");
+ return &rb_st->stat;
}
-static struct timespec stat_mtimespec(const struct stat *st);
+#if RUBY_USE_STATX
+static stat_timestamp
+statx_mtimespec(const rb_io_stat_data *st)
+{
+ return st->stx_mtime;
+}
+#else
+# define statx_mtimespec stat_mtimespec
+#endif
/*
* call-seq:
- * stat <=> other_stat -> -1, 0, 1, nil
+ * self <=> other -> -1, 0, 1, or nil
+ *
+ * Compares +self+ and +other+, by comparing their modification times;
+ * that is, by comparing <tt>self.mtime</tt> and <tt>other.mtime</tt>.
+ *
+ * Returns:
+ *
+ * - +-1+, if <tt>self.mtime</tt> is earlier.
+ * - +0+, if the two values are equal.
+ * - +1+, if <tt>self.mtime</tt> is later.
+ * - +nil+, if +other+ is not a File::Stat object.
*
- * Compares File::Stat objects by comparing their respective modification
- * times.
+ * Examples:
*
- * +nil+ is returned if +other_stat+ is not a File::Stat object
+ * stat0 = File.stat('README.md')
+ * stat1 = File.stat('NEWS.md')
+ * stat0.mtime # => 2025-12-20 15:33:05.6972341 -0600
+ * stat1.mtime # => 2025-12-20 16:02:08.2672945 -0600
+ * stat0 <=> stat1 # => -1
+ * stat0 <=> stat0.dup # => 0
+ * stat1 <=> stat0 # => 1
+ * stat0 <=> :foo # => nil
*
- * f1 = File.new("f1", "w")
- * sleep 1
- * f2 = File.new("f2", "w")
- * f1.stat <=> f2.stat #=> -1
+ * \Class \File::Stat includes module Comparable,
+ * each of whose methods uses File::Stat#<=> for comparison.
*/
static VALUE
rb_stat_cmp(VALUE self, VALUE other)
{
if (rb_obj_is_kind_of(other, rb_obj_class(self))) {
- struct timespec ts1 = stat_mtimespec(get_stat(self));
- struct timespec ts2 = stat_mtimespec(get_stat(other));
+ stat_timestamp ts1 = statx_mtimespec(get_stat(self));
+ stat_timestamp ts2 = statx_mtimespec(get_stat(other));
if (ts1.tv_sec == ts2.tv_sec) {
if (ts1.tv_nsec == ts2.tv_nsec) return INT2FIX(0);
if (ts1.tv_nsec < ts2.tv_nsec) return INT2FIX(-1);
@@ -577,7 +702,11 @@ rb_stat_cmp(VALUE self, VALUE other)
static VALUE
rb_stat_dev(VALUE self)
{
-#if SIZEOF_STRUCT_STAT_ST_DEV <= SIZEOF_DEV_T
+#if RUBY_USE_STATX
+ unsigned int m = get_stat(self)->stx_dev_major;
+ unsigned int n = get_stat(self)->stx_dev_minor;
+ return ULL2NUM(makedev(m, n));
+#elif SIZEOF_STRUCT_STAT_ST_DEV <= SIZEOF_DEV_T
return DEVT2NUM(get_stat(self)->st_dev);
#elif SIZEOF_STRUCT_STAT_ST_DEV <= SIZEOF_LONG
return ULONG2NUM(get_stat(self)->st_dev);
@@ -600,7 +729,9 @@ rb_stat_dev(VALUE self)
static VALUE
rb_stat_dev_major(VALUE self)
{
-#if defined(major)
+#if RUBY_USE_STATX
+ return UINT2NUM(get_stat(self)->stx_dev_major);
+#elif defined(major)
return UINT2NUM(major(get_stat(self)->st_dev));
#else
return Qnil;
@@ -621,7 +752,9 @@ rb_stat_dev_major(VALUE self)
static VALUE
rb_stat_dev_minor(VALUE self)
{
-#if defined(minor)
+#if RUBY_USE_STATX
+ return UINT2NUM(get_stat(self)->stx_dev_minor);
+#elif defined(minor)
return UINT2NUM(minor(get_stat(self)->st_dev));
#else
return Qnil;
@@ -641,16 +774,15 @@ rb_stat_dev_minor(VALUE self)
static VALUE
rb_stat_ino(VALUE self)
{
+ rb_io_stat_data *ptr = get_stat(self);
#ifdef HAVE_STRUCT_STAT_ST_INOHIGH
/* assume INTEGER_PACK_LSWORD_FIRST and st_inohigh is just next of st_ino */
- return rb_integer_unpack(&get_stat(self)->st_ino, 2,
+ return rb_integer_unpack(&ptr->st_ino, 2,
SIZEOF_STRUCT_STAT_ST_INO, 0,
INTEGER_PACK_LSWORD_FIRST|INTEGER_PACK_NATIVE_BYTE_ORDER|
INTEGER_PACK_2COMP);
-#elif SIZEOF_STRUCT_STAT_ST_INO > SIZEOF_LONG
- return ULL2NUM(get_stat(self)->st_ino);
#else
- return ULONG2NUM(get_stat(self)->st_ino);
+ return UIANY2NUM(ptr->ST_(ino));
#endif
}
@@ -670,7 +802,7 @@ rb_stat_ino(VALUE self)
static VALUE
rb_stat_mode(VALUE self)
{
- return UINT2NUM(ST2UINT(get_stat(self)->st_mode));
+ return UINT2NUM(ST2UINT(get_stat(self)->ST_(mode)));
}
/*
@@ -689,20 +821,9 @@ static VALUE
rb_stat_nlink(VALUE self)
{
/* struct stat::st_nlink is nlink_t in POSIX. Not the case for Windows. */
- const struct stat *ptr = get_stat(self);
+ const rb_io_stat_data *ptr = get_stat(self);
- if (sizeof(ptr->st_nlink) <= sizeof(int)) {
- return UINT2NUM((unsigned)ptr->st_nlink);
- }
- else if (sizeof(ptr->st_nlink) == sizeof(long)) {
- return ULONG2NUM((unsigned long)ptr->st_nlink);
- }
- else if (sizeof(ptr->st_nlink) == sizeof(LONG_LONG)) {
- return ULL2NUM((unsigned LONG_LONG)ptr->st_nlink);
- }
- else {
- rb_bug(":FIXME: don't know what to do");
- }
+ return UIANY2NUM(ptr->ST_(nlink));
}
/*
@@ -718,7 +839,7 @@ rb_stat_nlink(VALUE self)
static VALUE
rb_stat_uid(VALUE self)
{
- return UIDT2NUM(get_stat(self)->st_uid);
+ return UIDT2NUM(get_stat(self)->ST_(uid));
}
/*
@@ -734,7 +855,7 @@ rb_stat_uid(VALUE self)
static VALUE
rb_stat_gid(VALUE self)
{
- return GIDT2NUM(get_stat(self)->st_gid);
+ return GIDT2NUM(get_stat(self)->ST_(gid));
}
/*
@@ -752,16 +873,18 @@ rb_stat_gid(VALUE self)
static VALUE
rb_stat_rdev(VALUE self)
{
-#ifdef HAVE_STRUCT_STAT_ST_RDEV
-# if SIZEOF_STRUCT_STAT_ST_RDEV <= SIZEOF_DEV_T
- return DEVT2NUM(get_stat(self)->st_rdev);
-# elif SIZEOF_STRUCT_STAT_ST_RDEV <= SIZEOF_LONG
- return ULONG2NUM(get_stat(self)->st_rdev);
-# else
- return ULL2NUM(get_stat(self)->st_rdev);
-# endif
-#else
+#if RUBY_USE_STATX
+ unsigned int m = get_stat(self)->stx_rdev_major;
+ unsigned int n = get_stat(self)->stx_rdev_minor;
+ return ULL2NUM(makedev(m, n));
+#elif !defined(HAVE_STRUCT_STAT_ST_RDEV)
return Qnil;
+#elif SIZEOF_STRUCT_STAT_ST_RDEV <= SIZEOF_DEV_T
+ return DEVT2NUM(get_stat(self)->ST_(rdev));
+#elif SIZEOF_STRUCT_STAT_ST_RDEV <= SIZEOF_LONG
+ return ULONG2NUM(get_stat(self)->ST_(rdev));
+#else
+ return ULL2NUM(get_stat(self)->ST_(rdev));
#endif
}
@@ -779,8 +902,10 @@ rb_stat_rdev(VALUE self)
static VALUE
rb_stat_rdev_major(VALUE self)
{
-#if defined(HAVE_STRUCT_STAT_ST_RDEV) && defined(major)
- return UINT2NUM(major(get_stat(self)->st_rdev));
+#if RUBY_USE_STATX
+ return UINT2NUM(get_stat(self)->stx_rdev_major);
+#elif defined(HAVE_STRUCT_STAT_ST_RDEV) && defined(major)
+ return UINT2NUM(major(get_stat(self)->ST_(rdev)));
#else
return Qnil;
#endif
@@ -800,8 +925,10 @@ rb_stat_rdev_major(VALUE self)
static VALUE
rb_stat_rdev_minor(VALUE self)
{
-#if defined(HAVE_STRUCT_STAT_ST_RDEV) && defined(minor)
- return UINT2NUM(minor(get_stat(self)->st_rdev));
+#if RUBY_USE_STATX
+ return UINT2NUM(get_stat(self)->stx_rdev_minor);
+#elif defined(HAVE_STRUCT_STAT_ST_RDEV) && defined(minor)
+ return UINT2NUM(minor(get_stat(self)->ST_(rdev)));
#else
return Qnil;
#endif
@@ -819,7 +946,7 @@ rb_stat_rdev_minor(VALUE self)
static VALUE
rb_stat_size(VALUE self)
{
- return OFFT2NUM(get_stat(self)->st_size);
+ return OFFT2NUM(get_stat(self)->ST_(size));
}
/*
@@ -837,7 +964,7 @@ static VALUE
rb_stat_blksize(VALUE self)
{
#ifdef HAVE_STRUCT_STAT_ST_BLKSIZE
- return ULONG2NUM(get_stat(self)->st_blksize);
+ return ULONG2NUM(get_stat(self)->ST_(blksize));
#else
return Qnil;
#endif
@@ -859,34 +986,44 @@ rb_stat_blocks(VALUE self)
{
#ifdef HAVE_STRUCT_STAT_ST_BLOCKS
# if SIZEOF_STRUCT_STAT_ST_BLOCKS > SIZEOF_LONG
- return ULL2NUM(get_stat(self)->st_blocks);
+ return ULL2NUM(get_stat(self)->ST_(blocks));
# else
- return ULONG2NUM(get_stat(self)->st_blocks);
+ return ULONG2NUM(get_stat(self)->ST_(blocks));
# endif
#else
return Qnil;
#endif
}
-static struct timespec
+static stat_timestamp
stat_atimespec(const struct stat *st)
{
- struct timespec ts;
+ stat_timestamp ts;
ts.tv_sec = st->st_atime;
#if defined(HAVE_STRUCT_STAT_ST_ATIM)
- ts.tv_nsec = st->st_atim.tv_nsec;
+ ts.tv_nsec = (uint32_t)st->st_atim.tv_nsec;
#elif defined(HAVE_STRUCT_STAT_ST_ATIMESPEC)
- ts.tv_nsec = st->st_atimespec.tv_nsec;
+ ts.tv_nsec = (uint32_t)st->st_atimespec.tv_nsec;
#elif defined(HAVE_STRUCT_STAT_ST_ATIMENSEC)
- ts.tv_nsec = (long)st->st_atimensec;
+ ts.tv_nsec = (uint32_t)st->st_atimensec;
#else
- ts.tv_nsec = 0;
+ ts.tv_nsec = 0
#endif
return ts;
}
+#if RUBY_USE_STATX
+static stat_timestamp
+statx_atimespec(const rb_io_stat_data *st)
+{
+ return st->stx_atime;
+}
+#else
+# define statx_atimespec stat_atimespec
+#endif
+
static VALUE
-stat_time(const struct timespec ts)
+stat_time(const stat_timestamp ts)
{
return rb_time_nano_new(ts.tv_sec, ts.tv_nsec);
}
@@ -897,17 +1034,17 @@ stat_atime(const struct stat *st)
return stat_time(stat_atimespec(st));
}
-static struct timespec
+static stat_timestamp
stat_mtimespec(const struct stat *st)
{
- struct timespec ts;
+ stat_timestamp ts;
ts.tv_sec = st->st_mtime;
#if defined(HAVE_STRUCT_STAT_ST_MTIM)
- ts.tv_nsec = st->st_mtim.tv_nsec;
+ ts.tv_nsec = (uint32_t)st->st_mtim.tv_nsec;
#elif defined(HAVE_STRUCT_STAT_ST_MTIMESPEC)
- ts.tv_nsec = st->st_mtimespec.tv_nsec;
+ ts.tv_nsec = (uint32_t)st->st_mtimespec.tv_nsec;
#elif defined(HAVE_STRUCT_STAT_ST_MTIMENSEC)
- ts.tv_nsec = (long)st->st_mtimensec;
+ ts.tv_nsec = (uint32_t)st->st_mtimensec;
#else
ts.tv_nsec = 0;
#endif
@@ -920,23 +1057,33 @@ stat_mtime(const struct stat *st)
return stat_time(stat_mtimespec(st));
}
-static struct timespec
+static stat_timestamp
stat_ctimespec(const struct stat *st)
{
- struct timespec ts;
+ stat_timestamp ts;
ts.tv_sec = st->st_ctime;
#if defined(HAVE_STRUCT_STAT_ST_CTIM)
- ts.tv_nsec = st->st_ctim.tv_nsec;
+ ts.tv_nsec = (uint32_t)st->st_ctim.tv_nsec;
#elif defined(HAVE_STRUCT_STAT_ST_CTIMESPEC)
- ts.tv_nsec = st->st_ctimespec.tv_nsec;
+ ts.tv_nsec = (uint32_t)st->st_ctimespec.tv_nsec;
#elif defined(HAVE_STRUCT_STAT_ST_CTIMENSEC)
- ts.tv_nsec = (long)st->st_ctimensec;
+ ts.tv_nsec = (uint32_t)st->st_ctimensec;
#else
ts.tv_nsec = 0;
#endif
return ts;
}
+#if RUBY_USE_STATX
+static stat_timestamp
+statx_ctimespec(const rb_io_stat_data *st)
+{
+ return st->stx_ctime;
+}
+#else
+# define statx_ctimespec stat_ctimespec
+#endif
+
static VALUE
stat_ctime(const struct stat *st)
{
@@ -945,35 +1092,50 @@ stat_ctime(const struct stat *st)
#define HAVE_STAT_BIRTHTIME
#if defined(HAVE_STRUCT_STAT_ST_BIRTHTIMESPEC)
-typedef struct stat statx_data;
static VALUE
-stat_birthtime(const struct stat *st)
+statx_birthtime(const rb_io_stat_data *st)
{
- const struct timespec *ts = &st->st_birthtimespec;
+ const stat_timestamp *ts = &st->ST_(birthtimespec);
return rb_time_nano_new(ts->tv_sec, ts->tv_nsec);
}
+#elif defined(HAVE_STRUCT_STATX_STX_BTIME)
+static VALUE statx_birthtime(const rb_io_stat_data *st);
#elif defined(_WIN32)
-typedef struct stat statx_data;
-# define stat_birthtime stat_ctime
+# define statx_birthtime stat_ctime
#else
# undef HAVE_STAT_BIRTHTIME
-#endif
+#endif /* defined(HAVE_STRUCT_STAT_ST_BIRTHTIMESPEC) */
/*
* call-seq:
- * stat.atime -> time
- *
- * Returns the last access time for this file as an object of class
- * Time.
- *
- * File.stat("testfile").atime #=> Wed Dec 31 18:00:00 CST 1969
- *
+ * atime -> new_time
+ *
+ * Returns a new Time object containing the access time
+ * of the object represented by +self+
+ * at the time +self+ was created;
+ * see {Snapshot}[rdoc-ref:File::Stat@Snapshot]:
+ *
+ * filepath = 't.tmp'
+ * File.write(filepath, 'foo')
+ * file = File.new(filepath, 'w')
+ * stat = File::Stat.new(filepath)
+ * file.atime # => 2026-03-31 16:26:39.5913207 -0500
+ * stat.atime # => 2026-03-31 16:26:39.5913207 -0500
+ * File.write(filepath, 'bar')
+ * file.atime # => 2026-03-31 16:27:01.4981624 -0500 # Changed by access.
+ * stat.atime # => 2026-03-31 16:26:39.5913207 -0500 # Unchanged by access.
+ * stat = File::Stat.new(filepath)
+ * stat.atime # => 2026-03-31 16:27:01.4981624 -0500 # New access time.
+ * file.close
+ * File.delete(filepath)
+ *
+ * See {File System Timestamps}[rdoc-ref:file/timestamps.md].
*/
static VALUE
rb_stat_atime(VALUE self)
{
- return stat_atime(get_stat(self));
+ return stat_time(statx_atimespec(get_stat(self)));
}
/*
@@ -989,7 +1151,7 @@ rb_stat_atime(VALUE self)
static VALUE
rb_stat_mtime(VALUE self)
{
- return stat_mtime(get_stat(self));
+ return stat_time(statx_mtimespec(get_stat(self)));
}
/*
@@ -1009,36 +1171,34 @@ rb_stat_mtime(VALUE self)
static VALUE
rb_stat_ctime(VALUE self)
{
- return stat_ctime(get_stat(self));
+ return stat_time(statx_ctimespec(get_stat(self)));
}
#if defined(HAVE_STAT_BIRTHTIME)
/*
* call-seq:
- * stat.birthtime -> time
- *
- * Returns the birth time for <i>stat</i>.
- *
- * If the platform doesn't have birthtime, raises NotImplementedError.
- *
- * File.write("testfile", "foo")
- * sleep 10
- * File.write("testfile", "bar")
- * sleep 10
- * File.chmod(0644, "testfile")
- * sleep 10
- * File.read("testfile")
- * File.stat("testfile").birthtime #=> 2014-02-24 11:19:17 +0900
- * File.stat("testfile").mtime #=> 2014-02-24 11:19:27 +0900
- * File.stat("testfile").ctime #=> 2014-02-24 11:19:37 +0900
- * File.stat("testfile").atime #=> 2014-02-24 11:19:47 +0900
- *
+ * birthtime -> new_time
+ *
+ * Returns a new Time object containing the create time
+ * of the object represented by +self+
+ * at the time +self+ was created;
+ * see {Snapshot}[rdoc-ref:File::Stat@Snapshot]:
+ *
+ * filename = 't.tmp'
+ * stat = File::Stat.new(filename) # Raises Errno::ENOENT: No such file or directory
+ * File.write(filename, 'foo')
+ * stat = File::Stat.new(filename)
+ * stat.birthtime # => 2026-04-14 10:41:55.5146554 -0500
+ * File.delete(filename)
+ * stat.birthtime # => 2026-04-14 10:41:55.5146554 -0500
+ *
+ * See {File System Timestamps}[rdoc-ref:file/timestamps.md].
*/
static VALUE
rb_stat_birthtime(VALUE self)
{
- return stat_birthtime(get_stat(self));
+ return statx_birthtime(get_stat(self));
}
#else
# define rb_stat_birthtime rb_f_notimplement
@@ -1086,9 +1246,9 @@ rb_stat_inspect(VALUE self)
#endif
};
- struct stat* st;
- TypedData_Get_Struct(self, struct stat, &stat_data_type, st);
- if (!st) {
+ struct rb_stat* rb_st;
+ TypedData_Get_Struct(self, struct rb_stat, &stat_data_type, rb_st);
+ if (!rb_st->initialized) {
return rb_sprintf("#<%s: uninitialized>", rb_obj_classname(self));
}
@@ -1136,14 +1296,14 @@ no_gvl_fstat(void *data)
}
static int
-fstat_without_gvl(int fd, struct stat *st)
+fstat_without_gvl(rb_io_t *fptr, struct stat *st)
{
no_gvl_stat_data data;
- data.file.fd = fd;
+ data.file.fd = fptr->fd;
data.st = st;
- return (int)(VALUE)rb_thread_io_blocking_region(no_gvl_fstat, &data, fd);
+ return (int)rb_io_blocking_region(fptr, no_gvl_fstat, &data);
}
static void *
@@ -1161,13 +1321,14 @@ stat_without_gvl(const char *path, struct stat *st)
data.file.path = path;
data.st = st;
- return (int)(VALUE)rb_thread_call_without_gvl(no_gvl_stat, &data,
- RUBY_UBF_IO, NULL);
+ return IO_WITHOUT_GVL_INT(no_gvl_stat, &data);
}
#if !defined(HAVE_STRUCT_STAT_ST_BIRTHTIMESPEC) && \
defined(HAVE_STRUCT_STATX_STX_BTIME)
+# define STATX(path, st, mask) statx(AT_FDCWD, path, 0, mask, st)
+
# ifndef HAVE_STATX
# ifdef HAVE_SYSCALL_H
# include <syscall.h>
@@ -1182,21 +1343,21 @@ statx(int dirfd, const char *pathname, int flags,
{
return (int)syscall(__NR_statx, dirfd, pathname, flags, mask, statxbuf);
}
-# endif
-# endif
+# endif /* __linux__ */
+# endif /* HAVE_STATX */
-typedef struct no_gvl_statx_data {
+typedef struct no_gvl_rb_io_stat_data {
struct statx *stx;
int fd;
const char *path;
int flags;
unsigned int mask;
-} no_gvl_statx_data;
+} no_gvl_rb_io_stat_data;
static VALUE
io_blocking_statx(void *data)
{
- no_gvl_statx_data *arg = data;
+ no_gvl_rb_io_stat_data *arg = data;
return (VALUE)statx(arg->fd, arg->path, arg->flags, arg->mask, arg->stx);
}
@@ -1207,24 +1368,34 @@ no_gvl_statx(void *data)
}
static int
-statx_without_gvl(const char *path, struct statx *stx, unsigned int mask)
+statx_without_gvl(const char *path, rb_io_stat_data *stx, unsigned int mask)
{
- no_gvl_statx_data data = {stx, AT_FDCWD, path, 0, mask};
+ no_gvl_rb_io_stat_data data = {stx, AT_FDCWD, path, 0, mask};
/* call statx(2) with pathname */
- return (int)(VALUE)rb_thread_call_without_gvl(no_gvl_statx, &data,
- RUBY_UBF_IO, NULL);
+ return IO_WITHOUT_GVL_INT(no_gvl_statx, &data);
}
static int
-fstatx_without_gvl(int fd, struct statx *stx, unsigned int mask)
+lstatx_without_gvl(const char *path, rb_io_stat_data *stx, unsigned int mask)
{
- no_gvl_statx_data data = {stx, fd, "", AT_EMPTY_PATH, mask};
+ no_gvl_rb_io_stat_data data = {stx, AT_FDCWD, path, AT_SYMLINK_NOFOLLOW, mask};
+
+ /* call statx(2) with pathname */
+ return IO_WITHOUT_GVL_INT(no_gvl_statx, &data);
+}
+
+static int
+fstatx_without_gvl(rb_io_t *fptr, rb_io_stat_data *stx, unsigned int mask)
+{
+ no_gvl_rb_io_stat_data data = {stx, fptr->fd, "", AT_EMPTY_PATH, mask};
/* call statx(2) with fd */
- return (int)rb_thread_io_blocking_region(io_blocking_statx, &data, fd);
+ return (int)rb_io_blocking_region(fptr, io_blocking_statx, &data);
}
+#define FSTATX(fd, st) statx(fd, "", AT_EMPTY_PATH, STATX_ALL, st)
+
static int
rb_statx(VALUE file, struct statx *stx, unsigned int mask)
{
@@ -1234,8 +1405,9 @@ rb_statx(VALUE file, struct statx *stx, unsigned int mask)
tmp = rb_check_convert_type_with_id(file, T_FILE, "IO", idTo_io);
if (!NIL_P(tmp)) {
rb_io_t *fptr;
+
GetOpenFile(tmp, fptr);
- result = fstatx_without_gvl(fptr->fd, stx, mask);
+ result = fstatx_without_gvl(fptr, stx, mask);
file = tmp;
}
else {
@@ -1262,7 +1434,7 @@ statx_notimplement(const char *field_name)
}
static VALUE
-statx_birthtime(const struct statx *stx, VALUE fname)
+statx_birthtime(const rb_io_stat_data *stx)
{
if (!statx_has_birthtime(stx)) {
/* birthtime is not supported on the filesystem */
@@ -1271,19 +1443,27 @@ statx_birthtime(const struct statx *stx, VALUE fname)
return rb_time_nano_new((time_t)stx->stx_btime.tv_sec, stx->stx_btime.tv_nsec);
}
-typedef struct statx statx_data;
-# define HAVE_STAT_BIRTHTIME
+#else
-#elif defined(HAVE_STAT_BIRTHTIME)
# define statx_without_gvl(path, st, mask) stat_without_gvl(path, st)
-# define fstatx_without_gvl(fd, st, mask) fstat_without_gvl(fd, st)
-# define statx_birthtime(st, fname) stat_birthtime(st)
+# define fstatx_without_gvl(fptr, st, mask) fstat_without_gvl(fptr, st)
+# define lstatx_without_gvl(path, st, mask) lstat_without_gvl(path, st)
+# define rb_statx(file, stx, mask) rb_stat(file, stx)
+# define STATX(path, st, mask) STAT(path, st)
+
+#if defined(HAVE_STAT_BIRTHTIME)
# define statx_has_birthtime(st) 1
-# define rb_statx(file, st, mask) rb_stat(file, st)
#else
# define statx_has_birthtime(st) 0
#endif
+#endif /* !defined(HAVE_STRUCT_STAT_ST_BIRTHTIMESPEC) && \
+ defined(HAVE_STRUCT_STATX_STX_BTIME) */
+
+#ifndef FSTAT
+# define FSTAT(fd, st) fstat(fd, st)
+#endif
+
static int
rb_stat(VALUE file, struct stat *st)
{
@@ -1295,7 +1475,7 @@ rb_stat(VALUE file, struct stat *st)
rb_io_t *fptr;
GetOpenFile(tmp, fptr);
- result = fstat_without_gvl(fptr->fd, st);
+ result = fstat_without_gvl(fptr, st);
file = tmp;
}
else {
@@ -1320,14 +1500,14 @@ rb_stat(VALUE file, struct stat *st)
static VALUE
rb_file_s_stat(VALUE klass, VALUE fname)
{
- struct stat st;
+ rb_io_stat_data st;
FilePathValue(fname);
fname = rb_str_encode_ospath(fname);
- if (stat_without_gvl(RSTRING_PTR(fname), &st) < 0) {
+ if (statx_without_gvl(RSTRING_PTR(fname), &st, STATX_ALL) < 0) {
rb_sys_fail_path(fname);
}
- return rb_stat_new(&st);
+ return rb_statx_new(&st);
}
/*
@@ -1349,13 +1529,13 @@ static VALUE
rb_io_stat(VALUE obj)
{
rb_io_t *fptr;
- struct stat st;
+ rb_io_stat_data st;
GetOpenFile(obj, fptr);
- if (fstat(fptr->fd, &st) == -1) {
+ if (fstatx_without_gvl(fptr, &st, STATX_ALL) == -1) {
rb_sys_fail_path(fptr->pathv);
}
- return rb_stat_new(&st);
+ return rb_statx_new(&st);
}
#ifdef HAVE_LSTAT
@@ -1374,8 +1554,7 @@ lstat_without_gvl(const char *path, struct stat *st)
data.file.path = path;
data.st = st;
- return (int)(VALUE)rb_thread_call_without_gvl(no_gvl_lstat, &data,
- RUBY_UBF_IO, NULL);
+ return IO_WITHOUT_GVL_INT(no_gvl_lstat, &data);
}
#endif /* HAVE_LSTAT */
@@ -1396,14 +1575,14 @@ static VALUE
rb_file_s_lstat(VALUE klass, VALUE fname)
{
#ifdef HAVE_LSTAT
- struct stat st;
+ rb_io_stat_data st;
FilePathValue(fname);
fname = rb_str_encode_ospath(fname);
- if (lstat_without_gvl(StringValueCStr(fname), &st) == -1) {
+ if (lstatx_without_gvl(StringValueCStr(fname), &st, STATX_ALL) == -1) {
rb_sys_fail_path(fname);
}
- return rb_stat_new(&st);
+ return rb_statx_new(&st);
#else
return rb_file_s_stat(klass, fname);
#endif
@@ -1428,16 +1607,16 @@ rb_file_lstat(VALUE obj)
{
#ifdef HAVE_LSTAT
rb_io_t *fptr;
- struct stat st;
+ rb_io_stat_data st;
VALUE path;
GetOpenFile(obj, fptr);
if (NIL_P(fptr->pathv)) return Qnil;
path = rb_str_encode_ospath(fptr->pathv);
- if (lstat_without_gvl(RSTRING_PTR(path), &st) == -1) {
+ if (lstatx_without_gvl(RSTRING_PTR(path), &st, STATX_ALL) == -1) {
rb_sys_fail_path(fptr->pathv);
}
- return rb_stat_new(&st);
+ return rb_statx_new(&st);
#else
return rb_io_stat(obj);
#endif
@@ -1471,7 +1650,7 @@ rb_group_member(GETGROUPS_T gid)
ALLOCV_END(v);
return rv;
-#endif
+#endif /* defined(_WIN32) || !defined(HAVE_GETGROUPS) */
}
#ifndef S_IXUGO
@@ -1522,9 +1701,9 @@ eaccess(const char *path, int mode)
return -1;
#else
return access(path, mode);
-#endif
+#endif /* USE_GETEUID */
}
-#endif
+#endif /* HAVE_EACCESS */
struct access_arg {
const char *path;
@@ -1549,8 +1728,7 @@ rb_eaccess(VALUE fname, int mode)
aa.path = StringValueCStr(fname);
aa.mode = mode;
- return (int)(VALUE)rb_thread_call_without_gvl(nogvl_eaccess, &aa,
- RUBY_UBF_IO, 0);
+ return IO_WITHOUT_GVL_INT(nogvl_eaccess, &aa);
}
static void *
@@ -1571,8 +1749,7 @@ rb_access(VALUE fname, int mode)
aa.path = StringValueCStr(fname);
aa.mode = mode;
- return (int)(VALUE)rb_thread_call_without_gvl(nogvl_access, &aa,
- RUBY_UBF_IO, 0);
+ return IO_WITHOUT_GVL_INT(nogvl_access, &aa);
}
/*
@@ -1586,8 +1763,6 @@ rb_access(VALUE fname, int mode)
*/
/*
- * Document-method: directory?
- *
* call-seq:
* File.directory?(path) -> true or false
*
@@ -1723,8 +1898,8 @@ rb_file_socket_p(VALUE obj, VALUE fname)
if (rb_stat(fname, &st) < 0) return Qfalse;
if (S_ISSOCK(st.st_mode)) return Qtrue;
-
#endif
+
return Qfalse;
}
@@ -1766,7 +1941,7 @@ rb_file_blockdev_p(VALUE obj, VALUE fname)
*
* Returns +true+ if +filepath+ points to a character device, +false+ otherwise.
*
- * File.chardev?($stdin) # => true
+ * File.chardev?($stdin) # => true
* File.chardev?('t.txt') # => false
*
*/
@@ -2052,7 +2227,7 @@ rb_file_size_p(VALUE obj, VALUE fname)
* File.owned?(file_name) -> true or false
*
* Returns <code>true</code> if the named file exists and the
- * effective used id of the calling process is the owner of
+ * effective user id of the calling process is the owner of
* the file.
*
* _file_name_ can be an IO object.
@@ -2226,36 +2401,36 @@ rb_file_s_size(VALUE klass, VALUE fname)
}
static VALUE
-rb_file_ftype(const struct stat *st)
+rb_file_ftype(mode_t mode)
{
const char *t;
- if (S_ISREG(st->st_mode)) {
+ if (S_ISREG(mode)) {
t = "file";
}
- else if (S_ISDIR(st->st_mode)) {
+ else if (S_ISDIR(mode)) {
t = "directory";
}
- else if (S_ISCHR(st->st_mode)) {
+ else if (S_ISCHR(mode)) {
t = "characterSpecial";
}
#ifdef S_ISBLK
- else if (S_ISBLK(st->st_mode)) {
+ else if (S_ISBLK(mode)) {
t = "blockSpecial";
}
#endif
#ifdef S_ISFIFO
- else if (S_ISFIFO(st->st_mode)) {
+ else if (S_ISFIFO(mode)) {
t = "fifo";
}
#endif
#ifdef S_ISLNK
- else if (S_ISLNK(st->st_mode)) {
+ else if (S_ISLNK(mode)) {
t = "link";
}
#endif
#ifdef S_ISSOCK
- else if (S_ISSOCK(st->st_mode)) {
+ else if (S_ISSOCK(mode)) {
t = "socket";
}
#endif
@@ -2263,7 +2438,7 @@ rb_file_ftype(const struct stat *st)
t = "unknown";
}
- return rb_usascii_str_new2(t);
+ return rb_fstring_cstr(t);
}
/*
@@ -2292,19 +2467,30 @@ rb_file_s_ftype(VALUE klass, VALUE fname)
rb_sys_fail_path(fname);
}
- return rb_file_ftype(&st);
+ return rb_file_ftype(st.st_mode);
}
/*
* call-seq:
- * File.atime(file_name) -> time
+ * File.atime(object) -> new_time
*
- * Returns the last access time for the named file as a Time object.
+ * Returns a new Time object containing the time of the most recent
+ * access (read or write) to the object,
+ * which may be a string filepath or dirpath, or a File or Dir object:
*
- * _file_name_ can be an IO object.
+ * filepath = 't.tmp'
+ * File.exist?(filepath) # => false
+ * File.atime(filepath) # Raises Errno::ENOENT.
+ * File.write(filepath, 'foo')
+ * File.atime(filepath) # => 2026-03-31 16:39:37.9290772 -0500
+ * File.write(filepath, 'bar')
+ * File.atime(filepath) # => 2026-03-31 16:39:57.7710876 -0500
*
- * File.atime("testfile") #=> Wed Apr 09 08:51:48 CDT 2003
+ * File.atime('.') # => 2026-03-31 16:47:49.0970483 -0500
+ * File.atime(File.new('README.md')) # => 2026-03-31 11:15:27.8215934 -0500
+ * File.atime(Dir.new('.')) # => 2026-03-31 12:39:45.5910591 -0500
*
+ * See {File System Timestamps}[rdoc-ref:file/timestamps.md].
*/
static VALUE
@@ -2317,18 +2503,27 @@ rb_file_s_atime(VALUE klass, VALUE fname)
FilePathValue(fname);
rb_syserr_fail_path(e, fname);
}
- return stat_atime(&st);
+ return stat_time(stat_atimespec(&st));
}
/*
* call-seq:
- * file.atime -> time
- *
- * Returns the last access time (a Time object) for <i>file</i>, or
- * epoch if <i>file</i> has not been accessed.
- *
- * File.new("testfile").atime #=> Wed Dec 31 18:00:00 CST 1969
- *
+ * atime -> new_time
+ *
+ * Returns a new Time object containing the time of the most recent
+ * access (read or write) to the file represented by +self+:
+ *
+ * filepath = 't.tmp'
+ * file = File.new(filepath, 'a+')
+ * file.atime # => 2026-03-31 17:11:27.7285397 -0500
+ * file.write('foo')
+ * file.atime # => 2026-03-31 17:11:27.7285397 -0500 # Unchanged; not yet written.
+ * file.flush
+ * file.atime # => 2026-03-31 17:12:11.3408054 -0500 # Changed; now written.
+ * file.close
+ * File.delete(filename)
+ *
+ * See {File System Timestamps}[rdoc-ref:file/timestamps.md].
*/
static VALUE
@@ -2341,7 +2536,7 @@ rb_file_atime(VALUE obj)
if (fstat(fptr->fd, &st) == -1) {
rb_sys_fail_path(fptr->pathv);
}
- return stat_atime(&st);
+ return stat_time(stat_atimespec(&st));
}
/*
@@ -2366,7 +2561,7 @@ rb_file_s_mtime(VALUE klass, VALUE fname)
FilePathValue(fname);
rb_syserr_fail_path(e, fname);
}
- return stat_mtime(&st);
+ return stat_time(stat_mtimespec(&st));
}
/*
@@ -2389,7 +2584,7 @@ rb_file_mtime(VALUE obj)
if (fstat(fptr->fd, &st) == -1) {
rb_sys_fail_path(fptr->pathv);
}
- return stat_mtime(&st);
+ return stat_time(stat_mtimespec(&st));
}
/*
@@ -2418,7 +2613,7 @@ rb_file_s_ctime(VALUE klass, VALUE fname)
FilePathValue(fname);
rb_syserr_fail_path(e, fname);
}
- return stat_ctime(&st);
+ return stat_time(stat_ctimespec(&st));
}
/*
@@ -2444,35 +2639,40 @@ rb_file_ctime(VALUE obj)
if (fstat(fptr->fd, &st) == -1) {
rb_sys_fail_path(fptr->pathv);
}
- return stat_ctime(&st);
+ return stat_time(stat_ctimespec(&st));
}
+#if defined(HAVE_STAT_BIRTHTIME)
/*
* call-seq:
- * File.birthtime(file_name) -> time
- *
- * Returns the birth time for the named file.
- *
- * _file_name_ can be an IO object.
+ * File.birthtime(entry_path) -> new_time
*
- * File.birthtime("testfile") #=> Wed Apr 09 08:53:13 CDT 2003
+ * Returns a new Time object containing the create time
+ * of the entry at the given +path+:
*
- * If the platform doesn't have birthtime, raises NotImplementedError.
+ * path = 't.tmp'
+ * File.birthtime(path) # Raises Errno::ENOENT: No such file or directory
+ * File.write(path, 'foo')
+ * File.birthtime(path) # => 2026-04-14 11:10:43.2891695 -0500
+ * File.write(path, 'bar')
+ * File.birthtime(path) # => 2026-04-14 11:10:43.2891695 -0500
+ * File.delete(path)
+ * File.birthtime(path) # Raises Errno::ENOENT: No such file or directory
*
+ * See {File System Timestamps}[rdoc-ref:file/timestamps.md].
*/
-#if defined(HAVE_STAT_BIRTHTIME)
-RUBY_FUNC_EXPORTED VALUE
+VALUE
rb_file_s_birthtime(VALUE klass, VALUE fname)
{
- statx_data st;
+ rb_io_stat_data st;
if (rb_statx(fname, &st, STATX_BTIME) < 0) {
int e = errno;
FilePathValue(fname);
rb_syserr_fail_path(e, fname);
}
- return statx_birthtime(&st, fname);
+ return statx_birthtime(&st);
}
#else
# define rb_file_s_birthtime rb_f_notimplement
@@ -2481,43 +2681,40 @@ rb_file_s_birthtime(VALUE klass, VALUE fname)
#if defined(HAVE_STAT_BIRTHTIME)
/*
* call-seq:
- * file.birthtime -> time
- *
- * Returns the birth time for <i>file</i>.
+ * birthtime -> new_time
*
- * File.new("testfile").birthtime #=> Wed Apr 09 08:53:14 CDT 2003
+ * Returns a new Time object containing the create time for +self+:
*
- * If the platform doesn't have birthtime, raises NotImplementedError.
+ * filepath = 't.tmp'
+ * File.write(filepath, 'foo')
+ * file = File.new(filepath)
+ * file.birthtime # => 2026-04-14 15:53:45.002656 -0500
+ * File.write(filepath, 'bar')
+ * file.birthtime # => 2026-04-14 15:53:45.002656 -0500
+ * file.close
+ * File.delete(filepath)
+ * file.birthtime # Raises IOError: closed stream
*
+ * See {File System Timestamps}[rdoc-ref:file/timestamps.md].
*/
static VALUE
rb_file_birthtime(VALUE obj)
{
rb_io_t *fptr;
- statx_data st;
+ rb_io_stat_data st;
GetOpenFile(obj, fptr);
- if (fstatx_without_gvl(fptr->fd, &st, STATX_BTIME) == -1) {
+ if (fstatx_without_gvl(fptr, &st, STATX_BTIME) == -1) {
rb_sys_fail_path(fptr->pathv);
}
- return statx_birthtime(&st, fptr->pathv);
+ return statx_birthtime(&st);
}
#else
# define rb_file_birthtime rb_f_notimplement
#endif
-/*
- * call-seq:
- * file.size -> integer
- *
- * Returns the size of <i>file</i> in bytes.
- *
- * File.new("testfile").size #=> 66
- *
- */
-
-off_t
+rb_off_t
rb_file_size(VALUE file)
{
if (RB_TYPE_P(file, T_FILE)) {
@@ -2540,12 +2737,45 @@ rb_file_size(VALUE file)
}
}
+/*
+ * call-seq:
+ * file.size -> integer
+ *
+ * Returns the size of <i>file</i> in bytes.
+ *
+ * File.new("testfile").size #=> 66
+ *
+ */
+
static VALUE
file_size(VALUE self)
{
return OFFT2NUM(rb_file_size(self));
}
+struct nogvl_chmod_data {
+ const char *path;
+ mode_t mode;
+};
+
+static void *
+nogvl_chmod(void *ptr)
+{
+ struct nogvl_chmod_data *data = ptr;
+ int ret = chmod(data->path, data->mode);
+ return (void *)(VALUE)ret;
+}
+
+static int
+rb_chmod(const char *path, mode_t mode)
+{
+ struct nogvl_chmod_data data = {
+ .path = path,
+ .mode = mode,
+ };
+ return IO_WITHOUT_GVL_INT(nogvl_chmod, &data);
+}
+
static int
chmod_internal(const char *path, void *mode)
{
@@ -2576,6 +2806,29 @@ rb_file_s_chmod(int argc, VALUE *argv, VALUE _)
return apply2files(chmod_internal, argc, argv, &mode);
}
+#ifdef HAVE_FCHMOD
+struct nogvl_fchmod_data {
+ int fd;
+ mode_t mode;
+};
+
+static VALUE
+io_blocking_fchmod(void *ptr)
+{
+ struct nogvl_fchmod_data *data = ptr;
+ int ret = fchmod(data->fd, data->mode);
+ return (VALUE)ret;
+}
+
+static int
+rb_fchmod(struct rb_io* io, mode_t mode)
+{
+ (void)rb_chmod; /* suppress unused-function warning when HAVE_FCHMOD */
+ struct nogvl_fchmod_data data = {.fd = io->fd, .mode = mode};
+ return (int)rb_thread_io_blocking_region(io, io_blocking_fchmod, &data);
+}
+#endif
+
/*
* call-seq:
* file.chmod(mode_int) -> 0
@@ -2602,7 +2855,7 @@ rb_file_chmod(VALUE obj, VALUE vmode)
GetOpenFile(obj, fptr);
#ifdef HAVE_FCHMOD
- if (fchmod(fptr->fd, mode) == -1) {
+ if (rb_fchmod(fptr, mode) == -1) {
if (HAVE_FCHMOD || errno != ENOSYS)
rb_sys_fail_path(fptr->pathv);
}
@@ -2613,7 +2866,7 @@ rb_file_chmod(VALUE obj, VALUE vmode)
#if !defined HAVE_FCHMOD || !HAVE_FCHMOD
if (NIL_P(fptr->pathv)) return Qnil;
path = rb_str_encode_ospath(fptr->pathv);
- if (chmod(RSTRING_PTR(path), mode) == -1)
+ if (rb_chmod(RSTRING_PTR(path), mode) == -1)
rb_sys_fail_path(fptr->pathv);
#endif
@@ -2708,6 +2961,51 @@ rb_file_s_chown(int argc, VALUE *argv, VALUE _)
return apply2files(chown_internal, argc, argv, &arg);
}
+struct nogvl_chown_data {
+ union {
+ const char *path;
+ int fd;
+ } as;
+ struct chown_args new;
+};
+
+static void *
+nogvl_chown(void *ptr)
+{
+ struct nogvl_chown_data *data = ptr;
+ return (void *)(VALUE)chown(data->as.path, data->new.owner, data->new.group);
+}
+
+static int
+rb_chown(const char *path, rb_uid_t owner, rb_gid_t group)
+{
+ struct nogvl_chown_data data = {
+ .as = {.path = path},
+ .new = {.owner = owner, .group = group},
+ };
+ return IO_WITHOUT_GVL_INT(nogvl_chown, &data);
+}
+
+#ifdef HAVE_FCHOWN
+static void *
+nogvl_fchown(void *ptr)
+{
+ struct nogvl_chown_data *data = ptr;
+ return (void *)(VALUE)fchown(data->as.fd, data->new.owner, data->new.group);
+}
+
+static int
+rb_fchown(int fd, rb_uid_t owner, rb_gid_t group)
+{
+ (void)rb_chown; /* suppress unused-function warning when HAVE_FCHMOD */
+ struct nogvl_chown_data data = {
+ .as = {.fd = fd},
+ .new = {.owner = owner, .group = group},
+ };
+ return IO_WITHOUT_GVL_INT(nogvl_fchown, &data);
+}
+#endif
+
/*
* call-seq:
* file.chown(owner_int, group_int ) -> 0
@@ -2739,10 +3037,10 @@ rb_file_chown(VALUE obj, VALUE owner, VALUE group)
#ifndef HAVE_FCHOWN
if (NIL_P(fptr->pathv)) return Qnil;
path = rb_str_encode_ospath(fptr->pathv);
- if (chown(RSTRING_PTR(path), o, g) == -1)
+ if (rb_chown(RSTRING_PTR(path), o, g) == -1)
rb_sys_fail_path(fptr->pathv);
#else
- if (fchown(fptr->fd, o, g) == -1)
+ if (rb_fchown(fptr->fd, o, g) == -1)
rb_sys_fail_path(fptr->pathv);
#endif
@@ -2829,14 +3127,16 @@ utime_failed(struct apply_arg *aa)
}
rb_syserr_fail_path(e, path);
}
-#endif
+#endif /* UTIME_EINVAL */
#if defined(HAVE_UTIMES)
-# if defined(__APPLE__) && \
+# if !defined(HAVE_UTIMENSAT)
+/* utimensat() is not found, runtime check is not needed */
+# elif defined(__APPLE__) && \
(!defined(MAC_OS_X_VERSION_13_0) || (MAC_OS_X_VERSION_MIN_REQUIRED < MAC_OS_X_VERSION_13_0))
-# if defined(__has_attribute) && __has_attribute(availability)
+# if __has_attribute(availability) && __has_warning("-Wunguarded-availability-new")
typedef int utimensat_func(int, const char *, const struct timespec [2], int);
RBIMPL_WARNING_PUSH()
@@ -2851,12 +3151,8 @@ RBIMPL_WARNING_POP()
# define utimensat rb_utimensat()
# else /* __API_AVAILABLE macro does nothing on gcc */
__attribute__((weak)) int utimensat(int, const char *, const struct timespec [2], int);
-# endif
-
-# define utimensat_available_p() (utimensat != NULL)
-# else
-# define utimensat_available_p() 1
-# endif
+# endif /* utimesat availability */
+# endif /* __APPLE__ && < MAC_OS_X_VERSION_13_0 */
static int
utime_internal(const char *path, void *arg)
@@ -2869,15 +3165,15 @@ utime_internal(const char *path, void *arg)
# if defined(__APPLE__)
const int try_utimensat = utimensat != NULL;
const int try_utimensat_follow = utimensat != NULL;
-# else
+# else /* !__APPLE__ */
# define TRY_UTIMENSAT 1
static int try_utimensat = 1;
-# ifdef AT_SYMLINK_NOFOLLOW
+# ifdef AT_SYMLINK_NOFOLLOW
static int try_utimensat_follow = 1;
-# else
+# else
const int try_utimensat_follow = 0;
-# endif
-# endif
+# endif
+# endif /* __APPLE__ */
int flags = 0;
if (v->follow ? try_utimensat_follow : try_utimensat) {
@@ -2892,15 +3188,15 @@ utime_internal(const char *path, void *arg)
if (result < 0 && errno == ENOSYS) {
# ifdef AT_SYMLINK_NOFOLLOW
try_utimensat_follow = 0;
-# endif
+# endif /* AT_SYMLINK_NOFOLLOW */
if (!v->follow)
try_utimensat = 0;
}
else
-# endif
+# endif /* TRY_UTIMESAT */
return result;
}
-#endif
+#endif /* defined(HAVE_UTIMENSAT) */
if (tsp) {
tvbuf[0].tv_sec = tsp[0].tv_sec;
@@ -2915,7 +3211,7 @@ utime_internal(const char *path, void *arg)
return utimes(path, tvp);
}
-#else
+#else /* !defined(HAVE_UTIMES) */
#if !defined HAVE_UTIME_H && !defined HAVE_SYS_UTIME_H
struct utimbuf {
@@ -2928,7 +3224,7 @@ static int
utime_internal(const char *path, void *arg)
{
struct utime_args *v = arg;
- const struct timespec *tsp = v->tsp;
+ const stat_timestamp *tsp = v->tsp;
struct utimbuf utbuf, *utp = NULL;
if (tsp) {
utbuf.actime = tsp[0].tv_sec;
@@ -2937,8 +3233,7 @@ utime_internal(const char *path, void *arg)
}
return utime(path, utp);
}
-
-#endif
+#endif /* !defined(HAVE_UTIMES) */
static VALUE
utime_internal_i(int argc, VALUE *argv, int follow)
@@ -3114,7 +3409,6 @@ rb_file_s_readlink(VALUE klass, VALUE path)
return rb_readlink(path, rb_filesystem_encoding());
}
-#ifndef _WIN32
struct readlink_arg {
const char *path;
char *buf;
@@ -3138,8 +3432,7 @@ readlink_without_gvl(VALUE path, VALUE buf, size_t size)
ra.buf = RSTRING_PTR(buf);
ra.size = size;
- return (ssize_t)rb_thread_call_without_gvl(nogvl_readlink, &ra,
- RUBY_UBF_IO, 0);
+ return (ssize_t)IO_WITHOUT_GVL(nogvl_readlink, &ra);
}
VALUE
@@ -3170,7 +3463,6 @@ rb_readlink(VALUE path, rb_encoding *enc)
return v;
}
-#endif
#else
#define rb_file_s_readlink rb_f_notimplement
#endif
@@ -3191,7 +3483,7 @@ unlink_internal(const char *path, void *arg)
* Since the underlying implementation relies on the
* <code>unlink(2)</code> system call, the type of
* exception raised depends on its error type (see
- * https://linux.die.net/man/2/unlink) and has the form of
+ * https://man7.org/linux/man-pages/man2/unlink.2.html) and has the form of
* e.g. Errno::ENOENT.
*
* See also Dir::rmdir.
@@ -3241,8 +3533,7 @@ rb_file_s_rename(VALUE klass, VALUE from, VALUE to)
#if defined __CYGWIN__
errno = 0;
#endif
- if ((int)(VALUE)rb_thread_call_without_gvl(no_gvl_rename, &ra,
- RUBY_UBF_IO, 0) < 0) {
+ if (IO_WITHOUT_GVL_INT(no_gvl_rename, &ra) < 0) {
int e = errno;
#if defined DOSISH
switch (e) {
@@ -3311,12 +3602,13 @@ static const char file_alt_separator[] = {FILE_ALT_SEPARATOR, '\0'};
#endif
#ifndef USE_NTFS
-#if defined _WIN32
-#define USE_NTFS 1
-#else
-#define USE_NTFS 0
-#endif
+# if defined _WIN32
+# define USE_NTFS 1
+# else
+# define USE_NTFS 0
+# endif
#endif
+
#ifndef USE_NTFS_ADS
# if USE_NTFS
# define USE_NTFS_ADS 1
@@ -3330,14 +3622,17 @@ static const char file_alt_separator[] = {FILE_ALT_SEPARATOR, '\0'};
#else
#define istrailinggarbage(x) 0
#endif
+
#if USE_NTFS_ADS
# define isADS(x) ((x) == ':')
#else
# define isADS(x) 0
#endif
-#define Next(p, e, enc) ((p) + rb_enc_mbclen((p), (e), (enc)))
-#define Inc(p, e, enc) ((p) = Next((p), (e), (enc)))
+#define enc_mbclen_needed(enc) (!rb_str_encindex_fastpath(rb_enc_to_index(enc)))
+
+#define Next(p, e, mb_enc, enc) ((p) + ((mb_enc) ? rb_enc_mbclen((p), (e), (enc)) : 1))
+#define Inc(p, e, mb_enc, enc) ((p) = Next((p), (e), (mb_enc), (enc)))
#if defined(DOSISH_UNC)
#define has_unc(buf) (isdirsep((buf)[0]) && isdirsep((buf)[1]))
@@ -3397,11 +3692,11 @@ not_same_drive(VALUE path, int drive)
return has_unc(p);
}
}
-#endif
-#endif
+#endif /* _WIN32 */
+#endif /* DOSISH_DRIVE_LETTER */
static inline char *
-skiproot(const char *path, const char *end, rb_encoding *enc)
+skiproot(const char *path, const char *end)
{
#ifdef DOSISH_DRIVE_LETTER
if (path + 2 <= end && has_drive_letter(path)) path += 2;
@@ -3410,57 +3705,76 @@ skiproot(const char *path, const char *end, rb_encoding *enc)
return (char *)path;
}
-#define nextdirsep rb_enc_path_next
-char *
-rb_enc_path_next(const char *s, const char *e, rb_encoding *enc)
+static inline char *
+enc_path_next(const char *s, const char *e, bool mb_enc, rb_encoding *enc)
{
while (s < e && !isdirsep(*s)) {
- Inc(s, e, enc);
+ Inc(s, e, mb_enc, enc);
}
return (char *)s;
}
+#define nextdirsep rb_enc_path_next
+char *
+rb_enc_path_next(const char *s, const char *e, rb_encoding *enc)
+{
+ return enc_path_next(s, e, enc_mbclen_needed(enc), enc);
+}
+
#if defined(DOSISH_UNC) || defined(DOSISH_DRIVE_LETTER)
-#define skipprefix rb_enc_path_skip_prefix
+#define skipprefix enc_path_skip_prefix
#else
-#define skipprefix(path, end, enc) (path)
+#define skipprefix(path, end, mb_enc, enc) (path)
#endif
-char *
-rb_enc_path_skip_prefix(const char *path, const char *end, rb_encoding *enc)
+static inline char *
+enc_path_skip_prefix(const char *path, const char *end, bool mb_enc, rb_encoding *enc)
{
#if defined(DOSISH_UNC) || defined(DOSISH_DRIVE_LETTER)
#ifdef DOSISH_UNC
if (path + 2 <= end && isdirsep(path[0]) && isdirsep(path[1])) {
path += 2;
while (path < end && isdirsep(*path)) path++;
- if ((path = rb_enc_path_next(path, end, enc)) < end && path[0] && path[1] && !isdirsep(path[1]))
- path = rb_enc_path_next(path + 1, end, enc);
+ if ((path = enc_path_next(path, end, mb_enc, enc)) < end &&
+ path + 2 <= end && !isdirsep(path[1])) {
+ path = enc_path_next(path + 1, end, mb_enc, enc);
+ }
return (char *)path;
}
#endif
#ifdef DOSISH_DRIVE_LETTER
- if (has_drive_letter(path))
+ if (path + 2 <= end && has_drive_letter(path))
return (char *)(path + 2);
#endif
-#endif
+#endif /* defined(DOSISH_UNC) || defined(DOSISH_DRIVE_LETTER) */
return (char *)path;
}
+char *
+rb_enc_path_skip_prefix(const char *path, const char *end, rb_encoding *enc)
+{
+ return enc_path_skip_prefix(path, end, enc_mbclen_needed(enc), enc);
+}
+
static inline char *
skipprefixroot(const char *path, const char *end, rb_encoding *enc)
{
#if defined(DOSISH_UNC) || defined(DOSISH_DRIVE_LETTER)
- char *p = skipprefix(path, end, enc);
- while (isdirsep(*p)) p++;
+ char *p = skipprefix(path, end, enc_mbclen_needed(enc), enc);
+ while (p < end && isdirsep(*p)) p++;
return p;
#else
- return skiproot(path, end, enc);
+ return skiproot(path, end);
#endif
}
-#define strrdirsep rb_enc_path_last_separator
char *
-rb_enc_path_last_separator(const char *path, const char *end, rb_encoding *enc)
+rb_enc_path_skip_prefix_root(const char *path, const char *end, rb_encoding *enc)
+{
+ return skipprefixroot(path, end, enc);
+}
+
+static char *
+enc_path_last_separator(const char *path, const char *end, bool mb_enc, rb_encoding *enc)
{
char *last = NULL;
while (path < end) {
@@ -3471,14 +3785,44 @@ rb_enc_path_last_separator(const char *path, const char *end, rb_encoding *enc)
last = (char *)tmp;
}
else {
- Inc(path, end, enc);
+ Inc(path, end, mb_enc, enc);
}
}
return last;
}
+char *
+rb_enc_path_last_separator(const char *path, const char *end, rb_encoding *enc)
+{
+ return enc_path_last_separator(path, end, enc_mbclen_needed(enc), enc);
+}
+
+static inline char *
+strrdirsep(const char *path, const char *end, bool mb_enc, rb_encoding *enc)
+{
+ if (RB_UNLIKELY(mb_enc)) {
+ return enc_path_last_separator(path, end, mb_enc, enc);
+ }
+
+ const char *cursor = end - 1;
+
+ while (isdirsep(cursor[0])) {
+ cursor--;
+ }
+
+ while (cursor >= path) {
+ if (isdirsep(cursor[0])) {
+ while (cursor > path && isdirsep(cursor[-1])) {
+ cursor--;
+ }
+ return (char *)cursor;
+ }
+ cursor--;
+ }
+ return NULL;
+}
static char *
-chompdirsep(const char *path, const char *end, rb_encoding *enc)
+chompdirsep(const char *path, const char *end, bool mb_enc, rb_encoding *enc)
{
while (path < end) {
if (isdirsep(*path)) {
@@ -3487,7 +3831,7 @@ chompdirsep(const char *path, const char *end, rb_encoding *enc)
if (path >= end) return (char *)last;
}
else {
- Inc(path, end, enc);
+ Inc(path, end, mb_enc, enc);
}
}
return (char *)path;
@@ -3497,13 +3841,13 @@ char *
rb_enc_path_end(const char *path, const char *end, rb_encoding *enc)
{
if (path < end && isdirsep(*path)) path++;
- return chompdirsep(path, end, enc);
+ return chompdirsep(path, end, enc_mbclen_needed(enc), enc);
}
static rb_encoding *
fs_enc_check(VALUE path1, VALUE path2)
{
- rb_encoding *enc = rb_enc_check(path1, path2);
+ rb_encoding *enc = rb_enc_check_str(path1, path2);
int encidx = rb_enc_to_index(enc);
if (encidx == ENCINDEX_US_ASCII) {
encidx = rb_enc_get_index(path1);
@@ -3518,6 +3862,7 @@ fs_enc_check(VALUE path1, VALUE path2)
static char *
ntfs_tail(const char *path, const char *end, rb_encoding *enc)
{
+ bool mb_enc = enc_mbclen_needed(enc);
while (path < end && *path == '.') path++;
while (path < end && !isADS(*path)) {
if (istrailinggarbage(*path)) {
@@ -3532,12 +3877,12 @@ ntfs_tail(const char *path, const char *end, rb_encoding *enc)
if (isADS(*path)) path++;
}
else {
- Inc(path, end, enc);
+ Inc(path, end, mb_enc, enc);
}
}
return (char *)path;
}
-#endif
+#endif /* USE_NTFS */
#define BUFCHECK(cond) do {\
bdiff = p - buf;\
@@ -3580,10 +3925,6 @@ static VALUE
copy_home_path(VALUE result, const char *dir)
{
char *buf;
-#if defined DOSISH || defined __CYGWIN__
- char *p, *bend;
- rb_encoding *enc;
-#endif
long dirlen;
int encidx;
@@ -3592,10 +3933,11 @@ copy_home_path(VALUE result, const char *dir)
memcpy(buf = RSTRING_PTR(result), dir, dirlen);
encidx = rb_filesystem_encindex();
rb_enc_associate_index(result, encidx);
-#if defined DOSISH || defined __CYGWIN__
- enc = rb_enc_from_index(encidx);
- for (bend = (p = buf) + dirlen; p < bend; Inc(p, bend, enc)) {
- if (*p == '\\') {
+#if defined FILE_ALT_SEPARATOR
+ rb_encoding *enc = rb_enc_from_index(encidx);
+ bool mb_enc = enc_mbclen_needed(enc);
+ for (char *p = buf, *bend = p + dirlen; p < bend; Inc(p, bend, mb_enc, enc)) {
+ if (*p == FILE_ALT_SEPARATOR) {
*p = '/';
}
}
@@ -3607,12 +3949,16 @@ VALUE
rb_home_dir_of(VALUE user, VALUE result)
{
#ifdef HAVE_PWD_H
- struct passwd *pwPtr;
+ VALUE dirname = rb_getpwdirnam_for_login(user);
+ if (dirname == Qnil) {
+ rb_raise(rb_eArgError, "user %"PRIsVALUE" doesn't exist", user);
+ }
+ const char *dir = RSTRING_PTR(dirname);
#else
extern char *getlogin(void);
const char *pwPtr = 0;
+ const char *login;
# define endpwent() ((void)0)
-#endif
const char *dir, *username = RSTRING_PTR(user);
rb_encoding *enc = rb_enc_get(user);
#if defined _WIN32
@@ -3624,25 +3970,17 @@ rb_home_dir_of(VALUE user, VALUE result)
dir = username = RSTRING_PTR(rb_str_conv_enc(user, enc, fsenc));
}
-#ifdef HAVE_PWD_H
- pwPtr = getpwnam(username);
-#else
- if (strcasecmp(username, getlogin()) == 0)
+ if ((login = getlogin()) && strcasecmp(username, login) == 0)
dir = pwPtr = getenv("HOME");
-#endif
if (!pwPtr) {
- endpwent();
rb_raise(rb_eArgError, "user %"PRIsVALUE" doesn't exist", user);
}
-#ifdef HAVE_PWD_H
- dir = pwPtr->pw_dir;
#endif
copy_home_path(result, dir);
- endpwent();
return result;
}
-#ifndef _WIN32
+#ifndef _WIN32 /* this encompasses rb_file_expand_path_internal */
VALUE
rb_default_home_dir(VALUE result)
{
@@ -3670,15 +4008,15 @@ rb_default_home_dir(VALUE result)
* lookup by getuid() has a chance of succeeding.
*/
if (NIL_P(login_name)) {
- rb_raise(rb_eArgError, "couldn't find login name -- expanding `~'");
+ rb_raise(rb_eArgError, "couldn't find login name -- expanding '~'");
}
-# endif
+# endif /* !defined(HAVE_GETPWUID_R) && !defined(HAVE_GETPWUID) */
VALUE pw_dir = rb_getpwdirnam_for_login(login_name);
if (NIL_P(pw_dir)) {
pw_dir = rb_getpwdiruid();
if (NIL_P(pw_dir)) {
- rb_raise(rb_eArgError, "couldn't find home for uid `%ld'", (long)getuid());
+ rb_raise(rb_eArgError, "couldn't find home for uid '%ld'", (long)getuid());
}
}
@@ -3687,9 +4025,9 @@ rb_default_home_dir(VALUE result)
rb_str_resize(pw_dir, 0);
return result;
}
-#endif
+#endif /* defined HAVE_PWD_H */
if (!dir) {
- rb_raise(rb_eArgError, "couldn't find HOME environment -- expanding `~'");
+ rb_raise(rb_eArgError, "couldn't find HOME environment -- expanding '~'");
}
return copy_home_path(result, dir);
}
@@ -3714,7 +4052,15 @@ append_fspath(VALUE result, VALUE fname, char *dir, rb_encoding **enc, rb_encodi
size_t dirlen = strlen(dir), buflen = rb_str_capacity(result);
if (NORMALIZE_UTF8PATH || *enc != fsenc) {
- rb_encoding *direnc = fs_enc_check(fname, dirname = ospath_new(dir, dirlen, fsenc));
+ dirname = ospath_new(dir, dirlen, fsenc);
+ if (!rb_enc_compatible(fname, dirname)) {
+ xfree(dir);
+ /* rb_enc_check must raise because the two encodings are not
+ * compatible. */
+ rb_enc_check(fname, dirname);
+ rb_bug("unreachable");
+ }
+ rb_encoding *direnc = fs_enc_check(fname, dirname);
if (direnc != fsenc) {
dirname = rb_str_conv_enc(dirname, fsenc, direnc);
RSTRING_GETMEM(dirname, cwdp, dirlen);
@@ -3744,16 +4090,21 @@ rb_file_expand_path_internal(VALUE fname, VALUE dname, int abs_mode, int long_na
s = StringValuePtr(fname);
fend = s + RSTRING_LEN(fname);
- enc = rb_enc_get(fname);
+ enc = rb_str_enc_get(fname);
+ bool mb_enc = enc_mbclen_needed(enc);
+ if (!mb_enc && RTEST(dname)) {
+ mb_enc = enc_mbclen_needed(rb_str_enc_get(dname));
+ }
+
BUFINIT();
- if (s[0] == '~' && abs_mode == 0) { /* execute only if NOT absolute_path() */
+ if (s < fend && s[0] == '~' && abs_mode == 0) { /* execute only if NOT absolute_path() */
long userlen = 0;
- if (isdirsep(s[1]) || s[1] == '\0') {
+ if (s + 1 == fend || isdirsep(s[1])) {
buf = 0;
b = 0;
rb_str_set_len(result, 0);
- if (*++s) ++s;
+ if (++s < fend) ++s;
rb_default_home_dir(result);
}
else {
@@ -3783,8 +4134,8 @@ rb_file_expand_path_internal(VALUE fname, VALUE dname, int abs_mode, int long_na
}
#ifdef DOSISH_DRIVE_LETTER
/* skip drive letter */
- else if (has_drive_letter(s)) {
- if (isdirsep(s[2])) {
+ else if (s + 1 < fend && has_drive_letter(s)) {
+ if (s + 2 < fend && isdirsep(s[2])) {
/* specified drive letter, and full path */
/* skip drive letter */
BUFCHECK(bdiff + 2 >= buflen);
@@ -3813,12 +4164,12 @@ rb_file_expand_path_internal(VALUE fname, VALUE dname, int abs_mode, int long_na
rb_enc_associate(result, enc = fs_enc_check(result, fname));
p = pend;
}
- p = chompdirsep(skiproot(buf, p, enc), p, enc);
+ p = chompdirsep(skiproot(buf, p), p, mb_enc, enc);
s += 2;
}
}
-#endif
- else if (!rb_is_absolute_path(s)) {
+#endif /* DOSISH_DRIVE_LETTER */
+ else if (s == fend || !rb_is_absolute_path(s)) {
if (!NIL_P(dname)) {
rb_file_expand_path_internal(dname, Qnil, abs_mode, long_name, result);
rb_enc_associate(result, fs_enc_check(result, fname));
@@ -3830,20 +4181,20 @@ rb_file_expand_path_internal(VALUE fname, VALUE dname, int abs_mode, int long_na
BUFINIT();
p = e;
}
-#if defined DOSISH || defined __CYGWIN__
- if (isdirsep(*s)) {
+#if defined DOSISH_DRIVE_LETTER || defined DOSISH_UNC
+ if (s < fend && isdirsep(*s)) {
/* specified full path, but not drive letter nor UNC */
/* we need to get the drive letter or UNC share name */
- p = skipprefix(buf, p, enc);
+ p = skipprefix(buf, p, mb_enc, enc);
}
else
-#endif
- p = chompdirsep(skiproot(buf, p, enc), p, enc);
+#endif /* defined DOSISH_DRIVE_LETTER || defined DOSISH_UNC */
+ p = chompdirsep(skiproot(buf, p), p, mb_enc, enc);
}
else {
size_t len;
b = s;
- do s++; while (isdirsep(*s));
+ do s++; while (s < fend && isdirsep(*s));
len = s - b;
p = buf + len;
BUFCHECK(bdiff >= buflen);
@@ -3862,23 +4213,24 @@ rb_file_expand_path_internal(VALUE fname, VALUE dname, int abs_mode, int long_na
rb_str_set_len(result, p-buf+1);
BUFCHECK(bdiff + 1 >= buflen);
p[1] = 0;
- root = skipprefix(buf, p+1, enc);
+ root = skipprefix(buf, p+1, mb_enc, enc);
b = s;
- while (*s) {
+ while (s < fend) {
switch (*s) {
case '.':
if (b == s++) { /* beginning of path element */
- switch (*s) {
- case '\0':
+ if (s == fend) {
b = s;
break;
+ }
+ switch (*s) {
case '.':
- if (*(s+1) == '\0' || isdirsep(*(s+1))) {
+ if (s+1 == fend || isdirsep(*(s+1))) {
/* We must go back to the parent */
char *n;
*p = '\0';
- if (!(n = strrdirsep(root, p, enc))) {
+ if (!(n = strrdirsep(root, p, mb_enc, enc))) {
*p = '/';
}
else {
@@ -3888,13 +4240,13 @@ rb_file_expand_path_internal(VALUE fname, VALUE dname, int abs_mode, int long_na
}
#if USE_NTFS
else {
- do ++s; while (istrailinggarbage(*s));
+ do ++s; while (s < fend && istrailinggarbage(*s));
}
-#endif
+#endif /* USE_NTFS */
break;
case '/':
-#if defined DOSISH || defined __CYGWIN__
- case '\\':
+#if defined FILE_ALT_SEPARATOR
+ case FILE_ALT_SEPARATOR:
#endif
b = ++s;
break;
@@ -3915,11 +4267,11 @@ rb_file_expand_path_internal(VALUE fname, VALUE dname, int abs_mode, int long_na
}
}
}
-#endif
+#endif /* USE_NTFS */
break;
case '/':
-#if defined DOSISH || defined __CYGWIN__
- case '\\':
+#if defined FILE_ALT_SEPARATOR
+ case FILE_ALT_SEPARATOR:
#endif
if (s > b) {
WITH_ROOTDIFF(BUFCOPY(b, s-b));
@@ -3940,8 +4292,8 @@ rb_file_expand_path_internal(VALUE fname, VALUE dname, int abs_mode, int long_na
break;
}
}
-#endif
- Inc(s, fend, enc);
+#endif /* __APPLE__ */
+ Inc(s, fend, mb_enc, enc);
break;
}
}
@@ -3964,12 +4316,12 @@ rb_file_expand_path_internal(VALUE fname, VALUE dname, int abs_mode, int long_na
s -= prime_len; /* alternative */
}
}
-# endif
-#endif
+# endif /* USE_NTFS_ADS */
+#endif /* USE_NTFS */
BUFCOPY(b, s-b);
rb_str_set_len(result, p-buf);
}
- if (p == skiproot(buf, p + !!*p, enc) - 1) p++;
+ if (p == skiproot(buf, p + !!*p) - 1) p++;
#if USE_NTFS
*p = '\0';
@@ -3986,7 +4338,7 @@ rb_file_expand_path_internal(VALUE fname, VALUE dname, int abs_mode, int long_na
const int flags = CCP_POSIX_TO_WIN_A | CCP_RELATIVE;
#else
char w32buf[MAXPATHLEN];
-#endif
+#endif /* HAVE_CYGWIN_CONV_PATH */
const char *path;
ssize_t bufsize;
int lnk_added = 0, is_symlink = 0;
@@ -4010,12 +4362,12 @@ rb_file_expand_path_internal(VALUE fname, VALUE dname, int abs_mode, int long_na
b = w32buf;
}
}
-#else
+#else /* !HAVE_CYGWIN_CONV_PATH */
bufsize = MAXPATHLEN;
if (cygwin_conv_to_win32_path(path, w32buf) == 0) {
b = w32buf;
}
-#endif
+#endif /* !HAVE_CYGWIN_CONV_PATH */
if (is_symlink && b == w32buf) {
*p = '\\';
strlcat(w32buf, p, bufsize);
@@ -4027,7 +4379,7 @@ rb_file_expand_path_internal(VALUE fname, VALUE dname, int abs_mode, int long_na
lnk_added = 0;
}
*p = '/';
-#endif
+#endif /* __CYGWIN__ */
rb_str_set_len(result, p - buf + strlen(p));
encidx = ENCODING_GET(result);
tmp = result;
@@ -4075,14 +4427,14 @@ rb_file_expand_path_internal(VALUE fname, VALUE dname, int abs_mode, int long_na
}
#endif
}
-#endif
+#endif /* USE_NTFS */
rb_str_set_len(result, p - buf);
rb_enc_check(fname, result);
ENC_CODERANGE_CLEAR(result);
return result;
}
-#endif /* _WIN32 */
+#endif /* !_WIN32 (this ifdef started above rb_default_home_dir) */
#define EXPAND_PATH_BUFFER() rb_usascii_str_new(0, 1)
@@ -4127,31 +4479,43 @@ rb_file_s_expand_path(int argc, const VALUE *argv)
}
/*
+ * :markup: markdown
+ *
* call-seq:
- * File.expand_path(file_name [, dir_string] ) -> abs_file_name
+ * File.expand_path(path, dirpath = '.') -> absolute_path
*
- * Converts a pathname to an absolute pathname. Relative paths are
- * referenced from the current working directory of the process unless
- * +dir_string+ is given, in which case it will be used as the
- * starting point. The given pathname may start with a
- * ``<code>~</code>'', which expands to the process owner's home
- * directory (the environment variable +HOME+ must be set
- * correctly). ``<code>~</code><i>user</i>'' expands to the named
- * user's home directory.
+ * Returns the string absolute path for the given `path`.
*
- * File.expand_path("~oracle/bin") #=> "/home/oracle/bin"
+ * Evaluates a relative path with respect to the directory given by `dirpath`:
*
- * A simple example of using +dir_string+ is as follows.
- * File.expand_path("ruby", "/usr/bin") #=> "/usr/bin/ruby"
+ * ```ruby
+ * Dir.chdir('/snap')
+ * # Default dirpath.
+ * File.expand_path('README') # => "/snap/README"
+ * File.expand_path('bin') # => "/snap/bin"
+ * File.expand_path('bin/../var') # => "/snap/var" # Cleaned.
+ * # Other dirpath.
+ * File.expand_path('../zip', '/usr/bin/ruby') # => "/usr/bin/zip"
+ * Dir.chdir('/usr/bin')
+ * File.expand_path('../../snap', __FILE__) # => "/usr/snap"
+ * ```
*
- * A more complex example which also resolves parent directory is as follows.
- * Suppose we are in bin/mygem and want the absolute path of lib/mygem.rb.
+ * Evaluates an absolute path without respect to `dirpath`:
*
- * File.expand_path("../../lib/mygem.rb", __FILE__)
- * #=> ".../path/to/project/lib/mygem.rb"
+ * ```ruby
+ * File.expand_path('/snap') # => "/snap"
+ * File.expand_path('/snap', 'nosuch') # => "/snap"
+ * File.expand_path('/snap/../snap') # => "/snap" # Cleaned.
+ * ```
+ *
+ * More examples:
+ *
+ * ```
+ * Dir.chdir('/usr/bin')
+ * File.expand_path('../../snap', __FILE__) # => "/usr/snap"
+ * File.expand_path('../../snap') # => "/snap"
+ * ```
*
- * So first it resolves the parent of __FILE__, that is bin/, then go to the
- * parent, the root of the project and appends +lib/mygem.rb+.
*/
static VALUE
@@ -4239,9 +4603,10 @@ realpath_rec(long *prefixlenp, VALUE *resolvedp, const char *unresolved, VALUE f
}
else if (testnamelen == 2 && testname[0] == '.' && testname[1] == '.') {
if (*prefixlenp < RSTRING_LEN(*resolvedp)) {
+ bool mb_enc = enc_mbclen_needed(enc);
const char *resolved_str = RSTRING_PTR(*resolvedp);
const char *resolved_names = resolved_str + *prefixlenp;
- const char *lastsep = strrdirsep(resolved_names, resolved_str + RSTRING_LEN(*resolvedp), enc);
+ const char *lastsep = strrdirsep(resolved_names, resolved_str + RSTRING_LEN(*resolvedp), mb_enc, enc);
long len = lastsep ? lastsep - resolved_names : 0;
rb_str_resize(*resolvedp, *prefixlenp + len);
}
@@ -4322,7 +4687,7 @@ realpath_rec(long *prefixlenp, VALUE *resolvedp, const char *unresolved, VALUE f
rb_hash_aset(loopcheck, testpath, rb_str_dup_frozen(*resolvedp));
}
else
-#endif
+#endif /* HAVE_READLINK */
{
VALUE s = rb_str_dup_frozen(testpath);
rb_hash_aset(loopcheck, s, s);
@@ -4381,7 +4746,8 @@ rb_check_realpath_emulate(VALUE basedir, VALUE path, rb_encoding *origenc, enum
root_found:
RSTRING_GETMEM(resolved, prefixptr, prefixlen);
pend = prefixptr + prefixlen;
- ptr = chompdirsep(prefixptr, pend, enc);
+ bool mb_enc = enc_mbclen_needed(enc);
+ ptr = chompdirsep(prefixptr, pend, mb_enc, enc);
if (ptr < pend) {
prefixlen = ++ptr - prefixptr;
rb_str_set_len(resolved, prefixlen);
@@ -4391,7 +4757,7 @@ rb_check_realpath_emulate(VALUE basedir, VALUE path, rb_encoding *origenc, enum
if (*prefixptr == FILE_ALT_SEPARATOR) {
*prefixptr = '/';
}
- Inc(prefixptr, pend, enc);
+ Inc(prefixptr, pend, mb_enc, enc);
}
#endif
@@ -4427,7 +4793,7 @@ rb_check_realpath_emulate(VALUE basedir, VALUE path, rb_encoding *origenc, enum
return resolved;
}
-static VALUE rb_file_join(VALUE ary);
+static VALUE rb_file_join(long argc, VALUE *args);
#ifndef HAVE_REALPATH
static VALUE
@@ -4442,6 +4808,11 @@ rb_check_realpath_emulate_rescue(VALUE arg, VALUE exc)
{
return Qnil;
}
+#elif !defined(NEEDS_REALPATH_BUFFER) && defined(__APPLE__) && \
+ (!defined(MAC_OS_X_VERSION_10_6) || (MAC_OS_X_VERSION_MIN_REQUIRED < MAC_OS_X_VERSION_10_6))
+/* realpath() on OSX < 10.6 doesn't implement automatic allocation */
+# include <sys/syslimits.h>
+# define NEEDS_REALPATH_BUFFER 1
#endif /* HAVE_REALPATH */
static VALUE
@@ -4451,6 +4822,11 @@ rb_check_realpath_internal(VALUE basedir, VALUE path, rb_encoding *origenc, enum
VALUE unresolved_path;
char *resolved_ptr = NULL;
VALUE resolved;
+# if defined(NEEDS_REALPATH_BUFFER) && NEEDS_REALPATH_BUFFER
+ char resolved_buffer[PATH_MAX];
+# else
+ char *const resolved_buffer = NULL;
+# endif
if (mode == RB_REALPATH_DIR) {
return rb_check_realpath_emulate(basedir, path, origenc, mode);
@@ -4458,17 +4834,22 @@ rb_check_realpath_internal(VALUE basedir, VALUE path, rb_encoding *origenc, enum
unresolved_path = rb_str_dup_frozen(path);
if (*RSTRING_PTR(unresolved_path) != '/' && !NIL_P(basedir)) {
- unresolved_path = rb_file_join(rb_assoc_new(basedir, unresolved_path));
+ VALUE paths[2] = {basedir, unresolved_path};
+ unresolved_path = rb_file_join(2, paths);
}
if (origenc) unresolved_path = TO_OSPATH(unresolved_path);
- if ((resolved_ptr = realpath(RSTRING_PTR(unresolved_path), NULL)) == NULL) {
- /* glibc realpath(3) does not allow /path/to/file.rb/../other_file.rb,
+ if ((resolved_ptr = realpath(RSTRING_PTR(unresolved_path), resolved_buffer)) == NULL) {
+ /*
+ wasi-libc 22 and later support realpath(3) but return ENOTSUP
+ when the underlying host syscall returns it.
+ glibc realpath(3) does not allow /path/to/file.rb/../other_file.rb,
returning ENOTDIR in that case.
glibc realpath(3) can also return ENOENT for paths that exist,
such as /dev/fd/5.
Fallback to the emulated approach in either of those cases. */
- if (errno == ENOTDIR ||
+ if (errno == ENOTSUP ||
+ errno == ENOTDIR ||
(errno == ENOENT && rb_file_exist_p(0, unresolved_path))) {
return rb_check_realpath_emulate(basedir, path, origenc, mode);
@@ -4479,9 +4860,11 @@ rb_check_realpath_internal(VALUE basedir, VALUE path, rb_encoding *origenc, enum
rb_sys_fail_path(unresolved_path);
}
resolved = ospath_new(resolved_ptr, strlen(resolved_ptr), rb_filesystem_encoding());
+# if !(defined(NEEDS_REALPATH_BUFFER) && NEEDS_REALPATH_BUFFER)
free(resolved_ptr);
+# endif
-# if !defined(__LINUX__) && !defined(__APPLE__)
+# if !defined(__linux__) && !defined(__APPLE__)
/* As `resolved` is a String in the filesystem encoding, no
* conversion is needed */
struct stat st;
@@ -4491,7 +4874,7 @@ rb_check_realpath_internal(VALUE basedir, VALUE path, rb_encoding *origenc, enum
}
rb_sys_fail_path(unresolved_path);
}
-# endif
+# endif /* !defined(__linux__) && !defined(__APPLE__) */
if (origenc && origenc != rb_enc_get(resolved)) {
if (!rb_enc_str_asciionly_p(resolved)) {
@@ -4500,16 +4883,16 @@ rb_check_realpath_internal(VALUE basedir, VALUE path, rb_encoding *origenc, enum
rb_enc_associate(resolved, origenc);
}
- if (rb_enc_str_coderange(resolved) == ENC_CODERANGE_BROKEN) {
+ if (is_broken_string(resolved)) {
rb_enc_associate(resolved, rb_filesystem_encoding());
- if (rb_enc_str_coderange(resolved) == ENC_CODERANGE_BROKEN) {
+ if (is_broken_string(resolved)) {
rb_enc_associate(resolved, rb_ascii8bit_encoding());
}
}
RB_GC_GUARD(unresolved_path);
return resolved;
-#else
+#else /* !HAVE_REALPATH */
if (mode == RB_REALPATH_CHECK) {
VALUE arg[3];
arg[0] = basedir;
@@ -4606,7 +4989,7 @@ rmext(const char *p, long l0, long l1, const char *e, long l2, rb_encoding *enc)
if (l1 < l2) return l1;
s = p+l1-l2;
- if (rb_enc_left_char_head(p, s, p+l1, enc) != s) return 0;
+ if (!at_char_boundary(p, s, p+l1, enc)) return 0;
#if CASEFOLD_FILESYSTEM
#define fncomp strncasecmp
#else
@@ -4618,23 +5001,29 @@ rmext(const char *p, long l0, long l1, const char *e, long l2, rb_encoding *enc)
return 0;
}
-const char *
-ruby_enc_find_basename(const char *name, long *baselen, long *alllen, rb_encoding *enc)
+static inline const char *
+enc_find_basename(const char *name, long *baselen, long *alllen, bool mb_enc, rb_encoding *enc)
{
const char *p, *q, *e, *end;
-#if defined DOSISH_DRIVE_LETTER || defined DOSISH_UNC
- const char *root;
-#endif
long f = 0, n = -1;
- end = name + (alllen ? (size_t)*alllen : strlen(name));
- name = skipprefix(name, end, enc);
+ long len = (alllen ? (size_t)*alllen : strlen(name));
+
+ if (len <= 0) {
+ return name;
+ }
+
+ end = name + len;
+ name = skipprefix(name, end, mb_enc, enc);
#if defined DOSISH_DRIVE_LETTER || defined DOSISH_UNC
- root = name;
+ const char *root = name;
#endif
- while (isdirsep(*name))
+
+ while (name < end && isdirsep(*name)) {
name++;
- if (!*name) {
+ }
+
+ if (name == end) {
p = name - 1;
f = 1;
#if defined DOSISH_DRIVE_LETTER || defined DOSISH_UNC
@@ -4646,100 +5035,136 @@ ruby_enc_find_basename(const char *name, long *baselen, long *alllen, rb_encodin
p++;
f = 0;
}
-#endif
+#endif /* DOSISH_DRIVE_LETTER */
#ifdef DOSISH_UNC
else {
p = "/";
}
-#endif
-#endif
+#endif /* DOSISH_UNC */
+#endif /* defined DOSISH_DRIVE_LETTER || defined DOSISH_UNC */
}
else {
- if (!(p = strrdirsep(name, end, enc))) {
+ p = strrdirsep(name, end, mb_enc, enc);
+ if (!p) {
p = name;
}
else {
- while (isdirsep(*p)) p++; /* skip last / */
+ while (isdirsep(*p)) {
+ p++; /* skip last / */
+ }
}
#if USE_NTFS
n = ntfs_tail(p, end, enc) - p;
#else
- n = chompdirsep(p, end, enc) - p;
+ n = chompdirsep(p, end, mb_enc, enc) - p;
#endif
for (q = p; q - p < n && *q == '.'; q++);
- for (e = 0; q - p < n; Inc(q, end, enc)) {
+ for (e = 0; q - p < n; Inc(q, end, mb_enc, enc)) {
if (*q == '.') e = q;
}
- if (e) f = e - p;
- else f = n;
+ if (e) {
+ f = e - p;
+ }
+ else {
+ f = n;
+ }
}
- if (baselen)
+ if (baselen) {
*baselen = f;
- if (alllen)
+ }
+ if (alllen) {
*alllen = n;
+ }
return p;
}
+const char *
+ruby_enc_find_basename(const char *name, long *baselen, long *alllen, rb_encoding *enc)
+{
+ return enc_find_basename(name, baselen, alllen, enc_mbclen_needed(enc), enc);
+}
+
/*
* call-seq:
- * File.basename(file_name [, suffix] ) -> base_name
+ * File.basename(path, suffix = '') -> new_string
*
- * Returns the last component of the filename given in
- * <i>file_name</i> (after first stripping trailing separators),
- * which can be formed using both File::SEPARATOR and
- * File::ALT_SEPARATOR as the separator when File::ALT_SEPARATOR is
- * not <code>nil</code>. If <i>suffix</i> is given and present at the
- * end of <i>file_name</i>, it is removed. If <i>suffix</i> is ".*",
- * any extension will be removed.
+ * Returns a new string containing all or part of the last entry of the given +path+.
+ * Entries are delimited by the value of constant File::SEPARATOR
+ * and, if non-nil, the value of constant File::ALT_SEPARATOR.
+ *
+ * When +suffix+ is the empty string <tt>''</tt>,
+ * returns all of the last entry:
+ *
+ * File.basename('foo/bar/baz/bat.txt') # => "bat.txt"
+ * File.basename('foo/bar/baz') # => "baz"
+ *
+ * File::SEPARATOR # => "/"
+ * File.basename('foo/bar.txt////') # => "bar.txt"
+ * File::ALT_SEPARATOR # => "\\" # On Windows.
+ * File.basename('foo/bar.txt//\\\\//') # => "bar.txt"
+ *
+ * When +suffix+ is <tt>'.*'</tt>,
+ * the last {filename extension}[https://en.wikipedia.org/wiki/Filename_extension],
+ * if any, is removed:
+ *
+ * File.basename('foo/bar.txt', '.*') # => "bar"
+ * File.basename('foo/bar.txt.old', '.*') # => "bar.txt"
+ * File.basename('foo/bar', '.*') # => "bar"
+ *
+ * When +suffix+ is any string other than <tt>''</tt> or <tt>'.*'</tt>,
+ * the matching trailing substring, if any, is removed:
+ *
+ * File.basename('foo/bar.txt', '.txt') # => "bar"
+ * File.basename('foo/bar.txt', 'txt') # => "bar."
+ * File.basename('foo/bar.txt', '*') # => "bar.txt"
+ * File.basename('foo/bar.txt', '.') # => "bar.txt"
*
- * File.basename("/home/gumby/work/ruby.rb") #=> "ruby.rb"
- * File.basename("/home/gumby/work/ruby.rb", ".rb") #=> "ruby"
- * File.basename("/home/gumby/work/ruby.rb", ".*") #=> "ruby"
*/
static VALUE
rb_file_s_basename(int argc, VALUE *argv, VALUE _)
{
- VALUE fname, fext, basename;
- const char *name, *p;
- long f, n;
+ VALUE fname, fext = Qnil;
+ const char *name, *p, *fp = 0;
+ long f = 0, n;
rb_encoding *enc;
- fext = Qnil;
- if (rb_check_arity(argc, 1, 2) == 2) {
+ argc = rb_check_arity(argc, 1, 2);
+ fname = argv[0];
+ CheckPath(fname, name);
+ if (argc == 2) {
fext = argv[1];
- StringValue(fext);
- enc = check_path_encoding(fext);
+ fp = StringValueCStr(fext);
+ check_path_encoding(fext);
}
- fname = argv[0];
- FilePathStringValue(fname);
if (NIL_P(fext) || !(enc = rb_enc_compatible(fname, fext))) {
- enc = rb_enc_get(fname);
- fext = Qnil;
+ enc = rb_str_enc_get(fname);
}
- if ((n = RSTRING_LEN(fname)) == 0 || !*(name = RSTRING_PTR(fname)))
- return rb_str_new_shared(fname);
- p = ruby_enc_find_basename(name, &f, &n, enc);
+ n = RSTRING_LEN(fname);
+ if (n <= 0 || !*name) {
+ return rb_enc_str_new(0, 0, enc);
+ }
+
+ bool mb_enc = enc_mbclen_needed(enc);
+ p = enc_find_basename(name, &f, &n, mb_enc, enc);
if (n >= 0) {
- if (NIL_P(fext)) {
+ if (!fp) {
f = n;
}
else {
- const char *fp;
- fp = StringValueCStr(fext);
if (!(f = rmext(p, f, n, fp, RSTRING_LEN(fext), enc))) {
f = n;
}
RB_GC_GUARD(fext);
}
- if (f == RSTRING_LEN(fname)) return rb_str_new_shared(fname);
+ if (f == RSTRING_LEN(fname)) {
+ return rb_str_new_shared(fname);
+ }
}
- basename = rb_str_new(p, f);
- rb_enc_copy(basename, fname);
- return basename;
+ return rb_enc_str_new(p, f, enc);
}
static VALUE rb_file_dirname_n(VALUE fname, int n);
@@ -4784,19 +5209,18 @@ rb_file_dirname_n(VALUE fname, int n)
{
const char *name, *root, *p, *end;
VALUE dirname;
- rb_encoding *enc;
- VALUE sepsv = 0;
- const char **seps;
if (n < 0) rb_raise(rb_eArgError, "negative level: %d", n);
- FilePathStringValue(fname);
- name = StringValueCStr(fname);
+ CheckPath(fname, name);
end = name + RSTRING_LEN(fname);
- enc = rb_enc_get(fname);
- root = skiproot(name, end, enc);
+
+ bool mb_enc = !rb_str_enc_fastpath(fname);
+ rb_encoding *enc = rb_str_enc_get(fname);
+
+ root = skiproot(name, end);
#ifdef DOSISH_UNC
if (root > name + 1 && isdirsep(*name))
- root = skipprefix(name = root - 2, end, enc);
+ root = skipprefix(name = root - 2, end, mb_enc, enc);
#else
if (root > name + 1)
name = root - 1;
@@ -4805,72 +5229,41 @@ rb_file_dirname_n(VALUE fname, int n)
p = root;
}
else {
- int i;
- switch (n) {
- case 0:
- p = end;
- break;
- case 1:
- if (!(p = strrdirsep(root, end, enc))) p = root;
- break;
- default:
- seps = ALLOCV_N(const char *, sepsv, n);
- for (i = 0; i < n; ++i) seps[i] = root;
- i = 0;
- for (p = root; p < end; ) {
- if (isdirsep(*p)) {
- const char *tmp = p++;
- while (p < end && isdirsep(*p)) p++;
- if (p >= end) break;
- seps[i++] = tmp;
- if (i == n) i = 0;
- }
- else {
- Inc(p, end, enc);
- }
+ p = end;
+ while (n) {
+ if (!(p = strrdirsep(root, p, mb_enc, enc))) {
+ p = root;
+ break;
}
- p = seps[i];
- ALLOCV_END(sepsv);
- break;
+ n--;
}
}
- if (p == name)
- return rb_usascii_str_new2(".");
+
+ if (p == name) {
+ return rb_enc_str_new(".", 1, enc);
+ }
#ifdef DOSISH_DRIVE_LETTER
- if (has_drive_letter(name) && isdirsep(*(name + 2))) {
- const char *top = skiproot(name + 2, end, enc);
- dirname = rb_str_new(name, 3);
+ if (name + 3 < end && has_drive_letter(name) && isdirsep(*(name + 2))) {
+ const char *top = skiproot(name + 2, end);
+ dirname = rb_enc_str_new(name, 3, enc);
rb_str_cat(dirname, top, p - top);
}
else
#endif
- dirname = rb_str_new(name, p - name);
+ dirname = rb_enc_str_new(name, p - name, enc);
#ifdef DOSISH_DRIVE_LETTER
- if (has_drive_letter(name) && root == name + 2 && p - name == 2)
+ if (root == name + 2 && p == root && name[1] == ':')
rb_str_cat(dirname, ".", 1);
#endif
- rb_enc_copy(dirname, fname);
return dirname;
}
-/*
- * accept a String, and return the pointer of the extension.
- * if len is passed, set the length of extension to it.
- * returned pointer is in ``name'' or NULL.
- * returns *len
- * no dot NULL 0
- * dotfile top 0
- * end with dot dot 1
- * .ext dot len of .ext
- * .ext:stream dot len of .ext without :stream (NT only)
- *
- */
-const char *
-ruby_enc_find_extname(const char *name, long *len, rb_encoding *enc)
+static inline const char *
+enc_find_extname(const char *name, long *len, bool mb_enc, rb_encoding *enc)
{
const char *p, *e, *end = name + (len ? *len : (long)strlen(name));
- p = strrdirsep(name, end, enc); /* get the last path component */
+ p = strrdirsep(name, end, mb_enc, enc); /* get the last path component */
if (!p)
p = name;
else
@@ -4894,7 +5287,7 @@ ruby_enc_find_extname(const char *name, long *len, rb_encoding *enc)
continue;
#else
e = p; /* get the last dot of the last component */
-#endif
+#endif /* USE_NTFS */
}
#if USE_NTFS
else if (isADS(*p)) {
@@ -4903,7 +5296,7 @@ ruby_enc_find_extname(const char *name, long *len, rb_encoding *enc)
#endif
else if (isdirsep(*p))
break;
- Inc(p, end, enc);
+ Inc(p, end, mb_enc, enc);
}
if (len) {
@@ -4919,57 +5312,106 @@ ruby_enc_find_extname(const char *name, long *len, rb_encoding *enc)
}
/*
+ * accept a String, and return the pointer of the extension.
+ * if len is passed, set the length of extension to it.
+ * returned pointer is in ``name'' or NULL.
+ * returns *len
+ * no dot NULL 0
+ * dotfile top 0
+ * end with dot dot 1
+ * .ext dot len of .ext
+ * .ext:stream dot len of .ext without :stream (NTFS only)
+ *
+ */
+const char *
+ruby_enc_find_extname(const char *name, long *len, rb_encoding *enc)
+{
+ return enc_find_extname(name, len, enc_mbclen_needed(enc), enc);
+}
+
+/*
+ * :markup: markdown
+ *
* call-seq:
- * File.extname(path) -> string
+ * File.extname(path) -> extension
+ *
+ * Returns the filename extension --
+ * usually the portion of the string `path`
+ * beginning from the last period:
+ *
+ * ```ruby
+ * File.extname('t.rb') # => ".rb"
+ * File.extname('foo.bar.t.rb') # => ".rb"
+ * File.extname('foo/bar/t.rb') # => ".rb"
+ * File.extname('nosuch.txt') # => ".txt" # Path need not exist.
+ * ```
+ *
+ * Returns the entire string when there is no period:
*
- * Returns the extension (the portion of file name in +path+
- * starting from the last period).
+ * ```ruby
+ * Pathname('foo').extname # => ""
+ * ```
*
- * If +path+ is a dotfile, or starts with a period, then the starting
- * dot is not dealt with the start of the extension.
+ * Returns an empty string when the only period is the first character:
*
- * An empty string will also be returned when the period is the last character
- * in +path+.
+ * ```ruby
+ * File.extname('.irbrc') # => ""
+ * ```
*
- * On Windows, trailing dots are truncated.
+ * Returns an empty string or `'.'` when `path` ends with a period:
*
- * File.extname("test.rb") #=> ".rb"
- * File.extname("a/b/d/test.rb") #=> ".rb"
- * File.extname(".a/b/d/test.rb") #=> ".rb"
- * File.extname("foo.") #=> "" on Windows
- * File.extname("foo.") #=> "." on non-Windows
- * File.extname("test") #=> ""
- * File.extname(".profile") #=> ""
- * File.extname(".profile.sh") #=> ".sh"
+ * ```
+ * File.extname('foo.') # => "" # On Windows.
+ * File.extname('foo.') # => "." # Elsewhere.
+ * File.extname('foo....') # => "" # On Windows.
+ * File.extname('foo....') # => "." # Elsewhere.
+ * ```
*
*/
static VALUE
rb_file_s_extname(VALUE klass, VALUE fname)
{
- const char *name, *e;
- long len;
- VALUE extname;
+ const char *name;
+ CheckPath(fname, name);
+ long len = RSTRING_LEN(fname);
- FilePathStringValue(fname);
- name = StringValueCStr(fname);
- len = RSTRING_LEN(fname);
- e = ruby_enc_find_extname(name, &len, rb_enc_get(fname));
- if (len < 1)
- return rb_str_new(0, 0);
- extname = rb_str_subseq(fname, e - name, len); /* keep the dot, too! */
- return extname;
+ if (len < 1) {
+ return rb_enc_str_new(0, 0, rb_str_enc_get(fname));
+ }
+
+ bool mb_enc = !rb_str_enc_fastpath(fname);
+ rb_encoding *enc = rb_str_enc_get(fname);
+
+ const char *ext = enc_find_extname(name, &len, mb_enc, enc);
+ return rb_enc_str_new(ext, len, enc);
}
/*
- * call-seq:
+ * call-seq:
* File.path(path) -> string
*
- * Returns the string representation of the path
+ * Returns the string representation of the path
*
- * File.path("/dev/null") #=> "/dev/null"
+ * File.path(File::NULL) #=> "/dev/null"
* File.path(Pathname.new("/tmp")) #=> "/tmp"
*
+ * If +path+ is not a String:
+ *
+ * 1. If it has the +to_path+ method, that method will be called to
+ * coerce to a String.
+ *
+ * 2. Otherwise, or if the coerced result is not a String too, the
+ * standard coercion using +to_str+ method will take place on that
+ * object. (See also String.try_convert)
+ *
+ * The coerced string must satisfy the following conditions:
+ *
+ * 1. It must be in an ASCII-compatible encoding; otherwise, an
+ * Encoding::CompatibilityError is raised.
+ *
+ * 2. It must not contain the NUL character (<tt>\0</tt>); otherwise,
+ * an ArgumentError is raised.
*/
static VALUE
@@ -4996,15 +5438,17 @@ rb_file_s_split(VALUE klass, VALUE path)
return rb_assoc_new(rb_file_dirname(path), rb_file_s_basename(1,&path,Qundef));
}
+static VALUE rb_file_join_ary(VALUE ary);
+
static VALUE
file_inspect_join(VALUE ary, VALUE arg, int recur)
{
if (recur || ary == arg) rb_raise(rb_eArgError, "recursive array");
- return rb_file_join(arg);
+ return rb_file_join_ary(arg);
}
static VALUE
-rb_file_join(VALUE ary)
+rb_file_join_ary(VALUE ary)
{
long len, i;
VALUE result, tmp;
@@ -5052,11 +5496,11 @@ rb_file_join(VALUE ary)
rb_enc_copy(result, tmp);
}
else {
- tail = chompdirsep(name, name + len, rb_enc_get(result));
- if (RSTRING_PTR(tmp) && isdirsep(RSTRING_PTR(tmp)[0])) {
+ tail = chompdirsep(name, name + len, true, rb_enc_get(result));
+ if (RSTRING_LEN(tmp) > 0 && isdirsep(RSTRING_PTR(tmp)[0])) {
rb_str_set_len(result, tail - name);
}
- else if (!*tail) {
+ else if (tail == name + len) {
rb_str_cat(result, "/", 1);
}
}
@@ -5069,6 +5513,77 @@ rb_file_join(VALUE ary)
return result;
}
+static inline VALUE
+rb_file_join_fastpath(long argc, VALUE *args)
+{
+ long size = argc;
+
+ long i;
+ for (i = 0; i < argc; i++) {
+ VALUE tmp = args[i];
+ if (RB_LIKELY(RB_TYPE_P(tmp, T_STRING) && rb_str_enc_fastpath(tmp))) {
+ size += RSTRING_LEN(tmp);
+ }
+ else {
+ return 0;
+ }
+ }
+
+ VALUE result = rb_str_buf_new(size);
+
+ int encidx = ENCODING_GET_INLINED(args[0]);
+ ENCODING_SET_INLINED(result, encidx);
+ rb_str_buf_append(result, args[0]);
+
+ const char *name = RSTRING_PTR(result);
+ for (i = 1; i < argc; i++) {
+ VALUE tmp = args[i];
+ long len = RSTRING_LEN(result);
+
+ const char *tmp_s;
+ long tmp_len;
+ RSTRING_GETMEM(tmp, tmp_s, tmp_len);
+
+ if (tmp_len > 0 && isdirsep(tmp_s[0])) {
+ // right side has a leading separator, remove left side separators.
+ long chomp = len;
+ while (chomp > 0 && isdirsep(name[chomp - 1])) {
+ --chomp;
+ }
+ rb_str_set_len(result, chomp);
+ }
+ else if (len < 1 || !isdirsep(name[len - 1])) {
+ // neither side have a separator, append one;
+ rb_str_cat(result, "/", 1);
+ }
+
+ if (RB_UNLIKELY(ENCODING_GET_INLINED(tmp) != encidx)) {
+ rb_encoding *new_enc = fs_enc_check(result, tmp);
+ rb_enc_associate(result, new_enc);
+ encidx = rb_enc_to_index(new_enc);
+ }
+
+ rb_str_buf_cat(result, tmp_s, tmp_len);
+ }
+
+ rb_str_null_check(result);
+ return result;
+}
+
+static inline VALUE
+rb_file_join(long argc, VALUE *args)
+{
+ if (RB_UNLIKELY(argc == 0)) {
+ return rb_str_new(0, 0);
+ }
+
+ VALUE result = rb_file_join_fastpath(argc, args);
+ if (RB_LIKELY(result)) {
+ return result;
+ }
+
+ return rb_file_join_ary(rb_ary_new_from_values(argc, args));
+}
/*
* call-seq:
* File.join(string, ...) -> string
@@ -5081,46 +5596,22 @@ rb_file_join(VALUE ary)
*/
static VALUE
-rb_file_s_join(VALUE klass, VALUE args)
+rb_file_s_join(int argc, VALUE *argv, VALUE klass)
{
- return rb_file_join(args);
+ return rb_file_join(argc, argv);
}
-#if defined(HAVE_TRUNCATE) || defined(HAVE_CHSIZE)
+#if defined(HAVE_TRUNCATE)
struct truncate_arg {
const char *path;
-#if defined(HAVE_TRUNCATE)
-#define NUM2POS(n) NUM2OFFT(n)
- off_t pos;
-#else
-#define NUM2POS(n) NUM2LONG(n)
- long pos;
-#endif
+ rb_off_t pos;
};
static void *
nogvl_truncate(void *ptr)
{
struct truncate_arg *ta = ptr;
-#ifdef HAVE_TRUNCATE
return (void *)(VALUE)truncate(ta->path, ta->pos);
-#else /* defined(HAVE_CHSIZE) */
- {
- int tmpfd = rb_cloexec_open(ta->path, 0, 0);
-
- if (tmpfd < 0)
- return (void *)-1;
- rb_update_max_fd(tmpfd);
- if (chsize(tmpfd, ta->pos) < 0) {
- int e = errno;
- close(tmpfd);
- errno = e;
- return (void *)-1;
- }
- close(tmpfd);
- return 0;
- }
-#endif
}
/*
@@ -5144,32 +5635,24 @@ rb_file_s_truncate(VALUE klass, VALUE path, VALUE len)
struct truncate_arg ta;
int r;
- ta.pos = NUM2POS(len);
+ ta.pos = NUM2OFFT(len);
FilePathValue(path);
path = rb_str_encode_ospath(path);
ta.path = StringValueCStr(path);
- r = (int)(VALUE)rb_thread_call_without_gvl(nogvl_truncate, &ta,
- RUBY_UBF_IO, NULL);
+ r = IO_WITHOUT_GVL_INT(nogvl_truncate, &ta);
if (r < 0)
rb_sys_fail_path(path);
return INT2FIX(0);
-#undef NUM2POS
}
#else
#define rb_file_s_truncate rb_f_notimplement
#endif
-#if defined(HAVE_FTRUNCATE) || defined(HAVE_CHSIZE)
+#if defined(HAVE_FTRUNCATE)
struct ftruncate_arg {
int fd;
-#if defined(HAVE_FTRUNCATE)
-#define NUM2POS(n) NUM2OFFT(n)
- off_t pos;
-#else
-#define NUM2POS(n) NUM2LONG(n)
- long pos;
-#endif
+ rb_off_t pos;
};
static VALUE
@@ -5177,11 +5660,7 @@ nogvl_ftruncate(void *ptr)
{
struct ftruncate_arg *fa = ptr;
-#ifdef HAVE_FTRUNCATE
return (VALUE)ftruncate(fa->fd, fa->pos);
-#else /* defined(HAVE_CHSIZE) */
- return (VALUE)chsize(fa->fd, fa->pos);
-#endif
}
/*
@@ -5204,18 +5683,17 @@ rb_file_truncate(VALUE obj, VALUE len)
rb_io_t *fptr;
struct ftruncate_arg fa;
- fa.pos = NUM2POS(len);
+ fa.pos = NUM2OFFT(len);
GetOpenFile(obj, fptr);
if (!(fptr->mode & FMODE_WRITABLE)) {
rb_raise(rb_eIOError, "not opened for writing");
}
rb_io_flush_raw(obj, 0);
fa.fd = fptr->fd;
- if ((int)rb_thread_io_blocking_region(nogvl_ftruncate, &fa, fa.fd) < 0) {
+ if ((int)rb_io_blocking_region(fptr, nogvl_ftruncate, &fa) < 0) {
rb_sys_fail_path(fptr->pathv);
}
return INT2FIX(0);
-#undef NUM2POS
}
#else
#define rb_file_truncate rb_f_notimplement
@@ -5255,47 +5733,46 @@ rb_thread_flock(void *data)
return (VALUE)ret;
}
-/*
+/* :markup: markdown
+ *
* call-seq:
- * file.flock(locking_constant) -> 0 or false
- *
- * Locks or unlocks a file according to <i>locking_constant</i> (a
- * logical <em>or</em> of the values in the table below).
- * Returns <code>false</code> if File::LOCK_NB is specified and the
- * operation would otherwise have blocked. Not available on all
- * platforms.
- *
- * Locking constants (in class File):
- *
- * LOCK_EX | Exclusive lock. Only one process may hold an
- * | exclusive lock for a given file at a time.
- * ----------+------------------------------------------------
- * LOCK_NB | Don't block when locking. May be combined
- * | with other lock options using logical or.
- * ----------+------------------------------------------------
- * LOCK_SH | Shared lock. Multiple processes may each hold a
- * | shared lock for a given file at the same time.
- * ----------+------------------------------------------------
- * LOCK_UN | Unlock.
+ * flock(locking_constant) -> 0 or false
+ *
+ * Locks or unlocks file `self` according to the given `locking_constant`,
+ * a bitwise OR of the values in the table below.
+ *
+ * Not available on all platforms.
+ *
+ * Returns `false` if `File::LOCK_NB` is specified and the operation would have blocked;
+ * otherwise returns `0`.
+ *
+ * | Constant | Lock | Effect
+ * |-----------------|--------------|-----------------------------------------------------------------------------------------------------------------|
+ * | `File::LOCK_EX` | Exclusive | Only one process may hold an exclusive lock for `self` at a time. |
+ * | `File::LOCK_NB` | Non-blocking | No blocking; may be combined with `File::LOCK_SH` or `File::LOCK_EX` using the bitwise OR operator <tt>\|</tt>. |
+ * | `File::LOCK_SH` | Shared | Multiple processes may each hold a shared lock for `self` at the same time. |
+ * | `File::LOCK_UN` | Unlock | Remove an existing lock held by this process. |
*
* Example:
*
- * # update a counter using write lock
- * # don't use "w" because it truncates the file before lock.
- * File.open("counter", File::RDWR|File::CREAT, 0644) {|f|
- * f.flock(File::LOCK_EX)
- * value = f.read.to_i + 1
- * f.rewind
- * f.write("#{value}\n")
- * f.flush
- * f.truncate(f.pos)
- * }
- *
- * # read the counter using read lock
- * File.open("counter", "r") {|f|
- * f.flock(File::LOCK_SH)
- * p f.read
- * }
+ * ```ruby
+ * # Update a counter using an exclusive lock.
+ * # Don't use File::WRONLY because it truncates the file.
+ * File.open('counter', File::RDWR | File::CREAT, 0644) do |f|
+ * f.flock(File::LOCK_EX)
+ * value = f.read.to_i + 1
+ * f.rewind
+ * f.write("#{value}\n")
+ * f.flush
+ * f.truncate(f.pos)
+ * end
+ *
+ * # Read the counter using a shared lock.
+ * File.open('counter', 'r') do |f|
+ * f.flock(File::LOCK_SH)
+ * f.read
+ * end
+ * ```
*
*/
@@ -5313,7 +5790,7 @@ rb_file_flock(VALUE obj, VALUE operation)
if (fptr->mode & FMODE_WRITABLE) {
rb_io_flush_raw(obj, 0);
}
- while ((int)rb_thread_io_blocking_region(rb_thread_flock, op, fptr->fd) < 0) {
+ while ((int)rb_io_blocking_region(fptr, rb_thread_flock, op) < 0) {
int e = errno;
switch (e) {
case EAGAIN:
@@ -5359,61 +5836,83 @@ test_check(int n, int argc, VALUE *argv)
#define CHECK(n) test_check((n), argc, argv)
/*
+ * :markup: markdown
+ *
* call-seq:
- * test(cmd, file1 [, file2] ) -> obj
- *
- * Uses the character +cmd+ to perform various tests on +file1+ (first
- * table below) or on +file1+ and +file2+ (second table).
- *
- * File tests on a single file:
- *
- * Cmd Returns Meaning
- * "A" | Time | Last access time for file1
- * "b" | boolean | True if file1 is a block device
- * "c" | boolean | True if file1 is a character device
- * "C" | Time | Last change time for file1
- * "d" | boolean | True if file1 exists and is a directory
- * "e" | boolean | True if file1 exists
- * "f" | boolean | True if file1 exists and is a regular file
- * "g" | boolean | True if file1 has the \CF{setgid} bit
- * | | set (false under NT)
- * "G" | boolean | True if file1 exists and has a group
- * | | ownership equal to the caller's group
- * "k" | boolean | True if file1 exists and has the sticky bit set
- * "l" | boolean | True if file1 exists and is a symbolic link
- * "M" | Time | Last modification time for file1
- * "o" | boolean | True if file1 exists and is owned by
- * | | the caller's effective uid
- * "O" | boolean | True if file1 exists and is owned by
- * | | the caller's real uid
- * "p" | boolean | True if file1 exists and is a fifo
- * "r" | boolean | True if file1 is readable by the effective
- * | | uid/gid of the caller
- * "R" | boolean | True if file is readable by the real
- * | | uid/gid of the caller
- * "s" | int/nil | If file1 has nonzero size, return the size,
- * | | otherwise return nil
- * "S" | boolean | True if file1 exists and is a socket
- * "u" | boolean | True if file1 has the setuid bit set
- * "w" | boolean | True if file1 exists and is writable by
- * | | the effective uid/gid
- * "W" | boolean | True if file1 exists and is writable by
- * | | the real uid/gid
- * "x" | boolean | True if file1 exists and is executable by
- * | | the effective uid/gid
- * "X" | boolean | True if file1 exists and is executable by
- * | | the real uid/gid
- * "z" | boolean | True if file1 exists and has a zero length
- *
- * Tests that take two files:
- *
- * "-" | boolean | True if file1 and file2 are identical
- * "=" | boolean | True if the modification times of file1
- * | | and file2 are equal
- * "<" | boolean | True if the modification time of file1
- * | | is prior to that of file2
- * ">" | boolean | True if the modification time of file1
- * | | is after that of file2
+ * test(char, path0, path1 = nil) -> object
+ *
+ * Performs a test on one or both of the <i>filesystem entities</i> at the given paths
+ * `path0` and `path1`:
+ *
+ * - Each path `path0` or `path1` points to a file, directory, device, pipe, etc.
+ * - Character `char` selects a specific test.
+ *
+ * The tests:
+ *
+ * - Each of these tests operates only on the entity at `path0`,
+ * and returns `true` or `false`;
+ * for a non-existent entity, returns `false` (does not raise exception):
+ *
+ * | Character | Test |
+ * |:------------:|:--------------------------------------------------------------------------|
+ * | <tt>'b'</tt> | Whether the entity is a block device. |
+ * | <tt>'c'</tt> | Whether the entity is a character device. |
+ * | <tt>'d'</tt> | Whether the entity is a directory. |
+ * | <tt>'e'</tt> | Whether the entity is an existing entity. |
+ * | <tt>'f'</tt> | Whether the entity is an existing regular file. |
+ * | <tt>'g'</tt> | Whether the entity's setgid bit is set. |
+ * | <tt>'G'</tt> | Whether the entity's group ownership is equal to the caller's. |
+ * | <tt>'k'</tt> | Whether the entity's sticky bit is set. |
+ * | <tt>'l'</tt> | Whether the entity is a symbolic link. |
+ * | <tt>'o'</tt> | Whether the entity is owned by the caller's effective uid. |
+ * | <tt>'O'</tt> | Like <tt>'o'</tt>, but uses the real uid (not the effective uid). |
+ * | <tt>'p'</tt> | Whether the entity is a FIFO device (named pipe). |
+ * | <tt>'r'</tt> | Whether the entity is readable by the caller's effective uid/gid. |
+ * | <tt>'R'</tt> | Like <tt>'r'</tt>, but uses the real uid/gid (not the effective uid/gid). |
+ * | <tt>'S'</tt> | Whether the entity is a socket. |
+ * | <tt>'u'</tt> | Whether the entity's setuid bit is set. |
+ * | <tt>'w'</tt> | Whether the entity is writable by the caller's effective uid/gid. |
+ * | <tt>'W'</tt> | Like <tt>'w'</tt>, but uses the real uid/gid (not the effective uid/gid). |
+ * | <tt>'x'</tt> | Whether the entity is executable by the caller's effective uid/gid. |
+ * | <tt>'X'</tt> | Like <tt>'x'</tt>, but uses the real uid/gid (not the effective uid/git). |
+ * | <tt>'z'</tt> | Whether the entity exists and is of length zero. |
+ *
+ * - This test operates only on the entity at `path0`,
+ * and returns an integer size or `nil`:
+ *
+ * | Character | Test |
+ * |:------------:|:---------------------------------------------------------------------------------------------|
+ * | <tt>'s'</tt> | Returns positive integer size if the entity exists and has non-zero length, `nil` otherwise. |
+ *
+ * - Each of these tests operates only on the entity at `path0`,
+ * and returns a Time object;
+ * raises an exception if the entity does not exist:
+ *
+ * | Character | Test |
+ * |:------------:|:---------------------------------------|
+ * | <tt>'A'</tt> | Last access time for the entity. |
+ * | <tt>'C'</tt> | Last change time for the entity. |
+ * | <tt>'M'</tt> | Last modification time for the entity. |
+ *
+ * - Each of these tests operates on the modification time (`mtime`)
+ * of each of the entities at `path0` and `path1`,
+ * and returns a `true` or `false`;
+ * returns `false` if either entity does not exist:
+ *
+ * | Character | Test |
+ * |:------------:|:----------------------------------------------------------------|
+ * | <tt>'<'</tt> | Whether the `mtime` at `path0` is less than that at `path1`. |
+ * | <tt>'='</tt> | Whether the `mtime` at `path0` is equal to that at `path1`. |
+ * | <tt>'>'</tt> | Whether the `mtime` at `path0` is greater than that at `path1`. |
+ *
+ * - This test operates on the content of each of the entities at `path0` and `path1`,
+ * and returns a `true` or `false`;
+ * returns `false` if either entity does not exist:
+ *
+ * | Character | Test |
+ * |:------------:|:----------------------------------------------|
+ * | <tt>'-'</tt> | Whether the entities exist and are identical. |
+ *
*/
static VALUE
@@ -5525,7 +6024,7 @@ rb_f_test(int argc, VALUE *argv, VALUE _)
if (strchr("=<>", cmd)) {
struct stat st1, st2;
- struct timespec t1, t2;
+ stat_timestamp t1, t2;
CHECK(2);
if (rb_stat(argv[1], &st1) < 0) return Qfalse;
@@ -5565,24 +6064,60 @@ rb_f_test(int argc, VALUE *argv, VALUE _)
/*
* Document-class: File::Stat
*
- * Objects of class File::Stat encapsulate common status information
- * for File objects. The information is recorded at the moment the
- * File::Stat object is created; changes made to the file after that
- * point will not be reflected. File::Stat objects are returned by
- * IO#stat, File::stat, File#lstat, and File::lstat. Many of these
- * methods return platform-specific values, and not all values are
- * meaningful on all systems. See also Kernel#test.
+ * A \File::Stat object contains information about an entry in the file system.
+ *
+ * Each of these methods returns a new \File::Stat object:
+ *
+ * - File#lstat.
+ * - File::Stat.new.
+ * - File::lstat.
+ * - File::stat.
+ * - IO#stat.
+ *
+ * === Snapshot
+ *
+ * A new \File::Stat object takes an immediate "snapshot" of the entry's information;
+ * the captured information is never updated,
+ * regardless of changes in the actual entry:
+ *
+ * The entry must exist when File::Stat.new is called:
+ *
+ * filepath = 't.tmp'
+ * File.exist?(filepath) # => false
+ * File::Stat.new(filepath) # Raises Errno::ENOENT: No such file or directory.
+ * File.write(filepath, 'foo') # Create the file.
+ * stat = File::Stat.new(filepath) # Okay.
+ *
+ * Later changes to the actual entry do not change the \File::Stat object:
+ *
+ * File.atime(filepath) # => 2026-04-01 11:51:38.0014518 -0500
+ * stat.atime # => 2026-04-01 11:51:38.0014518 -0500
+ * File.write(filepath, 'bar')
+ * File.atime(filepath) # => 2026-04-01 11:58:11.922614 -0500
+ * stat.atime # => 2026-04-01 11:51:38.0014518 -0500
+ * File.delete(filepath)
+ * stat.atime # => 2026-04-01 11:51:38.0014518 -0500
+ *
+ * === OS-Dependencies
+ *
+ * Methods in a \File::Stat object may return platform-dependents values,
+ * and not all values are meaningful on all systems;
+ * for example, File::Stat#blocks returns +nil+ on Windows,
+ * but returns an integer on Linux.
+ *
+ * See also Kernel#test.
*/
static VALUE
rb_stat_s_alloc(VALUE klass)
{
- return stat_new_0(klass, 0);
+ VALUE obj;
+ stat_alloc(rb_cStat, &obj);
+ return obj;
}
/*
* call-seq:
- *
* File::Stat.new(file_name) -> stat
*
* Create a File::Stat object for the given file name (raising an
@@ -5592,20 +6127,19 @@ rb_stat_s_alloc(VALUE klass)
static VALUE
rb_stat_init(VALUE obj, VALUE fname)
{
- struct stat st, *nst;
+ rb_io_stat_data st;
FilePathValue(fname);
fname = rb_str_encode_ospath(fname);
- if (STAT(StringValueCStr(fname), &st) == -1) {
+ if (STATX(StringValueCStr(fname), &st, STATX_ALL) == -1) {
rb_sys_fail_path(fname);
}
- if (DATA_PTR(obj)) {
- xfree(DATA_PTR(obj));
- DATA_PTR(obj) = NULL;
- }
- nst = ALLOC(struct stat);
- *nst = st;
- DATA_PTR(obj) = nst;
+
+ struct rb_stat *rb_st;
+ TypedData_Get_Struct(obj, struct rb_stat, &stat_data_type, rb_st);
+
+ rb_st->stat = st;
+ rb_st->initialized = true;
return Qnil;
}
@@ -5614,19 +6148,15 @@ rb_stat_init(VALUE obj, VALUE fname)
static VALUE
rb_stat_init_copy(VALUE copy, VALUE orig)
{
- struct stat *nst;
-
if (!OBJ_INIT_COPY(copy, orig)) return copy;
- if (DATA_PTR(copy)) {
- xfree(DATA_PTR(copy));
- DATA_PTR(copy) = 0;
- }
- if (DATA_PTR(orig)) {
- nst = ALLOC(struct stat);
- *nst = *(struct stat*)DATA_PTR(orig);
- DATA_PTR(copy) = nst;
- }
+ struct rb_stat *orig_rb_st;
+ TypedData_Get_Struct(orig, struct rb_stat, &stat_data_type, orig_rb_st);
+
+ struct rb_stat *copy_rb_st;
+ TypedData_Get_Struct(copy, struct rb_stat, &stat_data_type, copy_rb_st);
+
+ *copy_rb_st = *orig_rb_st;
return copy;
}
@@ -5647,7 +6177,7 @@ rb_stat_init_copy(VALUE copy, VALUE orig)
static VALUE
rb_stat_ftype(VALUE obj)
{
- return rb_file_ftype(get_stat(obj));
+ return rb_file_ftype(get_stat(obj)->ST_(mode));
}
/*
@@ -5664,7 +6194,7 @@ rb_stat_ftype(VALUE obj)
static VALUE
rb_stat_d(VALUE obj)
{
- if (S_ISDIR(get_stat(obj)->st_mode)) return Qtrue;
+ if (S_ISDIR(get_stat(obj)->ST_(mode))) return Qtrue;
return Qfalse;
}
@@ -5680,7 +6210,7 @@ static VALUE
rb_stat_p(VALUE obj)
{
#ifdef S_IFIFO
- if (S_ISFIFO(get_stat(obj)->st_mode)) return Qtrue;
+ if (S_ISFIFO(get_stat(obj)->ST_(mode))) return Qtrue;
#endif
return Qfalse;
@@ -5706,7 +6236,7 @@ static VALUE
rb_stat_l(VALUE obj)
{
#ifdef S_ISLNK
- if (S_ISLNK(get_stat(obj)->st_mode)) return Qtrue;
+ if (S_ISLNK(get_stat(obj)->ST_(mode))) return Qtrue;
#endif
return Qfalse;
}
@@ -5727,7 +6257,7 @@ static VALUE
rb_stat_S(VALUE obj)
{
#ifdef S_ISSOCK
- if (S_ISSOCK(get_stat(obj)->st_mode)) return Qtrue;
+ if (S_ISSOCK(get_stat(obj)->ST_(mode))) return Qtrue;
#endif
return Qfalse;
@@ -5750,7 +6280,7 @@ static VALUE
rb_stat_b(VALUE obj)
{
#ifdef S_ISBLK
- if (S_ISBLK(get_stat(obj)->st_mode)) return Qtrue;
+ if (S_ISBLK(get_stat(obj)->ST_(mode))) return Qtrue;
#endif
return Qfalse;
@@ -5771,7 +6301,7 @@ rb_stat_b(VALUE obj)
static VALUE
rb_stat_c(VALUE obj)
{
- if (S_ISCHR(get_stat(obj)->st_mode)) return Qtrue;
+ if (S_ISCHR(get_stat(obj)->ST_(mode))) return Qtrue;
return Qfalse;
}
@@ -5791,14 +6321,14 @@ rb_stat_c(VALUE obj)
static VALUE
rb_stat_owned(VALUE obj)
{
- if (get_stat(obj)->st_uid == geteuid()) return Qtrue;
+ if (get_stat(obj)->ST_(uid) == geteuid()) return Qtrue;
return Qfalse;
}
static VALUE
rb_stat_rowned(VALUE obj)
{
- if (get_stat(obj)->st_uid == getuid()) return Qtrue;
+ if (get_stat(obj)->ST_(uid) == getuid()) return Qtrue;
return Qfalse;
}
@@ -5807,7 +6337,7 @@ rb_stat_rowned(VALUE obj)
* stat.grpowned? -> true or false
*
* Returns true if the effective group id of the process is the same as
- * the group id of <i>stat</i>. On Windows NT, returns <code>false</code>.
+ * the group id of <i>stat</i>. On Windows, returns <code>false</code>.
*
* File.stat("testfile").grpowned? #=> true
* File.stat("/etc/passwd").grpowned? #=> false
@@ -5818,7 +6348,7 @@ static VALUE
rb_stat_grpowned(VALUE obj)
{
#ifndef _WIN32
- if (rb_group_member(get_stat(obj)->st_gid)) return Qtrue;
+ if (rb_group_member(get_stat(obj)->ST_(gid))) return Qtrue;
#endif
return Qfalse;
}
@@ -5837,21 +6367,21 @@ rb_stat_grpowned(VALUE obj)
static VALUE
rb_stat_r(VALUE obj)
{
- struct stat *st = get_stat(obj);
+ rb_io_stat_data *st = get_stat(obj);
#ifdef USE_GETEUID
if (geteuid() == 0) return Qtrue;
#endif
#ifdef S_IRUSR
if (rb_stat_owned(obj))
- return RBOOL(st->st_mode & S_IRUSR);
+ return RBOOL(st->ST_(mode) & S_IRUSR);
#endif
#ifdef S_IRGRP
if (rb_stat_grpowned(obj))
- return RBOOL(st->st_mode & S_IRGRP);
+ return RBOOL(st->ST_(mode) & S_IRGRP);
#endif
#ifdef S_IROTH
- if (!(st->st_mode & S_IROTH)) return Qfalse;
+ if (!(st->ST_(mode) & S_IROTH)) return Qfalse;
#endif
return Qtrue;
}
@@ -5870,21 +6400,21 @@ rb_stat_r(VALUE obj)
static VALUE
rb_stat_R(VALUE obj)
{
- struct stat *st = get_stat(obj);
+ rb_io_stat_data *st = get_stat(obj);
#ifdef USE_GETEUID
if (getuid() == 0) return Qtrue;
#endif
#ifdef S_IRUSR
if (rb_stat_rowned(obj))
- return RBOOL(st->st_mode & S_IRUSR);
+ return RBOOL(st->ST_(mode) & S_IRUSR);
#endif
#ifdef S_IRGRP
- if (rb_group_member(get_stat(obj)->st_gid))
- return RBOOL(st->st_mode & S_IRGRP);
+ if (rb_group_member(get_stat(obj)->ST_(gid)))
+ return RBOOL(st->ST_(mode) & S_IRGRP);
#endif
#ifdef S_IROTH
- if (!(st->st_mode & S_IROTH)) return Qfalse;
+ if (!(st->ST_(mode) & S_IROTH)) return Qfalse;
#endif
return Qtrue;
}
@@ -5906,14 +6436,12 @@ static VALUE
rb_stat_wr(VALUE obj)
{
#ifdef S_IROTH
- struct stat *st = get_stat(obj);
- if ((st->st_mode & (S_IROTH)) == S_IROTH) {
- return UINT2NUM(st->st_mode & (S_IRUGO|S_IWUGO|S_IXUGO));
- }
- else {
- return Qnil;
+ rb_io_stat_data *st = get_stat(obj);
+ if ((st->ST_(mode) & (S_IROTH)) == S_IROTH) {
+ return UINT2NUM(st->ST_(mode) & (S_IRUGO|S_IWUGO|S_IXUGO));
}
#endif
+ return Qnil;
}
/*
@@ -5930,21 +6458,21 @@ rb_stat_wr(VALUE obj)
static VALUE
rb_stat_w(VALUE obj)
{
- struct stat *st = get_stat(obj);
+ rb_io_stat_data *st = get_stat(obj);
#ifdef USE_GETEUID
if (geteuid() == 0) return Qtrue;
#endif
#ifdef S_IWUSR
if (rb_stat_owned(obj))
- return RBOOL(st->st_mode & S_IWUSR);
+ return RBOOL(st->ST_(mode) & S_IWUSR);
#endif
#ifdef S_IWGRP
if (rb_stat_grpowned(obj))
- return RBOOL(st->st_mode & S_IWGRP);
+ return RBOOL(st->ST_(mode) & S_IWGRP);
#endif
#ifdef S_IWOTH
- if (!(st->st_mode & S_IWOTH)) return Qfalse;
+ if (!(st->ST_(mode) & S_IWOTH)) return Qfalse;
#endif
return Qtrue;
}
@@ -5963,21 +6491,21 @@ rb_stat_w(VALUE obj)
static VALUE
rb_stat_W(VALUE obj)
{
- struct stat *st = get_stat(obj);
+ rb_io_stat_data *st = get_stat(obj);
#ifdef USE_GETEUID
if (getuid() == 0) return Qtrue;
#endif
#ifdef S_IWUSR
if (rb_stat_rowned(obj))
- return RBOOL(st->st_mode & S_IWUSR);
+ return RBOOL(st->ST_(mode) & S_IWUSR);
#endif
#ifdef S_IWGRP
- if (rb_group_member(get_stat(obj)->st_gid))
- return RBOOL(st->st_mode & S_IWGRP);
+ if (rb_group_member(get_stat(obj)->ST_(gid)))
+ return RBOOL(st->ST_(mode) & S_IWGRP);
#endif
#ifdef S_IWOTH
- if (!(st->st_mode & S_IWOTH)) return Qfalse;
+ if (!(st->ST_(mode) & S_IWOTH)) return Qfalse;
#endif
return Qtrue;
}
@@ -5998,15 +6526,13 @@ rb_stat_W(VALUE obj)
static VALUE
rb_stat_ww(VALUE obj)
{
-#ifdef S_IROTH
- struct stat *st = get_stat(obj);
- if ((st->st_mode & (S_IWOTH)) == S_IWOTH) {
- return UINT2NUM(st->st_mode & (S_IRUGO|S_IWUGO|S_IXUGO));
- }
- else {
- return Qnil;
+#ifdef S_IWOTH
+ rb_io_stat_data *st = get_stat(obj);
+ if ((st->ST_(mode) & (S_IWOTH)) == S_IWOTH) {
+ return UINT2NUM(st->ST_(mode) & (S_IRUGO|S_IWUGO|S_IXUGO));
}
#endif
+ return Qnil;
}
/*
@@ -6025,23 +6551,23 @@ rb_stat_ww(VALUE obj)
static VALUE
rb_stat_x(VALUE obj)
{
- struct stat *st = get_stat(obj);
+ rb_io_stat_data *st = get_stat(obj);
#ifdef USE_GETEUID
if (geteuid() == 0) {
- return RBOOL(st->st_mode & S_IXUGO);
+ return RBOOL(st->ST_(mode) & S_IXUGO);
}
#endif
#ifdef S_IXUSR
if (rb_stat_owned(obj))
- return RBOOL(st->st_mode & S_IXUSR);
+ return RBOOL(st->ST_(mode) & S_IXUSR);
#endif
#ifdef S_IXGRP
if (rb_stat_grpowned(obj))
- return RBOOL(st->st_mode & S_IXGRP);
+ return RBOOL(st->ST_(mode) & S_IXGRP);
#endif
#ifdef S_IXOTH
- if (!(st->st_mode & S_IXOTH)) return Qfalse;
+ if (!(st->ST_(mode) & S_IXOTH)) return Qfalse;
#endif
return Qtrue;
}
@@ -6057,23 +6583,23 @@ rb_stat_x(VALUE obj)
static VALUE
rb_stat_X(VALUE obj)
{
- struct stat *st = get_stat(obj);
+ rb_io_stat_data *st = get_stat(obj);
#ifdef USE_GETEUID
if (getuid() == 0) {
- return RBOOL(st->st_mode & S_IXUGO);
+ return RBOOL(st->ST_(mode) & S_IXUGO);
}
#endif
#ifdef S_IXUSR
if (rb_stat_rowned(obj))
- return RBOOL(st->st_mode & S_IXUSR);
+ return RBOOL(st->ST_(mode) & S_IXUSR);
#endif
#ifdef S_IXGRP
- if (rb_group_member(get_stat(obj)->st_gid))
- return RBOOL(st->st_mode & S_IXGRP);
+ if (rb_group_member(get_stat(obj)->ST_(gid)))
+ return RBOOL(st->ST_(mode) & S_IXGRP);
#endif
#ifdef S_IXOTH
- if (!(st->st_mode & S_IXOTH)) return Qfalse;
+ if (!(st->ST_(mode) & S_IXOTH)) return Qfalse;
#endif
return Qtrue;
}
@@ -6092,7 +6618,7 @@ rb_stat_X(VALUE obj)
static VALUE
rb_stat_f(VALUE obj)
{
- if (S_ISREG(get_stat(obj)->st_mode)) return Qtrue;
+ if (S_ISREG(get_stat(obj)->ST_(mode))) return Qtrue;
return Qfalse;
}
@@ -6110,7 +6636,7 @@ rb_stat_f(VALUE obj)
static VALUE
rb_stat_z(VALUE obj)
{
- if (get_stat(obj)->st_size == 0) return Qtrue;
+ if (get_stat(obj)->ST_(size) == 0) return Qtrue;
return Qfalse;
}
@@ -6122,14 +6648,14 @@ rb_stat_z(VALUE obj)
* the file otherwise.
*
* File.stat("testfile").size? #=> 66
- * File.stat("/dev/null").size? #=> nil
+ * File.stat(File::NULL).size? #=> nil
*
*/
static VALUE
rb_stat_s(VALUE obj)
{
- off_t size = get_stat(obj)->st_size;
+ rb_off_t size = get_stat(obj)->ST_(size);
if (size == 0) return Qnil;
return OFFT2NUM(size);
@@ -6150,7 +6676,7 @@ static VALUE
rb_stat_suid(VALUE obj)
{
#ifdef S_ISUID
- if (get_stat(obj)->st_mode & S_ISUID) return Qtrue;
+ if (get_stat(obj)->ST_(mode) & S_ISUID) return Qtrue;
#endif
return Qfalse;
}
@@ -6171,7 +6697,7 @@ static VALUE
rb_stat_sgid(VALUE obj)
{
#ifdef S_ISGID
- if (get_stat(obj)->st_mode & S_ISGID) return Qtrue;
+ if (get_stat(obj)->ST_(mode) & S_ISGID) return Qtrue;
#endif
return Qfalse;
}
@@ -6192,7 +6718,7 @@ static VALUE
rb_stat_sticky(VALUE obj)
{
#ifdef S_ISVTX
- if (get_stat(obj)->st_mode & S_ISVTX) return Qtrue;
+ if (get_stat(obj)->ST_(mode) & S_ISVTX) return Qtrue;
#endif
return Qfalse;
}
@@ -6241,7 +6767,7 @@ rb_file_s_mkfifo(int argc, VALUE *argv, VALUE _)
FilePathValue(path);
path = rb_str_encode_ospath(path);
ma.path = RSTRING_PTR(path);
- if (rb_thread_call_without_gvl(nogvl_mkfifo, &ma, RUBY_UBF_IO, 0)) {
+ if (IO_WITHOUT_GVL(nogvl_mkfifo, &ma)) {
rb_sys_fail_path(path);
}
return INT2FIX(0);
@@ -6273,95 +6799,6 @@ rb_is_absolute_path(const char *path)
return 0;
}
-#ifndef ENABLE_PATH_CHECK
-# if defined DOSISH || defined __CYGWIN__
-# define ENABLE_PATH_CHECK 0
-# else
-# define ENABLE_PATH_CHECK 1
-# endif
-#endif
-
-#if ENABLE_PATH_CHECK
-static int
-path_check_0(VALUE path)
-{
- struct stat st;
- const char *p0 = StringValueCStr(path);
- const char *e0;
- rb_encoding *enc;
- char *p = 0, *s;
-
- if (!rb_is_absolute_path(p0)) {
- char *buf = ruby_getcwd();
- VALUE newpath;
-
- newpath = rb_str_new2(buf);
- xfree(buf);
-
- rb_str_cat2(newpath, "/");
- rb_str_cat2(newpath, p0);
- path = newpath;
- p0 = RSTRING_PTR(path);
- }
- e0 = p0 + RSTRING_LEN(path);
- enc = rb_enc_get(path);
- for (;;) {
-#ifndef S_IWOTH
-# define S_IWOTH 002
-#endif
- if (STAT(p0, &st) == 0 && S_ISDIR(st.st_mode) && (st.st_mode & S_IWOTH)
-#ifdef S_ISVTX
- && !(p && (st.st_mode & S_ISVTX))
-#endif
- && !access(p0, W_OK)) {
- rb_enc_warn(enc, "Insecure world writable dir %s in PATH, mode 0%"
-#if SIZEOF_DEV_T > SIZEOF_INT
- PRI_MODET_PREFIX"o",
-#else
- "o",
-#endif
- p0, st.st_mode);
- if (p) *p = '/';
- RB_GC_GUARD(path);
- return 0;
- }
- s = strrdirsep(p0, e0, enc);
- if (p) *p = '/';
- if (!s || s == p0) return 1;
- p = s;
- e0 = p;
- *p = '\0';
- }
-}
-#endif
-
-int
-rb_path_check(const char *path)
-{
-#if ENABLE_PATH_CHECK
- const char *p0, *p, *pend;
- const char sep = PATH_SEP_CHAR;
-
- if (!path) return 1;
-
- pend = path + strlen(path);
- p0 = path;
- p = strchr(path, sep);
- if (!p) p = pend;
-
- for (;;) {
- if (!path_check_0(rb_str_new(p0, p - p0))) {
- return 0; /* not safe */
- }
- p0 = p + 1;
- if (p0 > pend) break;
- p = strchr(p0, sep);
- if (!p) p = pend;
- }
-#endif
- return 1;
-}
-
int
ruby_is_fd_loadable(int fd)
{
@@ -6405,7 +6842,11 @@ rb_file_load_ok(const char *path)
#endif
0);
int fd = rb_cloexec_open(path, mode, 0);
- if (fd == -1) return 0;
+ if (fd < 0) {
+ if (!rb_gc_for_fd(errno)) return 0;
+ fd = rb_cloexec_open(path, mode, 0);
+ if (fd < 0) return 0;
+ }
rb_update_max_fd(fd);
ret = ruby_is_fd_loadable(fd);
(void)close(fd);
@@ -6540,12 +6981,10 @@ rb_find_file(VALUE path)
return copy_path_class(tmp, path);
}
-static void
-define_filetest_function(const char *name, VALUE (*func)(ANYARGS), int argc)
-{
- rb_define_module_function(rb_mFileTest, name, func, argc);
- rb_define_singleton_method(rb_cFile, name, func, argc);
-}
+#define define_filetest_function(name, func, argc) do { \
+ rb_define_module_function(rb_mFileTest, name, func, argc); \
+ rb_define_singleton_method(rb_cFile, name, func, argc); \
+} while(false)
const char ruby_null_device[] =
#if defined DOSISH
@@ -6562,9 +7001,605 @@ const char ruby_null_device[] =
/*
* A \File object is a representation of a file in the underlying platform.
*
- * \Class \File extends module FileTest, supporting such singleton methods
+ * Class \File extends module FileTest, supporting such singleton methods
* as <tt>File.exist?</tt>.
*
+ * == About the Examples
+ *
+ * Many examples here use these variables:
+ *
+ * :include: doc/examples/files.rdoc
+ *
+ * == Access Modes
+ *
+ * Methods File.new and File.open each create a \File object for a given file path.
+ *
+ * === \String Access Modes
+ *
+ * Methods File.new and File.open each may take string argument +mode+, which:
+ *
+ * - Begins with a 1- or 2-character
+ * {read/write mode}[rdoc-ref:File@ReadWrite+Mode].
+ * - May also contain a 1-character {data mode}[rdoc-ref:File@Data+Mode].
+ * - May also contain a 1-character
+ * {file-create mode}[rdoc-ref:File@File-Create+Mode].
+ *
+ * ==== Read/Write Mode
+ *
+ * The read/write +mode+ determines:
+ *
+ * - Whether the file is to be initially truncated.
+ *
+ * - Whether reading is allowed, and if so:
+ *
+ * - The initial read position in the file.
+ * - Where in the file reading can occur.
+ *
+ * - Whether writing is allowed, and if so:
+ *
+ * - The initial write position in the file.
+ * - Where in the file writing can occur.
+ *
+ * These tables summarize:
+ *
+ * Read/Write Modes for Existing File
+ *
+ * |------|-----------|----------|----------|----------|-----------|
+ * | R/W | Initial | | Initial | | Initial |
+ * | Mode | Truncate? | Read | Read Pos | Write | Write Pos |
+ * |------|-----------|----------|----------|----------|-----------|
+ * | 'r' | No | Anywhere | 0 | Error | - |
+ * | 'w' | Yes | Error | - | Anywhere | 0 |
+ * | 'a' | No | Error | - | End only | End |
+ * | 'r+' | No | Anywhere | 0 | Anywhere | 0 |
+ * | 'w+' | Yes | Anywhere | 0 | Anywhere | 0 |
+ * | 'a+' | No | Anywhere | End | End only | End |
+ * |------|-----------|----------|----------|----------|-----------|
+ *
+ * Read/Write Modes for \File To Be Created
+ *
+ * |------|----------|----------|----------|-----------|
+ * | R/W | | Initial | | Initial |
+ * | Mode | Read | Read Pos | Write | Write Pos |
+ * |------|----------|----------|----------|-----------|
+ * | 'w' | Error | - | Anywhere | 0 |
+ * | 'a' | Error | - | End only | 0 |
+ * | 'w+' | Anywhere | 0 | Anywhere | 0 |
+ * | 'a+' | Anywhere | 0 | End only | End |
+ * |------|----------|----------|----------|-----------|
+ *
+ * Note that modes <tt>'r'</tt> and <tt>'r+'</tt> are not allowed
+ * for a non-existent file (exception raised).
+ *
+ * In the tables:
+ *
+ * - +Anywhere+ means that methods IO#rewind, IO#pos=, and IO#seek
+ * may be used to change the file's position,
+ * so that allowed reading or writing may occur anywhere in the file.
+ * - <tt>End only</tt> means that writing can occur only at end-of-file,
+ * and that methods IO#rewind, IO#pos=, and IO#seek do not affect writing.
+ * - +Error+ means that an exception is raised if disallowed reading or writing
+ * is attempted.
+ *
+ * ===== Read/Write Modes for Existing \File
+ *
+ * - <tt>'r'</tt>:
+ *
+ * - \File is not initially truncated:
+ *
+ * f = File.new('t.txt') # => #<File:t.txt>
+ * f.size == 0 # => false
+ *
+ * - File's initial read position is 0:
+ *
+ * f.pos # => 0
+ *
+ * - \File may be read anywhere; see IO#rewind, IO#pos=, IO#seek:
+ *
+ * f.readline # => "First line\n"
+ * f.readline # => "Second line\n"
+ *
+ * f.rewind
+ * f.readline # => "First line\n"
+ *
+ * f.pos = 1
+ * f.readline # => "irst line\n"
+ *
+ * f.seek(1, :CUR)
+ * f.readline # => "econd line\n"
+ *
+ * - Writing is not allowed:
+ *
+ * f.write('foo') # Raises IOError.
+ *
+ * - <tt>'w'</tt>:
+ *
+ * - \File is initially truncated:
+ *
+ * path = 't.tmp'
+ * File.write(path, text)
+ * f = File.new(path, 'w')
+ * f.size == 0 # => true
+ *
+ * - File's initial write position is 0:
+ *
+ * f.pos # => 0
+ *
+ * - \File may be written anywhere (even past end-of-file);
+ * see IO#rewind, IO#pos=, IO#seek:
+ *
+ * f.write('foo')
+ * f.flush
+ * File.read(path) # => "foo"
+ * f.pos # => 3
+ *
+ * f.write('bar')
+ * f.flush
+ * File.read(path) # => "foobar"
+ * f.pos # => 6
+ *
+ * f.rewind
+ * f.write('baz')
+ * f.flush
+ * File.read(path) # => "bazbar"
+ * f.pos # => 3
+ *
+ * f.pos = 3
+ * f.write('foo')
+ * f.flush
+ * File.read(path) # => "bazfoo"
+ * f.pos # => 6
+ *
+ * f.seek(-3, :END)
+ * f.write('bam')
+ * f.flush
+ * File.read(path) # => "bazbam"
+ * f.pos # => 6
+ *
+ * f.pos = 8
+ * f.write('bah') # Zero padding as needed.
+ * f.flush
+ * File.read(path) # => "bazbam\u0000\u0000bah"
+ * f.pos # => 11
+ *
+ * - Reading is not allowed:
+ *
+ * f.read # Raises IOError.
+ *
+ * - <tt>'a'</tt>:
+ *
+ * - \File is not initially truncated:
+ *
+ * path = 't.tmp'
+ * File.write(path, 'foo')
+ * f = File.new(path, 'a')
+ * f.size == 0 # => false
+ *
+ * - File's initial position is 0 (but is ignored):
+ *
+ * f.pos # => 0
+ *
+ * - \File may be written only at end-of-file;
+ * IO#rewind, IO#pos=, IO#seek do not affect writing:
+ *
+ * f.write('bar')
+ * f.flush
+ * File.read(path) # => "foobar"
+ * f.write('baz')
+ * f.flush
+ * File.read(path) # => "foobarbaz"
+ *
+ * f.rewind
+ * f.write('bat')
+ * f.flush
+ * File.read(path) # => "foobarbazbat"
+ *
+ * - Reading is not allowed:
+ *
+ * f.read # Raises IOError.
+ *
+ * - <tt>'r+'</tt>:
+ *
+ * - \File is not initially truncated:
+ *
+ * path = 't.tmp'
+ * File.write(path, text)
+ * f = File.new(path, 'r+')
+ * f.size == 0 # => false
+ *
+ * - File's initial read position is 0:
+ *
+ * f.pos # => 0
+ *
+ * - \File may be read or written anywhere (even past end-of-file);
+ * see IO#rewind, IO#pos=, IO#seek:
+ *
+ * f.readline # => "First line\n"
+ * f.readline # => "Second line\n"
+ *
+ * f.rewind
+ * f.readline # => "First line\n"
+ *
+ * f.pos = 1
+ * f.readline # => "irst line\n"
+ *
+ * f.seek(1, :CUR)
+ * f.readline # => "econd line\n"
+ *
+ * f.rewind
+ * f.write('WWW')
+ * f.flush
+ * File.read(path)
+ * # => "WWWst line\nSecond line\nFourth line\nFifth line\n"
+ *
+ * f.pos = 10
+ * f.write('XXX')
+ * f.flush
+ * File.read(path)
+ * # => "WWWst lineXXXecond line\nFourth line\nFifth line\n"
+ *
+ * f.seek(-6, :END)
+ * # => 0
+ * f.write('YYY')
+ * # => 3
+ * f.flush
+ * # => #<File:t.tmp>
+ * File.read(path)
+ * # => "WWWst lineXXXecond line\nFourth line\nFifth YYYe\n"
+ *
+ * f.seek(2, :END)
+ * f.write('ZZZ') # Zero padding as needed.
+ * f.flush
+ * File.read(path)
+ * # => "WWWst lineXXXecond line\nFourth line\nFifth YYYe\n\u0000\u0000ZZZ"
+ *
+ *
+ * - <tt>'a+'</tt>:
+ *
+ * - \File is not initially truncated:
+ *
+ * path = 't.tmp'
+ * File.write(path, 'foo')
+ * f = File.new(path, 'a+')
+ * f.size == 0 # => false
+ *
+ * - File's initial read position is 0:
+ *
+ * f.pos # => 0
+ *
+ * - \File may be written only at end-of-file;
+ * IO#rewind, IO#pos=, IO#seek do not affect writing:
+ *
+ * f.write('bar')
+ * f.flush
+ * File.read(path) # => "foobar"
+ * f.write('baz')
+ * f.flush
+ * File.read(path) # => "foobarbaz"
+ *
+ * f.rewind
+ * f.write('bat')
+ * f.flush
+ * File.read(path) # => "foobarbazbat"
+ *
+ * - \File may be read anywhere; see IO#rewind, IO#pos=, IO#seek:
+ *
+ * f.rewind
+ * f.read # => "foobarbazbat"
+ *
+ * f.pos = 3
+ * f.read # => "barbazbat"
+ *
+ * f.seek(-3, :END)
+ * f.read # => "bat"
+ *
+ * ===== Read/Write Modes for \File To Be Created
+ *
+ * Note that modes <tt>'r'</tt> and <tt>'r+'</tt> are not allowed
+ * for a non-existent file (exception raised).
+ *
+ * - <tt>'w'</tt>:
+ *
+ * - File's initial write position is 0:
+ *
+ * path = 't.tmp'
+ * FileUtils.rm_f(path)
+ * f = File.new(path, 'w')
+ * f.pos # => 0
+ *
+ * - \File may be written anywhere (even past end-of-file);
+ * see IO#rewind, IO#pos=, IO#seek:
+ *
+ * f.write('foo')
+ * f.flush
+ * File.read(path) # => "foo"
+ * f.pos # => 3
+ *
+ * f.write('bar')
+ * f.flush
+ * File.read(path) # => "foobar"
+ * f.pos # => 6
+ *
+ * f.rewind
+ * f.write('baz')
+ * f.flush
+ * File.read(path) # => "bazbar"
+ * f.pos # => 3
+ *
+ * f.pos = 3
+ * f.write('foo')
+ * f.flush
+ * File.read(path) # => "bazfoo"
+ * f.pos # => 6
+ *
+ * f.seek(-3, :END)
+ * f.write('bam')
+ * f.flush
+ * File.read(path) # => "bazbam"
+ * f.pos # => 6
+ *
+ * f.pos = 8
+ * f.write('bah') # Zero padding as needed.
+ * f.flush
+ * File.read(path) # => "bazbam\u0000\u0000bah"
+ * f.pos # => 11
+ *
+ * - Reading is not allowed:
+ *
+ * f.read # Raises IOError.
+ *
+ * - <tt>'a'</tt>:
+ *
+ * - File's initial write position is 0:
+ *
+ * path = 't.tmp'
+ * FileUtils.rm_f(path)
+ * f = File.new(path, 'a')
+ * f.pos # => 0
+ *
+ * - Writing occurs only at end-of-file:
+ *
+ * f.write('foo')
+ * f.pos # => 3
+ * f.write('bar')
+ * f.pos # => 6
+ * f.flush
+ * File.read(path) # => "foobar"
+ *
+ * f.rewind
+ * f.write('baz')
+ * f.flush
+ * File.read(path) # => "foobarbaz"
+ *
+ * - Reading is not allowed:
+ *
+ * f.read # Raises IOError.
+ *
+ * - <tt>'w+'</tt>:
+ *
+ * - File's initial position is 0:
+ *
+ * path = 't.tmp'
+ * FileUtils.rm_f(path)
+ * f = File.new(path, 'w+')
+ * f.pos # => 0
+ *
+ * - \File may be written anywhere (even past end-of-file);
+ * see IO#rewind, IO#pos=, IO#seek:
+ *
+ * f.write('foo')
+ * f.flush
+ * File.read(path) # => "foo"
+ * f.pos # => 3
+ *
+ * f.write('bar')
+ * f.flush
+ * File.read(path) # => "foobar"
+ * f.pos # => 6
+ *
+ * f.rewind
+ * f.write('baz')
+ * f.flush
+ * File.read(path) # => "bazbar"
+ * f.pos # => 3
+ *
+ * f.pos = 3
+ * f.write('foo')
+ * f.flush
+ * File.read(path) # => "bazfoo"
+ * f.pos # => 6
+ *
+ * f.seek(-3, :END)
+ * f.write('bam')
+ * f.flush
+ * File.read(path) # => "bazbam"
+ * f.pos # => 6
+ *
+ * f.pos = 8
+ * f.write('bah') # Zero padding as needed.
+ * f.flush
+ * File.read(path) # => "bazbam\u0000\u0000bah"
+ * f.pos # => 11
+ *
+ * - \File may be read anywhere (even past end-of-file);
+ * see IO#rewind, IO#pos=, IO#seek:
+ *
+ * f.rewind
+ * # => 0
+ * f.read
+ * # => "bazbam\u0000\u0000bah"
+ *
+ * f.pos = 3
+ * # => 3
+ * f.read
+ * # => "bam\u0000\u0000bah"
+ *
+ * f.seek(-3, :END)
+ * # => 0
+ * f.read
+ * # => "bah"
+ *
+ * - <tt>'a+'</tt>:
+ *
+ * - File's initial write position is 0:
+ *
+ * path = 't.tmp'
+ * FileUtils.rm_f(path)
+ * f = File.new(path, 'a+')
+ * f.pos # => 0
+ *
+ * - Writing occurs only at end-of-file:
+ *
+ * f.write('foo')
+ * f.pos # => 3
+ * f.write('bar')
+ * f.pos # => 6
+ * f.flush
+ * File.read(path) # => "foobar"
+ *
+ * f.rewind
+ * f.write('baz')
+ * f.flush
+ * File.read(path) # => "foobarbaz"
+ *
+ * - \File may be read anywhere (even past end-of-file);
+ * see IO#rewind, IO#pos=, IO#seek:
+ *
+ * f.rewind
+ * f.read # => "foobarbaz"
+ *
+ * f.pos = 3
+ * f.read # => "barbaz"
+ *
+ * f.seek(-3, :END)
+ * f.read # => "baz"
+ *
+ * f.pos = 800
+ * f.read # => ""
+ *
+ * ==== \Data Mode
+ *
+ * To specify whether data is to be treated as text or as binary data,
+ * either of the following may be suffixed to any of the string read/write modes
+ * above:
+ *
+ * - <tt>'t'</tt>: Text data; sets the default external encoding
+ * to <tt>Encoding::UTF_8</tt>;
+ * on Windows, enables conversion between EOL and CRLF
+ * and enables interpreting <tt>0x1A</tt> as an end-of-file marker.
+ * - <tt>'b'</tt>: Binary data; sets the default external encoding
+ * to <tt>Encoding::ASCII_8BIT</tt>;
+ * on Windows, suppresses conversion between EOL and CRLF
+ * and disables interpreting <tt>0x1A</tt> as an end-of-file marker.
+ *
+ * If neither is given, the stream defaults to text data.
+ *
+ * Examples:
+ *
+ * File.new('t.txt', 'rt')
+ * File.new('t.dat', 'rb')
+ *
+ * When the data mode is specified, the read/write mode may not be omitted,
+ * and the data mode must precede the file-create mode, if given:
+ *
+ * File.new('t.dat', 'b') # Raises an exception.
+ * File.new('t.dat', 'rxb') # Raises an exception.
+ *
+ * ==== \File-Create Mode
+ *
+ * The following may be suffixed to any writable string mode above:
+ *
+ * - <tt>'x'</tt>: Creates the file if it does not exist;
+ * raises an exception if the file exists.
+ *
+ * Example:
+ *
+ * File.new('t.tmp', 'wx')
+ *
+ * When the file-create mode is specified, the read/write mode may not be omitted,
+ * and the file-create mode must follow the data mode:
+ *
+ * File.new('t.dat', 'x') # Raises an exception.
+ * File.new('t.dat', 'rxb') # Raises an exception.
+ *
+ * === \Integer Access Modes
+ *
+ * When mode is an integer it must be one or more of the following constants,
+ * which may be combined by the bitwise OR operator <tt>|</tt>:
+ *
+ * - +File::RDONLY+: Open for reading only.
+ * - +File::WRONLY+: Open for writing only.
+ * - +File::RDWR+: Open for reading and writing.
+ * - +File::APPEND+: Open for appending only.
+ *
+ * Examples:
+ *
+ * File.new('t.txt', File::RDONLY)
+ * File.new('t.tmp', File::RDWR | File::CREAT | File::EXCL)
+ *
+ * Note: Method IO#set_encoding does not allow the mode to be specified as an integer.
+ *
+ * === File-Create Mode Specified as an \Integer
+ *
+ * These constants may also be ORed into the integer mode:
+ *
+ * - +File::CREAT+: Create file if it does not exist.
+ * - +File::EXCL+: Raise an exception if +File::CREAT+ is given and the file exists.
+ *
+ * === \Data Mode Specified as an \Integer
+ *
+ * \Data mode cannot be specified as an integer.
+ * When the stream access mode is given as an integer,
+ * the data mode is always text, never binary.
+ *
+ * Note that although there is a constant +File::BINARY+,
+ * setting its value in an integer stream mode has no effect;
+ * this is because, as documented in File::Constants,
+ * the +File::BINARY+ value disables line code conversion,
+ * but does not change the external encoding.
+ *
+ * === Encodings
+ *
+ * Any of the string modes above may specify encodings -
+ * either external encoding only or both external and internal encodings -
+ * by appending one or both encoding names, separated by colons:
+ *
+ * f = File.new('t.dat', 'rb')
+ * f.external_encoding # => #<Encoding:ASCII-8BIT>
+ * f.internal_encoding # => nil
+ * f = File.new('t.dat', 'rb:UTF-16')
+ * f.external_encoding # => #<Encoding:UTF-16 (dummy)>
+ * f.internal_encoding # => nil
+ * f = File.new('t.dat', 'rb:UTF-16:UTF-16')
+ * f.external_encoding # => #<Encoding:UTF-16 (dummy)>
+ * f.internal_encoding # => #<Encoding:UTF-16>
+ * f.close
+ *
+ * The numerous encoding names are available in array Encoding.name_list:
+ *
+ * Encoding.name_list.take(3) # => ["ASCII-8BIT", "UTF-8", "US-ASCII"]
+ *
+ * When the external encoding is set, strings read are tagged by that encoding
+ * when reading, and strings written are converted to that encoding when
+ * writing.
+ *
+ * When both external and internal encodings are set,
+ * strings read are converted from external to internal encoding,
+ * and strings written are converted from internal to external encoding.
+ * For further details about transcoding input and output,
+ * see {Encodings}[rdoc-ref:encodings.rdoc@Encodings].
+ *
+ * If the external encoding is <tt>'BOM|UTF-8'</tt>, <tt>'BOM|UTF-16LE'</tt>
+ * or <tt>'BOM|UTF16-BE'</tt>,
+ * Ruby checks for a Unicode BOM in the input document
+ * to help determine the encoding.
+ * For UTF-16 encodings the file open mode must be binary.
+ * If the BOM is found,
+ * it is stripped and the external encoding from the BOM is used.
+ *
+ * Note that the BOM-style encoding option is case insensitive,
+ * so <tt>'bom|utf-8'</tt> is also valid.
+ *
* == \File Permissions
*
* A \File object has _permissions_, an octal integer representing
@@ -6572,7 +7607,6 @@ const char ruby_null_device[] =
*
* Note that file permissions are quite different from the _mode_
* of a file stream (\File object).
- * See IO@Modes.
*
* In a \File object, the permissions are available thus,
* where method +mode+, despite its name, returns permissions:
@@ -6616,47 +7650,19 @@ const char ruby_null_device[] =
* f.chmod(0644)
* f.chmod(0444)
*
- * == \File Constants
+ * == \File \Constants
*
- * Various constants for use in \File and \IO methods
+ * Various constants for use in \File and IO methods
* may be found in module File::Constants;
* an array of their names is returned by <tt>File::Constants.constants</tt>.
*
- * == Example Files
- *
- * Many examples here use these filenames and their corresponding files:
- *
- * - <tt>t.txt</tt>: A text-only file that is assumed to exist via:
- *
- * text = <<~EOT
- * First line
- * Second line
- *
- * Fourth line
- * Fifth line
- * EOT
- * File.write('t.txt', text)
- *
- * - <tt>t.dat</tt>: A data file that is assumed to exist via:
- *
- * data = "\u9990\u9991\u9992\u9993\u9994"
- * f = File.open('t.dat', 'wb:UTF-16')
- * f.write(data)
- * f.close
- *
- * - <tt>t.rus</tt>: A Russian-language text file that is assumed to exist via:
- *
- * File.write('t.rus', "\u{442 435 441 442}")
- *
- * - <tt>t.tmp</tt>: A file that is assumed _not_ to exist.
- *
* == What's Here
*
- * First, what's elsewhere. \Class \File:
+ * First, what's elsewhere. Class \File:
*
- * - Inherits from {class IO}[rdoc-ref:IO@What-27s+Here],
+ * - Inherits from {class IO}[rdoc-ref:IO@Whats+Here],
* in particular, methods for creating, reading, and writing files
- * - Includes {module FileTest}[rdoc-ref:FileTest@What-27s+Here].
+ * - Includes module FileTest,
* which provides dozens of additional methods.
*
* Here, class \File provides methods that are useful for:
@@ -6701,22 +7707,22 @@ const char ruby_null_device[] =
*
* _Times_
*
- * - ::atime: Returns a \Time for the most recent access to the given file.
- * - ::birthtime: Returns a \Time for the creation of the given file.
- * - ::ctime: Returns a \Time for the metadata change of the given file.
- * - ::mtime: Returns a \Time for the most recent data modification to
+ * - ::atime: Returns a Time for the most recent access to the given file.
+ * - ::birthtime: Returns a Time for the creation of the given file.
+ * - ::ctime: Returns a Time for the metadata change of the given file.
+ * - ::mtime: Returns a Time for the most recent data modification to
* the content of the given file.
- * - #atime: Returns a \Time for the most recent access to +self+.
- * - #birthtime: Returns a \Time the creation for +self+.
- * - #ctime: Returns a \Time for the metadata change of +self+.
- * - #mtime: Returns a \Time for the most recent data modification
+ * - #atime: Returns a Time for the most recent access to +self+.
+ * - #birthtime: Returns a Time the creation for +self+.
+ * - #ctime: Returns a Time for the metadata change of +self+.
+ * - #mtime: Returns a Time for the most recent data modification
* to the content of +self+.
*
* _Types_
*
* - ::blockdev?: Returns whether the file at the given path is a block device.
* - ::chardev?: Returns whether the file at the given path is a character device.
- * - ::directory?: Returns whether the file at the given path is a diretory.
+ * - ::directory?: Returns whether the file at the given path is a directory.
* - ::executable?: Returns whether the file at the given path is executable
* by the effective user and group of the current process.
* - ::executable_real?: Returns whether the file at the given path is executable
@@ -6787,6 +7793,10 @@ const char ruby_null_device[] =
void
Init_File(void)
{
+#if defined(__APPLE__) && defined(HAVE_WORKING_FORK)
+ rb_CFString_class_initialize_before_fork();
+#endif
+
VALUE separator;
rb_mFileTest = rb_define_module("FileTest");
@@ -6865,7 +7875,7 @@ Init_File(void)
/* separates directory parts in path */
rb_define_const(rb_cFile, "SEPARATOR", separator);
rb_define_singleton_method(rb_cFile, "split", rb_file_s_split, 1);
- rb_define_singleton_method(rb_cFile, "join", rb_file_s_join, -2);
+ rb_define_singleton_method(rb_cFile, "join", rb_file_s_join, -1);
#ifdef DOSISH
/* platform specific alternative separator */
@@ -6894,97 +7904,402 @@ Init_File(void)
/*
* Document-module: File::Constants
*
- * File::Constants provides file-related constants. All possible
- * file constants are listed in the documentation but they may not all
- * be present on your platform.
+ * Module +File::Constants+ defines file-related constants.
+ *
+ * There are two families of constants here:
+ *
+ * - Those having to do with {file access}[rdoc-ref:File::Constants@File+Access].
+ * - Those having to do with {filename globbing}[rdoc-ref:File::Constants@Filename+Globbing+Constants+-28File-3A-3AFNM_-2A-29].
+ *
+ * \File constants defined for the local process may be retrieved
+ * with method File::Constants.constants:
+ *
+ * File::Constants.constants.take(5)
+ * # => [:RDONLY, :WRONLY, :RDWR, :APPEND, :CREAT]
+ *
+ * == \File Access
+ *
+ * \File-access constants may be used with optional argument +mode+ in calls
+ * to the following methods:
+ *
+ * - File.new.
+ * - File.open.
+ * - IO.for_fd.
+ * - IO.new.
+ * - IO.open.
+ * - IO.popen.
+ * - IO.reopen.
+ * - IO.sysopen.
+ * - StringIO.new.
+ * - StringIO.open.
+ * - StringIO#reopen.
+ *
+ * === Read/Write Access
+ *
+ * Read-write access for a stream
+ * may be specified by a file-access constant.
+ *
+ * The constant may be specified as part of a bitwise OR of other such constants.
+ *
+ * Any combination of the constants in this section may be specified.
+ *
+ * ==== File::RDONLY
+ *
+ * Flag File::RDONLY specifies the stream should be opened for reading only:
+ *
+ * filepath = '/tmp/t.tmp'
+ * f = File.new(filepath, File::RDONLY)
+ * f.write('Foo') # Raises IOError (not opened for writing).
+ *
+ * ==== File::WRONLY
+ *
+ * Flag File::WRONLY specifies that the stream should be opened for writing only:
+ *
+ * f = File.new(filepath, File::WRONLY)
+ * f.read # Raises IOError (not opened for reading).
+ *
+ * ==== File::RDWR
+ *
+ * Flag File::RDWR specifies that the stream should be opened
+ * for both reading and writing:
+ *
+ * f = File.new(filepath, File::RDWR)
+ * f.write('Foo') # => 3
+ * f.rewind # => 0
+ * f.read # => "Foo"
+ *
+ * === \File Positioning
+ *
+ * ==== File::APPEND
+ *
+ * Flag File::APPEND specifies that the stream should be opened
+ * in append mode.
+ *
+ * Before each write operation, the position is set to end-of-stream.
+ * The modification of the position and the following write operation
+ * are performed as a single atomic step.
+ *
+ * ==== File::TRUNC
+ *
+ * Flag File::TRUNC specifies that the stream should be truncated
+ * at its beginning.
+ * If the file exists and is successfully opened for writing,
+ * it is to be truncated to position zero;
+ * its ctime and mtime are updated.
+ *
+ * There is no effect on a FIFO special file or a terminal device.
+ * The effect on other file types is implementation-defined.
+ * The result of using File::TRUNC with File::RDONLY is undefined.
+ *
+ * === Creating and Preserving
+ *
+ * ==== File::CREAT
+ *
+ * Flag File::CREAT specifies that the stream should be created
+ * if it does not already exist.
+ *
+ * If the file exists:
+ *
+ * - Raise an exception if File::EXCL is also specified.
+ * - Otherwise, do nothing.
+ *
+ * If the file does not exist, then it is created.
+ * Upon successful completion, the atime, ctime, and mtime of the file are updated,
+ * and the ctime and mtime of the parent directory are updated.
+ *
+ * ==== File::EXCL
+ *
+ * Flag File::EXCL specifies that the stream should not already exist;
+ * If flags File::CREAT and File::EXCL are both specified
+ * and the stream already exists, an exception is raised.
+ *
+ * The check for the existence and creation of the file is performed as an
+ * atomic operation.
+ *
+ * If both File::EXCL and File::CREAT are specified and the path names a symbolic link,
+ * an exception is raised regardless of the contents of the symbolic link.
+ *
+ * If File::EXCL is specified and File::CREAT is not specified,
+ * the result is undefined.
+ *
+ * === POSIX \File \Constants
+ *
+ * Some file-access constants are defined only on POSIX-compliant systems;
+ * those are:
+ *
+ * - File::SYNC.
+ * - File::DSYNC.
+ * - File::RSYNC.
+ * - File::DIRECT.
+ * - File::NOATIME.
+ * - File::NOCTTY.
+ * - File::NOFOLLOW.
+ * - File::TMPFILE.
+ *
+ * ==== File::SYNC, File::RSYNC, and File::DSYNC
+ *
+ * Flag File::SYNC, File::RSYNC, or File::DSYNC
+ * specifies synchronization of I/O operations with the underlying file system.
+ *
+ * These flags are valid only for POSIX-compliant systems.
+ *
+ * - File::SYNC specifies that all write operations (both data and metadata)
+ * are immediately to be flushed to the underlying storage device.
+ * This means that the data is written to the storage device,
+ * and the file's metadata (e.g., file size, timestamps, permissions)
+ * are also synchronized.
+ * This guarantees that data is safely stored on the storage medium
+ * before returning control to the calling program.
+ * This flag can have a significant impact on performance
+ * since it requires synchronous writes, which can be slower
+ * compared to asynchronous writes.
+ *
+ * - File::RSYNC specifies that any read operations on the file will not return
+ * until all outstanding write operations
+ * (those that have been issued but not completed) are also synchronized.
+ * This is useful when you want to read the most up-to-date data,
+ * which may still be in the process of being written.
+ *
+ * - File::DSYNC specifies that all _data_ write operations
+ * are immediately to be flushed to the underlying storage device;
+ * this differs from File::SYNC, which requires that _metadata_
+ * also be synchronized.
+ *
+ * Note that the behavior of these flags may vary slightly
+ * depending on the operating system and filesystem being used.
+ * Additionally, using these flags can have an impact on performance
+ * due to the synchronous nature of the I/O operations,
+ * so they should be used judiciously,
+ * especially in performance-critical applications.
+ *
+ * ==== File::NOCTTY
+ *
+ * Flag File::NOCTTY specifies that if the stream is a terminal device,
+ * that device does not become the controlling terminal for the process.
+ *
+ * Defined only for POSIX-compliant systems.
+ *
+ * ==== File::DIRECT
+ *
+ * Flag File::DIRECT requests that cache effects of the I/O to and from the stream
+ * be minimized.
*
- * If the underlying platform doesn't define a constant the corresponding
- * Ruby constant is not defined.
+ * Defined only for POSIX-compliant systems.
+ *
+ * ==== File::NOATIME
+ *
+ * Flag File::NOATIME specifies that act of opening the stream
+ * should not modify its access time (atime).
+ *
+ * Defined only for POSIX-compliant systems.
+ *
+ * ==== File::NOFOLLOW
+ *
+ * Flag File::NOFOLLOW specifies that if path is a symbolic link,
+ * it should not be followed.
+ *
+ * Defined only for POSIX-compliant systems.
+ *
+ * ==== File::TMPFILE
+ *
+ * Flag File::TMPFILE specifies that the opened stream
+ * should be a new temporary file.
+ *
+ * Defined only for POSIX-compliant systems.
+ *
+ * === Other File-Access \Constants
+ *
+ * ==== File::NONBLOCK
+ *
+ * When possible, the file is opened in nonblocking mode.
+ * Neither the open operation nor any subsequent I/O operations on
+ * the file will cause the calling process to wait.
+ *
+ * ==== File::BINARY
+ *
+ * Flag File::BINARY specifies that the stream is to be accessed in binary mode.
+ *
+ * ==== File::SHARE_DELETE
+ *
+ * Flag File::SHARE_DELETE enables other processes to open the stream
+ * with delete access.
+ *
+ * Windows only.
+ *
+ * If the stream is opened for (local) delete access without File::SHARE_DELETE,
+ * and another process attempts to open it with delete access,
+ * the attempt fails and the stream is not opened for that process.
+ *
+ * == Locking
+ *
+ * Four file constants relate to stream locking;
+ * see File#flock:
+ *
+ * ==== File::LOCK_EX
+ *
+ * Flag File::LOCK_EX specifies an exclusive lock;
+ * only one process a a time may lock the stream.
+ *
+ * ==== File::LOCK_NB
+ *
+ * Flag File::LOCK_NB specifies non-blocking locking for the stream;
+ * may be combined with File::LOCK_EX or File::LOCK_SH.
+ *
+ * ==== File::LOCK_SH
+ *
+ * Flag File::LOCK_SH specifies that multiple processes may lock
+ * the stream at the same time.
+ *
+ * ==== File::LOCK_UN
+ *
+ * Flag File::LOCK_UN specifies that the stream is not to be locked.
+ *
+ * == Filename Globbing \Constants (File::FNM_*)
+ *
+ * Filename-globbing constants may be used with optional argument +flags+
+ * in calls to the following methods:
+ *
+ * - Dir.glob.
+ * - File.fnmatch.
+ * - Pathname#fnmatch.
+ * - Pathname.glob.
+ * - Pathname#glob.
+ *
+ * The constants are:
+ *
+ * ==== File::FNM_CASEFOLD
+ *
+ * Flag File::FNM_CASEFOLD makes patterns case insensitive
+ * for File.fnmatch (but not Dir.glob).
+ *
+ * ==== File::FNM_DOTMATCH
+ *
+ * Flag File::FNM_DOTMATCH makes the <tt>'*'</tt> pattern
+ * match a filename starting with <tt>'.'</tt>.
+ *
+ * ==== File::FNM_EXTGLOB
+ *
+ * Flag File::FNM_EXTGLOB enables pattern <tt>'{a,b}'</tt>,
+ * which matches pattern '_a_' and pattern '_b_';
+ * behaves like
+ * a {regexp union}[rdoc-ref:Regexp.union]
+ * (e.g., <tt>'(?:a|b)'</tt>):
+ *
+ * pattern = '{LEGAL,BSDL}'
+ * Dir.glob(pattern) # => ["LEGAL", "BSDL"]
+ * Pathname.glob(pattern) # => [#<Pathname:LEGAL>, #<Pathname:BSDL>]
+ * pathname.glob(pattern) # => [#<Pathname:LEGAL>, #<Pathname:BSDL>]
+ *
+ * ==== File::FNM_NOESCAPE
+ *
+ * Flag File::FNM_NOESCAPE disables <tt>'\'</tt> escaping.
+ *
+ * ==== File::FNM_PATHNAME
+ *
+ * Flag File::FNM_PATHNAME specifies that patterns <tt>'*'</tt> and <tt>'?'</tt>
+ * do not match the directory separator
+ * (the value of constant File::SEPARATOR).
+ *
+ * ==== File::FNM_SHORTNAME
+ *
+ * Flag File::FNM_SHORTNAME allows patterns to match short names if they exist.
+ *
+ * Windows only.
+ *
+ * ==== File::FNM_SYSCASE
+ *
+ * Flag File::FNM_SYSCASE specifies that case sensitivity
+ * is the same as in the underlying operating system;
+ * effective for File.fnmatch, but not Dir.glob.
+ *
+ * == Other \Constants
+ *
+ * ==== File::NULL
+ *
+ * Flag File::NULL contains the string value of the null device:
+ *
+ * - On a Unix-like OS, <tt>'/dev/null'</tt>.
+ * - On Windows, <tt>'NUL'</tt>.
*
- * Your platform documentations (e.g. man open(2)) may describe more
- * detailed information.
*/
rb_mFConst = rb_define_module_under(rb_cFile, "Constants");
rb_include_module(rb_cIO, rb_mFConst);
-
- /* open for reading only */
+ /* {File::RDONLY}[rdoc-ref:File::Constants@File-3A-3ARDONLY] */
rb_define_const(rb_mFConst, "RDONLY", INT2FIX(O_RDONLY));
- /* open for writing only */
+ /* {File::WRONLY}[rdoc-ref:File::Constants@File-3A-3AWRONLY] */
rb_define_const(rb_mFConst, "WRONLY", INT2FIX(O_WRONLY));
- /* open for reading and writing */
+ /* {File::RDWR}[rdoc-ref:File::Constants@File-3A-3ARDWR] */
rb_define_const(rb_mFConst, "RDWR", INT2FIX(O_RDWR));
- /* append on each write */
+ /* {File::APPEND}[rdoc-ref:File::Constants@File-3A-3AAPPEND] */
rb_define_const(rb_mFConst, "APPEND", INT2FIX(O_APPEND));
- /* create file if it does not exist */
+ /* {File::CREAT}[rdoc-ref:File::Constants@File-3A-3ACREAT] */
rb_define_const(rb_mFConst, "CREAT", INT2FIX(O_CREAT));
- /* error if CREAT and the file exists */
+ /* {File::EXCL}[rdoc-ref:File::Constants@File-3A-3AEXCL] */
rb_define_const(rb_mFConst, "EXCL", INT2FIX(O_EXCL));
#if defined(O_NDELAY) || defined(O_NONBLOCK)
# ifndef O_NONBLOCK
# define O_NONBLOCK O_NDELAY
# endif
- /* do not block on open or for data to become available */
+ /* {File::NONBLOCK}[rdoc-ref:File::Constants@File-3A-3ANONBLOCK] */
rb_define_const(rb_mFConst, "NONBLOCK", INT2FIX(O_NONBLOCK));
#endif
- /* truncate size to 0 */
+ /* {File::TRUNC}[rdoc-ref:File::Constants@File-3A-3ATRUNC] */
rb_define_const(rb_mFConst, "TRUNC", INT2FIX(O_TRUNC));
#ifdef O_NOCTTY
- /* not to make opened IO the controlling terminal device */
+ /* {File::NOCTTY}[rdoc-ref:File::Constants@File-3A-3ANOCTTY] */
rb_define_const(rb_mFConst, "NOCTTY", INT2FIX(O_NOCTTY));
#endif
#ifndef O_BINARY
# define O_BINARY 0
#endif
- /* disable line code conversion */
+ /* {File::BINARY}[rdoc-ref:File::Constants@File-3A-3ABINARY] */
rb_define_const(rb_mFConst, "BINARY", INT2FIX(O_BINARY));
#ifndef O_SHARE_DELETE
# define O_SHARE_DELETE 0
#endif
- /* can delete opened file */
+ /* {File::SHARE_DELETE}[rdoc-ref:File::Constants@File-3A-3ASHARE_DELETE] */
rb_define_const(rb_mFConst, "SHARE_DELETE", INT2FIX(O_SHARE_DELETE));
#ifdef O_SYNC
- /* any write operation perform synchronously */
+ /* {File::SYNC}[rdoc-ref:File::Constants@File-3A-3ASYNC-2C+File-3A-3ARSYNC-2C+and+File-3A-3ADSYNC] */
rb_define_const(rb_mFConst, "SYNC", INT2FIX(O_SYNC));
#endif
#ifdef O_DSYNC
- /* any write operation perform synchronously except some meta data */
+ /* {File::DSYNC}[rdoc-ref:File::Constants@File-3A-3ASYNC-2C+File-3A-3ARSYNC-2C+and+File-3A-3ADSYNC] */
rb_define_const(rb_mFConst, "DSYNC", INT2FIX(O_DSYNC));
#endif
#ifdef O_RSYNC
- /* any read operation perform synchronously. used with SYNC or DSYNC. */
+ /* {File::RSYNC}[rdoc-ref:File::Constants@File-3A-3ASYNC-2C+File-3A-3ARSYNC-2C+and+File-3A-3ADSYNC] */
rb_define_const(rb_mFConst, "RSYNC", INT2FIX(O_RSYNC));
#endif
#ifdef O_NOFOLLOW
- /* do not follow symlinks */
+ /* {File::NOFOLLOW}[rdoc-ref:File::Constants@File-3A-3ANOFOLLOW] */
rb_define_const(rb_mFConst, "NOFOLLOW", INT2FIX(O_NOFOLLOW)); /* FreeBSD, Linux */
#endif
#ifdef O_NOATIME
- /* do not change atime */
+ /* {File::NOATIME}[rdoc-ref:File::Constants@File-3A-3ANOATIME] */
rb_define_const(rb_mFConst, "NOATIME", INT2FIX(O_NOATIME)); /* Linux */
#endif
#ifdef O_DIRECT
- /* Try to minimize cache effects of the I/O to and from this file. */
+ /* {File::DIRECT}[rdoc-ref:File::Constants@File-3A-3ADIRECT] */
rb_define_const(rb_mFConst, "DIRECT", INT2FIX(O_DIRECT));
#endif
#ifdef O_TMPFILE
- /* Create an unnamed temporary file */
+ /* {File::TMPFILE}[rdoc-ref:File::Constants@File-3A-3ATMPFILE] */
rb_define_const(rb_mFConst, "TMPFILE", INT2FIX(O_TMPFILE));
#endif
- /* shared lock. see File#flock */
+ /* {File::LOCK_SH}[rdoc-ref:File::Constants@File-3A-3ALOCK_SH] */
rb_define_const(rb_mFConst, "LOCK_SH", INT2FIX(LOCK_SH));
- /* exclusive lock. see File#flock */
+ /* {File::LOCK_EX}[rdoc-ref:File::Constants@File-3A-3ALOCK_EX] */
rb_define_const(rb_mFConst, "LOCK_EX", INT2FIX(LOCK_EX));
- /* unlock. see File#flock */
+ /* {File::LOCK_UN}[rdoc-ref:File::Constants@File-3A-3ALOCK_UN] */
rb_define_const(rb_mFConst, "LOCK_UN", INT2FIX(LOCK_UN));
- /* non-blocking lock. used with LOCK_SH or LOCK_EX. see File#flock */
+ /* {File::LOCK_NB}[rdoc-ref:File::Constants@File-3A-3ALOCK_NB] */
rb_define_const(rb_mFConst, "LOCK_NB", INT2FIX(LOCK_NB));
- /* Name of the null device */
+ /* {File::NULL}[rdoc-ref:File::Constants@File-3A-3ANULL] */
rb_define_const(rb_mFConst, "NULL", rb_fstring_cstr(ruby_null_device));
- rb_define_method(rb_cFile, "path", rb_file_path, 0);
- rb_define_method(rb_cFile, "to_path", rb_file_path, 0);
rb_define_global_function("test", rb_f_test, -1);
rb_cStat = rb_define_class_under(rb_cFile, "Stat", rb_cObject);