packages feed

Win32-junction-point (empty) → 0.0.1

raw patch · 4 files changed

+416/−0 lines, 4 filesdep +Win32dep +basedep +textsetup-changed

Dependencies added: Win32, base, text

Files

+ LICENSE view
@@ -0,0 +1,31 @@+Copyright (c) 2011, Michael Steele
+Copyright (c) 2000, Mikael Nordell
+
+All rights reserved.
+
+Redistribution and use in source and binary forms, with or without
+modification, are permitted provided that the following conditions are met:
+
+    * Redistributions of source code must retain the above copyright
+      notice, this list of conditions and the following disclaimer.
+
+    * Redistributions in binary form must reproduce the above
+      copyright notice, this list of conditions and the following
+      disclaimer in the documentation and/or other materials provided
+      with the distribution.
+
+    * Neither the name of Michael Steele nor the names of other
+      contributors may be used to endorse or promote products derived
+      from this software without specific prior written permission.
+
+THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ Setup.hs view
@@ -0,0 +1,2 @@+import Distribution.Simple
+main = defaultMain
+ System/Win32/JunctionPoint.hs view
@@ -0,0 +1,331 @@+{-# LANGUAGE ForeignFunctionInterface #-}
+
+module System.Win32.JunctionPoint
+    ( createJunctionPoint
+    , deleteJunctionPoint
+    , getJunctionPointInfo
+    ) where
+
+import Control.Exception (bracket)
+import Data.Bits
+import Data.Char (chr)
+import Data.Text (Text)
+import qualified Data.Text as T
+import Data.Text.Foreign
+import Foreign
+import Foreign.C
+import System.Win32 hiding (createFile)
+
+-- Macro taken from winioctl.h
+-- #define CTL_CODE( DeviceType, Function, Method, Access) ( \
+--     (DWORD) ((DeviceType) << 16 | ((Access) << 14) | ((Function) << 2) \
+--     | (Method))
+cTL_CODE :: DWORD -> DWORD -> DWORD -> DWORD -> DWORD
+cTL_CODE d f m a = d `shift` 16 .|. a `shift` 14 .|. f `shift` 2 .|. m
+
+-- taken from winioctl.h
+fILE_DEVICE_FILE_SYSTEM :: DWORD
+fILE_DEVICE_FILE_SYSTEM = 0x00000009
+
+-- taken from winioctl.h
+mETHOD_BUFFERED :: DWORD
+mETHOD_BUFFERED = 0
+
+-- taken from winioctl.h
+fILE_ANY_ACCESS, fILE_SPECIAL_ACCESS :: DWORD
+fILE_ANY_ACCESS     = 0
+fILE_SPECIAL_ACCESS = 0
+
+-- taken from winioctl.h
+fSCTL_SET_REPARSE_POINT :: DWORD
+fSCTL_SET_REPARSE_POINT = cTL_CODE fILE_DEVICE_FILE_SYSTEM 41
+    mETHOD_BUFFERED fILE_SPECIAL_ACCESS
+fSCTL_GET_REPARSE_POINT :: DWORD
+fSCTL_GET_REPARSE_POINT = cTL_CODE fILE_DEVICE_FILE_SYSTEM 42
+    mETHOD_BUFFERED fILE_ANY_ACCESS
+fSCTL_DELETE_REPARSE_POINT :: DWORD
+fSCTL_DELETE_REPARSE_POINT = cTL_CODE fILE_DEVICE_FILE_SYSTEM 43
+    mETHOD_BUFFERED fILE_SPECIAL_ACCESS
+
+iO_REPARSE_TAG_MOUNT_POINT :: DWORD
+iO_REPARSE_TAG_MOUNT_POINT = 0xA0000003
+
+--taken from MSDN's OpenFileById documentation
+fILE_FLAG_OPEN_REPARSE_POINT :: DWORD
+fILE_FLAG_OPEN_REPARSE_POINT = 0x00200000
+
+mAXIMUM_REPARSE_DATA_BUFFER_SIZE :: Int
+mAXIMUM_REPARSE_DATA_BUFFER_SIZE = 16 * 1024
+
+-- The first 3 fields of TMN_REPARSE_DATA_BUFFER are considered the
+-- header. This is used in setReparsePoint.
+tMN_REPARSE_DATA_BUFFER_HEADER_SIZE :: DWORD
+tMN_REPARSE_DATA_BUFFER_HEADER_SIZE = 8
+
+-- C structure. This is not documented in current versions of the
+-- Windows SDK.
+
+-- struct TMN_REPARSE_DATA_BUFFER
+-- {
+--     DWORD  ReparseTag;
+--     WORD   ReparseDataLength;
+--     WORD   Reserved;
+--     WORD   SubstituteNameOffset;
+--     WORD   SubstituteNameLength;
+--     WORD   PrintNameOffset;
+--     WORD   PrintNameLength;
+--     WCHAR  PathBuffer[1];
+-- };
+
+data TMN_REPARSE_DATA_BUFFER = TMN_REPARSE_DATA_BUFFER
+    { _reparseTag           :: !DWORD
+    -- Bytes in addition to first 8. This will be 12 + length of _pathBuffer
+    , _reparseDataLength    :: !WORD
+    , _reserved             :: !WORD
+    , _substituteNameOffset :: !WORD
+    , _substituteNameLength :: !WORD
+    , _printNameOffset      :: !WORD
+    , _printNameLength      :: !WORD
+    -- The C struct indicates a single-element array.
+    -- Actually, we need to allocate enough memory to
+    -- hold a string here.
+    , _pathBuffer :: !(Ptr CWchar)
+    }
+
+instance Storable TMN_REPARSE_DATA_BUFFER where
+  -- Storable does not allow the pokes of different sizes, so we're just
+  -- allocating the maximum size Microsoft documents.
+  sizeOf _    = mAXIMUM_REPARSE_DATA_BUFFER_SIZE
+  alignment _ = 1 -- no alignment? I don't know what this means.
+  peek ptr    = do
+    reparseTag           <- peek . castPtr $ ptr
+    reparseDataLength    <- castPtr ptr `peekByteOff` 4
+    reserved             <- castPtr ptr `peekByteOff` 6
+    substituteNameOffset <- castPtr ptr `peekByteOff` 8
+    substituteNameLength <- castPtr ptr `peekByteOff` 10
+    printNameOffset      <- castPtr ptr `peekByteOff` 12
+    printNameLength      <- castPtr ptr `peekByteOff` 14
+    let pathBuffer        = castPtr ptr `plusPtr`     16
+    return $ TMN_REPARSE_DATA_BUFFER reparseTag reparseDataLength reserved
+           substituteNameOffset substituteNameLength printNameOffset
+           printNameLength pathBuffer
+  poke ptr rdb = do
+    castPtr ptr `poke` _reparseTag rdb
+    castPtr ptr `pokeByteOff` 4  $ _reparseDataLength rdb
+    castPtr ptr `pokeByteOff` 6  $ _reserved rdb
+    castPtr ptr `pokeByteOff` 8  $ _substituteNameOffset rdb
+    castPtr ptr `pokeByteOff` 10 $ _substituteNameLength rdb
+    castPtr ptr `pokeByteOff` 12 $ _printNameOffset rdb
+    castPtr ptr `pokeByteOff` 14 $ _printNameLength rdb
+    strLen <- lengthArray0 0 $ _pathBuffer rdb
+    copyArray (ptr `plusPtr` 16) (_pathBuffer rdb) (strLen + 1)
+
+-- This data structure is almost identical to TMN_REPARSE_DATA_BUFFER, but the
+-- meaning of fields differ. Microsoft documents this structure. It is
+-- used for querying existing reparse points.
+data REPARSE_GUID_DATA_BUFFER = REPARSE_GUID_DATA_BUFFER
+    { _rgdb_ReparseTag :: DWORD
+    -- Bytes used by _rgdb_DataBuffer. Contrast this with TMN_REPARSE_DATA_BUFFER.
+    , _rgdb_ReparseDataLength :: !WORD
+    , _rgdb_Reserved          :: !WORD
+    -- MS specifies an odd structure, but a GUID is really just a 128-bit
+    -- value.
+    , _rgdb_GUID1             :: !DWORD
+    , _rgdb_GUID2             :: !DWORD
+    , _rgdb_GUID3             :: !DWORD
+    , _rgdb_GUID4             :: !DWORD
+    -- The C struct indicates a single-element array.
+    -- Actually, we need to allocate enough memory to
+    -- hold a string here.
+    , _rgdb_DataBuffer :: !(Ptr BYTE)
+    }
+
+instance Storable REPARSE_GUID_DATA_BUFFER where
+  -- Storable does not allow the pokes of different sizes, so we're just
+  -- allocating the maximum size Microsoft documents.
+  sizeOf _ = mAXIMUM_REPARSE_DATA_BUFFER_SIZE
+  alignment _ = 1 -- no alignment? I don't know what this means.
+  peek ptr = do
+    _rgdb_ReparseTag        <- peek . castPtr $ ptr
+    _rgdb_ReparseDataLength <- castPtr ptr `peekByteOff` 4
+    _rgdb_Reserved          <- castPtr ptr `peekByteOff` 6
+    _rgdb_GUID1             <- castPtr ptr `peekByteOff` 8
+    _rgdb_GUID2             <- castPtr ptr `peekByteOff` 12
+    _rgdb_GUID3             <- castPtr ptr `peekByteOff` 16
+    _rgdb_GUID4             <- castPtr ptr `peekByteOff` 20
+    let dataBuffer           = castPtr ptr `plusPtr`     24
+    return $ REPARSE_GUID_DATA_BUFFER _rgdb_ReparseTag
+           _rgdb_ReparseDataLength _rgdb_Reserved _rgdb_GUID1 _rgdb_GUID2
+           _rgdb_GUID3 _rgdb_GUID4 dataBuffer
+  poke ptr rdb = do
+    castPtr ptr `poke` _rgdb_ReparseTag rdb
+    castPtr ptr `pokeByteOff` 4  $ _rgdb_ReparseDataLength rdb
+    castPtr ptr `pokeByteOff` 6  $ _rgdb_Reserved rdb
+    castPtr ptr `pokeByteOff` 8  $ _rgdb_GUID1 rdb
+    castPtr ptr `pokeByteOff` 12 $ _rgdb_GUID2 rdb
+    castPtr ptr `pokeByteOff` 16 $ _rgdb_GUID3 rdb
+    castPtr ptr `pokeByteOff` 20 $ _rgdb_GUID4 rdb
+    buffer <- peekArray bufferSize (_rgdb_DataBuffer rdb)
+    plusPtr ptr 24 `pokeArray` buffer
+    where
+      bufferSize = fromIntegral $ _rgdb_ReparseDataLength rdb - 24
+
+withTMN_REPARSE_DATA_BUFFER :: Text
+    -> (Ptr TMN_REPARSE_DATA_BUFFER -> IO a) -> IO a
+withTMN_REPARSE_DATA_BUFFER dst f =
+    useAsPtr0 dst $ \c_dst ->
+    with (TMN_REPARSE_DATA_BUFFER
+          { _reparseTag           = iO_REPARSE_TAG_MOUNT_POINT
+          , _reparseDataLength    = dstLen + 12
+          , _reserved             = 0
+          , _substituteNameOffset = 0
+          , _substituteNameLength = dstLen
+          , _printNameOffset      = dstLen + 2
+          , _printNameLength      = 0
+          , _pathBuffer           = c_dst
+          }) f
+  where
+    dstLen = fromIntegral (T.length dst) * 2
+
+withREPARSE_GUID_DATA_BUFFER :: [BYTE]
+    -> (Ptr REPARSE_GUID_DATA_BUFFER -> IO a) -> IO a
+withREPARSE_GUID_DATA_BUFFER bx f =
+    withArray bx $ \dataBuffer ->
+    with (REPARSE_GUID_DATA_BUFFER
+          { _rgdb_ReparseTag = iO_REPARSE_TAG_MOUNT_POINT
+          -- must be 0 when deleting a junction point
+          , _rgdb_ReparseDataLength = 0
+          , _rgdb_Reserved = 0
+          , _rgdb_GUID1 = 0
+          , _rgdb_GUID2 = 0
+          , _rgdb_GUID3 = 0
+          , _rgdb_GUID4 = 0
+          , _rgdb_DataBuffer = dataBuffer
+          }) f
+
+-- | Create a junction point between two folders on the same filesystem.
+--
+-- 'mountDir' should be the full file path to an empty folder. This
+-- folder should be on a local filesystem.
+--
+-- 'destDir' should be the full file path to the junction point's target. The
+-- target must be a folder on the same filesystem as its source. The path
+-- should also be preceeded by \"\\??\\\". This indicates to Windows not to try
+-- validating the path before using it.
+--
+-- >>> createJunctionPoint "c:\\Windows\\System32" "\\??\\c:\\System32"
+createJunctionPoint :: Text -- ^ mountDir
+                    -> Text -- ^ destDir
+                    -> IO ()
+createJunctionPoint mountDir destDir =
+    withTMN_REPARSE_DATA_BUFFER destDir $ \rdb ->
+    bracket (openReparseHandle mountDir) closeHandle $ \handle ->
+    setReparsePoint handle rdb
+
+-- | This "Deletes" the junction point at the supplied path. On success an
+-- empty folder will be left in its place.
+deleteJunctionPoint :: Text -> IO ()
+deleteJunctionPoint dir =
+    bracket (openReparseHandle dir) closeHandle $ \handle -> do
+        deleteReparsePoint handle
+
+-- | Discover the target of a junction point at the supplied path. An
+-- exception will be raised if the target is either invalid or not a junction
+-- point. The returned path will be prefixed by \"\\??\\\".
+getJunctionPointInfo :: Text -> IO Text
+getJunctionPointInfo dir =
+    bracket (openReparseHandle dir) closeHandle $ \handle ->
+    with (0 :: DWORD) $ \bytesReturned ->
+    withTMN_REPARSE_DATA_BUFFER (T.pack "") $ \pRdb -> do
+        deviceIoControl handle fSCTL_GET_REPARSE_POINT Nothing 0
+            (Just $ castPtr pRdb)
+            (fromIntegral mAXIMUM_REPARSE_DATA_BUFFER_SIZE)
+            (Just bytesReturned) Nothing
+        --not sure how to call IsReparseTagValid.
+        rdb <- peek pRdb
+        fromPtr0 $ (_pathBuffer rdb)
+
+-- NTFS junction points are implemented as reparse points. Reparse points are
+-- extra tagged information attached to filesystem objects. It is up to
+-- application software such as the Windows Shell to detect the presence of
+-- reparse points and act approprietly.
+-- 
+-- There are many types of reparse points. For the internal purposes of
+-- this library I am pretending that junction points are the only kind of
+-- reparse point.
+setReparsePoint :: HANDLE -> Ptr TMN_REPARSE_DATA_BUFFER -> IO ()
+setReparsePoint handle pRdb =
+    -- we don't care about bytesReturned
+    with (0 :: DWORD) $ \bytesReturned -> do
+        rdb <- peek pRdb
+        deviceIoControl handle fSCTL_SET_REPARSE_POINT
+            (Just $ castPtr pRdb)
+            (tMN_REPARSE_DATA_BUFFER_HEADER_SIZE +
+                (fromIntegral $ _reparseDataLength rdb))
+            Nothing 0 (Just bytesReturned) Nothing
+
+-- 'deleteReparsePoint' will remove a reparse point pointed to by the
+-- argument. An exception will be raised if the argument does not point to an
+-- open reparse point, or possibly if the user does not have write attribute
+-- permissions to the object.
+deleteReparsePoint :: HANDLE -> IO ()
+deleteReparsePoint handle =
+    -- When deleting reparse points the destination does not matter.
+    -- msdn docs refer to REPARSE_GUID_DATA_BUFFER for this instead.
+    withREPARSE_GUID_DATA_BUFFER [] $ \pRgdb ->
+    with (0 :: DWORD) $ \bytesReturned -> do
+        deviceIoControl handle fSCTL_DELETE_REPARSE_POINT
+            (Just $ castPtr pRgdb)
+            tMN_REPARSE_DATA_BUFFER_HEADER_SIZE
+            Nothing 0 (Just bytesReturned) Nothing
+
+-- Open a reparse point attached to the supplied folder. An exception will be
+-- raised if the target does not exist, the user does not have read
+-- permissions to it, or the target does not have a reparse point attached.
+openReparseHandle :: Text -> IO HANDLE
+openReparseHandle path = createFile path (gENERIC_READ .|. gENERIC_WRITE)
+    fILE_SHARE_NONE Nothing oPEN_EXISTING
+    (fILE_FLAG_BACKUP_SEMANTICS .|. fILE_FLAG_OPEN_REPARSE_POINT)
+    Nothing
+
+-- Low-level filesystem manipulation function. Check documentation in the MSDN
+-- library.
+deviceIoControl :: HANDLE -> DWORD -> Maybe LPVOID -> DWORD -> Maybe LPVOID
+    -> DWORD -> Maybe LPDWORD -> Maybe LPOVERLAPPED -> IO ()
+deviceIoControl hDevice dwIoControlCode lpInBuffer nInBufferSize
+    lpOutBuffer nOutBufferSize lpBytesReturned lpOverlapped =
+    failIfFalse_ (unwords [ "DeviceIoControl", show hDevice
+                          , show dwIoControlCode]) $
+        c_DeviceIoControl hDevice dwIoControlCode
+            (maybe nullPtr id lpInBuffer) nInBufferSize
+            (maybe nullPtr id lpOutBuffer) nOutBufferSize
+            (maybe nullPtr id lpBytesReturned)
+            (maybe nullPtr id lpOverlapped)
+
+foreign import stdcall unsafe "windows.h DeviceIoControl"
+    c_DeviceIoControl :: HANDLE -> DWORD -> LPVOID -> DWORD -> LPVOID
+        -> DWORD -> LPDWORD -> LPOVERLAPPED -> IO Bool
+
+createFile :: Text -> AccessMode -> ShareMode -> Maybe LPSECURITY_ATTRIBUTES
+    -> CreateMode -> FileAttributeOrFlag -> Maybe HANDLE -> IO HANDLE
+createFile name access share mb_attr mode flag mb_h =
+    -- simply converting Text to a name does not add a null character
+    useAsPtr0 name $ \ c_name ->
+    failIf (==iNVALID_HANDLE_VALUE) (unwords ["CreateFile", show name]) $
+    c_CreateFile c_name access share (maybePtr mb_attr) mode flag (maybePtr mb_h)
+
+-- | useAsPtr returns a length and byte buffer, but all the win32 functions
+-- rely on null termination.
+useAsPtr0 :: Text -> (Ptr CWchar -> IO a) -> IO a
+useAsPtr0 t f = useAsPtr (T.snoc t (chr 0x0)) $ \ str _ -> f  (castPtr str)
+
+-- This traverses the string twice. Is there a faster way?
+fromPtr0 :: Ptr CWchar -> IO Text
+fromPtr0 ptr = do
+    -- length in 16-bit words.
+    len <- lengthArray0 0x0000 ptr'
+    -- no loss of precision here. I16 is a newtype wrapper around Int.
+    fromPtr ptr' $ fromIntegral len
+  where
+    ptr' :: Ptr Word16
+    ptr' = castPtr ptr
+ Win32-junction-point.cabal view
@@ -0,0 +1,52 @@+Name:          Win32-junction-point
+Version:       0.0.1
+Synopsis:      Support for manipulating NTFS junction points.
+Description:
+  This package provides the ability to manipulate NTFS junction points as
+  supported by Windows 2000 and above. Junction points, along with NTFS hard
+  links and NTFS symbolic links, are a type of symbolic link that can be made
+  between folders existing on the same filesystem. Please read Microsoft
+  KB205524 [1] for more information on junction points.
+  .
+  Junction points have always been left undocumented in the Win32 SDK. The
+  Windows 2000 Resource Kit came with a command-line utility named linkd.exe
+  to work with them. Later, Mark Russinovich of SysInternals distributed a
+  replacement utility named Junction [2] which accomplished the same thing.
+  .
+  This source code is based on an article [3] and C++ library [4] written by
+  Mike Nordell at codeproject.com.
+  .
+  It is against Microsoft's recommendation to make use of undocumented API
+  features. Use this library at your own risk.
+  .
+  (1) <http://support.microsoft.com/?kbid=205524>
+  .
+  (2) <http://technet.microsoft.com/en-us/sysinternals/bb896768>
+  .
+  (3) <http://www.codeproject.com/script/Articles/ViewDownloads.aspx?aid=194>
+  .
+  (4) <http://www.codeproject.com/KB/winsdk/junctionpoints.aspx>
+License:       BSD3
+License-file:  LICENSE
+Author:        Michael Steele
+Maintainer:    mikesteele81@gmail.com
+Copyright:     Copyright (c) 2011, Michael Steele. Copyright (c) 2000, Mikael Nordell
+Category:      System
+Build-type:    Simple
+Cabal-version: >=1.2
+Homepage:      http://github.com/mikesteele81/Win32-junction-point
+Stability:     experimental
+
+Library
+  Exposed-modules: System.Win32.JunctionPoint
+
+  Build-depends: base  >= 4.0  && < 4.4
+               , text  >= 0.11 && < 0.12
+               , Win32 >= 2.2  && < 2.3
+
+  -- Modules not exported by this package.
+  -- Other-modules:
+
+  -- Extra tools (e.g. alex, hsc2hs, ...) needed to build the source.
+  -- Build-tools:
+  Ghc-Options: -funbox-strict-fields -Wall