diff --git a/src/crystal/system/dir_handle.cr b/src/crystal/system/dir_handle.cr new file mode 100644 index 000000000000..38c3e650fd0f --- /dev/null +++ b/src/crystal/system/dir_handle.cr @@ -0,0 +1,35 @@ +struct Crystal::System::DirHandle + # Creates a new `DirHandle` + # def self.new(path : String) : DirHandle + + # Reads the next entry from dir and returns it as a string. Returns `nil` at the end of the stream. + # def read + + # Repositions this directory to the first entry. + # def rewind + + # Closes the directory stream. + # def close + + # Returns the current working directory. + # def self.current : String + + # Changes the current working directory of the process to the given string. + # def self.cd(path : String) + + # Returns `true` if the given path exists and is a directory + # def self.exists?(path : String) : Bool + + # Creates a new directory at the given path, including any non-existing + # intermediate directories. The linux-style permission mode can be specified. + # def self.mkdir(path : String, mode) + + # Removes the directory at the given path. + # def self.rmdir(path : String) +end + +{% if flag?(:win32) %} + require "./windows/dir_handle" +{% else %} + require "./unix/dir_handle" +{% end %} diff --git a/src/crystal/system/unix/dir_handle.cr b/src/crystal/system/unix/dir_handle.cr new file mode 100644 index 000000000000..f0c7eb20f2f0 --- /dev/null +++ b/src/crystal/system/unix/dir_handle.cr @@ -0,0 +1,79 @@ +require "c/dirent" +require "c/unistd" +require "c/sys/stat" + +struct Crystal::System::DirHandle + @dirhandle : LibC::DIR* + + @closed = false + + def initialize(path : String) + @dirhandle = LibC.opendir(path.check_no_null_byte) + unless @dirhandle + raise Errno.new("Error opening directory '#{path}'") + end + @closed = false + end + + def read + # readdir() returns NULL for failure and sets errno or returns NULL for EOF but leaves errno as is. wtf. + Errno.value = 0 + ent = LibC.readdir(@dirhandle) + if ent + String.new(ent.value.d_name.to_unsafe) + elsif Errno.value != 0 + raise Errno.new("readdir") + else + nil + end + end + + def rewind + LibC.rewinddir(@dirhandle) + end + + def close + return if @closed + if LibC.closedir(@dirhandle) != 0 + raise Errno.new("closedir") + end + @closed = true + end + + def self.current : String + if dir = LibC.getcwd(nil, 0) + String.new(dir).tap { LibC.free(dir.as(Void*)) } + else + raise Errno.new("getcwd") + end + end + + def self.cd(path : String) + if LibC.chdir(path.check_no_null_byte) != 0 + raise Errno.new("Error while changing directory to #{path.inspect}") + end + end + + def self.exists?(path : String) : Bool + if LibC.stat(path.check_no_null_byte, out stat) != 0 + if Errno.value == Errno::ENOENT || Errno.value == Errno::ENOTDIR + return false + else + raise Errno.new("stat") + end + end + File::Stat.new(stat).directory? + end + + def self.mkdir(path : String, mode) + if LibC.mkdir(path.check_no_null_byte, mode) == -1 + raise Errno.new("Unable to create directory '#{path}'") + end + end + + def self.rmdir(path : String) + if LibC.rmdir(path.check_no_null_byte) == -1 + raise Errno.new("Unable to remove directory '#{path}'") + end + end +end diff --git a/src/crystal/system/windows/dir_handle.cr b/src/crystal/system/windows/dir_handle.cr new file mode 100644 index 000000000000..831c4cfd4ecf --- /dev/null +++ b/src/crystal/system/windows/dir_handle.cr @@ -0,0 +1,83 @@ +require "c/winapi" +require "winerror" + +struct Crystal::System::DirHandle + @dir_handle : LibC::Handle + + def initialize(path : String) + if !DirHandle.exists?(path) + raise WinError.new("Error opening directory '#{path}'") + end + @dir_handle = LibC::INVALID_HANDLE_VALUE + end + + def read + data = LibC::WIN32_FIND_DATA_A.new + if @dir_handle == LibC::INVALID_HANDLE_VALUE + @dir_handle = LibC._FindFirstFileA((path + "\\*").check_no_null_byte, pointerof(data)) + if @dir_handle == LibC::INVALID_HANDLE_VALUE + raise WinError.new("FindFirstFileA") + end + elsif LibC._FindNextFileA(@dir_handle, pointerof(data)) == 0 + error = LibC._GetLastError + if error == WinError::ERROR_NO_MORE_FILES + return nil + else + raise WinError.new("FindNextFileA", error) + end + end + String.new(data.cFileName.to_slice) + end + + def rewind + close + end + + def close + if @dir_handle != LibC::INVALID_HANDLE_VALUE + if LibC._FindClose(@dir_handle) == 0 + raise WinError.new("FindClose") + end + @dir_handle = LibC::INVALID_HANDLE_VALUE + end + end + + def self.current : String + len = LibC._GetCurrentDirectoryA(0, nil) + if len == 0 + raise WinError.new("_GetCurrentDirectoryA") + end + String.new(len) do |buffer| + if LibC._GetCurrentDirectoryA(len, buffer) == 0 + raise WinError.new("_GetCurrentDirectoryA") + end + {len - 1, len - 1} # remove \0 at the end + end + end + + def self.cd(path : String) + if LibC._SetCurrentDirectoryA(path.check_no_null_byte) == 0 + raise WinError.new("Error while changing directory to #{path.inspect}") + end + end + + def self.exists?(path : String) : Bool + atr = LibC._GetFileAttributesA(path.check_no_null_byte) + if (atr == LibWindows::INVALID_FILE_ATTRIBUTES) + return false + end + return atr & LibC::FILE_ATTRIBUTE_DIRECTORY != 0 + end + + def self.mkdir(path : String, mode) + if LibC._CreateDirectoryA(path.check_no_null_byte, nil) == 0 + raise WinError.new("Unable to create directory '#{path}'") + end + end + + def self.rmdir(path : String) + if LibC._RemoveDirectoryA(path.check_no_null_byte) == 0 + raise WinError.new("Unable to remove directory '#{path}'") + end + end +end diff --git a/src/dir.cr b/src/dir.cr index 48f69dcce621..b5f5c1b5b81d 100644 --- a/src/dir.cr +++ b/src/dir.cr @@ -1,6 +1,4 @@ -require "c/dirent" -require "c/unistd" -require "c/sys/stat" +require "crystal/system/dir_handle" # Objects of class `Dir` are directory streams representing directories in the underlying file system. # They provide a variety of ways to list directories and their contents. @@ -17,11 +15,7 @@ class Dir # Returns a new directory object for the named directory. def initialize(@path) - @dir = LibC.opendir(@path.check_no_null_byte) - unless @dir - raise Errno.new("Error opening directory '#{@path}'") - end - @closed = false + @dir = Crystal::System::DirHandle.new(@path) end # Alias for `new(path)` @@ -125,47 +119,28 @@ class Dir # array.sort # => [".", "..", "config.h"] # ``` def read - # readdir() returns NULL for failure and sets errno or returns NULL for EOF but leaves errno as is. wtf. - Errno.value = 0 - ent = LibC.readdir(@dir) - if ent - String.new(ent.value.d_name.to_unsafe) - elsif Errno.value != 0 - raise Errno.new("readdir") - else - nil - end + @dir.read end # Repositions this directory to the first entry. def rewind - LibC.rewinddir(@dir) + @dir.rewind self end # Closes the directory stream. def close - return if @closed - if LibC.closedir(@dir) != 0 - raise Errno.new("closedir") - end - @closed = true + @dir.close end # Returns the current working directory. def self.current : String - if dir = LibC.getcwd(nil, 0) - String.new(dir).tap { LibC.free(dir.as(Void*)) } - else - raise Errno.new("getcwd") - end + Crystal::System::DirHandle.current end # Changes the current working directory of the process to the given string. def self.cd(path) - if LibC.chdir(path.check_no_null_byte) != 0 - raise Errno.new("Error while changing directory to #{path.inspect}") - end + Crystal::System::DirHandle.cd(path) end # Changes the current working directory of the process to the given string @@ -215,14 +190,7 @@ class Dir # Returns `true` if the given path exists and is a directory def self.exists?(path) : Bool - if LibC.stat(path.check_no_null_byte, out stat) != 0 - if Errno.value == Errno::ENOENT || Errno.value == Errno::ENOTDIR - return false - else - raise Errno.new("stat") - end - end - File::Stat.new(stat).directory? + Crystal::System::DirHandle.exists?(path) end # Returns `true` if the directory at *path* is empty, otherwise returns `false`. @@ -246,9 +214,7 @@ class Dir # Creates a new directory at the given path. The linux-style permission mode # can be specified, with a default of 777 (0o777). def self.mkdir(path, mode = 0o777) - if LibC.mkdir(path.check_no_null_byte, mode) == -1 - raise Errno.new("Unable to create directory '#{path}'") - end + Crystal::System::DirHandle.mkdir(path, mode) 0 end @@ -276,9 +242,7 @@ class Dir # Removes the directory at the given path. def self.rmdir(path) - if LibC.rmdir(path.check_no_null_byte) == -1 - raise Errno.new("Unable to remove directory '#{path}'") - end + Crystal::System::DirHandle.rmdir(path) 0 end