diff --git a/spec/std/file_spec.cr b/spec/std/file_spec.cr index f137478b56bc..fe91d1071b52 100644 --- a/spec/std/file_spec.cr +++ b/spec/std/file_spec.cr @@ -200,9 +200,24 @@ describe "File" do it "gives false when a component of the path is a file" do File.exists?(datapath("dir", "test_file.txt", "")).should be_false end + + it "follows symlinks" do + with_tempfile("good_symlink.txt", "bad_symlink.txt") do |good_path, bad_path| + File.symlink(File.expand_path(datapath("test_file.txt")), good_path) + File.symlink(File.expand_path(datapath("non_existing_file.txt")), bad_path) + + File.exists?(good_path).should be_true + File.exists?(bad_path).should be_false + end + end end describe "executable?" do + it "gives true" do + crystal = Process.executable_path || pending! "Unable to locate compiler executable" + File.executable?(crystal).should be_true + end + it "gives false" do File.executable?(datapath("test_file.txt")).should be_false end @@ -214,6 +229,17 @@ describe "File" do it "gives false when a component of the path is a file" do File.executable?(datapath("dir", "test_file.txt", "")).should be_false end + + it "follows symlinks" do + with_tempfile("good_symlink_x.txt", "bad_symlink_x.txt") do |good_path, bad_path| + crystal = Process.executable_path || pending! "Unable to locate compiler executable" + File.symlink(File.expand_path(crystal), good_path) + File.symlink(File.expand_path(datapath("non_existing_file.txt")), bad_path) + + File.executable?(good_path).should be_true + File.executable?(bad_path).should be_false + end + end end describe "readable?" do @@ -248,7 +274,28 @@ describe "File" do File.readable?(path).should be_false end end + + it "follows symlinks" do + with_tempfile("good_symlink_r.txt", "bad_symlink_r.txt", "unreadable.txt") do |good_path, bad_path, unreadable| + File.write(unreadable, "") + File.chmod(unreadable, 0o222) + pending_if_superuser! + + File.symlink(File.expand_path(datapath("test_file.txt")), good_path) + File.symlink(File.expand_path(unreadable), bad_path) + + File.readable?(good_path).should be_true + File.readable?(bad_path).should be_false + end + end {% end %} + + it "gives false when the symbolic link destination doesn't exist" do + with_tempfile("missing_symlink_r.txt") do |missing_path| + File.symlink(File.expand_path(datapath("non_existing_file.txt")), missing_path) + File.readable?(missing_path).should be_false + end + end end describe "writable?" do @@ -272,6 +319,27 @@ describe "File" do File.writable?(path).should be_false end end + + it "follows symlinks" do + with_tempfile("good_symlink_w.txt", "bad_symlink_w.txt", "readonly.txt") do |good_path, bad_path, readonly| + File.write(readonly, "") + File.chmod(readonly, 0o444) + pending_if_superuser! + + File.symlink(File.expand_path(datapath("test_file.txt")), good_path) + File.symlink(File.expand_path(readonly), bad_path) + + File.writable?(good_path).should be_true + File.writable?(bad_path).should be_false + end + end + + it "gives false when the symbolic link destination doesn't exist" do + with_tempfile("missing_symlink_w.txt") do |missing_path| + File.symlink(File.expand_path(datapath("non_existing_file.txt")), missing_path) + File.writable?(missing_path).should be_false + end + end end describe "file?" do diff --git a/src/crystal/system/win32/file.cr b/src/crystal/system/win32/file.cr index 5db0d297ce17..459cb86d977d 100644 --- a/src/crystal/system/win32/file.cr +++ b/src/crystal/system/win32/file.cr @@ -164,25 +164,28 @@ module Crystal::System::File end def self.exists?(path, *, follow_symlinks = true) - if follow_symlinks - path = realpath?(path) || return false - end - accessible?(path, check_writable: false) + accessible?(path, check_writable: false, follow_symlinks: follow_symlinks) end def self.readable?(path) : Bool - accessible?(path, check_writable: false) + accessible?(path, check_writable: false, follow_symlinks: true) end def self.writable?(path) : Bool - accessible?(path, check_writable: true) + accessible?(path, check_writable: true, follow_symlinks: true) end def self.executable?(path) : Bool + # NOTE: this always follows symlinks: + # https://learn.microsoft.com/en-us/windows/win32/api/winbase/nf-winbase-getbinarytypew#remarks LibC.GetBinaryTypeW(System.to_wstr(path), out result) != 0 end - private def self.accessible?(path, *, check_writable) + private def self.accessible?(path, *, check_writable, follow_symlinks) + if follow_symlinks + path = realpath?(path) || return false + end + attributes = LibC.GetFileAttributesW(System.to_wstr(path)) return false if attributes == LibC::INVALID_FILE_ATTRIBUTES return true if attributes.bits_set?(LibC::FILE_ATTRIBUTE_DIRECTORY)