packages feed

openpgp-asciiarmor 0.0 → 0.1

raw patch · 15 files changed

+264/−69 lines, 15 filesdep ~attoparsecdep ~cerealbinary-added

Dependency ranges changed: attoparsec, cereal

Files

Codec/Encryption/OpenPGP/ASCIIArmor.hs view
@@ -4,12 +4,14 @@ -- (See the LICENSE file).  module Codec.Encryption.OpenPGP.ASCIIArmor (-   encode- , decode+   decode+ , decodeLazy+ , encode+ , encodeLazy  , parseArmor  , multipartMerge ) where -import Codec.Encryption.OpenPGP.ASCIIArmor.Encode (encode)-import Codec.Encryption.OpenPGP.ASCIIArmor.Decode (decode, parseArmor)+import Codec.Encryption.OpenPGP.ASCIIArmor.Decode (decode, decodeLazy, parseArmor)+import Codec.Encryption.OpenPGP.ASCIIArmor.Encode (encode, encodeLazy) import Codec.Encryption.OpenPGP.ASCIIArmor.Multipart (multipartMerge)
Codec/Encryption/OpenPGP/ASCIIArmor/Decode.hs view
@@ -7,16 +7,21 @@ module Codec.Encryption.OpenPGP.ASCIIArmor.Decode (    parseArmor  , decode+ , decodeLazy ) where  import Codec.Encryption.OpenPGP.ASCIIArmor.Types-import Control.Applicative (many, (<|>), (<$>), Alternative, (*>))-import Data.Attoparsec.ByteString (Parser, many1, string, inClass, notInClass, satisfy, word8, (<?>), parse, IResult(..))+import Codec.Encryption.OpenPGP.ASCIIArmor.Utils+import Control.Applicative (many, (<|>), (<$>), Alternative, (<*), (<*>), (*>), optional)+import Data.Attoparsec.ByteString (Parser, many1, string, inClass, notInClass, satisfy, word8, (<?>))+import qualified Data.Attoparsec.ByteString as AS+import qualified Data.Attoparsec.ByteString.Lazy as AL import Data.Attoparsec.ByteString.Char8 (isDigit_w8, anyChar) import Data.Attoparsec.Combinator (manyTill) import Data.Bits (shiftL) import Data.ByteString (ByteString) import qualified Data.ByteString as B+import qualified Data.ByteString.Lazy as BL import qualified Data.ByteString.Char8 as BC8 import qualified Data.ByteString.Base64 as Base64 import Data.Digest.CRC24 (crc24)@@ -27,23 +32,42 @@ import Data.Word (Word32)  decode :: IsString e => ByteString -> Either e [Armor]-decode bs = go (parse parseArmors bs)+decode bs = go (AS.parse parseArmors bs)     where-        go (Fail t c e) = Left (fromString e)-        go (Partial cont) = go (cont B.empty)-        go (Done _ r) = Right r+        go (AS.Fail t c e) = Left (fromString e)+        go (AS.Partial cont) = go (cont B.empty)+        go (AS.Done _ r) = Right r +decodeLazy :: IsString e => BL.ByteString -> Either e [Armor]+decodeLazy bs = go (AL.parse parseArmors bs)+    where+        go (AL.Fail t c e) = Left (fromString e)+        go (AL.Done _ r) = Right r+ parseArmors :: Parser [Armor] parseArmors = many parseArmor  parseArmor :: Parser Armor-parseArmor = do-    atype <- prefixed beginLine <?> "begin line"+parseArmor = prefixed (clearsigned <|> armor) <?> "armor"++clearsigned :: Parser Armor+clearsigned = do+    string "-----BEGIN PGP SIGNED MESSAGE-----" <?> "clearsign header"+    lineEnding <?> "line ending"+    headers <- armorHeaders <?> "clearsign headers"+    blankishLine <?> "blank line"+    cleartext <- dashEscapedCleartext+    sig <- armor+    return $ ClearSigned headers (BL.fromChunks [cleartext]) sig++armor :: Parser Armor+armor = do+    atype <- beginLine <?> "begin line"     headers <- armorHeaders <?> "headers"     blankishLine <?> "blank line"     payload <- base64Data <?> "base64 data"     endLine atype <?> "end line"-    return $ Armor atype headers payload+    return $ Armor atype headers (BL.fromChunks [payload])  beginLine :: Parser ArmorType beginLine = do@@ -63,8 +87,8 @@             firstnum <- num             word8 (fromIntegral . fromEnum $ '/')             secondnum <- num-            return $ ArmorSplitMessage (B.pack firstnum) (B.pack secondnum)-        partsindef = ArmorSplitMessageIndefinite . B.pack <$> num+            return $ ArmorSplitMessage (BL.pack firstnum) (BL.pack secondnum)+        partsindef = ArmorSplitMessageIndefinite . BL.pack <$> num         num = many1 (satisfy isDigit_w8) <?> "number"  lineEnding :: Parser ByteString@@ -84,7 +108,7 @@         w8sToString = BC8.unpack . B.pack  blankishLine ::  Parser ByteString-blankishLine = many (satisfy (inClass " \t")) >> lineEnding+blankishLine = many (satisfy (inClass " \t")) *> lineEnding  endLine :: ArmorType -> Parser ByteString endLine atype = do@@ -95,10 +119,13 @@ aType (ArmorMessage) = BC8.pack "MESSAGE" aType (ArmorPublicKeyBlock) = BC8.pack "PUBLIC KEY BLOCK" aType (ArmorPrivateKeyBlock) = BC8.pack "PRIVATE KEY BLOCK"-aType (ArmorSplitMessage x y) = BC8.pack "MESSAGE, PART " `B.append` x `B.append` BC8.singleton '/' `B.append` y-aType (ArmorSplitMessageIndefinite x) = BC8.pack "MESSAGE, PART " `B.append` x+aType (ArmorSplitMessage x y) = BC8.pack "MESSAGE, PART " `B.append` l2s x `B.append` BC8.singleton '/' `B.append` l2s y+aType (ArmorSplitMessageIndefinite x) = BC8.pack "MESSAGE, PART " `B.append` l2s x aType (ArmorSignature) = BC8.pack "SIGNATURE" +l2s :: BL.ByteString -> ByteString+l2s = B.concat . BL.toChunks+ base64Data :: Parser ByteString base64Data = do     ls <- many1 base64Line@@ -138,3 +165,13 @@  prefixed :: Parser a -> Parser a prefixed end = end <|> anyChar *> prefixed end++dashEscapedCleartext :: Parser ByteString+dashEscapedCleartext = do+    ls <- many1 ((deLine <|> unescapedLine) <* lineEnding)+    return $ crlfUnlines ls+    where+        deLine :: Parser ByteString+        deLine = B.pack <$> (string "- " *> many (satisfy (notInClass "\n\r")))+        unescapedLine :: Parser ByteString+        unescapedLine = maybe B.empty B.pack <$> optional ((:) <$> satisfy (notInClass "-\n\r") <*> many (satisfy (notInClass "\n\r")))
Codec/Encryption/OpenPGP/ASCIIArmor/Encode.hs view
@@ -5,55 +5,71 @@  module Codec.Encryption.OpenPGP.ASCIIArmor.Encode (    encode+ , encodeLazy ) where  import Codec.Encryption.OpenPGP.ASCIIArmor.Types-import Data.ByteString (ByteString)+import Data.ByteString.Lazy (ByteString) import qualified Data.ByteString as B+import qualified Data.ByteString.Lazy as BL import qualified Data.ByteString.Char8 as BC8+import qualified Data.ByteString.Lazy.Char8 as BLC8 import qualified Data.ByteString.Base64 as Base64-import Data.Digest.CRC24 (crc24)+import Data.Digest.CRC24 (crc24Lazy) import Data.Serialize (put)-import Data.Serialize.Put (runPut, putWord32be)+import Data.Serialize.Put (runPutLazy, putWord32be) import Data.String (IsString, fromString) -encode :: [Armor] -> ByteString-encode = B.concat . map armor+encode :: [Armor] -> B.ByteString+encode = B.concat . BL.toChunks . encodeLazy +encodeLazy :: [Armor] -> ByteString+encodeLazy = BL.concat . map armor+ armor :: Armor -> ByteString-armor (Armor atype ahs bs) = beginLine atype `B.append` armorHeaders ahs `B.append` blankLine `B.append` armorData bs `B.append` armorChecksum bs `B.append` endLine atype+armor (Armor atype ahs bs) = beginLine atype `BL.append` armorHeaders ahs `BL.append` blankLine `BL.append` armorData bs `BL.append` armorChecksum bs `BL.append` endLine atype+armor (ClearSigned chs ctxt csig) = BLC8.pack "-----BEGIN PGP SIGNED MESSAGE-----\n" `BL.append` armorHeaders chs `BL.append` blankLine `BL.append` dashEscape ctxt `BL.append` armor csig  blankLine :: ByteString-blankLine = BC8.singleton '\n'+blankLine = BLC8.singleton '\n'  beginLine :: ArmorType -> ByteString-beginLine atype = BC8.pack "-----BEGIN PGP " `B.append` aType atype `B.append` BC8.pack "-----\n"+beginLine atype = BLC8.pack "-----BEGIN PGP " `BL.append` aType atype `BL.append` BLC8.pack "-----\n"  endLine :: ArmorType -> ByteString-endLine atype = BC8.pack "-----END PGP " `B.append` aType atype `B.append` BC8.pack "-----\n"+endLine atype = BLC8.pack "-----END PGP " `BL.append` aType atype `BL.append` BLC8.pack "-----\n"  aType :: ArmorType -> ByteString-aType (ArmorMessage) = BC8.pack "MESSAGE"-aType (ArmorPublicKeyBlock) = BC8.pack "PUBLIC KEY BLOCK"-aType (ArmorPrivateKeyBlock) = BC8.pack "PRIVATE KEY BLOCK"-aType (ArmorSplitMessage x y) = BC8.pack $ "MESSAGE, PART " ++ show x ++ "/" ++ show y-aType (ArmorSplitMessageIndefinite x) = BC8.pack $ "MESSAGE, PART " ++ show x-aType (ArmorSignature) = BC8.pack "SIGNATURE"+aType (ArmorMessage) = BLC8.pack "MESSAGE"+aType (ArmorPublicKeyBlock) = BLC8.pack "PUBLIC KEY BLOCK"+aType (ArmorPrivateKeyBlock) = BLC8.pack "PRIVATE KEY BLOCK"+aType (ArmorSplitMessage x y) = BLC8.pack $ "MESSAGE, PART " ++ show x ++ "/" ++ show y+aType (ArmorSplitMessageIndefinite x) = BLC8.pack $ "MESSAGE, PART " ++ show x+aType (ArmorSignature) = BLC8.pack "SIGNATURE"  armorHeaders :: [(String, String)] -> ByteString-armorHeaders ahs = BC8.unlines . map armorHeader $ ahs+armorHeaders ahs = BLC8.unlines . map armorHeader $ ahs     where         armorHeader :: (String, String) -> ByteString-        armorHeader (k, v) = BC8.pack k `B.append` BC8.pack ": " `B.append` BC8.pack v+        armorHeader (k, v) = BLC8.pack k `BL.append` BLC8.pack ": " `BL.append` BLC8.pack v  armorData :: ByteString -> ByteString-armorData = BC8.unlines . wordWrap 64 . Base64.encode+armorData = BLC8.unlines . wordWrap 64 . BL.fromChunks . return . Base64.encode . B.concat . BL.toChunks  wordWrap :: Int -> ByteString -> [ByteString] wordWrap lw bs-    | B.null bs = []+    | BL.null bs = []     | lw < 1 || lw > 76 = wordWrap 76 bs-    | otherwise = B.take lw bs : wordWrap lw (B.drop lw bs)+    | otherwise = BL.take (fromIntegral lw) bs : wordWrap lw (BL.drop (fromIntegral lw) bs)  armorChecksum :: ByteString -> ByteString-armorChecksum = BC8.cons '=' . armorData . B.tail . runPut . putWord32be . crc24+armorChecksum = BLC8.cons '=' . armorData . BL.tail . runPutLazy . putWord32be . crc24Lazy++dashEscape :: ByteString -> ByteString+dashEscape = BLC8.unlines . map escapeLine . BLC8.lines+    where+        escapeLine :: ByteString -> ByteString+        escapeLine l+            | BLC8.singleton '-' `BL.isPrefixOf` l = BLC8.pack "- " `BL.append` l+            | BLC8.pack "From " `BL.isPrefixOf` l = BLC8.pack "- " `BL.append` l+            | otherwise = l
Codec/Encryption/OpenPGP/ASCIIArmor/Multipart.hs view
@@ -9,16 +9,16 @@  import Codec.Encryption.OpenPGP.ASCIIArmor.Types -import Data.ByteString (ByteString)-import qualified Data.ByteString as B+import Data.ByteString.Lazy (ByteString)+import qualified Data.ByteString.Lazy as BL  multipartMerge :: [Armor] -> Armor-multipartMerge as = go as (Armor ArmorMessage [] B.empty)+multipartMerge as = go as (Armor ArmorMessage [] BL.empty)     where         go :: [Armor] -> Armor -> Armor         go [] state = state-        go ((Armor at hs bs):as) state = go as (go' at hs bs state)+        go (Armor at hs bs:as) state = go as (go' at hs bs state)         go' :: ArmorType -> [(String,String)] -> ByteString -> Armor -> Armor-        go' (ArmorSplitMessage _ _) hs bs (Armor _ ohs obs) = Armor ArmorMessage (ohs ++ hs) (obs `B.append` bs)-        go' (ArmorSplitMessageIndefinite _) hs bs (Armor _ ohs obs) = Armor ArmorMessage (ohs ++ hs) (obs `B.append` bs)+        go' (ArmorSplitMessage _ _) hs bs (Armor _ ohs obs) = Armor ArmorMessage (ohs ++ hs) (obs `BL.append` bs)+        go' (ArmorSplitMessageIndefinite _) hs bs (Armor _ ohs obs) = Armor ArmorMessage (ohs ++ hs) (obs `BL.append` bs)         go' _ _ _ state = state
Codec/Encryption/OpenPGP/ASCIIArmor/Types.hs view
@@ -8,10 +8,10 @@  , ArmorType(..) ) where -import Data.ByteString (ByteString)+import Data.ByteString.Lazy (ByteString)  data Armor = Armor ArmorType [(String, String)] ByteString-   | ClearSigned [(String, String)] String Armor+   | ClearSigned [(String, String)] ByteString Armor     deriving (Show, Eq)  data ArmorType = ArmorMessage
+ Codec/Encryption/OpenPGP/ASCIIArmor/Utils.hs view
@@ -0,0 +1,24 @@+-- ASCIIArmor/Utils.hs: OpenPGP (RFC4880) ASCII armor implementation+-- Copyright Ⓒ 2012  Clint Adams+-- This software is released under the terms of the ISC license.+-- (See the LICENSE file).++module Codec.Encryption.OpenPGP.ASCIIArmor.Utils (+   crlfUnlines+ , crlfUnlinesLazy+) where++import Data.ByteString (ByteString)+import qualified Data.ByteString as B+import qualified Data.ByteString.Char8 as BC8+import qualified Data.ByteString.Lazy as BL+import qualified Data.ByteString.Lazy.Char8 as BLC8+import Data.List (intersperse)++crlfUnlines :: [ByteString] -> ByteString+crlfUnlines [] = B.empty+crlfUnlines ss = B.concat $ intersperse (BC8.pack "\r\n") ss++crlfUnlinesLazy :: [BL.ByteString] -> BL.ByteString+crlfUnlinesLazy [] = BL.empty+crlfUnlinesLazy ss = BL.concat $ intersperse (BLC8.pack "\r\n") ss
Data/Digest/CRC24.hs view
@@ -4,22 +4,27 @@ -- (See the LICENSE file).  module Data.Digest.CRC24 (-  crc24+   crc24+ , crc24Lazy ) where  import Data.Bits (shiftL, (.&.), xor)-import Data.ByteString (ByteString)+import Data.ByteString.Lazy (ByteString) import qualified Data.ByteString as B+import qualified Data.ByteString.Lazy as BL import Data.Word (Word8, Word32) -crc24_init :: Word32-crc24_init = 0xB704CE+crc24Init :: Word32+crc24Init = 0xB704CE -crc24_poly :: Word32-crc24_poly = 0x1864CFB+crc24Poly :: Word32+crc24Poly = 0x1864CFB -crc24_update :: Word32 -> Word8 -> Word32-crc24_update c b = (last . take 9 $ iterate (\x -> if (shiftL x 1) .&. 0x1000000 == 0x1000000 then shiftL x 1 `xor` crc24_poly else shiftL x 1) (c `xor` shiftL (fromIntegral b) 16)) .&. 0xFFFFFF+crc24Update :: Word32 -> Word8 -> Word32+crc24Update c b = (last . take 9 $ iterate (\x -> if shiftL x 1 .&. 0x1000000 == 0x1000000 then shiftL x 1 `xor` crc24Poly else shiftL x 1) (c `xor` shiftL (fromIntegral b) 16)) .&. 0xFFFFFF -crc24 :: ByteString -> Word32-crc24 bs = B.foldl crc24_update crc24_init bs+crc24 :: B.ByteString -> Word32+crc24 bs = crc24Lazy . BL.fromChunks $ [bs]++crc24Lazy :: ByteString -> Word32+crc24Lazy = BL.foldl crc24Update crc24Init
openpgp-asciiarmor.cabal view
@@ -1,5 +1,5 @@ Name:                openpgp-asciiarmor-Version:             0.0+Version:             0.1 Synopsis:            OpenPGP (RFC4880) ASCII Armor codec Description:         OpenPGP (RFC4880) ASCII Armor codec Homepage:            http://floss.scru.org/openpgp-asciiarmor@@ -15,9 +15,15 @@   , tests/data/msg1a.asc   , tests/data/msg1b.asc   , tests/data/msg1c.asc-  , tests/data/msg2.asc   , tests/data/msg1.gpg+  , tests/data/msg2.asc   , tests/data/msg2.pgp+  , tests/data/msg3+  , tests/data/msg3.asc+  , tests/data/msg3.sig+  , tests/data/msg4+  , tests/data/msg4.asc+  , tests/data/msg4.sig  Cabal-version:       >= 1.10 @@ -29,6 +35,7 @@                      , Codec.Encryption.OpenPGP.ASCIIArmor.Types   Other-Modules: Data.Digest.CRC24                , Codec.Encryption.OpenPGP.ASCIIArmor.Multipart+               , Codec.Encryption.OpenPGP.ASCIIArmor.Utils   Build-depends: attoparsec                , base                  > 4       && < 5                , base64-bytestring
+ tests/data/msg3 view
@@ -0,0 +1,9 @@+This is a message that will be clearsigned.++From RFC4880, we know that some of these lines should be+dash-escaped.++-Lines starting with a minus-hyphen MUST be escaped.+- Lines starting with "From" SHOULD be escaped.++Other lines MAY be escaped.
+ tests/data/msg3.asc view
@@ -0,0 +1,29 @@+-----BEGIN PGP SIGNED MESSAGE-----+Hash: SHA1++This is a message that will be clearsigned.++- From RFC4880, we know that some of these lines should be+dash-escaped.++- -Lines starting with a minus-hyphen MUST be escaped.+- - Lines starting with "From" SHOULD be escaped.++Other lines MAY be escaped.+-----BEGIN PGP SIGNATURE-----+Version: OpenPrivacy 0.99++iQIcBAEBAgAGBQJPmXqrAAoJEN/7iwtcb1WCG3AQAJ6TBeX12YDI1f/AdtV46quG+augJYpYZvBbKESGXue1Nv22a7uH4h8LgWRsaEQxMBUwJvlMJfNkjEMAkXQbkj/Og+J+78bAGMV1GtC5MuwPr8E+M8Z/uHhbzj3fWuUask0Q057u655YIEdlnY4OcZv9jW+hT+/2kNcC8aw9+kg0I175XNxwBhRXoRKX6dhyAkRSnz7yuQtGXH7kQJAt7TOxxAb+dud+u5IJixDPebG+NONPfuW5VB8erByW6UPIy4BQBnaxflSD8qJXxCDMWNzOBlYG+whKXDmlcVgy3J7ghSh2zcFcZhM30Ng49t6k57HOXR9XnI5dskY45yns2nD4kt58/+7anmscGGj0S3pzoUuFAdVIEvziYDhISs35CmTmNh4r4LVuh4R+Zurt3mbe8O6amm+ZOWZzPsEDX/13B/DnL//70jVhTXUBqDj6MeNj5XHXVbIlfmIyeVLOIXiT/+u1FFt++0ERqwFI152GGJJWlikn5bR6P89Xz+04OeTBdxV1fCGt+hlvN6e5X9K15P16QDiq+4COHMZyIyHbtQr92BIj0P46WNsNZJDaoegHl6xtbq60eV3W+LRvgHNphjE+mdIp0+EV5lBqDGupGYpHkPjZBg0pqASs0Xd3P7SwkoVimtH7mXCPrL4K+o6X8IwsfEMCGY+Ddej4NK0eolGoz/1sKB0+=ZShm+-----END PGP SIGNATURE-----
+ tests/data/msg3.sig view

binary file changed (absent → 543 bytes)

+ tests/data/msg4 view
@@ -0,0 +1,1 @@+This message is detach-signed.
+ tests/data/msg4.asc view
@@ -0,0 +1,17 @@+-----BEGIN PGP SIGNATURE-----+Version: GnuPG v1.4.12 (GNU/Linux)++iQIcBAABAgAGBQJPmZE0AAoJEN/7iwtcb1WC6EcQAK+NpDfsgTJgq+5nhZLZQcDC++b+8K5eDn+Z/btFZz1h2mF3Y+MpJdgG5fvSSHsYRWiGuT8OBK5wm3vSYnSr8BeA5+JUJDdhasF7LVosb0ToNiWLbtj9D9iiqmCaPW56Y2u3Ktv5Y4nOAWZw31OGv21B1c+ptYVv2iy1qPGgnHxYgM5ib37hzKlkTEGKFNMpfYqsMBZyXiKuLpVhEWelawMnCPJ+3b7loOhYvR1Odmolg0dmcFsay7s3uXC/nze+xqTM0Z1HnMxQVW54aixhOoajDO6V+p7OUUN5g/PBXHvx/gh7gPqgRCazo93rHSgoP//AoEuljrU4W9iNpCEcE5HTdYx0b+xh10WxWE4VHI8BVt5qhSzPe8BmFRWz+g9CmsJdmhw45W8XxRJMJwoTWzGPcmHTFk+JTOgJ9/MKcBvYGsBd0wGYq82DbDPdtwGgh5Aa3nz1dxzLCq7qIhOa3VEYKAhCjTx+UHWnKZrdjSKz8U4D6CECyxlK5UApPc4jWzn3XYCX8s2F84YW7htduE57Yf60bXom+aefIbDWef1Q4MOUV10h1gyjXtdSiIHvJ0ItvGKmRiXztjhq6+azYN+4RaXzpF5/N+pqG/DNeTouzMRSzEoLTQ2oBHB8VIbCnP6J2Ck3wPfJZfc6FyCv0gMPQ5pTToll97+6toTAyOHl3pI/inp7IGj+=TJqj+-----END PGP SIGNATURE-----
+ tests/data/msg4.sig view

binary file changed (absent → 543 bytes)

tests/suite.hs view
@@ -3,12 +3,15 @@  import Test.HUnit -import Codec.Encryption.OpenPGP.ASCIIArmor (encode, decode, multipartMerge)+import Codec.Encryption.OpenPGP.ASCIIArmor (decode, decodeLazy, encode, encodeLazy, multipartMerge) import Codec.Encryption.OpenPGP.ASCIIArmor.Types+import Codec.Encryption.OpenPGP.ASCIIArmor.Utils  import Data.ByteString (ByteString) import qualified Data.ByteString as B+import qualified Data.ByteString.Lazy as BL import qualified Data.ByteString.Char8 as BC8+import qualified Data.ByteString.Lazy.Char8 as BLC8 import Data.Digest.CRC24 (crc24) import Data.Word (Word32) @@ -17,9 +20,9 @@  testArmorDecode :: FilePath -> [FilePath] -> Assertion testArmorDecode fp targets = do-    bs <- B.readFile $ "tests/data/" ++ fp-    tbss <- mapM (\target -> B.readFile $ "tests/data/" ++ target) targets-    case decode bs of+    bs <- BL.readFile $ "tests/data/" ++ fp+    tbss <- mapM (\target -> BL.readFile $ "tests/data/" ++ target) targets+    case decodeLazy bs of         Left e -> assertFailure $ "Decode failed (" ++ e ++ ") on " ++ fp         Right as -> assertEqual ("for " ++ fp) tbss (map getPayload as)     where@@ -27,20 +30,59 @@  testArmorMultipartDecode :: FilePath -> FilePath -> Assertion testArmorMultipartDecode fp target = do-    bs <- B.readFile $ "tests/data/" ++ fp-    tbs <- B.readFile $ "tests/data/" ++ target-    case decode bs of+    bs <- BL.readFile $ "tests/data/" ++ fp+    tbs <- BL.readFile $ "tests/data/" ++ target+    case decodeLazy bs of         Left e -> assertFailure $ "Decode failed (" ++ e ++ ") on " ++ fp         Right as -> assertEqual ("for " ++ fp) tbs (getPayload (multipartMerge as))     where         getPayload (Armor _ _ pl) = pl +testClearsignedDecodeBody :: FilePath -> FilePath -> Assertion+testClearsignedDecodeBody fp target = do+    bs <- BL.readFile $ "tests/data/" ++ fp+    tbs <- BL.readFile $ "tests/data/" ++ target+    case decodeLazy bs of+        Left e -> assertFailure $ "Decode failed (" ++ e ++ ") on " ++ fp+        Right [a] -> assertEqual ("for " ++ fp) (convertEndings tbs) (getBody a)+    where+        getBody (ClearSigned _ txt _) = txt+        convertEndings = crlfUnlinesLazy . BLC8.lines++testClearsignedDecodeSig :: FilePath -> FilePath -> Assertion+testClearsignedDecodeSig fp target = do+    bs <- BL.readFile $ "tests/data/" ++ fp+    tbs <- BL.readFile $ "tests/data/" ++ target+    case decodeLazy bs of+        Left e -> assertFailure $ "Decode failed (" ++ e ++ ") on " ++ fp+        Right [a] -> assertEqual ("for " ++ fp) tbs (getSig a)+    where+        getSig (ClearSigned _ _ (Armor _ _ sig)) = sig+ testArmorEncode :: [FilePath] -> FilePath -> Assertion testArmorEncode fps target = do-    bss <- mapM (\fp -> B.readFile $ "tests/data/" ++ fp) fps-    tbs <- B.readFile $ "tests/data/" ++ target-    assertEqual ("literaldata") (encode (map (\bs -> Armor ArmorMessage [("Version","OpenPrivacy 0.99")] bs) bss)) tbs+    bss <- mapM (\fp -> BL.readFile $ "tests/data/" ++ fp) fps+    tbs <- BL.readFile $ "tests/data/" ++ target+    assertEqual ("literaldata") tbs (encodeLazy (map (\bs -> Armor ArmorMessage [("Version","OpenPrivacy 0.99")] bs) bss)) +testClearsignedEncode :: FilePath -> FilePath -> FilePath -> Assertion+testClearsignedEncode ftxt fsig ftarget = do+    txt <- BL.readFile $ "tests/data/" ++ ftxt+    sig <- BL.readFile $ "tests/data/" ++ fsig+    target <- BL.readFile $ "tests/data/" ++ ftarget+    assertEqual ("clearsigned encode") target (encodeLazy [ClearSigned [("Hash","SHA1")] txt (Armor ArmorSignature [("Version","OpenPrivacy 0.99")] sig)])++testStrictDecode :: FilePath -> Assertion+testStrictDecode fp = do+    bs <- BL.readFile $ "tests/data/" ++ fp+    assertEqual ("strict decode") (decodeLazy bs :: Either String [Armor]) (decode (B.concat . BL.toChunks $ bs) :: Either String [Armor])++testStrictEncode :: FilePath -> Assertion+testStrictEncode fp = do+    bs <- BL.readFile $ "tests/data/" ++ fp+    let fakearmors = [Armor ArmorMessage [("Version","OpenPrivacy 0.99")] bs, ClearSigned [("Hash","SHA1")] bs (Armor ArmorSignature [("Version","OpenPrivacy 0.99")] bs)]+    assertEqual ("strict encode") (encodeLazy fakearmors) (BL.fromChunks [(encode fakearmors)])+ tests = [    testGroup "CRC24" [       testCase "CRC24: A" (testCRC24 (BC8.pack "A") 16680698)@@ -51,9 +93,15 @@       testCase "Decode sample armor" (testArmorDecode "msg1.asc" ["msg1.gpg"])     , testCase "Decode sample armor with cruft" (testArmorDecode "msg1a.asc" ["msg1.gpg"])     , testCase "Decode multiple sample armors" (testArmorDecode "msg1b.asc" ["msg1.gpg","msg1.gpg","msg1.gpg"])+    , testCase "Decode detached signature" (testArmorDecode "msg4.asc" ["msg4.sig"])     , testCase "Decode multi-part armor" (testArmorMultipartDecode "msg2.asc" "msg2.pgp")+    , testCase "Decode body of clear-signed" (testClearsignedDecodeBody "msg3.asc" "msg3")+    , testCase "Decode sig of clear-signed" (testClearsignedDecodeSig "msg3.asc" "msg3.sig")     , testCase "Encode sample armor" (testArmorEncode ["msg1.gpg"] "msg1.asc")     , testCase "Encode multiple sample armors" (testArmorEncode ["msg1.gpg","msg1.gpg","msg1.gpg"] "msg1c.asc")+    , testCase "Encode clear-signed sig" (testClearsignedEncode "msg3" "msg3.sig" "msg3.asc")+    , testCase "Decode from strict ByteString" (testStrictDecode "msg1.asc")+    , testCase "Encode to strict ByteString" (testStrictEncode "msg1.gpg")     ]  ]