Skip to content

Commit 0321cf2

Browse files
committed
Issue18314 Allow unlink to remove junctions. Includes support for creating junctions. Patch by Kim Gräsman
1 parent a479096 commit 0321cf2

4 files changed

Lines changed: 235 additions & 36 deletions

File tree

Lib/test/test_os.py

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,10 @@
3939
import fcntl
4040
except ImportError:
4141
fcntl = None
42+
try:
43+
import _winapi
44+
except ImportError:
45+
_winapi = None
4246

4347
from test.script_helper import assert_python_ok
4448

@@ -1773,6 +1777,37 @@ def test_12084(self):
17731777
shutil.rmtree(level1)
17741778

17751779

1780+
@unittest.skipUnless(sys.platform == "win32", "Win32 specific tests")
1781+
class Win32JunctionTests(unittest.TestCase):
1782+
junction = 'junctiontest'
1783+
junction_target = os.path.dirname(os.path.abspath(__file__))
1784+
1785+
def setUp(self):
1786+
assert os.path.exists(self.junction_target)
1787+
assert not os.path.exists(self.junction)
1788+
1789+
def tearDown(self):
1790+
if os.path.exists(self.junction):
1791+
# os.rmdir delegates to Windows' RemoveDirectoryW,
1792+
# which removes junction points safely.
1793+
os.rmdir(self.junction)
1794+
1795+
def test_create_junction(self):
1796+
_winapi.CreateJunction(self.junction_target, self.junction)
1797+
self.assertTrue(os.path.exists(self.junction))
1798+
self.assertTrue(os.path.isdir(self.junction))
1799+
1800+
# Junctions are not recognized as links.
1801+
self.assertFalse(os.path.islink(self.junction))
1802+
1803+
def test_unlink_removes_junction(self):
1804+
_winapi.CreateJunction(self.junction_target, self.junction)
1805+
self.assertTrue(os.path.exists(self.junction))
1806+
1807+
os.unlink(self.junction)
1808+
self.assertFalse(os.path.exists(self.junction))
1809+
1810+
17761811
@support.skip_unless_symlink
17771812
class NonLocalSymlinkTests(unittest.TestCase):
17781813

@@ -2544,6 +2579,7 @@ def test_main():
25442579
RemoveDirsTests,
25452580
CPUCountTests,
25462581
FDInheritanceTests,
2582+
Win32JunctionTests,
25472583
)
25482584

25492585
if __name__ == "__main__":

Modules/_winapi.c

Lines changed: 137 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,7 @@
4040
#define WINDOWS_LEAN_AND_MEAN
4141
#include "windows.h"
4242
#include <crtdbg.h>
43+
#include "winreparse.h"
4344

4445
#if defined(MS_WIN32) && !defined(MS_WIN64)
4546
#define HANDLE_TO_PYNUM(handle) \
@@ -400,6 +401,140 @@ winapi_CreateFile(PyObject *self, PyObject *args)
400401
return Py_BuildValue(F_HANDLE, handle);
401402
}
402403

404+
static PyObject *
405+
winapi_CreateJunction(PyObject *self, PyObject *args)
406+
{
407+
/* Input arguments */
408+
LPWSTR src_path = NULL;
409+
LPWSTR dst_path = NULL;
410+
411+
/* Privilege adjustment */
412+
HANDLE token = NULL;
413+
TOKEN_PRIVILEGES tp;
414+
415+
/* Reparse data buffer */
416+
const USHORT prefix_len = 4;
417+
USHORT print_len = 0;
418+
USHORT rdb_size = 0;
419+
PREPARSE_DATA_BUFFER rdb = NULL;
420+
421+
/* Junction point creation */
422+
HANDLE junction = NULL;
423+
DWORD ret = 0;
424+
425+
if (!PyArg_ParseTuple(args, "uu", &src_path, &dst_path))
426+
return NULL;
427+
428+
if (src_path == NULL || dst_path == NULL)
429+
return PyErr_SetFromWindowsErr(ERROR_INVALID_PARAMETER);
430+
431+
if (wcsncmp(src_path, L"\\??\\", prefix_len) == 0)
432+
return PyErr_SetFromWindowsErr(ERROR_INVALID_PARAMETER);
433+
434+
/* Adjust privileges to allow rewriting directory entry as a
435+
junction point. */
436+
if (!OpenProcessToken(GetCurrentProcess(), TOKEN_ADJUST_PRIVILEGES, &token))
437+
goto cleanup;
438+
439+
if (!LookupPrivilegeValue(NULL, SE_RESTORE_NAME, &tp.Privileges[0].Luid))
440+
goto cleanup;
441+
442+
tp.PrivilegeCount = 1;
443+
tp.Privileges[0].Attributes = SE_PRIVILEGE_ENABLED;
444+
if (!AdjustTokenPrivileges(token, FALSE, &tp, sizeof(TOKEN_PRIVILEGES),
445+
NULL, NULL))
446+
goto cleanup;
447+
448+
if (GetFileAttributesW(src_path) == INVALID_FILE_ATTRIBUTES)
449+
goto cleanup;
450+
451+
/* Store the absolute link target path length in print_len. */
452+
print_len = (USHORT)GetFullPathNameW(src_path, 0, NULL, NULL);
453+
if (print_len == 0)
454+
goto cleanup;
455+
456+
/* NUL terminator should not be part of print_len. */
457+
--print_len;
458+
459+
/* REPARSE_DATA_BUFFER usage is heavily under-documented, especially for
460+
junction points. Here's what I've learned along the way:
461+
- A junction point has two components: a print name and a substitute
462+
name. They both describe the link target, but the substitute name is
463+
the physical target and the print name is shown in directory listings.
464+
- The print name must be a native name, prefixed with "\??\".
465+
- Both names are stored after each other in the same buffer (the
466+
PathBuffer) and both must be NUL-terminated.
467+
- There are four members defining their respective offset and length
468+
inside PathBuffer: SubstituteNameOffset, SubstituteNameLength,
469+
PrintNameOffset and PrintNameLength.
470+
- The total size we need to allocate for the REPARSE_DATA_BUFFER, thus,
471+
is the sum of:
472+
- the fixed header size (REPARSE_DATA_BUFFER_HEADER_SIZE)
473+
- the size of the MountPointReparseBuffer member without the PathBuffer
474+
- the size of the prefix ("\??\") in bytes
475+
- the size of the print name in bytes
476+
- the size of the substitute name in bytes
477+
- the size of two NUL terminators in bytes */
478+
rdb_size = REPARSE_DATA_BUFFER_HEADER_SIZE +
479+
sizeof(rdb->MountPointReparseBuffer) -
480+
sizeof(rdb->MountPointReparseBuffer.PathBuffer) +
481+
/* Two +1's for NUL terminators. */
482+
(prefix_len + print_len + 1 + print_len + 1) * sizeof(WCHAR);
483+
rdb = (PREPARSE_DATA_BUFFER)PyMem_RawMalloc(rdb_size);
484+
if (rdb == NULL)
485+
goto cleanup;
486+
487+
memset(rdb, 0, rdb_size);
488+
rdb->ReparseTag = IO_REPARSE_TAG_MOUNT_POINT;
489+
rdb->ReparseDataLength = rdb_size - REPARSE_DATA_BUFFER_HEADER_SIZE;
490+
rdb->MountPointReparseBuffer.SubstituteNameOffset = 0;
491+
rdb->MountPointReparseBuffer.SubstituteNameLength =
492+
(prefix_len + print_len) * sizeof(WCHAR);
493+
rdb->MountPointReparseBuffer.PrintNameOffset =
494+
rdb->MountPointReparseBuffer.SubstituteNameLength + sizeof(WCHAR);
495+
rdb->MountPointReparseBuffer.PrintNameLength = print_len * sizeof(WCHAR);
496+
497+
/* Store the full native path of link target at the substitute name
498+
offset (0). */
499+
wcscpy(rdb->MountPointReparseBuffer.PathBuffer, L"\\??\\");
500+
if (GetFullPathNameW(src_path, print_len + 1,
501+
rdb->MountPointReparseBuffer.PathBuffer + prefix_len,
502+
NULL) == 0)
503+
goto cleanup;
504+
505+
/* Copy everything but the native prefix to the print name offset. */
506+
wcscpy(rdb->MountPointReparseBuffer.PathBuffer +
507+
prefix_len + print_len + 1,
508+
rdb->MountPointReparseBuffer.PathBuffer + prefix_len);
509+
510+
/* Create a directory for the junction point. */
511+
if (!CreateDirectoryW(dst_path, NULL))
512+
goto cleanup;
513+
514+
junction = CreateFileW(dst_path, GENERIC_READ | GENERIC_WRITE, 0, NULL,
515+
OPEN_EXISTING,
516+
FILE_FLAG_OPEN_REPARSE_POINT | FILE_FLAG_BACKUP_SEMANTICS, NULL);
517+
if (junction == INVALID_HANDLE_VALUE)
518+
goto cleanup;
519+
520+
/* Make the directory entry a junction point. */
521+
if (!DeviceIoControl(junction, FSCTL_SET_REPARSE_POINT, rdb, rdb_size,
522+
NULL, 0, &ret, NULL))
523+
goto cleanup;
524+
525+
cleanup:
526+
ret = GetLastError();
527+
528+
CloseHandle(token);
529+
CloseHandle(junction);
530+
PyMem_RawFree(rdb);
531+
532+
if (ret != 0)
533+
return PyErr_SetFromWindowsErr(ret);
534+
535+
Py_RETURN_NONE;
536+
}
537+
403538
static PyObject *
404539
winapi_CreateNamedPipe(PyObject *self, PyObject *args)
405540
{
@@ -1225,6 +1360,8 @@ static PyMethodDef winapi_functions[] = {
12251360
METH_VARARGS | METH_KEYWORDS, ""},
12261361
{"CreateFile", winapi_CreateFile, METH_VARARGS,
12271362
""},
1363+
{"CreateJunction", winapi_CreateJunction, METH_VARARGS,
1364+
""},
12281365
{"CreateNamedPipe", winapi_CreateNamedPipe, METH_VARARGS,
12291366
""},
12301367
{"CreatePipe", winapi_CreatePipe, METH_VARARGS,

Modules/posixmodule.c

Lines changed: 9 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,8 @@
2727
#include "Python.h"
2828
#ifndef MS_WINDOWS
2929
#include "posixmodule.h"
30+
#else
31+
#include "winreparse.h"
3032
#endif
3133

3234
#ifdef __cplusplus
@@ -301,6 +303,9 @@ extern int lstat(const char *, struct stat *);
301303
#ifndef IO_REPARSE_TAG_SYMLINK
302304
#define IO_REPARSE_TAG_SYMLINK (0xA000000CL)
303305
#endif
306+
#ifndef IO_REPARSE_TAG_MOUNT_POINT
307+
#define IO_REPARSE_TAG_MOUNT_POINT (0xA0000003L)
308+
#endif
304309
#include "osdefs.h"
305310
#include <malloc.h>
306311
#include <windows.h>
@@ -1109,41 +1114,6 @@ _PyVerify_fd_dup2(int fd1, int fd2)
11091114
#endif
11101115

11111116
#ifdef MS_WINDOWS
1112-
/* The following structure was copied from
1113-
http://msdn.microsoft.com/en-us/library/ms791514.aspx as the required
1114-
include doesn't seem to be present in the Windows SDK (at least as included
1115-
with Visual Studio Express). */
1116-
typedef struct _REPARSE_DATA_BUFFER {
1117-
ULONG ReparseTag;
1118-
USHORT ReparseDataLength;
1119-
USHORT Reserved;
1120-
union {
1121-
struct {
1122-
USHORT SubstituteNameOffset;
1123-
USHORT SubstituteNameLength;
1124-
USHORT PrintNameOffset;
1125-
USHORT PrintNameLength;
1126-
ULONG Flags;
1127-
WCHAR PathBuffer[1];
1128-
} SymbolicLinkReparseBuffer;
1129-
1130-
struct {
1131-
USHORT SubstituteNameOffset;
1132-
USHORT SubstituteNameLength;
1133-
USHORT PrintNameOffset;
1134-
USHORT PrintNameLength;
1135-
WCHAR PathBuffer[1];
1136-
} MountPointReparseBuffer;
1137-
1138-
struct {
1139-
UCHAR DataBuffer[1];
1140-
} GenericReparseBuffer;
1141-
};
1142-
} REPARSE_DATA_BUFFER, *PREPARSE_DATA_BUFFER;
1143-
1144-
#define REPARSE_DATA_BUFFER_HEADER_SIZE FIELD_OFFSET(REPARSE_DATA_BUFFER,\
1145-
GenericReparseBuffer)
1146-
#define MAXIMUM_REPARSE_DATA_BUFFER_SIZE ( 16 * 1024 )
11471117

11481118
static int
11491119
win32_get_reparse_tag(HANDLE reparse_point_handle, ULONG *reparse_tag)
@@ -4492,7 +4462,10 @@ BOOL WINAPI Py_DeleteFileW(LPCWSTR lpFileName)
44924462
find_data_handle = FindFirstFileW(lpFileName, &find_data);
44934463

44944464
if(find_data_handle != INVALID_HANDLE_VALUE) {
4495-
is_link = find_data.dwReserved0 == IO_REPARSE_TAG_SYMLINK;
4465+
/* IO_REPARSE_TAG_SYMLINK if it is a symlink and
4466+
IO_REPARSE_TAG_MOUNT_POINT if it is a junction point. */
4467+
is_link = find_data.dwReserved0 == IO_REPARSE_TAG_SYMLINK ||
4468+
find_data.dwReserved0 == IO_REPARSE_TAG_MOUNT_POINT;
44964469
FindClose(find_data_handle);
44974470
}
44984471
}

Modules/winreparse.h

Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
1+
#ifndef Py_WINREPARSE_H
2+
#define Py_WINREPARSE_H
3+
4+
#ifdef MS_WINDOWS
5+
#include <Windows.h>
6+
7+
#ifdef __cplusplus
8+
extern "C" {
9+
#endif
10+
11+
/* The following structure was copied from
12+
http://msdn.microsoft.com/en-us/library/ff552012.aspx as the required
13+
include doesn't seem to be present in the Windows SDK (at least as included
14+
with Visual Studio Express). */
15+
typedef struct _REPARSE_DATA_BUFFER {
16+
ULONG ReparseTag;
17+
USHORT ReparseDataLength;
18+
USHORT Reserved;
19+
union {
20+
struct {
21+
USHORT SubstituteNameOffset;
22+
USHORT SubstituteNameLength;
23+
USHORT PrintNameOffset;
24+
USHORT PrintNameLength;
25+
ULONG Flags;
26+
WCHAR PathBuffer[1];
27+
} SymbolicLinkReparseBuffer;
28+
29+
struct {
30+
USHORT SubstituteNameOffset;
31+
USHORT SubstituteNameLength;
32+
USHORT PrintNameOffset;
33+
USHORT PrintNameLength;
34+
WCHAR PathBuffer[1];
35+
} MountPointReparseBuffer;
36+
37+
struct {
38+
UCHAR DataBuffer[1];
39+
} GenericReparseBuffer;
40+
};
41+
} REPARSE_DATA_BUFFER, *PREPARSE_DATA_BUFFER;
42+
43+
#define REPARSE_DATA_BUFFER_HEADER_SIZE FIELD_OFFSET(REPARSE_DATA_BUFFER,\
44+
GenericReparseBuffer)
45+
#define MAXIMUM_REPARSE_DATA_BUFFER_SIZE ( 16 * 1024 )
46+
47+
#ifdef __cplusplus
48+
}
49+
#endif
50+
51+
#endif /* MS_WINDOWS */
52+
53+
#endif /* !Py_WINREPARSE_H */

0 commit comments

Comments
 (0)