2011-06-29 29 views
5

假設我需要解析一個二進制文件,它以三個4字節的幻數開始。其中兩個是固定的字符串。另一個是文件的長度。Iteratee I/O:事先需要知道文件大小

{-# LANGUAGE OverloadedStrings #-} 
module Main where 

import Data.Attoparsec 
import Data.Attoparsec.Enumerator 
import Data.Enumerator hiding (foldl, foldl', map, head) 
import Data.Enumerator.Binary hiding (map) 
import qualified Data.ByteString as S 
import System 

main = do 
    f:_ <- getArgs 
    eitherStat <- run (enumFile f $$ iterMagics) 
    case eitherStat of 
     Left _err -> putStrLn $ "Not a beam file: " ++ f 
     Right _ -> return() 

iterMagics :: Monad m => Iteratee S.ByteString m() 
iterMagics = iterParser parseMagics 

parseMagics :: Parser() 
parseMagics = do 
    _ <- string "FOR1" 
    len <- big_endians 4 -- need to compare with actual file length 
    _ <- string "BEAM" 
    return() 

big_endians :: Int -> Parser Int 
big_endians n = do 
    ws <- count n anyWord8 
    return $ foldl1 (\a b -> a * 256 + b) $ map fromIntegral ws 

如果規定長度不符合實際的長度,最好iterMagics應該返回一個錯誤。但是如何?是否只有將實際長度作爲參數傳遞的唯一方法?這是迭代式的方式嗎?對我來說不是非常增量:)

+0

最初生成迭代器時,將實際文件長度作爲參數傳遞的程序是什麼?也許改變你的函數'iterMagics'把文件長度作爲參數。如果你聰明地編程,你的代碼只需要傳遞一次。 – fuz

回答

5

這可以很容易地用枚舉來完成。首先閱讀三個4字節的幻數,然後在其餘部分運行一個內部迭代器。如果您使用iteratee,它看起來像更多或更少的這樣的:

parseMagics :: Parser() 
parseMagics = do 
    _ <- string "FOR1" 
    len <- big_endians 4 -- need to compare with actual file length 
    _ <- string "BEAM" 
    return len 

iterMagics :: Monad m => Iteratee S.ByteString m (Either String SomeResult) 
iterMagics = do 
    len <- iterParser parseMagics 
    (result, bytesConsumed) <- joinI $ takeUpTo len (enumWith iterData I.length) 
    if len == bytesConsumed 
    then return $ Right result 
    else return $ Left "Data too short" 

在這種情況下,如果該文件是太長,會不會引發錯誤,但它會停止閱讀。您可以修改它以相當容易地檢查該情況。我不認爲Enumerator對enumWith有模擬功能,所以你可能需要手動計算字節數,但同樣的原則適用。

可能更實用的方法是在運行枚舉器之前檢查文件大小,然後將其與頭中的值進行比較。您需要將文件大小或文件路徑作爲參數傳遞給迭代器(但不是解析器)。

import System.Posix 

iterMagics2 filepath = do 
    fsize <- liftIO . liftM fileSize $ getFileStatus filepath 
    len <- iterParser parseMagics 
+0

不錯。這是我正在尋找的。 – edwardw

0

您可能會提出的一種解決方案是使用兩步解析。這裏我們用一個解析器枚舉一個文件,該文件從文件的魔術部分獲取長度並返回長度爲'len'的字節串。否則失敗。之後,我們使用普通attoparsec解析器超過該字節串:

{-# LANGUAGE OverloadedStrings #-} 
module Main where 

import Data.Attoparsec 
import Data.Attoparsec.Enumerator 
import Data.Enumerator hiding (foldl, foldl', map, head) 
import Data.Enumerator.Binary hiding (map) 
import qualified Data.ByteString as S 
import System 

main = do 
    f:_ <- getArgs 
    eitherStat <- run (enumFile f $$ iterParser parseMagics) 
    case eitherStat of 
     Left _err -> putStrLn $ "Not a beam file: " ++ f 
     Right bs -> parse parseContents bs 

parseContents :: Parser() 
parseContents = do 
    ... 


parseMagics :: Parser ByteString 
parseMagics = do 
    _ <- string "FOR1" 
    len <- big_endians 4 
    _ <- string "BEAM" 
    rest <- take len 
    return rest 

big_endians :: Int -> Parser Int 
big_endians n = do 
    ws <- count n anyWord8 
    return $ foldl1 (\a b -> a * 256 + b) $ map fromIntegral ws 
+0

不錯的做法!不過,做'拿len'(可能會很長)並將它提供給下一個迭代器的性能影響是什麼? – edwardw

+0

我自己發現了另一個解決方案,attoparsec 0.8.x有'確保'符合法案。但不幸的是0.9.x刪除了這樣的功能。想知道爲什麼。 – edwardw

+0

@edwardw:以這種方式執行'len'與完成'Data.ByteString.readFile'完全相同。再也不用使用迭代器了。 –

0

找到一個解決自己:

parseMagics :: Parser() 
parseMagics = do 
    _ <- string "FOR1" 
    len <- big_endians 4 
    _ <- string "BEAM" 
    return $ ensure len 

但attoparsec最近取消了ensure。我已經向bitbucket上的attoparsec作者提交了一個錯誤報告。

+0

我認爲'確保'已被刪除,因爲這正是您不想使用它的情況。 '確保'強制下一個'n'字節的數據,這意味着如果你想確保大的值,你完全否定了增量解析的好處。 –

+0

@John L,我懷疑是一樣的。感謝您的確認。 – edwardw