/* ****************************************************************************
*
* Copyright (c) Microsoft Corporation.
*
* This source code is subject to terms and conditions of the Apache License, Version 2.0. A
* copy of the license can be found in the License.html file at the root of this distribution. If
* you cannot locate the Apache License, Version 2.0, please send an email to
* ironruby@microsoft.com. By using this source code in any fashion, you are agreeing to be bound
* by the terms of the Apache License, Version 2.0.
*
* You must not remove this notice, or any other, from this software.
*
*
* ***************************************************************************/
using System;
using System.Collections;
using System.Collections.Generic;
using System.Diagnostics;
using System.IO;
using System.Runtime.InteropServices;
using System.Text;
using IronRuby.Runtime;
using IronRuby.Runtime.Calls;
using Microsoft.Scripting;
using Microsoft.Scripting.Runtime;
using IronRuby.Compiler;
using System.Globalization;
using IronRuby.Runtime.Conversions;
using System.Runtime.CompilerServices;
using System.Reflection;
using Microsoft.Scripting.Utils;
namespace IronRuby.Builtins {
///
/// File builtin class. Derives from IO
///
[RubyClass("File", Extends = typeof(RubyFile))]
public static class RubyFileOps {
internal static bool FileExists(RubyContext/*!*/ context, MutableString/*!*/ path) {
return context.Platform.FileExists(context.DecodePath(path));
}
internal static bool DirectoryExists(RubyContext/*!*/ context, MutableString/*!*/ path) {
return context.Platform.DirectoryExists(context.DecodePath(path));
}
internal static bool Exists(RubyContext/*!*/ context, MutableString/*!*/ path) {
var strPath = context.DecodePath(path);
return context.Platform.DirectoryExists(strPath) || context.Platform.FileExists(strPath);
}
#region Construction
[RubyConstructor]
public static RubyFile/*!*/ CreateFile(
ConversionStorage/*!*/ toInt,
ConversionStorage>/*!*/ toHash,
ConversionStorage/*!*/ toPath,
ConversionStorage/*!*/ toStr,
RubyClass/*!*/ self,
object descriptorOrPath,
[Optional]object optionsOrMode,
[Optional]object optionsOrPermissions,
[DefaultParameterValue(null), DefaultProtocol]IDictionary options) {
return Reinitialize(toInt, toHash, toPath, toStr, new RubyFile(self.Context), descriptorOrPath, optionsOrMode, optionsOrPermissions, options);
}
[RubyMethod("initialize", RubyMethodAttributes.PrivateInstance)]
public static RubyFile/*!*/ Reinitialize(
ConversionStorage/*!*/ toInt,
ConversionStorage>/*!*/ toHash,
ConversionStorage/*!*/ toPath,
ConversionStorage/*!*/ toStr,
RubyFile/*!*/ self,
object descriptorOrPath,
[Optional]object optionsOrMode,
[Optional]object optionsOrPermissions,
[DefaultParameterValue(null), DefaultProtocol]IDictionary options) {
var context = self.Context;
Protocols.TryConvertToOptions(toHash, ref options, ref optionsOrMode, ref optionsOrPermissions);
var toIntSite = toInt.GetSite(TryConvertToFixnumAction.Make(toInt.Context));
// A nil mode or permission means "not given", as in CRuby.
IOInfo info = new IOInfo();
if (optionsOrMode != Missing.Value && optionsOrMode != null) {
int? m = toIntSite.Target(toIntSite, optionsOrMode);
info = m.HasValue ? new IOInfo(IOModeNative.ToIOMode(m.Value)) : IOInfo.Parse(context, Protocols.CastToString(toStr, optionsOrMode));
}
int permissions = 0;
if (optionsOrPermissions != Missing.Value && optionsOrPermissions != null) {
int? p = toIntSite.Target(toIntSite, optionsOrPermissions);
if (!p.HasValue) {
throw RubyExceptions.CreateImplicitConversionError(context.GetClassName(optionsOrPermissions), "Integer");
}
permissions = p.Value;
}
if (options != null) {
info = info.AddOptions(toStr, options);
// perm: says what the third argument would, and MRI refuses both at once.
object permOption;
if (options.TryGetValue(context.CreateAsciiSymbol("perm"), out permOption) && permOption != null) {
if (optionsOrPermissions != Missing.Value && optionsOrPermissions != null) {
throw RubyExceptions.CreateArgumentError("perm specified twice");
}
int? p = toIntSite.Target(toIntSite, permOption);
if (!p.HasValue) {
throw RubyExceptions.CreateImplicitConversionError(context.GetClassName(permOption), "Integer");
}
permissions = p.Value;
}
}
self.ConversionOptions = options;
// descriptor or path:
int? descriptor = toIntSite.Target(toIntSite, descriptorOrPath);
if (descriptor.HasValue) {
RubyIOOps.Reinitialize(self, descriptor.Value, info);
// File.new(fd, path: "...") names the descriptor; File#path answers it.
object path;
if (options != null && options.TryGetValue(context.CreateAsciiSymbol("path"), out path) && path != null) {
MutableString strPath = Protocols.CastToString(toStr, path);
self.Path = context.DecodePath(strPath);
self.PathEncoding = strPath.Encoding;
}
} else {
Reinitialize(self, Protocols.CastToPath(toPath, descriptorOrPath), info, permissions);
}
return self;
}
internal static void Reinitialize(RubyFile/*!*/ file, MutableString/*!*/ path, IOInfo info, int permission) {
var strPath = file.Context.DecodePath(path);
// open(2) only honours the permission argument when it actually creates the
// file, and applies the umask to it.
bool creating = (info.Mode & IOMode.CreateIfNotExists) != 0
&& !file.Context.Platform.FileExists(strPath);
var stream = RubyFile.OpenFileStream(file.Context, strPath, info.Mode);
if (creating && permission != 0 && Posix.IsAvailable) {
int umask = NativeUmask(0);
NativeUmask(umask);
int errno;
Posix.Chmod(strPath, permission & ~umask, out errno);
}
file.Path = strPath;
file.PathEncoding = path.Encoding;
file.Mode = info.Mode;
file.SetStream(stream);
file.SetFileDescriptor(file.Context.AllocateFileDescriptor(stream));
if (info.HasEncoding) {
file.SetEncodings(info.ExternalEncoding, info.InternalEncoding);
} else if ((info.Mode & IOMode.PreserveEndOfLines) != 0) {
// The "b" flag with no explicit encoding means BINARY.
file.SetEncodings(RubyEncoding.Binary, null);
} else {
// MRI still runs the defaults through rb_io_ext_int_to_encs when the stream is
// opened, so a stream opened while a default internal encoding was in force keeps
// the external encoding it had then, rather than following later reassignments.
file.SetEncodings(null, null);
}
}
#endregion
#region Declared Constants
static RubyFileOps() {
// On Unix '\\' is an ordinary filename character and PATH is colon
// separated; File::ALT_SEPARATOR is nil there, as in CRuby.
ALT_SEPARATOR = IsWindows
? MutableString.CreateAscii(AltDirectorySeparatorChar.ToString()).Freeze()
: null;
SEPARATOR = MutableString.CreateAscii(DirectorySeparatorChar.ToString()).Freeze();
Separator = SEPARATOR;
PATH_SEPARATOR = MutableString.CreateAscii(PathSeparatorChar.ToString()).Freeze();
}
private const char AltDirectorySeparatorChar = '\\';
private const char DirectorySeparatorChar = '/';
internal static readonly bool IsWindows = System.IO.Path.DirectorySeparatorChar == '\\';
private static char PathSeparatorChar {
get { return IsWindows ? ';' : ':'; }
}
private static readonly char[] SeparatorChars = IsWindows
? new[] { DirectorySeparatorChar, AltDirectorySeparatorChar }
: new[] { DirectorySeparatorChar };
internal static bool IsDirectorySeparator(int c) {
return c == DirectorySeparatorChar || (IsWindows && c == AltDirectorySeparatorChar);
}
[RubyConstant]
public readonly static MutableString ALT_SEPARATOR;
[RubyConstant]
public readonly static MutableString PATH_SEPARATOR;
[RubyConstant]
public readonly static MutableString SEPARATOR;
[RubyConstant]
public readonly static MutableString Separator = SEPARATOR;
private const string NUL_VALUE = "NUL";
[RubyModule("Constants")]
public static class Constants {
[RubyConstant]
public readonly static int APPEND = IOModeNative.Append;
[RubyConstant]
public readonly static int BINARY = IOModeNative.Binary;
[RubyConstant]
public readonly static int CREAT = IOModeNative.CreateIfNotExists;
[RubyConstant]
public readonly static int EXCL = IOModeNative.ErrorIfExists;
[RubyConstant]
public readonly static int FNM_CASEFOLD = 0x08;
[RubyConstant]
public readonly static int FNM_DOTMATCH = 0x04;
[RubyConstant]
public readonly static int FNM_NOESCAPE = 0x01;
[RubyConstant]
public readonly static int FNM_PATHNAME = 0x02;
[RubyConstant]
public readonly static int FNM_EXTGLOB = Glob.FnmExtGlob;
[RubyConstant]
public readonly static int FNM_SHORTNAME = 0x00;
// FNM_CASEFOLD only on a case insensitive file system; 0 on Unix.
[RubyConstant]
public readonly static int FNM_SYSCASE = Glob.FnmSysCase;
[RubyConstant]
public readonly static int LOCK_EX = 0x02;
[RubyConstant]
public readonly static int LOCK_NB = 0x04;
[RubyConstant]
public readonly static int LOCK_SH = 0x01;
[RubyConstant]
public readonly static int LOCK_UN = 0x08;
[RubyConstant]
public readonly static int NONBLOCK = IOModeNative.NonBlocking;
[RubyConstant]
public readonly static int NOCTTY = IOModeNative.NoControllingTerminal;
[RubyConstant]
public readonly static int SYNC = IOModeNative.Synchronized;
[RubyConstant]
public readonly static int SHARE_DELETE = IOModeNative.ShareDelete;
[RubyConstant]
public readonly static int RDONLY = 0;
[RubyConstant]
public readonly static int RDWR = 2;
[RubyConstant]
public readonly static int TRUNC = IOModeNative.Truncate;
[RubyConstant]
public readonly static int WRONLY = 1;
}
#endregion
internal const int WriteModeMask = 0x80; // Oct 0200
internal const int ReadWriteMode = 0x1B6; // Oct 0666
[RubyMethod("open", RubyMethodAttributes.PublicSingleton)]
public static RuleGenerator/*!*/ Open() {
return RubyIOOps.Open();
}
#region chmod, chown, lchmod, lchown, umask
[RubyMethod("chmod")]
public static object Chmod(RubyFile/*!*/ self, [DefaultProtocol]int permission) {
self.RequireInitialized();
if (Posix.IsAvailable) {
int fd = GetNativeFileDescriptor(self);
int errno;
if (fd >= 0) {
if (Posix.FChmod(fd, permission, out errno) != 0) {
throw Posix.Error(errno, self.Path);
}
return 0;
}
}
if (self.Path == null) {
// Without fchmod there is nothing to change a pathless File through; MRI's
// rb_file_chmod answers nil then, rather than raising.
return null;
}
Chmod(self.Path, permission);
return 0;
}
[RubyMethod("chmod", RubyMethodAttributes.PublicSingleton)]
public static int Chmod(ConversionStorage/*!*/ toPath, RubyClass/*!*/ self, [DefaultProtocol]int permission, params object[]/*!*/ paths) {
foreach (var path in paths) {
Chmod(self.Context.DecodePath(Protocols.CastToPath(toPath, path)), permission);
}
return paths.Length;
}
[RubyMethod("lchmod", RubyMethodAttributes.PublicSingleton)]
public static int LChmod(ConversionStorage/*!*/ toPath, RubyClass/*!*/ self, [DefaultProtocol]int permission, params object[]/*!*/ paths) {
// Linux has no lchmod(2): the permission bits of a symbolic link are
// meaningless there, so there is nothing correct to do.
throw new IronRuby.Builtins.NotImplementedError("lchmod() function is unimplemented on this machine");
}
internal static void Chmod(string/*!*/ path, int permission) {
#if FEATURE_FILESYSTEM
if (Posix.IsAvailable) {
int errno;
if (Posix.Chmod(path, permission, out errno) != 0) {
throw Posix.Error(errno, path);
}
return;
}
FileAttributes oldAttributes = File.GetAttributes(path);
if ((permission & WriteModeMask) == 0) {
File.SetAttributes(path, oldAttributes | FileAttributes.ReadOnly);
} else {
File.SetAttributes(path, oldAttributes & ~FileAttributes.ReadOnly);
}
#endif
}
private static int ToUidGid(RubyContext/*!*/ context, object value, bool owner) {
if (value == null) {
return -1;
}
if (value is int) {
return (int)value;
}
throw RubyExceptions.CreateUnexpectedTypeError(context, value, "Integer");
}
[RubyMethod("chown")]
public static int ChangeOwner(RubyContext/*!*/ context, RubyFile/*!*/ self, object owner, object group) {
self.RequireInitialized();
int uid = ToUidGid(context, owner, true);
int gid = ToUidGid(context, group, false);
if (Posix.IsAvailable) {
int fd = GetNativeFileDescriptor(self);
int errno;
if (fd >= 0) {
if (Posix.FChown(fd, uid, gid, out errno) != 0) {
throw Posix.Error(errno, self.Path);
}
}
}
return 0;
}
[RubyMethod("chown", RubyMethodAttributes.PublicSingleton)]
public static int ChangeOwner(ConversionStorage/*!*/ toPath, RubyClass/*!*/ self, object owner, object group,
params object[]/*!*/ paths) {
return ChangeOwner(toPath, self, owner, group, paths, true);
}
[RubyMethod("lchown", RubyMethodAttributes.PublicSingleton)]
public static int LChangeOwner(ConversionStorage/*!*/ toPath, RubyClass/*!*/ self, object owner, object group,
params object[]/*!*/ paths) {
return ChangeOwner(toPath, self, owner, group, paths, false);
}
private static int ChangeOwner(ConversionStorage/*!*/ toPath, RubyClass/*!*/ self, object owner, object group,
object[]/*!*/ paths, bool followLinks) {
var context = self.Context;
int uid = ToUidGid(context, owner, true);
int gid = ToUidGid(context, group, false);
foreach (var path in paths) {
string strPath = context.DecodePath(Protocols.CastToPath(toPath, path));
if (Posix.IsAvailable) {
int errno;
int rc = followLinks ? Posix.Chown(strPath, uid, gid, out errno) : Posix.LChown(strPath, uid, gid, out errno);
if (rc != 0) {
throw Posix.Error(errno, strPath);
}
} else if (!Exists(context, Protocols.CastToPath(toPath, path))) {
throw RubyExceptions.CreateENOENT("No such file or directory - {0}", strPath);
}
}
return paths.Length;
}
internal static readonly object UmaskKey = new object();
private static bool IsUnixPlatform {
get { return System.IO.Path.DirectorySeparatorChar == '/'; }
}
[System.Runtime.InteropServices.DllImport("libc", EntryPoint = "umask")]
private static extern int NativeUmask(int mask);
[RubyMethod("umask", RubyMethodAttributes.PublicSingleton)]
public static int GetUmask(RubyClass/*!*/ self, [DefaultProtocol]int mask) {
if (IsUnixPlatform) {
return NativeUmask(mask);
}
int result = (int)self.Context.GetOrCreateLibraryData(UmaskKey, () => 0);
self.Context.TrySetLibraryData(UmaskKey, CalculateUmask(mask));
return result;
}
[RubyMethod("umask", RubyMethodAttributes.PublicSingleton)]
public static int GetUmask(RubyClass/*!*/ self) {
if (IsUnixPlatform) {
int current = NativeUmask(0);
NativeUmask(current);
return current;
}
return (int)self.Context.GetOrCreateLibraryData(UmaskKey, () => 0);
}
private static int CalculateUmask(int mask) {
return (mask % 512) / 128 * 128;
}
#endregion
#region delete, unlink, truncate, rename
[RubyMethod("delete", RubyMethodAttributes.PublicSingleton)]
[RubyMethod("unlink", RubyMethodAttributes.PublicSingleton)]
public static int Delete(ConversionStorage/*!*/ toPath, RubyClass/*!*/ self, object path) {
string strPath = self.Context.DecodePath(Protocols.CastToPath(toPath, path));
if (Posix.IsAvailable) {
// unlink(2) rather than System.IO: the FileExists guard below answers false for a
// symbolic link that points at a directory -- System.IO sees a directory -- so
// removing such a link reported ENOENT rather than doing it. Letting the kernel
// decide also gets EISDIR, EPERM and EACCES right without guessing.
int errno;
if (Posix.Unlink(strPath, out errno) != 0) {
throw Posix.Error(errno, strPath);
}
return 1;
}
if (!self.Context.Platform.FileExists(strPath)) {
throw RubyExceptions.CreateENOENT("{0}", strPath);
}
Delete(self.Context, strPath);
return 1;
}
internal static void Delete(RubyContext/*!*/ context, string/*!*/ path) {
try {
context.Platform.DeleteFile(path, true);
#if FEATURE_FILESYSTEM
} catch (DirectoryNotFoundException) {
throw RubyExceptions.CreateENOENT("No such file or directory - {0}", path);
#endif
} catch (IOException e) {
throw Errno.CreateEACCES(e.Message, e);
}
}
[RubyMethod("delete", RubyMethodAttributes.PublicSingleton)]
[RubyMethod("unlink", RubyMethodAttributes.PublicSingleton)]
public static int Delete(ConversionStorage/*!*/ toPath, RubyClass/*!*/ self, params object[] paths) {
foreach (MutableString path in paths) {
Delete(toPath, self, path);
}
return paths.Length;
}
#if FEATURE_FILESYSTEM
// The size is an off_t: a file of 4 GiB and more is an ordinary thing to ask for.
[RubyMethod("truncate", BuildConfig = "FEATURE_FILESYSTEM")]
public static int Truncate(ConversionStorage/*!*/ integerConversion, RubyFile/*!*/ self, object length) {
long size = Protocols.CastToInteger(integerConversion, length).ToInt64();
self.RequireWritable();
if (size < 0) {
throw self.Path != null ? RubyExceptions.CreateEINVAL("Invalid argument - {0}", self.Path) : RubyExceptions.CreateEINVAL();
}
// ftruncate(2) leaves the file offset alone; Stream.SetLength pulls it back
// to the new end, which would make a later write land in the wrong place.
long position = self.Position;
self.Length = size;
self.Position = position;
return 0;
}
[RubyMethod("truncate", RubyMethodAttributes.PublicSingleton, BuildConfig = "FEATURE_FILESYSTEM")]
public static int Truncate(ConversionStorage/*!*/ toPath, ConversionStorage/*!*/ integerConversion,
RubyClass/*!*/ self, object path, object length) {
string strPath = self.Context.DecodePath(Protocols.CastToPath(toPath, path));
long size = Protocols.CastToInteger(integerConversion, length).ToInt64();
if (size < 0) {
throw RubyExceptions.CreateEINVAL("Invalid argument - {0}", strPath);
}
using (RubyFile f = new RubyFile(self.Context, strPath, IOMode.ReadWrite)) {
f.Length = size;
}
return 0;
}
#endif
[RubyMethod("rename", RubyMethodAttributes.PublicSingleton)]
public static int Rename(ConversionStorage/*!*/ toPath, RubyClass/*!*/ self, object oldPath, object newPath) {
var context = self.Context;
string strOldPath = context.DecodePath(Protocols.CastToPath(toPath, oldPath));
string strNewPath = context.DecodePath(Protocols.CastToPath(toPath, newPath));
if (strOldPath.Length == 0 || strNewPath.Length == 0) {
throw RubyExceptions.CreateENOENT();
}
// MRI names both ends of a failed rename: "No such file or directory - (old, new)".
string paths = "(" + strOldPath + ", " + strNewPath + ")";
if (!context.Platform.FileExists(strOldPath) && !context.Platform.DirectoryExists(strOldPath)) {
throw RubyExceptions.CreateENOENT("No such file or directory - {0}", paths);
}
if (RubyUtils.ExpandPath(context.Platform, strOldPath) == RubyUtils.ExpandPath(context.Platform, strNewPath)) {
return 0;
}
if (context.Platform.FileExists(strNewPath)) {
Delete(context, strNewPath);
}
try {
context.Platform.MoveFileSystemEntry(strOldPath, strNewPath);
} catch (DirectoryNotFoundException e) {
throw RubyExceptions.CreateENOENT(e, "No such file or directory - {0}", paths);
} catch (FileNotFoundException e) {
throw RubyExceptions.CreateENOENT(e, "No such file or directory - {0}", paths);
} catch (IOException e) {
throw Errno.CreateEACCES(e.Message, e);
}
return 0;
}
#endregion
#region path, basename, dirname, extname, expand_path, absolute_path, fnmatch
[RubyMethod("path", RubyMethodAttributes.PublicSingleton)]
public static MutableString/*!*/ ToPath(ConversionStorage/*!*/ toPath, RubyClass/*!*/ self, object path) {
return Protocols.CastToPath(toPath, path);
}
[RubyMethod("basename", RubyMethodAttributes.PublicSingleton)]
public static MutableString/*!*/ BaseName(ConversionStorage/*!*/ toPath, RubyClass/*!*/ self,
object path, [DefaultProtocol, NotNull, Optional]MutableString suffix) {
return BaseName(Protocols.CastToPath(toPath, path), suffix);
}
private static MutableString/*!*/ BaseName(MutableString/*!*/ path, MutableString suffix) {
if (path.IsEmpty) {
return path;
}
string strPath = path.ConvertToString();
string[] parts = strPath.Split(SeparatorChars, StringSplitOptions.RemoveEmptyEntries);
if (parts.Length == 0) {
return MutableString.CreateMutable(path.Encoding).Append((char)path.GetLastChar()).TaintBy(path);
}
bool isWindows = Environment.OSVersion.Platform != PlatformID.Unix && Environment.OSVersion.Platform != PlatformID.MacOSX;
if (isWindows) {
string first = parts[0];
if (strPath.Length >= 2 && IsDirectorySeparator(strPath[0]) && IsDirectorySeparator(strPath[1])) {
// UNC: skip 2 parts. The root's base name is its trailing separator as written
// ("\\foo\bar\" gives "\"), or "/" when there is none (MRI).
if (parts.Length <= 2) {
char lastChar = strPath[strPath.Length - 1];
return MutableString.CreateMutable(path.Encoding)
.Append(IsDirectorySeparator(lastChar) ? lastChar : DirectorySeparatorChar).TaintBy(path);
}
} else if (first.Length == 2 && Tokenizer.IsLetter(first[0]) && first[1] == ':') {
// skip drive letter "X:"
if (parts.Length <= 1) {
var result = MutableString.CreateMutable(path.Encoding).TaintBy(path);
if (strPath.Length > 2) {
result.Append(strPath[2]);
}
return result;
}
}
}
string last = parts[parts.Length - 1];
// "C:x" is x relative to drive C's current directory: its base name is "x" (MRI).
if (isWindows && parts.Length == 1 && IsDriveRelative(strPath)) {
last = last.Substring(2);
}
if (MutableString.IsNullOrEmpty(suffix)) {
return MutableString.CreateMutable(last, path.Encoding);
}
StringComparison comparison = IsWindows ? StringComparison.OrdinalIgnoreCase : StringComparison.Ordinal;
int matchLength = last.Length;
string strSuffix = suffix.ToString();
if (strSuffix == ".*") {
// ".*" is the only wildcard rmext() understands; any other suffix,
// including ".t*", is matched literally.
matchLength = last.Length - FindExtension(last).Length;
} else if (last.EndsWith(strSuffix, comparison)) {
matchLength = last.Length - strSuffix.Length;
}
// Stripping the suffix never leaves nothing behind: basename("bar", "bar")
// is "bar", not "".
if (matchLength == 0) {
matchLength = last.Length;
}
return MutableString.CreateMutable(path.Encoding).Append(last, 0, matchLength).TaintBy(path);
}
[RubyMethod("dirname", RubyMethodAttributes.PublicSingleton)]
public static MutableString/*!*/ DirName(ConversionStorage/*!*/ toPath, RubyClass/*!*/ self, object path) {
return DirName(Protocols.CastToPath(toPath, path));
}
[RubyMethod("dirname", RubyMethodAttributes.PublicSingleton)]
public static MutableString/*!*/ DirName(ConversionStorage/*!*/ toPath, RubyClass/*!*/ self, object path,
[DefaultProtocol]int level) {
if (level < 0) {
throw RubyExceptions.CreateArgumentError("negative level: {0}", level);
}
MutableString result = Protocols.CastToPath(toPath, path);
if (level == 0) {
// Level 0 removes no component, but still normalises: the leading run of
// slashes collapses and an empty path becomes ".", as in CRuby.
string strPath = result.ConvertToString();
if (!IsWindows) {
strPath = NormalizeRoot(strPath);
}
return MutableString.CreateMutable(strPath, result.Encoding).TaintBy(result);
}
for (int i = 0; i < level; i++) {
MutableString next = DirName(result);
// "/" and "." are fixed points; stop early so a huge level is cheap.
if (next.ConvertToString() == result.ConvertToString()) {
return next;
}
result = next;
}
return result;
}
private static MutableString/*!*/ DirName(MutableString/*!*/ path) {
if (!IsWindows) {
return MutableString.CreateMutable(PosixDirName(path.ConvertToString()), path.Encoding).TaintBy(path);
}
return WindowsDirName(path);
}
///
/// dirname(3) as CRuby implements it on Unix: a leading run of slashes collapses
/// to a single "/", interior runs are preserved, and a path with no slash left is ".".
///
private static string/*!*/ NormalizeRoot(string/*!*/ path) {
int start = 0;
while (start < path.Length && path[start] == '/') {
start++;
}
if (start == 0) {
return path.Length == 0 ? "." : path;
}
return "/" + path.Substring(start);
}
private static string/*!*/ PosixDirName(string/*!*/ path) {
int start = 0;
while (start < path.Length && path[start] == '/') {
start++;
}
bool rooted = start > 0;
if (start == path.Length) {
return rooted ? "/" : ".";
}
// Everything after the root: "/////foo/bar/" is treated as root + "foo/bar/".
string rest = path.Substring(start);
int end = rest.Length;
while (end > 0 && rest[end - 1] == '/') {
end--;
}
int slash = rest.LastIndexOf('/', end - 1);
if (slash < 0) {
return rooted ? "/" : ".";
}
while (slash > 0 && rest[slash - 1] == '/') {
slash--;
}
return (rooted ? "/" : "") + rest.Substring(0, slash);
}
///
/// "C:" or "C:name" with no separator after the colon: a name relative to the current
/// directory of drive C.
///
private static bool IsDriveRelative(string/*!*/ path) {
return path.Length >= 2 && Tokenizer.IsLetter(path[0]) && path[1] == ':' &&
path.IndexOfAny(SeparatorChars, 2) < 0;
}
private static MutableString/*!*/ WindowsDirName(MutableString/*!*/ path) {
string strPath = path.ConvertToString();
string directoryName = strPath;
// MRI: the directory of "C:" and of "C:x" is "C:.", the current directory of drive C.
// pathname tells a DOS-ish platform by File.dirname("A:") == "A:.", and without it
// Pathname#absolute? was false for every "C:/..." path.
if (IsDriveRelative(strPath)) {
return MutableString.CreateMutable(strPath.Substring(0, 2) + ".", path.Encoding);
}
// More than two leading separators are a UNC prefix all the same: "\\\foo\bar" is
// \\foo\bar, the root of share bar on server foo (MRI).
int leading = 0;
while (leading < strPath.Length && IsDirectorySeparator(strPath[leading])) {
leading++;
}
if (leading > 2 && leading < strPath.Length) {
strPath = strPath.Substring(leading - 2);
directoryName = strPath;
}
if (IsValidPath(strPath)) {
strPath = StripPathCharacters(strPath);
// handle top-level UNC paths
directoryName = Path.GetDirectoryName(strPath);
if (directoryName == null) {
return MutableString.CreateMutable(strPath, path.Encoding);
}
string fileName = Path.GetFileName(strPath);
if (!String.IsNullOrEmpty(fileName)) {
directoryName = StripPathCharacters(strPath.Substring(0, strPath.LastIndexOf(fileName, StringComparison.Ordinal)));
}
} else {
if (directoryName.Length > 1) {
directoryName = "//";
}
}
directoryName = String.IsNullOrEmpty(directoryName) ? "." : directoryName;
return MutableString.CreateMutable(directoryName, path.Encoding);
}
private static bool IsValidPath(string path) {
foreach (char c in path) {
if (c != '/' && c != '\\') {
return true;
}
}
return false;
}
private static string StripPathCharacters(string path) {
int limit = 0;
for (int charIndex = path.Length - 1; charIndex > 0; charIndex--) {
if (!((path[charIndex] == '/') || (path[charIndex] == '\\')))
break;
limit++;
}
if (limit > 0) {
limit--;
if (path.Length == 3 && path[1] == ':') limit--;
return path.Substring(0, path.Length - limit - 1);
}
return path;
}
[RubyMethod("extname", RubyMethodAttributes.PublicSingleton)]
public static MutableString/*!*/ GetExtension(ConversionStorage/*!*/ toPath, RubyClass/*!*/ self, object path) {
MutableString pathStr = Protocols.CastToPath(toPath, path);
string last = LastPathComponent(pathStr.ConvertToString());
string extension = FindExtension(last);
// CRuby hands back a shared empty binary string when there is no extension,
// so File.extname("foo").encoding is ASCII-8BIT, not the path's encoding.
return extension.Length == 0
? MutableString.CreateBinary().TaintBy(pathStr)
: MutableString.Create(extension, pathStr.Encoding).TaintBy(pathStr);
}
private static string/*!*/ LastPathComponent(string/*!*/ path) {
for (int i = path.Length - 1; i >= 0; i--) {
if (IsDirectorySeparator(path[i])) {
return path.Substring(i + 1);
}
}
return path;
}
///
/// ruby_enc_find_extname: leading dots belong to the name ("/.config" is hidden,
/// not an extension of ""), and the extension runs from the last remaining dot to
/// the end, so "foo." has the extension ".".
///
private static string/*!*/ FindExtension(string/*!*/ name) {
int start = 0;
while (start < name.Length && name[start] == '.') {
start++;
}
if (start >= name.Length) {
return "";
}
int dot = name.LastIndexOf('.');
return dot < start ? "" : name.Substring(dot);
}
[RubyMethod("expand_path", RubyMethodAttributes.PublicSingleton)]
public static MutableString/*!*/ ExpandPath(ConversionStorage/*!*/ toPath, RubyClass/*!*/ self, object path,
[DefaultParameterValue(null)]object basePath) {
var context = self.Context;
MutableString pathStr = Protocols.CastToPath(toPath, path);
RequireUsablePathEncoding(context, pathStr);
string result = RubyUtils.ExpandPath(
context.Platform,
ExpandTilde(context, DecodePathLike(context, pathStr)),
(basePath == null)
? context.Platform.CurrentDirectory
: ExpandTilde(context, DecodePathLike(context, Protocols.CastToPath(toPath, basePath))),
false
);
return EncodePathLike(context, result, pathStr);
}
///
/// "~" uses $HOME and falls back to the user database; "~name" always goes to the
/// user database. A $HOME that is empty or relative is an error, as in CRuby, and
/// an unknown user name is an ArgumentError rather than a silent pass-through.
///
private static string/*!*/ ExpandTilde(RubyContext/*!*/ context, string/*!*/ path) {
if (path.Length == 0 || path[0] != '~') {
return path;
}
if (IsWindows) {
return ExpandWindowsTilde(context, path);
}
int slash = path.IndexOf('/', 1);
string userName = (slash < 0) ? path.Substring(1) : path.Substring(1, slash - 1);
string rest = (slash < 0) ? null : path.Substring(slash + 1);
string home;
if (userName.Length == 0) {
home = RubyEnvironment.GetVariable(context.Platform, "HOME");
if (home == null) {
home = Posix.GetHomeDirectory(Posix.GetEUid());
}
if (home == null) {
// MRI's rb_default_home_dir names the real uid, whatever it looked up
throw RubyExceptions.CreateArgumentError("couldn't find home for uid '{0}'", Posix.GetUid());
}
if (home.Length == 0 || home[0] != DirectorySeparatorChar) {
throw RubyExceptions.CreateArgumentError("non-absolute home");
}
} else {
home = Posix.GetHomeDirectory(userName);
if (home == null) {
throw RubyExceptions.CreateArgumentError("user {0} doesn't exist", userName);
}
}
return (rest == null) ? home : home + "/" + rest;
}
///
/// "~" on Windows, where there is no user database to ask. MRI looks at $HOME, then at
/// $USERPROFILE, then at $HOMEDRIVE + $HOMEPATH, as Dir.home does, and refuses "~name"
/// outright. Leaving
/// the tilde alone - which is what this used to do - made Dir.home, Gem's user directory
/// and every "~/.somethingrc" resolve inside the working directory.
///
private static string/*!*/ ExpandWindowsTilde(RubyContext/*!*/ context, string/*!*/ path) {
int slash = path.IndexOfAny(SeparatorChars, 1);
string userName = (slash < 0) ? path.Substring(1) : path.Substring(1, slash - 1);
string rest = (slash < 0) ? null : path.Substring(slash + 1);
if (userName.Length != 0) {
throw RubyExceptions.CreateArgumentError("can't find user {0}", userName);
}
string home = RubyEnvironment.GetVariable(context.Platform, "HOME");
if (home == null) {
home = RubyEnvironment.GetVariable(context.Platform, "USERPROFILE");
}
if (home == null) {
string drive = RubyEnvironment.GetVariable(context.Platform, "HOMEDRIVE");
string tail = RubyEnvironment.GetVariable(context.Platform, "HOMEPATH");
if (tail != null) {
home = (drive ?? "") + tail;
}
}
if (home == null) {
throw RubyExceptions.CreateArgumentError("couldn't find HOME environment -- expanding '~'");
}
home = home.Replace(AltDirectorySeparatorChar, DirectorySeparatorChar);
return (rest == null) ? home : home + "/" + rest;
}
///
/// A path in the filesystem encoding has to survive a trip through a .NET string, and one
/// whose default external encoding is not ASCII compatible cannot: there is no byte
/// sequence for a separator to look for. MRI reports the same thing, as an incompatibility
/// between the path and the external encoding.
///
private static void RequireUsablePathEncoding(RubyContext/*!*/ context, MutableString/*!*/ path) {
var external = context.DefaultExternalEncoding;
if (external != null && !external.IsAsciiIdentity) {
throw RubyExceptions.CreateEncodingCompatibilityError(path.Encoding, external);
}
}
///
/// A BINARY path is a sequence of bytes MRI never interprets, so it crosses the .NET
/// string layer one byte to one character rather than being decoded - decoding it would
/// replace whatever is not valid in the filesystem encoding, and the bytes would be gone.
///
private static string/*!*/ DecodePathLike(RubyContext/*!*/ context, MutableString/*!*/ path) {
if (path.Encoding != RubyEncoding.Binary) {
return context.DecodePath(path);
}
int count = path.GetByteCount();
var chars = new char[count];
for (int i = 0; i < count; i++) {
chars[i] = (char)path.GetByte(i);
}
return new String(chars);
}
///
/// The path methods hand back a string in the encoding of the path they were
/// given, not in the filesystem encoding.
///
private static MutableString/*!*/ EncodePathLike(RubyContext/*!*/ context, string/*!*/ result, MutableString/*!*/ original) {
if (original.Encoding == RubyEncoding.Binary) {
// the counterpart of DecodePathLike; anything above U+00FF did not come from
// this path's bytes, so that one is left to the general route below
var bytes = new byte[result.Length];
int i = 0;
for (; i < result.Length; i++) {
if (result[i] > 0xFF) {
break;
}
bytes[i] = (byte)result[i];
}
if (i == result.Length) {
return MutableString.CreateBinary(bytes).TaintBy(original);
}
}
// Encode with the filesystem encoding and then relabel, rather than transcode:
// the bytes came from the OS, and a path whose bytes are not representable in
// the argument's encoding still has to come back (CRuby "forces" the encoding).
MutableString encoded = context.EncodePath(result);
encoded.ForceEncoding(original.Encoding);
return encoded.TaintBy(original);
}
[RubyMethod("absolute_path", RubyMethodAttributes.PublicSingleton)]
public static MutableString/*!*/ AbsolutePath(ConversionStorage/*!*/ toPath, RubyClass/*!*/ self, object path,
[DefaultParameterValue(null)]object basePath) {
var context = self.Context;
MutableString pathStr = Protocols.CastToPath(toPath, path);
string result = RubyUtils.ExpandPath(
context.Platform,
context.DecodePath(pathStr),
(basePath == null) ? context.Platform.CurrentDirectory : context.DecodePath(Protocols.CastToPath(toPath, basePath)),
false
);
return EncodePathLike(context, result, pathStr);
}
[RubyMethod("absolute_path?", RubyMethodAttributes.PublicSingleton)]
public static bool IsAbsolutePath(ConversionStorage/*!*/ toPath, RubyClass/*!*/ self, object path) {
// No expansion and no filesystem access: "~" and "C:/x" are relative on Unix.
string strPath = Protocols.CastToPath(toPath, path).ConvertToString();
if (IsWindows) {
return strPath.Length >= 3 && Tokenizer.IsLetter(strPath[0]) && strPath[1] == ':' && IsDirectorySeparator(strPath[2])
|| strPath.Length >= 2 && IsDirectorySeparator(strPath[0]) && IsDirectorySeparator(strPath[1]);
}
return strPath.Length > 0 && strPath[0] == DirectorySeparatorChar;
}
[RubyMethod("fnmatch", RubyMethodAttributes.PublicSingleton)]
[RubyMethod("fnmatch?", RubyMethodAttributes.PublicSingleton)]
public static bool FnMatch(ConversionStorage/*!*/ toPath, object/*!*/ self,
[DefaultProtocol, NotNull]MutableString/*!*/ pattern, object path, [DefaultProtocol, Optional]int flags) {
return Glob.FnMatch(pattern.ConvertToString(), Protocols.CastToPath(toPath, path).ConvertToString(), flags);
}
#endregion
#region split, join
[RubyMethod("split", RubyMethodAttributes.PublicSingleton)]
public static RubyArray Split(ConversionStorage/*!*/ toPath, RubyClass/*!*/ self, object path) {
MutableString p = Protocols.CastToPath(toPath, path);
RubyArray result = new RubyArray(2);
result.Add(DirName(p));
result.Add(BaseName(p, null));
return result;
}
[RubyMethod("join", RubyMethodAttributes.PublicSingleton)]
public static MutableString Join(ConversionStorage/*!*/ toPath, RubyClass/*!*/ self, params object[]/*!*/ parts) {
MutableString result = null;
Dictionary visitedLists = null;
var worklist = new Stack();
int current = 0;
MutableString str;
Push(worklist, parts);
while (worklist.Count > 0) {
object part = worklist.Pop();
var list = part as IList;
if (list != null) {
if (list.Count == 0) {
str = MutableString.FrozenEmpty;
} else if (visitedLists != null && visitedLists.ContainsKey(list)) {
throw RubyExceptions.CreateArgumentError("recursive array");
} else {
if (visitedLists == null) {
visitedLists = new Dictionary(ReferenceEqualityComparer.Instance);
}
visitedLists.Add(list, true);
Push(worklist, list);
continue;
}
} else if (part == null) {
throw RubyExceptions.CreateImplicitConversionError("NilClass", "String");
} else {
try {
str = Protocols.CastToPath(toPath, part);
} catch (ArgumentException e) when (e.Message == "path name contains null byte") {
// rb_file_join reports a NUL as a plain string problem.
throw RubyExceptions.CreateArgumentError("string contains null byte");
}
}
if (current > 0) {
// Append negotiates the encodings, so the join of two EUC-JP paths
// stays EUC-JP instead of decaying to binary.
AppendDirectoryName(result, str);
} else {
result = MutableString.CreateMutable(str.Encoding).Append(str);
}
current++;
}
return result ?? MutableString.CreateEmpty();
}
private static void Push(Stack/*!*/ stack, IList/*!*/ values) {
for (int i = values.Count - 1; i >= 0; i--) {
stack.Push(values[i]);
}
}
private static void AppendDirectoryName(MutableString/*!*/ result, MutableString/*!*/ name) {
int resultLength = result.GetCharCount();
int i;
for (i = resultLength - 1; i >= 0; i--) {
if (!IsDirectorySeparator(result.GetChar(i))) {
break;
}
}
if (i == resultLength - 1) {
if (!IsDirectorySeparator(name.GetFirstChar())) {
result.Append(DirectorySeparatorChar);
}
result.Append(name);
} else if (IsDirectorySeparator(name.GetFirstChar())) {
result.Replace(i + 1, resultLength - i - 1, name);
} else {
result.Append(name);
}
}
#endregion
#region flock, readlink, link, symlink
#if FEATURE_FILESYSTEM
///
/// IronRuby's "file descriptor" is an index into a per-context table, not
/// an OS descriptor, so syscalls that need a real fd have to dig the
/// handle out of the underlying FileStream.
///
internal static int GetNativeFileDescriptor(RubyIO/*!*/ io) {
var stream = io.GetStream().BaseStream;
var fs = stream as FileStream;
if (fs == null) {
return -1;
}
return (int)fs.SafeFileHandle.DangerousGetHandle();
}
[RubyMethod("flock", BuildConfig = "FEATURE_FILESYSTEM")]
public static object FileLock(RubyFile/*!*/ self, [DefaultProtocol]int operation) {
self.RequireInitialized();
if (OperatingSystem.IsWindows()) {
return WindowsFileLock(self, operation);
}
if (!Posix.IsFlockAvailable) {
throw new IronRuby.Builtins.NotImplementedError("flock() function is unimplemented on this machine");
}
int fd = GetNativeFileDescriptor(self);
if (fd < 0) {
throw RubyExceptions.CreateEBADF();
}
int errno;
bool nonBlocking = (operation & Posix.LOCK_NB) != 0;
// A blocking flock(2) is issued as poll + sleep rather than as one blocking call:
// a thread parked inside a P/Invoke is reported as running by the CLR and cannot
// be woken, so Thread#status would never say "sleep" and Thread#kill would never
// land. MRI's flock is interruptible, so this is also the closer behaviour.
while (true) {
if (Posix.Flock(fd, operation | Posix.LOCK_NB, out errno) == 0) {
return 0;
}
if (errno != Posix.EWouldBlock) {
throw Posix.Error(errno, self.Path);
}
if (nonBlocking) {
// LOCK_NB on a locked file reports "would block" rather than raising
return false;
}
System.Threading.Thread.Sleep(10);
}
}
private const int LOCKFILE_FAIL_IMMEDIATELY = 1;
private const int LOCKFILE_EXCLUSIVE_LOCK = 2;
private const int ERROR_LOCK_VIOLATION = 33;
private const int ERROR_NOT_LOCKED = 158;
private const int ERROR_IO_PENDING = 997;
[System.Runtime.InteropServices.DllImport("kernel32", SetLastError = true)]
private static extern bool LockFileEx(Microsoft.Win32.SafeHandles.SafeFileHandle handle, int flags, int reserved,
uint lengthLow, uint lengthHigh, ref System.Threading.NativeOverlapped overlapped);
[System.Runtime.InteropServices.DllImport("kernel32", SetLastError = true)]
private static extern bool UnlockFileEx(Microsoft.Win32.SafeHandles.SafeFileHandle handle, int reserved,
uint lengthLow, uint lengthHigh, ref System.Threading.NativeOverlapped overlapped);
[System.Runtime.InteropServices.DllImport("kernel32", CharSet = CharSet.Unicode, SetLastError = true)]
private static extern bool CreateHardLinkW(string newFileName, string existingFileName, IntPtr securityAttributes);
///
/// The errno win32.c's map_errno gives a Win32 error, for the ones File.link, File.symlink
/// and File.readlink can meet; EINVAL for the rest, as map_errno does.
///
private static int Win32ErrorToErrno(int error) {
switch (error) {
case 2: // ERROR_FILE_NOT_FOUND
case 3: // ERROR_PATH_NOT_FOUND
case 15: // ERROR_INVALID_DRIVE
case 161: // ERROR_BAD_PATHNAME
case 206: // ERROR_FILENAME_EXCED_RANGE
return Posix.ENOENT;
case 5: // ERROR_ACCESS_DENIED
case 32: // ERROR_SHARING_VIOLATION
case 33: // ERROR_LOCK_VIOLATION
case 1314: // ERROR_PRIVILEGE_NOT_HELD
return Posix.EACCES;
case 17: // ERROR_NOT_SAME_DEVICE
return Posix.EXDEV;
case 80: // ERROR_FILE_EXISTS
case 183: // ERROR_ALREADY_EXISTS
return Posix.EEXIST;
case 267: // ERROR_DIRECTORY
return Posix.ENOTDIR;
case 1921: // ERROR_CANT_RESOLVE_FILENAME
return Posix.ELOOP;
default:
return Posix.EINVAL;
}
}
// MRI reports an existing target by its own path, anything else by both paths.
private static Exception/*!*/ WindowsLinkError(int win32Error, string/*!*/ strOld, string/*!*/ strNew) {
int errno = Win32ErrorToErrno(win32Error);
return Posix.Error(errno, errno == Posix.EEXIST ? strNew : "(" + strOld + ", " + strNew + ")");
}
///
/// win32.c's symlink(): the target with its slashes turned around, a directory link when the
/// target names a directory (relative to the current directory, as there), created without
/// the privilege where Windows allows that - which the BCL asks for itself.
///
private static void WindowsSymLink(string/*!*/ strOld, string/*!*/ strNew) {
if (strNew.Length == 0) {
throw Posix.Error(Posix.ENOENT, "(" + strOld + ", " + strNew + ")");
}
if (strOld.Length == 0) {
throw Posix.Error(Posix.EINVAL, "(" + strOld + ", " + strNew + ")");
}
string target = strOld.Replace('/', '\\');
try {
if (Directory.Exists(target)) {
Directory.CreateSymbolicLink(strNew, target);
} else {
File.CreateSymbolicLink(strNew, target);
}
} catch (UnauthorizedAccessException) {
throw WindowsLinkError(5, strOld, strNew);
} catch (IOException e) {
throw WindowsLinkError(e.HResult & 0xFFFF, strOld, strNew);
}
}
///
/// win32.c's readlink(): the target of a symbolic link or a junction, with backslashes as
/// slashes; EINVAL for anything that is neither, ENOENT for nothing at all.
///
private static string/*!*/ WindowsReadLink(string/*!*/ strPath) {
FileAttributes attributes;
try {
// the attributes of the link itself, which may dangle
attributes = File.GetAttributes(strPath);
} catch (FileNotFoundException) {
throw Posix.Error(Posix.ENOENT, strPath);
} catch (DirectoryNotFoundException) {
throw Posix.Error(Posix.ENOENT, strPath);
} catch (UnauthorizedAccessException) {
throw Posix.Error(Posix.EACCES, strPath);
} catch (IOException e) {
throw Posix.Error(Win32ErrorToErrno(e.HResult & 0xFFFF), strPath);
}
string target = null;
if ((attributes & FileAttributes.ReparsePoint) != 0) {
FileSystemInfo info = (attributes & FileAttributes.Directory) != 0
? (FileSystemInfo)new DirectoryInfo(strPath) : new FileInfo(strPath);
target = info.LinkTarget;
}
if (target == null) {
throw Posix.Error(Posix.EINVAL, strPath);
}
return target.Replace('\\', '/');
}
///
/// flock on Windows, as CRuby's win32.c does it: LockFileEx/UnlockFileEx over the
/// whole file, shared or exclusive. A blocking lock polls with FAIL_IMMEDIATELY for
/// the reason the Posix path above does. RubyGems takes one for every binstub it
/// writes, so without it `gem install` of any gem with an executable failed.
///
[System.Runtime.Versioning.SupportedOSPlatform("windows")]
private static object WindowsFileLock(RubyFile/*!*/ self, int operation) {
var fs = self.GetStream().BaseStream as FileStream;
if (fs == null) {
throw RubyExceptions.CreateEBADF();
}
// Windows locks are mandatory: what is buffered has to reach the file first.
fs.Flush();
var overlapped = new System.Threading.NativeOverlapped();
if ((operation & Posix.LOCK_UN) != 0) {
if (!UnlockFileEx(fs.SafeFileHandle, 0, UInt32.MaxValue, UInt32.MaxValue, ref overlapped)) {
int error = System.Runtime.InteropServices.Marshal.GetLastWin32Error();
// Unlocking a file that is not locked is not an error for flock(2).
if (error != ERROR_NOT_LOCKED) {
throw RubyExceptions.CreateEINVAL("{0}", self.Path);
}
}
return 0;
}
int flags = LOCKFILE_FAIL_IMMEDIATELY;
if ((operation & Posix.LOCK_EX) != 0) {
flags |= LOCKFILE_EXCLUSIVE_LOCK;
} else if ((operation & Posix.LOCK_SH) == 0) {
throw RubyExceptions.CreateEINVAL("{0}", self.Path);
}
while (true) {
overlapped = new System.Threading.NativeOverlapped();
if (LockFileEx(fs.SafeFileHandle, flags, 0, UInt32.MaxValue, UInt32.MaxValue, ref overlapped)) {
return 0;
}
int error = System.Runtime.InteropServices.Marshal.GetLastWin32Error();
if (error != ERROR_LOCK_VIOLATION && error != ERROR_IO_PENDING) {
throw RubyExceptions.CreateEINVAL("{0}", self.Path);
}
if ((operation & Posix.LOCK_NB) != 0) {
return false;
}
System.Threading.Thread.Sleep(10);
}
}
[RubyMethod("readlink", RubyMethodAttributes.PublicSingleton, BuildConfig = "FEATURE_FILESYSTEM")]
public static MutableString/*!*/ Readlink(ConversionStorage/*!*/ toPath, RubyClass/*!*/ self, object path) {
string strPath = self.Context.DecodePath(Protocols.CastToPath(toPath, path));
if (!Posix.IsAvailable) {
if (OperatingSystem.IsWindows()) {
return self.Context.EncodePath(WindowsReadLink(strPath));
}
throw new IronRuby.Builtins.NotImplementedError("readlink() function is unimplemented on this machine");
}
int errno;
string target = Posix.ReadLink(strPath, out errno);
if (target == null) {
throw Posix.Error(errno, strPath);
}
return self.Context.EncodePath(target);
}
[RubyMethod("link", RubyMethodAttributes.PublicSingleton, BuildConfig = "FEATURE_FILESYSTEM")]
public static int Link(ConversionStorage/*!*/ toPath, RubyClass/*!*/ self, object oldPath, object newPath) {
string strOld = self.Context.DecodePath(Protocols.CastToPath(toPath, oldPath));
string strNew = self.Context.DecodePath(Protocols.CastToPath(toPath, newPath));
if (!Posix.IsAvailable) {
if (OperatingSystem.IsWindows()) {
// win32.c's link(): CreateHardLinkW, new name first.
if (!CreateHardLinkW(strNew, strOld, IntPtr.Zero)) {
throw WindowsLinkError(Marshal.GetLastWin32Error(), strOld, strNew);
}
return 0;
}
throw new IronRuby.Builtins.NotImplementedError("link() function is unimplemented on this machine");
}
int errno;
if (Posix.Link(strOld, strNew, out errno) != 0) {
// MRI: an existing target is reported by its own path, anything else by both paths.
throw Posix.Error(errno, errno == Posix.EEXIST ? strNew : "(" + strOld + ", " + strNew + ")");
}
return 0;
}
[RubyMethod("symlink", RubyMethodAttributes.PublicSingleton, BuildConfig = "FEATURE_FILESYSTEM")]
public static int SymLink(ConversionStorage/*!*/ toPath, RubyClass/*!*/ self, object oldPath, object newPath) {
string strOld = self.Context.DecodePath(Protocols.CastToPath(toPath, oldPath));
string strNew = self.Context.DecodePath(Protocols.CastToPath(toPath, newPath));
if (!Posix.IsAvailable) {
if (OperatingSystem.IsWindows()) {
WindowsSymLink(strOld, strNew);
return 0;
}
throw new IronRuby.Builtins.NotImplementedError("symlink() function is unimplemented on this machine");
}
int errno;
if (Posix.Symlink(strOld, strNew, out errno) != 0) {
throw Posix.Error(errno, errno == Posix.EEXIST ? strNew : "(" + strOld + ", " + strNew + ")");
}
return 0;
}
// MRI on Windows has no mkfifo: the rb_f_notimplement stub, which #respond_to? denies.
[RubyMethod("mkfifo", RubyMethodAttributes.PublicSingleton, BuildConfig = "FEATURE_FILESYSTEM")]
[RubyNotImplemented(OnWindowsOnly = true)]
public static int MakeFifo(ConversionStorage/*!*/ toPath, RubyClass/*!*/ self, object path,
[DefaultParameterValue(0x1B6 /* 0666 */)]int mode) {
string strPath = self.Context.DecodePath(Protocols.CastToPath(toPath, path));
if (!Posix.IsAvailable) {
throw new IronRuby.Builtins.NotImplementedError("mkfifo() function is unimplemented on this machine");
}
int errno;
if (Posix.MkFifo(strPath, mode, out errno) != 0) {
throw Posix.Error(errno, strPath);
}
return 0;
}
#region realpath, realdirpath
///
/// Resolves ".", ".." and every symbolic link in .
/// When the whole path must exist (realpath);
/// otherwise only everything but the last component must (realdirpath).
///
private static string/*!*/ ResolvePath(RubyContext/*!*/ context, string/*!*/ path, string basedir, bool strict) {
int links = 0;
return ResolvePath(context, path, basedir, strict, ref links);
}
// The symlink budget is shared across the whole resolution, not reset per component:
// a link that points at itself ("a" -> "a") otherwise recurses until the stack dies.
private static string/*!*/ ResolvePath(RubyContext/*!*/ context, string/*!*/ path, string basedir, bool strict, ref int links) {
string absolute = RubyUtils.ExpandPath(context.Platform, path, basedir ?? context.Platform.CurrentDirectory, false);
var components = new List();
foreach (var part in absolute.Split('/')) {
if (part.Length == 0 || part == ".") {
continue;
}
if (part == "..") {
if (components.Count > 0) {
components.RemoveAt(components.Count - 1);
}
continue;
}
components.Add(part);
}
string resolved = "";
for (int i = 0; i < components.Count; i++) {
bool last = i == components.Count - 1;
string candidate = resolved + "/" + components[i];
while (true) {
Posix.StatData data;
int errno;
if (!Posix.TryStat(candidate, false, out data, out errno)) {
if (last && !strict && errno == Posix.ENOENT) {
break;
}
throw Posix.Error(errno, candidate);
}
if (data.FileType != Posix.S_IFLNK) {
if (!last && data.FileType != Posix.S_IFDIR) {
throw Posix.Error(Posix.ENOTDIR, candidate);
}
break;
}
if (++links > 32) {
throw Posix.Error(Posix.ELOOP, candidate);
}
string target = Posix.ReadLink(candidate, out errno);
if (target == null) {
throw Posix.Error(errno, candidate);
}
if (target.StartsWith("/", StringComparison.Ordinal)) {
candidate = ResolvePath(context, target, null, strict || !last, ref links);
break;
}
candidate = ResolvePath(context, target, resolved.Length == 0 ? "/" : resolved, strict || !last, ref links);
break;
}
resolved = candidate;
}
return resolved.Length == 0 ? "/" : resolved;
}
[RubyMethod("realpath", RubyMethodAttributes.PublicSingleton, BuildConfig = "FEATURE_FILESYSTEM")]
public static MutableString/*!*/ RealPath(ConversionStorage/*!*/ toPath, RubyClass/*!*/ self, object path,
[Optional]object basedir) {
return RealPath(toPath, self, path, basedir, true);
}
[RubyMethod("realdirpath", RubyMethodAttributes.PublicSingleton, BuildConfig = "FEATURE_FILESYSTEM")]
public static MutableString/*!*/ RealDirPath(ConversionStorage/*!*/ toPath, RubyClass/*!*/ self, object path,
[Optional]object basedir) {
return RealPath(toPath, self, path, basedir, false);
}
private static MutableString/*!*/ RealPath(ConversionStorage/*!*/ toPath, RubyClass/*!*/ self, object path,
object basedir, bool strict) {
MutableString pathStr = Protocols.CastToPath(toPath, path);
string strPath = self.Context.DecodePath(pathStr);
string strBase = (basedir == Missing.Value || basedir == null)
? null
: self.Context.DecodePath(Protocols.CastToPath(toPath, basedir));
if (!Posix.IsAvailable) {
MutableString expanded = ExpandPath(toPath, self, path, basedir == Missing.Value ? null : basedir);
// No realpath(3) here, so links are not resolved; but what has to exist does, as in
// MRI: the whole path for realpath, all but the last component for realdirpath.
// pry asks Pathname#realpath whether a ./.pryrc is there and expects ENOENT if not.
string full = self.Context.DecodePath(expanded).TrimEnd('/', '\\');
string mustExist = strict ? full : System.IO.Path.GetDirectoryName(full);
if (!String.IsNullOrEmpty(mustExist) && !System.IO.File.Exists(mustExist) && !System.IO.Directory.Exists(mustExist)) {
throw RubyExceptions.CreateENOENT("{0}", strPath);
}
return expanded;
}
string resolved = ResolvePath(self.Context, strPath, strBase, strict);
// MRI's realdirpath converts a non-ASCII result into the argument's encoding, and where
// that cannot be done leaves it in the file system encoding; realpath, which uses
// realpath(3), relabels it either way.
if (!strict && pathStr.Encoding != RubyEncoding.Binary && !IsAsciiString(resolved)) {
return (CanEncode(resolved, pathStr.Encoding)
? MutableString.Create(resolved, pathStr.Encoding)
: self.Context.EncodePath(resolved)).TaintBy(pathStr);
}
return EncodePathLike(self.Context, resolved, pathStr);
}
private static bool CanEncode(string/*!*/ str, RubyEncoding/*!*/ encoding) {
try {
encoding.StrictEncoding.GetByteCount(str);
return true;
} catch (EncoderFallbackException) {
return false;
}
}
private static bool IsAsciiString(string/*!*/ str) {
foreach (char c in str) {
if (c > 0x7F) {
return false;
}
}
return true;
}
#endregion
#endif
#endregion
#region atime, ctime, mtime, utime
#if FEATURE_FILESYSTEM
[RubyMethod("atime", BuildConfig = "FEATURE_FILESYSTEM")]
public static RubyTime AccessTime(RubyContext/*!*/ context, RubyFile/*!*/ self) {
return RubyStatOps.AccessTime(RubyStatOps.Create(self));
}
[RubyMethod("atime", RubyMethodAttributes.PublicSingleton, BuildConfig = "FEATURE_FILESYSTEM")]
public static RubyTime AccessTime(RespondToStorage/*!*/ respondToStorage, CallSiteStorage>/*!*/ toIoStorage,
ConversionStorage/*!*/ toPath, RubyClass/*!*/ self, object path) {
return RubyStatOps.AccessTime(FileTest.Stat(respondToStorage, toIoStorage, toPath, self.Context, path));
}
[RubyMethod("ctime", BuildConfig = "FEATURE_FILESYSTEM")]
public static RubyTime CreateTime(RubyContext/*!*/ context, RubyFile/*!*/ self) {
return RubyStatOps.CreateTime(RubyStatOps.Create(self));
}
[RubyMethod("ctime", RubyMethodAttributes.PublicSingleton, BuildConfig = "FEATURE_FILESYSTEM")]
public static RubyTime CreateTime(RespondToStorage/*!*/ respondToStorage, CallSiteStorage>/*!*/ toIoStorage,
ConversionStorage/*!*/ toPath, RubyClass/*!*/ self, object path) {
return RubyStatOps.CreateTime(FileTest.Stat(respondToStorage, toIoStorage, toPath, self.Context, path));
}
[RubyMethod("mtime", BuildConfig = "FEATURE_FILESYSTEM")]
public static RubyTime ModifiedTime(RubyContext/*!*/ context, RubyFile/*!*/ self) {
return RubyStatOps.ModifiedTime(RubyStatOps.Create(self));
}
[RubyMethod("mtime", RubyMethodAttributes.PublicSingleton, BuildConfig = "FEATURE_FILESYSTEM")]
public static RubyTime ModifiedTime(RespondToStorage/*!*/ respondToStorage, CallSiteStorage>/*!*/ toIoStorage,
ConversionStorage/*!*/ toPath, RubyClass/*!*/ self, object path) {
return RubyStatOps.ModifiedTime(FileTest.Stat(respondToStorage, toIoStorage, toPath, self.Context, path));
}
[RubyMethod("utime", RubyMethodAttributes.PublicSingleton, BuildConfig = "FEATURE_FILESYSTEM")]
public static int UpdateTimes(ConversionStorage/*!*/ toPath, RubyClass/*!*/ self, object accessTime, object modifiedTime,
params object[]/*!*/ paths) {
return UpdateTimes(toPath, self, accessTime, modifiedTime, paths, true);
}
// MRI on Windows has neither lutimes nor utimensat: File.lutime is the rb_f_notimplement
// stub there, which #respond_to? denies. Setting the times through the BCL would set the
// target's, not the link's.
[RubyMethod("lutime", RubyMethodAttributes.PublicSingleton, BuildConfig = "FEATURE_FILESYSTEM")]
[RubyNotImplemented(OnWindowsOnly = true)]
public static int UpdateLinkTimes(ConversionStorage/*!*/ toPath, RubyClass/*!*/ self, object accessTime, object modifiedTime,
params object[]/*!*/ paths) {
if (OperatingSystem.IsWindows()) {
throw new IronRuby.Builtins.NotImplementedError("lutime() function is unimplemented on this machine");
}
return UpdateTimes(toPath, self, accessTime, modifiedTime, paths, false);
}
private static int UpdateTimes(ConversionStorage/*!*/ toPath, RubyClass/*!*/ self, object accessTime, object modifiedTime,
object[]/*!*/ paths, bool followLinks) {
var context = self.Context;
RubyTime atime = MakeTime(context, accessTime);
RubyTime mtime = MakeTime(context, modifiedTime);
foreach (var path in paths) {
string strPath = context.DecodePath(Protocols.CastToPath(toPath, path));
if (Posix.IsAvailable) {
long[] times = new long[4];
SplitTime(atime, out times[0], out times[1]);
SplitTime(mtime, out times[2], out times[3]);
int errno;
if (Posix.UTimes(strPath, times, followLinks, out errno) != 0) {
throw Posix.Error(errno, strPath);
}
continue;
}
FileInfo info = new FileInfo(strPath);
if (!info.Exists) {
throw RubyExceptions.CreateENOENT("No such file or directory - {0}", strPath);
}
info.LastAccessTimeUtc = atime.ToUniversalTime();
info.LastWriteTimeUtc = mtime.ToUniversalTime();
}
return paths.Length;
}
private static void SplitTime(RubyTime/*!*/ time, out long seconds, out long nanoseconds) {
// whole seconds and nanoseconds rather than ticks, which overflow long long before
// the times a file system can store
seconds = time.Seconds;
nanoseconds = time.Nanoseconds;
}
#endif
private static RubyTime MakeTime(RubyContext/*!*/ context, object obj) {
if (obj == null) {
return new RubyTime(DateTime.Now);
} else if (obj is RubyTime) {
return (RubyTime)obj;
} else if (obj is int) {
return new RubyTime(RubyTime.ToLocalTime(RubyTime.Epoch.AddSeconds((int)obj)));
} else if (obj is double) {
return new RubyTime(RubyTime.ToLocalTime(RubyTime.Epoch.AddSeconds((double)obj)));
} else {
string name = context.GetClassOf(obj).Name;
throw RubyExceptions.CreateTypeConversionError(name, "time");
}
}
#endregion
#region ftype, stat, inspect, path, to_path
#if FEATURE_FILESYSTEM
[RubyMethod("ftype", RubyMethodAttributes.PublicSingleton, BuildConfig = "FEATURE_FILESYSTEM")]
public static MutableString FileType(ConversionStorage/*!*/ toPath, RubyClass/*!*/ self, object path) {
// ftype reports on the link itself, not on its target
return RubyStatOps.FileType(RubyStatOps.Create(self.Context, self.Context.DecodePath(Protocols.CastToPath(toPath, path)), false));
}
[RubyMethod("stat", RubyMethodAttributes.PublicSingleton, BuildConfig = "FEATURE_FILESYSTEM")]
public static FileSystemInfo/*!*/ Stat(ConversionStorage/*!*/ toPath, RubyClass/*!*/ self, object path) {
MutableString strPath = Protocols.CastToPath(toPath, path);
string decoded = self.Context.DecodePath(strPath);
try {
return RubyStatOps.Create(self.Context, decoded);
} catch (Exception e) {
// MRI's message carries the path's own bytes, so a binary path that is not valid
// in any encoding still shows up in it (and the message stays binary).
string message = e.Message;
if (strPath.Encoding == RubyEncoding.Binary && !strPath.IsAscii() && message.EndsWith(decoded, StringComparison.Ordinal)) {
var binary = MutableString.CreateBinary(System.Text.Encoding.ASCII.GetBytes(message.Substring(0, message.Length - decoded.Length)));
RubyExceptionData.GetInstance(e).Message = binary.Append(strPath);
}
throw;
}
}
[RubyMethod("lstat", RubyMethodAttributes.PublicSingleton, BuildConfig = "FEATURE_FILESYSTEM")]
public static FileSystemInfo/*!*/ LStat(ConversionStorage/*!*/ toPath, RubyClass/*!*/ self, object path) {
return RubyStatOps.Create(self.Context, self.Context.DecodePath(Protocols.CastToPath(toPath, path)), false);
}
[RubyMethod("lstat", BuildConfig = "FEATURE_FILESYSTEM")]
[RubyMethod("stat", BuildConfig = "FEATURE_FILESYSTEM")]
public static FileSystemInfo Stat(RubyFile/*!*/ self) {
return RubyStatOps.Create(self);
}
[RubyMethod("birthtime", BuildConfig = "FEATURE_FILESYSTEM")]
public static RubyTime BirthTime(RubyContext/*!*/ context, RubyFile/*!*/ self) {
return RubyStatOps.BirthTime(RubyStatOps.Create(self));
}
[RubyMethod("birthtime", RubyMethodAttributes.PublicSingleton, BuildConfig = "FEATURE_FILESYSTEM")]
public static RubyTime BirthTime(ConversionStorage/*!*/ toPath, RubyClass/*!*/ self, object path) {
return RubyStatOps.BirthTime(RubyStatOps.Create(self.Context, Protocols.CastToPath(toPath, path)));
}
[RubyMethod("size", BuildConfig = "FEATURE_FILESYSTEM")]
public static object FileSize(RubyFile/*!*/ self) {
// fstat the descriptor rather than the path: the size of a file that has
// since been unlinked is still readable, and a closed File raises IOError.
self.RequireOpen();
return RubyStatOps.Size(RubyStatOps.Create((RubyIO)self));
}
#endif
[RubyMethod("inspect")]
public static MutableString/*!*/ Inspect(RubyFile/*!*/ self) {
var result = MutableString.CreateMutable(self.Context.GetPathEncoding()).
Append("#<").
Append(self.Context.GetClassOf(self).GetName(self.Context)).
Append(':');
if (self.Path != null) {
result.Append(self.Path).Append(self.Closed ? " (closed)" : "");
} else if (self.Closed) {
// a File made from a descriptor has no path to show, only the descriptor
result.Append("(closed)");
} else {
result.Append("fd ").Append(self.GetFileDescriptor().ToString(CultureInfo.InvariantCulture));
}
return result.Append('>');
}
[RubyMethod("path")]
[RubyMethod("to_path")]
public static MutableString GetPath(RubyFile/*!*/ self) {
self.RequireInitialized();
if (self.Path == null) {
return null;
}
// File#path hands back the path in the encoding it was opened with.
MutableString result = self.Context.EncodePath(self.Path);
if (self.PathEncoding != null) {
result = MutableString.CreateMutable(result.ConvertToString(), self.PathEncoding);
}
return result;
}
#endregion
#region File::Stat
#if FEATURE_FILESYSTEM
///
/// A File::Stat backed by a real statx(2) result. Off Unix (or if libc
/// is unavailable) File::Stat keeps using bare FileInfo/DirectoryInfo
/// and the legacy approximations below.
///
internal sealed class StatInfo : FileSystemInfo {
internal readonly Posix.StatData Data;
// Off Unix there is no statx result to hold, so the FileInfo/DirectoryInfo the
// old implementation used rides along instead. File::Stat is this class either
// way, which is what makes File.stat(...).instance_of?(File::Stat) true.
internal readonly FileSystemInfo Legacy;
// Off Unix: an lstat of a symbolic link (or junction), which the BCL object cannot say
// by itself - File.symlink? and File.lstat(...).symlink? read it.
internal bool IsLink;
internal StatInfo(string/*!*/ path, Posix.StatData/*!*/ data) {
Data = data;
OriginalPath = path;
FullPath = path;
}
internal StatInfo(FileSystemInfo/*!*/ legacy) {
Legacy = legacy;
OriginalPath = legacy.FullName;
FullPath = legacy.FullName;
}
public override bool Exists {
get { return Legacy == null || Legacy.Exists; }
}
public override string/*!*/ Name {
get { return System.IO.Path.GetFileName(FullPath); }
}
public override void Delete() {
if (Legacy != null) {
Legacy.Delete();
} else {
File.Delete(FullPath);
}
}
}
///
/// Stat
///
[RubyClass("Stat", Extends = typeof(StatInfo), Inherits = typeof(object), BuildConfig = "FEATURE_FILESYSTEM"), Includes(typeof(Comparable))]
public class RubyStatOps {
///
/// fstat(2) on any IO, including one that never had a path (a pipe, a socket,
/// or a file opened from a descriptor).
///
internal static FileSystemInfo/*!*/ Create(RubyIO/*!*/ io) {
io.RequireOpen();
// Buffered writes have to reach the descriptor before it is stat'd, and
// fstat needs the real OS handle, not IronRuby's descriptor table index.
try {
io.Flush();
} catch (NotSupportedException) {
// A read-only console stream cannot be flushed and needs no flushing.
}
// Off POSIX (Windows) there is no fstat to call - Posix.TryFStat answers false
// with no errno, which used to surface as EBADF from every File#stat, #lstat and
// #size on an open file, and so from FileUtils.cp. Answer from the path the file
// was opened with instead, as File.stat does there; a file renamed or deleted
// since it was opened cannot be stat'd this way, which is Windows' own limit too.
if (!Posix.IsAvailable) {
var file = io as RubyFile;
if (file != null && file.Path != null) {
return Create(io.Context, file.Path);
}
}
int fd = RubyIO.PrimaryDescriptorOf(io.GetStream().BaseStream);
if (fd < 0) {
// The console streams are not FileStreams, but their descriptors are
// the well-known 0/1/2.
switch (io.ConsoleStreamType) {
case Microsoft.Scripting.Utils.ConsoleStreamType.Input: fd = 0; break;
case Microsoft.Scripting.Utils.ConsoleStreamType.Output: fd = 1; break;
case Microsoft.Scripting.Utils.ConsoleStreamType.ErrorOutput: fd = 2; break;
default: throw RubyExceptions.CreateEBADF();
}
}
Posix.StatData data;
int errno;
if (Posix.TryFStat(fd, out data, out errno)) {
return new StatInfo("", data);
}
throw Posix.Error(errno == 0 ? Posix.EBADF : errno, "");
}
internal static FileSystemInfo/*!*/ Create(RubyFile/*!*/ file) {
file.RequireInitialized();
file.RequireOpen();
// fstat the descriptor rather than the path: the file may have been renamed or
// unlinked since it was opened, and MRI still answers for what it holds open.
// The IO overload is the one that knows how to find the real descriptor behind
// IronRuby's own table.
return Create((RubyIO)file);
}
internal static FileSystemInfo/*!*/ Create(RubyContext/*!*/ context, MutableString/*!*/ path) {
return Create(context, context.DecodePath(path));
}
internal static FileSystemInfo/*!*/ Create(RubyContext/*!*/ context, string/*!*/ path) {
return Create(context, path, true);
}
internal static FileSystemInfo/*!*/ Create(RubyContext/*!*/ context, string/*!*/ path, bool followLinks) {
FileSystemInfo fsi;
int errno;
if (TryCreate(context, path, followLinks, out fsi, out errno)) {
return fsi;
}
throw Posix.Error(errno == 0 ? Posix.ENOENT : errno, path);
}
internal static bool TryCreate(RubyContext/*!*/ context, string/*!*/ path, out FileSystemInfo result) {
int errno;
return TryCreate(context, path, true, out result, out errno);
}
internal static bool TryCreate(RubyContext/*!*/ context, string/*!*/ path, bool followLinks, out FileSystemInfo result, out int errno) {
result = null;
errno = 0;
if (Posix.IsAvailable) {
Posix.StatData data;
if (Posix.TryStat(path, followLinks, out data, out errno)) {
result = new StatInfo(path, data);
return true;
}
if (errno == 0) {
errno = Posix.ENOENT;
}
return false;
}
PlatformAdaptationLayer pal = context.Platform;
if (!followLinks) {
// lstat: a link answers for itself, dangling or not, as win32.c's does
FileSystemInfo link = TryGetLink(path);
if (link != null) {
result = new StatInfo(link) { IsLink = true };
return true;
}
}
if (pal.FileExists(path)) {
result = new StatInfo(new FileInfo(path));
} else if (pal.DirectoryExists(path)) {
result = new StatInfo(new DirectoryInfo(path));
} else if (path.ToUpperInvariant().Equals(NUL_VALUE)) {
result = new StatInfo(new DeviceInfo(NUL_VALUE));
} else {
errno = Posix.ENOENT;
return false;
}
return true;
}
// The link at path itself if it is a symbolic link or a junction; null otherwise.
private static FileSystemInfo TryGetLink(string/*!*/ path) {
try {
FileAttributes attributes = File.GetAttributes(path);
if ((attributes & FileAttributes.ReparsePoint) == 0) {
return null;
}
FileSystemInfo info = (attributes & FileAttributes.Directory) != 0
? (FileSystemInfo)new DirectoryInfo(path) : new FileInfo(path);
return info.LinkTarget != null ? info : null;
} catch (Exception e) when (e is IOException || e is UnauthorizedAccessException || e is ArgumentException || e is NotSupportedException) {
return null;
}
}
private static Posix.StatData D(FileSystemInfo/*!*/ self) {
var si = self as StatInfo;
return si != null ? si.Data : null;
}
/// The System.IO object behind an off-Unix File::Stat, if there is one.
private static FileSystemInfo L(FileSystemInfo self) {
var si = self as StatInfo;
return si != null ? si.Legacy : self;
}
[RubyConstructor]
public static FileSystemInfo/*!*/ Create(ConversionStorage/*!*/ toPath, RubyClass/*!*/ self, object path) {
return Create(self.Context, Protocols.CastToPath(toPath, path));
}
[RubyMethod("<=>")]
public static int Compare(FileSystemInfo/*!*/ self, [NotNull]FileSystemInfo/*!*/ other) {
var a = D(self);
var b = D(other);
if (a != null && b != null) {
int c = a.MTimeSec.CompareTo(b.MTimeSec);
return c != 0 ? c : a.MTimeNsec.CompareTo(b.MTimeNsec);
}
return self.LastWriteTime.CompareTo(other.LastWriteTime);
}
[RubyMethod("<=>")]
public static object Compare(FileSystemInfo/*!*/ self, object other) {
Debug.Assert(other as FileSystemInfo == null);
return null;
}
internal static RubyTime/*!*/ MakeTime(long seconds, long nanoseconds) {
// exact, as Time.at is: a file time can lie beyond DateTime's year 9999
var exact = ExactNum.FromInteger(seconds) +
ExactNum.FromInteger(nanoseconds) / ExactNum.FromInteger(RubyTime.NanosecondsPerSecond);
return RubyTime.FromExactSeconds(exact, RubyTimeZoneKind.Local, ExactNum.Zero);
}
[RubyMethod("atime")]
public static RubyTime/*!*/ AccessTime(FileSystemInfo/*!*/ self) {
var d = D(self);
return d != null ? MakeTime(d.ATimeSec, d.ATimeNsec) : new RubyTime(self.LastAccessTime);
}
[RubyMethod("mtime")]
public static RubyTime/*!*/ ModifiedTime(FileSystemInfo/*!*/ self) {
var d = D(self);
return d != null ? MakeTime(d.MTimeSec, d.MTimeNsec) : new RubyTime(self.LastWriteTime);
}
[RubyMethod("ctime")]
public static RubyTime/*!*/ CreateTime(FileSystemInfo/*!*/ self) {
var d = D(self);
return d != null ? MakeTime(d.CTimeSec, d.CTimeNsec) : new RubyTime(self.CreationTime);
}
[RubyMethod("birthtime")]
public static RubyTime/*!*/ BirthTime(FileSystemInfo/*!*/ self) {
var d = D(self);
if (d == null) {
return new RubyTime(self.CreationTime);
}
if (!d.HasBirthTime) {
// MRI says "birthtime() function is unimplemented" when the OS has no
// statx at all, and "birthtime is unimplemented" when statx answered but
// the filesystem does not track it. We only reach here in the latter case.
throw new IronRuby.Builtins.NotImplementedError("birthtime is unimplemented on this filesystem");
}
return MakeTime(d.BTimeSec, d.BTimeNsec);
}
[RubyMethod("blksize")]
public static object BlockSize(FileSystemInfo/*!*/ self) {
var d = D(self);
return d != null ? (object)d.BlockSize : null;
}
[RubyMethod("blocks")]
public static object Blocks(FileSystemInfo/*!*/ self) {
var d = D(self);
return d != null ? (object)Protocols.Normalize(d.Blocks) : null;
}
[RubyMethod("blockdev?")]
public static bool IsBlockDevice(FileSystemInfo/*!*/ self) {
var d = D(self);
return d != null && d.FileType == Posix.S_IFBLK;
}
[RubyMethod("chardev?")]
public static bool IsCharDevice(FileSystemInfo/*!*/ self) {
var d = D(self);
return d != null && d.FileType == Posix.S_IFCHR;
}
[RubyMethod("dev")]
public static object DeviceId(FileSystemInfo/*!*/ self) {
var d = D(self);
return d != null ? Protocols.Normalize(d.Dev) : (object)3;
}
[RubyMethod("rdev")]
public static object RDeviceId(FileSystemInfo/*!*/ self) {
var d = D(self);
return d != null ? Protocols.Normalize(d.Rdev) : (object)3;
}
[RubyMethod("dev_major")]
public static object DeviceIdMajor(FileSystemInfo/*!*/ self) {
var d = D(self);
return d != null ? (object)d.DevMajor : null;
}
[RubyMethod("dev_minor")]
public static object DeviceIdMinor(FileSystemInfo/*!*/ self) {
var d = D(self);
return d != null ? (object)d.DevMinor : null;
}
[RubyMethod("rdev_major")]
public static object RDeviceIdMajor(FileSystemInfo/*!*/ self) {
var d = D(self);
return d != null ? (object)d.RdevMajor : null;
}
[RubyMethod("rdev_minor")]
public static object RDeviceIdMinor(FileSystemInfo/*!*/ self) {
var d = D(self);
return d != null ? (object)d.RdevMinor : null;
}
[RubyMethod("directory?")]
public static bool IsDirectory(FileSystemInfo/*!*/ self) {
var d = D(self);
return d != null ? d.FileType == Posix.S_IFDIR : (L(self) is DirectoryInfo);
}
[RubyMethod("file?")]
public static bool IsFile(FileSystemInfo/*!*/ self) {
var d = D(self);
return d != null ? d.FileType == Posix.S_IFREG : (L(self) is FileInfo);
}
[RubyMethod("pipe?")]
public static bool IsPipe(FileSystemInfo/*!*/ self) {
var d = D(self);
return d != null && d.FileType == Posix.S_IFIFO;
}
[RubyMethod("socket?")]
public static bool IsSocket(FileSystemInfo/*!*/ self) {
var d = D(self);
return d != null && d.FileType == Posix.S_IFSOCK;
}
[RubyMethod("symlink?")]
public static bool IsSymLink(FileSystemInfo/*!*/ self) {
var d = D(self);
if (d == null) {
var si = self as StatInfo;
return si != null && si.IsLink;
}
return d.FileType == Posix.S_IFLNK;
}
[RubyMethod("ftype")]
public static MutableString/*!*/ FileType(FileSystemInfo/*!*/ self) {
var d = D(self);
if (d == null) {
var si = self as StatInfo;
if (si != null && si.IsLink) {
return MutableString.CreateAscii("link");
}
return MutableString.CreateAscii(IsFile(self) ? "file" : "directory");
}
switch (d.FileType) {
case Posix.S_IFREG: return MutableString.CreateAscii("file");
case Posix.S_IFDIR: return MutableString.CreateAscii("directory");
case Posix.S_IFCHR: return MutableString.CreateAscii("characterSpecial");
case Posix.S_IFBLK: return MutableString.CreateAscii("blockSpecial");
case Posix.S_IFIFO: return MutableString.CreateAscii("fifo");
case Posix.S_IFLNK: return MutableString.CreateAscii("link");
case Posix.S_IFSOCK: return MutableString.CreateAscii("socket");
default: return MutableString.CreateAscii("unknown");
}
}
#region permission predicates
private static bool ModeAccess(Posix.StatData/*!*/ d, int bit, bool real) {
int uid = real ? Posix.GetUid() : Posix.GetEUid();
if (uid == 0) {
// root bypasses read/write; execute still needs some x bit
return bit != Posix.X_OK || (d.Mode & 0x49 /* 0111 */) != 0;
}
int shift;
if (d.Uid == uid) {
shift = 6;
} else if (d.Gid == (real ? Posix.GetGid() : Posix.GetEGid()) || Array.IndexOf(Posix.GetGroups(), d.Gid) >= 0) {
shift = 3;
} else {
shift = 0;
}
return (d.Mode & (bit << shift)) != 0;
}
[RubyMethod("executable?")]
public static bool IsExecutable(FileSystemInfo/*!*/ self) {
var d = D(self);
if (d != null) {
return ModeAccess(d, Posix.X_OK, false);
}
if (System.IO.Path.DirectorySeparatorChar == '/') {
var mode = self.UnixFileMode;
return (mode & (UnixFileMode.UserExecute | UnixFileMode.GroupExecute | UnixFileMode.OtherExecute)) != 0;
}
return IsWindowsExecutableExtension(self.Extension);
}
///
/// Windows has no execute bit: what makes a file runnable is its extension being
/// in %PATHEXT%, and that is what MRI's rb_file_executable_p looks at there.
/// Accepting only ".exe" said no to every .bat and .cmd - including the ir.cmd
/// that mspec is pointed at as RUBY_EXE, which it then silently replaced with a
/// command line reconstructed from RbConfig.
///
private static bool IsWindowsExecutableExtension(string extension) {
if (String.IsNullOrEmpty(extension)) {
return false;
}
string pathExt = Environment.GetEnvironmentVariable("PATHEXT") ?? ".COM;.EXE;.BAT;.CMD";
foreach (string candidate in pathExt.Split(';')) {
if (candidate.Length != 0 && extension.Equals(candidate, StringComparison.OrdinalIgnoreCase)) {
return true;
}
}
return false;
}
[RubyMethod("executable_real?")]
public static bool IsExecutableReal(FileSystemInfo/*!*/ self) {
var d = D(self);
return d != null ? ModeAccess(d, Posix.X_OK, true) : IsExecutable(self);
}
[RubyMethod("readable?")]
public static bool IsReadable(FileSystemInfo/*!*/ self) {
var d = D(self);
return d != null ? ModeAccess(d, Posix.R_OK, false) : true;
}
[RubyMethod("readable_real?")]
public static bool IsReadableReal(FileSystemInfo/*!*/ self) {
var d = D(self);
return d != null ? ModeAccess(d, Posix.R_OK, true) : true;
}
[RubyMethod("writable?")]
public static bool IsWritable(FileSystemInfo/*!*/ self) {
var d = D(self);
return d != null ? ModeAccess(d, Posix.W_OK, false) : ((self.Attributes & FileAttributes.ReadOnly) == 0);
}
[RubyMethod("writable_real?")]
public static bool IsWritableReal(FileSystemInfo/*!*/ self) {
var d = D(self);
return d != null ? ModeAccess(d, Posix.W_OK, true) : IsWritable(self);
}
[RubyMethod("world_readable?")]
public static object IsWorldReadable(FileSystemInfo/*!*/ self) {
var d = D(self);
if (d == null || (d.Mode & 04) == 0) {
return null;
}
return d.Mode & 0xFFF /* 07777 */;
}
[RubyMethod("world_writable?")]
public static object IsWorldWritable(FileSystemInfo/*!*/ self) {
var d = D(self);
if (d == null || (d.Mode & 02) == 0) {
return null;
}
return d.Mode & 0xFFF /* 07777 */;
}
[RubyMethod("owned?")]
public static bool IsUserOwned(FileSystemInfo/*!*/ self) {
var d = D(self);
return d != null ? d.Uid == Posix.GetEUid() : true;
}
/// Owned by the real uid rather than the effective one - what `test ?O` asks.
public static bool IsUserOwnedReal(FileSystemInfo/*!*/ self) {
var d = D(self);
return d != null ? d.Uid == Posix.GetUid() : true;
}
[RubyMethod("grpowned?")]
public static bool IsGroupOwned(FileSystemInfo/*!*/ self) {
var d = D(self);
if (d == null) {
return false;
}
return d.Gid == Posix.GetEGid() || Array.IndexOf(Posix.GetGroups(), d.Gid) >= 0;
}
[RubyMethod("setgid?")]
public static bool IsSetGid(FileSystemInfo/*!*/ self) {
var d = D(self);
return d != null && (d.Mode & Posix.S_ISGID) != 0;
}
[RubyMethod("setuid?")]
public static bool IsSetUid(FileSystemInfo/*!*/ self) {
var d = D(self);
return d != null && (d.Mode & Posix.S_ISUID) != 0;
}
[RubyMethod("sticky?")]
public static object IsSticky(FileSystemInfo/*!*/ self) {
var d = D(self);
return d != null ? (object)((d.Mode & Posix.S_ISVTX) != 0) : null;
}
#endregion
[RubyMethod("identical?")]
public static bool AreIdentical(RubyContext/*!*/ context, FileSystemInfo/*!*/ self, [NotNull]FileSystemInfo/*!*/ other) {
var a = D(self);
var b = D(other);
if (a != null && b != null) {
return a.Dev == b.Dev && a.Ino == b.Ino;
}
return self.Exists && other.Exists && context.Platform.PathComparer.Compare(self.FullName, other.FullName) == 0;
}
[RubyMethod("gid")]
public static int GroupId(FileSystemInfo/*!*/ self) {
var d = D(self);
return d != null ? d.Gid : 0;
}
[RubyMethod("uid")]
public static int UserId(FileSystemInfo/*!*/ self) {
var d = D(self);
return d != null ? d.Uid : 0;
}
[RubyMethod("ino")]
public static object Inode(FileSystemInfo/*!*/ self) {
var d = D(self);
return d != null ? Protocols.Normalize(d.Ino) : (object)0;
}
[RubyMethod("nlink")]
public static int NumberOfLinks(FileSystemInfo/*!*/ self) {
var d = D(self);
return d != null ? d.Nlink : 1;
}
[RubyMethod("mode")]
public static int Mode(FileSystemInfo/*!*/ self) {
var d = D(self);
if (d != null) {
return d.Mode;
}
int mode = (L(self) is FileInfo) ? 0x8000 : 0x4000;
mode |= 0x100; // S_IREAD;
if ((L(self).Attributes & FileAttributes.ReadOnly) == 0) {
mode |= 0x80; // S_IWRITE;
}
return mode;
}
[RubyMethod("size")]
public static object Size(FileSystemInfo/*!*/ self) {
var d = D(self);
if (d != null) {
return Protocols.Normalize(d.Size);
}
if (L(self) is DeviceInfo) {
return 0;
}
FileInfo info = (L(self) as FileInfo);
return (info == null) ? 0 : (object)Protocols.Normalize(info.Length);
}
[RubyMethod("size?")]
public static object NullableSize(FileSystemInfo/*!*/ self) {
var d = D(self);
if (d != null) {
return d.Size == 0 ? null : Protocols.Normalize(d.Size);
}
if (L(self) is DeviceInfo) {
return 0;
}
FileInfo info = (L(self) as FileInfo);
if (info == null) {
return null;
}
return (info.Length == 0) ? null : (object)(int)info.Length;
}
[RubyMethod("zero?")]
public static bool IsZeroLength(FileSystemInfo/*!*/ self) {
var d = D(self);
if (d != null) {
return d.FileType != Posix.S_IFDIR && d.Size == 0;
}
if (L(self) is DeviceInfo) {
return true;
}
FileInfo info = (L(self) as FileInfo);
return (info == null) ? false : info.Length == 0;
}
[RubyMethod("inspect")]
public static MutableString/*!*/ Inspect(RubyContext/*!*/ context, FileSystemInfo/*!*/ self) {
return MutableString.CreateAscii(String.Format(CultureInfo.InvariantCulture,
"#",
FormatDev(DeviceId(self)),
context.Inspect(Inode(self)),
FormatMode(Mode(self)),
context.Inspect(NumberOfLinks(self)),
context.Inspect(UserId(self)),
context.Inspect(GroupId(self)),
FormatDev(RDeviceId(self)),
context.Inspect(Size(self)),
context.Inspect(BlockSize(self)),
context.Inspect(Blocks(self)),
context.Inspect(AccessTime(self)),
context.Inspect(ModifiedTime(self)),
context.Inspect(CreateTime(self))
));
}
private static string FormatDev(object dev) {
return dev is int ? "0x" + ((int)dev).ToString("x") : dev.ToString();
}
private static string FormatMode(int mode) {
return "0" + Convert.ToString(mode, 8);
}
internal class DeviceInfo : FileSystemInfo {
private string/*!*/ _name;
internal DeviceInfo(string/*!*/ name) {
_name = name;
}
public override void Delete() {
throw new NotImplementedException();
}
public override bool Exists {
get { return true; }
}
public override string Name {
get { return _name; }
}
}
}
#endif
#endregion
#region FileTest methods (public singletons only)
#if FEATURE_FILESYSTEM
// Which of these take a stream as well as a path is FileTest's business: see FileTest.cs.
[RubyMethod("blockdev?", RubyMethodAttributes.PublicSingleton, BuildConfig = "FEATURE_FILESYSTEM")]
public static bool IsBlockDevice(RespondToStorage/*!*/ respondToStorage, CallSiteStorage>/*!*/ toIoStorage,
ConversionStorage/*!*/ toPath, RubyModule/*!*/ self, object path) {
return FileTest.IsBlockDevice(respondToStorage, toIoStorage, toPath, self, path);
}
[RubyMethod("chardev?", RubyMethodAttributes.PublicSingleton, BuildConfig = "FEATURE_FILESYSTEM")]
public static bool IsCharDevice(RespondToStorage/*!*/ respondToStorage, CallSiteStorage>/*!*/ toIoStorage,
ConversionStorage/*!*/ toPath, RubyModule/*!*/ self, object path) {
return FileTest.IsCharDevice(respondToStorage, toIoStorage, toPath, self, path);
}
[RubyMethod("directory?", RubyMethodAttributes.PublicSingleton, BuildConfig = "FEATURE_FILESYSTEM")]
public static bool IsDirectory(RespondToStorage/*!*/ respondToStorage,
CallSiteStorage>/*!*/ toIoStorage,
ConversionStorage/*!*/ toPath, RubyModule/*!*/ self, object path) {
return FileTest.IsDirectory(respondToStorage, toIoStorage, toPath, self, path);
}
[RubyMethod("executable?", RubyMethodAttributes.PublicSingleton, BuildConfig = "FEATURE_FILESYSTEM")]
public static bool IsExecutable(ConversionStorage/*!*/ toPath, RubyModule/*!*/ self, object path) {
return FileTest.IsExecutable(toPath, self, path);
}
[RubyMethod("executable_real?", RubyMethodAttributes.PublicSingleton, BuildConfig = "FEATURE_FILESYSTEM")]
public static bool IsExecutableReal(ConversionStorage/*!*/ toPath, RubyModule/*!*/ self, object path) {
return FileTest.IsExecutableReal(toPath, self, path);
}
[RubyMethod("exist?", RubyMethodAttributes.PublicSingleton, BuildConfig = "FEATURE_FILESYSTEM")]
// #exists? was deprecated in 2.1 and removed in 3.9.
public static bool Exists(RespondToStorage/*!*/ respondToStorage, CallSiteStorage>/*!*/ toIoStorage,
ConversionStorage/*!*/ toPath, RubyModule/*!*/ self, object path) {
return FileTest.Exists(respondToStorage, toIoStorage, toPath, self, path);
}
[RubyMethod("file?", RubyMethodAttributes.PublicSingleton, BuildConfig = "FEATURE_FILESYSTEM")]
public static bool IsFile(RespondToStorage/*!*/ respondToStorage, CallSiteStorage>/*!*/ toIoStorage,
ConversionStorage/*!*/ toPath, RubyModule/*!*/ self, object path) {
return FileTest.IsFile(respondToStorage, toIoStorage, toPath, self, path);
}
[RubyMethod("grpowned?", RubyMethodAttributes.PublicSingleton, BuildConfig = "FEATURE_FILESYSTEM")]
public static bool IsGroupOwned(RespondToStorage/*!*/ respondToStorage, CallSiteStorage>/*!*/ toIoStorage,
ConversionStorage/*!*/ toPath, RubyModule/*!*/ self, object path) {
return FileTest.IsGroupOwned(respondToStorage, toIoStorage, toPath, self, path);
}
[RubyMethod("identical?", RubyMethodAttributes.PublicSingleton, BuildConfig = "FEATURE_FILESYSTEM")]
public static bool AreIdentical(RespondToStorage/*!*/ respondToStorage, CallSiteStorage>/*!*/ toIoStorage,
ConversionStorage/*!*/ toPath, RubyModule/*!*/ self, object path1, object path2) {
return FileTest.AreIdentical(respondToStorage, toIoStorage, toPath, self, path1, path2);
}
[RubyMethod("owned?", RubyMethodAttributes.PublicSingleton, BuildConfig = "FEATURE_FILESYSTEM")]
public static bool IsUserOwned(RespondToStorage/*!*/ respondToStorage, CallSiteStorage>/*!*/ toIoStorage,
ConversionStorage/*!*/ toPath, RubyModule/*!*/ self, object path) {
return FileTest.IsUserOwned(respondToStorage, toIoStorage, toPath, self, path);
}
[RubyMethod("pipe?", RubyMethodAttributes.PublicSingleton, BuildConfig = "FEATURE_FILESYSTEM")]
public static bool IsPipe(RespondToStorage/*!*/ respondToStorage, CallSiteStorage>/*!*/ toIoStorage,
ConversionStorage/*!*/ toPath, RubyModule/*!*/ self, object path) {
return FileTest.IsPipe(respondToStorage, toIoStorage, toPath, self, path);
}
[RubyMethod("readable?", RubyMethodAttributes.PublicSingleton, BuildConfig = "FEATURE_FILESYSTEM")]
public static bool IsReadable(ConversionStorage/*!*/ toPath, RubyModule/*!*/ self, object path) {
return FileTest.IsReadable(toPath, self, path);
}
[RubyMethod("readable_real?", RubyMethodAttributes.PublicSingleton, BuildConfig = "FEATURE_FILESYSTEM")]
public static bool IsReadableReal(ConversionStorage/*!*/ toPath, RubyModule/*!*/ self, object path) {
return FileTest.IsReadableReal(toPath, self, path);
}
[RubyMethod("setgid?", RubyMethodAttributes.PublicSingleton, BuildConfig = "FEATURE_FILESYSTEM")]
public static bool IsSetGid(RespondToStorage/*!*/ respondToStorage, CallSiteStorage>/*!*/ toIoStorage,
ConversionStorage/*!*/ toPath, RubyModule/*!*/ self, object path) {
return FileTest.IsSetGid(respondToStorage, toIoStorage, toPath, self, path);
}
[RubyMethod("setuid?", RubyMethodAttributes.PublicSingleton, BuildConfig = "FEATURE_FILESYSTEM")]
public static bool IsSetUid(RespondToStorage/*!*/ respondToStorage, CallSiteStorage>/*!*/ toIoStorage,
ConversionStorage/*!*/ toPath, RubyModule/*!*/ self, object path) {
return FileTest.IsSetUid(respondToStorage, toIoStorage, toPath, self, path);
}
[RubyMethod("size", RubyMethodAttributes.PublicSingleton, BuildConfig = "FEATURE_FILESYSTEM")]
public static object Size(RespondToStorage/*!*/ respondToStorage, CallSiteStorage>/*!*/ toIoStorage,
ConversionStorage/*!*/ toPath, RubyModule/*!*/ self, object path) {
return FileTest.Size(respondToStorage, toIoStorage, toPath, self, path);
}
[RubyMethod("size?", RubyMethodAttributes.PublicSingleton, BuildConfig = "FEATURE_FILESYSTEM")]
public static object NullableSize(RespondToStorage/*!*/ respondToStorage, CallSiteStorage>/*!*/ toIoStorage,
ConversionStorage/*!*/ toPath, RubyModule/*!*/ self, object path) {
return FileTest.NullableSize(respondToStorage, toIoStorage, toPath, self, path);
}
[RubyMethod("socket?", RubyMethodAttributes.PublicSingleton, BuildConfig = "FEATURE_FILESYSTEM")]
public static bool IsSocket(RespondToStorage/*!*/ respondToStorage, CallSiteStorage>/*!*/ toIoStorage,
ConversionStorage/*!*/ toPath, RubyModule/*!*/ self, object path) {
return FileTest.IsSocket(respondToStorage, toIoStorage, toPath, self, path);
}
[RubyMethod("sticky?", RubyMethodAttributes.PublicSingleton, BuildConfig = "FEATURE_FILESYSTEM")]
public static object IsSticky(RespondToStorage/*!*/ respondToStorage, CallSiteStorage>/*!*/ toIoStorage,
ConversionStorage/*!*/ toPath, RubyModule/*!*/ self, object path) {
return FileTest.IsSticky(respondToStorage, toIoStorage, toPath, self, path);
}
[RubyMethod("symlink?", RubyMethodAttributes.PublicSingleton, BuildConfig = "FEATURE_FILESYSTEM")]
public static bool IsSymLink(ConversionStorage/*!*/ toPath, RubyModule/*!*/ self, object path) {
return FileTest.IsSymLink(toPath, self, path);
}
[RubyMethod("writable?", RubyMethodAttributes.PublicSingleton, BuildConfig = "FEATURE_FILESYSTEM")]
public static bool IsWritable(ConversionStorage/*!*/ toPath, RubyModule/*!*/ self, object path) {
return FileTest.IsWritable(toPath, self, path);
}
[RubyMethod("writable_real?", RubyMethodAttributes.PublicSingleton, BuildConfig = "FEATURE_FILESYSTEM")]
public static bool IsWritableReal(ConversionStorage/*!*/ toPath, RubyModule/*!*/ self, object path) {
return FileTest.IsWritableReal(toPath, self, path);
}
[RubyMethod("world_readable?", RubyMethodAttributes.PublicSingleton, BuildConfig = "FEATURE_FILESYSTEM")]
public static object IsWorldReadable(RespondToStorage/*!*/ respondToStorage, CallSiteStorage>/*!*/ toIoStorage,
ConversionStorage/*!*/ toPath, RubyModule/*!*/ self, object path) {
return FileTest.IsWorldReadable(respondToStorage, toIoStorage, toPath, self, path);
}
[RubyMethod("world_writable?", RubyMethodAttributes.PublicSingleton, BuildConfig = "FEATURE_FILESYSTEM")]
public static object IsWorldWritable(RespondToStorage/*!*/ respondToStorage, CallSiteStorage>/*!*/ toIoStorage,
ConversionStorage/*!*/ toPath, RubyModule/*!*/ self, object path) {
return FileTest.IsWorldWritable(respondToStorage, toIoStorage, toPath, self, path);
}
[RubyMethod("empty?", RubyMethodAttributes.PublicSingleton, BuildConfig = "FEATURE_FILESYSTEM")]
[RubyMethod("zero?", RubyMethodAttributes.PublicSingleton, BuildConfig = "FEATURE_FILESYSTEM")]
public static bool IsZeroLength(RespondToStorage/*!*/ respondToStorage, CallSiteStorage>/*!*/ toIoStorage,
ConversionStorage/*!*/ toPath, RubyModule/*!*/ self, object path) {
return FileTest.IsZeroLength(respondToStorage, toIoStorage, toPath, self, path);
}
#endif
#endregion
}
}