在 Haskell 中解析 OUI 数据库

在这篇文章中,我们将展示一种方法并提供一个能够解析 IEEE OUI 列表IEEE IAB 列表的 Haskell 模块。

虽然我们的代码只将数据库解析为对象形式,并没有将它们插入到能够快速 MAC 地址 -> 供应商查找的树中,但它基于 Attoparsec,提供了良好的性能和更改的高灵活性。

要解析 oui.txt,只需调用 parseOUIFile "oui.txt"。注意虽然为解析错误提供了 Either 错误消息,但当前版本不为文件 IO 错误提供基于 Either 的错误。

OUIParser.hs
{-# LANGUAGE OverloadedStrings #-}
module OUIParser where
{-
    IEEE OUI / IAB 列表的解析器

    下载地址
    https://standards.ieee.org/develop/regauth/oui/oui.txt
    https://standards.ieee.org/develop/regauth/iab/iab.txt

    版权所有 (c) 2014 Uli Koehler
    在 Apache License v2.0 下许可

    版本 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)

-- 跳过空格,但不跳过 \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
    -- 前导行:空
    skipSpaceNoNewline >> endOfLine
    -- OUI 头
    skipSpaceNoNewline >> string "OUI/MA-L"
    skipSpaceNoNewline >> string "Organization"
    endOfLine
    -- 公司 ID 头
    skipSpaceNoNewline >> string "company_id"
    skipSpaceNoNewline >> string "Organization"
    skipSpaceNoNewline >> endOfLine
    -- 地址头
    skipSpaceNoNewline >> string "Address" >> endOfLine
    ---- 两个空白行
    skipSpaceNoNewline >> endOfLine
    skipSpaceNoNewline >> endOfLine
    -- 不返回任何内容
    return ()

iabHeaderParser :: Parser ()
iabHeaderParser = do
    -- 前导行:空
    skipSpaceNoNewline >> endOfLine
    -- OUI 头
    skipSpaceNoNewline >> string "OUI"
    skipSpaceNoNewline >> string "Organization"
    endOfLine
    -- 公司 ID 头
    skipSpaceNoNewline >> string "IAB Range"
    skipSpaceNoNewline >> string "Organization"
    skipSpaceNoNewline >> endOfLine
    -- 地址头
    skipSpaceNoNewline >> string "Address" >> endOfLine
    ---- 两个空白行
    skipSpaceNoNewline >> endOfLine
    skipSpaceNoNewline >> endOfLine
    -- 不返回任何内容
    return ()

-- 消耗一行,返回非空的剥离部分
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)

-- 第一个 OUI 记录行的解析器
ouiLineParser :: Parser (OUIPrefix, ByteString)
ouiLineParser = do
    skipSpace
    ouiPrefix <- ouiPrefixParser
    skipSpace
    string "(hex)"
    -- 公司名称
    companyName <- stripLineParser1
    return (ouiPrefix, companyName)

-- 解析包含公司 ID 的行
-- 忽略公司名称,因为它与
-- 公司 ID 行中的相同(并且 PRIVATE 保留在此行中不包含它)
companyLineParser :: Parser ByteString
companyLineParser = do
    skipSpace
    companyId <- takeWhile1 isHexDigit_w8
    skipSpace
    string "(base 16)"
    -- 跳过公司
    skipWhile (not . isEndOfLine)
    endOfLine
    return companyId

iabLineParser :: Parser IABRange
iabLineParser = do
    skipSpace
    iabRange <- iabRangeParser
    skipSpace
    string "(base 16)"
    -- 跳过公司
    skipWhile (not . isEndOfLine)
    endOfLine
    return iabRange

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

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

Check out similar posts by category: Haskell