Skip to content

Commit

Permalink
Make require case-sensitive on all platforms
Browse files Browse the repository at this point in the history
  • Loading branch information
malmaud committed Oct 13, 2015
1 parent 6aa15b8 commit b78f560
Show file tree
Hide file tree
Showing 3 changed files with 115 additions and 5 deletions.
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
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))
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"
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

0 comments on commit b78f560

Please sign in to comment.