From b78f5608f49cc01d2ee37d1f73f17bd126068db8 Mon Sep 17 00:00:00 2001 From: Jon Malmaud Date: Tue, 13 Oct 2015 00:06:36 -0400 Subject: [PATCH] Make require case-sensitive on all platforms --- base/loading.jl | 88 ++++++++++++++++++++++++++++++++++++++++++++++--- base/path.jl | 17 ++++++++++ test/loading.jl | 15 +++++++++ 3 files changed, 115 insertions(+), 5 deletions(-) diff --git a/base/loading.jl b/base/loading.jl index 0aeda4c3cac88..89584055f4a6e 100644 --- a/base/loading.jl +++ b/base/loading.jl @@ -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 + 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()) @@ -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 @@ -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 diff --git a/base/path.jl b/base/path.jl index 24f47a69be6e7..0c941fdc1c35d 100644 --- a/base/path.jl +++ b/base/path.jl @@ -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) diff --git a/test/loading.jl b/test/loading.jl index 0e7f05aeeba76..773180084cf7b 100644 --- a/test/loading.jl +++ b/test/loading.jl @@ -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