Skip to content

Commit

Permalink
Merge pull request #7299 from kaleidicassociates/fix_issue_17167
Browse files Browse the repository at this point in the history
Fix issue 17167 - handle long filepaths on Windows
merged-on-behalf-of: Walter Bright <WalterBright@users.noreply.github.com>
  • Loading branch information
dlang-bot authored Nov 15, 2017
2 parents eeb7fd1 + a55fa15 commit 24ab619
Show file tree
Hide file tree
Showing 3 changed files with 232 additions and 35 deletions.
30 changes: 27 additions & 3 deletions src/ddmd/root/file.d
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down Expand Up @@ -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)
Expand Down
213 changes: 181 additions & 32 deletions src/ddmd/root/filename.d
Original file line number Diff line number Diff line change
Expand Up @@ -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)*);
Expand Down Expand Up @@ -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
{
Expand Down Expand Up @@ -631,6 +630,7 @@ nothrow:
}
bool r = ensurePathExists(p);
mem.xfree(cast(void*)p);

if (r)
return r;
}
Expand All @@ -644,7 +644,6 @@ nothrow:
}
if (path[strlen(path) - 1] != sep)
{
//printf("mkdir(%s)\n", path);
version (Windows)
{
int r = _mkdir(path);
Expand All @@ -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;
}

Expand All @@ -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
{
Expand All @@ -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);
}
}
24 changes: 24 additions & 0 deletions test/compilable/issue17167.sh
Original file line number Diff line number Diff line change
@@ -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

0 comments on commit 24ab619

Please sign in to comment.