Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Make require case-sensitive on all platforms #13542

Merged
merged 1 commit into from
Oct 13, 2015
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
88 changes: 83 additions & 5 deletions base/loading.jl
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,84 @@

# Base.require is the implementation for the `import` statement

# Cross-platform case-sensitive path canonicalization

if OS_NAME (:Linux, :FreeBSD)
# Case-sensitive filesystems, don't have to do anything
isfile_casesensitive(path) = isfile(path)
elseif OS_NAME == :Windows
# GetLongPathName Win32 function returns the case-preserved filename on NTFS.
function isfile_casesensitive(path)
isfile(path) || return false # Fail fast
longpath(path) == path
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Possibly this should be basename(longpath(path)) == basename(path)? Two reasons:

  • On MacOS, we only check that the basename matches, so this is inconsistent.
  • The intent of this was that import Foo would check for Foo in a case-sensitive way. But this does more than that, and checks the whole path. This means that users LOAD_PATH and HOME variables becomes case-sensitive, for example, which is unexpected on case-insensitive systems, and has led to at least one confused user on discourse.

end
elseif OS_NAME == :Darwin
# HFS+ filesystem is case-preserving. The getattrlist API returns
# a case-preserved filename. In the rare event that HFS+ is operating
# in case-sensitive mode, this will still work but will be redundant.

# Constants from <sys/attr.h>
const ATRATTR_BIT_MAP_COUNT = 5
const ATTR_CMN_NAME = 1
const BITMAPCOUNT = 1
const COMMONATTR = 5
const FSOPT_NOFOLLOW = 1 # Don't follow symbolic links

const attr_list = zeros(UInt8, 24)
attr_list[BITMAPCOUNT] = ATRATTR_BIT_MAP_COUNT
attr_list[COMMONATTR] = ATTR_CMN_NAME

# This essentially corresponds to the following C code:
# attrlist attr_list;
# memset(&attr_list, 0, sizeof(attr_list));
# attr_list.bitmapcount = ATTR_BIT_MAP_COUNT;
# attr_list.commonattr = ATTR_CMN_NAME;
# struct Buffer {
# u_int32_t total_length;
# u_int32_t filename_offset;
# u_int32_t filename_length;
# char filename[max_filename_length];
# };
# Buffer buf;
# getattrpath(path, &attr_list, &buf, sizeof(buf), FSOPT_NOFOLLOW);
function isfile_casesensitive(path)
isfile(path) || return false
path_basename = bytestring(basename(path))
local casepreserved_basename
const header_size = 12
buf = Array(UInt8, length(path_basename) + header_size + 1)
while true
ret = ccall(:getattrlist, Cint,
(Cstring, Ptr{Void}, Ptr{Void}, Csize_t, Culong),
path, attr_list, buf, sizeof(buf), FSOPT_NOFOLLOW)
systemerror(:getattrlist, ret 0)
filename_length = unsafe_load(
convert(Ptr{UInt32}, pointer(buf) + 8))
if (filename_length + header_size) > length(buf)
resize!(buf, filename_length + header_size)
continue
end
casepreserved_basename =
sub(buf, (header_size+1):(header_size+filename_length-1))
break
end
# Hack to compensate for inability to create a string from a subarray with no allocations.
path_basename.data == casepreserved_basename && return true

# If there is no match, it's possible that the file does exist but HFS+
# performed unicode normalization. See https://developer.apple.com/library/mac/qa/qa1235/_index.html.
isascii(path_basename) && return false
normalize_string(path_basename, :NFD).data == casepreserved_basename
end
else
# Generic fallback that performs a slow directory listing.
function isfile_casesensitive(path)
isfile(path) || return false
dir, filename = splitdir(path)
any(readdir(dir) .== filename)
end
end

# `wd` is a working directory to search. defaults to current working directory.
# if `wd === nothing`, no extra path is searched.
function find_in_path(name::AbstractString, wd = pwd())
Expand All @@ -13,15 +91,15 @@ function find_in_path(name::AbstractString, wd = pwd())
name = string(base,".jl")
end
if wd !== nothing
isfile(joinpath(wd,name)) && return joinpath(wd,name)
isfile_casesensitive(joinpath(wd,name)) && return joinpath(wd,name)
end
for prefix in [Pkg.dir(); LOAD_PATH]
path = joinpath(prefix, name)
isfile(path) && return abspath(path)
isfile_casesensitive(path) && return abspath(path)
path = joinpath(prefix, base, "src", name)
isfile(path) && return abspath(path)
isfile_casesensitive(path) && return abspath(path)
path = joinpath(prefix, name, "src", name)
isfile(path) && return abspath(path)
isfile_casesensitive(path) && return abspath(path)
end
return nothing
end
Expand All @@ -47,7 +125,7 @@ function find_all_in_cache_path(mod::Symbol)
paths = AbstractString[]
for prefix in LOAD_CACHE_PATH
path = joinpath(prefix, name*".ji")
if isfile(path)
if isfile_casesensitive(path)
push!(paths, path)
end
end
Expand Down
17 changes: 17 additions & 0 deletions base/path.jl
Original file line number Diff line number Diff line change
Expand Up @@ -125,6 +125,23 @@ abspath(a::AbstractString, b::AbstractString...) = abspath(joinpath(a,b...))
end
end

@windows_only longpath(path::AbstractString) = longpath(utf16(path))
@windows_only function longpath(path::UTF16String)
buf = Array(UInt16, length(path.data))
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can you also fix realpath accordingly?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ya, but maybe it should be a separate PR just for organizational reasons.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Okay.

while true
p = ccall((:GetLongPathNameW, "Kernel32"), stdcall, UInt32,
(Cwstring, Ptr{UInt16}, UInt32),
path, buf, length(buf))
systemerror(:longpath, p == 0)
# Buffer wasn't big enough, in which case `p` is the necessary buffer size
if (p > length(buf))
resize!(buf, p)
continue
end
return utf8(UTF16String(buf))
end
end

@unix_only function realpath(path::AbstractString)
p = ccall(:realpath, Ptr{UInt8}, (Cstring, Ptr{UInt8}), path, C_NULL)
systemerror(:realpath, p == C_NULL)
Expand Down
15 changes: 15 additions & 0 deletions test/loading.jl
Original file line number Diff line number Diff line change
Expand Up @@ -10,3 +10,18 @@ thefname = "the fname!//\\&\0\1*"
@test include_string("Base.source_path()", thefname) == Base.source_path()
@test basename(@__FILE__) == "loading.jl"
@test isabspath(@__FILE__)

# Issue #5789 and PR #13542:
let true_filename = "cAsEtEsT.jl", lowered_filename="casetest.jl"
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Maybe add a comment here:

# Issue #5789 and PR #13542:

touch(true_filename)
@test Base.isfile_casesensitive(true_filename)
@test !Base.isfile_casesensitive(lowered_filename)
rm(true_filename)
end

# Test Unicode normalization; pertinent for OS X
let nfc_name = "\U00F4.jl"
touch(nfc_name)
@test Base.isfile_casesensitive(nfc_name)
rm(nfc_name)
end