diff options
Diffstat (limited to 'file.c')
| -rw-r--r-- | file.c | 3331 |
1 files changed, 2323 insertions, 1008 deletions
@@ -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); |
