diff options
Diffstat (limited to 'file.c')
| -rw-r--r-- | file.c | 8490 |
1 files changed, 5989 insertions, 2501 deletions
@@ -3,37 +3,55 @@ file.c - $Author$ - $Date$ created at: Mon Nov 15 12:24:34 JST 1993 - Copyright (C) 1993-2003 Yukihiro Matsumoto + Copyright (C) 1993-2007 Yukihiro Matsumoto Copyright (C) 2000 Network Applied Communication Laboratory, Inc. Copyright (C) 2000 Information-technology Promotion Agency, Japan **********************************************************************/ +#include "ruby/internal/config.h" +#include "ruby/internal/attr/nonstring.h" + #ifdef _WIN32 -#include "missing/file.h" +# include "missing/file.h" +# include "ruby.h" #endif + +#include <ctype.h> +#include <time.h> + #ifdef __CYGWIN__ -#include <windows.h> -#include <sys/cygwin.h> +# include <windows.h> +# include <sys/cygwin.h> +# include <wchar.h> +#endif + +#ifdef __APPLE__ +# if !(defined(__has_feature) && defined(__has_attribute)) +/* Maybe a bug in SDK of Xcode 10.2.1 */ +/* In this condition, <os/availability.h> does not define + * API_AVAILABLE and similar, but __API_AVAILABLE and similar which + * are defined in <Availability.h> */ +# define API_AVAILABLE(...) +# define API_DEPRECATED(...) +# endif +# include <CoreFoundation/CFString.h> #endif -#include "ruby.h" -#include "rubyio.h" -#include "rubysig.h" -#include "util.h" -#include "dln.h" - #ifdef HAVE_UNISTD_H -#include <unistd.h> +# include <unistd.h> +#endif + +#ifdef HAVE_SYS_TIME_H +# include <sys/time.h> #endif #ifdef HAVE_SYS_FILE_H # include <sys/file.h> #else -int flock _((int, int)); +int flock(int, int); #endif #ifdef HAVE_SYS_PARAM_H @@ -43,223 +61,665 @@ int flock _((int, int)); # define MAXPATHLEN 1024 #endif -#include <time.h> - -VALUE rb_time_new _((time_t, time_t)); - #ifdef HAVE_UTIME_H -#include <utime.h> +# include <utime.h> #elif defined HAVE_SYS_UTIME_H -#include <sys/utime.h> +# include <sys/utime.h> #endif #ifdef HAVE_PWD_H -#include <pwd.h> +# include <pwd.h> #endif -#ifndef HAVE_STRING_H -char *strrchr _((const char*,const char)); +#ifdef HAVE_SYS_SYSMACROS_H +# include <sys/sysmacros.h> #endif #include <sys/types.h> #include <sys/stat.h> #ifdef HAVE_SYS_MKDEV_H -#include <sys/mkdev.h> +# include <sys/mkdev.h> +#endif + +#if defined(HAVE_FCNTL_H) +# include <fcntl.h> +#endif + +#if defined(HAVE_SYS_TIME_H) +# include <sys/time.h> #endif #if !defined HAVE_LSTAT && !defined lstat -#define lstat stat +# define lstat stat +#endif + +/* define system APIs */ +#ifdef _WIN32 +# include "win32/file.h" +# define STAT(p, s) rb_w32_ustati128((p), (s)) +# undef lstat +# define lstat(p, s) rb_w32_ulstati128((p), (s)) +# undef access +# define access(p, m) rb_w32_uaccess((p), (m)) +# undef truncate +# define truncate(p, n) rb_w32_utruncate((p), (n)) +# undef chmod +# define chmod(p, m) rb_w32_uchmod((p), (m)) +# undef chown +# define chown(p, o, g) rb_w32_uchown((p), (o), (g)) +# undef lchown +# define lchown(p, o, g) rb_w32_ulchown((p), (o), (g)) +# undef utimensat +# define utimensat(s, p, t, f) rb_w32_uutimensat((s), (p), (t), (f)) +# undef link +# 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 +# define symlink(s, l) rb_w32_usymlink((s), (l)) + +# ifdef HAVE_REALPATH +/* Don't use native realpath(3) on Windows, as the check for + absolute paths does not work for drive letters. */ +# undef HAVE_REALPATH +# 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 !HAVE_FSEEKO && !defined(fseeko) -# define fseeko fseek + +#if defined _WIN32 || defined __APPLE__ +# define USE_OSPATH 1 +# define TO_OSPATH(str) rb_str_encode_ospath(str) +#else +# define USE_OSPATH 0 +# define TO_OSPATH(str) (str) #endif -#ifdef __BEOS__ /* should not change ID if -1 */ -static int -be_chown(const char *path, uid_t owner, gid_t group) +/* utime may fail if time is out-of-range for the FS [ruby-dev:38277] */ +#if defined DOSISH || defined __CYGWIN__ +# define UTIME_EINVAL +#endif + +/* Solaris 10 realpath(3) doesn't support File.realpath */ +#if defined HAVE_REALPATH && defined __sun && defined __SVR4 +#undef HAVE_REALPATH +#endif + +#ifdef HAVE_REALPATH +# include <limits.h> +# include <stdlib.h> +#endif + +#include "dln.h" +#include "encindex.h" +#include "id.h" +#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" +#include "internal/load.h" +#include "internal/object.h" +#include "internal/process.h" +#include "internal/thread.h" +#include "internal/vm.h" +#include "ruby/encoding.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; + +static VALUE +file_path_convert(VALUE name) +{ +#ifndef _WIN32 /* non Windows == Unix */ + int fname_encidx = ENCODING_GET(name); + int fs_encidx; + if (ENCINDEX_US_ASCII != fname_encidx && + ENCINDEX_ASCII_8BIT != fname_encidx && + (fs_encidx = rb_filesystem_encindex()) != fname_encidx && + rb_default_internal_encoding() && + !rb_enc_str_asciionly_p(name)) { + /* Don't call rb_filesystem_encoding() before US-ASCII and ASCII-8BIT */ + /* fs_encoding should be ascii compatible */ + rb_encoding *fname_encoding = rb_enc_from_index(fname_encidx); + rb_encoding *fs_encoding = rb_enc_from_index(fs_encidx); + name = rb_str_conv_enc(name, fname_encoding, fs_encoding); + } +#endif + return name; +} + +static void +check_path_encoding(VALUE 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)); + } + } +} + +VALUE +rb_get_path_check_to_string(VALUE obj) { - if (owner == -1 || group == -1) { - struct stat st; - if (stat(path, &st) < 0) return -1; - if (owner == -1) owner = st.st_uid; - if (group == -1) group = st.st_gid; + VALUE tmp; + ID to_path; + + if (RB_TYPE_P(obj, T_STRING)) { + return obj; } - return chown(path, owner, group); + CONST_ID(to_path, "to_path"); + tmp = rb_check_funcall_default(obj, to_path, 0, 0, obj); + StringValue(tmp); + return tmp; } -#define chown be_chown -static int -be_fchown(int fd, uid_t owner, gid_t group) + +VALUE +rb_get_path_check_convert(VALUE obj) { - if (owner == -1 || group == -1) { - struct stat st; - if (fstat(fd, &st) < 0) return -1; - if (owner == -1) owner = st.st_uid; - if (group == -1) group = st.st_gid; + obj = file_path_convert(obj); + + check_path_encoding(obj); + if (!rb_str_to_cstr(obj)) { + rb_raise(rb_eArgError, "path name contains null byte"); } - return fchown(fd, owner, group); + + return rb_str_new_frozen(obj); } -#define fchown be_fchown -#endif /* __BEOS__ */ -VALUE rb_cFile; -VALUE rb_mFileTest; -VALUE rb_cStat; +VALUE +rb_get_path_no_checksafe(VALUE obj) +{ + return rb_get_path(obj); +} -static long apply2files _((void (*)(const char *, void *), VALUE, void *)); -static long -apply2files(func, vargs, arg) - void (*func)_((const char *, void *)); - VALUE vargs; - void *arg; +VALUE +rb_get_path(VALUE obj) { - long i; - VALUE path; - struct RArray *args = RARRAY(vargs); + 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; +} - for (i=0; i<args->len; i++) { - path = args->ptr[i]; - SafeStringValue(path); - (*func)(StringValueCStr(path), arg); +#define CheckPath(str, cstr) RB_GC_GUARD(str) = check_path(str, &cstr); + +VALUE +rb_str_encode_ospath(VALUE path) +{ +#if USE_OSPATH + int encidx = ENCODING_GET(path); +#if 0 && defined _WIN32 + if (encidx == ENCINDEX_ASCII_8BIT) { + encidx = rb_filesystem_encindex(); } +#endif + if (encidx != ENCINDEX_ASCII_8BIT && encidx != ENCINDEX_UTF_8) { + rb_encoding *enc = rb_enc_from_index(encidx); + rb_encoding *utf8 = rb_utf8_encoding(); + path = rb_str_conv_enc(path, enc, utf8); + } +#endif /* USE_OSPATH */ + return path; +} + +#ifdef __APPLE__ +# define NORMALIZE_UTF8PATH 1 - return args->len; +# 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); } -/* - * call-seq: - * file.path -> filename - * - * Returns the pathname used to create <i>file</i> as a string. Does - * not normalize the name. - * - * File.new("testfile").path #=> "testfile" - * File.new("/tmp/../tmp/xxx", "w").path #=> "/tmp/../tmp/xxx" - * - */ +# 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_file_path(obj) - VALUE obj; +rb_str_append_normalized_ospath(VALUE str, const char *ptr, long len) +{ + CFIndex buflen = 0; + CFRange all; + CFStringRef s; + CFMutableStringRef m = mutable_CFString_new(&s, ptr, len); + long oldlen = RSTRING_LEN(str); + + CFStringNormalize(m, kCFStringNormalizationFormC); + all = CFRangeMake(0, CFStringGetLength(m)); + CFStringGetBytes(m, all, kCFStringEncodingUTF8, '?', FALSE, NULL, 0, &buflen); + rb_str_modify_expand(str, buflen); + CFStringGetBytes(m, all, kCFStringEncodingUTF8, '?', FALSE, + (UInt8 *)(RSTRING_PTR(str) + oldlen), buflen, &buflen); + rb_str_set_len(str, oldlen + buflen); + mutable_CFString_release(m, s); + return str; +} + +VALUE +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); + + 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 */ + 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; + p1 = p; + continue; + } + l = MBCLEN_CHARFOUND_LEN(r); + c = rb_enc_mbc_to_codepoint(p, e, enc); + if ((0x2000 <= c && c <= 0x2FFF) || (0xF900 <= c && c <= 0xFAFF) || + (0x2F800 <= c && c <= 0x2FAFF)) { + if (p - p1 > 0) { + rb_str_append_normalized_ospath(str, p1, p-p1); + } + rb_str_cat(str, p, l); + p += l; + p1 = p; + } + else { + p += l; + } + } + if (p - p1 > 0) { + rb_str_append_normalized_ospath(str, p1, p-p1); + } + + return str; +} + +static int +ignored_char_p(const char *p, const char *e, rb_encoding *enc) +{ + unsigned char c; + if (p+3 > e) return 0; + switch ((unsigned char)*p) { + case 0xe2: + switch ((unsigned char)p[1]) { + case 0x80: + c = (unsigned char)p[2]; + /* c >= 0x200c && c <= 0x200f */ + if (c >= 0x8c && c <= 0x8f) return 3; + /* c >= 0x202a && c <= 0x202e */ + if (c >= 0xaa && c <= 0xae) return 3; + return 0; + case 0x81: + c = (unsigned char)p[2]; + /* c >= 0x206a && c <= 0x206f */ + if (c >= 0xaa && c <= 0xaf) return 3; + return 0; + } + break; + case 0xef: + /* c == 0xfeff */ + if ((unsigned char)p[1] == 0xbb && + (unsigned char)p[2] == 0xbf) + return 3; + break; + } + return 0; +} +#else /* !__APPLE__ */ +# define NORMALIZE_UTF8PATH 0 +#endif /* __APPLE__ */ + +#define apply2args(n) (rb_check_arity(argc, n, UNLIMITED_ARGUMENTS), argc-=n) + +struct apply_filename { + const char *ptr; + VALUE path; +}; + +struct apply_arg { + int i; + int argc; + int errnum; + int (*func)(const char *, void *); + void *arg; + struct apply_filename fn[FLEX_ARY_LEN]; +}; + +static void * +no_gvl_apply2files(void *ptr) { - rb_io_t *fptr; + struct apply_arg *aa = ptr; - fptr = RFILE(rb_io_taint_check(obj))->fptr; - rb_io_check_initialized(fptr); - if (!fptr->path) return Qnil; - return rb_tainted_str_new2(fptr->path); + for (aa->i = 0; aa->i < aa->argc; aa->i++) { + if (aa->func(aa->fn[aa->i].ptr, aa->arg) < 0) { + aa->errnum = errno; + break; + } + } + return 0; } +#ifdef UTIME_EINVAL +NORETURN(static void utime_failed(struct apply_arg *)); +static int utime_internal(const char *, void *); +#endif + static VALUE -stat_new_0(klass, st) - VALUE klass; - struct stat *st; +apply2files(int (*func)(const char *, void *), int argc, VALUE *argv, void *arg) { - struct stat *nst = 0; + VALUE v; + const size_t size = sizeof(struct apply_filename); + const long len = (long)(offsetof(struct apply_arg, fn) + (size * argc)); + struct apply_arg *aa = ALLOCV(v, len); + + aa->errnum = 0; + aa->argc = argc; + aa->arg = arg; + aa->func = func; + + for (aa->i = 0; aa->i < argc; aa->i++) { + VALUE path = rb_get_path(argv[aa->i]); + + path = rb_str_encode_ospath(path); + aa->fn[aa->i].ptr = RSTRING_PTR(path); + aa->fn[aa->i].path = path; + } + + IO_WITHOUT_GVL(no_gvl_apply2files, aa); + if (aa->errnum) { +#ifdef UTIME_EINVAL + if (func == utime_internal) { + utime_failed(aa); + } +#endif + rb_syserr_fail_path(aa->errnum, aa->fn[aa->i].path); + } + if (v) { + ALLOCV_END(v); + } + return LONG2FIX(argc); +} + +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, + NULL, // No external memory to report + }, + 0, 0, RUBY_TYPED_FREE_IMMEDIATELY | RUBY_TYPED_WB_PROTECTED | RUBY_TYPED_EMBEDDABLE +}; + +struct rb_stat { + rb_io_stat_data stat; + bool initialized; +}; + +static struct rb_stat * +stat_alloc(VALUE klass, VALUE *obj) +{ + 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; +#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 Data_Wrap_Struct(klass, NULL, free, nst); + + return obj; } -static VALUE -stat_new(st) - struct stat *st; +#ifndef rb_statx_new +VALUE +rb_statx_new(const rb_io_stat_data *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 rb_io_stat_data* +get_stat(VALUE self) { - return stat_new_0(rb_cStat, 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 stat* -get_stat(self) - VALUE self; +#if RUBY_USE_STATX +static stat_timestamp +statx_mtimespec(const rb_io_stat_data *st) { - struct stat* st; - Data_Get_Struct(self, struct stat, st); - if (!st) rb_raise(rb_eTypeError, "uninitialized File::Stat"); - return st; + return st->stx_mtime; } +#else +# define statx_mtimespec stat_mtimespec +#endif /* * call-seq: - * stat <=> other_stat => -1, 0, 1 - * - * Compares <code>File::Stat</code> objects by comparing their - * respective modification times. - * - * f1 = File.new("f1", "w") - * sleep 1 - * f2 = File.new("f2", "w") - * f1.stat <=> f2.stat #=> -1 + * 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. + * + * Examples: + * + * 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 + * + * \Class \File::Stat includes module Comparable, + * each of whose methods uses File::Stat#<=> for comparison. */ static VALUE -rb_stat_cmp(self, other) - VALUE self, other; +rb_stat_cmp(VALUE self, VALUE other) { if (rb_obj_is_kind_of(other, rb_obj_class(self))) { - time_t t1 = get_stat(self)->st_mtime; - time_t t2 = get_stat(other)->st_mtime; - if (t1 == t2) - return INT2FIX(0); - else if (t1 < t2) - return INT2FIX(-1); - else - return INT2FIX(1); + 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); + return INT2FIX(1); + } + if (ts1.tv_sec < ts2.tv_sec) return INT2FIX(-1); + return INT2FIX(1); } return Qnil; } -static VALUE rb_stat_dev _((VALUE)); -static VALUE rb_stat_ino _((VALUE)); -static VALUE rb_stat_mode _((VALUE)); -static VALUE rb_stat_nlink _((VALUE)); -static VALUE rb_stat_uid _((VALUE)); -static VALUE rb_stat_gid _((VALUE)); -static VALUE rb_stat_rdev _((VALUE)); -static VALUE rb_stat_size _((VALUE)); -static VALUE rb_stat_blksize _((VALUE)); -static VALUE rb_stat_blocks _((VALUE)); -static VALUE rb_stat_atime _((VALUE)); -static VALUE rb_stat_mtime _((VALUE)); -static VALUE rb_stat_ctime _((VALUE)); +#define ST2UINT(val) ((val) & ~(~1UL << (sizeof(val) * CHAR_BIT - 1))) + +#ifndef NUM2DEVT +# define NUM2DEVT(v) NUM2UINT(v) +#endif +#ifndef DEVT2NUM +# define DEVT2NUM(v) UINT2NUM(v) +#endif +#ifndef PRI_DEVT_PREFIX +# define PRI_DEVT_PREFIX "" +#endif /* * call-seq: - * stat.dev => fixnum - * + * stat.dev -> integer + * * Returns an integer representing the device on which <i>stat</i> * resides. - * + * * File.stat("testfile").dev #=> 774 */ static VALUE -rb_stat_dev(self) - VALUE self; -{ - return INT2NUM(get_stat(self)->st_dev); +rb_stat_dev(VALUE self) +{ +#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); +#else + return ULL2NUM(get_stat(self)->st_dev); +#endif } /* * call-seq: - * stat.dev_major => fixnum - * + * stat.dev_major -> integer + * * Returns the major part of <code>File_Stat#dev</code> or * <code>nil</code>. - * + * * File.stat("/dev/fd1").dev_major #=> 2 * File.stat("/dev/tty").dev_major #=> 5 */ static VALUE -rb_stat_dev_major(self) - VALUE self; +rb_stat_dev_major(VALUE self) { -#if defined(major) - long dev = get_stat(self)->st_dev; - return ULONG2NUM(major(dev)); +#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; #endif @@ -267,169 +727,172 @@ rb_stat_dev_major(self) /* * call-seq: - * stat.dev_minor => fixnum - * + * stat.dev_minor -> integer + * * Returns the minor part of <code>File_Stat#dev</code> or * <code>nil</code>. - * + * * File.stat("/dev/fd1").dev_minor #=> 1 * File.stat("/dev/tty").dev_minor #=> 0 */ static VALUE -rb_stat_dev_minor(self) - VALUE self; +rb_stat_dev_minor(VALUE self) { -#if defined(minor) - long dev = get_stat(self)->st_dev; - return ULONG2NUM(minor(dev)); +#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; #endif } - /* * call-seq: - * stat.ino => fixnum - * + * stat.ino -> integer + * * Returns the inode number for <i>stat</i>. - * + * * File.stat("testfile").ino #=> 1083669 - * + * */ static VALUE -rb_stat_ino(self) - VALUE self; -{ -#ifdef HUGE_ST_INO - return ULL2NUM(get_stat(self)->st_ino); +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(&ptr->st_ino, 2, + SIZEOF_STRUCT_STAT_ST_INO, 0, + INTEGER_PACK_LSWORD_FIRST|INTEGER_PACK_NATIVE_BYTE_ORDER| + INTEGER_PACK_2COMP); #else - return ULONG2NUM(get_stat(self)->st_ino); + return UIANY2NUM(ptr->ST_(ino)); #endif } /* * call-seq: - * stat.mode => fixnum - * + * stat.mode -> integer + * * Returns an integer representing the permission bits of * <i>stat</i>. The meaning of the bits is platform dependent; on * Unix systems, see <code>stat(2)</code>. - * + * * File.chmod(0644, "testfile") #=> 1 * s = File.stat("testfile") * sprintf("%o", s.mode) #=> "100644" */ static VALUE -rb_stat_mode(self) - VALUE self; +rb_stat_mode(VALUE self) { -#ifdef __BORLANDC__ - return UINT2NUM((unsigned short)(get_stat(self)->st_mode)); -#else - return UINT2NUM(get_stat(self)->st_mode); -#endif + return UINT2NUM(ST2UINT(get_stat(self)->ST_(mode))); } /* * call-seq: - * stat.nlink => fixnum - * + * stat.nlink -> integer + * * Returns the number of hard links to <i>stat</i>. - * + * * File.stat("testfile").nlink #=> 1 * File.link("testfile", "testfile.bak") #=> 0 * File.stat("testfile").nlink #=> 2 - * + * */ static VALUE -rb_stat_nlink(self) - VALUE self; +rb_stat_nlink(VALUE self) { - return UINT2NUM(get_stat(self)->st_nlink); -} + /* struct stat::st_nlink is nlink_t in POSIX. Not the case for Windows. */ + const rb_io_stat_data *ptr = get_stat(self); + return UIANY2NUM(ptr->ST_(nlink)); +} /* * call-seq: - * stat.uid => fixnum - * + * stat.uid -> integer + * * Returns the numeric user id of the owner of <i>stat</i>. - * + * * File.stat("testfile").uid #=> 501 - * + * */ static VALUE -rb_stat_uid(self) - VALUE self; +rb_stat_uid(VALUE self) { - return UINT2NUM(get_stat(self)->st_uid); + return UIDT2NUM(get_stat(self)->ST_(uid)); } /* * call-seq: - * stat.gid => fixnum - * + * stat.gid -> integer + * * Returns the numeric group id of the owner of <i>stat</i>. - * + * * File.stat("testfile").gid #=> 500 - * + * */ static VALUE -rb_stat_gid(self) - VALUE self; +rb_stat_gid(VALUE self) { - return UINT2NUM(get_stat(self)->st_gid); + return GIDT2NUM(get_stat(self)->ST_(gid)); } - /* * call-seq: - * stat.rdev => fixnum or nil - * + * stat.rdev -> integer or nil + * * Returns an integer representing the device type on which * <i>stat</i> resides. Returns <code>nil</code> if the operating * system doesn't support this feature. - * + * * File.stat("/dev/fd1").rdev #=> 513 * File.stat("/dev/tty").rdev #=> 1280 */ static VALUE -rb_stat_rdev(self) - VALUE self; +rb_stat_rdev(VALUE self) { -#ifdef HAVE_ST_RDEV - return ULONG2NUM(get_stat(self)->st_rdev); -#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 } /* * call-seq: - * stat.rdev_major => fixnum - * + * stat.rdev_major -> integer + * * Returns the major part of <code>File_Stat#rdev</code> or * <code>nil</code>. - * + * * File.stat("/dev/fd1").rdev_major #=> 2 * File.stat("/dev/tty").rdev_major #=> 5 */ static VALUE -rb_stat_rdev_major(self) - VALUE self; +rb_stat_rdev_major(VALUE self) { -#if defined(HAVE_ST_RDEV) && defined(major) - long rdev = get_stat(self)->st_rdev; - return ULONG2NUM(major(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 @@ -437,22 +900,22 @@ rb_stat_rdev_major(self) /* * call-seq: - * stat.rdev_minor => fixnum - * + * stat.rdev_minor -> integer + * * Returns the minor part of <code>File_Stat#rdev</code> or * <code>nil</code>. - * + * * File.stat("/dev/fd1").rdev_minor #=> 1 * File.stat("/dev/tty").rdev_minor #=> 0 */ static VALUE -rb_stat_rdev_minor(self) - VALUE self; +rb_stat_rdev_minor(VALUE self) { -#if defined(HAVE_ST_RDEV) && defined(minor) - long rdev = get_stat(self)->st_rdev; - return ULONG2NUM(minor(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 @@ -460,37 +923,35 @@ rb_stat_rdev_minor(self) /* * call-seq: - * stat.size => fixnum - * + * stat.size -> integer + * * Returns the size of <i>stat</i> in bytes. - * + * * File.stat("testfile").size #=> 66 */ static VALUE -rb_stat_size(self) - VALUE self; +rb_stat_size(VALUE self) { - return OFFT2NUM(get_stat(self)->st_size); + return OFFT2NUM(get_stat(self)->ST_(size)); } /* * call-seq: - * stat.blksize => integer or nil - * + * stat.blksize -> integer or nil + * * Returns the native file system's block size. Will return <code>nil</code> * on platforms that don't support this information. - * + * * File.stat("testfile").blksize #=> 4096 - * + * */ static VALUE -rb_stat_blksize(self) - VALUE self; +rb_stat_blksize(VALUE self) { -#ifdef HAVE_ST_BLKSIZE - return ULONG2NUM(get_stat(self)->st_blksize); +#ifdef HAVE_STRUCT_STAT_ST_BLKSIZE + return ULONG2NUM(get_stat(self)->ST_(blksize)); #else return Qnil; #endif @@ -498,358 +959,673 @@ rb_stat_blksize(self) /* * call-seq: - * stat.blocks => integer or nil - * + * stat.blocks -> integer or nil + * * Returns the number of native file system blocks allocated for this - * file, or <code>nil</code> if the operating system doesn't + * file, or <code>nil</code> if the operating system doesn't * support this feature. - * + * * File.stat("testfile").blocks #=> 2 */ static VALUE -rb_stat_blocks(self) - VALUE self; +rb_stat_blocks(VALUE self) { -#ifdef HAVE_ST_BLOCKS - return ULONG2NUM(get_stat(self)->st_blocks); +#ifdef HAVE_STRUCT_STAT_ST_BLOCKS +# if SIZEOF_STRUCT_STAT_ST_BLOCKS > SIZEOF_LONG + return ULL2NUM(get_stat(self)->ST_(blocks)); +# else + return ULONG2NUM(get_stat(self)->ST_(blocks)); +# endif #else return Qnil; #endif } +static stat_timestamp +stat_atimespec(const struct stat *st) +{ + stat_timestamp ts; + ts.tv_sec = st->st_atime; +#if defined(HAVE_STRUCT_STAT_ST_ATIM) + ts.tv_nsec = (uint32_t)st->st_atim.tv_nsec; +#elif defined(HAVE_STRUCT_STAT_ST_ATIMESPEC) + ts.tv_nsec = (uint32_t)st->st_atimespec.tv_nsec; +#elif defined(HAVE_STRUCT_STAT_ST_ATIMENSEC) + ts.tv_nsec = (uint32_t)st->st_atimensec; +#else + 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 stat_timestamp ts) +{ + return rb_time_nano_new(ts.tv_sec, ts.tv_nsec); +} + +static VALUE +stat_atime(const struct stat *st) +{ + return stat_time(stat_atimespec(st)); +} + +static stat_timestamp +stat_mtimespec(const struct stat *st) +{ + stat_timestamp ts; + ts.tv_sec = st->st_mtime; +#if defined(HAVE_STRUCT_STAT_ST_MTIM) + ts.tv_nsec = (uint32_t)st->st_mtim.tv_nsec; +#elif defined(HAVE_STRUCT_STAT_ST_MTIMESPEC) + ts.tv_nsec = (uint32_t)st->st_mtimespec.tv_nsec; +#elif defined(HAVE_STRUCT_STAT_ST_MTIMENSEC) + ts.tv_nsec = (uint32_t)st->st_mtimensec; +#else + ts.tv_nsec = 0; +#endif + return ts; +} + +static VALUE +stat_mtime(const struct stat *st) +{ + return stat_time(stat_mtimespec(st)); +} + +static stat_timestamp +stat_ctimespec(const struct stat *st) +{ + stat_timestamp ts; + ts.tv_sec = st->st_ctime; +#if defined(HAVE_STRUCT_STAT_ST_CTIM) + ts.tv_nsec = (uint32_t)st->st_ctim.tv_nsec; +#elif defined(HAVE_STRUCT_STAT_ST_CTIMESPEC) + ts.tv_nsec = (uint32_t)st->st_ctimespec.tv_nsec; +#elif defined(HAVE_STRUCT_STAT_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) +{ + return stat_time(stat_ctimespec(st)); +} + +#define HAVE_STAT_BIRTHTIME +#if defined(HAVE_STRUCT_STAT_ST_BIRTHTIMESPEC) +static VALUE +statx_birthtime(const rb_io_stat_data *st) +{ + 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) +# define statx_birthtime stat_ctime +#else +# undef HAVE_STAT_BIRTHTIME +#endif /* defined(HAVE_STRUCT_STAT_ST_BIRTHTIMESPEC) */ /* * call-seq: - * stat.atime => time - * + * stat.atime -> time + * * Returns the last access time for this file as an object of class - * <code>Time</code>. - * + * Time. + * * File.stat("testfile").atime #=> Wed Dec 31 18:00:00 CST 1969 - * + * */ static VALUE -rb_stat_atime(self) - VALUE self; +rb_stat_atime(VALUE self) { - return rb_time_new(get_stat(self)->st_atime, 0); + return stat_time(statx_atimespec(get_stat(self))); } /* * call-seq: - * stat.mtime -> aTime - * + * stat.mtime -> time + * * Returns the modification time of <i>stat</i>. - * + * * File.stat("testfile").mtime #=> Wed Apr 09 08:53:14 CDT 2003 - * + * */ static VALUE -rb_stat_mtime(self) - VALUE self; +rb_stat_mtime(VALUE self) { - return rb_time_new(get_stat(self)->st_mtime, 0); + return stat_time(statx_mtimespec(get_stat(self))); } /* * call-seq: - * stat.ctime -> aTime - * + * stat.ctime -> time + * * Returns the change time for <i>stat</i> (that is, the time * directory information about the file was changed, not the file * itself). - * + * + * Note that on Windows (NTFS), returns creation time (birth time). + * * File.stat("testfile").ctime #=> Wed Apr 09 08:53:14 CDT 2003 - * + * + */ + +static VALUE +rb_stat_ctime(VALUE 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 + * */ static VALUE -rb_stat_ctime(self) - VALUE self; +rb_stat_birthtime(VALUE self) { - return rb_time_new(get_stat(self)->st_ctime, 0); + return statx_birthtime(get_stat(self)); } +#else +# define rb_stat_birthtime rb_f_notimplement +#endif /* * call-seq: - * stat.inspect => string + * stat.inspect -> string * * Produce a nicely formatted description of <i>stat</i>. * * File.stat("/etc/passwd").inspect - * #=> "#<File::Stat dev=0xe000005, ino=1078078, mode=0100644, - * nlink=1, uid=0, gid=0, rdev=0x0, size=1374, blksize=4096, - * blocks=8, atime=Wed Dec 10 10:16:12 CST 2003, - * mtime=Fri Sep 12 15:41:41 CDT 2003, - * ctime=Mon Oct 27 11:20:27 CST 2003>" + * #=> "#<File::Stat dev=0xe000005, ino=1078078, mode=0100644, + * # nlink=1, uid=0, gid=0, rdev=0x0, size=1374, blksize=4096, + * # blocks=8, atime=Wed Dec 10 10:16:12 CST 2003, + * # mtime=Fri Sep 12 15:41:41 CDT 2003, + * # ctime=Mon Oct 27 11:20:27 CST 2003, + * # birthtime=Mon Aug 04 08:13:49 CDT 2003>" */ static VALUE -rb_stat_inspect(self) - VALUE self; +rb_stat_inspect(VALUE self) { VALUE str; - int i; + size_t i; static const struct { - const char *name; - VALUE (*func)_((VALUE)); + const char *name; + VALUE (*func)(VALUE); } member[] = { - {"dev", rb_stat_dev}, - {"ino", rb_stat_ino}, - {"mode", rb_stat_mode}, - {"nlink", rb_stat_nlink}, - {"uid", rb_stat_uid}, - {"gid", rb_stat_gid}, - {"rdev", rb_stat_rdev}, - {"size", rb_stat_size}, - {"blksize", rb_stat_blksize}, - {"blocks", rb_stat_blocks}, - {"atime", rb_stat_atime}, - {"mtime", rb_stat_mtime}, - {"ctime", rb_stat_ctime}, + {"dev", rb_stat_dev}, + {"ino", rb_stat_ino}, + {"mode", rb_stat_mode}, + {"nlink", rb_stat_nlink}, + {"uid", rb_stat_uid}, + {"gid", rb_stat_gid}, + {"rdev", rb_stat_rdev}, + {"size", rb_stat_size}, + {"blksize", rb_stat_blksize}, + {"blocks", rb_stat_blocks}, + {"atime", rb_stat_atime}, + {"mtime", rb_stat_mtime}, + {"ctime", rb_stat_ctime}, +#if defined(HAVE_STRUCT_STAT_ST_BIRTHTIMESPEC) + {"birthtime", rb_stat_birthtime}, +#endif }; + 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)); + } + str = rb_str_buf_new2("#<"); rb_str_buf_cat2(str, rb_obj_classname(self)); rb_str_buf_cat2(str, " "); for (i = 0; i < sizeof(member)/sizeof(member[0]); i++) { - VALUE v; - - if (i > 0) { - rb_str_buf_cat2(str, ", "); - } - rb_str_buf_cat2(str, member[i].name); - rb_str_buf_cat2(str, "="); - v = (*member[i].func)(self); - if (i == 2) { /* mode */ - char buf[32]; - - sprintf(buf, "0%lo", NUM2ULONG(v)); - rb_str_buf_cat2(str, buf); - } - else if (i == 0 || i == 6) { /* dev/rdev */ - char buf[32]; - - sprintf(buf, "0x%lx", NUM2ULONG(v)); - rb_str_buf_cat2(str, buf); - } - else { - rb_str_append(str, rb_inspect(v)); - } + VALUE v; + + if (i > 0) { + rb_str_buf_cat2(str, ", "); + } + rb_str_buf_cat2(str, member[i].name); + rb_str_buf_cat2(str, "="); + v = (*member[i].func)(self); + if (i == 2) { /* mode */ + rb_str_catf(str, "0%lo", (unsigned long)NUM2ULONG(v)); + } + else if (i == 0 || i == 6) { /* dev/rdev */ + rb_str_catf(str, "0x%"PRI_DEVT_PREFIX"x", NUM2DEVT(v)); + } + else { + rb_str_append(str, rb_inspect(v)); + } } rb_str_buf_cat2(str, ">"); - OBJ_INFECT(str, self); return str; } -static int -rb_stat(file, st) - VALUE file; +typedef struct no_gvl_stat_data { struct stat *st; + union { + const char *path; + int fd; + } file; +} no_gvl_stat_data; + +static VALUE +no_gvl_fstat(void *data) +{ + no_gvl_stat_data *arg = data; + return (VALUE)fstat(arg->file.fd, arg->st); +} + +static int +fstat_without_gvl(rb_io_t *fptr, struct stat *st) +{ + no_gvl_stat_data data; + + data.file.fd = fptr->fd; + data.st = st; + + return (int)rb_io_blocking_region(fptr, no_gvl_fstat, &data); +} + +static void * +no_gvl_stat(void * data) +{ + no_gvl_stat_data *arg = data; + return (void *)(VALUE)STAT(arg->file.path, arg->st); +} + +static int +stat_without_gvl(const char *path, struct stat *st) +{ + no_gvl_stat_data data; + + data.file.path = path; + data.st = st; + + 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> +# elif defined HAVE_SYS_SYSCALL_H +# include <sys/syscall.h> +# endif +# if defined __linux__ +# include <linux/stat.h> +static inline int +statx(int dirfd, const char *pathname, int flags, + unsigned int mask, struct statx *statxbuf) +{ + return (int)syscall(__NR_statx, dirfd, pathname, flags, mask, statxbuf); +} +# endif /* __linux__ */ +# endif /* HAVE_STATX */ + +typedef struct no_gvl_rb_io_stat_data { + struct statx *stx; + int fd; + const char *path; + int flags; + unsigned int mask; +} no_gvl_rb_io_stat_data; + +static VALUE +io_blocking_statx(void *data) +{ + no_gvl_rb_io_stat_data *arg = data; + return (VALUE)statx(arg->fd, arg->path, arg->flags, arg->mask, arg->stx); +} + +static void * +no_gvl_statx(void *data) +{ + return (void *)io_blocking_statx(data); +} + +static int +statx_without_gvl(const char *path, rb_io_stat_data *stx, unsigned int mask) +{ + no_gvl_rb_io_stat_data data = {stx, AT_FDCWD, path, 0, mask}; + + /* call statx(2) with pathname */ + return IO_WITHOUT_GVL_INT(no_gvl_statx, &data); +} + +static int +lstatx_without_gvl(const char *path, rb_io_stat_data *stx, unsigned int 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_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) { VALUE tmp; + int result; - tmp = rb_check_convert_type(file, T_FILE, "IO", "to_io"); + tmp = rb_check_convert_type_with_id(file, T_FILE, "IO", idTo_io); if (!NIL_P(tmp)) { - rb_io_t *fptr; + rb_io_t *fptr; - rb_secure(2); - GetOpenFile(tmp, fptr); - return fstat(fileno(fptr->f), st); + GetOpenFile(tmp, fptr); + result = fstatx_without_gvl(fptr, stx, mask); + file = tmp; } - SafeStringValue(file); - return stat(StringValueCStr(file), st); + else { + FilePathValue(file); + file = rb_str_encode_ospath(file); + result = statx_without_gvl(RSTRING_PTR(file), stx, mask); + } + RB_GC_GUARD(file); + return result; } -#ifdef _WIN32 -static HANDLE -w32_io_info(file, st) - VALUE *file; - BY_HANDLE_FILE_INFORMATION *st; +# define statx_has_birthtime(st) ((st)->stx_mask & STATX_BTIME) + +NORETURN(static void statx_notimplement(const char *field_name)); + +/* rb_notimplement() shows "function is unimplemented on this machine". + It is not applicable to statx which behavior depends on the filesystem. */ +static void +statx_notimplement(const char *field_name) +{ + rb_raise(rb_eNotImpError, + "%s is unimplemented on this filesystem", + field_name); +} + +static VALUE +statx_birthtime(const rb_io_stat_data *stx) +{ + if (!statx_has_birthtime(stx)) { + /* birthtime is not supported on the filesystem */ + statx_notimplement("birthtime"); + } + return rb_time_nano_new((time_t)stx->stx_btime.tv_sec, stx->stx_btime.tv_nsec); +} + +#else + +# define statx_without_gvl(path, st, mask) stat_without_gvl(path, 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 +#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) { VALUE tmp; - HANDLE f, ret = 0; + int result; - tmp = rb_check_convert_type(*file, T_FILE, "IO", "to_io"); + tmp = rb_check_convert_type_with_id(file, T_FILE, "IO", idTo_io); if (!NIL_P(tmp)) { - rb_io_t *fptr; + rb_io_t *fptr; - GetOpenFile(tmp, fptr); - f = (HANDLE)rb_w32_get_osfhandle(fileno(fptr->f)); - if (f == (HANDLE)-1) return INVALID_HANDLE_VALUE; + GetOpenFile(tmp, fptr); + result = fstat_without_gvl(fptr, st); + file = tmp; } else { - SafeStringValue(*file); - f = CreateFile(StringValueCStr(*file), 0, - FILE_SHARE_READ | FILE_SHARE_WRITE, NULL, OPEN_EXISTING, - rb_w32_iswin95() ? 0 : FILE_FLAG_BACKUP_SEMANTICS, NULL); - if (f == INVALID_HANDLE_VALUE) return f; - ret = f; + FilePathValue(file); + file = rb_str_encode_ospath(file); + result = stat_without_gvl(RSTRING_PTR(file), st); } - if (GetFileType(f) == FILE_TYPE_DISK) { - ZeroMemory(st, sizeof(*st)); - if (GetFileInformationByHandle(f, st)) return ret; - } - if (ret) CloseHandle(ret); - return INVALID_HANDLE_VALUE; + RB_GC_GUARD(file); + return result; } -#endif /* * call-seq: - * File.stat(file_name) => stat - * - * Returns a <code>File::Stat</code> object for the named file (see - * <code>File::Stat</code>). - * - * File.stat("testfile").mtime #=> Tue Apr 08 12:58:04 CDT 2003 - * + * File.stat(filepath) -> stat + * + * Returns a File::Stat object for the file at +filepath+ (see File::Stat): + * + * File.stat('t.txt').class # => File::Stat + * */ static VALUE -rb_file_s_stat(klass, fname) - VALUE klass, fname; +rb_file_s_stat(VALUE klass, VALUE fname) { - struct stat st; + rb_io_stat_data st; - SafeStringValue(fname); - if (rb_stat(fname, &st) < 0) { - rb_sys_fail(StringValueCStr(fname)); + FilePathValue(fname); + fname = rb_str_encode_ospath(fname); + if (statx_without_gvl(RSTRING_PTR(fname), &st, STATX_ALL) < 0) { + rb_sys_fail_path(fname); } - return stat_new(&st); + return rb_statx_new(&st); } /* * call-seq: - * ios.stat => stat - * + * ios.stat -> stat + * * Returns status information for <em>ios</em> as an object of type - * <code>File::Stat</code>. - * + * File::Stat. + * * f = File.new("testfile") * s = f.stat * "%o" % s.mode #=> "100644" * s.blksize #=> 4096 * s.atime #=> Wed Apr 09 08:53:54 CDT 2003 - * + * */ static VALUE -rb_io_stat(obj) - VALUE obj; +rb_io_stat(VALUE obj) { rb_io_t *fptr; - struct stat st; + rb_io_stat_data st; GetOpenFile(obj, fptr); - if (fstat(fileno(fptr->f), &st) == -1) { - rb_sys_fail(fptr->path); + if (fstatx_without_gvl(fptr, &st, STATX_ALL) == -1) { + rb_sys_fail_path(fptr->pathv); } - return stat_new(&st); + return rb_statx_new(&st); +} + +#ifdef HAVE_LSTAT +static void * +no_gvl_lstat(void *ptr) +{ + no_gvl_stat_data *arg = ptr; + return (void *)(VALUE)lstat(arg->file.path, arg->st); +} + +static int +lstat_without_gvl(const char *path, struct stat *st) +{ + no_gvl_stat_data data; + + data.file.path = path; + data.st = st; + + return IO_WITHOUT_GVL_INT(no_gvl_lstat, &data); } +#endif /* HAVE_LSTAT */ /* * call-seq: - * File.lstat(file_name) => stat - * - * Same as <code>File::stat</code>, but does not follow the last symbolic - * link. Instead, reports on the link itself. - * - * File.symlink("testfile", "link2test") #=> 0 - * File.stat("testfile").size #=> 66 - * File.lstat("link2test").size #=> 8 - * File.stat("link2test").size #=> 66 - * + * File.lstat(filepath) -> stat + * + * Like File::stat, but does not follow the last symbolic link; + * instead, returns a File::Stat object for the link itself. + * + * File.symlink('t.txt', 'symlink') + * File.stat('symlink').size # => 47 + * File.lstat('symlink').size # => 5 + * */ static VALUE -rb_file_s_lstat(klass, fname) - VALUE klass, fname; +rb_file_s_lstat(VALUE klass, VALUE fname) { #ifdef HAVE_LSTAT - struct stat st; + rb_io_stat_data st; - SafeStringValue(fname); - if (lstat(StringValueCStr(fname), &st) == -1) { - rb_sys_fail(RSTRING(fname)->ptr); + FilePathValue(fname); + fname = rb_str_encode_ospath(fname); + if (lstatx_without_gvl(StringValueCStr(fname), &st, STATX_ALL) == -1) { + rb_sys_fail_path(fname); } - return stat_new(&st); + return rb_statx_new(&st); #else return rb_file_s_stat(klass, fname); #endif } - /* * call-seq: - * file.lstat => stat - * - * Same as <code>IO#stat</code>, but does not follow the last symbolic - * link. Instead, reports on the link itself. - * - * File.symlink("testfile", "link2test") #=> 0 - * File.stat("testfile").size #=> 66 - * f = File.new("link2test") - * f.lstat.size #=> 8 - * f.stat.size #=> 66 + * lstat -> stat + * + * Like File#stat, but does not follow the last symbolic link; + * instead, returns a File::Stat object for the link itself: + * + * File.symlink('t.txt', 'symlink') + * f = File.new('symlink') + * f.stat.size # => 47 + * f.lstat.size # => 11 + * */ static VALUE -rb_file_lstat(obj) - VALUE obj; +rb_file_lstat(VALUE obj) { #ifdef HAVE_LSTAT rb_io_t *fptr; - struct stat st; + rb_io_stat_data st; + VALUE path; - rb_secure(2); GetOpenFile(obj, fptr); - if (!fptr->path) return Qnil; - if (lstat(fptr->path, &st) == -1) { - rb_sys_fail(fptr->path); + if (NIL_P(fptr->pathv)) return Qnil; + path = rb_str_encode_ospath(fptr->pathv); + if (lstatx_without_gvl(RSTRING_PTR(path), &st, STATX_ALL) == -1) { + rb_sys_fail_path(fptr->pathv); } - return stat_new(&st); + return rb_statx_new(&st); #else return rb_io_stat(obj); #endif } -#ifndef HAVE_GROUP_MEMBER static int -group_member(gid) - GETGROUPS_T gid; +rb_group_member(GETGROUPS_T gid) { -#ifndef _WIN32 - if (getgid() == gid || getegid() == gid) - return Qtrue; - -# ifdef HAVE_GETGROUPS -# ifndef NGROUPS -# ifdef NGROUPS_MAX -# define NGROUPS NGROUPS_MAX -# else -# define NGROUPS 32 -# endif -# endif - { - GETGROUPS_T gary[NGROUPS]; - int anum; +#if defined(_WIN32) || !defined(HAVE_GETGROUPS) + return FALSE; +#else + int rv = FALSE; + int groups; + VALUE v = 0; + GETGROUPS_T *gary; + int anum = -1; - anum = getgroups(NGROUPS, gary); - while (--anum >= 0) - if (gary[anum] == gid) - return Qtrue; + if (getgid() == gid || getegid() == gid) + return TRUE; + + groups = getgroups(0, NULL); + gary = ALLOCV_N(GETGROUPS_T, v, groups); + anum = getgroups(groups, gary); + while (--anum >= 0) { + if (gary[anum] == gid) { + rv = TRUE; + break; + } } -# endif -#endif - return Qfalse; + if (v) + ALLOCV_END(v); + + return rv; +#endif /* defined(_WIN32) || !defined(HAVE_GETGROUPS) */ } -#endif #ifndef S_IXUGO # define S_IXUGO (S_IXUSR | S_IXGRP | S_IXOTH) @@ -861,76 +1637,128 @@ group_member(gid) #ifndef HAVE_EACCESS int -eaccess(path, mode) - const char *path; - int mode; +eaccess(const char *path, int mode) { #ifdef USE_GETEUID struct stat st; - int euid; - - if (stat(path, &st) < 0) return -1; + rb_uid_t euid; euid = geteuid(); + /* no setuid nor setgid. run shortcut. */ + if (getuid() == euid && getgid() == getegid()) + return access(path, mode); + + if (STAT(path, &st) < 0) + return -1; + if (euid == 0) { - /* Root can read or write any file. */ - if (!(mode & X_OK)) - return 0; + /* Root can read or write any file. */ + if (!(mode & X_OK)) + return 0; - /* Root can execute any file that has any one of the execute - bits set. */ - if (st.st_mode & S_IXUGO) - return 0; + /* Root can execute any file that has any one of the execute + bits set. */ + if (st.st_mode & S_IXUGO) + return 0; - return -1; + return -1; } if (st.st_uid == euid) /* owner */ - mode <<= 6; - else if (group_member(st.st_gid)) - mode <<= 3; + mode <<= 6; + else if (rb_group_member(st.st_gid)) + mode <<= 3; - if ((st.st_mode & mode) == mode) return 0; + if ((int)(st.st_mode & mode) == mode) return 0; return -1; #else -# if defined(_MSC_VER) || defined(__MINGW32__) - mode &= ~1; -# endif return access(path, mode); -#endif +#endif /* USE_GETEUID */ } -#endif +#endif /* HAVE_EACCESS */ + +struct access_arg { + const char *path; + int mode; +}; + +static void * +nogvl_eaccess(void *ptr) +{ + struct access_arg *aa = ptr; + + return (void *)(VALUE)eaccess(aa->path, aa->mode); +} + +static int +rb_eaccess(VALUE fname, int mode) +{ + struct access_arg aa; + + FilePathValue(fname); + fname = rb_str_encode_ospath(fname); + aa.path = StringValueCStr(fname); + aa.mode = mode; + + return IO_WITHOUT_GVL_INT(nogvl_eaccess, &aa); +} + +static void * +nogvl_access(void *ptr) +{ + struct access_arg *aa = ptr; + + return (void *)(VALUE)access(aa->path, aa->mode); +} + +static int +rb_access(VALUE fname, int mode) +{ + struct access_arg aa; + FilePathValue(fname); + fname = rb_str_encode_ospath(fname); + aa.path = StringValueCStr(fname); + aa.mode = mode; + + return IO_WITHOUT_GVL_INT(nogvl_access, &aa); +} /* * Document-class: FileTest * - * <code>FileTest</code> implements file test operations similar to - * those used in <code>File::Stat</code>. It exists as a standalone - * module, and its methods are also insinuated into the <code>File</code> - * class. (Note that this is not done by inclusion: the interpreter cheats). - * + * FileTest implements file test operations similar to those used in + * File::Stat. It exists as a standalone module, and its methods are + * also insinuated into the File class. (Note that this is not done + * by inclusion: the interpreter cheats). + * */ - /* * call-seq: - * File.directory?(file_name) => true or false + * File.directory?(path) -> true or false * - * Returns <code>true</code> if the named file is a directory, - * <code>false</code> otherwise. + * With string +object+ given, returns +true+ if +path+ is a string path + * leading to a directory, or to a symbolic link to a directory; +false+ otherwise: + * + * File.directory?('.') # => true + * File.directory?('foo') # => false + * File.symlink('.', 'dirlink') # => 0 + * File.directory?('dirlink') # => true + * File.symlink('t,txt', 'filelink') # => 0 + * File.directory?('filelink') # => false + * + * Argument +path+ can be an IO object. * - * File.directory?(".") */ -static VALUE -test_d(obj, fname) - VALUE obj, fname; +VALUE +rb_file_directory_p(VALUE obj, VALUE fname) { #ifndef S_ISDIR -# define S_ISDIR(m) ((m & S_IFMT) == S_IFDIR) +# define S_ISDIR(m) (((m) & S_IFMT) == S_IFDIR) #endif struct stat st; @@ -942,18 +1770,22 @@ test_d(obj, fname) /* * call-seq: - * File.pipe?(file_name) => true or false + * File.pipe?(filepath) -> true or false + * + * Returns +true+ if +filepath+ points to a pipe, +false+ otherwise: + * + * File.mkfifo('tmp/fifo') + * File.pipe?('tmp/fifo') # => true + * File.pipe?('t.txt') # => false * - * Returns <code>true</code> if the named file is a pipe. */ static VALUE -test_p(obj, fname) - VALUE obj, fname; +rb_file_pipe_p(VALUE obj, VALUE fname) { #ifdef S_IFIFO # ifndef S_ISFIFO -# define S_ISFIFO(m) ((m & S_IFMT) == S_IFIFO) +# define S_ISFIFO(m) (((m) & S_IFMT) == S_IFIFO) # endif struct stat st; @@ -967,32 +1799,28 @@ test_p(obj, fname) /* * call-seq: - * File.symlink?(file_name) => true or false + * File.symlink?(filepath) -> true or false + * + * Returns +true+ if +filepath+ points to a symbolic link, +false+ otherwise: + * + * symlink = File.symlink('t.txt', 'symlink') + * File.symlink?('symlink') # => true + * File.symlink?('t.txt') # => false * - * Returns <code>true</code> if the named file is a symbolic link. */ static VALUE -test_l(obj, fname) - VALUE obj, fname; +rb_file_symlink_p(VALUE obj, VALUE fname) { #ifndef S_ISLNK # ifdef _S_ISLNK # define S_ISLNK(m) _S_ISLNK(m) -# elif defined __BORLANDC__ -# ifdef _S_IFLNK -# define S_ISLNK(m) (((unsigned short)(m) & S_IFMT) == _S_IFLNK) -# else -# ifdef S_IFLNK -# define S_ISLNK(m) (((unsigned short)(m) & S_IFMT) == S_IFLNK) -# endif -# endif # else # ifdef _S_IFLNK -# define S_ISLNK(m) ((m & S_IFMT) == _S_IFLNK) +# define S_ISLNK(m) (((m) & S_IFMT) == _S_IFLNK) # else # ifdef S_IFLNK -# define S_ISLNK(m) ((m & S_IFMT) == S_IFLNK) +# define S_ISLNK(m) (((m) & S_IFMT) == S_IFLNK) # endif # endif # endif @@ -1001,8 +1829,9 @@ test_l(obj, fname) #ifdef S_ISLNK struct stat st; - SafeStringValue(fname); - if (lstat(StringValueCStr(fname), &st) < 0) return Qfalse; + FilePathValue(fname); + fname = rb_str_encode_ospath(fname); + if (lstat_without_gvl(StringValueCStr(fname), &st) < 0) return Qfalse; if (S_ISLNK(st.st_mode)) return Qtrue; #endif @@ -1011,32 +1840,28 @@ test_l(obj, fname) /* * call-seq: - * File.socket?(file_name) => true or false + * File.socket?(filepath) -> true or false + * + * Returns +true+ if +filepath+ points to a socket, +false+ otherwise: + * + * require 'socket' + * File.socket?(Socket.new(:INET, :STREAM)) # => true + * File.socket?(File.new('t.txt')) # => false * - * Returns <code>true</code> if the named file is a socket. */ static VALUE -test_S(obj, fname) - VALUE obj, fname; +rb_file_socket_p(VALUE obj, VALUE fname) { #ifndef S_ISSOCK # ifdef _S_ISSOCK # define S_ISSOCK(m) _S_ISSOCK(m) -# elif defined __BORLANDC__ -# ifdef _S_IFSOCK -# define S_ISSOCK(m) (((unsigned short)(m) & S_IFMT) == _S_IFSOCK) -# else -# ifdef S_IFSOCK -# define S_ISSOCK(m) (((unsigned short)(m) & S_IFMT) == S_IFSOCK) -# endif -# endif # else # ifdef _S_IFSOCK -# define S_ISSOCK(m) ((m & S_IFMT) == _S_IFSOCK) +# define S_ISSOCK(m) (((m) & S_IFMT) == _S_IFSOCK) # else # ifdef S_IFSOCK -# define S_ISSOCK(m) ((m & S_IFMT) == S_IFSOCK) +# define S_ISSOCK(m) (((m) & S_IFMT) == S_IFSOCK) # endif # endif # endif @@ -1047,25 +1872,28 @@ test_S(obj, fname) if (rb_stat(fname, &st) < 0) return Qfalse; if (S_ISSOCK(st.st_mode)) return Qtrue; - #endif + return Qfalse; } /* * call-seq: - * File.blockdev?(file_name) => true or false + * File.blockdev?(filepath) -> true or false + * + * Returns +true+ if +filepath+ points to a block device, +false+ otherwise: + * + * File.blockdev?('/dev/sda1') # => true + * File.blockdev?(File.new('t.tmp')) # => false * - * Returns <code>true</code> if the named file is a block device. */ static VALUE -test_b(obj, fname) - VALUE obj, fname; +rb_file_blockdev_p(VALUE obj, VALUE fname) { #ifndef S_ISBLK # ifdef S_IFBLK -# define S_ISBLK(m) ((m & S_IFMT) == S_IFBLK) +# define S_ISBLK(m) (((m) & S_IFMT) == S_IFBLK) # else # define S_ISBLK(m) (0) /* anytime false */ # endif @@ -1083,16 +1911,19 @@ test_b(obj, fname) /* * call-seq: - * File.chardev?(file_name) => true or false + * File.chardev?(filepath) -> true or false + * + * Returns +true+ if +filepath+ points to a character device, +false+ otherwise. + * + * File.chardev?($stdin) # => true + * File.chardev?('t.txt') # => false * - * Returns <code>true</code> if the named file is a character device. */ static VALUE -test_c(obj, fname) - VALUE obj, fname; +rb_file_chardev_p(VALUE obj, VALUE fname) { #ifndef S_ISCHR -# define S_ISCHR(m) ((m & S_IFMT) == S_IFCHR) +# define S_ISCHR(m) (((m) & S_IFMT) == S_IFCHR) #endif struct stat st; @@ -1103,18 +1934,19 @@ test_c(obj, fname) return Qfalse; } - /* * call-seq: - * File.exist?(file_name) => true or false - * File.exists?(file_name) => true or false (obsolete) + * File.exist?(file_name) -> true or false * * Return <code>true</code> if the named file exists. + * + * _file_name_ can be an IO object. + * + * "file exists" means that stat() or fstat() system call is successful. */ static VALUE -test_e(obj, fname) - VALUE obj, fname; +rb_file_exist_p(VALUE obj, VALUE fname) { struct stat st; @@ -1124,160 +1956,238 @@ test_e(obj, fname) /* * call-seq: - * File.readable?(file_name) => true or false + * File.readable?(file_name) -> true or false * * Returns <code>true</code> if the named file is readable by the effective - * user id of this process. + * user and group id of this process. See eaccess(3). + * + * Note that some OS-level security features may cause this to return true + * even though the file is not readable by the effective user/group. */ static VALUE -test_r(obj, fname) - VALUE obj, fname; +rb_file_readable_p(VALUE obj, VALUE fname) { - SafeStringValue(fname); - if (eaccess(StringValueCStr(fname), R_OK) < 0) return Qfalse; - return Qtrue; + return RBOOL(rb_eaccess(fname, R_OK) >= 0); } /* * call-seq: - * File.readable_real?(file_name) => true or false + * File.readable_real?(file_name) -> true or false * * Returns <code>true</code> if the named file is readable by the real - * user id of this process. + * user and group id of this process. See access(3). + * + * Note that some OS-level security features may cause this to return true + * even though the file is not readable by the real user/group. */ static VALUE -test_R(obj, fname) - VALUE obj, fname; +rb_file_readable_real_p(VALUE obj, VALUE fname) { - SafeStringValue(fname); - if (access(StringValueCStr(fname), R_OK) < 0) return Qfalse; - return Qtrue; + return RBOOL(rb_access(fname, R_OK) >= 0); } +#ifndef S_IRUGO +# define S_IRUGO (S_IRUSR | S_IRGRP | S_IROTH) +#endif + +#ifndef S_IWUGO +# define S_IWUGO (S_IWUSR | S_IWGRP | S_IWOTH) +#endif /* * call-seq: - * File.writable?(file_name) => true or false + * File.world_readable?(file_name) -> integer or nil + * + * If <i>file_name</i> is readable by others, returns an integer + * representing the file permission bits of <i>file_name</i>. Returns + * <code>nil</code> otherwise. The meaning of the bits is platform + * dependent; on Unix systems, see <code>stat(2)</code>. + * + * _file_name_ can be an IO object. + * + * File.world_readable?("/etc/passwd") #=> 420 + * m = File.world_readable?("/etc/passwd") + * sprintf("%o", m) #=> "644" + */ + +static VALUE +rb_file_world_readable_p(VALUE obj, VALUE fname) +{ +#ifdef S_IROTH + struct stat st; + + if (rb_stat(fname, &st) < 0) return Qnil; + if ((st.st_mode & (S_IROTH)) == S_IROTH) { + return UINT2NUM(st.st_mode & (S_IRUGO|S_IWUGO|S_IXUGO)); + } +#endif + return Qnil; +} + +/* + * call-seq: + * File.writable?(file_name) -> true or false * * Returns <code>true</code> if the named file is writable by the effective - * user id of this process. + * user and group id of this process. See eaccess(3). + * + * Note that some OS-level security features may cause this to return true + * even though the file is not writable by the effective user/group. */ static VALUE -test_w(obj, fname) - VALUE obj, fname; +rb_file_writable_p(VALUE obj, VALUE fname) { - SafeStringValue(fname); - if (eaccess(StringValueCStr(fname), W_OK) < 0) return Qfalse; - return Qtrue; + return RBOOL(rb_eaccess(fname, W_OK) >= 0); } /* * call-seq: - * File.writable_real?(file_name) => true or false + * File.writable_real?(file_name) -> true or false * * Returns <code>true</code> if the named file is writable by the real - * user id of this process. + * user and group id of this process. See access(3). + * + * Note that some OS-level security features may cause this to return true + * even though the file is not writable by the real user/group. */ static VALUE -test_W(obj, fname) - VALUE obj, fname; +rb_file_writable_real_p(VALUE obj, VALUE fname) { - SafeStringValue(fname); - if (access(StringValueCStr(fname), W_OK) < 0) return Qfalse; - return Qtrue; + return RBOOL(rb_access(fname, W_OK) >= 0); +} + +/* + * call-seq: + * File.world_writable?(file_name) -> integer or nil + * + * If <i>file_name</i> is writable by others, returns an integer + * representing the file permission bits of <i>file_name</i>. Returns + * <code>nil</code> otherwise. The meaning of the bits is platform + * dependent; on Unix systems, see <code>stat(2)</code>. + * + * _file_name_ can be an IO object. + * + * File.world_writable?("/tmp") #=> 511 + * m = File.world_writable?("/tmp") + * sprintf("%o", m) #=> "777" + */ + +static VALUE +rb_file_world_writable_p(VALUE obj, VALUE fname) +{ +#ifdef S_IWOTH + struct stat st; + + if (rb_stat(fname, &st) < 0) return Qnil; + if ((st.st_mode & (S_IWOTH)) == S_IWOTH) { + return UINT2NUM(st.st_mode & (S_IRUGO|S_IWUGO|S_IXUGO)); + } +#endif + return Qnil; } /* * call-seq: - * File.executable?(file_name) => true or false + * File.executable?(file_name) -> true or false * * Returns <code>true</code> if the named file is executable by the effective - * user id of this process. + * user and group id of this process. See eaccess(3). + * + * Windows does not support execute permissions separately from read + * permissions. On Windows, a file is only considered executable if it ends in + * .bat, .cmd, .com, or .exe. + * + * Note that some OS-level security features may cause this to return true + * even though the file is not executable by the effective user/group. */ static VALUE -test_x(obj, fname) - VALUE obj, fname; +rb_file_executable_p(VALUE obj, VALUE fname) { - SafeStringValue(fname); - if (eaccess(StringValueCStr(fname), X_OK) < 0) return Qfalse; - return Qtrue; + return RBOOL(rb_eaccess(fname, X_OK) >= 0); } /* * call-seq: - * File.executable_real?(file_name) => true or false + * File.executable_real?(file_name) -> true or false * * Returns <code>true</code> if the named file is executable by the real - * user id of this process. + * user and group id of this process. See access(3). + * + * Windows does not support execute permissions separately from read + * permissions. On Windows, a file is only considered executable if it ends in + * .bat, .cmd, .com, or .exe. + * + * Note that some OS-level security features may cause this to return true + * even though the file is not executable by the real user/group. */ static VALUE -test_X(obj, fname) - VALUE obj, fname; +rb_file_executable_real_p(VALUE obj, VALUE fname) { - SafeStringValue(fname); - if (access(StringValueCStr(fname), X_OK) < 0) return Qfalse; - return Qtrue; + return RBOOL(rb_access(fname, X_OK) >= 0); } #ifndef S_ISREG -# define S_ISREG(m) ((m & S_IFMT) == S_IFREG) +# define S_ISREG(m) (((m) & S_IFMT) == S_IFREG) #endif /* * call-seq: - * File.file?(file_name) => true or false + * File.file?(file) -> true or false * - * Returns <code>true</code> if the named file exists and is a - * regular file. + * Returns +true+ if the named +file+ exists and is a regular file. + * + * +file+ can be an IO object. + * + * If the +file+ argument is a symbolic link, it will resolve the symbolic link + * and use the file referenced by the link. */ static VALUE -test_f(obj, fname) - VALUE obj, fname; +rb_file_file_p(VALUE obj, VALUE fname) { struct stat st; if (rb_stat(fname, &st) < 0) return Qfalse; - if (S_ISREG(st.st_mode)) return Qtrue; - return Qfalse; + return RBOOL(S_ISREG(st.st_mode)); } /* * call-seq: - * File.zero?(file_name) => true or false + * File.zero?(file_name) -> true or false * * Returns <code>true</code> if the named file exists and has * a zero size. + * + * _file_name_ can be an IO object. */ static VALUE -test_z(obj, fname) - VALUE obj, fname; +rb_file_zero_p(VALUE obj, VALUE fname) { struct stat st; if (rb_stat(fname, &st) < 0) return Qfalse; - if (st.st_size == 0) return Qtrue; - return Qfalse; + return RBOOL(st.st_size == 0); } /* * call-seq: - * File.size?(file_name) => Integer or nil + * File.size?(file_name) -> Integer or nil * * Returns +nil+ if +file_name+ doesn't exist or has zero size, the size of the * file otherwise. + * + * _file_name_ can be an IO object. */ static VALUE -test_s(obj, fname) - VALUE obj, fname; +rb_file_size_p(VALUE obj, VALUE fname) { struct stat st; @@ -1288,82 +2198,78 @@ test_s(obj, fname) /* * call-seq: - * File.owned?(file_name) => true or false + * 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. */ static VALUE -test_owned(obj, fname) - VALUE obj, fname; +rb_file_owned_p(VALUE obj, VALUE fname) { struct stat st; if (rb_stat(fname, &st) < 0) return Qfalse; - if (st.st_uid == geteuid()) return Qtrue; - return Qfalse; + return RBOOL(st.st_uid == geteuid()); } static VALUE -test_rowned(obj, fname) - VALUE obj, fname; +rb_file_rowned_p(VALUE obj, VALUE fname) { struct stat st; if (rb_stat(fname, &st) < 0) return Qfalse; - if (st.st_uid == getuid()) return Qtrue; - return Qfalse; + return RBOOL(st.st_uid == getuid()); } /* * call-seq: - * File.grpowned?(file_name) => true or false + * File.grpowned?(file_name) -> true or false * * Returns <code>true</code> if the named file exists and the * effective group id of the calling process is the owner of * the file. Returns <code>false</code> on Windows. + * + * _file_name_ can be an IO object. */ static VALUE -test_grpowned(obj, fname) - VALUE obj, fname; +rb_file_grpowned_p(VALUE obj, VALUE fname) { #ifndef _WIN32 struct stat st; if (rb_stat(fname, &st) < 0) return Qfalse; - if (group_member(st.st_gid)) return Qtrue; + if (rb_group_member(st.st_gid)) return Qtrue; #endif return Qfalse; } #if defined(S_ISUID) || defined(S_ISGID) || defined(S_ISVTX) static VALUE -check3rdbyte(fname, mode) - VALUE fname; - int mode; +check3rdbyte(VALUE fname, int mode) { struct stat st; - SafeStringValue(fname); - if (stat(StringValueCStr(fname), &st) < 0) return Qfalse; - if (st.st_mode & mode) return Qtrue; - return Qfalse; + if (rb_stat(fname, &st) < 0) return Qfalse; + return RBOOL(st.st_mode & mode); } #endif /* * call-seq: - * File.setuid?(file_name) => true or false + * File.setuid?(file_name) -> true or false * * Returns <code>true</code> if the named file has the setuid bit set. + * + * _file_name_ can be an IO object. */ static VALUE -test_suid(obj, fname) - VALUE obj, fname; +rb_file_suid_p(VALUE obj, VALUE fname) { #ifdef S_ISUID return check3rdbyte(fname, S_ISUID); @@ -1374,14 +2280,15 @@ test_suid(obj, fname) /* * call-seq: - * File.setgid?(file_name) => true or false + * File.setgid?(file_name) -> true or false * * Returns <code>true</code> if the named file has the setgid bit set. + * + * _file_name_ can be an IO object. */ static VALUE -test_sgid(obj, fname) - VALUE obj, fname; +rb_file_sgid_p(VALUE obj, VALUE fname) { #ifdef S_ISGID return check3rdbyte(fname, S_ISGID); @@ -1392,28 +2299,31 @@ test_sgid(obj, fname) /* * call-seq: - * File.sticky?(file_name) => true or false + * File.sticky?(file_name) -> true or false * * Returns <code>true</code> if the named file has the sticky bit set. + * + * _file_name_ can be an IO object. */ static VALUE -test_sticky(obj, fname) - VALUE obj, fname; +rb_file_sticky_p(VALUE obj, VALUE fname) { #ifdef S_ISVTX return check3rdbyte(fname, S_ISVTX); #else - return Qnil; + return Qfalse; #endif } /* * call-seq: - * File.identical?(file_1, file_2) => true or false + * File.identical?(file_1, file_2) -> true or false * * Returns <code>true</code> if the named files are identical. * + * _file_1_ and _file_2_ can be an IO object. + * * open("a", "w") {} * p File.identical?("a", "a") #=> true * p File.identical?("a", "./a") #=> true @@ -1426,611 +2336,824 @@ test_sticky(obj, fname) */ static VALUE -test_identical(obj, fname1, fname2) - VALUE obj, fname1, fname2; +rb_file_identical_p(VALUE obj, VALUE fname1, VALUE fname2) { -#ifndef DOSISH +#ifndef _WIN32 struct stat st1, st2; if (rb_stat(fname1, &st1) < 0) return Qfalse; if (rb_stat(fname2, &st2) < 0) return Qfalse; if (st1.st_dev != st2.st_dev) return Qfalse; if (st1.st_ino != st2.st_ino) return Qfalse; + return Qtrue; #else -#ifdef _WIN32 - BY_HANDLE_FILE_INFORMATION st1, st2; - HANDLE f1 = 0, f2 = 0; -#endif - - rb_secure(2); -#ifdef _WIN32 - f1 = w32_io_info(&fname1, &st1); - if (f1 == INVALID_HANDLE_VALUE) return Qfalse; - f2 = w32_io_info(&fname2, &st2); - if (f1) CloseHandle(f1); - if (f2 == INVALID_HANDLE_VALUE) return Qfalse; - if (f2) CloseHandle(f2); - - if (st1.dwVolumeSerialNumber == st2.dwVolumeSerialNumber && - st1.nFileIndexHigh == st2.nFileIndexHigh && - st1.nFileIndexLow == st2.nFileIndexLow) - return Qtrue; - if (!f1 || !f2) return Qfalse; - if (rb_w32_iswin95()) return Qfalse; -#else - SafeStringValue(fname1); - fname1 = rb_str_new4(fname1); - SafeStringValue(fname2); - if (access(RSTRING(fname1)->ptr, 0)) return Qfalse; - if (access(RSTRING(fname2)->ptr, 0)) return Qfalse; -#endif - fname1 = rb_file_expand_path(fname1, Qnil); - fname2 = rb_file_expand_path(fname2, Qnil); - if (RSTRING(fname1)->len != RSTRING(fname2)->len) return Qfalse; - if (rb_memcicmp(RSTRING(fname1)->ptr, RSTRING(fname2)->ptr, RSTRING(fname1)->len)) - return Qfalse; + extern VALUE rb_w32_file_identical_p(VALUE, VALUE); + return rb_w32_file_identical_p(fname1, fname2); #endif - return Qtrue; } /* * call-seq: - * File.size(file_name) => integer + * File.size(file_name) -> integer * * Returns the size of <code>file_name</code>. + * + * _file_name_ can be an IO object. */ static VALUE -rb_file_s_size(klass, fname) - VALUE klass, fname; +rb_file_s_size(VALUE klass, VALUE fname) { struct stat st; - if (rb_stat(fname, &st) < 0) - rb_sys_fail(StringValueCStr(fname)); + if (rb_stat(fname, &st) < 0) { + int e = errno; + FilePathValue(fname); + rb_syserr_fail_path(e, fname); + } return OFFT2NUM(st.st_size); } static VALUE -rb_file_ftype(st) - struct stat *st; +rb_file_ftype(mode_t mode) { - char *t; + const char *t; - if (S_ISREG(st->st_mode)) { - t = "file"; + if (S_ISREG(mode)) { + t = "file"; } - else if (S_ISDIR(st->st_mode)) { - t = "directory"; + else if (S_ISDIR(mode)) { + t = "directory"; } - else if (S_ISCHR(st->st_mode)) { - t = "characterSpecial"; + else if (S_ISCHR(mode)) { + t = "characterSpecial"; } #ifdef S_ISBLK - else if (S_ISBLK(st->st_mode)) { - t = "blockSpecial"; + else if (S_ISBLK(mode)) { + t = "blockSpecial"; } #endif #ifdef S_ISFIFO - else if (S_ISFIFO(st->st_mode)) { - t = "fifo"; + else if (S_ISFIFO(mode)) { + t = "fifo"; } #endif #ifdef S_ISLNK - else if (S_ISLNK(st->st_mode)) { - t = "link"; + else if (S_ISLNK(mode)) { + t = "link"; } #endif #ifdef S_ISSOCK - else if (S_ISSOCK(st->st_mode)) { - t = "socket"; + else if (S_ISSOCK(mode)) { + t = "socket"; } #endif else { - t = "unknown"; + t = "unknown"; } - return rb_str_new2(t); + return rb_usascii_str_new2(t); } /* * call-seq: - * File.ftype(file_name) => string - * + * File.ftype(file_name) -> string + * * Identifies the type of the named file; the return string is one of * ``<code>file</code>'', ``<code>directory</code>'', * ``<code>characterSpecial</code>'', ``<code>blockSpecial</code>'', * ``<code>fifo</code>'', ``<code>link</code>'', * ``<code>socket</code>'', or ``<code>unknown</code>''. - * + * * File.ftype("testfile") #=> "file" * File.ftype("/dev/tty") #=> "characterSpecial" * File.ftype("/tmp/.X11-unix/X0") #=> "socket" */ static VALUE -rb_file_s_ftype(klass, fname) - VALUE klass, fname; +rb_file_s_ftype(VALUE klass, VALUE fname) { struct stat st; - SafeStringValue(fname); - if (lstat(StringValueCStr(fname), &st) == -1) { - rb_sys_fail(RSTRING(fname)->ptr); + FilePathValue(fname); + fname = rb_str_encode_ospath(fname); + if (lstat_without_gvl(StringValueCStr(fname), &st) == -1) { + rb_sys_fail_path(fname); } - return rb_file_ftype(&st); + return rb_file_ftype(st.st_mode); } /* * call-seq: - * File.atime(file_name) => time - * - * Returns the last access time for the named file as a Time object). - * + * File.atime(file_name) -> time + * + * Returns the last access time for the named file as a Time object. + * + * _file_name_ can be an IO object. + * * File.atime("testfile") #=> Wed Apr 09 08:51:48 CDT 2003 - * + * */ static VALUE -rb_file_s_atime(klass, fname) - VALUE klass, fname; +rb_file_s_atime(VALUE klass, VALUE fname) { struct stat st; - if (rb_stat(fname, &st) < 0) - rb_sys_fail(StringValueCStr(fname)); - return rb_time_new(st.st_atime, 0); + if (rb_stat(fname, &st) < 0) { + int e = errno; + FilePathValue(fname); + rb_syserr_fail_path(e, fname); + } + return stat_time(stat_atimespec(&st)); } /* * call-seq: - * file.atime => time - * - * Returns the last access time (a <code>Time</code> object) - * for <i>file</i>, or epoch if <i>file</i> has not been accessed. - * + * 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 - * + * */ static VALUE -rb_file_atime(obj) - VALUE obj; +rb_file_atime(VALUE obj) { rb_io_t *fptr; struct stat st; GetOpenFile(obj, fptr); - if (fstat(fileno(fptr->f), &st) == -1) { - rb_sys_fail(fptr->path); + if (fstat(fptr->fd, &st) == -1) { + rb_sys_fail_path(fptr->pathv); } - return rb_time_new(st.st_atime, 0); + return stat_time(stat_atimespec(&st)); } /* * call-seq: - * File.mtime(file_name) => time - * + * File.mtime(file_name) -> time + * * Returns the modification time for the named file as a Time object. - * + * + * _file_name_ can be an IO object. + * * File.mtime("testfile") #=> Tue Apr 08 12:58:04 CDT 2003 - * + * */ static VALUE -rb_file_s_mtime(klass, fname) - VALUE klass, fname; +rb_file_s_mtime(VALUE klass, VALUE fname) { struct stat st; - if (rb_stat(fname, &st) < 0) - rb_sys_fail(RSTRING(fname)->ptr); - return rb_time_new(st.st_mtime, 0); + if (rb_stat(fname, &st) < 0) { + int e = errno; + FilePathValue(fname); + rb_syserr_fail_path(e, fname); + } + return stat_time(stat_mtimespec(&st)); } /* * call-seq: - * file.mtime -> time - * + * file.mtime -> time + * * Returns the modification time for <i>file</i>. - * + * * File.new("testfile").mtime #=> Wed Apr 09 08:53:14 CDT 2003 - * + * */ static VALUE -rb_file_mtime(obj) - VALUE obj; +rb_file_mtime(VALUE obj) { rb_io_t *fptr; struct stat st; GetOpenFile(obj, fptr); - if (fstat(fileno(fptr->f), &st) == -1) { - rb_sys_fail(fptr->path); + if (fstat(fptr->fd, &st) == -1) { + rb_sys_fail_path(fptr->pathv); } - return rb_time_new(st.st_mtime, 0); + return stat_time(stat_mtimespec(&st)); } /* * call-seq: - * File.ctime(file_name) => time - * + * File.ctime(file_name) -> time + * * Returns the change time for the named file (the time at which * directory information about the file was changed, not the file * itself). - * + * + * _file_name_ can be an IO object. + * + * Note that on Windows (NTFS), returns creation time (birth time). + * * File.ctime("testfile") #=> Wed Apr 09 08:53:13 CDT 2003 - * + * */ static VALUE -rb_file_s_ctime(klass, fname) - VALUE klass, fname; +rb_file_s_ctime(VALUE klass, VALUE fname) { struct stat st; - if (rb_stat(fname, &st) < 0) - rb_sys_fail(RSTRING(fname)->ptr); - return rb_time_new(st.st_ctime, 0); + if (rb_stat(fname, &st) < 0) { + int e = errno; + FilePathValue(fname); + rb_syserr_fail_path(e, fname); + } + return stat_time(stat_ctimespec(&st)); } /* * call-seq: - * file.ctime -> time - * + * file.ctime -> time + * * Returns the change time for <i>file</i> (that is, the time directory * information about the file was changed, not the file itself). - * + * + * Note that on Windows (NTFS), returns creation time (birth time). + * * File.new("testfile").ctime #=> Wed Apr 09 08:53:14 CDT 2003 - * + * */ static VALUE -rb_file_ctime(obj) - VALUE obj; +rb_file_ctime(VALUE obj) { rb_io_t *fptr; struct stat st; GetOpenFile(obj, fptr); - if (fstat(fileno(fptr->f), &st) == -1) { - rb_sys_fail(fptr->path); + if (fstat(fptr->fd, &st) == -1) { + rb_sys_fail_path(fptr->pathv); } - return rb_time_new(st.st_ctime, 0); + return stat_time(stat_ctimespec(&st)); } -static void chmod_internal _((const char *, void *)); -static void -chmod_internal(path, mode) +#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("testfile") #=> Wed Apr 09 08:53:13 CDT 2003 + * + * If the platform doesn't have birthtime, raises NotImplementedError. + * + */ + +VALUE +rb_file_s_birthtime(VALUE klass, VALUE fname) +{ + 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); +} +#else +# define rb_file_s_birthtime rb_f_notimplement +#endif + +#if defined(HAVE_STAT_BIRTHTIME) +/* + * call-seq: + * file.birthtime -> time + * + * Returns the birth time for <i>file</i>. + * + * File.new("testfile").birthtime #=> Wed Apr 09 08:53:14 CDT 2003 + * + * If the platform doesn't have birthtime, raises NotImplementedError. + * + */ + +static VALUE +rb_file_birthtime(VALUE obj) +{ + rb_io_t *fptr; + rb_io_stat_data st; + + GetOpenFile(obj, fptr); + if (fstatx_without_gvl(fptr, &st, STATX_BTIME) == -1) { + rb_sys_fail_path(fptr->pathv); + } + return statx_birthtime(&st); +} +#else +# define rb_file_birthtime rb_f_notimplement +#endif + +rb_off_t +rb_file_size(VALUE file) +{ + if (RB_TYPE_P(file, T_FILE)) { + rb_io_t *fptr; + struct stat st; + + RB_IO_POINTER(file, fptr); + if (fptr->mode & FMODE_WRITABLE) { + rb_io_flush_raw(file, 0); + } + + if (fstat(fptr->fd, &st) == -1) { + rb_sys_fail_path(fptr->pathv); + } + + return st.st_size; + } + else { + return NUM2OFFT(rb_funcall(file, idSize, 0)); + } +} + +/* + * 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; - void *mode; + 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) { - if (chmod(path, *(int *)mode) < 0) - rb_sys_fail(path); + 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) +{ + return chmod(path, *(mode_t *)mode); } /* * call-seq: - * File.chmod(mode_int, file_name, ... ) -> integer - * + * File.chmod(mode_int, file_name, ... ) -> integer + * * Changes permission bits on the named file(s) to the bit pattern * represented by <i>mode_int</i>. Actual effects are operating system * dependent (see the beginning of this section). On Unix systems, see * <code>chmod(2)</code> for details. Returns the number of files * processed. - * + * * File.chmod(0644, "testfile", "out") #=> 2 */ static VALUE -rb_file_s_chmod(argc, argv) - int argc; - VALUE *argv; +rb_file_s_chmod(int argc, VALUE *argv, VALUE _) { - VALUE vmode; - VALUE rest; - int mode; - long n; + mode_t mode; + + apply2args(1); + mode = NUM2MODET(*argv++); + + return apply2files(chmod_internal, argc, argv, &mode); +} + +#ifdef HAVE_FCHMOD +struct nogvl_fchmod_data { + int fd; + mode_t mode; +}; - rb_secure(2); - rb_scan_args(argc, argv, "1*", &vmode, &rest); - mode = NUM2INT(vmode); +static VALUE +io_blocking_fchmod(void *ptr) +{ + struct nogvl_fchmod_data *data = ptr; + int ret = fchmod(data->fd, data->mode); + return (VALUE)ret; +} - n = apply2files(chmod_internal, rest, &mode); - return LONG2FIX(n); +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 - * + * file.chmod(mode_int) -> 0 + * * Changes permission bits on <i>file</i> to the bit pattern * represented by <i>mode_int</i>. Actual effects are platform * dependent; on Unix systems, see <code>chmod(2)</code> for details. - * Follows symbolic links. Also see <code>File#lchmod</code>. - * + * Follows symbolic links. Also see File#lchmod. + * * f = File.new("out", "w"); * f.chmod(0644) #=> 0 */ static VALUE -rb_file_chmod(obj, vmode) - VALUE obj, vmode; +rb_file_chmod(VALUE obj, VALUE vmode) { rb_io_t *fptr; - int mode; + mode_t mode; +#if !defined HAVE_FCHMOD || !HAVE_FCHMOD + VALUE path; +#endif - rb_secure(2); - mode = NUM2INT(vmode); + mode = NUM2MODET(vmode); GetOpenFile(obj, fptr); #ifdef HAVE_FCHMOD - if (fchmod(fileno(fptr->f), mode) == -1) - rb_sys_fail(fptr->path); -#else - if (!fptr->path) return Qnil; - if (chmod(fptr->path, mode) == -1) - rb_sys_fail(fptr->path); + if (rb_fchmod(fptr, mode) == -1) { + if (HAVE_FCHMOD || errno != ENOSYS) + rb_sys_fail_path(fptr->pathv); + } + else { + if (!HAVE_FCHMOD) return INT2FIX(0); + } +#endif +#if !defined HAVE_FCHMOD || !HAVE_FCHMOD + if (NIL_P(fptr->pathv)) return Qnil; + path = rb_str_encode_ospath(fptr->pathv); + if (rb_chmod(RSTRING_PTR(path), mode) == -1) + rb_sys_fail_path(fptr->pathv); #endif return INT2FIX(0); } #if defined(HAVE_LCHMOD) -static void lchmod_internal _((const char *, void *)); -static void -lchmod_internal(path, mode) - const char *path; - void *mode; +static int +lchmod_internal(const char *path, void *mode) { - if (lchmod(path, (int)(VALUE)mode) < 0) - rb_sys_fail(path); + return lchmod(path, *(mode_t *)mode); } /* * call-seq: - * File.lchmod(mode_int, file_name, ...) => integer - * - * Equivalent to <code>File::chmod</code>, but does not follow symbolic - * links (so it will change the permissions associated with the link, - * not the file referenced by the link). Often not available. - * + * File.lchmod(mode_int, file_name, ...) -> integer + * + * Equivalent to File::chmod, but does not follow symbolic links (so + * it will change the permissions associated with the link, not the + * file referenced by the link). Often not available. + * */ static VALUE -rb_file_s_lchmod(argc, argv) - int argc; - VALUE *argv; +rb_file_s_lchmod(int argc, VALUE *argv, VALUE _) { - VALUE vmode; - VALUE rest; - long mode, n; + mode_t mode; - rb_secure(2); - rb_scan_args(argc, argv, "1*", &vmode, &rest); - mode = NUM2INT(vmode); + apply2args(1); + mode = NUM2MODET(*argv++); - n = apply2files(lchmod_internal, rest, (void *)(long)mode); - return LONG2FIX(n); + return apply2files(lchmod_internal, argc, argv, &mode); } #else -static VALUE -rb_file_s_lchmod(argc, argv) - int argc; - VALUE *argv; +#define rb_file_s_lchmod rb_f_notimplement +#endif + +static inline rb_uid_t +to_uid(VALUE u) { - rb_notimplement(); - return Qnil; /* not reached */ + if (NIL_P(u)) { + return (rb_uid_t)-1; + } + return NUM2UIDT(u); +} + +static inline rb_gid_t +to_gid(VALUE g) +{ + if (NIL_P(g)) { + return (rb_gid_t)-1; + } + return NUM2GIDT(g); } -#endif struct chown_args { - int owner, group; + rb_uid_t owner; + rb_gid_t group; }; -static void chown_internal _((const char *, void *)); -static void -chown_internal(path, argp) - const char *path; - void *argp; +static int +chown_internal(const char *path, void *arg) { - struct chown_args *args = (struct chown_args *)argp; - if (chown(path, args->owner, args->group) < 0) - rb_sys_fail(path); + struct chown_args *args = arg; + return chown(path, args->owner, args->group); } /* * call-seq: - * File.chown(owner_int, group_int, file_name,... ) -> integer - * + * File.chown(owner_int, group_int, file_name, ...) -> integer + * * Changes the owner and group of the named file(s) to the given * numeric owner and group id's. Only a process with superuser * privileges may change the owner of a file. The current owner of a * file may change the file's group to any group to which the owner * belongs. A <code>nil</code> or -1 owner or group id is ignored. * Returns the number of files processed. - * + * * File.chown(nil, 100, "testfile") - * + * */ static VALUE -rb_file_s_chown(argc, argv) - int argc; - VALUE *argv; +rb_file_s_chown(int argc, VALUE *argv, VALUE _) { - VALUE o, g, rest; struct chown_args arg; - long n; - rb_secure(2); - rb_scan_args(argc, argv, "2*", &o, &g, &rest); - if (NIL_P(o)) { - arg.owner = -1; - } - else { - arg.owner = NUM2INT(o); - } - if (NIL_P(g)) { - arg.group = -1; - } - else { - arg.group = NUM2INT(g); - } + apply2args(2); + arg.owner = to_uid(*argv++); + arg.group = to_gid(*argv++); + + 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); +} - n = apply2files(chown_internal, rest, &arg); - return LONG2FIX(n); +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 - * + * file.chown(owner_int, group_int ) -> 0 + * * Changes the owner and group of <i>file</i> to the given numeric * owner and group id's. Only a process with superuser privileges may * change the owner of a file. The current owner of a file may change * the file's group to any group to which the owner belongs. A * <code>nil</code> or -1 owner or group id is ignored. Follows - * symbolic links. See also <code>File#lchown</code>. - * + * symbolic links. See also File#lchown. + * * File.new("testfile").chown(502, 1000) - * + * */ static VALUE -rb_file_chown(obj, owner, group) - VALUE obj, owner, group; +rb_file_chown(VALUE obj, VALUE owner, VALUE group) { rb_io_t *fptr; - int o, g; + rb_uid_t o; + rb_gid_t g; +#ifndef HAVE_FCHOWN + VALUE path; +#endif - rb_secure(2); - o = NIL_P(owner) ? -1 : NUM2INT(owner); - g = NIL_P(group) ? -1 : NUM2INT(group); + o = to_uid(owner); + g = to_gid(group); GetOpenFile(obj, fptr); -#if defined(DJGPP) || defined(__CYGWIN32__) || defined(_WIN32) || defined(__EMX__) - if (!fptr->path) return Qnil; - if (chown(fptr->path, o, g) == -1) - rb_sys_fail(fptr->path); +#ifndef HAVE_FCHOWN + if (NIL_P(fptr->pathv)) return Qnil; + path = rb_str_encode_ospath(fptr->pathv); + if (rb_chown(RSTRING_PTR(path), o, g) == -1) + rb_sys_fail_path(fptr->pathv); #else - if (fchown(fileno(fptr->f), o, g) == -1) - rb_sys_fail(fptr->path); + if (rb_fchown(fptr->fd, o, g) == -1) + rb_sys_fail_path(fptr->pathv); #endif return INT2FIX(0); } -#if defined(HAVE_LCHOWN) && !defined(__CHECKER__) -static void lchown_internal _((const char *, void *)); -static void -lchown_internal(path, argp) - const char *path; - void *argp; +#if defined(HAVE_LCHOWN) +static int +lchown_internal(const char *path, void *arg) { - struct chown_args *args = (struct chown_args *)argp; - if (lchown(path, args->owner, args->group) < 0) - rb_sys_fail(path); + struct chown_args *args = arg; + return lchown(path, args->owner, args->group); } - /* * call-seq: - * file.lchown(owner_int, group_int, file_name,..) => integer - * - * Equivalent to <code>File::chown</code>, but does not follow symbolic + * File.lchown(owner_int, group_int, file_name,..) -> integer + * + * Equivalent to File::chown, but does not follow symbolic * links (so it will change the owner associated with the link, not the * file referenced by the link). Often not available. Returns number * of files in the argument list. - * + * */ static VALUE -rb_file_s_lchown(argc, argv) - int argc; - VALUE *argv; +rb_file_s_lchown(int argc, VALUE *argv, VALUE _) { - VALUE o, g, rest; struct chown_args arg; - long n; - rb_secure(2); - rb_scan_args(argc, argv, "2*", &o, &g, &rest); - if (NIL_P(o)) { - arg.owner = -1; - } - else { - arg.owner = NUM2INT(o); - } - if (NIL_P(g)) { - arg.group = -1; - } - else { - arg.group = NUM2INT(g); - } + apply2args(2); + arg.owner = to_uid(*argv++); + arg.group = to_gid(*argv++); - n = apply2files(lchown_internal, rest, &arg); - return LONG2FIX(n); + return apply2files(lchown_internal, argc, argv, &arg); } #else -static VALUE -rb_file_s_lchown(argc, argv) - int argc; - VALUE *argv; -{ - rb_notimplement(); -} +#define rb_file_s_lchown rb_f_notimplement #endif -struct timeval rb_time_timeval(); - -static void utime_internal _((const char *, void *)); +struct utime_args { + const struct timespec* tsp; + VALUE atime, mtime; + int follow; /* Whether to act on symlinks (1) or their referent (0) */ +}; -#if defined(HAVE_UTIMES) && !defined(__CHECKER__) +#ifdef UTIME_EINVAL +NORETURN(static void utime_failed(struct apply_arg *)); static void -utime_internal(path, arg) - const char *path; - void *arg; -{ - struct timeval *tvp = arg; - if (utimes(path, tvp) < 0) - rb_sys_fail(path); +utime_failed(struct apply_arg *aa) +{ + int e = aa->errnum; + VALUE path = aa->fn[aa->i].path; + struct utime_args *ua = aa->arg; + + if (ua->tsp && e == EINVAL) { + VALUE e[2], a = Qnil, m = Qnil; + int d = 0; + VALUE atime = ua->atime; + VALUE mtime = ua->mtime; + + if (!NIL_P(atime)) { + a = rb_inspect(atime); + } + if (!NIL_P(mtime) && mtime != atime && !rb_equal(atime, mtime)) { + m = rb_inspect(mtime); + } + if (NIL_P(a)) e[0] = m; + else if (NIL_P(m) || rb_str_cmp(a, m) == 0) e[0] = a; + else { + e[0] = rb_str_plus(a, rb_str_new_cstr(" or ")); + rb_str_append(e[0], m); + d = 1; + } + if (!NIL_P(e[0])) { + if (path) { + if (!d) e[0] = rb_str_dup(e[0]); + rb_str_append(rb_str_cat2(e[0], " for "), path); + } + e[1] = INT2FIX(EINVAL); + rb_exc_raise(rb_class_new_instance(2, e, rb_eSystemCallError)); + } + } + rb_syserr_fail_path(e, path); } +#endif /* UTIME_EINVAL */ -/* - * call-seq: - * File.utime(atime, mtime, file_name,...) => integer - * - * Sets the access and modification times of each - * named file to the first two arguments. Returns - * the number of file names in the argument list. - */ +#if defined(HAVE_UTIMES) -static VALUE -rb_file_s_utime(argc, argv) - int argc; - VALUE *argv; +# 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 __has_attribute(availability) && __has_warning("-Wunguarded-availability-new") +typedef int utimensat_func(int, const char *, const struct timespec [2], int); + +RBIMPL_WARNING_PUSH() +RBIMPL_WARNING_IGNORED(-Wunguarded-availability-new) +static inline utimensat_func * +rb_utimensat(void) { - VALUE atime, mtime, rest; - struct timeval tvs[2], *tvp = NULL; - long n; + return &utimensat; +} +RBIMPL_WARNING_POP() - rb_secure(2); - rb_scan_args(argc, argv, "2*", &atime, &mtime, &rest); +# 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 /* utimesat availability */ +# endif /* __APPLE__ && < MAC_OS_X_VERSION_13_0 */ - if (!NIL_P(atime) || !NIL_P(mtime)) { - tvp = tvs; - tvp[0] = rb_time_timeval(atime); - tvp[1] = rb_time_timeval(mtime); - } +static int +utime_internal(const char *path, void *arg) +{ + struct utime_args *v = arg; + const struct timespec *tsp = v->tsp; + struct timeval tvbuf[2], *tvp = NULL; + +#if defined(HAVE_UTIMENSAT) +# if defined(__APPLE__) + const int try_utimensat = utimensat != NULL; + const int try_utimensat_follow = utimensat != NULL; +# else /* !__APPLE__ */ +# define TRY_UTIMENSAT 1 + static int try_utimensat = 1; +# ifdef AT_SYMLINK_NOFOLLOW + static int try_utimensat_follow = 1; +# else + const int try_utimensat_follow = 0; +# endif +# endif /* __APPLE__ */ + int flags = 0; + + if (v->follow ? try_utimensat_follow : try_utimensat) { +# ifdef AT_SYMLINK_NOFOLLOW + if (v->follow) { + flags = AT_SYMLINK_NOFOLLOW; + } +# endif - n = apply2files(utime_internal, rest, tvp); - return LONG2FIX(n); + int result = utimensat(AT_FDCWD, path, tsp, flags); +# ifdef TRY_UTIMENSAT + if (result < 0 && errno == ENOSYS) { +# ifdef AT_SYMLINK_NOFOLLOW + try_utimensat_follow = 0; +# endif /* AT_SYMLINK_NOFOLLOW */ + if (!v->follow) + try_utimensat = 0; + } + else +# endif /* TRY_UTIMESAT */ + return result; + } +#endif /* defined(HAVE_UTIMENSAT) */ + + if (tsp) { + tvbuf[0].tv_sec = tsp[0].tv_sec; + tvbuf[0].tv_usec = (int)(tsp[0].tv_nsec / 1000); + tvbuf[1].tv_sec = tsp[1].tv_sec; + tvbuf[1].tv_usec = (int)(tsp[1].tv_nsec / 1000); + tvp = tvbuf; + } +#ifdef HAVE_LUTIMES + if (v->follow) return lutimes(path, tvp); +#endif + return utimes(path, tvp); } -#else +#else /* !defined(HAVE_UTIMES) */ #if !defined HAVE_UTIME_H && !defined HAVE_SYS_UTIME_H struct utimbuf { @@ -2039,229 +3162,331 @@ struct utimbuf { }; #endif -static void -utime_internal(path, arg) - const char *path; - void *arg; +static int +utime_internal(const char *path, void *arg) { - struct utimbuf *utp = arg; - if (utime(path, utp) < 0) - rb_sys_fail(path); + struct utime_args *v = arg; + const stat_timestamp *tsp = v->tsp; + struct utimbuf utbuf, *utp = NULL; + if (tsp) { + utbuf.actime = tsp[0].tv_sec; + utbuf.modtime = tsp[1].tv_sec; + utp = &utbuf; + } + return utime(path, utp); } +#endif /* !defined(HAVE_UTIMES) */ static VALUE -rb_file_s_utime(argc, argv) - int argc; - VALUE *argv; +utime_internal_i(int argc, VALUE *argv, int follow) { - VALUE atime, mtime, rest; - long n; - struct timeval tv; - struct utimbuf utbuf, *utp = NULL; + struct utime_args args; + struct timespec tss[2], *tsp = NULL; - rb_scan_args(argc, argv, "2*", &atime, &mtime, &rest); + apply2args(2); + args.atime = *argv++; + args.mtime = *argv++; - if (!NIL_P(atime) || !NIL_P(mtime)) { - utp = &utbuf; - tv = rb_time_timeval(atime); - utp->actime = tv.tv_sec; - tv = rb_time_timeval(mtime); - utp->modtime = tv.tv_sec; + args.follow = follow; + + if (!NIL_P(args.atime) || !NIL_P(args.mtime)) { + tsp = tss; + tsp[0] = rb_time_timespec(args.atime); + if (args.atime == args.mtime) + tsp[1] = tsp[0]; + else + tsp[1] = rb_time_timespec(args.mtime); } + args.tsp = tsp; - n = apply2files(utime_internal, rest, utp); - return LONG2FIX(n); + return apply2files(utime_internal, argc, argv, &args); } +/* + * call-seq: + * File.utime(atime, mtime, file_name, ...) -> integer + * + * Sets the access and modification times of each named file to the + * first two arguments. If a file is a symlink, this method acts upon + * its referent rather than the link itself; for the inverse + * behavior see File.lutime. Returns the number of file + * names in the argument list. + */ + +static VALUE +rb_file_s_utime(int argc, VALUE *argv, VALUE _) +{ + return utime_internal_i(argc, argv, FALSE); +} + +#if defined(HAVE_UTIMES) && (defined(HAVE_LUTIMES) || (defined(HAVE_UTIMENSAT) && defined(AT_SYMLINK_NOFOLLOW))) + +/* + * call-seq: + * File.lutime(atime, mtime, file_name, ...) -> integer + * + * Sets the access and modification times of each named file to the + * first two arguments. If a file is a symlink, this method acts upon + * the link itself as opposed to its referent; for the inverse + * behavior, see File.utime. Returns the number of file + * names in the argument list. + */ + +static VALUE +rb_file_s_lutime(int argc, VALUE *argv, VALUE _) +{ + return utime_internal_i(argc, argv, TRUE); +} +#else +#define rb_file_s_lutime rb_f_notimplement #endif -NORETURN(static void sys_fail2 _((VALUE,VALUE))); +#ifdef RUBY_FUNCTION_NAME_STRING +# define syserr_fail2(e, s1, s2) syserr_fail2_in(RUBY_FUNCTION_NAME_STRING, e, s1, s2) +#else +# define syserr_fail2_in(func, e, s1, s2) syserr_fail2(e, s1, s2) +#endif +#define sys_fail2(s1, s2) syserr_fail2(errno, s1, s2) +NORETURN(static void syserr_fail2_in(const char *,int,VALUE,VALUE)); static void -sys_fail2(s1, s2) - VALUE s1, s2; +syserr_fail2_in(const char *func, int e, VALUE s1, VALUE s2) { - char *buf; - int len; + VALUE str; +#ifdef MAX_PATH + const int max_pathlen = MAX_PATH; +#else + const int max_pathlen = MAXPATHLEN; +#endif - len = RSTRING(s1)->len + RSTRING(s2)->len + 5; - buf = ALLOCA_N(char, len); - snprintf(buf, len, "%s or %s", RSTRING(s1)->ptr, RSTRING(s2)->ptr); - rb_sys_fail(buf); + if (e == EEXIST) { + rb_syserr_fail_path(e, rb_str_ellipsize(s2, max_pathlen)); + } + str = rb_str_new_cstr("("); + rb_str_append(str, rb_str_ellipsize(s1, max_pathlen)); + rb_str_cat2(str, ", "); + rb_str_append(str, rb_str_ellipsize(s2, max_pathlen)); + rb_str_cat2(str, ")"); +#ifdef RUBY_FUNCTION_NAME_STRING + rb_syserr_fail_path_in(func, e, str); +#else + rb_syserr_fail_path(e, str); +#endif } +#ifdef HAVE_LINK /* * call-seq: - * File.link(old_name, new_name) => 0 - * + * File.link(old_name, new_name) -> 0 + * * Creates a new name for an existing file using a hard link. Will not * overwrite <i>new_name</i> if it already exists (raising a subclass - * of <code>SystemCallError</code>). Not available on all platforms. - * + * of SystemCallError). Not available on all platforms. + * * File.link("testfile", ".testfile") #=> 0 * IO.readlines(".testfile")[0] #=> "This is line one\n" */ static VALUE -rb_file_s_link(klass, from, to) - VALUE klass, from, to; +rb_file_s_link(VALUE klass, VALUE from, VALUE to) { -#ifdef HAVE_LINK - SafeStringValue(from); - SafeStringValue(to); + FilePathValue(from); + FilePathValue(to); + from = rb_str_encode_ospath(from); + to = rb_str_encode_ospath(to); if (link(StringValueCStr(from), StringValueCStr(to)) < 0) { - sys_fail2(from, to); + sys_fail2(from, to); } return INT2FIX(0); +} #else - rb_notimplement(); - return Qnil; /* not reached */ +#define rb_file_s_link rb_f_notimplement #endif -} +#ifdef HAVE_SYMLINK /* * call-seq: - * File.symlink(old_name, new_name) => 0 - * + * File.symlink(old_name, new_name) -> 0 + * * Creates a symbolic link called <i>new_name</i> for the existing file - * <i>old_name</i>. Raises a <code>NotImplemented</code> exception on + * <i>old_name</i>. Raises a NotImplemented exception on * platforms that do not support symbolic links. - * + * * File.symlink("testfile", "link2test") #=> 0 - * + * */ static VALUE -rb_file_s_symlink(klass, from, to) - VALUE klass, from, to; +rb_file_s_symlink(VALUE klass, VALUE from, VALUE to) { -#ifdef HAVE_SYMLINK - SafeStringValue(from); - SafeStringValue(to); + FilePathValue(from); + FilePathValue(to); + from = rb_str_encode_ospath(from); + to = rb_str_encode_ospath(to); if (symlink(StringValueCStr(from), StringValueCStr(to)) < 0) { - sys_fail2(from, to); + sys_fail2(from, to); } return INT2FIX(0); +} #else - rb_notimplement(); - return Qnil; /* not reached */ +#define rb_file_s_symlink rb_f_notimplement #endif -} +#ifdef HAVE_READLINK /* * call-seq: - * File.readlink(link_name) -> file_name - * + * File.readlink(link_name) -> file_name + * * Returns the name of the file referenced by the given link. * Not available on all platforms. - * + * * File.symlink("testfile", "link2test") #=> 0 * File.readlink("link2test") #=> "testfile" */ static VALUE -rb_file_s_readlink(klass, path) - VALUE klass, path; +rb_file_s_readlink(VALUE klass, VALUE path) { -#ifdef HAVE_READLINK + return rb_readlink(path, rb_filesystem_encoding()); +} + +struct readlink_arg { + const char *path; char *buf; + size_t size; +}; + +static void * +nogvl_readlink(void *ptr) +{ + struct readlink_arg *ra = ptr; + + return (void *)(VALUE)readlink(ra->path, ra->buf, ra->size); +} + +static ssize_t +readlink_without_gvl(VALUE path, VALUE buf, size_t size) +{ + struct readlink_arg ra; + + ra.path = RSTRING_PTR(path); + ra.buf = RSTRING_PTR(buf); + ra.size = size; + + return (ssize_t)IO_WITHOUT_GVL(nogvl_readlink, &ra); +} + +VALUE +rb_readlink(VALUE path, rb_encoding *enc) +{ int size = 100; - int rv; + ssize_t rv; VALUE v; - SafeStringValue(path); - buf = xmalloc(size); - while ((rv = readlink(RSTRING(path)->ptr, buf, size)) == size + FilePathValue(path); + path = rb_str_encode_ospath(path); + v = rb_enc_str_new(0, size, enc); + while ((rv = readlink_without_gvl(path, v, size)) == size #ifdef _AIX - || (rv < 0 && errno == ERANGE) /* quirky behavior of GPFS */ + || (rv < 0 && errno == ERANGE) /* quirky behavior of GPFS */ #endif - ) { - size *= 2; - buf = xrealloc(buf, size); + ) { + rb_str_modify_expand(v, size); + size *= 2; + rb_str_set_len(v, size); } if (rv < 0) { - free(buf); - rb_sys_fail(RSTRING(path)->ptr); + int e = errno; + rb_str_resize(v, 0); + rb_syserr_fail_path(e, path); } - v = rb_tainted_str_new(buf, rv); - free(buf); + rb_str_resize(v, rv); return v; +} #else - rb_notimplement(); - return Qnil; /* not reached */ +#define rb_file_s_readlink rb_f_notimplement #endif -} -static void unlink_internal _((const char *, void *)); -static void -unlink_internal(path, arg) - const char *path; - void *arg; +static int +unlink_internal(const char *path, void *arg) { - if (unlink(path) < 0) - rb_sys_fail(path); + return unlink(path); } /* * call-seq: - * File.delete(file_name, ...) => integer - * File.unlink(file_name, ...) => integer - * + * File.delete(file_name, ...) -> integer + * File.unlink(file_name, ...) -> integer + * * Deletes the named files, returning the number of names * passed as arguments. Raises an exception on any error. - * See also <code>Dir::rmdir</code>. + * 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 + * e.g. Errno::ENOENT. + * + * See also Dir::rmdir. */ static VALUE -rb_file_s_unlink(klass, args) - VALUE klass, args; +rb_file_s_unlink(int argc, VALUE *argv, VALUE klass) { - long n; + return apply2files(unlink_internal, argc, argv, 0); +} + +struct rename_args { + const char *src; + const char *dst; +}; - rb_secure(2); - n = apply2files(unlink_internal, args, 0); - return LONG2FIX(n); +static void * +no_gvl_rename(void *ptr) +{ + struct rename_args *ra = ptr; + + return (void *)(VALUE)rename(ra->src, ra->dst); } /* * call-seq: - * File.rename(old_name, new_name) => 0 - * - * Renames the given file to the new name. Raises a - * <code>SystemCallError</code> if the file cannot be renamed. - * + * File.rename(old_name, new_name) -> 0 + * + * Renames the given file to the new name. Raises a SystemCallError + * if the file cannot be renamed. + * * File.rename("afile", "afile.bak") #=> 0 */ static VALUE -rb_file_s_rename(klass, from, to) - VALUE klass, from, to; +rb_file_s_rename(VALUE klass, VALUE from, VALUE to) { - const char *src, *dst; - SafeStringValue(from); - SafeStringValue(to); + struct rename_args ra; + VALUE f, t; - src = StringValueCStr(from); - dst = StringValueCStr(to); + FilePathValue(from); + FilePathValue(to); + f = rb_str_encode_ospath(from); + t = rb_str_encode_ospath(to); + ra.src = StringValueCStr(f); + ra.dst = StringValueCStr(t); #if defined __CYGWIN__ errno = 0; #endif - if (rename(src, dst) < 0) { -#if defined DOSISH && !defined _WIN32 - switch (errno) { - case EEXIST: -#if defined (__EMX__) - case EACCES: -#endif - if (chmod(dst, 0666) == 0 && - unlink(dst) == 0 && - rename(src, dst) == 0) - return INT2FIX(0); - } -#endif - sys_fail2(from, to); + if (IO_WITHOUT_GVL_INT(no_gvl_rename, &ra) < 0) { + int e = errno; +#if defined DOSISH + switch (e) { + case EEXIST: + if (chmod(ra.dst, 0666) == 0 && + unlink(ra.dst) == 0 && + rename(ra.src, ra.dst) == 0) + return INT2FIX(0); + } +#endif + syserr_fail2(e, from, to); } return INT2FIX(0); @@ -2269,38 +3494,36 @@ rb_file_s_rename(klass, from, to) /* * call-seq: - * File.umask() => integer - * File.umask(integer) => integer - * + * File.umask() -> integer + * File.umask(integer) -> integer + * * Returns the current umask value for this process. If the optional * argument is given, set the umask to that value and return the * previous value. Umask values are <em>subtracted</em> from the * default permissions, so a umask of <code>0222</code> would make a * file read-only for everyone. - * + * * File.umask(0006) #=> 18 * File.umask #=> 6 */ static VALUE -rb_file_s_umask(argc, argv) - int argc; - VALUE *argv; -{ - int omask = 0; - - rb_secure(2); - if (argc == 0) { - omask = umask(0); - umask(omask); - } - else if (argc == 1) { - omask = umask(NUM2INT(argv[0])); - } - else { - rb_raise(rb_eArgError, "wrong number of arguments"); +rb_file_s_umask(int argc, VALUE *argv, VALUE _) +{ + mode_t omask = 0; + + switch (argc) { + case 0: + omask = umask(0); + umask(omask); + break; + case 1: + omask = umask(NUM2MODET(argv[0])); + break; + default: + rb_error_arity(argc, 0, 1); } - return INT2FIX(omask); + return MODET2NUM(omask); } #ifdef __CYGWIN__ @@ -2309,47 +3532,69 @@ rb_file_s_umask(argc, argv) #if defined __CYGWIN__ || defined DOSISH #define DOSISH_UNC #define DOSISH_DRIVE_LETTER -#define isdirsep(x) ((x) == '/' || (x) == '\\') +#define FILE_ALT_SEPARATOR '\\' +#endif +#ifdef FILE_ALT_SEPARATOR +#define isdirsep(x) ((x) == '/' || (x) == FILE_ALT_SEPARATOR) +# ifdef DOSISH +static const char file_alt_separator[] = {FILE_ALT_SEPARATOR, '\0'}; +# endif #else #define isdirsep(x) ((x) == '/') #endif -#if defined _WIN32 || defined __CYGWIN__ -#define USE_NTFS 1 -#else -#define USE_NTFS 0 +#ifndef USE_NTFS +# 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 +# else +# define USE_NTFS_ADS 0 +# endif #endif #if USE_NTFS -#define istrailinggabage(x) ((x) == '.' || (x) == ' ') +#define istrailinggarbage(x) ((x) == '.' || (x) == ' ') #else -#define istrailinggabage(x) 0 +#define istrailinggarbage(x) 0 #endif -#ifndef CharNext /* defined as CharNext[AW] on Windows. */ -# if defined(DJGPP) -# define CharNext(p) ((p) + mblen(p, MB_CUR_MAX)) -# else -# define CharNext(p) ((p) + 1) -# endif +#if USE_NTFS_ADS +# define isADS(x) ((x) == ':') +#else +# define isADS(x) 0 +#endif + +#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])) +#else +#define has_unc(buf) 0 #endif #ifdef DOSISH_DRIVE_LETTER static inline int -has_drive_letter(buf) - const char *buf; +has_drive_letter(const char *buf) { if (ISALPHA(buf[0]) && buf[1] == ':') { - return 1; + return 1; } else { - return 0; + return 0; } } +#ifndef _WIN32 static char* -getcwdofdrv(drv) - int drv; +getcwdofdrv(int drv) { char drive[4]; char *drvcwd, *oldcwd; @@ -2362,883 +3607,1951 @@ getcwdofdrv(drv) of a particular drive is to change chdir() to that drive, so save the old cwd before chdir() */ - oldcwd = my_getcwd(); + oldcwd = ruby_getcwd(); if (chdir(drive) == 0) { - drvcwd = my_getcwd(); - chdir(oldcwd); - free(oldcwd); + drvcwd = ruby_getcwd(); + chdir(oldcwd); + xfree(oldcwd); } else { - /* perhaps the drive is not exist. we return only drive letter */ - drvcwd = strdup(drive); + /* perhaps the drive is not exist. we return only drive letter */ + drvcwd = strdup(drive); } return drvcwd; } -#endif + +static inline int +not_same_drive(VALUE path, int drive) +{ + const char *p = RSTRING_PTR(path); + if (RSTRING_LEN(path) < 2) return 0; + if (has_drive_letter(p)) { + return TOLOWER(p[0]) != TOLOWER(drive); + } + else { + return has_unc(p); + } +} +#endif /* _WIN32 */ +#endif /* DOSISH_DRIVE_LETTER */ static inline char * -skiproot(path) - const char *path; +skiproot(const char *path, const char *end) { #ifdef DOSISH_DRIVE_LETTER - if (has_drive_letter(path)) path += 2; + if (path + 2 <= end && has_drive_letter(path)) path += 2; #endif - while (isdirsep(*path)) path++; + while (path < end && isdirsep(*path)) path++; return (char *)path; } -#define nextdirsep rb_path_next -char * -rb_path_next(s) - const char *s; +static inline char * +enc_path_next(const char *s, const char *e, bool mb_enc, rb_encoding *enc) { - while (*s && !isdirsep(*s)) { - s = CharNext(s); + while (s < e && !isdirsep(*s)) { + Inc(s, e, mb_enc, enc); } return (char *)s; } -#if defined(DOSISH_UNC) || defined(DOSISH_DRIVE_LETTER) -#define skipprefix rb_path_skip_prefix +#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, true, enc); +} + +#if defined(DOSISH_UNC) || defined(DOSISH_DRIVE_LETTER) +#define skipprefix enc_path_skip_prefix #else -#define skipprefix(path) (path) +#define skipprefix(path, end, mb_enc, enc) (path) #endif -char * -rb_path_skip_prefix(path) - const char *path; +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) +#if defined(DOSISH_UNC) || defined(DOSISH_DRIVE_LETTER) #ifdef DOSISH_UNC - if (isdirsep(path[0]) && isdirsep(path[1])) { - path += 2; - while (isdirsep(*path)) path++; - if (*(path = nextdirsep(path)) && path[1] && !isdirsep(path[1])) - path = nextdirsep(path + 1); - return (char *)path; + if (path + 2 <= end && isdirsep(path[0]) && isdirsep(path[1])) { + path += 2; + while (path < end && isdirsep(*path)) path++; + if ((path = enc_path_next(path, end, mb_enc, enc)) < end && path[0] && path[1] && !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)) - return (char *)(path + 2); -#endif + return (char *)(path + 2); #endif +#endif /* defined(DOSISH_UNC) || defined(DOSISH_DRIVE_LETTER) */ return (char *)path; } -#define strrdirsep rb_path_last_separator char * -rb_path_last_separator(path) - const char *path; +rb_enc_path_skip_prefix(const char *path, const char *end, rb_encoding *enc) +{ + return enc_path_skip_prefix(path, end, true, 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, true, enc); + while (isdirsep(*p)) p++; + return p; +#else + return skiproot(path, end); +#endif +} + +char * +rb_enc_path_last_separator(const char *path, const char *end, rb_encoding *enc) { char *last = NULL; - while (*path) { - if (isdirsep(*path)) { - const char *tmp = path++; - while (isdirsep(*path)) path++; - if (!*path) break; - last = (char *)tmp; - } - else { - path = CharNext(path); - } + while (path < end) { + if (isdirsep(*path)) { + const char *tmp = path++; + while (path < end && isdirsep(*path)) path++; + if (path >= end) break; + last = (char *)tmp; + } + else { + Inc(path, end, true, enc); + } } return last; } -static char * -chompdirsep(path) - const char *path; +static inline char * +strrdirsep(const char *path, const char *end, bool mb_enc, rb_encoding *enc) { - while (*path) { - if (isdirsep(*path)) { - const char *last = path++; - while (isdirsep(*path)) path++; - if (!*path) return (char *)last; - } - else { - path = CharNext(path); - } + if (RB_UNLIKELY(mb_enc)) { + return rb_enc_path_last_separator(path, end, 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, bool mb_enc, rb_encoding *enc) +{ + while (path < end) { + if (isdirsep(*path)) { + const char *last = path++; + while (path < end && isdirsep(*path)) path++; + if (path >= end) return (char *)last; + } + else { + Inc(path, end, mb_enc, enc); + } } return (char *)path; } char * -rb_path_end(path) - const char *path; +rb_enc_path_end(const char *path, const char *end, rb_encoding *enc) { - if (isdirsep(*path)) path++; - return chompdirsep(path); + if (path < end && isdirsep(*path)) path++; + return chompdirsep(path, end, true, enc); +} + +static rb_encoding * +fs_enc_check(VALUE path1, VALUE 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); + if (encidx == ENCINDEX_US_ASCII) + encidx = rb_enc_get_index(path2); + enc = rb_enc_from_index(encidx); + } + return enc; } #if USE_NTFS static char * -ntfs_tail(const char *path) -{ - while (*path && *path != ':') { - if (istrailinggabage(*path)) { - const char *last = path++; - while (istrailinggabage(*path)) path++; - if (!*path || *path == ':') return (char *)last; - } - else if (isdirsep(*path)) { - const char *last = path++; - while (isdirsep(*path)) path++; - if (!*path) return (char *)last; - if (*path == ':') path++; - } - else { - path = CharNext(path); - } +ntfs_tail(const char *path, const char *end, rb_encoding *enc) +{ + while (path < end && *path == '.') path++; + while (path < end && !isADS(*path)) { + if (istrailinggarbage(*path)) { + const char *last = path++; + while (path < end && istrailinggarbage(*path)) path++; + if (path >= end || isADS(*path)) return (char *)last; + } + else if (isdirsep(*path)) { + const char *last = path++; + while (path < end && isdirsep(*path)) path++; + if (path >= end) return (char *)last; + if (isADS(*path)) path++; + } + else { + Inc(path, end, true, enc); + } } return (char *)path; } -#endif +#endif /* USE_NTFS */ #define BUFCHECK(cond) do {\ - long bdiff = p - buf;\ - while (cond) {\ - buflen *= 2;\ + bdiff = p - buf;\ + if (cond) {\ + do {buflen *= 2;} while (cond);\ + rb_str_resize(result, buflen);\ + buf = RSTRING_PTR(result);\ + p = buf + bdiff;\ + pend = buf + buflen;\ }\ - rb_str_resize(result, buflen);\ - buf = RSTRING(result)->ptr;\ - p = buf + bdiff;\ - pend = buf + buflen;\ } while (0) #define BUFINIT() (\ - p = buf = RSTRING(result)->ptr,\ - buflen = RSTRING(result)->len,\ + p = buf = RSTRING_PTR(result),\ + buflen = RSTRING_LEN(result),\ pend = p + buflen) -#if !defined(TOLOWER) -#define TOLOWER(c) (ISUPPER(c) ? tolower(c) : (c)) +#ifdef __APPLE__ +# define SKIPPATHSEP(p) ((*(p)) ? 1 : 0) +#else +# define SKIPPATHSEP(p) 1 #endif -static int is_absolute_path _((const char*)); +#define BUFCOPY(srcptr, srclen) do { \ + const int skip = SKIPPATHSEP(p); \ + rb_str_set_len(result, p-buf+skip); \ + BUFCHECK(bdiff + ((srclen)+skip) >= buflen); \ + p += skip; \ + memcpy(p, (srcptr), (srclen)); \ + p += (srclen); \ +} while (0) + +#define WITH_ROOTDIFF(stmt) do { \ + long rootdiff = root - buf; \ + stmt; \ + root = buf + rootdiff; \ +} while (0) static VALUE -file_expand_path(fname, dname, result) - VALUE fname, dname, result; +copy_home_path(VALUE result, const char *dir) { - const char *s, *b; - char *buf, *p, *pend, *root; - long buflen, dirlen; - int tainted; + char *buf; +#if defined DOSISH || defined __CYGWIN__ + char *p, *bend; + rb_encoding *enc; +#endif + long dirlen; + int encidx; - s = StringValuePtr(fname); - BUFINIT(); - tainted = OBJ_TAINTED(fname); - - if (s[0] == '~') { - if (isdirsep(s[1]) || s[1] == '\0') { - char *dir = getenv("HOME"); - - if (!dir) { - rb_raise(rb_eArgError, "couldn't find HOME environment -- expanding `%s'", s); - } - dirlen = strlen(dir); - BUFCHECK(dirlen > buflen); - strcpy(buf, dir); + dirlen = strlen(dir); + rb_str_resize(result, dirlen); + memcpy(buf = RSTRING_PTR(result), dir, dirlen); + encidx = rb_filesystem_encindex(); + rb_enc_associate_index(result, encidx); #if defined DOSISH || defined __CYGWIN__ - for (p = buf; *p; p = CharNext(p)) { - if (*p == '\\') { - *p = '/'; - } - } -#else - p = buf + strlen(dir); + enc = rb_enc_from_index(encidx); + for (bend = (p = buf) + dirlen; p < bend; Inc(p, bend, true, enc)) { + if (*p == '\\') { + *p = '/'; + } + } #endif - s++; - tainted = 1; - } - else { -#ifdef HAVE_PWD_H - struct passwd *pwPtr; - s++; -#endif - s = nextdirsep(b = s); - BUFCHECK(bdiff + (s-b) >= buflen); - memcpy(p, b, s-b); - p += s-b; - *p = '\0'; + return result; +} + +VALUE +rb_home_dir_of(VALUE user, VALUE result) +{ #ifdef HAVE_PWD_H - pwPtr = getpwnam(buf); - if (!pwPtr) { - endpwent(); - rb_raise(rb_eArgError, "user %s doesn't exist", buf); - } - dirlen = strlen(pwPtr->pw_dir); - BUFCHECK(dirlen > buflen); - strcpy(buf, pwPtr->pw_dir); - p = buf + strlen(pwPtr->pw_dir); - endpwent(); -#endif - } + 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) + const char *dir, *username = RSTRING_PTR(user); + rb_encoding *enc = rb_enc_get(user); +#if defined _WIN32 + rb_encoding *fsenc = rb_utf8_encoding(); +#else + rb_encoding *fsenc = rb_filesystem_encoding(); +#endif + if (enc != fsenc) { + dir = username = RSTRING_PTR(rb_str_conv_enc(user, enc, fsenc)); + } + + if ((login = getlogin()) && strcasecmp(username, login) == 0) + dir = pwPtr = getenv("HOME"); + if (!pwPtr) { + rb_raise(rb_eArgError, "user %"PRIsVALUE" doesn't exist", user); + } +#endif + copy_home_path(result, dir); + return result; +} + +#ifndef _WIN32 /* this encompasses rb_file_expand_path_internal */ +VALUE +rb_default_home_dir(VALUE result) +{ + const char *dir = getenv("HOME"); + +#if defined HAVE_PWD_H + if (!dir) { + /* We'll look up the user's default home dir in the password db by + * login name, if possible, and failing that will fall back to looking + * the information up by uid (as would be needed for processes that + * are not a descendant of login(1) or a work-alike). + * + * While the lookup by uid is more likely to succeed (since we always + * have a uid, but may or may not have a login name), we prefer first + * looking up by name to accommodate the possibility of multiple login + * names (each with its own record in the password database, so each + * with a potentially different home directory) being mapped to the + * same uid (as explicitly allowed for by POSIX; see getlogin(3posix)). + */ + VALUE login_name = rb_getlogin(); + +# if !defined(HAVE_GETPWUID_R) && !defined(HAVE_GETPWUID) + /* This is a corner case, but for backward compatibility reasons we + * want to emit this error if neither the lookup by login name nor + * lookup by getuid() has a chance of succeeding. + */ + if (NIL_P(login_name)) { + rb_raise(rb_eArgError, "couldn't find login name -- expanding '~'"); + } +# 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()); + } + } + + /* found it */ + copy_home_path(result, RSTRING_PTR(pw_dir)); + rb_str_resize(pw_dir, 0); + return result; + } +#endif /* defined HAVE_PWD_H */ + if (!dir) { + rb_raise(rb_eArgError, "couldn't find HOME environment -- expanding '~'"); + } + return copy_home_path(result, dir); +} + +static VALUE +ospath_new(const char *ptr, long len, rb_encoding *fsenc) +{ +#if NORMALIZE_UTF8PATH + VALUE path = rb_str_normalize_ospath(ptr, len); + rb_enc_associate(path, fsenc); + return path; +#else + return rb_enc_str_new(ptr, len, fsenc); +#endif +} + +static char * +append_fspath(VALUE result, VALUE fname, char *dir, rb_encoding **enc, rb_encoding *fsenc) +{ + char *buf, *cwdp = dir; + VALUE dirname = Qnil; + size_t dirlen = strlen(dir), buflen = rb_str_capacity(result); + + if (NORMALIZE_UTF8PATH || *enc != 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); + } + else if (NORMALIZE_UTF8PATH) { + RSTRING_GETMEM(dirname, cwdp, dirlen); + } + *enc = direnc; + } + do {buflen *= 2;} while (dirlen > buflen); + rb_str_resize(result, buflen); + buf = RSTRING_PTR(result); + memcpy(buf, cwdp, dirlen); + xfree(dir); + if (!NIL_P(dirname)) rb_str_resize(dirname, 0); + rb_enc_associate(result, *enc); + return buf + dirlen; +} + +VALUE +rb_file_expand_path_internal(VALUE fname, VALUE dname, int abs_mode, int long_name, VALUE result) +{ + const char *s, *b, *fend; + char *buf, *p, *pend, *root; + size_t buflen, bdiff; + rb_encoding *enc, *fsenc = rb_filesystem_encoding(); + + s = StringValuePtr(fname); + fend = s + RSTRING_LEN(fname); + enc = rb_enc_get(fname); + BUFINIT(); + + if (s[0] == '~' && abs_mode == 0) { /* execute only if NOT absolute_path() */ + long userlen = 0; + if (isdirsep(s[1]) || s[1] == '\0') { + buf = 0; + b = 0; + rb_str_set_len(result, 0); + if (*++s) ++s; + rb_default_home_dir(result); + } + else { + s = nextdirsep(b = s, fend, enc); + b++; /* b[0] is '~' */ + userlen = s - b; + BUFCHECK(bdiff + userlen >= buflen); + memcpy(p, b, userlen); + ENC_CODERANGE_CLEAR(result); + rb_str_set_len(result, userlen); + rb_enc_associate(result, enc); + rb_home_dir_of(result, result); + buf = p + 1; + p += userlen; + } + if (!rb_is_absolute_path(RSTRING_PTR(result))) { + if (userlen) { + rb_enc_raise(enc, rb_eArgError, "non-absolute home of %.*s%.0"PRIsVALUE, + (int)userlen, b, fname); + } + else { + rb_raise(rb_eArgError, "non-absolute home"); + } + } + BUFINIT(); + p = pend; } #ifdef DOSISH_DRIVE_LETTER /* skip drive letter */ else if (has_drive_letter(s)) { - if (isdirsep(s[2])) { - /* specified drive letter, and full path */ - /* skip drive letter */ - BUFCHECK(bdiff + 2 >= buflen); - memcpy(p, s, 2); - p += 2; - s += 2; - } - else { - /* specified drive, but not full path */ - int same = 0; - if (!NIL_P(dname)) { - file_expand_path(dname, Qnil, result); - BUFINIT(); - if (has_drive_letter(p) && TOLOWER(p[0]) == TOLOWER(s[0])) { - /* ok, same drive */ - same = 1; - } - } - if (!same) { - char *dir = getcwdofdrv(*s); - - tainted = 1; - dirlen = strlen(dir); - BUFCHECK(dirlen > buflen); - strcpy(buf, dir); - free(dir); - } - p = chompdirsep(skiproot(buf)); - s += 2; - } - } -#endif - else if (!is_absolute_path(s)) { - if (!NIL_P(dname)) { - file_expand_path(dname, Qnil, result); - BUFINIT(); - } - else { - char *dir = my_getcwd(); - - tainted = 1; - dirlen = strlen(dir); - BUFCHECK(dirlen > buflen); - strcpy(buf, dir); - free(dir); - } + if (isdirsep(s[2])) { + /* specified drive letter, and full path */ + /* skip drive letter */ + BUFCHECK(bdiff + 2 >= buflen); + memcpy(p, s, 2); + p += 2; + s += 2; + rb_enc_copy(result, fname); + } + else { + /* specified drive, but not full path */ + int same = 0; + if (!NIL_P(dname) && !not_same_drive(dname, s[0])) { + rb_file_expand_path_internal(dname, Qnil, abs_mode, long_name, result); + BUFINIT(); + if (has_drive_letter(p) && TOLOWER(p[0]) == TOLOWER(s[0])) { + /* ok, same drive */ + same = 1; + } + } + if (!same) { + char *e = append_fspath(result, fname, getcwdofdrv(*s), &enc, fsenc); + BUFINIT(); + p = e; + } + else { + rb_enc_associate(result, enc = fs_enc_check(result, fname)); + p = pend; + } + p = chompdirsep(skiproot(buf, p), p, true, enc); + s += 2; + } + } +#endif /* DOSISH_DRIVE_LETTER */ + else if (!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)); + BUFINIT(); + p = pend; + } + else { + char *e = append_fspath(result, fname, ruby_getcwd(), &enc, fsenc); + BUFINIT(); + p = e; + } #if defined DOSISH || defined __CYGWIN__ - if (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); - } - else -#endif - p = chompdirsep(skiproot(buf)); + if (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, true, enc); + } + else +#endif /* defined DOSISH || defined __CYGWIN__ */ + p = chompdirsep(skiproot(buf, p), p, true, enc); } else { - b = s; - do s++; while (isdirsep(*s)); - p = buf + (s - b); - BUFCHECK(bdiff >= buflen); - memset(buf, '/', p - buf); + size_t len; + b = s; + do s++; while (isdirsep(*s)); + len = s - b; + p = buf + len; + BUFCHECK(bdiff >= buflen); + memset(buf, '/', len); + rb_str_set_len(result, len); + rb_enc_associate(result, fs_enc_check(result, fname)); } if (p > buf && p[-1] == '/') - --p; - else - *p = '/'; + --p; + else { + rb_str_set_len(result, p-buf); + BUFCHECK(bdiff + 1 >= buflen); + *p = '/'; + } + rb_str_set_len(result, p-buf+1); + BUFCHECK(bdiff + 1 >= buflen); p[1] = 0; - root = skipprefix(buf); + root = skipprefix(buf, p+1, true, enc); b = s; while (*s) { - switch (*s) { - case '.': - if (b == s++) { /* beginning of path element */ - switch (*s) { - case '\0': - b = s; - break; - case '.': - if (*(s+1) == '\0' || isdirsep(*(s+1))) { - /* We must go back to the parent */ - char *n; - *p = '\0'; - if (!(n = strrdirsep(root))) { - *p = '/'; - } - else { - p = n; - } - b = ++s; - } + switch (*s) { + case '.': + if (b == s++) { /* beginning of path element */ + switch (*s) { + case '\0': + b = s; + break; + case '.': + if (*(s+1) == '\0' || isdirsep(*(s+1))) { + /* We must go back to the parent */ + char *n; + *p = '\0'; + if (!(n = strrdirsep(root, p, true, enc))) { + *p = '/'; + } + else { + p = n; + } + b = ++s; + } #if USE_NTFS - else { - do *++s; while (istrailinggabage(*s)); - } -#endif - break; - case '/': + else { + do ++s; while (istrailinggarbage(*s)); + } +#endif /* USE_NTFS */ + break; + case '/': #if defined DOSISH || defined __CYGWIN__ - case '\\': -#endif - b = ++s; - break; - default: - /* ordinary path element, beginning don't move */ - break; - } - } + case '\\': +#endif + b = ++s; + break; + default: + /* ordinary path element, beginning don't move */ + break; + } + } #if USE_NTFS - else { - --s; - case ' ': { - const char *e = s; - while (istrailinggabage(*s)) s++; - if (!*s) { - s = e; - goto endpath; - } - } - } -#endif - break; - case '/': + else { + --s; + case ' ': { + const char *e = s; + while (s < fend && istrailinggarbage(*s)) s++; + if (s >= fend) { + s = e; + goto endpath; + } + } + } +#endif /* USE_NTFS */ + break; + case '/': #if defined DOSISH || defined __CYGWIN__ - case '\\': -#endif - if (s > b) { - long rootdiff = root - buf; - BUFCHECK(bdiff + (s-b+1) >= buflen); - root = buf + rootdiff; - memcpy(++p, b, s-b); - p += s-b; - *p = '/'; - } - b = ++s; - break; - default: - s = CharNext(s); - break; - } + case '\\': +#endif + if (s > b) { + WITH_ROOTDIFF(BUFCOPY(b, s-b)); + *p = '/'; + } + b = ++s; + break; + default: +#ifdef __APPLE__ + { + int n = ignored_char_p(s, fend, enc); + if (n) { + if (s > b) { + WITH_ROOTDIFF(BUFCOPY(b, s-b)); + *p = '\0'; + } + b = s += n; + break; + } + } +#endif /* __APPLE__ */ + Inc(s, fend, true, enc); + break; + } } if (s > b) { #if USE_NTFS +# if USE_NTFS_ADS + static const char prime[] = ":$DATA"; + enum {prime_len = sizeof(prime) -1}; +# endif endpath: - if (s > b + 6 && strncasecmp(s - 6, ":$DATA", 6) == 0) { - /* alias of stream */ - /* get rid of a bug of x64 VC++ */ - if (*(s-7) == ':') s -= 7; /* prime */ - else if (memchr(b, ':', s - 6 - b)) s -= 6; /* alternative */ - } -#endif - BUFCHECK(bdiff + (s-b) >= buflen); - memcpy(++p, b, s-b); - p += s-b; +# if USE_NTFS_ADS + if (s > b + prime_len && strncasecmp(s - prime_len, prime, prime_len) == 0) { + /* alias of stream */ + /* get rid of a bug of x64 VC++ */ + if (isADS(*(s - (prime_len+1)))) { + s -= prime_len + 1; /* prime */ + } + else if (memchr(b, ':', s - prime_len - b)) { + s -= prime_len; /* alternative */ + } + } +# endif /* USE_NTFS_ADS */ +#endif /* USE_NTFS */ + BUFCOPY(b, s-b); + rb_str_set_len(result, p-buf); } - if (p == skiproot(buf) - 1) p++; - buflen = p - buf; + if (p == skiproot(buf, p + !!*p) - 1) p++; #if USE_NTFS *p = '\0'; - if (1 && + if ((s = strrdirsep(b = buf, p, enc)) != 0 && !strpbrk(s, "*?")) { + VALUE tmp, v; + size_t len; + int encidx; + WCHAR *wstr; + WIN32_FIND_DATAW wfd; + HANDLE h; #ifdef __CYGWIN__ - !(buf[0] == '/' && !buf[1]) && -#endif - !strpbrk(b = buf, "*?")) { - size_t len; - WIN32_FIND_DATA wfd; +#ifdef HAVE_CYGWIN_CONV_PATH + char *w32buf = NULL; + const int flags = CCP_POSIX_TO_WIN_A | CCP_RELATIVE; +#else + char w32buf[MAXPATHLEN]; +#endif /* HAVE_CYGWIN_CONV_PATH */ + const char *path; + ssize_t bufsize; + int lnk_added = 0, is_symlink = 0; + struct stat st; + p = (char *)s; + len = strlen(p); + if (lstat_without_gvl(buf, &st) == 0 && S_ISLNK(st.st_mode)) { + is_symlink = 1; + if (len > 4 && STRCASECMP(p + len - 4, ".lnk") != 0) { + lnk_added = 1; + } + } + path = *buf ? buf : "/"; +#ifdef HAVE_CYGWIN_CONV_PATH + bufsize = cygwin_conv_path(flags, path, NULL, 0); + if (bufsize > 0) { + bufsize += len; + if (lnk_added) bufsize += 4; + w32buf = ALLOCA_N(char, bufsize); + if (cygwin_conv_path(flags, path, w32buf, bufsize) == 0) { + b = w32buf; + } + } +#else /* !HAVE_CYGWIN_CONV_PATH */ + bufsize = MAXPATHLEN; + if (cygwin_conv_to_win32_path(path, w32buf) == 0) { + b = w32buf; + } +#endif /* !HAVE_CYGWIN_CONV_PATH */ + if (is_symlink && b == w32buf) { + *p = '\\'; + strlcat(w32buf, p, bufsize); + if (lnk_added) { + strlcat(w32buf, ".lnk", bufsize); + } + } + else { + lnk_added = 0; + } + *p = '/'; +#endif /* __CYGWIN__ */ + rb_str_set_len(result, p - buf + strlen(p)); + encidx = ENCODING_GET(result); + tmp = result; + if (encidx != ENCINDEX_UTF_8 && !is_ascii_string(result)) { + tmp = rb_str_encode_ospath(result); + } + len = MultiByteToWideChar(CP_UTF8, 0, RSTRING_PTR(tmp), -1, NULL, 0); + wstr = ALLOCV_N(WCHAR, v, len); + MultiByteToWideChar(CP_UTF8, 0, RSTRING_PTR(tmp), -1, wstr, len); + if (tmp != result) rb_str_set_len(tmp, 0); + h = FindFirstFileW(wstr, &wfd); + ALLOCV_END(v); + if (h != INVALID_HANDLE_VALUE) { + size_t wlen; + FindClose(h); + len = lstrlenW(wfd.cFileName); #ifdef __CYGWIN__ - int lnk_added = 0, is_symlink = 0; - struct stat st; - char w32buf[MAXPATHLEN], sep = 0; - p = 0; - if (lstat(buf, &st) == 0 && S_ISLNK(st.st_mode)) { - is_symlink = 1; - p = strrdirsep(buf); - if (!p) p = skipprefix(buf); - if (p) { - sep = *p; - *p = '\0'; - } - } - if (cygwin_conv_to_win32_path(buf, w32buf) == 0) { - b = w32buf; - } - if (p) *p = sep; - else p = buf; - if (is_symlink && b == w32buf) { - len = strlen(p); - if (len > 4 && strcasecmp(p + len - 4, ".lnk") != 0) { - lnk_added = 1; - strlcat(w32buf, ".lnk", sizeof(w32buf)); - } - } -#endif - HANDLE h = FindFirstFile(b, &wfd); - if (h != INVALID_HANDLE_VALUE) { - FindClose(h); - p = strrdirsep(buf); - len = strlen(wfd.cFileName); + if (lnk_added && len > 4 && + wcscasecmp(wfd.cFileName + len - 4, L".lnk") == 0) { + wfd.cFileName[len -= 4] = L'\0'; + } +#else + p = (char *)s; +#endif + ++p; + wlen = (int)len; + len = WideCharToMultiByte(CP_UTF8, 0, wfd.cFileName, wlen, NULL, 0, NULL, NULL); + if (tmp == result) { + BUFCHECK(bdiff + len >= buflen); + WideCharToMultiByte(CP_UTF8, 0, wfd.cFileName, wlen, p, len + 1, NULL, NULL); + } + else { + rb_str_modify_expand(tmp, len); + WideCharToMultiByte(CP_UTF8, 0, wfd.cFileName, wlen, RSTRING_PTR(tmp), len + 1, NULL, NULL); + rb_str_cat_conv_enc_opts(result, bdiff, RSTRING_PTR(tmp), len, + rb_utf8_encoding(), 0, Qnil); + BUFINIT(); + rb_str_resize(tmp, 0); + } + p += len; + } #ifdef __CYGWIN__ - if (lnk_added && len > 4 && - strcasecmp(wfd.cFileName + len - 4, ".lnk") == 0) { - len -= 4; - } + else { + p += strlen(p); + } #endif - if (!p) p = buf; - buflen = ++p - buf + len; - rb_str_resize(result, buflen); - memcpy(p, wfd.cFileName, len + 1); - } } -#endif +#endif /* USE_NTFS */ - if (tainted) OBJ_TAINT(result); - rb_str_set_len(result, buflen); + rb_str_set_len(result, p - buf); + rb_enc_check(fname, result); + ENC_CODERANGE_CLEAR(result); return result; } +#endif /* !_WIN32 (this ifdef started above rb_default_home_dir) */ + +#define EXPAND_PATH_BUFFER() rb_usascii_str_new(0, 1) + +static VALUE +str_shrink(VALUE str) +{ + rb_str_resize(str, RSTRING_LEN(str)); + return str; +} + +#define expand_path(fname, dname, abs_mode, long_name, result) \ + str_shrink(rb_file_expand_path_internal(fname, dname, abs_mode, long_name, result)) + +#define check_expand_path_args(fname, dname) \ + (((fname) = rb_get_path(fname)), \ + (void)(NIL_P(dname) ? (dname) : ((dname) = rb_get_path(dname)))) + +static VALUE +file_expand_path_1(VALUE fname) +{ + return rb_file_expand_path_internal(fname, Qnil, 0, 0, EXPAND_PATH_BUFFER()); +} VALUE -rb_file_expand_path(fname, dname) - VALUE fname, dname; +rb_file_expand_path(VALUE fname, VALUE dname) { - return file_expand_path(fname, dname, rb_str_new(0, MAXPATHLEN + 2)); + check_expand_path_args(fname, dname); + return expand_path(fname, dname, 0, 1, EXPAND_PATH_BUFFER()); +} + +VALUE +rb_file_expand_path_fast(VALUE fname, VALUE dname) +{ + return expand_path(fname, dname, 0, 0, EXPAND_PATH_BUFFER()); +} + +VALUE +rb_file_s_expand_path(int argc, const VALUE *argv) +{ + rb_check_arity(argc, 1, 2); + return rb_file_expand_path(argv[0], argc > 1 ? argv[1] : Qnil); } /* * call-seq: - * File.expand_path(file_name [, dir_string] ) -> abs_file_name - * + * File.expand_path(file_name [, dir_string] ) -> abs_file_name + * * Converts a pathname to an absolute pathname. Relative paths are * referenced from the current working directory of the process unless - * <i>dir_string</i> is given, in which case it will be used as the + * +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 <code>HOME</code> must be set + * directory (the environment variable +HOME+ must be set * correctly). ``<code>~</code><i>user</i>'' expands to the named * user's home directory. - * + * * File.expand_path("~oracle/bin") #=> "/home/oracle/bin" - * File.expand_path("../../bin", "/tmp/x") #=> "/bin" + * + * A simple example of using +dir_string+ is as follows. + * File.expand_path("ruby", "/usr/bin") #=> "/usr/bin/ruby" + * + * 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. + * + * File.expand_path("../../lib/mygem.rb", __FILE__) + * #=> ".../path/to/project/lib/mygem.rb" + * + * 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 +s_expand_path(int c, const VALUE * v, VALUE _) +{ + return rb_file_s_expand_path(c, v); +} + VALUE -rb_file_s_expand_path(argc, argv) - int argc; - VALUE *argv; +rb_file_absolute_path(VALUE fname, VALUE dname) { - VALUE fname, dname; + check_expand_path_args(fname, dname); + return expand_path(fname, dname, 1, 1, EXPAND_PATH_BUFFER()); +} - if (argc == 1) { - return rb_file_expand_path(argv[0], Qnil); - } - rb_scan_args(argc, argv, "11", &fname, &dname); +VALUE +rb_file_s_absolute_path(int argc, const VALUE *argv) +{ + rb_check_arity(argc, 1, 2); + return rb_file_absolute_path(argv[0], argc > 1 ? argv[1] : Qnil); +} + +/* + * call-seq: + * File.absolute_path(file_name [, dir_string] ) -> abs_file_name + * + * Converts a pathname to an absolute pathname. Relative paths are + * referenced from the current working directory of the process unless + * <i>dir_string</i> is given, in which case it will be used as the + * starting point. If the given pathname starts with a ``<code>~</code>'' + * it is NOT expanded, it is treated as a normal directory name. + * + * File.absolute_path("~oracle/bin") #=> "<relative_path>/~oracle/bin" + */ - return rb_file_expand_path(fname, dname); +static VALUE +s_absolute_path(int c, const VALUE * v, VALUE _) +{ + return rb_file_s_absolute_path(c, v); +} + +/* + * call-seq: + * File.absolute_path?(file_name) -> true or false + * + * Returns <code>true</code> if +file_name+ is an absolute path, and + * <code>false</code> otherwise. + * + * File.absolute_path?("c:/foo") #=> false (on Linux), true (on Windows) + */ + +static VALUE +s_absolute_path_p(VALUE klass, VALUE fname) +{ + VALUE path = rb_get_path(fname); + + if (!rb_is_absolute_path(RSTRING_PTR(path))) return Qfalse; + return Qtrue; } +enum rb_realpath_mode { + RB_REALPATH_CHECK, + RB_REALPATH_DIR, + RB_REALPATH_STRICT, + RB_REALPATH_MODE_MAX +}; + static int -rmext(p, l1, e) - const char *p, *e; - int l1; +realpath_rec(long *prefixlenp, VALUE *resolvedp, const char *unresolved, VALUE fallback, + VALUE loopcheck, enum rb_realpath_mode mode, int last) +{ + const char *pend = unresolved + strlen(unresolved); + rb_encoding *enc = rb_enc_get(*resolvedp); + ID resolving; + CONST_ID(resolving, "resolving"); + while (unresolved < pend) { + const char *testname = unresolved; + const char *unresolved_firstsep = rb_enc_path_next(unresolved, pend, enc); + long testnamelen = unresolved_firstsep - unresolved; + const char *unresolved_nextname = unresolved_firstsep; + while (unresolved_nextname < pend && isdirsep(*unresolved_nextname)) + unresolved_nextname++; + unresolved = unresolved_nextname; + if (testnamelen == 1 && testname[0] == '.') { + } + else if (testnamelen == 2 && testname[0] == '.' && testname[1] == '.') { + if (*prefixlenp < RSTRING_LEN(*resolvedp)) { + 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), true, enc); + long len = lastsep ? lastsep - resolved_names : 0; + rb_str_resize(*resolvedp, *prefixlenp + len); + } + } + else { + VALUE checkval; + VALUE testpath = rb_str_dup(*resolvedp); + if (*prefixlenp < RSTRING_LEN(testpath)) + rb_str_cat2(testpath, "/"); +#if defined(DOSISH_UNC) || defined(DOSISH_DRIVE_LETTER) + if (*prefixlenp > 1 && *prefixlenp == RSTRING_LEN(testpath)) { + const char *prefix = RSTRING_PTR(testpath); + const char *last = rb_enc_left_char_head(prefix, prefix + *prefixlenp - 1, prefix + *prefixlenp, enc); + if (!isdirsep(*last)) rb_str_cat2(testpath, "/"); + } +#endif + rb_str_cat(testpath, testname, testnamelen); + checkval = rb_hash_aref(loopcheck, testpath); + if (!NIL_P(checkval)) { + if (checkval == ID2SYM(resolving)) { + if (mode == RB_REALPATH_CHECK) { + errno = ELOOP; + return -1; + } + rb_syserr_fail_path(ELOOP, testpath); + } + else { + *resolvedp = rb_str_dup(checkval); + } + } + else { + struct stat sbuf; + int ret; + ret = lstat_without_gvl(RSTRING_PTR(testpath), &sbuf); + if (ret == -1) { + int e = errno; + if (e == ENOENT && !NIL_P(fallback)) { + if (stat_without_gvl(RSTRING_PTR(fallback), &sbuf) == 0) { + rb_str_replace(*resolvedp, fallback); + return 0; + } + } + if (mode == RB_REALPATH_CHECK) return -1; + if (e == ENOENT) { + if (mode == RB_REALPATH_STRICT || !last || *unresolved_firstsep) + rb_syserr_fail_path(e, testpath); + *resolvedp = testpath; + break; + } + else { + rb_syserr_fail_path(e, testpath); + } + } +#ifdef HAVE_READLINK + if (S_ISLNK(sbuf.st_mode)) { + VALUE link; + VALUE link_orig = Qnil; + const char *link_prefix, *link_names; + long link_prefixlen; + rb_hash_aset(loopcheck, testpath, ID2SYM(resolving)); + link = rb_readlink(testpath, enc); + link_prefix = RSTRING_PTR(link); + link_names = skipprefixroot(link_prefix, link_prefix + RSTRING_LEN(link), rb_enc_get(link)); + link_prefixlen = link_names - link_prefix; + if (link_prefixlen > 0) { + rb_encoding *tmpenc, *linkenc = rb_enc_get(link); + link_orig = link; + link = rb_str_subseq(link, 0, link_prefixlen); + tmpenc = fs_enc_check(*resolvedp, link); + if (tmpenc != linkenc) link = rb_str_conv_enc(link, linkenc, tmpenc); + *resolvedp = link; + *prefixlenp = link_prefixlen; + } + if (realpath_rec(prefixlenp, resolvedp, link_names, testpath, + loopcheck, mode, !*unresolved_firstsep)) + return -1; + RB_GC_GUARD(link_orig); + rb_hash_aset(loopcheck, testpath, rb_str_dup_frozen(*resolvedp)); + } + else +#endif /* HAVE_READLINK */ + { + VALUE s = rb_str_dup_frozen(testpath); + rb_hash_aset(loopcheck, s, s); + *resolvedp = testpath; + } + } + } + } + return 0; +} + +static VALUE +rb_check_realpath_emulate(VALUE basedir, VALUE path, rb_encoding *origenc, enum rb_realpath_mode mode) { - int l2; + long prefixlen; + VALUE resolved; + VALUE unresolved_path; + VALUE loopcheck; + VALUE curdir = Qnil; + + rb_encoding *enc; + char *path_names = NULL, *basedir_names = NULL, *curdir_names = NULL; + char *ptr, *prefixptr = NULL, *pend; + long len; + + unresolved_path = rb_str_dup_frozen(path); - if (!e) return 0; + if (!NIL_P(basedir)) { + FilePathValue(basedir); + basedir = TO_OSPATH(rb_str_dup_frozen(basedir)); + } + + enc = rb_enc_get(unresolved_path); + unresolved_path = TO_OSPATH(unresolved_path); + RSTRING_GETMEM(unresolved_path, ptr, len); + path_names = skipprefixroot(ptr, ptr + len, rb_enc_get(unresolved_path)); + if (ptr != path_names) { + resolved = rb_str_subseq(unresolved_path, 0, path_names - ptr); + goto root_found; + } + + if (!NIL_P(basedir)) { + RSTRING_GETMEM(basedir, ptr, len); + basedir_names = skipprefixroot(ptr, ptr + len, rb_enc_get(basedir)); + if (ptr != basedir_names) { + resolved = rb_str_subseq(basedir, 0, basedir_names - ptr); + goto root_found; + } + } + + curdir = rb_dir_getwd_ospath(); + RSTRING_GETMEM(curdir, ptr, len); + curdir_names = skipprefixroot(ptr, ptr + len, rb_enc_get(curdir)); + resolved = rb_str_subseq(curdir, 0, curdir_names - ptr); + + root_found: + RSTRING_GETMEM(resolved, prefixptr, prefixlen); + pend = prefixptr + prefixlen; + ptr = chompdirsep(prefixptr, pend, true, enc); + if (ptr < pend) { + prefixlen = ++ptr - prefixptr; + rb_str_set_len(resolved, prefixlen); + } +#ifdef FILE_ALT_SEPARATOR + while (prefixptr < ptr) { + if (*prefixptr == FILE_ALT_SEPARATOR) { + *prefixptr = '/'; + } + Inc(prefixptr, pend, true, enc); + } +#endif + + switch (rb_enc_to_index(enc)) { + case ENCINDEX_ASCII_8BIT: + case ENCINDEX_US_ASCII: + rb_enc_associate_index(resolved, rb_filesystem_encindex()); + } + + loopcheck = rb_hash_new(); + if (curdir_names) { + if (realpath_rec(&prefixlen, &resolved, curdir_names, Qnil, loopcheck, mode, 0)) + return Qnil; + } + if (basedir_names) { + if (realpath_rec(&prefixlen, &resolved, basedir_names, Qnil, loopcheck, mode, 0)) + return Qnil; + } + if (realpath_rec(&prefixlen, &resolved, path_names, Qnil, loopcheck, mode, 1)) + return Qnil; + + if (origenc && origenc != rb_enc_get(resolved)) { + if (rb_enc_str_asciionly_p(resolved)) { + rb_enc_associate(resolved, origenc); + } + else { + resolved = rb_str_conv_enc(resolved, NULL, origenc); + } + } + + RB_GC_GUARD(unresolved_path); + RB_GC_GUARD(curdir); + return resolved; +} + +static VALUE rb_file_join(long argc, VALUE *args); + +#ifndef HAVE_REALPATH +static VALUE +rb_check_realpath_emulate_try(VALUE arg) +{ + VALUE *args = (VALUE *)arg; + return rb_check_realpath_emulate(args[0], args[1], (rb_encoding *)args[2], RB_REALPATH_CHECK); +} + +static VALUE +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 +rb_check_realpath_internal(VALUE basedir, VALUE path, rb_encoding *origenc, enum rb_realpath_mode mode) +{ +#ifdef HAVE_REALPATH + 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); + } + + unresolved_path = rb_str_dup_frozen(path); + if (*RSTRING_PTR(unresolved_path) != '/' && !NIL_P(basedir)) { + 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), 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 == ENOTSUP || + errno == ENOTDIR || + (errno == ENOENT && rb_file_exist_p(0, unresolved_path))) { + return rb_check_realpath_emulate(basedir, path, origenc, mode); + + } + if (mode == RB_REALPATH_CHECK) { + return Qnil; + } + 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__) + /* As `resolved` is a String in the filesystem encoding, no + * conversion is needed */ + struct stat st; + if (stat_without_gvl(RSTRING_PTR(resolved), &st) < 0) { + if (mode == RB_REALPATH_CHECK) { + return Qnil; + } + rb_sys_fail_path(unresolved_path); + } +# endif /* !defined(__linux__) && !defined(__APPLE__) */ + + if (origenc && origenc != rb_enc_get(resolved)) { + if (!rb_enc_str_asciionly_p(resolved)) { + resolved = rb_str_conv_enc(resolved, NULL, origenc); + } + rb_enc_associate(resolved, origenc); + } + + if (is_broken_string(resolved)) { + rb_enc_associate(resolved, rb_filesystem_encoding()); + if (is_broken_string(resolved)) { + rb_enc_associate(resolved, rb_ascii8bit_encoding()); + } + } + + RB_GC_GUARD(unresolved_path); + return resolved; +#else /* !HAVE_REALPATH */ + if (mode == RB_REALPATH_CHECK) { + VALUE arg[3]; + arg[0] = basedir; + arg[1] = path; + arg[2] = (VALUE)origenc; + + return rb_rescue(rb_check_realpath_emulate_try, (VALUE)arg, + rb_check_realpath_emulate_rescue, Qnil); + } + else { + return rb_check_realpath_emulate(basedir, path, origenc, mode); + } +#endif /* HAVE_REALPATH */ +} + +VALUE +rb_realpath_internal(VALUE basedir, VALUE path, int strict) +{ + const enum rb_realpath_mode mode = + strict ? RB_REALPATH_STRICT : RB_REALPATH_DIR; + return rb_check_realpath_internal(basedir, path, rb_enc_get(path), mode); +} + +VALUE +rb_check_realpath(VALUE basedir, VALUE path, rb_encoding *enc) +{ + return rb_check_realpath_internal(basedir, path, enc, RB_REALPATH_CHECK); +} + +/* + * call-seq: + * File.realpath(pathname [, dir_string]) -> real_pathname + * + * Returns the real (absolute) pathname of _pathname_ in the actual + * filesystem not containing symlinks or useless dots. + * + * If _dir_string_ is given, it is used as a base directory + * for interpreting relative pathname instead of the current directory. + * + * All components of the pathname must exist when this method is + * called. + */ +static VALUE +rb_file_s_realpath(int argc, VALUE *argv, VALUE klass) +{ + VALUE basedir = (rb_check_arity(argc, 1, 2) > 1) ? argv[1] : Qnil; + VALUE path = argv[0]; + FilePathValue(path); + return rb_realpath_internal(basedir, path, 1); +} - l2 = strlen(e); - if (l2 == 2 && e[1] == '*') { - unsigned char c = *e; - e = p + l1; - do { - if (e <= p) return 0; - } while (*--e != c); - return e - p; +/* + * call-seq: + * File.realdirpath(pathname [, dir_string]) -> real_pathname + * + * Returns the real (absolute) pathname of _pathname_ in the actual filesystem. + * The real pathname doesn't contain symlinks or useless dots. + * + * If _dir_string_ is given, it is used as a base directory + * for interpreting relative pathname instead of the current directory. + * + * The last component of the real pathname can be nonexistent. + */ +static VALUE +rb_file_s_realdirpath(int argc, VALUE *argv, VALUE klass) +{ + VALUE basedir = (rb_check_arity(argc, 1, 2) > 1) ? argv[1] : Qnil; + VALUE path = argv[0]; + FilePathValue(path); + return rb_realpath_internal(basedir, path, 0); +} + +static size_t +rmext(const char *p, long l0, long l1, const char *e, long l2, rb_encoding *enc) +{ + int len1, len2; + unsigned int c; + const char *s, *last; + + if (!e || !l2) return 0; + + c = rb_enc_codepoint_len(e, e + l2, &len1, enc); + if (rb_enc_ascget(e + len1, e + l2, &len2, enc) == '*' && len1 + len2 == l2) { + if (c == '.') return l0; + s = p; + e = p + l1; + last = e; + while (s < e) { + if (rb_enc_codepoint_len(s, e, &len1, enc) == c) last = s; + s += len1; + } + return last - p; } if (l1 < l2) return l1; + s = p+l1-l2; + if (!at_char_boundary(p, s, p+l1, enc)) return 0; #if CASEFOLD_FILESYSTEM #define fncomp strncasecmp #else #define fncomp strncmp #endif - if (fncomp(p+l1-l2, e, l2) == 0) { - return l1-l2; + if (fncomp(s, e, l2) == 0) { + return l1-l2; } return 0; } -/* - * call-seq: - * File.basename(file_name [, suffix] ) -> base_name - * - * Returns the last component of the filename given in <i>file_name</i>, - * which must be formed using forward slashes (``<code>/</code>'') - * regardless of the separator used on the local file system. If - * <i>suffix</i> is given and present at the end of <i>file_name</i>, - * it is removed. - * - * File.basename("/home/gumby/work/ruby.rb") #=> "ruby.rb" - * File.basename("/home/gumby/work/ruby.rb", ".rb") #=> "ruby" - */ - -static VALUE -rb_file_s_basename(argc, argv) - int argc; - VALUE *argv; +static inline const char * +enc_find_basename(const char *name, long *baselen, long *alllen, bool mb_enc, rb_encoding *enc) { - VALUE fname, fext, basename; - char *name, *p; + const char *p, *q, *e, *end; #if defined DOSISH_DRIVE_LETTER || defined DOSISH_UNC - char *root; + const char *root; #endif - int f, n; + long f = 0, n = -1; + + long len = (alllen ? (size_t)*alllen : strlen(name)); - if (rb_scan_args(argc, argv, "11", &fname, &fext) == 2) { - StringValue(fext); + if (len <= 0) { + return name; } - StringValue(fname); - if (RSTRING(fname)->len == 0 || !*(name = RSTRING(fname)->ptr)) - return fname; - name = skipprefix(name); + + end = name + len; + name = skipprefix(name, end, mb_enc, enc); #if defined DOSISH_DRIVE_LETTER || defined DOSISH_UNC root = name; #endif - while (isdirsep(*name)) - name++; + + while (isdirsep(*name)) { + name++; + } + if (!*name) { - p = name - 1; - f = 1; + p = name - 1; + f = 1; #if defined DOSISH_DRIVE_LETTER || defined DOSISH_UNC - if (name != root) { - /* has slashes */ - } + if (name != root) { + /* has slashes */ + } #ifdef DOSISH_DRIVE_LETTER - else if (*p == ':') { - p++; - f = 0; - } -#endif + else if (*p == ':') { + p++; + f = 0; + } +#endif /* DOSISH_DRIVE_LETTER */ #ifdef DOSISH_UNC - else { - p = "/"; - } -#endif -#endif + else { + p = "/"; + } +#endif /* DOSISH_UNC */ +#endif /* defined DOSISH_DRIVE_LETTER || defined DOSISH_UNC */ } else { - if (!(p = strrdirsep(name))) { - p = name; - } - else { - while (isdirsep(*p)) p++; /* skip last / */ - } + p = strrdirsep(name, end, mb_enc, enc); + if (!p) { + p = name; + } + else { + while (isdirsep(*p)) { + p++; /* skip last / */ + } + } #if USE_NTFS - n = ntfs_tail(p) - p; + n = ntfs_tail(p, end, enc) - p; #else - n = chompdirsep(p) - p; -#endif - if (NIL_P(fext) || !(f = rmext(p, n, StringValueCStr(fext)))) { - f = n; - } - if (f == RSTRING_LEN(fname)) return fname; + 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, mb_enc, enc)) { + if (*q == '.') e = q; + } + if (e) { + f = e - p; + } + else { + f = n; + } + } + + if (baselen) { + *baselen = f; + } + 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, true, enc); +} + +/* + * call-seq: + * File.basename(file_name [, suffix] ) -> base_name + * + * 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. + * + * 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; + const char *name, *p; + long f, n; + rb_encoding *enc; + + fext = Qnil; + if (rb_check_arity(argc, 1, 2) == 2) { + fext = argv[1]; + StringValue(fext); + check_path_encoding(fext); + enc = rb_str_enc_get(fext); } - basename = rb_str_new(p, f); - OBJ_INFECT(basename, fname); - return basename; + fname = argv[0]; + CheckPath(fname, name); + if (NIL_P(fext) || !(enc = rb_enc_compatible(fname, fext))) { + enc = rb_str_enc_get(fname); + fext = Qnil; + } + + n = RSTRING_LEN(fname); + if (n == 0 || !*name) { + rb_enc_str_new(0, 0, enc); + } + + bool mb_enc = !rb_str_encindex_fastpath(rb_enc_to_index(enc)); + p = enc_find_basename(name, &f, &n, mb_enc, enc); + if (n >= 0) { + if (NIL_P(fext)) { + 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); + } + } + + return rb_enc_str_new(p, f, enc); } +static VALUE rb_file_dirname_n(VALUE fname, int n); + /* * call-seq: - * File.dirname(file_name ) -> dir_name - * + * File.dirname(file_name, level = 1) -> dir_name + * * Returns all components of the filename given in <i>file_name</i> - * except the last one. The filename must be formed using forward - * slashes (``<code>/</code>'') regardless of the separator used on the - * local file system. - * + * except the last one (after first stripping trailing separators). + * The filename can be formed using both File::SEPARATOR and + * File::ALT_SEPARATOR as the separator when File::ALT_SEPARATOR is + * not <code>nil</code>. + * * File.dirname("/home/gumby/work/ruby.rb") #=> "/home/gumby/work" + * + * If +level+ is given, removes the last +level+ components, not only + * one. + * + * File.dirname("/home/gumby/work/ruby.rb", 2) #=> "/home/gumby" + * File.dirname("/home/gumby/work/ruby.rb", 4) #=> "/" */ static VALUE -rb_file_s_dirname(klass, fname) - VALUE klass, fname; +rb_file_s_dirname(int argc, VALUE *argv, VALUE klass) { - const char *name, *root, *p; + int n = 1; + if ((argc = rb_check_arity(argc, 1, 2)) > 1) { + n = NUM2INT(argv[1]); + } + return rb_file_dirname_n(argv[0], n); +} + +VALUE +rb_file_dirname(VALUE fname) +{ + return rb_file_dirname_n(fname, 1); +} + +static VALUE +rb_file_dirname_n(VALUE fname, int n) +{ + const char *name, *root, *p, *end; VALUE dirname; - name = StringValueCStr(fname); - root = skiproot(name); + if (n < 0) rb_raise(rb_eArgError, "negative level: %d", n); + CheckPath(fname, name); + end = name + RSTRING_LEN(fname); + + 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); + root = skipprefix(name = root - 2, end, mb_enc, enc); #else if (root > name + 1) - name = root - 1; + name = root - 1; #endif - p = strrdirsep(root); - if (!p) { - p = root; + if (n > (end - root + 1) / 2) { + p = root; + } + else { + p = end; + while (n) { + if (!(p = strrdirsep(root, p, mb_enc, enc))) { + p = root; + break; + } + n--; + } + } + + if (p == name) { + return rb_enc_str_new(".", 1, enc); } - if (p == name) - return rb_str_new2("."); #ifdef DOSISH_DRIVE_LETTER if (has_drive_letter(name) && isdirsep(*(name + 2))) { - const char *top = skiproot(name + 2); - dirname = rb_str_new(name, 3); - rb_str_cat(dirname, top, p - top); + 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) - rb_str_cat(dirname, ".", 1); + rb_str_cat(dirname, ".", 1); #endif - OBJ_INFECT(dirname, fname); return dirname; } -/* - * call-seq: - * File.extname(path) -> string - * - * Returns the extension (the portion of file name in <i>path</i> - * after the period). - * - * File.extname("test.rb") #=> ".rb" - * File.extname("a/b/d/test.rb") #=> ".rb" - * File.extname("test") #=> "" - * File.extname(".profile") #=> "" - * - */ - -static VALUE -rb_file_s_extname(klass, fname) - VALUE klass, fname; +static inline const char * +enc_find_extname(const char *name, long *len, bool mb_enc, rb_encoding *enc) { - const char *name, *p, *e; - VALUE extname; + const char *p, *e, *end = name + (len ? *len : (long)strlen(name)); - name = StringValueCStr(fname); - p = strrdirsep(name); /* get the last path component */ + p = strrdirsep(name, end, mb_enc, enc); /* get the last path component */ if (!p) - p = name; + p = name; else - name = ++p; + do name = ++p; while (isdirsep(*p)); e = 0; + while (*p && *p == '.') p++; while (*p) { - if (*p == '.' || istrailinggabage(*p)) { + if (*p == '.' || istrailinggarbage(*p)) { #if USE_NTFS - const char *last = p++, *dot = last; - while (istrailinggabage(*p)) { - if (*p == '.') dot = p; - p++; - } - if (!*p || *p == ':') { - p = last; - break; - } - e = dot; - continue; + const char *last = p++, *dot = last; + while (istrailinggarbage(*p)) { + if (*p == '.') dot = p; + p++; + } + if (!*p || isADS(*p)) { + p = last; + break; + } + if (*last == '.' || dot > last) e = dot; + continue; #else - e = p; /* get the last dot of the last component */ -#endif - } + e = p; /* get the last dot of the last component */ +#endif /* USE_NTFS */ + } #if USE_NTFS - else if (*p == ':') { - break; - } + else if (isADS(*p)) { + break; + } #endif - else if (isdirsep(*p)) - break; - p = CharNext(p); + else if (isdirsep(*p)) + break; + Inc(p, end, mb_enc, enc); } - if (!e || e == name || e+1 == p) /* no dot, or the only dot is first or end? */ - return rb_str_new(0, 0); - extname = rb_str_new(e, p - e); /* keep the dot, too! */ - OBJ_INFECT(extname, fname); - return extname; + + if (len) { + /* no dot, or the only dot is first or end? */ + if (!e || e == name) + *len = 0; + else if (e+1 == p) + *len = 1; + else + *len = p - e; + } + return e; +} + +/* + * 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, true, enc); } /* * call-seq: - * File.split(file_name) => array - * + * File.extname(path) -> string + * + * Returns the extension (the portion of file name in +path+ + * starting from the last period). + * + * If +path+ is a dotfile, or starts with a period, then the starting + * dot is not dealt with the start of the extension. + * + * An empty string will also be returned when the period is the last character + * in +path+. + * + * On Windows, trailing dots are truncated. + * + * 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" + * + */ + +static VALUE +rb_file_s_extname(VALUE klass, VALUE fname) +{ + const char *name; + CheckPath(fname, name); + long len = RSTRING_LEN(fname); + + 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: + * File.path(path) -> string + * + * Returns the string representation of the path + * + * 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 coersion 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 +rb_file_s_path(VALUE klass, VALUE fname) +{ + return rb_get_path(fname); +} + +/* + * call-seq: + * File.split(file_name) -> array + * * Splits the given string into a directory and a file component and - * returns them in a two-element array. See also - * <code>File::dirname</code> and <code>File::basename</code>. - * + * returns them in a two-element array. See also File::dirname and + * File::basename. + * * File.split("/home/gumby/.profile") #=> ["/home/gumby", ".profile"] */ static VALUE -rb_file_s_split(klass, path) - VALUE klass, path; +rb_file_s_split(VALUE klass, VALUE path) { - StringValue(path); /* get rid of converting twice */ - return rb_assoc_new(rb_file_s_dirname(Qnil, path), rb_file_s_basename(1,&path)); + FilePathStringValue(path); /* get rid of converting twice */ + return rb_assoc_new(rb_file_dirname(path), rb_file_s_basename(1,&path,Qundef)); } -static VALUE separator; - -static VALUE rb_file_join _((VALUE ary, VALUE sep)); +static VALUE rb_file_join_ary(VALUE ary); static VALUE -file_inspect_join(ary, arg) - VALUE ary; - VALUE *arg; +file_inspect_join(VALUE ary, VALUE arg, int recur) { - return rb_file_join(arg[0], arg[1]); + if (recur || ary == arg) rb_raise(rb_eArgError, "recursive array"); + return rb_file_join_ary(arg); } static VALUE -rb_file_join(ary, sep) - VALUE ary, sep; +rb_file_join_ary(VALUE ary) { long len, i; - int taint = 0; VALUE result, tmp; - char *name, *tail; + const char *name, *tail; + int checked = TRUE; + rb_encoding *enc; - if (RARRAY(ary)->len == 0) return rb_str_new(0, 0); - if (OBJ_TAINTED(ary)) taint = 1; - if (OBJ_TAINTED(sep)) taint = 1; + if (RARRAY_LEN(ary) == 0) return rb_str_new(0, 0); len = 1; - for (i=0; i<RARRAY(ary)->len; i++) { - if (TYPE(RARRAY(ary)->ptr[i]) == T_STRING) { - len += RSTRING(RARRAY(ary)->ptr[i])->len; - } - else { - len += 10; - } - } - if (!NIL_P(sep) && TYPE(sep) == T_STRING) { - len += RSTRING(sep)->len * RARRAY(ary)->len - 1; + for (i=0; i<RARRAY_LEN(ary); i++) { + tmp = RARRAY_AREF(ary, i); + if (RB_TYPE_P(tmp, T_STRING)) { + check_path_encoding(tmp); + len += RSTRING_LEN(tmp); + } + else { + len += 10; + } } + len += RARRAY_LEN(ary) - 1; result = rb_str_buf_new(len); - for (i=0; i<RARRAY(ary)->len; i++) { - tmp = RARRAY(ary)->ptr[i]; - switch (TYPE(tmp)) { - case T_STRING: - break; - case T_ARRAY: - if (rb_inspecting_p(tmp)) { - tmp = rb_str_new2("[...]"); - } - else { - VALUE args[2]; - - args[0] = tmp; - args[1] = sep; - tmp = rb_protect_inspect(file_inspect_join, ary, (VALUE)args); - } - break; - default: - StringValueCStr(tmp); - } - name = StringValueCStr(result); - if (i > 0 && !NIL_P(sep)) { - tail = chompdirsep(name); - if (RSTRING(tmp)->ptr && isdirsep(RSTRING(tmp)->ptr[0])) { - RSTRING(result)->len = tail - name; - } - else if (!*tail) { - rb_str_buf_append(result, sep); - } - } - rb_str_buf_append(result, tmp); - if (OBJ_TAINTED(tmp)) taint = 1; - } - - if (taint) OBJ_TAINT(result); + RBASIC_CLEAR_CLASS(result); + for (i=0; i<RARRAY_LEN(ary); i++) { + tmp = RARRAY_AREF(ary, i); + switch (OBJ_BUILTIN_TYPE(tmp)) { + case T_STRING: + if (!checked) check_path_encoding(tmp); + StringValueCStr(tmp); + break; + case T_ARRAY: + if (ary == tmp) { + rb_raise(rb_eArgError, "recursive array"); + } + else { + tmp = rb_exec_recursive(file_inspect_join, ary, tmp); + } + break; + default: + FilePathStringValue(tmp); + checked = FALSE; + } + RSTRING_GETMEM(result, name, len); + if (i == 0) { + rb_enc_copy(result, tmp); + } + else { + tail = chompdirsep(name, name + len, true, rb_enc_get(result)); + if (RSTRING_PTR(tmp) && isdirsep(RSTRING_PTR(tmp)[0])) { + rb_str_set_len(result, tail - name); + } + else if (!*tail) { + rb_str_cat(result, "/", 1); + } + } + enc = fs_enc_check(result, tmp); + rb_str_buf_append(result, tmp); + rb_enc_associate(result, enc); + } + RBASIC_SET_CLASS_RAW(result, rb_cString); + 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 (isdirsep(tmp_s[0])) { + // right side has a leading separator, remove left side separators. + long trailing_seps = 0; + while (isdirsep(name[len - trailing_seps - 1])) { + trailing_seps++; + } + rb_str_set_len(result, len - trailing_seps); + } + else if (!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, ...) -> path - * + * File.join(string, ...) -> string + * * Returns a new string formed by joining the strings using - * <code>File::SEPARATOR</code>. - * + * <code>"/"</code>. + * * File.join("usr", "mail", "gumby") #=> "usr/mail/gumby" - * + * */ static VALUE -rb_file_s_join(klass, args) - VALUE klass, args; +rb_file_s_join(int argc, VALUE *argv, VALUE klass) +{ + return rb_file_join(argc, argv); +} + +#if defined(HAVE_TRUNCATE) +struct truncate_arg { + const char *path; + rb_off_t pos; +}; + +static void * +nogvl_truncate(void *ptr) { - return rb_file_join(args, separator); + struct truncate_arg *ta = ptr; + return (void *)(VALUE)truncate(ta->path, ta->pos); } /* * call-seq: - * File.truncate(file_name, integer) => 0 - * + * File.truncate(file_name, integer) -> 0 + * * Truncates the file <i>file_name</i> to be at most <i>integer</i> * bytes long. Not available on all platforms. - * + * * f = File.new("out", "w") * f.write("1234567890") #=> 10 * f.close #=> nil * File.truncate("out", 5) #=> 0 * File.size("out") #=> 5 - * + * */ static VALUE -rb_file_s_truncate(klass, path, len) - VALUE klass, path, len; +rb_file_s_truncate(VALUE klass, VALUE path, VALUE len) { - off_t pos; + struct truncate_arg ta; + int r; - rb_secure(2); - pos = NUM2OFFT(len); - SafeStringValue(path); + ta.pos = NUM2OFFT(len); + FilePathValue(path); + path = rb_str_encode_ospath(path); + ta.path = StringValueCStr(path); -#ifdef HAVE_TRUNCATE - if (truncate(StringValueCStr(path), pos) < 0) - rb_sys_fail(RSTRING(path)->ptr); + r = IO_WITHOUT_GVL_INT(nogvl_truncate, &ta); + if (r < 0) + rb_sys_fail_path(path); + return INT2FIX(0); +} #else -# ifdef HAVE_CHSIZE - { - int tmpfd; - -# ifdef _WIN32 - if ((tmpfd = open(StringValueCStr(path), O_RDWR)) < 0) { - rb_sys_fail(RSTRING(path)->ptr); - } -# else - if ((tmpfd = open(StringValueCStr(path), 0)) < 0) { - rb_sys_fail(RSTRING(path)->ptr); - } -# endif - if (chsize(tmpfd, pos) < 0) { - close(tmpfd); - rb_sys_fail(RSTRING(path)->ptr); - } - close(tmpfd); - } -# else - rb_notimplement(); -# endif +#define rb_file_s_truncate rb_f_notimplement #endif - return INT2FIX(0); + +#if defined(HAVE_FTRUNCATE) +struct ftruncate_arg { + int fd; + rb_off_t pos; +}; + +static VALUE +nogvl_ftruncate(void *ptr) +{ + struct ftruncate_arg *fa = ptr; + + return (VALUE)ftruncate(fa->fd, fa->pos); } /* * call-seq: - * file.truncate(integer) => 0 - * + * file.truncate(integer) -> 0 + * * Truncates <i>file</i> to at most <i>integer</i> bytes. The file * must be opened for writing. Not available on all platforms. - * + * * f = File.new("out", "w") * f.syswrite("1234567890") #=> 10 * f.truncate(5) #=> 0 @@ -3247,35 +5560,26 @@ rb_file_s_truncate(klass, path, len) */ static VALUE -rb_file_truncate(obj, len) - VALUE obj, len; +rb_file_truncate(VALUE obj, VALUE len) { rb_io_t *fptr; - FILE *f; - off_t pos; + struct ftruncate_arg fa; - rb_secure(2); - pos = NUM2OFFT(len); + fa.pos = NUM2OFFT(len); GetOpenFile(obj, fptr); if (!(fptr->mode & FMODE_WRITABLE)) { - rb_raise(rb_eIOError, "not opened for writing"); - } - f = GetWriteFile(fptr); - fflush(f); - fseeko(f, (off_t)0, SEEK_CUR); -#ifdef HAVE_FTRUNCATE - if (ftruncate(fileno(f), pos) < 0) - rb_sys_fail(fptr->path); -#else -# ifdef HAVE_CHSIZE - if (chsize(fileno(f), pos) < 0) - rb_sys_fail(fptr->path); -# else - rb_notimplement(); -# endif -#endif + rb_raise(rb_eIOError, "not opened for writing"); + } + rb_io_flush_raw(obj, 0); + fa.fd = fptr->fd; + if ((int)rb_io_blocking_region(fptr, nogvl_ftruncate, &fa) < 0) { + rb_sys_fail_path(fptr->pathv); + } return INT2FIX(0); } +#else +#define rb_file_truncate rb_f_notimplement +#endif # ifndef LOCK_SH # define LOCK_SH 1 @@ -3292,476 +5596,468 @@ rb_file_truncate(obj, len) #ifdef __CYGWIN__ #include <winerror.h> -extern unsigned long __attribute__((stdcall)) GetLastError(void); +#endif -static int -cygwin_flock(int fd, int op) +static VALUE +rb_thread_flock(void *data) { +#ifdef __CYGWIN__ int old_errno = errno; - int ret = flock(fd, op); - if (GetLastError() == ERROR_NOT_LOCKED) { - ret = 0; - errno = old_errno; - } - return ret; -} -# define flock(fd, op) cygwin_flock(fd, op) #endif + int *op = data, ret = flock(op[0], op[1]); -static int -rb_thread_flock(fd, op, fptr) - int fd, op; - rb_io_t *fptr; -{ - if (rb_thread_alone() || (op & LOCK_NB)) { - int ret; - TRAP_BEG; - ret = flock(fd, op); - TRAP_END; - return ret; - } - op |= LOCK_NB; - while (flock(fd, op) < 0) { - switch (errno) { - case EAGAIN: - case EACCES: -#if defined(EWOULDBLOCK) && EWOULDBLOCK != EAGAIN - case EWOULDBLOCK: -#endif - rb_thread_polling(); /* busy wait */ - rb_io_check_closed(fptr); - continue; - default: - return -1; - } - } - return 0; -} #ifdef __CYGWIN__ -# undef flock + if (GetLastError() == ERROR_NOT_LOCKED) { + ret = 0; + errno = old_errno; + } #endif -#define flock(fd, op) rb_thread_flock(fd, op, fptr) + 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 <code>File::LOCK_NB</code> 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: * - * File.new("testfile").flock(File::LOCK_UN) #=> 0 - * + * ```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 + * ``` + * */ static VALUE -rb_file_flock(obj, operation) - VALUE obj; - VALUE operation; +rb_file_flock(VALUE obj, VALUE operation) { -#ifndef __CHECKER__ rb_io_t *fptr; - int op; + int op[2], op1; + struct timeval time; - rb_secure(2); - op = NUM2INT(operation); + op[1] = op1 = NUM2INT(operation); GetOpenFile(obj, fptr); + op[0] = fptr->fd; if (fptr->mode & FMODE_WRITABLE) { - fflush(GetWriteFile(fptr)); + rb_io_flush_raw(obj, 0); } - retry: - if (flock(fileno(fptr->f), op) < 0) { - switch (errno) { - case EAGAIN: - case EACCES: + while ((int)rb_io_blocking_region(fptr, rb_thread_flock, op) < 0) { + int e = errno; + switch (e) { + case EAGAIN: + case EACCES: #if defined(EWOULDBLOCK) && EWOULDBLOCK != EAGAIN - case EWOULDBLOCK: + case EWOULDBLOCK: #endif - return Qfalse; - case EINTR: + if (op1 & LOCK_NB) return Qfalse; + + time.tv_sec = 0; + time.tv_usec = 100 * 1000; /* 0.1 sec */ + rb_thread_wait_for(time); + rb_io_check_closed(fptr); + continue; + + case EINTR: #if defined(ERESTART) - case ERESTART: + case ERESTART: #endif - goto retry; - } - rb_sys_fail(fptr->path); + break; + + default: + rb_syserr_fail_path(e, fptr->pathv); + } } -#endif return INT2FIX(0); } -#undef flock static void -test_check(n, argc, argv) - int n, argc; - VALUE *argv; +test_check(int n, int argc, VALUE *argv) { int i; n+=1; - if (n != argc) rb_raise(rb_eArgError, "wrong number of arguments (%d for %d)", argc, n); + rb_check_arity(argc, n, n); for (i=1; i<n; i++) { - switch (TYPE(argv[i])) { - case T_STRING: - default: - SafeStringValue(argv[i]); - break; - case T_FILE: - break; - } + if (!RB_TYPE_P(argv[i], T_FILE)) { + FilePathValue(argv[i]); + } } } #define CHECK(n) test_check((n), argc, argv) /* + * :markup: markdown + * * call-seq: - * test(int_cmd, file1 [, file2] ) => obj - * - * Uses the integer <i>aCmd</i> to perform various tests on - * <i>file1</i> (first table below) or on <i>file1</i> and - * <i>file2</i> (second table). - * - * File tests on a single file: - * - * Test 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 -rb_f_test(argc, argv) - int argc; - VALUE *argv; +rb_f_test(int argc, VALUE *argv, VALUE _) { int cmd; - if (argc == 0) rb_raise(rb_eArgError, "wrong number of arguments"); -#if 0 /* 1.7 behavior? */ - if (argc == 1) { - return RTEST(argv[0]) ? Qtrue : Qfalse; - } -#endif + if (argc == 0) rb_check_arity(argc, 2, 3); cmd = NUM2CHR(argv[0]); - if (cmd == 0) return Qfalse; + if (cmd == 0) { + goto unknown; + } if (strchr("bcdefgGkloOprRsSuwWxXz", cmd)) { - CHECK(1); - switch (cmd) { - case 'b': - return test_b(0, argv[1]); + CHECK(1); + switch (cmd) { + case 'b': + return rb_file_blockdev_p(0, argv[1]); - case 'c': - return test_c(0, argv[1]); + case 'c': + return rb_file_chardev_p(0, argv[1]); - case 'd': - return test_d(0, argv[1]); + case 'd': + return rb_file_directory_p(0, argv[1]); - case 'a': - case 'e': - return test_e(0, argv[1]); + case 'e': + return rb_file_exist_p(0, argv[1]); - case 'f': - return test_f(0, argv[1]); + case 'f': + return rb_file_file_p(0, argv[1]); - case 'g': - return test_sgid(0, argv[1]); + case 'g': + return rb_file_sgid_p(0, argv[1]); - case 'G': - return test_grpowned(0, argv[1]); + case 'G': + return rb_file_grpowned_p(0, argv[1]); - case 'k': - return test_sticky(0, argv[1]); + case 'k': + return rb_file_sticky_p(0, argv[1]); - case 'l': - return test_l(0, argv[1]); + case 'l': + return rb_file_symlink_p(0, argv[1]); - case 'o': - return test_owned(0, argv[1]); + case 'o': + return rb_file_owned_p(0, argv[1]); - case 'O': - return test_rowned(0, argv[1]); + case 'O': + return rb_file_rowned_p(0, argv[1]); - case 'p': - return test_p(0, argv[1]); + case 'p': + return rb_file_pipe_p(0, argv[1]); - case 'r': - return test_r(0, argv[1]); + case 'r': + return rb_file_readable_p(0, argv[1]); - case 'R': - return test_R(0, argv[1]); + case 'R': + return rb_file_readable_real_p(0, argv[1]); - case 's': - return test_s(0, argv[1]); + case 's': + return rb_file_size_p(0, argv[1]); - case 'S': - return test_S(0, argv[1]); + case 'S': + return rb_file_socket_p(0, argv[1]); - case 'u': - return test_suid(0, argv[1]); + case 'u': + return rb_file_suid_p(0, argv[1]); - case 'w': - return test_w(0, argv[1]); + case 'w': + return rb_file_writable_p(0, argv[1]); - case 'W': - return test_W(0, argv[1]); + case 'W': + return rb_file_writable_real_p(0, argv[1]); - case 'x': - return test_x(0, argv[1]); + case 'x': + return rb_file_executable_p(0, argv[1]); - case 'X': - return test_X(0, argv[1]); + case 'X': + return rb_file_executable_real_p(0, argv[1]); - case 'z': - return test_z(0, argv[1]); - } + case 'z': + return rb_file_zero_p(0, argv[1]); + } } if (strchr("MAC", cmd)) { - struct stat st; - - CHECK(1); - if (rb_stat(argv[1], &st) == -1) { - rb_sys_fail(RSTRING(argv[1])->ptr); - } - - switch (cmd) { - case 'A': - return rb_time_new(st.st_atime, 0); - case 'M': - return rb_time_new(st.st_mtime, 0); - case 'C': - return rb_time_new(st.st_ctime, 0); - } + struct stat st; + VALUE fname = argv[1]; + + CHECK(1); + if (rb_stat(fname, &st) == -1) { + int e = errno; + FilePathValue(fname); + rb_syserr_fail_path(e, fname); + } + + switch (cmd) { + case 'A': + return stat_atime(&st); + case 'M': + return stat_mtime(&st); + case 'C': + return stat_ctime(&st); + } } if (cmd == '-') { - CHECK(2); - return test_identical(0, argv[1], argv[2]); + CHECK(2); + return rb_file_identical_p(0, argv[1], argv[2]); } if (strchr("=<>", cmd)) { - struct stat st1, st2; - - CHECK(2); - if (rb_stat(argv[1], &st1) < 0) return Qfalse; - if (rb_stat(argv[2], &st2) < 0) return Qfalse; - - switch (cmd) { - case '=': - if (st1.st_mtime == st2.st_mtime) return Qtrue; - return Qfalse; - - case '>': - if (st1.st_mtime > st2.st_mtime) return Qtrue; - return Qfalse; - - case '<': - if (st1.st_mtime < st2.st_mtime) return Qtrue; - return Qfalse; - } + struct stat st1, st2; + stat_timestamp t1, t2; + + CHECK(2); + if (rb_stat(argv[1], &st1) < 0) return Qfalse; + if (rb_stat(argv[2], &st2) < 0) return Qfalse; + + t1 = stat_mtimespec(&st1); + t2 = stat_mtimespec(&st2); + + switch (cmd) { + case '=': + if (t1.tv_sec == t2.tv_sec && t1.tv_nsec == t2.tv_nsec) return Qtrue; + return Qfalse; + + case '>': + if (t1.tv_sec > t2.tv_sec) return Qtrue; + if (t1.tv_sec == t2.tv_sec && t1.tv_nsec > t2.tv_nsec) return Qtrue; + return Qfalse; + + case '<': + if (t1.tv_sec < t2.tv_sec) return Qtrue; + if (t1.tv_sec == t2.tv_sec && t1.tv_nsec < t2.tv_nsec) return Qtrue; + return Qfalse; + } } + unknown: /* unknown command */ - rb_raise(rb_eArgError, "unknown command ?%c", cmd); - return Qnil; /* not reached */ + if (ISPRINT(cmd)) { + rb_raise(rb_eArgError, "unknown command '%s%c'", cmd == '\'' || cmd == '\\' ? "\\" : "", cmd); + } + else { + rb_raise(rb_eArgError, "unknown command \"\\x%02X\"", cmd); + } + UNREACHABLE_RETURN(Qundef); } - /* * Document-class: File::Stat * - * Objects of class <code>File::Stat</code> encapsulate common status - * information for <code>File</code> objects. The information is - * recorded at the moment the <code>File::Stat</code> object is - * created; changes made to the file after that point will not be - * reflected. <code>File::Stat</code> objects are returned by - * <code>IO#stat</code>, <code>File::stat</code>, - * <code>File#lstat</code>, and <code>File::lstat</code>. Many of these + * 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 <code>Kernel#test</code>. + * meaningful on all systems. See also Kernel#test. */ -static VALUE rb_stat_s_alloc _((VALUE)); static VALUE -rb_stat_s_alloc(klass) - VALUE klass; +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 + * File::Stat.new(file_name) -> stat * * Create a File::Stat object for the given file name (raising an * exception if the file doesn't exist). */ static VALUE -rb_stat_init(obj, fname) - VALUE obj, fname; +rb_stat_init(VALUE obj, VALUE fname) { - struct stat st, *nst; - - SafeStringValue(fname); + rb_io_stat_data st; - if (stat(StringValueCStr(fname), &st) == -1) { - rb_sys_fail(RSTRING(fname)->ptr); + FilePathValue(fname); + fname = rb_str_encode_ospath(fname); + if (STATX(StringValueCStr(fname), &st, STATX_ALL) == -1) { + rb_sys_fail_path(fname); } - if (DATA_PTR(obj)) { - free(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; } /* :nodoc: */ static VALUE -rb_stat_init_copy(copy, orig) - VALUE copy, orig; +rb_stat_init_copy(VALUE copy, VALUE orig) { - struct stat *nst; + if (!OBJ_INIT_COPY(copy, orig)) return copy; - if (copy == orig) return orig; - rb_check_frozen(copy); - /* need better argument type check */ - if (!rb_obj_is_instance_of(orig, rb_obj_class(copy))) { - rb_raise(rb_eTypeError, "wrong argument class"); - } - if (DATA_PTR(copy)) { - free(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; } /* * call-seq: - * stat.ftype => string - * + * stat.ftype -> string + * * Identifies the type of <i>stat</i>. The return string is one of: * ``<code>file</code>'', ``<code>directory</code>'', * ``<code>characterSpecial</code>'', ``<code>blockSpecial</code>'', * ``<code>fifo</code>'', ``<code>link</code>'', * ``<code>socket</code>'', or ``<code>unknown</code>''. - * + * * File.stat("/dev/tty").ftype #=> "characterSpecial" - * + * */ static VALUE -rb_stat_ftype(obj) - VALUE obj; +rb_stat_ftype(VALUE obj) { - return rb_file_ftype(get_stat(obj)); + return rb_file_ftype(get_stat(obj)->ST_(mode)); } /* * call-seq: - * stat.directory? => true or false - * + * stat.directory? -> true or false + * * Returns <code>true</code> if <i>stat</i> is a directory, * <code>false</code> otherwise. - * + * * File.stat("testfile").directory? #=> false * File.stat(".").directory? #=> true */ static VALUE -rb_stat_d(obj) - VALUE obj; +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; } /* * call-seq: - * stat.pipe? => true or false - * + * stat.pipe? -> true or false + * * Returns <code>true</code> if the operating system supports pipes and * <i>stat</i> is a pipe; <code>false</code> otherwise. */ static VALUE -rb_stat_p(obj) - VALUE obj; +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; @@ -3769,49 +6065,46 @@ rb_stat_p(obj) /* * call-seq: - * stat.symlink? => true or false - * + * stat.symlink? -> true or false + * * Returns <code>true</code> if <i>stat</i> is a symbolic link, * <code>false</code> if it isn't or if the operating system doesn't - * support this feature. As <code>File::stat</code> automatically - * follows symbolic links, <code>symlink?</code> will always be - * <code>false</code> for an object returned by - * <code>File::stat</code>. - * + * support this feature. As File::stat automatically follows symbolic + * links, #symlink? will always be <code>false</code> for an object + * returned by File::stat. + * * File.symlink("testfile", "alink") #=> 0 * File.stat("alink").symlink? #=> false * File.lstat("alink").symlink? #=> true - * + * */ static VALUE -rb_stat_l(obj) - VALUE obj; +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; } /* * call-seq: - * stat.socket? => true or false - * + * stat.socket? -> true or false + * * Returns <code>true</code> if <i>stat</i> is a socket, * <code>false</code> if it isn't or if the operating system doesn't * support this feature. - * + * * File.stat("testfile").socket? #=> false - * + * */ static VALUE -rb_stat_S(obj) - VALUE obj; +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; @@ -3819,23 +6112,22 @@ rb_stat_S(obj) /* * call-seq: - * stat.blockdev? => true or false - * + * stat.blockdev? -> true or false + * * Returns <code>true</code> if the file is a block device, * <code>false</code> if it isn't or if the operating system doesn't * support this feature. - * + * * File.stat("testfile").blockdev? #=> false * File.stat("/dev/hda1").blockdev? #=> true - * + * */ static VALUE -rb_stat_b(obj) - VALUE obj; +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; @@ -3843,339 +6135,374 @@ rb_stat_b(obj) /* * call-seq: - * stat.chardev? => true or false - * + * stat.chardev? -> true or false + * * Returns <code>true</code> if the file is a character device, * <code>false</code> if it isn't or if the operating system doesn't * support this feature. - * + * * File.stat("/dev/tty").chardev? #=> true - * + * */ static VALUE -rb_stat_c(obj) - VALUE obj; +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; } /* * call-seq: - * stat.owned? => true or false - * + * stat.owned? -> true or false + * * Returns <code>true</code> if the effective user id of the process is * the same as the owner of <i>stat</i>. - * + * * File.stat("testfile").owned? #=> true * File.stat("/etc/passwd").owned? #=> false - * + * */ static VALUE -rb_stat_owned(obj) - VALUE obj; +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(obj) - VALUE obj; +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; } /* * call-seq: - * stat.grpowned? => true or false - * + * 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 - * + * */ static VALUE -rb_stat_grpowned(obj) - VALUE obj; +rb_stat_grpowned(VALUE obj) { #ifndef _WIN32 - if (group_member(get_stat(obj)->st_gid)) return Qtrue; + if (rb_group_member(get_stat(obj)->ST_(gid))) return Qtrue; #endif return Qfalse; } /* * call-seq: - * stat.readable? => true or false - * + * stat.readable? -> true or false + * * Returns <code>true</code> if <i>stat</i> is readable by the * effective user id of this process. - * + * * File.stat("testfile").readable? #=> true - * + * */ static VALUE -rb_stat_r(obj) - VALUE obj; +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 st->st_mode & S_IRUSR ? Qtrue : Qfalse; + return RBOOL(st->ST_(mode) & S_IRUSR); #endif #ifdef S_IRGRP if (rb_stat_grpowned(obj)) - return st->st_mode & S_IRGRP ? Qtrue : Qfalse; + 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; } - - /* * call-seq: - * stat.readable_real? -> true or false - * + * stat.readable_real? -> true or false + * * Returns <code>true</code> if <i>stat</i> is readable by the real * user id of this process. - * + * * File.stat("testfile").readable_real? #=> true - * + * */ static VALUE -rb_stat_R(obj) - VALUE obj; +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 st->st_mode & S_IRUSR ? Qtrue : Qfalse; + return RBOOL(st->ST_(mode) & S_IRUSR); #endif #ifdef S_IRGRP - if (group_member(get_stat(obj)->st_gid)) - return st->st_mode & S_IRGRP ? Qtrue : Qfalse; + 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; } /* + * call-seq: + * stat.world_readable? -> integer or nil + * + * If <i>stat</i> is readable by others, returns an integer + * representing the file permission bits of <i>stat</i>. Returns + * <code>nil</code> otherwise. The meaning of the bits is platform + * dependent; on Unix systems, see <code>stat(2)</code>. + * + * m = File.stat("/etc/passwd").world_readable? #=> 420 + * sprintf("%o", m) #=> "644" + */ + +static VALUE +rb_stat_wr(VALUE obj) +{ +#ifdef S_IROTH + 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; +} + +/* * call-seq: - * stat.writable? -> true or false - * + * stat.writable? -> true or false + * * Returns <code>true</code> if <i>stat</i> is writable by the * effective user id of this process. - * + * * File.stat("testfile").writable? #=> true - * + * */ static VALUE -rb_stat_w(obj) - VALUE obj; +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 st->st_mode & S_IWUSR ? Qtrue : Qfalse; + return RBOOL(st->ST_(mode) & S_IWUSR); #endif #ifdef S_IWGRP if (rb_stat_grpowned(obj)) - return st->st_mode & S_IWGRP ? Qtrue : Qfalse; + 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; } /* * call-seq: - * stat.writable_real? -> true or false - * + * stat.writable_real? -> true or false + * * Returns <code>true</code> if <i>stat</i> is writable by the real * user id of this process. - * + * * File.stat("testfile").writable_real? #=> true - * + * */ static VALUE -rb_stat_W(obj) - VALUE obj; +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 st->st_mode & S_IWUSR ? Qtrue : Qfalse; + return RBOOL(st->ST_(mode) & S_IWUSR); #endif #ifdef S_IWGRP - if (group_member(get_stat(obj)->st_gid)) - return st->st_mode & S_IWGRP ? Qtrue : Qfalse; + 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; } /* + * call-seq: + * stat.world_writable? -> integer or nil + * + * If <i>stat</i> is writable by others, returns an integer + * representing the file permission bits of <i>stat</i>. Returns + * <code>nil</code> otherwise. The meaning of the bits is platform + * dependent; on Unix systems, see <code>stat(2)</code>. + * + * m = File.stat("/tmp").world_writable? #=> 511 + * sprintf("%o", m) #=> "777" + */ + +static VALUE +rb_stat_ww(VALUE obj) +{ +#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; +} + +/* * call-seq: - * stat.executable? => true or false - * + * stat.executable? -> true or false + * * Returns <code>true</code> if <i>stat</i> is executable or if the * operating system doesn't distinguish executable files from * nonexecutable files. The tests are made using the effective owner of * the process. - * + * * File.stat("testfile").executable? #=> false - * + * */ static VALUE -rb_stat_x(obj) - VALUE obj; +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 st->st_mode & S_IXUGO ? Qtrue : Qfalse; + return RBOOL(st->ST_(mode) & S_IXUGO); } #endif #ifdef S_IXUSR if (rb_stat_owned(obj)) - return st->st_mode & S_IXUSR ? Qtrue : Qfalse; + return RBOOL(st->ST_(mode) & S_IXUSR); #endif #ifdef S_IXGRP if (rb_stat_grpowned(obj)) - return st->st_mode & S_IXGRP ? Qtrue : Qfalse; + 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; } /* * call-seq: - * stat.executable_real? => true or false - * + * stat.executable_real? -> true or false + * * Same as <code>executable?</code>, but tests using the real owner of * the process. */ - static VALUE -rb_stat_X(obj) - VALUE obj; +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 st->st_mode & S_IXUGO ? Qtrue : Qfalse; + return RBOOL(st->ST_(mode) & S_IXUGO); } #endif #ifdef S_IXUSR if (rb_stat_rowned(obj)) - return st->st_mode & S_IXUSR ? Qtrue : Qfalse; + return RBOOL(st->ST_(mode) & S_IXUSR); #endif #ifdef S_IXGRP - if (group_member(get_stat(obj)->st_gid)) - return st->st_mode & S_IXGRP ? Qtrue : Qfalse; + 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; } /* * call-seq: - * stat.file? => true or false - * + * stat.file? -> true or false + * * Returns <code>true</code> if <i>stat</i> is a regular file (not * a device file, pipe, socket, etc.). - * + * * File.stat("testfile").file? #=> true - * + * */ static VALUE -rb_stat_f(obj) - VALUE obj; +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; } /* * call-seq: - * stat.zero? => true or false - * + * stat.zero? -> true or false + * * Returns <code>true</code> if <i>stat</i> is a zero-length file; * <code>false</code> otherwise. - * + * * File.stat("testfile").zero? #=> false - * + * */ static VALUE -rb_stat_z(obj) - VALUE obj; +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; } - /* * call-seq: - * state.size => integer - * - * Returns the size of <i>stat</i> in bytes. - * - * File.stat("testfile").size #=> 66 - * + * stat.size? -> Integer or nil + * + * Returns +nil+ if <i>stat</i> is a zero-length file, the size of + * the file otherwise. + * + * File.stat("testfile").size? #=> 66 + * File.stat(File::NULL).size? #=> nil + * */ static VALUE -rb_stat_s(obj) - VALUE obj; +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); @@ -4183,82 +6510,129 @@ rb_stat_s(obj) /* * call-seq: - * stat.setuid? => true or false - * + * stat.setuid? -> true or false + * * Returns <code>true</code> if <i>stat</i> has the set-user-id * permission bit set, <code>false</code> if it doesn't or if the * operating system doesn't support this feature. - * + * * File.stat("/bin/su").setuid? #=> true */ static VALUE -rb_stat_suid(obj) - VALUE obj; +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; } /* * call-seq: - * stat.setgid? => true or false - * + * stat.setgid? -> true or false + * * Returns <code>true</code> if <i>stat</i> has the set-group-id * permission bit set, <code>false</code> if it doesn't or if the * operating system doesn't support this feature. - * + * * File.stat("/usr/sbin/lpc").setgid? #=> true - * + * */ static VALUE -rb_stat_sgid(obj) - VALUE obj; +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; } /* * call-seq: - * stat.sticky? => true or false - * + * stat.sticky? -> true or false + * * Returns <code>true</code> if <i>stat</i> has its sticky bit set, * <code>false</code> if it doesn't or if the operating system doesn't * support this feature. - * + * * File.stat("testfile").sticky? #=> false - * + * */ static VALUE -rb_stat_sticky(obj) - VALUE obj; +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; } -VALUE rb_mFConst; +#if !defined HAVE_MKFIFO && defined HAVE_MKNOD && defined S_IFIFO +#define mkfifo(path, mode) mknod(path, (mode)&~S_IFMT|S_IFIFO, 0) +#define HAVE_MKFIFO +#endif + +#ifdef HAVE_MKFIFO +struct mkfifo_arg { + const char *path; + mode_t mode; +}; + +static void * +nogvl_mkfifo(void *ptr) +{ + struct mkfifo_arg *ma = ptr; + + return (void *)(VALUE)mkfifo(ma->path, ma->mode); +} + +/* + * call-seq: + * File.mkfifo(file_name, mode=0666) => 0 + * + * Creates a FIFO special file with name _file_name_. _mode_ + * specifies the FIFO's permissions. It is modified by the process's + * umask in the usual way: the permissions of the created file are + * (mode & ~umask). + */ + +static VALUE +rb_file_s_mkfifo(int argc, VALUE *argv, VALUE _) +{ + VALUE path; + struct mkfifo_arg ma; + + ma.mode = 0666; + rb_check_arity(argc, 1, 2); + if (argc > 1) { + ma.mode = NUM2MODET(argv[1]); + } + path = argv[0]; + FilePathValue(path); + path = rb_str_encode_ospath(path); + ma.path = RSTRING_PTR(path); + if (IO_WITHOUT_GVL(nogvl_mkfifo, &ma)) { + rb_sys_fail_path(path); + } + return INT2FIX(0); +} +#else +#define rb_file_s_mkfifo rb_f_notimplement +#endif + +static VALUE rb_mFConst; void -rb_file_const(name, value) - const char *name; - VALUE value; +rb_file_const(const char *name, VALUE value) { rb_define_const(rb_mFConst, name, value); } -static int -is_absolute_path(path) - const char *path; +int +rb_is_absolute_path(const char *path) { #ifdef DOSISH_DRIVE_LETTER if (has_drive_letter(path) && isdirsep(path[2])) return 1; @@ -4272,333 +6646,1039 @@ is_absolute_path(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(fpath, execpath) - VALUE fpath; - int execpath; +int +ruby_is_fd_loadable(int fd) { +#ifdef _WIN32 + return 1; +#else struct stat st; - char *p0 = StringValueCStr(fpath); - char *p = 0, *s; - if (!is_absolute_path(p0)) { - char *buf = my_getcwd(); - VALUE newpath; + if (fstat(fd, &st) < 0) + return 0; - newpath = rb_str_new2(buf); - free(buf); + if (S_ISREG(st.st_mode)) + return 1; - rb_str_cat2(newpath, "/"); - rb_str_cat2(newpath, p0); - p0 = RSTRING(fpath = newpath)->ptr; - } - 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 && execpath && (st.st_mode & S_ISVTX)) -#endif - ) { - rb_warn("Insecure world writable dir %s in %sPATH, mode 0%o", - p0, (execpath ? "" : "LOAD_"), st.st_mode); - if (p) *p = '/'; - return 0; - } - s = strrdirsep(p0); - if (p) *p = '/'; - if (!s || s == p0) return 1; - p = s; - *p = '\0'; - } -} -#endif + if (S_ISFIFO(st.st_mode) || S_ISCHR(st.st_mode)) + return -1; -static int -fpath_check(path) - char *path; -{ -#if ENABLE_PATH_CHECK - return path_check_0(rb_str_new2(path), Qfalse); -#else - return 1; + if (S_ISDIR(st.st_mode)) + errno = EISDIR; + else + errno = ENXIO; + + return 0; #endif } +#ifndef _WIN32 int -rb_path_check(path) - char *path; +rb_file_load_ok(const char *path) { -#if ENABLE_PATH_CHECK - 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), Qtrue)) { - return 0; /* not safe */ - } - p0 = p + 1; - if (p0 > pend) break; - p = strchr(p0, sep); - if (!p) p = pend; + int ret = 1; + /* + open(2) may block if path is FIFO and it's empty. Let's use O_NONBLOCK. + FIXME: Why O_NDELAY is checked? + */ + int mode = (O_RDONLY | +#if defined O_NONBLOCK + O_NONBLOCK | +#elif defined O_NDELAY + O_NDELAY | +#endif + 0); + int fd = rb_cloexec_open(path, mode, 0); + if (fd < 0) { + if (!rb_gc_for_fd(errno)) return 0; + fd = rb_cloexec_open(path, mode, 0); + if (fd < 0) return 0; } -#endif - return 1; + rb_update_max_fd(fd); + ret = ruby_is_fd_loadable(fd); + (void)close(fd); + return ret; } +#endif -#if defined(__MACOS__) || defined(riscos) static int -is_macos_native_path(path) - const char *path; +is_explicit_relative(const char *path) { - if (strchr(path, ':')) return 1; - return 0; + if (*path++ != '.') return 0; + if (*path == '.') path++; + return isdirsep(*path); } -#endif -static int -file_load_ok(file) - char *file; +static VALUE +copy_path_class(VALUE path, VALUE orig) { - FILE *f; - - if (!file) return 0; - f = fopen(file, "r"); - if (f == NULL) return 0; - fclose(f); - return 1; + int encidx = rb_enc_get_index(orig); + if (encidx == ENCINDEX_ASCII_8BIT || encidx == ENCINDEX_US_ASCII) + encidx = rb_filesystem_encindex(); + rb_enc_associate_index(path, encidx); + str_shrink(path); + RBASIC_SET_CLASS(path, rb_obj_class(orig)); + OBJ_FREEZE(path); + return path; } -extern VALUE rb_load_path; - int -rb_find_file_ext(filep, ext) - VALUE *filep; - const char * const *ext; +rb_find_file_ext(VALUE *filep, const char *const *ext) { - char *path, *found; - char *f = RSTRING(*filep)->ptr; - VALUE fname; - long i, j; + const char *f = StringValueCStr(*filep); + VALUE fname = *filep, load_path, tmp; + long i, j, fnlen; + int expanded = 0; + + if (!ext[0]) return 0; if (f[0] == '~') { - fname = rb_file_expand_path(*filep, Qnil); - if (rb_safe_level() >= 2 && OBJ_TAINTED(fname)) { - rb_raise(rb_eSecurityError, "loading from unsafe file %s", f); - } - OBJ_FREEZE(fname); - f = StringValueCStr(fname); - *filep = fname; - } - - if (is_absolute_path(f)) { - for (i=0; ext[i]; i++) { - fname = rb_str_dup(*filep); - rb_str_cat2(fname, ext[i]); - OBJ_FREEZE(fname); - if (file_load_ok(StringValueCStr(fname))) { - *filep = fname; - return i+1; - } - } - return 0; - } - - if (!rb_load_path) return 0; - - Check_Type(rb_load_path, T_ARRAY); - for (i=0;i<RARRAY(rb_load_path)->len;i++) { - VALUE str = RARRAY(rb_load_path)->ptr[i]; - - SafeStringValue(str); - if (RSTRING(str)->len == 0) continue; - path = RSTRING(str)->ptr; - for (j=0; ext[j]; j++) { - fname = rb_str_dup(*filep); - rb_str_cat2(fname, ext[j]); - OBJ_FREEZE(fname); - found = dln_find_file(StringValueCStr(fname), path); - if (found && file_load_ok(found)) { - *filep = fname; - return j+1; - } - } + fname = file_expand_path_1(fname); + f = RSTRING_PTR(fname); + *filep = fname; + expanded = 1; + } + + if (expanded || rb_is_absolute_path(f) || is_explicit_relative(f)) { + if (!expanded) fname = file_expand_path_1(fname); + fnlen = RSTRING_LEN(fname); + for (i=0; ext[i]; i++) { + rb_str_cat2(fname, ext[i]); + if (rb_file_load_ok(RSTRING_PTR(fname))) { + *filep = copy_path_class(fname, *filep); + return (int)(i+1); + } + rb_str_set_len(fname, fnlen); + } + return 0; } + + RB_GC_GUARD(load_path) = rb_get_expanded_load_path(); + if (!load_path) return 0; + + fname = rb_str_dup(*filep); + RBASIC_CLEAR_CLASS(fname); + fnlen = RSTRING_LEN(fname); + tmp = rb_str_tmp_new(MAXPATHLEN + 2); + rb_enc_associate_index(tmp, rb_usascii_encindex()); + for (j=0; ext[j]; j++) { + rb_str_cat2(fname, ext[j]); + for (i = 0; i < RARRAY_LEN(load_path); i++) { + VALUE str = RARRAY_AREF(load_path, i); + + RB_GC_GUARD(str) = rb_get_path(str); + if (RSTRING_LEN(str) == 0) continue; + rb_file_expand_path_internal(fname, str, 0, 0, tmp); + if (rb_file_load_ok(RSTRING_PTR(tmp))) { + *filep = copy_path_class(tmp, *filep); + return (int)(j+1); + } + } + rb_str_set_len(fname, fnlen); + } + rb_str_resize(tmp, 0); + RB_GC_GUARD(load_path); return 0; } VALUE -rb_find_file(path) - VALUE path; +rb_find_file(VALUE path) { - VALUE tmp; - char *f = StringValueCStr(path); - char *lpath; + VALUE tmp, load_path; + const char *f = StringValueCStr(path); + int expanded = 0; if (f[0] == '~') { - path = rb_file_expand_path(path, Qnil); - if (rb_safe_level() >= 1 && OBJ_TAINTED(path)) { - rb_raise(rb_eSecurityError, "loading from unsafe path %s", f); - } - OBJ_FREEZE(path); - f = StringValueCStr(path); - } - -#if defined(__MACOS__) || defined(riscos) - if (is_macos_native_path(f)) { - if (rb_safe_level() >= 1 && !fpath_check(f)) { - rb_raise(rb_eSecurityError, "loading from unsafe file %s", f); - } - if (file_load_ok(f)) return path; - } -#endif - - if (is_absolute_path(f)) { - if (rb_safe_level() >= 1 && !fpath_check(f)) { - rb_raise(rb_eSecurityError, "loading from unsafe file %s", f); - } - if (file_load_ok(f)) return path; - } - - if (rb_safe_level() >= 4) { - rb_raise(rb_eSecurityError, "loading from non-absolute path %s", f); - } - - if (rb_load_path) { - long i; - - Check_Type(rb_load_path, T_ARRAY); - tmp = rb_ary_new(); - for (i=0;i<RARRAY(rb_load_path)->len;i++) { - VALUE str = RARRAY(rb_load_path)->ptr[i]; - SafeStringValue(str); - if (RSTRING(str)->len > 0) { - rb_ary_push(tmp, str); - } - } - tmp = rb_ary_join(tmp, rb_str_new2(PATH_SEP)); - if (RSTRING(tmp)->len == 0) { - lpath = 0; - } - else { - lpath = RSTRING(tmp)->ptr; - } - } - else { - lpath = 0; + tmp = file_expand_path_1(path); + path = copy_path_class(tmp, path); + f = RSTRING_PTR(path); + expanded = 1; } - if (!lpath) { - return 0; /* no path, no load */ + if (expanded || rb_is_absolute_path(f) || is_explicit_relative(f)) { + if (!rb_file_load_ok(f)) return 0; + if (!expanded) + path = copy_path_class(file_expand_path_1(path), path); + return path; } - if (!(f = dln_find_file(f, lpath))) { - return 0; - } - if (rb_safe_level() >= 1 && !fpath_check(f)) { - rb_raise(rb_eSecurityError, "loading from unsafe file %s", f); + + RB_GC_GUARD(load_path) = rb_get_expanded_load_path(); + if (load_path) { + long i; + + tmp = rb_str_tmp_new(MAXPATHLEN + 2); + rb_enc_associate_index(tmp, rb_usascii_encindex()); + for (i = 0; i < RARRAY_LEN(load_path); i++) { + VALUE str = RARRAY_AREF(load_path, i); + RB_GC_GUARD(str) = rb_get_path(str); + if (RSTRING_LEN(str) > 0) { + rb_file_expand_path_internal(path, str, 0, 0, tmp); + f = RSTRING_PTR(tmp); + if (rb_file_load_ok(f)) goto found; + } + } + rb_str_resize(tmp, 0); + return 0; } - if (file_load_ok(f)) { - tmp = rb_str_new2(f); - OBJ_FREEZE(tmp); - return tmp; + else { + return 0; /* no path, no load */ } - return 0; -} -static void -define_filetest_function(name, func, argc) - const char *name; - VALUE (*func)(); - int argc; -{ - rb_define_module_function(rb_mFileTest, name, func, argc); - rb_define_singleton_method(rb_cFile, name, func, argc); + found: + return copy_path_class(tmp, path); } +#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 + "NUL" +#elif defined AMIGA || defined __amigaos__ + "NIL" +#elif defined __VMS + "NL:" +#else + "/dev/null" +#endif + ; /* - * A <code>File</code> is an abstraction of any file object accessible - * by the program and is closely associated with class <code>IO</code> - * <code>File</code> includes the methods of module - * <code>FileTest</code> as class methods, allowing you to write (for - * example) <code>File.exist?("foo")</code>. - * - * In the description of File methods, - * <em>permission bits</em> are a platform-specific - * set of bits that indicate permissions of a file. On Unix-based - * systems, permissions are viewed as a set of three octets, for the - * owner, the group, and the rest of the world. For each of these - * entities, permissions may be set to read, write, or execute the - * file: - * - * The permission bits <code>0644</code> (in octal) would thus be - * interpreted as read/write for owner, and read-only for group and - * other. Higher-order bits may also be used to indicate the type of - * file (plain, directory, pipe, socket, and so on) and various other - * special features. If the permissions are for a directory, the - * meaning of the execute bit changes; when set the directory can be - * searched. - * - * On non-Posix operating systems, there may be only the ability to - * make a file read-only or read-write. In this case, the remaining - * permission bits will be synthesized to resemble typical values. For - * instance, on Windows NT the default permission bits are - * <code>0644</code>, which means read/write for owner, read-only for - * all others. The only change that can be made is to make the file + * A \File object is a representation of a file in the underlying platform. + * + * 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@Read-2FWrite+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 + * the permissions of an actual file in the underlying platform. + * + * Note that file permissions are quite different from the _mode_ + * of a file stream (\File object). + * + * In a \File object, the permissions are available thus, + * where method +mode+, despite its name, returns permissions: + * + * f = File.new('t.txt') + * f.lstat.mode.to_s(8) # => "100644" + * + * On a Unix-based operating system, + * the three low-order octal digits represent the permissions + * for owner (6), group (4), and world (4). + * The triplet of bits in each octal digit represent, respectively, + * read, write, and execute permissions. + * + * Permissions <tt>0644</tt> thus represent read-write access for owner + * and read-only access for group and world. + * See man pages {open(2)}[https://www.unix.com/man-page/bsd/2/open] + * and {chmod(2)}[https://www.unix.com/man-page/bsd/2/chmod]. + * + * For a directory, the meaning of the execute bit changes: + * when set, the directory can be searched. + * + * Higher-order bits in permissions may indicate the type of file + * (plain, directory, pipe, socket, etc.) and various other special features. + * + * On non-Posix operating systems, permissions may include only read-only or read-write, + * in which case, the remaining permission will resemble typical values. + * On Windows, for instance, the default permissions are <code>0644</code>; + * The only change that can be made is to make the file * read-only, which is reported as <code>0444</code>. + * + * For a method that actually creates a file in the underlying platform + * (as opposed to merely creating a \File object), + * permissions may be specified: + * + * File.new('t.tmp', File::CREAT, 0644) + * File.new('t.tmp', File::CREAT, 0444) + * + * Permissions may also be changed: + * + * f = File.new('t.tmp', File::CREAT, 0444) + * f.chmod(0644) + * f.chmod(0444) + * + * == \File \Constants + * + * 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>. + * + * == What's Here + * + * First, what's elsewhere. Class \File: + * + * - Inherits from {class IO}[rdoc-ref:IO@What-27s+Here], + * in particular, methods for creating, reading, and writing files + * - Includes module FileTest, + * which provides dozens of additional methods. + * + * Here, class \File provides methods that are useful for: + * + * - {Creating}[rdoc-ref:File@Creating] + * - {Querying}[rdoc-ref:File@Querying] + * - {Settings}[rdoc-ref:File@Settings] + * - {Other}[rdoc-ref:File@Other] + * + * === Creating + * + * - ::new: Opens the file at the given path; returns the file. + * - ::open: Same as ::new, but when given a block will yield the file to the block, + * and close the file upon exiting the block. + * - ::link: Creates a new name for an existing file using a hard link. + * - ::mkfifo: Returns the FIFO file created at the given path. + * - ::symlink: Creates a symbolic link for the given file path. + * + * === Querying + * + * _Paths_ + * + * - ::absolute_path: Returns the absolute file path for the given path. + * - ::absolute_path?: Returns whether the given path is the absolute file path. + * - ::basename: Returns the last component of the given file path. + * - ::dirname: Returns all but the last component of the given file path. + * - ::expand_path: Returns the absolute file path for the given path, + * expanding <tt>~</tt> for a home directory. + * - ::extname: Returns the file extension for the given file path. + * - ::fnmatch? (aliased as ::fnmatch): Returns whether the given file path + * matches the given pattern. + * - ::join: Joins path components into a single path string. + * - ::path: Returns the string representation of the given path. + * - ::readlink: Returns the path to the file at the given symbolic link. + * - ::realdirpath: Returns the real path for the given file path, + * where the last component need not exist. + * - ::realpath: Returns the real path for the given file path, + * where all components must exist. + * - ::split: Returns an array of two strings: the directory name and basename + * of the file at the given path. + * - #path (aliased as #to_path): Returns the string representation of the given path. + * + * _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 + * 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 + * 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 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 + * by the real user and group of the current process. + * - ::exist?: Returns whether the file at the given path exists. + * - ::file?: Returns whether the file at the given path is a regular file. + * - ::ftype: Returns a string giving the type of the file at the given path. + * - ::grpowned?: Returns whether the effective group of the current process + * owns the file at the given path. + * - ::identical?: Returns whether the files at two given paths are identical. + * - ::lstat: Returns the File::Stat object for the last symbolic link + * in the given path. + * - ::owned?: Returns whether the effective user of the current process + * owns the file at the given path. + * - ::pipe?: Returns whether the file at the given path is a pipe. + * - ::readable?: Returns whether the file at the given path is readable + * by the effective user and group of the current process. + * - ::readable_real?: Returns whether the file at the given path is readable + * by the real user and group of the current process. + * - ::setgid?: Returns whether the setgid bit is set for the file at the given path. + * - ::setuid?: Returns whether the setuid bit is set for the file at the given path. + * - ::socket?: Returns whether the file at the given path is a socket. + * - ::stat: Returns the File::Stat object for the file at the given path. + * - ::sticky?: Returns whether the file at the given path has its sticky bit set. + * - ::symlink?: Returns whether the file at the given path is a symbolic link. + * - ::umask: Returns the umask value for the current process. + * - ::world_readable?: Returns whether the file at the given path is readable + * by others. + * - ::world_writable?: Returns whether the file at the given path is writable + * by others. + * - ::writable?: Returns whether the file at the given path is writable + * by the effective user and group of the current process. + * - ::writable_real?: Returns whether the file at the given path is writable + * by the real user and group of the current process. + * - #lstat: Returns the File::Stat object for the last symbolic link + * in the path for +self+. + * + * _Contents_ + * + * - ::empty? (aliased as ::zero?): Returns whether the file at the given path + * exists and is empty. + * - ::size: Returns the size (bytes) of the file at the given path. + * - ::size?: Returns +nil+ if there is no file at the given path, + * or if that file is empty; otherwise returns the file size (bytes). + * - #size: Returns the size (bytes) of +self+. + * + * === Settings + * + * - ::chmod: Changes permissions of the file at the given path. + * - ::chown: Change ownership of the file at the given path. + * - ::lchmod: Changes permissions of the last symbolic link in the given path. + * - ::lchown: Change ownership of the last symbolic in the given path. + * - ::lutime: For each given file path, sets the access time and modification time + * of the last symbolic link in the path. + * - ::rename: Moves the file at one given path to another given path. + * - ::utime: Sets the access time and modification time of each file + * at the given paths. + * - #flock: Locks or unlocks +self+. + * + * === Other + * + * - ::truncate: Truncates the file at the given file path to the given size. + * - ::unlink (aliased as ::delete): Deletes the file for each given file path. + * - #truncate: Truncates +self+ to the given size. + * */ void -Init_File() +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"); rb_cFile = rb_define_class("File", rb_cIO); - define_filetest_function("directory?", test_d, 1); - define_filetest_function("exist?", test_e, 1); - define_filetest_function("exists?", test_e, 1); /* temporary */ - define_filetest_function("readable?", test_r, 1); - define_filetest_function("readable_real?", test_R, 1); - define_filetest_function("writable?", test_w, 1); - define_filetest_function("writable_real?", test_W, 1); - define_filetest_function("executable?", test_x, 1); - define_filetest_function("executable_real?", test_X, 1); - define_filetest_function("file?", test_f, 1); - define_filetest_function("zero?", test_z, 1); - define_filetest_function("size?", test_s, 1); + define_filetest_function("directory?", rb_file_directory_p, 1); + define_filetest_function("exist?", rb_file_exist_p, 1); + define_filetest_function("readable?", rb_file_readable_p, 1); + define_filetest_function("readable_real?", rb_file_readable_real_p, 1); + define_filetest_function("world_readable?", rb_file_world_readable_p, 1); + define_filetest_function("writable?", rb_file_writable_p, 1); + define_filetest_function("writable_real?", rb_file_writable_real_p, 1); + define_filetest_function("world_writable?", rb_file_world_writable_p, 1); + define_filetest_function("executable?", rb_file_executable_p, 1); + define_filetest_function("executable_real?", rb_file_executable_real_p, 1); + define_filetest_function("file?", rb_file_file_p, 1); + define_filetest_function("zero?", rb_file_zero_p, 1); + define_filetest_function("empty?", rb_file_zero_p, 1); + define_filetest_function("size?", rb_file_size_p, 1); define_filetest_function("size", rb_file_s_size, 1); - define_filetest_function("owned?", test_owned, 1); - define_filetest_function("grpowned?", test_grpowned, 1); + define_filetest_function("owned?", rb_file_owned_p, 1); + define_filetest_function("grpowned?", rb_file_grpowned_p, 1); - define_filetest_function("pipe?", test_p, 1); - define_filetest_function("symlink?", test_l, 1); - define_filetest_function("socket?", test_S, 1); + define_filetest_function("pipe?", rb_file_pipe_p, 1); + define_filetest_function("symlink?", rb_file_symlink_p, 1); + define_filetest_function("socket?", rb_file_socket_p, 1); - define_filetest_function("blockdev?", test_b, 1); - define_filetest_function("chardev?", test_c, 1); + define_filetest_function("blockdev?", rb_file_blockdev_p, 1); + define_filetest_function("chardev?", rb_file_chardev_p, 1); - define_filetest_function("setuid?", test_suid, 1); - define_filetest_function("setgid?", test_sgid, 1); - define_filetest_function("sticky?", test_sticky, 1); + define_filetest_function("setuid?", rb_file_suid_p, 1); + define_filetest_function("setgid?", rb_file_sgid_p, 1); + define_filetest_function("sticky?", rb_file_sticky_p, 1); - define_filetest_function("identical?", test_identical, 2); + define_filetest_function("identical?", rb_file_identical_p, 2); rb_define_singleton_method(rb_cFile, "stat", rb_file_s_stat, 1); rb_define_singleton_method(rb_cFile, "lstat", rb_file_s_lstat, 1); @@ -4607,39 +7687,51 @@ Init_File() rb_define_singleton_method(rb_cFile, "atime", rb_file_s_atime, 1); rb_define_singleton_method(rb_cFile, "mtime", rb_file_s_mtime, 1); rb_define_singleton_method(rb_cFile, "ctime", rb_file_s_ctime, 1); + rb_define_singleton_method(rb_cFile, "birthtime", rb_file_s_birthtime, 1); rb_define_singleton_method(rb_cFile, "utime", rb_file_s_utime, -1); rb_define_singleton_method(rb_cFile, "chmod", rb_file_s_chmod, -1); rb_define_singleton_method(rb_cFile, "chown", rb_file_s_chown, -1); rb_define_singleton_method(rb_cFile, "lchmod", rb_file_s_lchmod, -1); rb_define_singleton_method(rb_cFile, "lchown", rb_file_s_lchown, -1); + rb_define_singleton_method(rb_cFile, "lutime", rb_file_s_lutime, -1); rb_define_singleton_method(rb_cFile, "link", rb_file_s_link, 2); rb_define_singleton_method(rb_cFile, "symlink", rb_file_s_symlink, 2); rb_define_singleton_method(rb_cFile, "readlink", rb_file_s_readlink, 1); - rb_define_singleton_method(rb_cFile, "unlink", rb_file_s_unlink, -2); - rb_define_singleton_method(rb_cFile, "delete", rb_file_s_unlink, -2); + rb_define_singleton_method(rb_cFile, "unlink", rb_file_s_unlink, -1); + rb_define_singleton_method(rb_cFile, "delete", rb_file_s_unlink, -1); rb_define_singleton_method(rb_cFile, "rename", rb_file_s_rename, 2); rb_define_singleton_method(rb_cFile, "umask", rb_file_s_umask, -1); rb_define_singleton_method(rb_cFile, "truncate", rb_file_s_truncate, 2); - rb_define_singleton_method(rb_cFile, "expand_path", rb_file_s_expand_path, -1); + rb_define_singleton_method(rb_cFile, "mkfifo", rb_file_s_mkfifo, -1); + rb_define_singleton_method(rb_cFile, "expand_path", s_expand_path, -1); + rb_define_singleton_method(rb_cFile, "absolute_path", s_absolute_path, -1); + rb_define_singleton_method(rb_cFile, "absolute_path?", s_absolute_path_p, 1); + rb_define_singleton_method(rb_cFile, "realpath", rb_file_s_realpath, -1); + rb_define_singleton_method(rb_cFile, "realdirpath", rb_file_s_realdirpath, -1); rb_define_singleton_method(rb_cFile, "basename", rb_file_s_basename, -1); - rb_define_singleton_method(rb_cFile, "dirname", rb_file_s_dirname, 1); + rb_define_singleton_method(rb_cFile, "dirname", rb_file_s_dirname, -1); rb_define_singleton_method(rb_cFile, "extname", rb_file_s_extname, 1); + rb_define_singleton_method(rb_cFile, "path", rb_file_s_path, 1); - separator = rb_obj_freeze(rb_str_new2("/")); + separator = rb_fstring_lit("/"); + /* separates directory parts in path */ rb_define_const(rb_cFile, "Separator", separator); + /* 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 - rb_define_const(rb_cFile, "ALT_SEPARATOR", rb_obj_freeze(rb_str_new2("\\"))); + /* platform specific alternative separator */ + rb_define_const(rb_cFile, "ALT_SEPARATOR", rb_obj_freeze(rb_usascii_str_new2(file_alt_separator))); #else rb_define_const(rb_cFile, "ALT_SEPARATOR", Qnil); #endif - rb_define_const(rb_cFile, "PATH_SEPARATOR", rb_obj_freeze(rb_str_new2(PATH_SEP))); + /* path list separator */ + rb_define_const(rb_cFile, "PATH_SEPARATOR", rb_fstring_cstr(PATH_SEP)); rb_define_method(rb_cIO, "stat", rb_io_stat, 0); /* this is IO's method */ rb_define_method(rb_cFile, "lstat", rb_file_lstat, 0); @@ -4647,6 +7739,8 @@ Init_File() rb_define_method(rb_cFile, "atime", rb_file_atime, 0); rb_define_method(rb_cFile, "mtime", rb_file_mtime, 0); rb_define_method(rb_cFile, "ctime", rb_file_ctime, 0); + rb_define_method(rb_cFile, "birthtime", rb_file_birthtime, 0); + rb_define_method(rb_cFile, "size", file_size, 0); rb_define_method(rb_cFile, "chmod", rb_file_chmod, 1); rb_define_method(rb_cFile, "chown", rb_file_chown, 2); @@ -4654,14 +7748,405 @@ Init_File() rb_define_method(rb_cFile, "flock", rb_file_flock, 1); + /* + * Document-module: File::Constants + * + * 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. + * + * 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>. + * + */ rb_mFConst = rb_define_module_under(rb_cFile, "Constants"); rb_include_module(rb_cIO, rb_mFConst); - rb_file_const("LOCK_SH", INT2FIX(LOCK_SH)); - rb_file_const("LOCK_EX", INT2FIX(LOCK_EX)); - rb_file_const("LOCK_UN", INT2FIX(LOCK_UN)); - rb_file_const("LOCK_NB", INT2FIX(LOCK_NB)); + /* {File::RDONLY}[rdoc-ref:File::Constants@File-3A-3ARDONLY] */ + rb_define_const(rb_mFConst, "RDONLY", INT2FIX(O_RDONLY)); + /* {File::WRONLY}[rdoc-ref:File::Constants@File-3A-3AWRONLY] */ + rb_define_const(rb_mFConst, "WRONLY", INT2FIX(O_WRONLY)); + /* {File::RDWR}[rdoc-ref:File::Constants@File-3A-3ARDWR] */ + rb_define_const(rb_mFConst, "RDWR", INT2FIX(O_RDWR)); + /* {File::APPEND}[rdoc-ref:File::Constants@File-3A-3AAPPEND] */ + rb_define_const(rb_mFConst, "APPEND", INT2FIX(O_APPEND)); + /* {File::CREAT}[rdoc-ref:File::Constants@File-3A-3ACREAT] */ + rb_define_const(rb_mFConst, "CREAT", INT2FIX(O_CREAT)); + /* {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 + /* {File::NONBLOCK}[rdoc-ref:File::Constants@File-3A-3ANONBLOCK] */ + rb_define_const(rb_mFConst, "NONBLOCK", INT2FIX(O_NONBLOCK)); +#endif + /* {File::TRUNC}[rdoc-ref:File::Constants@File-3A-3ATRUNC] */ + rb_define_const(rb_mFConst, "TRUNC", INT2FIX(O_TRUNC)); +#ifdef O_NOCTTY + /* {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 + /* {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 + /* {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 + /* {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 + /* {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 + /* {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 + /* {File::NOFOLLOW}[rdoc-ref:File::Constants@File-3A-3ANOFOLLOW] */ + rb_define_const(rb_mFConst, "NOFOLLOW", INT2FIX(O_NOFOLLOW)); /* FreeBSD, Linux */ +#endif +#ifdef O_NOATIME + /* {File::NOATIME}[rdoc-ref:File::Constants@File-3A-3ANOATIME] */ + rb_define_const(rb_mFConst, "NOATIME", INT2FIX(O_NOATIME)); /* Linux */ +#endif +#ifdef O_DIRECT + /* {File::DIRECT}[rdoc-ref:File::Constants@File-3A-3ADIRECT] */ + rb_define_const(rb_mFConst, "DIRECT", INT2FIX(O_DIRECT)); +#endif +#ifdef O_TMPFILE + /* {File::TMPFILE}[rdoc-ref:File::Constants@File-3A-3ATMPFILE] */ + rb_define_const(rb_mFConst, "TMPFILE", INT2FIX(O_TMPFILE)); +#endif + + /* {File::LOCK_SH}[rdoc-ref:File::Constants@File-3A-3ALOCK_SH] */ + rb_define_const(rb_mFConst, "LOCK_SH", INT2FIX(LOCK_SH)); + /* {File::LOCK_EX}[rdoc-ref:File::Constants@File-3A-3ALOCK_EX] */ + rb_define_const(rb_mFConst, "LOCK_EX", INT2FIX(LOCK_EX)); + /* {File::LOCK_UN}[rdoc-ref:File::Constants@File-3A-3ALOCK_UN] */ + rb_define_const(rb_mFConst, "LOCK_UN", INT2FIX(LOCK_UN)); + /* {File::LOCK_NB}[rdoc-ref:File::Constants@File-3A-3ALOCK_NB] */ + rb_define_const(rb_mFConst, "LOCK_NB", INT2FIX(LOCK_NB)); + + /* {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_global_function("test", rb_f_test, -1); rb_cStat = rb_define_class_under(rb_cFile, "Stat", rb_cObject); @@ -4690,6 +8175,7 @@ Init_File() rb_define_method(rb_cStat, "atime", rb_stat_atime, 0); rb_define_method(rb_cStat, "mtime", rb_stat_mtime, 0); rb_define_method(rb_cStat, "ctime", rb_stat_ctime, 0); + rb_define_method(rb_cStat, "birthtime", rb_stat_birthtime, 0); rb_define_method(rb_cStat, "inspect", rb_stat_inspect, 0); @@ -4698,8 +8184,10 @@ Init_File() rb_define_method(rb_cStat, "directory?", rb_stat_d, 0); rb_define_method(rb_cStat, "readable?", rb_stat_r, 0); rb_define_method(rb_cStat, "readable_real?", rb_stat_R, 0); + rb_define_method(rb_cStat, "world_readable?", rb_stat_wr, 0); rb_define_method(rb_cStat, "writable?", rb_stat_w, 0); rb_define_method(rb_cStat, "writable_real?", rb_stat_W, 0); + rb_define_method(rb_cStat, "world_writable?", rb_stat_ww, 0); rb_define_method(rb_cStat, "executable?", rb_stat_x, 0); rb_define_method(rb_cStat, "executable_real?", rb_stat_X, 0); rb_define_method(rb_cStat, "file?", rb_stat_f, 0); |
