On Wed, Dec 20, 2023 at 08:42:02PM +0000, Dave Cridland wrote: > Half the battle is, of course, being able to read the result and decide if > it's actually a valid approach - but there are tools to assist with that > too. Yes, and gets a quite a bit more dicey with less popular languages. The same question for Haskell gave me, rather unworkable code, which attempts to do too much of the work "by hand", rather than use existing libraries: import Data.ByteString (ByteString) import Data.List (intercalate) import Data.Word (Word8, Word16) import Network.Socket (inet_pton) -- [V] No such interface. -- Wouldn't return a family if it existed. -- Also, no attempt to import inet_ntoa. -- Function to validate and normalize an IP address normalizeIP :: ByteString -> Maybe ByteString normalizeIP addr = do family <- inet_pton addr case family of -- IPv4: Normalize by converting to a string and back AF_INET -> Just $ ByteString.pack $ show (inet_ntoa family) -- IPv6: Expand zero compressions and lowercase hex digits AF_INET6 -> Just $ ByteString.concat $ map (ByteString.pack . showHex) segments where -- Syntax error: insufficient indentation -- [V] This is no way to split 16 bytes into 8 Word16 groups. segments = expandZeros $ map Word16 $ ByteString.unpack family -- [V] This function is nonsense. expandZeros :: [Word16] -> [[Word16]] expandZeros (0:xs) = [0] : expandZeros xs expandZeros (x:xs) = [x] : expandZeros xs expandZeros [] = [] -- [V] This is broken, also, there's a correct `showHex` in "Numeric" -- Convert a Word16 to lowercase hex string showHex :: Word16 -> String showHex w = toLower . showHex' $ w showHex' :: Word16 -> String showHex' w = concat $ map (\n -> showHexDigit $ fromIntegral (w `shiftR` (4 * n))) [0..3] - [V] This only works for decimal digits -- Convert a single nibble to a hex digit showHexDigit :: Word8 -> Char showHexDigit n = chr $ 48 + fromIntegral n _ -> Nothing An much more natural version (be it one that tolerates leading whitespace, but does not tolerate excess leading 0's in IPv4 quads) would be: import qualified Data.ByteString.Builder as B import qualified Data.ByteString.Builder.Extra as B import qualified Data.ByteString.Char8 as B import qualified Data.ByteString.Lazy as L import qualified Data.IP as IP import qualified Data.IP.Builder as IP normalizeIP :: B.ByteString -> (Maybe B.ByteString) normalizeIP addr = case (reads (B.unpack addr)) of [(ip, "")] -> build ip _ -> Nothing where build (IP.IPv4 ip) = sized 16 $ IP.ipv4Builder ip build (IP.IPv6 ip) = sized 39 $ IP.ipv6Builder ip sized sz bldr = Just $! L.toStrict $ tolazy bldr where tolazy = B.toLazyByteStringWith (B.untrimmedStrategy sz sz) L.empty I shared the above solution with Bard, which then however gave sensible rationale notes for the essential features. It does feel a bit odd to step up to train the AI for free... A more precise version via getaddrinfo(3) that handles more exotic IPv4 addresses and does not accept leading whitespace: import qualified Data.ByteString.Builder as B import qualified Data.ByteString.Builder.Extra as B import qualified Data.ByteString.Char8 as B import qualified Data.ByteString.Lazy as L import qualified Data.IP as IP import qualified Data.IP.Builder as IP import Network.Socket ( AddrInfo(..), AddrInfoFlag(..), Family(..), SockAddr(..) , SocketType(..), defaultHints, getAddrInfo ) -- Function to validate and normalize an IP address normalizeIP :: B.ByteString -> IO (Maybe B.ByteString) normalizeIP addr = do ais <- getAddrInfo numeric (Just (B.unpack addr)) Nothing case addrAddress <$> ais of [SockAddrInet _ sin_addr] -> hostAddr4 sin_addr [SockAddrInet6 _ _ sin6_addr _] -> hostAddr6 sin6_addr _ -> pure Nothing where numeric = Just defaultHints { addrFlags = [AI_NUMERICHOST, AI_NUMERICSERV] , addrSocketType = Datagram } hostAddr4 = sized 16 . IP.ipv4Builder . IP.fromHostAddress hostAddr6 = sized 39 . IP.ipv6Builder . IP.fromHostAddress6 sized sz bldr = pure $ Just $! L.toStrict $ norm where norm = B.toLazyByteStringWith (B.untrimmedStrategy sz sz) L.empty bldr -- Viktor.