diff --git a/src/ddmd/root/file.d b/src/ddmd/root/file.d index 632eb420b5da..4169a5f8c63f 100644 --- a/src/ddmd/root/file.d +++ b/src/ddmd/root/file.d @@ -188,9 +188,21 @@ nothrow: } else version (Windows) { + import ddmd.root.filename: extendedPathThen; + DWORD size; DWORD numread; - HANDLE h = CreateFileA(name, GENERIC_READ, FILE_SHARE_READ, null, OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL | FILE_FLAG_SEQUENTIAL_SCAN, null); + + // work around Windows file path length limitation + // (see documentation for extendedPathThen). + HANDLE h = name.extendedPathThen! + (p => CreateFileW(&p[0], + GENERIC_READ, + FILE_SHARE_READ, + null, + OPEN_EXISTING, + FILE_ATTRIBUTE_NORMAL | FILE_FLAG_SEQUENTIAL_SCAN, + null)); if (h == INVALID_HANDLE_VALUE) goto err1; if (!_ref) @@ -254,11 +266,23 @@ nothrow: } else version (Windows) { - DWORD numwritten; + import ddmd.root.filename: extendedPathThen; + + DWORD numwritten; // here because of the gotos const(char)* name = this.name.toChars(); - HANDLE h = CreateFileA(name, GENERIC_WRITE, 0, null, CREATE_ALWAYS, FILE_ATTRIBUTE_NORMAL | FILE_FLAG_SEQUENTIAL_SCAN, null); + // work around Windows file path length limitation + // (see documentation for extendedPathThen). + HANDLE h = name.extendedPathThen! + (p => CreateFileW(&p[0], + GENERIC_WRITE, + 0, + null, + CREATE_ALWAYS, + FILE_ATTRIBUTE_NORMAL | FILE_FLAG_SEQUENTIAL_SCAN, + null)); if (h == INVALID_HANDLE_VALUE) goto err; + if (WriteFile(h, buffer, cast(DWORD)len, &numwritten, null) != TRUE) goto err2; if (len != numwritten) diff --git a/src/ddmd/root/filename.d b/src/ddmd/root/filename.d index 92622745ad7c..d15e89b492d4 100644 --- a/src/ddmd/root/filename.d +++ b/src/ddmd/root/filename.d @@ -26,11 +26,10 @@ import ddmd.root.rootobject; nothrow { -version (Windows) extern (C) int mkdir(const char*); -version (Windows) alias _mkdir = mkdir; -version (Posix) extern (C) char* canonicalize_file_name(const char*); version (Windows) extern (C) int stricmp(const char*, const char*) pure; -version (Windows) extern (Windows) DWORD GetFullPathNameA(LPCSTR lpFileName, DWORD nBufferLength, LPSTR lpBuffer, LPSTR* lpFilePart); +version (Windows) extern (Windows) DWORD GetFullPathNameW(LPCWSTR, DWORD, LPWSTR, LPWSTR*) @nogc; +version (Windows) extern (Windows) void SetLastError(DWORD) @nogc; +version (Posix) extern (C) char* canonicalize_file_name(const char*); } alias Strings = Array!(const(char)*); @@ -593,16 +592,16 @@ nothrow: } else version (Windows) { - DWORD dw; - int result; - dw = GetFileAttributesA(name); - if (dw == -1) - result = 0; - else if (dw & FILE_ATTRIBUTE_DIRECTORY) - result = 2; - else - result = 1; - return result; + return name.toWStringzThen!((wname) + { + const dw = GetFileAttributesW(&wname[0]); + if (dw == -1) + return 0; + else if (dw & FILE_ATTRIBUTE_DIRECTORY) + return 2; + else + return 1; + }); } else { @@ -631,6 +630,7 @@ nothrow: } bool r = ensurePathExists(p); mem.xfree(cast(void*)p); + if (r) return r; } @@ -644,7 +644,6 @@ nothrow: } if (path[strlen(path) - 1] != sep) { - //printf("mkdir(%s)\n", path); version (Windows) { int r = _mkdir(path); @@ -658,12 +657,25 @@ nothrow: /* Don't error out if another instance of dmd just created * this directory */ - if (errno != EEXIST) - return true; + version (Windows) + { + // see core.sys.windows.winerror - the reason it's not imported here is because + // the autotester's dmd is too old and doesn't have that module + enum ERROR_ALREADY_EXISTS = 183; + + if (GetLastError() != ERROR_ALREADY_EXISTS) + return true; + } + version (Posix) + { + if (errno != EEXIST) + return true; + } } } } } + return false; } @@ -680,22 +692,33 @@ nothrow: } else version (Windows) { - /* Apparently, there is no good way to do this on Windows. - * GetFullPathName isn't it, but use it anyway. - */ - DWORD result = GetFullPathNameA(name, 0, null, null); - if (result) + // Convert to wstring first since otherwise the Win32 APIs have a character limit + return name.toWStringzThen!((wname) { - char* buf = cast(char*)malloc(result); - result = GetFullPathNameA(name, result, buf, null); - if (result == 0) - { - .free(buf); - return null; - } - return buf; - } - return null; + /* Apparently, there is no good way to do this on Windows. + * GetFullPathName isn't it, but use it anyway. + */ + // First find out how long the buffer has to be. + auto fullPathLength = GetFullPathNameW(&wname[0], 0, null, null); + if (!fullPathLength) return null; + auto fullPath = new wchar[fullPathLength]; + + // Actually get the full path name + const fullPathLengthNoTerminator = GetFullPathNameW(&wname[0], fullPath.length, &fullPath[0], null /*filePart*/); + // Unfortunately, when the buffer is large enough the return value is the number of characters + // _not_ counting the null terminator, so fullPathLength2 should be smaller + assert(fullPathLength == fullPathLengthNoTerminator + 1); + + // Find out size of the converted string + const retLength = WideCharToMultiByte(0 /*codepage*/, 0 /*flags*/, &fullPath[0], fullPathLength, null, 0, null, null); + auto ret = new char[retLength]; + + // Actually convert to char + const retLength2 = WideCharToMultiByte(0 /*codepage*/, 0 /*flags*/, &fullPath[0], fullPathLength, &ret[0], ret.length, null, null); + assert(retLength == retLength2); + + return &ret[0]; + }); } else { @@ -721,3 +744,129 @@ nothrow: return str; } } + +version(Windows) +{ + /**************************************************************** + * The code before used the POSIX function `mkdir` on Windows. That + * function is now deprecated and fails with long paths, so instead + * we use the newer `CreateDirectoryW`. + * + * `CreateDirectoryW` is the unicode version of the generic macro + * `CreateDirectory`. `CreateDirectoryA` has a file path + * limitation of 248 characters, `mkdir` fails with less and might + * fail due to the number of consecutive `..`s in the + * path. `CreateDirectoryW` also normally has a 248 character + * limit, unless the path is absolute and starts with `\\?\`. Note + * that this is different from starting with the almost identical + * `\\?`. + * + * Params: + * path = The path to create. + * Returns: + * 0 on success, 1 on failure. + * + * References: + * https://msdn.microsoft.com/en-us/library/windows/desktop/aa363855(v=vs.85).aspx + */ + private int _mkdir(const(char)* path) nothrow + { + const createRet = path.extendedPathThen!(p => CreateDirectoryW(&p[0], + null /*securityAttributes*/)); + // different conventions for CreateDirectory and mkdir + return createRet == 0 ? 1 : 0; + } + + /************************************** + * Converts a path to one suitable to be passed to Win32 API + * functions that can deal with paths longer than 248 + * characters then calls the supplied function on it. + * Params: + * path = The Path to call F on. + * Returns: + * The result of calling F on path. + * References: + * https://msdn.microsoft.com/en-us/library/windows/desktop/aa365247(v=vs.85).aspx + */ + package auto extendedPathThen(alias F)(const(char*) path) + { + return path.toWStringzThen!((wpath) + { + // GetFullPathNameW expects a sized buffer to store the result in. Since we don't + // know how larget it has to be, we pass in null and get the needed buffer length + // as the return code. + const pathLength = GetFullPathNameW(&wpath[0], + 0 /*length8*/, + null /*output buffer*/, + null /*filePartBuffer*/); + if (pathLength == 0) + { + return F(""w); + } + + // wpath is the UTF16 version of path, but to be able to use + // extended paths, we need to prefix with `\\?\` and the absolute + // path. + static immutable prefix = `\\?\`w; + + // +1 for the null terminator + const bufferLength = pathLength + prefix.length + 1; + + wchar[1024] absBuf; + auto absPath = bufferLength > absBuf.length ? new wchar[bufferLength] : absBuf[]; + + absPath[0 .. prefix.length] = prefix[]; + + const absPathRet = GetFullPathNameW(&wpath[0], + absPath.length - prefix.length, + &absPath[prefix.length], + null /*filePartBuffer*/); + + if (absPathRet == 0 || absPathRet > absPath.length - prefix.length) + { + return F(""w); + } + + auto extendedPath = absPath[0 .. absPathRet]; + return F(extendedPath); + + }); + } + + /********************************** + * Converts a null-terminated string to an array of wchar that's null + * terminated so it can be passed to Win32 APIs then calls the supplied + * function on it. + * Params: + * str = The string to convert. + * Returns: + * The result of calling F on the UTF16 version of str. + */ + private auto toWStringzThen(alias F)(const(char*) str) nothrow + { + import core.stdc.string: strlen; + import core.stdc.stdlib: malloc, free; + + wchar[1024] buf; + // cache this for efficiency + const strLength = strlen(str) + 1; + // first find out how long the buffer must be to store the result + const length = MultiByteToWideChar(0 /*codepage*/, 0 /*flags*/, str, strLength, null, 0); + wchar[] empty; + if (!length) return F(empty); + + auto ret = length > buf.length + ? (cast(wchar*)malloc(length * wchar.sizeof))[0 .. length] + : buf[0 .. length]; + scope (exit) + { + if (&ret[0] != &buf[0]) + free(&ret[0]); + } + // actually do the conversion + const length2 = MultiByteToWideChar(0 /*codepage*/, 0 /*flags*/, str, strLength, &ret[0], ret.length); + assert(length == length2); // should always be true according to the API + + return F(ret); + } +} diff --git a/test/compilable/issue17167.sh b/test/compilable/issue17167.sh new file mode 100755 index 000000000000..86549202cc19 --- /dev/null +++ b/test/compilable/issue17167.sh @@ -0,0 +1,24 @@ +#!/usr/bin/env bash + +set -euo pipefail + +# Test that file paths larger than 248 characters can be used +# Test CRLF and mixed line ending handling in D lexer. + +name=$(basename "$0" .sh) +dir=${RESULTS_DIR}/compilable/ + +test_dir=${dir}/${name}/uuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuu +[[ -d $test_dir ]] || mkdir -p "$test_dir" +bin_base=${test_dir}/${name} +bin="$bin_base$OBJ" +src="$bin_base.d" + +echo 'void main() {}' > "${src}" + +# Only compile, not link, since optlink can't handle long file names +$DMD -m"${MODEL}" "${DFLAGS}" -c -of"${bin}" "${src}" || exit 1 + +rm -rf "${dir:?}"/"$name" + +echo Success >"${dir}"/"$(basename $0)".out