packages feed

Win32-dhcp-server 0.1 → 0.2

raw patch · 9 files changed

+258/−113 lines, 9 filesdep +Win32-errorsdep +textdep −safe

Dependencies added: Win32-errors, text

Dependencies removed: safe

Files

+ ChangeLog view
@@ -0,0 +1,6 @@+0.2
+===
+
+* All functions throw Win32Exception exceptions.
+* strict Text values are favored over String
+* drop dependency on safe
Data/Ip.hs view
@@ -1,4 +1,5 @@ {-# LANGUAGE GeneralizedNewtypeDeriving #-}
+{-# LANGUAGE OverloadedStrings #-}
 
 -- | This module was taken, with modifications, from the
 -- <http://hackage.haskell.org/package/maccatcher maccatcher> package.
@@ -15,12 +16,16 @@     ) where
 
 import Control.Applicative
+import Control.Monad (unless)
 import Data.Bits
-import Data.List (intercalate)
+import Data.Text (Text)
+import qualified Data.Text as T
+import Data.Text.Read
 import Data.Word
 import Foreign
-import Safe
 
+import Utils (fmapL, note)
+
 -- |An Ip can be used as an IP address or subnet address.
 --
 -- A `Show` instance is omitted to avoid confusion. Use `showIp` and `readIp`
@@ -32,25 +37,30 @@ --
 -- >>> showIp $ fromOctets 192 168 1 100
 -- "192.168.1.100"
-showIp :: Ip -> String
-showIp ip = intercalate "." . map show $ [a, b, c, d]
+showIp :: Ip -> Text
+showIp ip = T.intercalate "." . map (T.pack . show) $ [a, b, c, d]
   where (a, b, c, d) = toOctets ip
 
--- |Parse a `String` value as an `Ip`. The string should be of the form
+-- |Parse a `Text` value as an `Ip`. The string should be of the form
 -- "X.X.X.X" where each 'X' is a decimal value between 0 and 255 inclusive.
 --
--- >>> let Just ip = readIp "192.168.1.100"
+-- >>> let Right ip = readIp "192.168.1.100"
 -- >>> toOctets ip
 -- (192, 168, 1, 100)
-readIp :: String -> Maybe Ip
-readIp s = case octets' s of
-             [a, b, c, d] -> fromOctets <$> readMay a <*> readMay b
-                                        <*> readMay c <*> readMay d
-             _ -> Nothing
-  where 
-    octets' os = case break (=='.') (dropWhile (=='.') os) of
-                  ("", _) -> []
-                  (o, os') -> o : octets' os'
+readIp :: Text -> Either String Ip
+readIp s = fmapL (\e -> "Error parsing IP address: " ++ e) $ do
+   (a, s2) <- decimal s
+   (b, s3) <- dot s2 >>= decimal
+   (c, s4) <- dot s3 >>= decimal
+   (d, s5) <- dot s4 >>= decimal
+   unless (s5 == "") $ Left "exactly 4 octets were expected."
+   -- Confirm that all digits are in range
+   fromOctets <$> digit a <*> digit b <*> digit c <*> digit d
+  where
+    dot = note "Expected '.' character." . T.stripPrefix "."
+    digit :: Int -> Either String Word8
+    digit x | x < 0 || x > 255 = Left "digit out of range."
+            | otherwise = Right $ fromIntegral x
 
 -- |An IP address is 32-bits wide. This function will construct an `Ip` from
 -- 4 octets.
Data/Mac.hs view
@@ -1,3 +1,5 @@+{-# LANGUAGE OverloadedStrings #-}
+
 -- | This module was taken, with modifications, from the
 -- <http://hackage.haskell.org/package/maccatcher maccatcher> package.
 module Data.Mac
@@ -13,13 +15,20 @@     ) where
 
 import Control.Applicative
+import Control.Monad (unless)
 import Data.Bits
-import Data.List
+import Data.Monoid
+import Data.Text (Text)
+import qualified Data.Text as T
+import qualified Data.Text.Lazy as L
+import qualified Data.Text.Lazy.Builder as L
+import qualified Data.Text.Lazy.Builder.Int as L
+import Data.Text.Read
 import Data.Word
 import Foreign
-import Numeric (showHex)
-import Safe
 
+import Utils
+
 -- |A Mac is a 6-byte unique identifier used in layer-two network addressing.
 -- Its `Storable` instance occupies 6 bytes of memory when `poke`d with the
 -- first byte occupying the lowest address, and the last byte occupying the
@@ -57,35 +66,41 @@ --
 -- >>> showMac ":" $ fromOctets 0x11 0x22 0x33 0x44 0x55 0x66
 -- "11:22:33:44:55:66"
-showMac :: String -> Mac -> String
-showMac sep mac = intercalate sep
-                . map (\n -> pad 2 $ showHex n "")
-                $ [a, b, c, d, e, f]
+showMac :: Text -> Mac -> Text
+showMac sep mac = T.intercalate sep . map octet $ [a, b, c, d, e, f]
   where
+    octet x = pad (2 :: Int) . L.toStrict . L.toLazyText . L.hexadecimal $ x
     (a, b, c, d, e, f) = toOctets mac
-
-pad :: Int -> String -> String
-pad n str = replicate zeroes '0' ++ str
-  where
-    zeroes = max 0 (n - length str)
+    pad n str = T.replicate (max 0 (n - T.length str)) "0" <> str
 
--- |Parse a `String` value as a `Mac`. The string should not use any
+-- |Parse a `Text` value as a `Mac`. The string should not use any
 -- separators between octets.
 --
--- >>> let Just mac = readMac "000102030405"
+-- >>> let Right mac = readMac "000102030405"
 -- >>> toOctets mac
 -- (0, 1, 2, 3, 4, 5)
-readMac :: String -> Maybe Mac
-readMac str = case sequence (go str) of
-    Just [a, b, c, d, e, f] -> Just $ fromOctets a b c d e f
-    _ -> Nothing
-   where
-    go :: String -> [Maybe Word8]
-    -- There should always be 2 characters
-    go [_] = [Nothing]
-    -- We've reached the end
-    go [] = []
-    go s = let (b, rest) = splitAt 2 s in readMay ("0x" ++ b) : go rest
+readMac :: Text -> Either String Mac
+readMac s = fmapL (\e -> "Error parsing MAC address: " ++ e) $ do
+    (a, s2) <- octet s
+    (b, s3) <- octet s2
+    (c, s4) <- octet s3
+    (d, s5) <- octet s4
+    (e, s6) <- octet s5
+    (f, s7) <- octet s6
+    unless (s7 == "") $ Left "exactly 6 octets were expected."
+    fromOctets <$> digit a <*> digit b <*> digit c
+               <*> digit d <*> digit e <*> digit f
+  where
+    octet :: Text -> Either String (Int, Text)
+    octet s0 = do
+        (a, s2) <- hexadecimal a0
+        unless (s2 == "") $ Left "invalid characters"
+        return (a, rest)
+      where
+        (a0, rest) = T.splitAt 2 s0
+    digit :: Int -> Either String Word8
+    digit x | x < 0 || x > 255 = Left "digit out of range."
+            | otherwise = Right $ fromIntegral x
 
 -- |A Mac address is 48-bits wide. This function will construct a `Mac` from
 -- 6 octets.
System/Win32/DHCP.hs view
@@ -1,3 +1,12 @@+{-# LANGUAGE OverloadedStrings #-}
+
+-- |
+-- module: System.Win32.DHCP
+-- copyright: (c) Michael Steele, 2014
+-- license: BSD3
+-- maintainer: mikesteele81@gmail.com
+-- stability: experimental
+-- portability: Windows
 module System.Win32.DHCP
     ( DhcpApi ()
     , loadDHCP
@@ -20,14 +29,20 @@     , removeReservation
     ) where
 
+import Control.Applicative
 import Control.Monad (unless)
+import Data.Char (chr)
+import Data.Text (Text)
+import qualified Data.Text as T
+import qualified Data.Text.Foreign as T
 import Foreign
 import Foreign.C.Types
 
-import System.Win32.Types
+import System.Win32.Types hiding (withTString, withTStringLen)
+import qualified System.Win32.Error as E
+import qualified System.Win32.Error.Foreign as E
 
 import Data.Ip
-
 import System.Win32.DHCP.DhcpStructure
 import System.Win32.DHCP.HOST_INFO
 import System.Win32.DHCP.Internal
@@ -47,7 +62,7 @@ data Context = Context
     { -- | The DHCP server management API uses an RPC mechanism to control the
       -- server.
-      contextServer :: !String
+      contextServer :: !Text
     , -- | Scopes are identified by a subnet identifier. This is useful in
       -- cases where multiple scopes are defined, but still required in the
       -- common case of a single scope.
@@ -60,21 +75,27 @@ -- with that name. This action corresponds to MSDN's DhcpDeleteClientInfoV4
 -- function.
 deleteClient :: DhcpApi
-    -> String
-    -- ^ Unicode string that specifies the IP address or hostname of the DHCP
+    -> Text
+    -- ^ String which specifies the IP address or hostname of the DHCP
     -- server.
     -> SEARCH_INFO
     -- ^ Define how to lookup a client to delete. Only deleting based on
     -- IP addresses have been tested.
     -> IO ()
-    -- ^ This action may throw exceptions. According to Microsoft's protocol
-    -- specification an ERROR_DHCP_JET_ERROR (0x00004E2D) will be returned
-    -- if no client was found on the server.
+    -- ^ This function will throw an 'E.Win32Exception' when the internal Win32
+    -- call returnes an error condition. MSDN lists the following exceptions,
+    -- but others might be thrown as well:
+    --
+    --   ['E.DhcpReservedClient'] The specified DHCP client is a reserved DHCP
+    --   client.
+    --
+    --   ['E.DhcpJetError'] An error occurred while accessing the DHCP server
+    --   database, or the client entry is not present in the database.
 deleteClient api server si =
     withTString server $ \pserver ->
-    withSearchInfo si $ \psi -> do
-    failUnlessSuccess (unwords ["DeleteClientInfo", server, show si])
-        $ c_DeleteClientInfo api pserver psi
+    withSearchInfo si $ \psi ->
+    E.failUnlessSuccess "DeleteClientInfo"
+    $ c_DeleteClientInfo api pserver psi
 
 -- | Perform a lookup operation for all client lease records within a
 -- scope. This action corresponds to MSDN's DhcpEnumSubnetClientsV4 function.
@@ -83,8 +104,12 @@     -- ^ Specify which server and scope to search for client leases.
     -> IO [Client]
     -- ^ The empty list means that no client records exist for the provided
-    -- subnet. An exception will be thrown if the provided server or subnet
-    -- does not exist, or any other error occurs.
+    -- subnet. This function will throw an 'E.Win32Exception' when the
+    -- internal Win32 call returnes an error condition. MSDN lists the
+    -- following exceptions, but others might be thrown as well:
+    --
+    --   ['E.DhcpJetError'] An error occurred while accessing the DHCP server
+    --   database.
 enumClients dhcp (Context server subnet) =
     enumSubnetClientsV4 dhcp server subnet
 
@@ -92,7 +117,7 @@ -- `Nothing` is returned when no lease was found. This corresponds to MSDN's
 -- DhcpGetClientInfoV4 function.
 lookupClient :: DhcpApi
-    -> String
+    -> Text
     -- ^ According to MSDN this must specify the IP address of the server.
     -- Other functions (including this one) may or may not also accept
     -- a Unicode host name.
@@ -100,11 +125,12 @@     -- ^ Define how to lookup a client. Only searching based on an
     -- IP addresses has been tested.
     -> IO (Maybe Client)
-    -- ^ A `Nothing` indicates that no client was found. An exception will
-    -- be thrown if any internal error occurs. Though MSDN documents don't say
-    -- so, an ERROR_DHCP_JET_ERROR (0x00004E2D) may be thrown if no client
-    -- record is found. The reason I think this is that other functions are
-    -- said to behave like this.
+    -- ^ A `Nothing` indicates that no client was found. This function will throw an 'E.Win32Exception' when the
+    -- internal Win32 call returnes an error condition. MSDN lists the
+    -- following exceptions, but others might be thrown as well:
+    --
+    --   ['E.DhcpJetError'] An error occurred while accessing the DHCP server
+    --   database.
 lookupClient api serverip si =
     withTString serverip $ \pserverip ->
     withSearchInfo si $ \psi ->
@@ -112,7 +138,7 @@     -- DhcpGetClientInfo takes a structure on the stack.
     siType <- peek (castPtr psi :: Ptr CInt)
     siPayload <- peekElemOff (castPtr psi :: Ptr (Ptr ())) 4
-    failUnlessSuccess (unwords ["GetClientInfoV4", serverip, show si])
+    E.failUnlessSuccess "GetClientInfoV4"
         $ c_GetClientInfoV4  api pserverip siType siPayload ppclientinfo
     pclientinfo <- peek ppclientinfo
     if pclientinfo == nullPtr
@@ -123,7 +149,28 @@         poke (castPtr pclientinfo) nullPtr
         return $ Just clientinfo
 
-addReservation :: DhcpApi -> Context -> Mapping -> IO ()
+addReservation :: DhcpApi -> Context -> Mapping
+    -> IO ()
+    -- ^ This function will throw an 'E.Win32Exception' when the internal Win32
+    -- call returnes an error condition. MSDN lists the following exceptions,
+    -- but others might be thrown as well:
+    --
+    --   ['E.DhcpSubnetNotPresent'] The specified IPv4 subnet does not exist
+    --   on the DHCP server.
+    --
+    --   ['E.DhcpJetError'] An error occurred while accessing the DHCP server
+    --   database.
+    --
+    --   ['E.DhcpNotReservedClient'] The specified DHCP client is not a
+    --   reserved client.
+    --
+    --   ['E.DhcpInvalidRange'] The specified IPv4 range does not match an
+    --   existing IPv4 range.
+    --
+    --   ['E.ScopeRangePolicyChangeConflict'] An IP address range is
+    --   configured for a policy in this scope. This operation cannot be
+    --   performed on the scope IP address range until the policy IP address
+    --   range is suitably modified.
 addReservation dhcp (Context server subnet) mapping =
     addSubnetElementV4 dhcp server subnet
     $ ReservedIps (Reservation mapping Both)
@@ -145,13 +192,33 @@     -- ^ Specify whether any active leases for the reservation should be
     -- removed as well.
     -> IO ()
+    -- ^ This function will throw an 'E.Win32Exception' when the internal Win32
+    -- call returnes an error condition. MSDN lists the following exceptions,
+    -- but others might be thrown as well:
+    --
+    --   ['E.DhcpSubnetNotPresent'] The specified IPv4 subnet does not exist
+    --   on the DHCP server.
+    --
+    --   ['E.DhcpElementCantRemove'] Failure can occur for any number of
+    --   reasons.
+    --
+    --   ['E.DhcpJetError'] An error occurred while accessing the DHCP server
+    --   database.
+    --
+    --   ['E.DhcpInvalidRange'] The specified IPv4 range does not match an
+    --   existing IPv4 range.
+    --
+    --   ['E.ScopeRangePolicyChangeConflict'] An IP address range is
+    --   configured for a policy in this scope. This operation cannot be
+    --   performed on the scope IP address range until the policy IP address
+    --   range is suitably modified.
 removeReservation dhcp (Context server subnet) mapping ct force =
     removeSubnetElementV4 dhcp server subnet
     (ReservedIps $ Reservation mapping ct) fForce
  where
    fForce = if force then FullForce else NoForce
 
-enumSubnetClientsV4 :: DhcpApi -> String -> Ip -> IO [Client]
+enumSubnetClientsV4 :: DhcpApi -> Text -> Ip -> IO [Client]
 enumSubnetClientsV4 dhcp server subnet =
     withTString server $ \pServer ->
     -- inout parameter. Supply on successive calls to retreive more elements
@@ -163,11 +230,11 @@     -- Failure to use the returned resumeHandle may result in an internal access
     -- violation within RPCRT4.dll.
     let loop acc = do
-            ret <- c_EnumSubnetClientsV4 dhcp pServer (toWord32 subnet)
-                       pResumeHandle 0xFFFFFFFF ppInfoArray
-                       pElementsRead pElementsTotal
-            unless (elem ret [eRROR_SUCCESS, eRROR_MORE_DATA, eRROR_NO_MORE_ITEMS]) $ do
-                failWith (unwords ["EnumSubnetClientsV4", server, showIp subnet]) ret
+            ret <- E.fromDWORD <$> c_EnumSubnetClientsV4 dhcp pServer (toWord32 subnet)
+                                   pResumeHandle 0xFFFFFFFF ppInfoArray
+                                   pElementsRead pElementsTotal
+            unless (elem ret [E.Success, E.MoreData, E.NoMoreItems])
+                $ E.failWith "EnumSubnetClientsV4" ret
             pInfoArray <- peek ppInfoArray
             elems <- if pInfoArray == nullPtr
                      then return []
@@ -178,20 +245,14 @@                        return elems
             elementsTotal <- peek pElementsTotal
             elementsRead <- peek pElementsRead
-            if (ret == eRROR_NO_MORE_ITEMS || (ret == eRROR_SUCCESS && elementsTotal == elementsRead))
-              then return (eRROR_SUCCESS, elems:acc)
+            if (ret == E.NoMoreItems || (ret == E.Success && elementsTotal == elementsRead))
+              then return (elems:acc)
               else loop (elems:acc)
 
-    (ret, revelemss) <- loop []
-    failUnlessSuccess (unwords ["EnumSubnetClientsV4", server, showIp subnet])
-        $ return ret
+    revelemss <- loop []
     return $ concat $ reverse revelemss
-  where
-    eRROR_SUCCESS       = 0x00000000
-    eRROR_NO_MORE_ITEMS = 0x00000103
-    eRROR_MORE_DATA     = 0x000000ea
 
-enumSubnetElementsV4 :: DhcpApi -> String -> Ip -> CInt
+enumSubnetElementsV4 :: DhcpApi -> Text -> Ip -> CInt
     -> IO [SUBNET_ELEMENT_DATA_V4]
 enumSubnetElementsV4 dhcp server subnet elementType =
     withTString server $ \pServer ->
@@ -204,12 +265,11 @@     -- Failure to use the returned resumeHandle may result in an internal access
     -- violation within RPCRT4.dll.
     let loop acc = do
-            ret <- c_EnumSubnetElementsV4 dhcp pServer (toWord32 subnet) elementType
-                       pResumeHandle 0xFFFFFFFF ppEnumElementInfoArray
-                       pElementsRead pElementsTotal
-            unless (elem ret [eRROR_SUCCESS, eRROR_MORE_DATA, eRROR_NO_MORE_ITEMS]) $ do
-                failWith (unwords ["EnumSubnetElementsV4", server
-                                  , showIp subnet, show elementType]) ret
+            ret <- E.fromDWORD <$> c_EnumSubnetElementsV4 dhcp pServer (toWord32 subnet) elementType
+                                   pResumeHandle 0xFFFFFFFF ppEnumElementInfoArray
+                                   pElementsRead pElementsTotal
+            unless (elem ret [E.Success, E.MoreData, E.NoMoreItems])
+                $ E.failWith "EnumSubnetElementsV4" ret
             pEnumElementInfoArray <- peek ppEnumElementInfoArray
             elems <- if pEnumElementInfoArray == nullPtr
                      then return []
@@ -219,33 +279,25 @@                        poke ppEnumElementInfoArray nullPtr
                        return elems
             elementsTotal <- peek pElementsTotal
-            if (ret == eRROR_NO_MORE_ITEMS || (ret == eRROR_SUCCESS && elementsTotal == 0))
-              then return (eRROR_SUCCESS, elems:acc)
+            if (ret == E.NoMoreItems || (ret == E.Success && elementsTotal == 0))
+              then return (elems:acc)
               else loop (elems:acc)
 
-    (ret, revelemss) <- loop []
-    failUnlessSuccess (unwords ["EnumSubnetElementsV4", server
-                               , showIp subnet, show elementType])
-        $ return ret
+    revelemss <- loop []
     return $ concat $ reverse revelemss
-  where
-    eRROR_SUCCESS       = 0x00000000
-    eRROR_NO_MORE_ITEMS = 0x00000103
-    eRROR_MORE_DATA     = 0x000000ea
 
-
-addSubnetElementV4 :: DhcpApi -> String -> Ip -> SUBNET_ELEMENT_DATA_V4 -> IO ()
+addSubnetElementV4 :: DhcpApi -> Text -> Ip -> SUBNET_ELEMENT_DATA_V4 -> IO ()
 addSubnetElementV4 dhcp server subnet elementData =
     withTString server $ \pServer ->
     withDhcp subnetElementData elementData $ \pElementData ->
-    failUnlessSuccess (unwords ["AddSubnetElementsV4", server, showIp subnet])
+    E.failUnlessSuccess "AddSubnetElementsV4"
     $ c_AddSubnetElementV4 dhcp pServer (toWord32 subnet) pElementData
 
 -- | Remove an IPv4 subnet element from an IPv4 subnet defined on the DHCPv4
 --   server.
 removeSubnetElementV4
     :: DhcpApi
-    -> String -- ^ Unicode string that specifies the IP address, such as
+    -> Text   -- ^ Unicode string that specifies the IP address, such as
               --   \"123.456.789.012\", or hostname, such as \"server\",
               --   of the DHCP server.
     -> Ip -- ^ 'DWORD' value that specifies the IP address of the
@@ -261,21 +313,45 @@     -> FORCE_FLAG -- ^ DHCP_FORCE_FLAG enumeration value that indicates
                   --   whether or not the clients affected by the removal of
                   --   the subnet element should also be deleted.
-                  -- 
+                  --
                   --   Note If the flag is set to DhcpNoForce and this subnet
                   --   has served an IPv4 address to DHCPv4/BOOTP clients, the
                   --   IPv4 range is not deleted; conversely, if the flag is
                   --   set to DhcpFullForce, the IPv4 range is deleted along
                   --   with the DHCPv4 client lease record on the DHCPv4
                   --   server.
-    -> IO () -- ^ Following the convention of the Win32 package, an exeption
-             --   indicating the win32 error code will be raised if the
-             --   underlying API call returns anything other than
-             --   ERROR_SUCCESS.
+    -> IO ()
+    -- ^ This function will throw an 'E.Win32Exception' when the internal Win32
+    -- call returnes an error condition. MSDN lists the following exceptions,
+    -- but others might be thrown as well:
+    --
+    --   ['E.DhcpSubnetNotPresent'] The specified IPv4 subnet does not exist
+    --   on the DHCP server.
+    --
+    --   ['E.DhcpElementCantRemove'] Failure can occur for any number of
+    --   reasons.
+    --
+    --   ['E.DhcpJetError'] An error occurred while accessing the DHCP server
+    --   database.
+    --
+    --   ['E.DhcpInvalidRange'] The specified IPv4 range does not match an
+    --   existing IPv4 range.
+    --
+    --   ['E.ScopeRangePolicyChangeConflict'] An IP address range is
+    --   configured for a policy in this scope. This operation cannot be
+    --   performed on the scope IP address range until the policy IP address
+    --   range is suitably modified.
 removeSubnetElementV4 dhcp server subnet elementData forceFlag =
     withTString server $ \pServer ->
     withDhcp subnetElementData elementData $ \pElementData ->
-    failUnlessSuccess (unwords ["RemoveSubnetElementV4", server, showIp subnet
-                               , (show . elementTypeOf) elementData])
+    E.failUnlessSuccess "RemoveSubnetElementV4"
     $ c_RemoveSubnetElementV4 dhcp pServer (toWord32 subnet) pElementData
           (fromIntegral . fromEnum $ forceFlag)
+
+withTStringLen :: Text -> (LPTSTR -> T.I16 -> IO a) -> IO a
+withTStringLen text act = T.useAsPtr (T.snoc text (chr 0x0)) $ \ptr len ->
+    act (castPtr ptr) len
+
+withTString :: Text -> (LPTSTR -> IO a) -> IO a
+withTString text act = withTStringLen text $ \ptr _ -> do
+    act ptr
System/Win32/DHCP/LengthBuffer.hs view
@@ -9,7 +9,6 @@ import System.Win32.Types (DWORD)
 
 import System.Win32.DHCP.DhcpStructure
-import Utils (freeptr)
 
 -- |A LengthBuffer is a list of items which can be marshalled in and out
 -- of memory with a `DhcpArray` instance. 
System/Win32/DHCP/SEARCH_INFO.hs view
@@ -1,9 +1,13 @@+{-# LANGUAGE OverloadedStrings #-}
+
 module System.Win32.DHCP.SEARCH_INFO
   ( SEARCH_INFO_TYPE
   , SEARCH_INFO (..)
   , withSearchInfo
   ) where
 
+import Data.Monoid ((<>))
+import qualified Data.Text as T
 import Foreign
 import Foreign.C.Types
 
@@ -44,8 +48,8 @@   | ClientName            !String
 
 instance Show SEARCH_INFO where
-  show (ClientIpAddress ip) = "ClientIpAddress " ++ showIp ip
-  show (ClientHardwareAddress mac) = "ClientHardwareAddress " ++ showMac ":" mac
+  show (ClientIpAddress ip) = T.unpack $ "ClientIpAddress " <> showIp ip
+  show (ClientHardwareAddress mac) = T.unpack $ "ClientHardwareAddress " <> showMac ":" mac
   show (ClientName name) = "ClientName " ++ name
 
 siTypeOf :: SEARCH_INFO -> SEARCH_INFO_TYPE
Utils.hs view
@@ -28,3 +28,11 @@ withMaybeTString :: Maybe String -> (LPTSTR -> IO a) -> IO a
 withMaybeTString Nothing    f = f nullPtr
 withMaybeTString (Just str) f = withTString str f
+
+note :: e -> Maybe a -> Either e a
+note e Nothing = Left e
+note _ (Just x) = Right x
+
+fmapL :: (a -> b) -> Either a r -> Either b r
+fmapL f (Left x) = Left (f x)
+fmapL _ (Right x) = (Right x)
Win32-dhcp-server.cabal view
@@ -1,6 +1,6 @@ Name:                Win32-dhcp-server
 Synopsis:            Win32 DHCP Server Management API.
-Version:             0.1
+Version:             0.2
 License:             BSD3
 License-file:        LICENSE
 Author:              Michael Steele
@@ -17,7 +17,7 @@   This package provides a partial binding to the Win32 DHCP Server Management
   API. Its purpose is to query and control a Microsoft DHCP server. Enough
   functionality is defined so so that Ipv4 client lease and reservation
-  records can be manipulated from software.
+  records can be manipulated programmatically.
   .
   Here are a few notes on the required environment:
   .
@@ -35,6 +35,7 @@   .
   import Data.Ip
   import Data.Mac
+  import qualified Data.Text.IO as T
   import System.Win32.DHCP
   .
   main :: IO ()
@@ -42,16 +43,20 @@   &#x20;   api <- loadDHCP
   &#x20;   clients <- enumClients api context
   &#x20;   let macs = map (showMac \":\" . clientHardwareAddress) clients
-  &#x20;   mapM_ putStrLn macs
+  &#x20;   mapM_ T.putStrLn macs
   &#x20; where
-  &#x20;   Just subnet = readIp \"192.168.1.0\"
+  &#x20;   Right subnet = readIp \"192.168.1.0\"
   &#x20;   context = Context \"192.168.1.5\" subnet
   @
+extra-source-files:
+    ChangeLog
+    examples/*.hs
 
 Library
-  Build-depends: base  >= 4.6 && < 4.8
-               , safe
+  Build-depends: base         >= 4.6  && < 4.8
+               , text          > 0.11 && < 1.2
                , Win32
+               , Win32-errors >= 0.2  && < 1.0
   default-language: Haskell2010
   Exposed-modules: Data.Ip
                  , Data.Mac
+ examples/cabal.hs view
@@ -0,0 +1,22 @@+-- This is the example used in the main .cabal file.
+
+{-# LANGUAGE OverloadedStrings #-}
+
+-- Print all MAC addresses with an active client lease
+module Main where
+
+import Data.Ip
+import Data.Mac
+import qualified Data.Text.IO as T
+import System.Win32.DHCP
+
+main :: IO ()
+main = do
+    api <- loadDHCP
+    clients <- enumClients api context
+    let macs = map (showMac ":" . clientHardwareAddress) clients
+    mapM_ T.putStrLn macs
+ where
+    Right subnet = readIp "192.168.1.0"
+    context = Context "192.168.1.5" subnet
+