Skip to content
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
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