Die OUI-Datenbank in Haskell parsen

In diesem Beitrag zeigen wir eine Methode und stellen ein Haskell-Modul zur Verfügung, das sowohl die IEEE OUI-Liste als auch die IEEE IAB-Liste parsen kann.

Unser Code parst die Datenbanken zwar nur in eine Objektform und fügt sie nicht in einen Baum ein, der schnelle MAC-Adressen-Suche nach Hersteller ermöglicht, aber er basiert auf Attoparsec und bietet gute Performance und hohe Flexibilität für Änderungen.

Um oui.txt zu parsen, rufe einfach parseOUIFile "oui.txt" auf. Beachte, dass zwar eine Either-Fehlermeldung für Parsing-Fehler bereitgestellt wird, die aktuelle Version jedoch keine Either-basierten Fehler für Datei-IO-Fehler liefert.

OUIParser.hs
{-# LANGUAGE OverloadedStrings #-}
module OUIParser where
{-
    Parser für die IEEE OUI / IAB-Liste

    Download unter
    https://standards.ieee.org/develop/regauth/oui/oui.txt
    https://standards.ieee.org/develop/regauth/iab/iab.txt

    Copyright (c) 2014 Uli Koehler
    Lizenziert unter der Apache License v2.0

    Version 1.0
-}

import Prelude hiding (takeWhile)
import qualified Data.ByteString.Char8 as B
import qualified Data.ByteString.Lazy.Char8 as LB
import Data.ByteString.Char8 (ByteString)
import Data.Attoparsec.Char8 (skipSpace, endOfLine, isEndOfLine, isDigit_w8, hexadecimal, char, isSpace_w8)
import Data.Attoparsec.ByteString.Lazy
import GHC.Word (Word8)
import Control.Applicative
import Data.Char (isHexDigit)
import Control.Monad
import qualified Data.ByteString.Internal as BI (c2w, w2c)
import Debug.Trace

isHexDigit_w8 = isHexDigit . BI.w2c

skipWhile1 :: (Word8 -> Bool) -> Parser ()
skipWhile1 p = skip p *> skipWhile p

type OUIPrefix = (Int, Int, Int)
type IABRange = (Int, Int)
type CompanyId = ByteString

data OUIInfo = OUIInfo {
                    ouiPrefix :: OUIPrefix,
                    ouiCompanyId :: CompanyId,
                    ouiCompanyName :: ByteString,
                    ouiCompanyAddress :: ByteString
                } deriving (Show)

data IABInfo = IABInfo {
                    iabPrefix :: OUIPrefix,
                    iabRange :: IABRange,
                    iabCompanyName :: ByteString,
                    iabCompanyAddress :: ByteString
                } deriving (Show)

-- Leerzeichen überspringen, aber nicht \n
skipSpaceNoNewline = skipWhile (\x -> isSpace_w8 x && not (isEndOfLine x))
skipSpaceNoNewline1 = skipWhile1 (\x -> isSpace_w8 x && not (isEndOfLine x))

parseOUIFile :: FilePath -> IO (Either String [OUIInfo])
parseOUIFile filename = do
    fileContent <- LB.readFile filename
    return $ eitherResult $ parse ouiFileParser fileContent

parseIABFile :: FilePath -> IO (Either String [IABInfo])
parseIABFile filename = do
    fileContent <- LB.readFile filename
    return $ eitherResult $ parse iabFileParser fileContent

ouiFileParser :: Parser [OUIInfo]
ouiFileParser = do
    ouiHeaderParser
    result <- many ouiRecordParser
    return result

iabFileParser :: Parser [IABInfo]
iabFileParser = do
    iabHeaderParser
    result <- many iabRecordParser
    return result

ouiHeaderParser :: Parser ()
ouiHeaderParser = do
    -- Führende Zeile: leer
    skipSpaceNoNewline >> endOfLine
    -- OUI-Header
    skipSpaceNoNewline >> string "OUI/MA-L"
    skipSpaceNoNewline >> string "Organization"
    endOfLine
    -- Company-ID-Header
    skipSpaceNoNewline >> string "company_id"
    skipSpaceNoNewline >> string "Organization"
    skipSpaceNoNewline >> endOfLine
    -- Adress-Header
    skipSpaceNoNewline >> string "Address" >> endOfLine
    ---- Zwei Leerzeilen
    skipSpaceNoNewline >> endOfLine
    skipSpaceNoNewline >> endOfLine
    -- Nichts zurückgeben
    return ()

iabHeaderParser :: Parser ()
iabHeaderParser = do
    -- Führende Zeile: leer
    skipSpaceNoNewline >> endOfLine
    -- OUI-Header
    skipSpaceNoNewline >> string "OUI"
    skipSpaceNoNewline >> string "Organization"
    endOfLine
    -- Company-ID-Header
    skipSpaceNoNewline >> string "IAB Range"
    skipSpaceNoNewline >> string "Organization"
    skipSpaceNoNewline >> endOfLine
    -- Adress-Header
    skipSpaceNoNewline >> string "Address" >> endOfLine
    ---- Zwei Leerzeilen
    skipSpaceNoNewline >> endOfLine
    skipSpaceNoNewline >> endOfLine
    -- Nichts zurückgeben
    return ()

-- Eine Zeile konsumieren, den nicht-leeren abgeschnittenen Teil zurückgeben
stripLineParser1 :: Parser ByteString
stripLineParser1 = skipSpaceNoNewline *> takeWhile1 (not . isEndOfLine) <* endOfLine

addressLineParser :: Parser ByteString
addressLineParser = skipSpaceNoNewline1 *> takeWhile (not . isEndOfLine) <* endOfLine

ouiPrefixParser :: Parser OUIPrefix
ouiPrefixParser = do
    part1 <- hexadecimal
    char '-'
    part2 <- hexadecimal
    char '-'
    part3 <- hexadecimal
    return $ (part1, part2, part3)

iabRangeParser :: Parser IABRange
iabRangeParser = do
    part1 <- hexadecimal
    char '-'
    part2 <- hexadecimal
    return (part1, part2)

-- Parser für die erste OUI-Datensatzzeile
ouiLineParser :: Parser (OUIPrefix, ByteString)
ouiLineParser = do
    skipSpace
    ouiPrefix <- ouiPrefixParser
    skipSpace
    string "(hex)"
    -- Firmenname
    companyName <- stripLineParser1
    return (ouiPrefix, companyName)

-- Parst die Zeile mit der Company-ID
-- Ignoriert den Firmennamen, da er derselbe ist wie
-- in der Company-ID-Zeile (und PRIVATE-Reservierungen enthalten ihn nicht in dieser Zeile)
companyLineParser :: Parser ByteString
companyLineParser = do
    skipSpace
    companyId <- takeWhile1 isHexDigit_w8
    skipSpace
    string "(base 16)"
    -- Firma überspringen
    skipWhile (not . isEndOfLine)
    endOfLine
    return companyId

iabLineParser :: Parser IABRange
iabLineParser = do
    skipSpace
    iabRange <- iabRangeParser
    skipSpace
    string "(base 16)"
    -- Firma überspringen
    skipWhile (not . isEndOfLine)
    endOfLine
    return iabRange

iabRecordParser :: Parser IABInfo
iabRecordParser = do
    (ouiPrefix, companyName) <- ouiLineParser
    iabRange <- iabLineParser
    companyAddress <- many $ addressLineParser
    -- Leerzeile
    endOfLine
    return $ IABInfo ouiPrefix iabRange companyName $ B.unlines companyAddress

ouiRecordParser :: Parser OUIInfo
ouiRecordParser = do
    (ouiPrefix, companyName) <- ouiLineParser
    companyId <- companyLineParser
    companyAddress <- many $ addressLineParser
    -- Leerzeile
    endOfLine
    return $ OUIInfo ouiPrefix companyId companyName $ B.unlines companyAddress

Check out similar posts by category: Haskell