[Python] [Windows] NTFS のシンボリックリンク、ハードリンク、ジャンクションの情報

NTFS においてシンボリックリンクやらハードリンクやらはリパースポイントという機能で実装されています。

パスがリパースポイントかどうか判別する

from ctypes import *

FILE_ATTRIBUTE_REPARSE_POINT = 0x00000400

kernel = windll.kernel32
GetFileAttributes = kernel.GetFileAttributesW
GetFileAttributes.argtypes = [c_wchar_p]

def islink(path):
    attributes = GetFileAttributes(path)
    return attributes != -1 and bool(attributes & FILE_ATTRIBUTE_REPARSE_POINT)

リンク先を取得する

from os.path import *
from ctypes import *
from ctypes.wintypes import *


OPEN_EXISTING = 0x3
FILE_FLAG_BACKUP_SEMANTICS = 0x02000000
FILE_FLAG_OPEN_REPARSE_POINT = 0x00200000

FSCTL_GET_REPARSE_POINT = 0x000900A8
MAXIMUM_REPARSE_DATA_BUFFER_SIZE = 16 * 1024
IO_REPARSE_TAG_MOUNT_POINT = 0xA0000003L
IO_REPARSE_TAG_SYMLINK = 0xA000000CL


class REPARSE_DATA_BUFFER(Structure):

    class Data(Union):

        class SymbolicLinkReparseBuffer(Structure):
            _fields_ = [
                ('SubstituteNameOffset', WORD, ),
                ('SubstituteNameLength', WORD, ),
                ('PrintNameOffset', WORD, ),
                ('PrintNameLength', WORD, ),
                ('Flags', ULONG, ),
                ('PathBuffer', WCHAR*MAXIMUM_REPARSE_DATA_BUFFER_SIZE, ),
                ]

        class MountPointReparseBuffer(Structure):
            _fields_ = [
                ('SubstituteNameOffset', WORD, ),
                ('SubstituteNameLength', WORD, ),
                ('PrintNameOffset', WORD, ),
                ('PrintNameLength', WORD, ),
                ('PathBuffer', WCHAR*MAXIMUM_REPARSE_DATA_BUFFER_SIZE, ),
                ]

        class GenericReparseBuffer(Structure):
            _fields_ = [
                ('DataBuffer', BYTE*MAXIMUM_REPARSE_DATA_BUFFER_SIZE, ),
                ]

        _fields_ = [
            ('SymbolicLinkReparseBuffer', SymbolicLinkReparseBuffer, ),
            ('MountPointReparseBuffer', MountPointReparseBuffer, ),
            ('GenericReparseBuffer', GenericReparseBuffer, ),
            ]

    _fields_ = [
        ('ReparseTag', DWORD, ),
        ('ReparseDataLength', WORD, ),
        ('Reserved', WORD, ),
        ('Data', Data, ),
        ]
    _anonymous_ = ('Data', )


kernel = windll.kernel32
CreateFile = kernel.CreateFileW
CloseHandle = kernel.CloseHandle
DeviceIoControl = kernel.DeviceIoControl
GetVolumeNameForVolumeMountPoint = kernel.GetVolumeNameForVolumeMountPointW
GetVolumeNameForVolumeMountPoint.argtypes = [c_wchar_p, c_wchar_p, c_ulong, ]
GetVolumeNameForVolumeMountPoint.restype = bool


def GetVolumeNameForVolumeMountPoint(path):
    if not path.endswith(sep): path += sep
    volume = (c_wchar * 1024)()
    if kernel.GetVolumeNameForVolumeMountPointW(path, volume, len(volume)):
        return volume.value
    else:
        return u''


def ismount(path):
    return bool(GetVolumeNameForVolumeMountPoint(path))


def readlink(path):

    if ismount(path):
        return path

    data = REPARSE_DATA_BUFFER()

    handle = CreateFile(c_wchar_p(path), 0, 0, None, OPEN_EXISTING,
        FILE_FLAG_BACKUP_SEMANTICS | FILE_FLAG_OPEN_REPARSE_POINT, None)
    try:
        DeviceIoControl(handle, FSCTL_GET_REPARSE_POINT, None, 0,
            byref(data), 1024, byref(c_ulong()), None)
    finally:
        CloseHandle(handle)

    if data.ReparseTag == IO_REPARSE_TAG_MOUNT_POINT:
        info = data.MountPointReparseBuffer
        start = info.SubstituteNameOffset / sizeof(c_wchar)
        end = start + info.SubstituteNameLength / sizeof(c_wchar)
        src = info.PathBuffer[start:end]

    elif data.ReparseTag == IO_REPARSE_TAG_SYMLINK:
        info = data.SymbolicLinkReparseBuffer
        start = info.SubstituteNameOffset / sizeof(c_wchar)
        end = start + info.SubstituteNameLength / sizeof(c_wchar)
        src = info.PathBuffer[start:end]
        #info.Flags    # 0x0: abs, 0x1: rel

    else:
        raise IOError(0, 'Target object is not link object.', path)

    if src.startswith(u'\\??\\'):
        src = src[4:]

    return normpath(src)

リンク先取得するだけで DeviceIoControl かよ……