diff --git a/Src/IronRuby.Tests/RubyTests.cs b/Src/IronRuby.Tests/RubyTests.cs index d04fb77a5..71564ec72 100644 --- a/Src/IronRuby.Tests/RubyTests.cs +++ b/Src/IronRuby.Tests/RubyTests.cs @@ -857,6 +857,12 @@ public Tests(Driver/*!*/ driver) { File_StatPredicatesToIo2, IO_SysOpenArguments1, IO_SysOpenPermission1, + String_BrokenPattern1, + Thread_StopAlone1, + Thread_HostMainThread1, + Thread_HostMainThread2, + Windows_NotImplementedAndLinks1, + Windows_SymLinks1, Range_IntegerFastPaths1, Io_BuiltinReaders1, Io_LineReaders2, diff --git a/Src/IronRuby.Tests/Runtime/MiscTests.cs b/Src/IronRuby.Tests/Runtime/MiscTests.cs index d17a92833..f95a30f2f 100644 --- a/Src/IronRuby.Tests/Runtime/MiscTests.cs +++ b/Src/IronRuby.Tests/Runtime/MiscTests.cs @@ -2808,6 +2808,308 @@ public void IO_SysOpenPermission1() { "); } + /// + /// A String pattern whose bytes are not valid in its encoding, as MRI treats it: #sub, #gsub, their + /// bang forms and #scan raise Regexp.new's RegexpError (string.c's get_pat_quoted), where IronRuby said + /// ArgumentError or, with a valid receiver, matched nothing; the searches - #index, #rindex, #byteindex, + /// #byterindex, #[], #slice, #slice!, #[]=, #partition, #rpartition - find nothing (rb_strseq_index), + /// where IronRuby found the bytes. + /// + public void String_BrokenPattern1() { + TestOutput(@" +require 'ruby4' +def show(l); v = begin; yield; rescue Exception => e; ""#{e.class}: #{e.message}""; end; puts ""#{l}: #{v.inspect}""; end +s = ""a\xffb\xffc""; pat = ""\xff"" +show(""Regexp.new"") { Regexp.new(pat) } +show(""sub"") { s.sub(pat, ""Z"") } +show(""sub!"") { s.dup.sub!(pat, ""Z"") } +show(""sub blk"") { s.sub(pat) { ""Z"" } } +show(""gsub"") { s.gsub(pat, ""Z"") } +show(""gsub!"") { s.dup.gsub!(pat) { ""Z"" } } +show(""gsub enum"") { s.gsub(pat).to_a } +show(""gsub enum is lazy"") { [""abc"".gsub(pat).class, ""abc"".dup.gsub!(pat).class] } +show(""gsub hash"") { s.gsub(pat, {}) } +show(""scan"") { s.scan(pat) } +show(""scan blk"") { s.scan(pat) { } } +show(""split"") { s.split(pat) } +show(""index"") { s.index(pat) } +show(""include?"") { s.include?(pat) } +show(""start_with?"") { s.start_with?(pat) } +show(""partition"") { s.partition(pat) } +show(""count"") { s.count(pat) } +show(""binary ok"") { ""a\xffb"".b.gsub(""\xff"".b, ""Z"") } +show(""other enc broken"") { ""a\xffb"".force_encoding(""EUC-JP"").gsub(""\xff"".force_encoding(""EUC-JP""), ""Z"") } +show(""to_str broken"") { o = Object.new; def o.to_str; ""\xff""; end; ""a"".gsub(o, ""Z"") } +show(""valid receiver"") { ""abc"".gsub(""\xff"", ""Z"") } +show(""mid"") { ""x"".sub(""a\xe3b"", ""Z"") } +show(""index"") { s.index(pat) } +show(""index off"") { s.index(pat, 2) } +show(""rindex"") { s.rindex(pat) } +show(""byteindex"") { s.byteindex(pat) } +show(""byterindex"") { s.byterindex(pat) } +show(""include?"") { s.include?(pat) } +show(""[]"") { s[pat] } +show(""slice"") { s.slice(pat) } +show(""slice!"") { t = s.dup; [t.slice!(pat), t] } +show(""[]="") { t = s.dup; t[pat] = ""Z""; t } +show(""partition"") { s.partition(pat) } +show(""rpartition"") { s.rpartition(pat) } +show(""valid in broken"") { s.index(""b"") } +show(""broken mid"") { ""ab\xe3"".index(""\xe3"") } +", @" +Regexp.new: ""RegexpError: invalid multibyte character: /\\xFF/"" +sub: ""RegexpError: invalid multibyte character: /\\xFF/"" +sub!: ""RegexpError: invalid multibyte character: /\\xFF/"" +sub blk: ""RegexpError: invalid multibyte character: /\\xFF/"" +gsub: ""RegexpError: invalid multibyte character: /\\xFF/"" +gsub!: ""RegexpError: invalid multibyte character: /\\xFF/"" +gsub enum: ""RegexpError: invalid multibyte character: /\\xFF/"" +gsub enum is lazy: [Enumerator, Enumerator] +gsub hash: ""RegexpError: invalid multibyte character: /\\xFF/"" +scan: ""RegexpError: invalid multibyte character: /\\xFF/"" +scan blk: ""RegexpError: invalid multibyte character: /\\xFF/"" +split: ""ArgumentError: invalid byte sequence in UTF-8"" +index: nil +include?: false +start_with?: false +partition: [""a\xFFb\xFFc"", """", """"] +count: ""ArgumentError: invalid byte sequence in UTF-8"" +binary ok: ""aZb"" +other enc broken: ""RegexpError: invalid multibyte character: /\\xFF/"" +to_str broken: ""RegexpError: invalid multibyte character: /\\xFF/"" +valid receiver: ""RegexpError: invalid multibyte character: /\\xFF/"" +mid: ""RegexpError: invalid multibyte character: /a\\xE3b/"" +index: nil +index off: nil +rindex: nil +byteindex: nil +byterindex: nil +include?: false +[]: nil +slice: nil +slice!: [nil, ""a\xFFb\xFFc""] +[]=: ""IndexError: string not matched"" +partition: [""a\xFFb\xFFc"", """", """"] +rpartition: ["""", """", ""a\xFFb\xFFc""] +valid in broken: 2 +broken mid: nil +"); + } + + /// + /// A host that creates the runtime on one thread and runs Ruby on another: the creating + /// thread is Thread.main and is listed even though no Ruby code ever ran on it. + /// + public void Thread_HostMainThread1() { + Microsoft.Scripting.Hosting.ScriptEngine engine = null; + var created = new System.Threading.ManualResetEvent(false); + var done = new System.Threading.ManualResetEvent(false); + var creator = new System.Threading.Thread(() => { + engine = Ruby.CreateEngine(); + created.Set(); + done.WaitOne(); + }); + creator.IsBackground = true; + creator.Start(); + try { + Assert(created.WaitOne(30000)); + var result = engine.Execute(@" +[Thread.list.include?(Thread.main), Thread.list.size, Thread.current != Thread.main] +"); + Assert(((RubyArray)result)[0].Equals(true)); + Assert(((RubyArray)result)[1].Equals(2)); + Assert(((RubyArray)result)[2].Equals(true)); + } finally { + done.Set(); + } + } + + /// + /// A runtime created on a thread another runtime's Thread.new made: that thread is the new + /// runtime's Thread.main and is listed there, whoever made it. + /// + public void Thread_HostMainThread2() { + Microsoft.Scripting.Hosting.ScriptEngine engine = null; + var created = new System.Threading.ManualResetEvent(false); + var done = new System.Threading.ManualResetEvent(false); + Action create = () => { + engine = Ruby.CreateEngine(); + created.Set(); + done.WaitOne(); + }; + var scope = Engine.CreateScope(); + scope.SetVariable("create", create); + Engine.Execute("$host_main_thread2 = Thread.new { create.invoke }", scope); + try { + Assert(created.WaitOne(30000)); + var result = (RubyArray)engine.Execute("[Thread.list.include?(Thread.main), Thread.list.size]"); + Assert(result[0].Equals(true)); + Assert(result[1].Equals(2)); + } finally { + done.Set(); + Engine.Execute("$host_main_thread2.join; $host_main_thread2 = nil"); + } + } + + /// + /// Thread.stop with no other thread alive is MRI's ThreadError "stopping only thread" (rb_thread_stop), + /// where it slept forever. A fiber runs on a CLR thread of its own here but is part of the thread that + /// owns it, so neither it nor its thread counts as another thread - for Thread.stop, nor for Thread.list + /// and ThreadGroup#list, which used to list it. + /// + public void Thread_StopAlone1() { + TestOutput(@" +require 'ruby4' +def show(l); v = begin; yield; rescue => e; ""#{e.class}: #{e.message.inspect}""; end; puts ""#{l}: #{v.inspect}""; end +show(""alone"") { Thread.stop } +show(""list"") { Thread.list.size } +show(""in fiber: list"") { Fiber.new { Thread.list.size }.resume } +show(""in fiber: group list"") { Fiber.new { ThreadGroup::Default.list.size }.resume } +show(""in fiber: stop"") { Fiber.new { Thread.stop }.resume } +f = Fiber.new { Fiber.yield }; f.resume +show(""suspended fiber: list"") { [Thread.list.size, Thread.list == [Thread.main]] } +# bounded, so that a Thread.stop that raised where it should sleep fails the test instead of hanging it +def wait_for(seconds = 10); deadline = Time.now + seconds; Thread.pass until yield || Time.now > deadline; end +show(""another thread stops"") { t = Thread.new { Thread.stop; :woke }; begin; wait_for { !t.alive? || t.status == ""sleep"" }; t.run if t.alive?; t.value; ensure; t.kill; end } +show(""main stops, another wakes it"") { t = Thread.new { wait_for { Thread.main.status == ""sleep"" }; Thread.main.wakeup }; begin; Thread.stop; t.join; :ok; ensure; t.kill; end } +show(""fiber of another thread"") { Thread.new { Fiber.new { Thread.list.size }.resume }.value } +f.resume +", @" +alone: ""ThreadError: \""stopping only thread\\n\\tnote: use sleep to stop forever\"""" +list: 1 +in fiber: list: 1 +in fiber: group list: 1 +in fiber: stop: ""ThreadError: \""stopping only thread\\n\\tnote: use sleep to stop forever\"""" +suspended fiber: list: [1, true] +another thread stops: :woke +main stops, another wakes it: :ok +fiber of another thread: 2 +"); + } + + /// + /// What MRI does on Windows with the functions win32/Makefile.sub leaves out, and with the ones + /// win32.c supplies. Dir.fchdir, Dir.for_fd, Dir#fileno, Dir.chroot, File.lutime, File.mkfifo and the + /// process-group and set-id parts of Process are rb_f_notimplement stubs there: NotImplementedError, + /// and #respond_to? false. Dir's threw CLR NotImplementedException or InvalidOperationException, and + /// all of them answered #respond_to? with true. File.link and File.readlink work there, + /// through CreateHardLinkW and the reparse point; they raised NotImplementedError. File.symlink's + /// cases are Windows_SymLinks1, which needs the right to make symbolic links. Windows only. The + /// expected output is MRI's as its win32 sources give it, not a recorded run. + /// + public void Windows_NotImplementedAndLinks1() { + if (Environment.OSVersion.Platform != PlatformID.Win32NT) { + return; + } + TestOutput(@" +require 'ruby4' +require 'tmpdir' +require 'fileutils' +def show(l); v = begin; yield; rescue Exception => e; e.is_a?(SystemCallError) ? e.class : ""#{e.class}: #{e.message}""; end; puts ""#{l}: #{v.inspect}""; end +show(""stubs respond_to?"") { [Dir.respond_to?(:fchdir), Dir.respond_to?(:for_fd), Dir.respond_to?(:chroot), Dir.open(""."") { |x| x.respond_to?(:fileno) }, File.respond_to?(:lutime), File.respond_to?(:mkfifo), Process.respond_to?(:getpgid), Process.respond_to?(:setsid), Process.respond_to?(:groups), Process.respond_to?(:uid=)] } +show(""links respond_to?"") { [File.respond_to?(:link), File.respond_to?(:symlink), File.respond_to?(:readlink)] } +show(""Dir.fchdir"") { Dir.fchdir(0) } +show(""Dir.for_fd"") { Dir.for_fd(0) } +show(""Dir#fileno"") { Dir.open(""."") { |x| x.fileno } } +show(""Dir.chroot"") { Dir.chroot(""."") } +show(""File.lutime"") { File.lutime(nil, nil, ""x"") } +show(""File.mkfifo"") { File.mkfifo(""x"") } +show(""Process.getpgid"") { Process.getpgid(0) } +show(""Process.uid="") { Process.uid = 0 } +dir = Dir.mktmpdir +begin + Dir.chdir(dir) do + File.write(""f"", ""x"") + Dir.mkdir(""sub"") + show(""link"") { File.link(""f"", ""h""); File.read(""h"") } + show(""link existing"") { File.link(""f"", ""h"") } + show(""link missing"") { File.link(""nope"", ""h2"") } + show(""readlink not a link"") { File.readlink(""f"") } + show(""readlink missing"") { File.readlink(""nope"") } + end +ensure + FileUtils.rm_rf(dir) rescue nil +end +", @" +stubs respond_to?: [false, false, false, false, false, false, false, false, false, false] +links respond_to?: [true, true, true] +Dir.fchdir: ""NotImplementedError: fchdir() function is unimplemented on this machine"" +Dir.for_fd: ""NotImplementedError: for_fd() function is unimplemented on this machine"" +Dir#fileno: ""NotImplementedError: fileno() function is unimplemented on this machine"" +Dir.chroot: ""NotImplementedError: chroot() function is unimplemented on this machine"" +File.lutime: ""NotImplementedError: lutime() function is unimplemented on this machine"" +File.mkfifo: ""NotImplementedError: mkfifo() function is unimplemented on this machine"" +Process.getpgid: ""NotImplementedError: getpgid() function is unimplemented on this machine"" +Process.uid=: ""NotImplementedError: uid=() function is unimplemented on this machine"" +link: ""x"" +link existing: Errno::EEXIST +link missing: Errno::ENOENT +readlink not a link: Errno::EINVAL +readlink missing: Errno::ENOENT +"); + } + + /// + /// File.symlink and the File::Stat of a link on Windows, as win32.c has them: CreateSymbolicLinkW + /// with a directory link for a directory target (looked up from the link's directory, as ruby/ruby + /// master does), readlink of the reparse point, an lstat that describes the link itself (S_IFLNK and + /// S_IEXEC, neither file? nor directory?), and a stat that follows it - ENOENT for a dangling one. + /// Windows only, and only where this process may make symbolic links (an administrator, or Developer + /// Mode): elsewhere File.symlink is EACCES, as in MRI, and there is nothing here to check. The + /// expected output is MRI's as its win32 sources give it; the same script prints the same under + /// CRuby and IronRuby on Linux. + /// + public void Windows_SymLinks1() { + if (Environment.OSVersion.Platform != PlatformID.Win32NT || !CanCreateSymbolicLinks()) { + return; + } + TestOutput(@" +require 'ruby4' +require 'tmpdir' +require 'fileutils' +def show(l); v = begin; yield; rescue Exception => e; e.is_a?(SystemCallError) ? e.class : ""#{e.class}: #{e.message}""; end; puts ""#{l}: #{v.inspect}""; end +dir = Dir.mktmpdir +begin + Dir.chdir(dir) do + File.write(""f"", ""x"") + Dir.mkdir(""sub"") + show(""symlink to file"") { File.symlink(""f"", ""s""); [File.symlink?(""s""), File.readlink(""s""), File.read(""s"")] } + show(""symlink to dir"") { File.symlink(""sub"", ""sd""); [File.symlink?(""sd""), File.directory?(""sd""), File.readlink(""sd"")] } + show(""symlink dangling"") { File.symlink(""sub/x"", ""sn""); [File.symlink?(""sn""), File.readlink(""sn"")] } + show(""relative symlink in a subdirectory"") { Dir.mkdir(""d2""); Dir.mkdir(""d2/tgt""); File.symlink(""tgt"", ""d2/ln""); [File.symlink?(""d2/ln""), File.directory?(""d2/ln""), File.readlink(""d2/ln"")] } + show(""lstat and stat"") { [File.lstat(""s"").symlink?, File.stat(""s"").symlink?, File.ftype(""s""), File.lstat(""sd"").symlink?, File.ftype(""sn"")] } + show(""lstat of a link"") { st = File.lstat(""s""); [st.file?, st.directory?, st.symlink?, st.mode & 0170000 == 0120000, st.mode & 0100 != 0] } + show(""stat follows the link"") { [File.stat(""s"").file?, File.size(""s""), File.stat(""sd"").directory?] } + show(""stat of a dangling link"") { File.stat(""sn"") } + show(""symlink existing"") { File.symlink(""f"", ""s"") } + end +ensure + FileUtils.rm_rf(dir) rescue nil +end +", @" +symlink to file: [true, ""f"", ""x""] +symlink to dir: [true, true, ""sub""] +symlink dangling: [true, ""sub/x""] +relative symlink in a subdirectory: [true, true, ""tgt""] +lstat and stat: [true, false, ""link"", true, ""link""] +lstat of a link: [false, false, true, true, true] +stat follows the link: [true, 1, true] +stat of a dangling link: Errno::ENOENT +symlink existing: Errno::EEXIST +"); + } + + private static bool CanCreateSymbolicLinks() { + string link = System.IO.Path.Combine(System.IO.Path.GetTempPath(), "ir-symlink-probe-" + Guid.NewGuid().ToString("N")); + try { + System.IO.File.CreateSymbolicLink(link, "target"); + System.IO.File.Delete(link); + return true; + } catch (Exception) { + return false; + } + } + /// /// Text mode's universal newline decorator - "\r\n" and a lone "\r" are folded before the /// line is split - and the newline options that are decorators for the way out only diff --git a/Src/Libraries/Builtins/Dir.cs b/Src/Libraries/Builtins/Dir.cs index a5383205d..e88757a70 100644 --- a/Src/Libraries/Builtins/Dir.cs +++ b/Src/Libraries/Builtins/Dir.cs @@ -160,10 +160,17 @@ private static object ChangeDirectory(PlatformAdaptationLayer/*!*/ pal, string/* } } + // On Windows these four are MRI's rb_f_notimplement stubs (no chroot, fchdir, dirfd or + // fdopendir there): NotImplementedError, and #respond_to? denies them. + private static Exception/*!*/ Unimplemented(string/*!*/ function) { + return new NotImplementedError(function + "() function is unimplemented on this machine"); + } + [RubyMethod("chroot", RubyMethodAttributes.PublicSingleton)] + [RubyNotImplemented(OnWindowsOnly = true)] public static int ChangeRoot(ConversionStorage/*!*/ toPath, RubyClass/*!*/ self, object dir) { if (!Posix.IsAvailable) { - throw new InvalidOperationException(); + throw Unimplemented("chroot"); } var path = Protocols.CastToPath(toPath, dir); string strPath = self.Context.DecodePath(path); @@ -180,9 +187,10 @@ public static int ChangeRoot(ConversionStorage/*!*/ toPath, RubyC /// lasts for the block only, and the block's value is the result. /// [RubyMethod("fchdir", RubyMethodAttributes.PublicSingleton)] + [RubyNotImplemented(OnWindowsOnly = true)] public static object ChangeDirectoryToDescriptor(BlockParam block, RubyClass/*!*/ self, [DefaultProtocol]int fd) { if (!Posix.IsAvailable) { - throw new NotImplementedException("fchdir"); + throw Unimplemented("fchdir"); } var pal = self.Context.Platform; @@ -509,15 +517,17 @@ public static void Close(RubyDir/*!*/ self) { /// is opened the first time it is asked for and closed with the Dir. /// [RubyMethod("fileno")] + [RubyNotImplemented(OnWindowsOnly = true)] public static int FileNo(RubyDir/*!*/ self) { self.ThrowIfClosed(); return self.GetFileDescriptor(); } [RubyMethod("for_fd", RubyMethodAttributes.PublicSingleton)] + [RubyNotImplemented(OnWindowsOnly = true)] public static RubyDir/*!*/ ForFileDescriptor(RubyClass/*!*/ self, [DefaultProtocol]int fd) { if (!Posix.IsAvailable) { - throw new NotImplementedException("Dir.for_fd"); + throw Unimplemented("for_fd"); } // There is no fdopendir here, so the descriptor is turned back into the path it @@ -661,7 +671,7 @@ private int GetFileDescriptor() { return _fd; } if (!Posix.IsAvailable) { - throw new NotImplementedException("Dir#fileno"); + throw Unimplemented("fileno"); } string path = ImmediateClass.Context.DecodePath(_dirName); diff --git a/Src/Libraries/Builtins/FileOps.cs b/Src/Libraries/Builtins/FileOps.cs index 3fad83e4e..f37e60e73 100644 --- a/Src/Libraries/Builtins/FileOps.cs +++ b/Src/Libraries/Builtins/FileOps.cs @@ -1196,6 +1196,109 @@ private static extern bool LockFileEx(Microsoft.Win32.SafeHandles.SafeFileHandle 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, created without the privilege where Windows allows that - + /// which the BCL asks for itself. A relative target is looked up from the link's directory, + /// where the link will resolve it, as ruby/ruby master's win32.c does (4.0.0 looked in the + /// current directory, and made a file link to a directory that is not there). + /// + 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('/', '\\'); + string link = strNew.Replace('/', '\\'); + bool independent = (target.Length >= 2 && char.IsAsciiLetter(target[0]) && target[1] == ':') || target[0] == '\\'; + int separator = link.LastIndexOf('\\'); + string probe = (!independent && separator >= 0) ? link.Substring(0, separator + 1) + target : target; + try { + if (Directory.Exists(probe)) { + 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 @@ -1250,6 +1353,9 @@ private static object WindowsFileLock(RubyFile/*!*/ self, int operation) { 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"); } @@ -1266,6 +1372,13 @@ public static int Link(ConversionStorage/*!*/ toPath, RubyClass/* 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"); } @@ -1282,6 +1395,10 @@ public static int SymLink(ConversionStorage/*!*/ toPath, RubyClas 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"); } @@ -1292,7 +1409,9 @@ public static int SymLink(ConversionStorage/*!*/ toPath, RubyClas 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) { @@ -1499,10 +1618,18 @@ public static int UpdateTimes(ConversionStorage/*!*/ toPath, Ruby 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); } @@ -1669,6 +1796,10 @@ internal sealed class StatInfo : FileSystemInfo { // 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; @@ -1800,6 +1931,49 @@ internal static bool TryCreate(RubyContext/*!*/ context, string/*!*/ path, bool } PlatformAdaptationLayer pal = context.Platform; + FileSystemInfo link = TryGetLink(path); + if (link != null) { + if (!followLinks) { + // lstat: a link answers for itself, dangling or not, as win32.c's does + result = new StatInfo(link) { IsLink = true }; + return true; + } + // stat: what the link leads to - the BCL's FileInfo of a link describes the link + // (its length, its times) - and ENOENT for a link that leads nowhere + FileSystemInfo target; + try { + target = link.ResolveLinkTarget(true); + } catch (UnauthorizedAccessException) { + errno = Posix.EACCES; + return false; + } catch (IOException e) { + errno = Win32ErrorToErrno(e.HResult & 0xFFFF); + return false; + } + if (target == null) { + errno = Posix.ENOENT; + return false; + } + // FileSystemInfo.Exists says false for any failure; ask the file system so a + // denied target reports EACCES, not ENOENT + try { + File.GetAttributes(target.FullName); + } catch (UnauthorizedAccessException) { + errno = Posix.EACCES; + return false; + } catch (FileNotFoundException) { + errno = Posix.ENOENT; + return false; + } catch (DirectoryNotFoundException) { + errno = Posix.ENOENT; + return false; + } catch (IOException e) { + errno = Win32ErrorToErrno(e.HResult & 0xFFFF); + return false; + } + result = new StatInfo(target); + return true; + } if (pal.FileExists(path)) { result = new StatInfo(new FileInfo(path)); } else if (pal.DirectoryExists(path)) { @@ -1813,6 +1987,21 @@ internal static bool TryCreate(RubyContext/*!*/ context, string/*!*/ path, bool 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; @@ -1949,13 +2138,13 @@ public static object RDeviceIdMinor(FileSystemInfo/*!*/ self) { [RubyMethod("directory?")] public static bool IsDirectory(FileSystemInfo/*!*/ self) { var d = D(self); - return d != null ? d.FileType == Posix.S_IFDIR : (L(self) is DirectoryInfo); + return d != null ? d.FileType == Posix.S_IFDIR : (!IsLinkStat(self) && 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); + return d != null ? d.FileType == Posix.S_IFREG : (!IsLinkStat(self) && L(self) is FileInfo); } [RubyMethod("pipe?")] @@ -1973,13 +2162,22 @@ public static bool IsSocket(FileSystemInfo/*!*/ self) { [RubyMethod("symlink?")] public static bool IsSymLink(FileSystemInfo/*!*/ self) { var d = D(self); - return d != null && d.FileType == Posix.S_IFLNK; + return d != null ? d.FileType == Posix.S_IFLNK : IsLinkStat(self); + } + + // Off Unix: an lstat of a symbolic link or junction (StatInfo.IsLink). + private static bool IsLinkStat(FileSystemInfo/*!*/ self) { + var si = self as StatInfo; + return si != null && si.IsLink; } [RubyMethod("ftype")] public static MutableString/*!*/ FileType(FileSystemInfo/*!*/ self) { var d = D(self); if (d == null) { + if (IsLinkStat(self)) { + return MutableString.CreateAscii("link"); + } return MutableString.CreateAscii(IsFile(self) ? "file" : "directory"); } switch (d.FileType) { @@ -2176,7 +2374,9 @@ public static int Mode(FileSystemInfo/*!*/ self) { if (d != null) { return d.Mode; } - int mode = (L(self) is FileInfo) ? 0x8000 : 0x4000; + // an lstat of a link describes the link: S_IFLNK, and S_IEXEC as win32.c gives a link, + // not the type of what it points at + int mode = IsLinkStat(self) ? Posix.S_IFLNK | 0x40 : (L(self) is FileInfo) ? 0x8000 : 0x4000; mode |= 0x100; // S_IREAD; if ((L(self).Attributes & FileAttributes.ReadOnly) == 0) { mode |= 0x80; // S_IWRITE; diff --git a/Src/Libraries/Builtins/MutableStringOps.cs b/Src/Libraries/Builtins/MutableStringOps.cs index b0e96aeae..81a942010 100644 --- a/Src/Libraries/Builtins/MutableStringOps.cs +++ b/Src/Libraries/Builtins/MutableStringOps.cs @@ -932,6 +932,11 @@ public static MutableString RemoveSubstringInPlace(MutableString/*!*/ self, [Not // CLR indices of the character representations: a byte-level operation (#start_with?, // #getbyte) may have left either one as bytes. + // rb_str_index: incompatible encodings are an error before anything is searched + self.RequireCompatibleEncoding(searchStr); + if (IsBrokenNeedle(searchStr)) { + return null; + } self.PrepareForCharacterRead(); searchStr.PrepareForCharacterRead(); int index = self.IndexOf(searchStr); @@ -1068,7 +1073,8 @@ public static MutableString GetSubstring(ConversionStorage/*!*/ fixnumCast, [RubyMethod("[]")] [RubyMethod("slice")] public static MutableString GetSubstring(MutableString/*!*/ self, [NotNull]MutableString/*!*/ searchStr) { - return (self.IndexOf(searchStr) != -1) ? searchStr.CloneDerived() : null; + self.RequireCompatibleEncoding(searchStr); + return (!IsBrokenNeedle(searchStr) && self.IndexOf(searchStr) != -1) ? searchStr.CloneDerived() : null; } [RubyMethod("[]")] @@ -1284,9 +1290,10 @@ public static MutableString ReplaceSubstring(MutableString/*!*/ self, [NotNull]MutableString/*!*/ substring, [DefaultProtocol, NotNull]MutableString/*!*/ value) { // see RemoveSubstringInPlace(self, searchStr) + self.RequireCompatibleEncoding(substring); self.PrepareForCharacterRead(); substring.PrepareForCharacterRead(); - int index = self.IndexOf(substring); + int index = IsBrokenNeedle(substring) ? -1 : self.IndexOf(substring); if (index == -1) { throw RubyExceptions.CreateIndexError("string not matched"); } @@ -3124,8 +3131,8 @@ public static object BlockReplaceFirst(ConversionStorage/*!*/ tos object blockResult; MutableString result; - // TODO: - var regex = new RubyRegex(MutableString.CreateMutable(Regex.Escape(matchString.ToString()), matchString.Encoding), RubyRegexOptions.NONE); + // The same literal pattern the String forms without a block get from the Regexp conversion. + var regex = RubyRegex.FromLiteralPattern(matchString); return BlockReplaceFirst(tosConversion, scope, self, block, regex, out blockResult, out result) ? blockResult : (result ?? self.CloneDerived()); } @@ -3137,8 +3144,8 @@ public static object BlockReplaceAll(ConversionStorage/*!*/ tosCo object blockResult; MutableString result; - // TODO: - var regex = new RubyRegex(MutableString.CreateMutable(Regex.Escape(matchString.ToString()), matchString.Encoding), RubyRegexOptions.NONE); + // The same literal pattern the String forms without a block get from the Regexp conversion. + var regex = RubyRegex.FromLiteralPattern(matchString); self.TrackChanges(); object r = BlockReplaceAll(tosConversion, scope, self, block, regex, out blockResult, out result) ? blockResult : (result ?? self.CloneDerived()); @@ -3276,18 +3283,46 @@ public static object ReplaceFirst(RubyScope/*!*/ scope, MutableString/*!*/ self, throw RubyExceptions.CreateArgumentError("wrong number of arguments (given 1, expected 2)"); } + // The pattern is converted - and a String one checked (RubyRegex.FromLiteralPattern) - when + // the Enumerator runs, not when it is made: MRI's str_gsub returns the Enumerator before + // get_pat_quoted, so "abc".gsub("\xFF") is an Enumerator whose #to_a raises. [RubyMethod("gsub")] - public static object ReplaceAll(ConversionStorage/*!*/ tosConversion, RubyScope/*!*/ scope, - MutableString/*!*/ self, [DefaultProtocol, NotNull]RubyRegex/*!*/ pattern) { + // + // The binder may pick these for a call with a block too (an object parameter needs no + // conversion), so they do the work at once when there is one, as MRI does. + public static object ReplaceAll(ConversionStorage/*!*/ tosConversion, ConversionStorage/*!*/ stringTryCast, + RubyScope/*!*/ scope, BlockParam block, MutableString/*!*/ self, object pattern) { + if (block != null) { + return BlockReplaceAll(tosConversion, scope, block, self, ToPattern(stringTryCast, pattern)); + } scope.MarkCaptured(); - return new Enumerator((_, block) => BlockReplaceAll(tosConversion, scope, block, self, pattern)); + return new Enumerator((_, b) => BlockReplaceAll(tosConversion, scope, b, self, ToPattern(stringTryCast, pattern))); } [RubyMethod("gsub!")] - public static object ReplaceAllInPlace(ConversionStorage/*!*/ tosConversion, RubyScope/*!*/ scope, - MutableString/*!*/ self, [DefaultProtocol, NotNull]RubyRegex/*!*/ pattern) { + public static object ReplaceAllInPlace(ConversionStorage/*!*/ tosConversion, ConversionStorage/*!*/ stringTryCast, + RubyScope/*!*/ scope, BlockParam block, MutableString/*!*/ self, object pattern) { + // rb_str_gsub_bang checks before anything else, the Enumerator included + self.RequireNotFrozen(); + if (block != null) { + return BlockReplaceInPlace(tosConversion, scope, block, self, ToPattern(stringTryCast, pattern), true); + } scope.MarkCaptured(); - return new Enumerator((_, block) => BlockReplaceInPlace(tosConversion, scope, block, self, pattern, true)); + return new Enumerator((_, b) => BlockReplaceInPlace(tosConversion, scope, b, self, ToPattern(stringTryCast, pattern), true)); + } + + // MRI's get_pat_quoted: a Regexp as it is, a String (or what #to_str makes one) as a + // literal pattern, anything else Check_Type's TypeError. + private static RubyRegex/*!*/ ToPattern(ConversionStorage/*!*/ stringTryCast, object pattern) { + var regex = pattern as RubyRegex; + if (regex != null) { + return regex; + } + MutableString str = Protocols.TryCastToString(stringTryCast, pattern); + if (str == null) { + throw RubyExceptions.CreateUnexpectedTypeError(stringTryCast.Context, pattern, "Regexp"); + } + return RubyRegex.FromLiteralPattern(str); } #endregion @@ -3295,6 +3330,15 @@ public static object ReplaceAllInPlace(ConversionStorage/*!*/ tos #region index, rindex + /// + /// MRI's rb_strseq_index and rb_str_rindex give up on a needle whose bytes are not valid in + /// its encoding: not found, rather than a search for the bytes. So do #index, #rindex, + /// #byteindex, #byterindex, #include?, #[], #slice, #slice!, #[]=, #partition and #rpartition. + /// + private static bool IsBrokenNeedle(MutableString/*!*/ needle) { + return needle.ContainsInvalidCharacters(); + } + // encoding aware [RubyMethod("index")] public static object Index(MutableString/*!*/ self, @@ -3305,6 +3349,9 @@ public static object Index(MutableString/*!*/ self, } self.RequireCompatibleEncoding(substring); + if (IsBrokenNeedle(substring)) { + return null; + } substring.PrepareForCharacterRead(); int result = self.IndexOf(substring, self.ToClrIndex(start)); @@ -3351,6 +3398,9 @@ public static object LastIndexOf(MutableString/*!*/ self, } self.RequireCompatibleEncoding(substring); + if (IsBrokenNeedle(substring)) { + return null; + } // A substring with a pair cannot occur in a receiver without one, so its CLR length - // no scan of a fresh literal needed - is good enough for the checks below then. int subCharCount = self.KnowsCharIndexIsClrIndex diff --git a/Src/Libraries/Builtins/RubyRegexOps.cs b/Src/Libraries/Builtins/RubyRegexOps.cs index 4719d4b93..3f39d0af3 100644 --- a/Src/Libraries/Builtins/RubyRegexOps.cs +++ b/Src/Libraries/Builtins/RubyRegexOps.cs @@ -96,27 +96,11 @@ internal static bool NormalizeGroupIndex(ref int index, int groupCount) { public static RubyRegex/*!*/ Create(RubyClass/*!*/ self, [DefaultProtocol, NotNull]MutableString/*!*/ pattern, [Optional]object options, [DefaultProtocol, Optional]MutableString encoding) { - // MRI rejects a pattern that is not valid in its own encoding before compiling it. - if (pattern.Encoding != RubyEncoding.Binary && pattern.ContainsInvalidCharacters()) { - throw new RegexpError("invalid multibyte character: /" + EscapeNonAsciiBytes(pattern) + "/"); - } + RubyRegex.RequireValidPattern(pattern); double? timeout = TakeTimeout(self.Context, ref options); return new RubyRegex(pattern, MakeOptions(self.Context, options, encoding)) { Timeout = timeout }; } - // For an error message about bytes that do not decode: ASCII as is, the rest as \xHH. - private static string/*!*/ EscapeNonAsciiBytes(MutableString/*!*/ str) { - var result = new System.Text.StringBuilder(); - foreach (byte b in str.ToByteArray()) { - if (b < 0x80) { - result.Append((char)b); - } else { - result.Append("\\x").Append(b.ToString("X2", System.Globalization.CultureInfo.InvariantCulture)); - } - } - return result.ToString(); - } - [RubyConstructor] public static RubyRegex/*!*/ Create(RubyClass/*!*/ self, DynamicNull pattern, [Optional]object options, [Optional]object encoding) { throw RubyExceptions.CreateImplicitConversionError("nil", "String"); diff --git a/Src/Libraries/Builtins/ThreadGroup.cs b/Src/Libraries/Builtins/ThreadGroup.cs index 79c748167..643deba5e 100644 --- a/Src/Libraries/Builtins/ThreadGroup.cs +++ b/Src/Libraries/Builtins/ThreadGroup.cs @@ -62,8 +62,8 @@ public static bool IsEnclosed([NotNull]ThreadGroup/*!*/ self) { } [RubyMethod("list")] - public static RubyArray/*!*/ List([NotNull]ThreadGroup/*!*/ self) { - ThreadOps.RubyThreadInfo[] threads = ThreadOps.RubyThreadInfo.Threads; + public static RubyArray/*!*/ List(RubyContext/*!*/ context, [NotNull]ThreadGroup/*!*/ self) { + ThreadOps.RubyThreadInfo[] threads = ThreadOps.RubyThreadInfo.GetRubyThreads(context); RubyArray result = new RubyArray(threads.Length); foreach (ThreadOps.RubyThreadInfo threadInfo in threads) { Thread thread = threadInfo.Thread; diff --git a/Src/Libraries/Builtins/ThreadOps.Fiber.cs b/Src/Libraries/Builtins/ThreadOps.Fiber.cs index 74353e6ee..acabbc088 100644 --- a/Src/Libraries/Builtins/ThreadOps.Fiber.cs +++ b/Src/Libraries/Builtins/ThreadOps.Fiber.cs @@ -229,6 +229,11 @@ public object Handle { } } + /// The thread this fiber belongs to, which is Thread.current inside it. + internal Thread/*!*/ Owner { + get { return _owner; } + } + internal FiberCore(RubyContext context, object handle, Thread owner, bool root) { _mailbox = new FiberMailbox(); _owner = owner; @@ -264,6 +269,11 @@ private void Activate() { var info = _threadInfo ?? (_threadInfo = RubyThreadInfo.FromThread(Thread.CurrentThread)); info[_currentKey] = handle; info.FiberExecution = this; + if (!_root) { + // set before it is ever cleared again, so that no moment between two fibers - + // FiberExecution gone, not yet parked - counts this thread as a Ruby thread + info.IsFiberWorker = true; + } RubyUtils.SetFiberOwnerThread(_owner); _mailbox.GetOwnerInfo(_owner).ActiveFiberThread = Thread.CurrentThread; } diff --git a/Src/Libraries/Builtins/ThreadOps.cs b/Src/Libraries/Builtins/ThreadOps.cs index c9cb35d54..2c940243d 100644 --- a/Src/Libraries/Builtins/ThreadOps.cs +++ b/Src/Libraries/Builtins/ThreadOps.cs @@ -207,6 +207,30 @@ internal Thread Thread { internal Exception Exception { get; set; } internal object Result { get; set; } internal bool CreatedFromRuby { get; set; } + + /// + /// The runtime whose Thread.new made this thread; null for a thread Ruby did not create + /// (a host's, the main thread). Each RubyContext is a VM of its own, as far as which + /// threads it has goes. + /// + internal RubyContext Context { + get { + RubyContext context; + return (_context != null && _context.TryGetTarget(out context)) ? context : null; + } + set { _context = (value != null) ? new WeakReference(value) : null; } + } + + // Weak: _mapping keeps every entry for good, and a strong reference here would keep a + // runtime the host has let go of alive for as long as the process runs. + private WeakReference _context; + + /// + /// A CLR thread that has run a fiber of some thread's (ThreadOps.Fiber.cs). It is never a + /// Ruby thread itself, before, between or after its fibers - Thread.current inside a + /// fiber is the thread that owns it - so the Ruby thread lists leave it out for good. + /// + internal volatile bool IsFiberWorker; internal bool ExitRequested { get; set; } internal MutableString Name { get; set; } internal bool ReportOnException { get; set; } @@ -316,6 +340,42 @@ internal static RubyThreadInfo[] Threads { } } + /// + /// The Ruby threads of a runtime, as Thread.list and ThreadGroup#list see them: + /// less the threads of another runtime - two ScriptRuntimes in one + /// process are two VMs, and neither lists the other's threads - and less the CLR threads + /// that run (or have run, or hold suspended) a fiber of a thread's. A fiber + /// has a CLR thread of its own here, but in MRI it is part of the thread that owns it, and + /// Thread.current inside it is that thread. + /// + internal static RubyThreadInfo[]/*!*/ GetRubyThreads(RubyContext/*!*/ context) { + // The runtime's main thread counts even when no Ruby code has run on it yet + // (a host that creates the runtime on one thread and runs Ruby on another). + Thread main = context.MainThread; + if (main != null && main.IsAlive) { + FromThread(main); + } + RubyThreadInfo[] all = Threads; + var result = new List(all.Length); + foreach (RubyThreadInfo info in all) { + if (info.IsFiberWorker) { + continue; + } + // The runtime's main thread is always its own, even when another runtime's Thread.new + // made it; any other thread belongs to the runtime that made it, or, made by none (a + // host's), to the one asking from it now. + RubyContext owner = info.Context; + if (info.Thread != context.MainThread && (owner != null ? owner != context : info.Thread != Thread.CurrentThread)) { + continue; + } + FiberCore fiber = info.FiberExecution; + if (fiber == null || fiber.Owner == info.Thread) { + result.Add(info); + } + } + return result.ToArray(); + } + /// /// We do not use Thread.Sleep here as another thread can call Thread#wakeup/Thread#run. Instead, we use our own /// lock which can be signalled from another thread. @@ -986,10 +1046,10 @@ public static void Critical(RubyContext/*!*/ context, object self, bool value) { // exclusive // fork [RubyMethod("list", RubyMethodAttributes.PublicSingleton)] - public static RubyArray/*!*/ List(object self) { + public static RubyArray/*!*/ List(RubyContext/*!*/ context, object self) { RubyThreadInfo.RegisterCurrentThread(); - RubyThreadInfo[] threads = RubyThreadInfo.Threads; + RubyThreadInfo[] threads = RubyThreadInfo.GetRubyThreads(context); RubyArray result = new RubyArray(threads.Length); foreach (RubyThreadInfo threadInfo in threads) { Thread thread = threadInfo.Thread; @@ -1097,6 +1157,7 @@ private static Action CreateThreadTerminator(RubyContext context) { // #inspect, #priority, #report_on_exception - has to be in place before Start(). RubyThreadInfo info = RubyThreadInfo.FromThread(result); info.CreatedFromRuby = true; + info.Context = context; info.Group = group; info.Priority = creator.Priority; info.Location = GetCallerLocation(context); @@ -1311,6 +1372,12 @@ public static void Yield(object self) { [RubyMethod("stop", RubyMethodAttributes.PublicSingleton)] public static void Stop(RubyContext/*!*/ context, object self) { + // MRI's rb_thread_stop: with no other thread alive nothing could ever wake this one. + // (Kernel#sleep has no such check: a signal can end it.) + RubyThreadInfo.RegisterCurrentThread(); + if (RubyThreadInfo.GetRubyThreads(context).Length <= 1) { + throw new ThreadError("stopping only thread\n\tnote: use sleep to stop forever"); + } if (context.CriticalThread == Thread.CurrentThread) { SetCritical(context, false); } @@ -1319,7 +1386,6 @@ public static void Stop(RubyContext/*!*/ context, object self) { internal static void DoSleep() { RubyThreadInfo.RegisterCurrentThread(); - // TODO: MRI throws an exception if you try to stop the main thread RubyThreadInfo info = RubyThreadInfo.FromThread(Thread.CurrentThread); info.Sleep(); // Waking up is a safe point: a signal that arrived while we slept runs its trap handler diff --git a/Src/Libraries/Initializers.Generated.cs b/Src/Libraries/Initializers.Generated.cs index 39415c633..225323810 100644 --- a/Src/Libraries/Initializers.Generated.cs +++ b/Src/Libraries/Initializers.Generated.cs @@ -8227,18 +8227,18 @@ private static void LoadString_Instance(IronRuby.Builtins.RubyModule/*!*/ module ); DefineLibraryMethod(module, "gsub", 0x51, - 0x00000014U, 0x00000014U, 0x00100060U, 0x00040008U, + 0x00000014U, 0x00000014U, 0x00100060U, 0x00000000U, new Func, IronRuby.Runtime.RubyScope, IronRuby.Runtime.BlockParam, IronRuby.Builtins.MutableString, IronRuby.Builtins.RubyRegex, System.Object>(IronRuby.Builtins.MutableStringOps.BlockReplaceAll), new Func, IronRuby.Runtime.RubyScope, IronRuby.Runtime.BlockParam, IronRuby.Builtins.MutableString, IronRuby.Builtins.MutableString, System.Object>(IronRuby.Builtins.MutableStringOps.BlockReplaceAll), new Func, IronRuby.Runtime.BinaryOpStorage, IronRuby.Runtime.ConversionStorage, IronRuby.Runtime.RubyScope, IronRuby.Builtins.MutableString, IronRuby.Builtins.RubyRegex, System.Object, IronRuby.Builtins.MutableString>(IronRuby.Builtins.MutableStringOps.ReplaceAll), - new Func, IronRuby.Runtime.RubyScope, IronRuby.Builtins.MutableString, IronRuby.Builtins.RubyRegex, System.Object>(IronRuby.Builtins.MutableStringOps.ReplaceAll) + new Func, IronRuby.Runtime.ConversionStorage, IronRuby.Runtime.RubyScope, IronRuby.Runtime.BlockParam, IronRuby.Builtins.MutableString, System.Object, System.Object>(IronRuby.Builtins.MutableStringOps.ReplaceAll) ); DefineLibraryMethod(module, "gsub!", 0x51, - 0x00080014U, 0x00100060U, 0x00040008U, + 0x00080014U, 0x00100060U, 0x00000000U, new Func, IronRuby.Runtime.RubyScope, IronRuby.Runtime.BlockParam, IronRuby.Builtins.MutableString, IronRuby.Builtins.RubyRegex, System.Object>(IronRuby.Builtins.MutableStringOps.BlockReplaceAllInPlace), new Func, IronRuby.Runtime.BinaryOpStorage, IronRuby.Runtime.ConversionStorage, IronRuby.Runtime.RubyScope, IronRuby.Builtins.MutableString, IronRuby.Builtins.RubyRegex, System.Object, IronRuby.Builtins.MutableString>(IronRuby.Builtins.MutableStringOps.ReplaceAllInPlace), - new Func, IronRuby.Runtime.RubyScope, IronRuby.Builtins.MutableString, IronRuby.Builtins.RubyRegex, System.Object>(IronRuby.Builtins.MutableStringOps.ReplaceAllInPlace) + new Func, IronRuby.Runtime.ConversionStorage, IronRuby.Runtime.RubyScope, IronRuby.Runtime.BlockParam, IronRuby.Builtins.MutableString, System.Object, System.Object>(IronRuby.Builtins.MutableStringOps.ReplaceAllInPlace) ); DefineLibraryMethod(module, "hex", 0x51, @@ -10488,7 +10488,7 @@ private static void LoadThread_Class(IronRuby.Builtins.RubyModule/*!*/ module) { DefineLibraryMethod(module, "list", 0x61, 0x00000000U, - new Func(IronRuby.Builtins.ThreadOps.List) + new Func(IronRuby.Builtins.ThreadOps.List) ); DefineLibraryMethod(module, "main", 0x61, @@ -10564,8 +10564,8 @@ private static void LoadThreadGroup_Instance(IronRuby.Builtins.RubyModule/*!*/ m ); DefineLibraryMethod(module, "list", 0x51, - 0x00000001U, - new Func(IronRuby.Builtins.ThreadGroup.List) + 0x00000002U, + new Func(IronRuby.Builtins.ThreadGroup.List) ); DefineLibraryMethod(module, "to_s", 0x51, diff --git a/Src/Ruby/Builtins/RubyRegex.cs b/Src/Ruby/Builtins/RubyRegex.cs index b74b568ff..54e201c44 100644 --- a/Src/Ruby/Builtins/RubyRegex.cs +++ b/Src/Ruby/Builtins/RubyRegex.cs @@ -1541,6 +1541,39 @@ private static int SkipToUnescapedForwardSlash(MutableString/*!*/ pattern, int p return str.EscapeRegularExpression(); } + /// + /// MRI refuses a pattern whose bytes are not valid in its own encoding before compiling it: + /// Regexp.new, and #sub, #gsub, their bang forms and #scan given a String (string.c's + /// get_pat_quoted), all raise "RegexpError: invalid multibyte character: /\xFF/". + /// + public static void RequireValidPattern(MutableString/*!*/ pattern) { + if (pattern.Encoding != RubyEncoding.Binary && pattern.ContainsInvalidCharacters()) { + throw new RegexpError("invalid multibyte character: /" + EscapeNonAsciiBytes(pattern) + "/"); + } + } + + // For an error message about bytes that do not decode: ASCII as is, the rest as \xHH. + private static string/*!*/ EscapeNonAsciiBytes(MutableString/*!*/ str) { + var result = new StringBuilder(); + foreach (byte b in str.ToByteArray()) { + if (b < 0x80) { + result.Append((char)b); + } else { + result.Append("\\x").Append(b.ToString("X2", System.Globalization.CultureInfo.InvariantCulture)); + } + } + return result.ToString(); + } + + /// + /// A String used as a pattern by #sub, #gsub, #sub!, #gsub! and #scan: matched literally, and + /// refused first if it is not valid in its encoding, as MRI does. + /// + public static RubyRegex/*!*/ FromLiteralPattern(MutableString/*!*/ pattern) { + RequireValidPattern(pattern); + return new RubyRegex(Escape(pattern), RubyRegexOptions.NONE); + } + private static int SkipNonSpecial(string/*!*/ pattern, int i, out char escaped) { while (i < pattern.Length) { char c = pattern[i]; diff --git a/Src/Ruby/Runtime/RubyOps.cs b/Src/Ruby/Runtime/RubyOps.cs index 35b716946..7316a11bf 100644 --- a/Src/Ruby/Runtime/RubyOps.cs +++ b/Src/Ruby/Runtime/RubyOps.cs @@ -3736,7 +3736,7 @@ public static MutableString TryToStringValidator(string/*!*/ className, object o [Emitted] // ProtocolConversionAction public static RubyRegex/*!*/ ToRegexValidator(string/*!*/ className, object obj) { - return new RubyRegex(RubyRegex.Escape(ToStringValidator(className, obj)), RubyRegexOptions.NONE); + return RubyRegex.FromLiteralPattern(ToStringValidator(className, obj)); } /// diff --git a/Src/StdLib/ironruby/ruby4.rb b/Src/StdLib/ironruby/ruby4.rb index f120efccf..201a9743f 100644 --- a/Src/StdLib/ironruby/ruby4.rb +++ b/Src/StdLib/ironruby/ruby4.rb @@ -9539,8 +9539,14 @@ def self.waitall # and that function is exactly the case respond_to? answers false for - so portable # code asks whether the feature is there rather than whether the name is. # On Windows the resource-limit and priority functions are the same kind of stub in - # MRI; the libc behind ours is not there to call. - WINDOWS_NOT_IMPLEMENTED_METHODS__ = [:getrlimit, :setrlimit, :getpriority, :setpriority].freeze unless const_defined?(:WINDOWS_NOT_IMPLEMENTED_METHODS__) + # MRI; the libc behind ours is not there to call. So are the process-group, session, + # supplementary-group and set-id ones: win32/Makefile.sub defines none of getpgid, + # setpgid, getsid, setsid, getpgrp, setpgrp, getgroups, setgroups, initgroups or the + # setuid/setgid family, and process.c makes each of these methods rb_f_notimplement. + WINDOWS_NOT_IMPLEMENTED_METHODS__ = [:getrlimit, :setrlimit, :getpriority, :setpriority, + :getpgid, :setpgid, :getsid, :setsid, :getpgrp, :setpgrp, + :groups, :groups=, :initgroups, :maxgroups, :maxgroups=, + :uid=, :gid=, :euid=, :egid=].freeze unless const_defined?(:WINDOWS_NOT_IMPLEMENTED_METHODS__) unless const_defined?(:NOT_IMPLEMENTED_METHODS) NOT_IMPLEMENTED_METHODS = (::File::ALT_SEPARATOR == "\\" ? [:daemon, :fork, :_fork] + WINDOWS_NOT_IMPLEMENTED_METHODS__ : [:daemon, :fork, :_fork]).freeze