Skip to content

Commit

Permalink
Make File.readable? and .writable? follow symlinks on Windows (#1…
Browse files Browse the repository at this point in the history
  • Loading branch information
HertzDevil authored Apr 23, 2024
1 parent c428bab commit 10414fd
Show file tree
Hide file tree
Showing 2 changed files with 78 additions and 7 deletions.
68 changes: 68 additions & 0 deletions spec/std/file_spec.cr
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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
Expand Down Expand Up @@ -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
Expand All @@ -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
Expand Down
17 changes: 10 additions & 7 deletions src/crystal/system/win32/file.cr
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down

0 comments on commit 10414fd

Please sign in to comment.