iterIO (empty) → 0.1
raw patch · 22 files changed
+7504/−0 lines, 22 filesdep +HsOpenSSLdep +ListLikedep +arraysetup-changed
Dependencies added: HsOpenSSL, ListLike, array, attoparsec, base, bytestring, containers, filepath, mtl, network, old-locale, process, stringsearch, time, unix
Files
- Data/IterIO.hs +933/−0
- Data/IterIO/Atto.hs +52/−0
- Data/IterIO/Extra.hs +223/−0
- Data/IterIO/Http.hs +1333/−0
- Data/IterIO/HttpRoute.hs +380/−0
- Data/IterIO/Inum.hs +1114/−0
- Data/IterIO/Iter.hs +1020/−0
- Data/IterIO/ListLike.hs +487/−0
- Data/IterIO/Parse.hs +561/−0
- Data/IterIO/SSL.hs +100/−0
- Data/IterIO/Search.hs +114/−0
- Data/IterIO/Trans.hs +394/−0
- Data/IterIO/Zlib.hs +261/−0
- Data/IterIO/ZlibInt.hsc +124/−0
- Examples/fgrep.hs +59/−0
- Examples/httptest.hs +115/−0
- Examples/zpipe.hs +24/−0
- GNUmakefile +68/−0
- LICENSE +29/−0
- README +11/−0
- Setup.hs +3/−0
- iterIO.cabal +99/−0
+ Data/IterIO.hs view
@@ -0,0 +1,933 @@++{- |++This is the main module to import for the IterIO package. It+re-exports several other modules and mostly consists of+documentation--first a high-level overview of the iteratee model, then+a more detailed tutorial, finally a discussion of the differences from+other iteratee packages and acknowledgments.++See the "Data.IterIO.Iter", "Data.IterIO.Inum", and+"Data.IterIO.ListLike" modules for more detailed documentation of data+structures and functions. In addition, "Data.IterIO.Trans" (also+re-exported by this module) supplies functions that help you invoke+monad transformers from the mtl library from within the 'Iter' monad.++Several other potentially useful modules in the package are not+exported by default:++ * "Data.IterIO.Parse" includes parsec-like parsing combinators for+ iteratee input.++ * "Data.IterIO.Zlib" provides zlib and gzip format compression and+ decompression.++ * "Data.IterIO.SSL" provides support for SSL.++ * "Data.IterIO.Http" provides support for parsing and formatting+ HTTP, including handling form and file uploads (which can be+ processed in constant space). This may be useful in conjunction+ with "Data.IterIO.HttpRoute", which provides simple request routing+ support for web servers.++ * "Data.IterIO.Atto" provides support for running attoparsec parsers+ on iteratee input (see+ <http://hackage.haskell.org/package/attoparsec/>).++ * "Data.IterIO.Extra" provides debugging functions, as well as a+ loopback iteratee that can be used to test a protocol+ implementation against itself.++-}++module Data.IterIO+ (module Data.IterIO.Iter+ , module Data.IterIO.Trans+ , module Data.IterIO.Inum+ , module Data.IterIO.ListLike++ -- * Overview+ -- $Overview++ -- * Tutorial+ -- $Tutorial++ -- * Differences from other iteratee packages+ -- $Differences++ -- * Acknowledgments+ -- $Acknowledgments+ ) where++import Data.IterIO.Iter hiding (null, run -- names that might collide+ )+import Data.IterIO.Trans+import Data.IterIO.Inum+import Data.IterIO.ListLike++{- $Overview++ At a high level, an iteratee is a data sink that is fed chunks of+ data. It may return a useful result, or its utility may lie in+ monadic side-effects, such as storing received data to a file.+ Iteratees are represented by the type @'Iter' t m a@. Here @t@ is+ the type of data that the iteratee receives as input. (@t@ must be+ an instance of 'ChunkData', such as 'String' or lazy @ByteString@.)+ @m@ is the 'Monad' in which the iteratee runs--for instance 'IO'+ (or an instance of 'MonadIO') for the iteratee to perform IO. @a@+ is the type that the iteratee will return when it has consumed+ enough input to produce a result.++ An enumerator is a data source that feeds data chunks to an+ iteratee. Enumerators are also iteratees. We use the type @'Inum'+ tIn tOut m a@ to represent these /iteratee-enumerators/. As an+ iteratee, an 'Inum' sinks data of some input type, generally+ designated @tIn@. As an enumerator, the 'Inum' feeds data of a+ potentially different type, @tOut@, to another iteratee. Thus, the+ 'Inum' can be viewed as transcoding data from type @tIn@ to type+ @tOut@ for consumption by another iteratee.++ 'Inum's are generally constructed using the functions @'mkInum'@+ and @'mkInumM'@ in module "Data.IterIO.Inum". The first function+ uses a simple @'Iter' tIn m tOut@ to translate between input type+ @tIn@ and output type @tOut@. The second function, @'mkInumM'@,+ allows construction of more complex 'Inum's.++ An important special kind of 'Inum' is an /outer enumerator/,+ which is just an 'Inum' with the void input type @()@. Outer+ enumerators are sources of data. Rather than transcode input+ data, they produce data from monadic actions (or from pure data+ in the case of 'inumPure'). The type 'Onum' represents outer+ enumerators and is a synonym for 'Inum' with an input type of+ @()@.++ To execute iteratee-based IO, you must apply an 'Onum' to an+ 'Iter' with the '|$' (\"pipe apply\") binary operator.++ An important property of enumerators and iteratees is that they can+ be /fused/. The '|.' (\"fuse leftward\") operator fuses two+ 'Inum's together (provided the output type of the first is the+ input type of the second), yielding a new 'Inum' that transcodes+ from the input type of the first to the output type of the second.+ Similarly, the '.|' (\"fuse rightward\") operator fuses an 'Inum'+ to an 'Iter', yielding a new 'Iter' with a potentially different+ input type.++ Enumerators of the same type can also be /concatenated/, using+ the 'cat' function. @enum1 ``cat`` enum2@ produces an enumerator+ whose effect is to feed first @enum1@'s data then @enum2@'s data+ to an 'Iter'.+-}++{- $Tutorial++#tutorial#++The iterIO library performs IO by hooking up sources of data, called+/enumerators/, to data sinks, called /iteratees/, in a manner+reminiscent of Unix command pipelines. Compared to lazy IO, the+enumerator/iteratee paradigm provides better error handing,+referential transparency (which should, after all, be one of the big+advantages of Haskell), and equally convenient composition of protocol+layers and parsers without worrying about IO chunk boundaries.++Enumerators, implemented by the type 'Onum' (short for+/outer enumerator/, for reasons that will become clear below), are so+called because they enumerate all data elements (e.g., bytes or+packets) in some source such as a file or socket. Hence, an+enumerator should be viewed as a /source/ outputting chunks of data+whose type is a @'Monoid'@. (Actually, the input type must be of+class 'ChunkData', which is a @'Monoid'@ that additionally has a+method @'null'@ to test whether a piece of data is equal to+'mempty'.)++Iteratees, implemented by the type 'Iter', should be viewed as /sinks/+consuming data. When executing IO, the library /iterates/ over all+data elements output by the source, using an iteratee to produce a+result. The source may output data in chunks whose boundaries do not+coincide with logical message units; iteratees handle this+transparently, simplifying programming.++Here is a simple example:++@+ -- Return the first line of a file+ headFile :: FilePath -> IO String+ headFile path = 'enumFile' path '|$' 'lineI'+@++'enumFile' enumerates the contents of a file. 'lineI' returns a line+of input (discarding the newline). '|$' is the /pipe apply/ operator+that applies an 'Onum' to an 'Iter', returning the result of the+'Iter'--in this case the first line of the file named @path@.++An `Iter`'s main purpose may not be to produce a result. Some 'Iter's+are primarily useful for their side effects. For example, 'stdoutI'+writes data to standard output; 'handleI' similarly writes output to+an arbitrary file handle. Thus, the following function copies the+contents of a file to standard output:++@+ -- Copy file to standard output+ catFile :: FilePath -> IO ()+ catFile path = 'enumFile'' path '|$' 'stdoutI'+@++'enumFile'' is like 'enumFile' above, but type restricted to data in+the lazy @'ByteString'@ format, which is more efficient than plain+'String's. ('enumFile' supports multiple types, but in this example+there is not enough information for Haskell to choose one of them, so+we must use 'enumfile'' or use @::@ to specify a type explicitly.)+Once again, '|$' is used to execute the IO actions, but, this time,+the return value is just @()@; the interesting action lies in the side+effects of writing data to standard output while iterating over the+input with 'stdoutI'.++The real power of the iteratee abstraction lies in the fact that+'Iter's are monadic computations. One 'Iter' may invoke another to+make use of the first one's results. Here is an example of a function+that returns the first two lines of a file:++@+ -- | Return first two lines of file+ head2File :: FilePath -> IO (String, String)+ head2File path = 'enumFile' path '|$' lines2I+@++@+ -- | Iter that returns next two lines as a pair+ lines2I :: (Monad m) => 'Iter' String m (String, String)+ lines2I = do+ line1 <- 'lineI'+ line2 <- 'lineI'+ return (line1, line2)+@++This example illustrates several points. First, consider the type of+the @lines2I@ function: @'Iter' String m (String, String)@. The+'Iter' type constructor takes three type arguments. The first,+'String' in this case, specifies the type of input expected by the+iteratee. The last type, @(String, String)@ in this case, specifies+the result type of the iteratee. Finally, the middle type, @m@, is a+monad, because @'Iter' t@ (for a given input type @t@) is a monad+transformer (i.e., it is an instance of the 'MonadTrans' class). In+this case, when @head2File@ invokes @lines2I@, @m@ will be @IO@,+because @head2File@ is returning a result in the @IO@ monad. However,+@lines2I@ would work equally well with any other monad.++Next, notice the functioning of @'Iter' String m@ as a monad. The+type of 'lineI' in the above example is @'Iter' String m String@. The+@lines2I@ function executes 'lineI' twice using monadic @do@ syntax to+bind the results to @line1@ and @line2@. The monadic bind operator+hides the details of IO chunk boundaries. If, for instance, 'lineI'+needs more input because a newline character has not yet been read,+'lineI' returns to the containing enumerator asking for more data. If+the first 'lineI' receives more than a line of input, it simply passes+the residual input to the next invocation of 'lineI'. Both of these+actions are hidden by the syntax, making most code much easier to read+and write.++That explains the iteratee type 'Iter'. The enumerator type, 'Onum',+has the same three type arguments. Thus, the type of 'enumFile', as+instantiated in the above examples, is @'enumFile' :: 'Onum' String IO+a@. Most 'Onum' types are polymorphic in the last argument, so as to+be able to return whatever type the 'Iter' is returning. (In fact,+'enumFile' is polymorphic in the first two arguments, too, so as to+work with multiple @String@-like types and any monad in the+@'MonadIO'@ class.)++Here is an example of an 'Iter' with side effects:++@+ liftIOexampleI :: (MonadIO m) => 'Iter' String m ()+ liftIOexampleI = do+ line <- 'lineI'+ 'liftIO' $ putStrLn $ \"First line is: \" ++ line+ next <- 'takeI' 40+ 'liftIO' $ putStrLn $ \"And the next 40 bytes are: \" ++ next+@++Unlike @lines2I@, @liftIOexampleI@ does not return any interesting+result, but it uses the @'liftIO'@ monad transformer method to output+the first line of the file, followed by the next 40 bytes. The+'takeI' iteratee returns a 'String' (or @ByteString@) with exactly the+requested number of characters or bytes, unless an EOF (end-of-file)+is encountered.++Of course, the real power of command pipelines is that you can hook+multiple commands together. For instance, say you want to know how+many words in the system dictionary files contain a double k and start+with a lower-case letter. You could run a command like this:++> cat /usr/share/dict/words /usr/share/dict/extra.words \+> | grep kk | grep '^[a-z]' | wc -l++Let's see how to do something equivalent with iteratees, starting with+the @wc -l@ command, which counts lines. Here is an equivalent iteratee:++@+ lineCountI :: (Monad m) => 'Iter' String m Int+ lineCountI = count 0+ where count n = do+ line <- 'safeLineI'+ case line of+ Just _ -> count (n+1)+ Nothing -> return n+@++The 'safeLineI' function is like 'lineI', but returns a @'Maybe'+'String'@ (or @'Maybe' 'ByteString'@) which is 'Nothing' upon an EOF+condition. ('lineI' throws an exception on EOF.)++What about the @grep@ command? @grep@ sits in the middle of a+pipeline, so it acts both as a data sink and as a data source.+This is why we call such a pipeline stage an+/iteratee-enumerator/, or 'Inum'. Before defining our @grep@+equivalent, since multiple pipeline stages are going to be considering+the file one line at a time, let's first build an 'Inum' to separate+input into lines:++@+ import Data.ByteString as S+ import Data.ByteString.Char8 as S8+@++@+ -- | Break input into lines of type S.ByteString, as this type+ -- works most conveniently with regular expressions. (Otherwise,+ -- we would prefer lazy ByteStrings.)+ inumToLines :: (Monad m) => 'Inum' S.ByteString [S.ByteString] m a+ inumToLines = 'mkInum' $ do+ line <- 'lineI'+ return [line]+@++'Inum' takes four type arguments, compared to only three for 'Onum'.+That's because an 'Inum' is acting as both an iteratee and an+enumerator; it needn't be processing the same type of data in both+roles. In the above example, when acting as an iteratee,+@inumToLines@ consumes data of type @S.ByteString@ (the first type+argument), accepting one long stream of unstructured bytes. However,+as an enumerator, @inumToLines@ produces output of type+@[S.ByteString]@ (the second type argument), a /list/ of strings, one+per line of the file. In general the type @'Inum' tIn tOut m a@ is an+iteratee-enumerator taking input type @tIn@, producing output type+@tOut@, and feeding the output to an iteratee of type @'Iter' tOut m+a@.++In fact, an 'Onum' is just a special kind of 'Inum' with the void+input type @()@. The type @'Onum' t m a@ is just a synonym for+@'Inum' () t m a@. Most operations on 'Inum's can be used with+'Onum's as well, since an 'Onum' /is/ an 'Inum'. The converse is not+true, however. For example, the '|$' operator requires an 'Onum', as+it wouldn't know what data to feed to an arbitrary 'Inum'. (If you+need it, however, there is a function @run@, hidden by this module but+exported by "Data.IterIO.Iter", that executes an iteratee computation+of arbitrary input type by feeding EOF as input.)++Iteratee-enumerators are generally constructed using either 'mkInum'+or `mkInumM`, and by convention most 'Inum's have names starting+\"@inum@...\", except that 'Onum' names start \"@enum@...\". 'mkInum'+takes an argument of type @'Iter' tIn m tOut@ that consumes input of+type @tIn@ to produce output of type @tOut@. (For @inumToLines@,+@tIn@ is @S.ByteString@ and @tOut@ is @[S.ByteString]@). This is fine+for simple stateless translation functions, but sometimes one would+like to keep state and use more complex logic in an 'Inum'. For that,+the 'mkInumM' function creates an 'Inum' out of a computation in a+dedicated 'InumM' monad. See the "Data.IterIO.Inum" documentation for+more information on 'mkInumM'. In @inumToLines@, we do not need to+keep state. We are happy just to let 'lineI' throw an exception on+EOF, which `mkInum` will catch and handle gracefully.++Throwing an EOF exception--either implicitly by executing another+'Iter', or explicitly with 'throwEOFI'--is one of the standard ways to+exit an 'Inum' created by 'mkInum'. The other way is to return empty+input.++We similarly define an 'Inum' to filter out lines not matching a+regular expression (using the "Text.Regex.Posix.ByteString" library),+and a simple 'Inum' to count list elements (since @lineCountI ::+'Iter' String m Int@ has input data type @String@, while after+@inumToLines@ we need an 'Iter' with input data type+@[S.ByteString]@).++@+ inumGrep :: (Monad m) => String -> 'Inum' [S.ByteString] [S.ByteString] m a+ inumGrep re = `mkInum` $ do+ line <- 'headI'+ if line =~ packedRe then return [line] else inumGrep re+ where+ packedRe = S8.pack re+@++@+ lengthI :: (Monad m) => 'Iter' [t] m Int+ lengthI = count 0+ where count n = do+ line <- 'safeHeadI'+ case line of+ Just _ -> count (n+1)+ Nothing -> return n+@++Notice that when a line doesn't match, @inumGrep@ calls itself+recursively. This is necessary because returning an empty list of+lines signals to 'mkInum' that there is no more input. Thus, the+following code would cause our grep implementation to exit at the+first non-matching line:++@+ return $ if line =~ packedRe then [line] else [] -- Incorrect+@++(If you don't like this 'mempty'-means-EOF behavior, you can also wrap+the argument to 'mkInum' in the function 'whileNullI'.)++Now we are almost ready to assemble all the pieces. But recall that+the '|$' operator applies one 'Onum' to one 'Iter', yet now we have+two 'Onum's (because we want to look through two files), and three+'Inum's that we want to compose into a pipeline. The library+supports two types of composition for pipeline stages:+/concatenation/ and /fusing/.++Two 'Inum's (or 'Onum's) of the same type can be /concatenated/ with+the 'cat' function, producing a new data source that enumerates all of+the data in the first 'Inum' followed by all of the data in the+second.++There are two /fusing/ operators. The left-associative '|.' operator+fuses two 'Inum's, provided the output type of the first is the input+type of the second. (Mnemonic: it produces a pipeline stage that is+open on the right hand side, as it still needs to be applied to an+iteratee with '|$'.) The right-associative '.|' operator fuses an+'Inum' to an 'Iter', producing a new 'Iter'.++The fusing operators bind more tightly than the infix concatenation+functions, which in turn bind more tightly than '|$'. (Concatenation+operators can also be used through prefix function application, which+binds most tightly.) Hence, putting it all together, we produce the+following Haskell equivalent to the above Unix pipeline:++@+ grepCount :: IO Int+ grepCount = 'enumFile' \"\/usr\/share\/dict\/words\" '|.' inumToLines+ ``cat`` 'enumFile' \"\/usr\/share\/dict\/extra.words\" '|.' inumToLines+ '|$' inumGrep \"kk\"+ '.|' inumGrep \"^[a-z]\"+ '.|' lengthI+@++One often has a choice as to whether to fuse an 'Inum' to the+'Onum', or to the 'Iter'. For example, @grepCount@ could+alternatively have been implemented as:++@+ grepCount' :: IO Int+ grepCount' = 'cat' ('enumFile' \"\/usr\/share\/dict\/words\" '|.' inumToLines)+ ('enumFile' \"\/usr\/share\/dict\/extra.words\" '|.' inumToLines)+ '|.' inumGrep \"kk\"+ '|.' inumGrep \"^[a-z]\"+ '|$' lengthI+@++In this case, the two are essentially equivalent. However, for error+handling purposes, one should fuse together pipeline stages in which+errors have similar consequences. Often an 'Inum' or 'Onum' failure+is less serious than an 'Iter' failure. For example, in the above+example, if 'enumFile' fails because one of the files does not exist,+we might want to continue processing lines from the next file.+Conversely, if @lengthI@ fails or one of the @inumGrep@ stages fails+(most likely because the regular expression is illegal), there is not+much point in continuing the program. This is why the first example+fused @inumGrep@ to @lengthI@, though this won't matter until we+actually handle errors (see below).++Another alternative would have been to swap the order of concatenation+and fusing:++@+ grepCount'' :: IO Int+ grepCount'' = 'cat' ('enumFile' \"\/usr\/share\/dict\/words\")+ ('enumFile' \"\/usr\/share\/dict\/extra.words\")+ '|.' inumToLines+ '|$' inumGrep \"kk\"+ '.|' inumGrep \"^[a-z]\"+ '.|' lengthI+@++This last version changes the semantics of the counting slightly.+With @grepCount''@, if the first file has an incomplete last line,+this line will be merged with the first line of the second file, which+is probably not what you want. (For instance, if the incomplete last+line of the first file starts with a capital letter, then the first+line of the second file will not be counted even if it starts with a+lower-case letter and contains two \"k\"s.)++One limitation of all the @grepCount@ variants shown so far is that if+the first file does not exist, the whole operation aborts. This+might or might not be reasonable when counting lines, but in other+contexts we may want to resume after failure. Suppose we want to+implement a function like the Unix @grep@ command that searches for a+string in a bunch of files and prints all matching lines. If opening+or reading a file produces an error, the function should print the+error message and continue on with the next file.++Error handling is provided by the 'catchI' and 'inumCatch' functions,+which are roughly equivalent to the standard library @'catch'@+function. There is also a 'throwI' function analogous to @'throwIO'@+in the standard library. Because @'catch'@ only works in the IO+monad, 'catchI' and 'inumCatch' work by propagating synchronous+exceptions through the 'Iter' monad. @'liftIO'@ transforms IO errors+into such synchronous exceptions. Unfortunately, there is no way to+handle asynchronous exceptions such as those that arise in lazily+evaluated pure code (e.g., divide by zero) or those thrown by another+thread using @'throwTo'@. Fortunately, for our @grep@ example, we+only need to catch IO errors.++Here is the @grep@ code. We will analyze it below.++@+ grep :: String -> [FilePath] -> IO ()+ grep re files+ | null files = 'enumStdin' '|.' inumToLines '|$' inumGrep re '.|' linesOutI+ | otherwise = foldr1 'cat' (map enumLines files) '|$' inumGrep re '.|' linesOutI+ where+ enumLines file = 'inumCatch' ('enumFile' file '|.' inumToLines) handler+ handler :: 'IOError'+ -> 'IterR' () IO ('IterR' [S.ByteString] IO a)+ -> 'Iter' () IO ('IterR' [S.ByteString] IO a)+ handler e result = do+ liftIO (hPutStrLn stderr $ show e)+ 'resumeI' result+ linesOutI = do+ mline <- 'safeHeadI'+ case mline of+ Just line -> do liftIO $ S.putStrLn line+ linesOutI+ Nothing -> return ()+@++There are two cases. If the list of files to search is null, @grep@+simply reads from standard input, in which case there is only one+input stream and we do not care about resuming. In the second case,+we use @'foldr1' 'cat'@ to concatenate a list of 'Onum's. Each 'Onum'+is generated by the function @enumLines@, which fuses 'enumFile' to+our previously defined @inumToLines@, but also wraps the exception+handler function @handler@ around the enumerator using 'inumCatch'.++Note that unlike @catch@, 'inumCatch' expects an exception handler to+have /two/ arguments. The first argument, @e@ in this example, is the+exception itself. As with @catch@, the type of @e@ determines which+exceptions are caught, which is why we must either specify an explicit+type signature for @handler@ or somewhere specify @e@'s type+explicitly, for instance with:++> ...+> liftIO (hPutStrLn stderr $ show (e :: IOError))+> ...++Note that 'IOError' doesn't expose a type constructor, but for+exception types that do, it often suffices to define the function with+the exception constructor, as:++> handler e@(SomeException _) result = do ...++The second argument to @handler@, @result@, is the failed state of the+iteratee, which contains more information than just the exception. In+the case of an 'Inum' failure, it contains the state of the 'Iter'+that the 'Inum' was feeding when it failed. The type of 'result' is+'IterR'--which is the type returned by 'Iter's when they are fed+chunks of data. 'IterR' takes the same three type arguments as+'Iter'. The function 'resumeI' extracts and returns an @'Iter'+[S.ByteString] IO a@ from this failed result. Thus, the next+enumerator in a concatenated series can continue feeding it input.+If, instead of resuming, you want to re-throw the error, it suffices+to re-execute the failed result with @'reRunIter'@. For instance,+suppose we want to continue executing @grep@ when a named file does+not exist, but if some other error happens, we want to re-throw the+exception to abort the whole program. This could be achieved as+follows:++> handler e result = do+> if isDoesNotExistError e+> then do liftIO (hPutStrLn stderr $ show e)+> resumeI result+> else reRunIter result++Because printing an exception is so common, there is a function+'verboseResumeI' that prints exceptions before resuming (also+prefixing the program name). Thus, we can simplify the above function+to:++> handler e result = if isDoesNotExistError e+> then verboseResumeI result+> else reRunIter result++These last two @handler@ functions also do away with the need for an+explicit type signature, because the function @'isDoesNotExistError'@+has argument type 'IOError', constraining the type of @e@ to the type+of exceptions we want to catch.++-}++{- $Differences++The Iteratee approach was originally advocated by Oleg Kiselyov (see+talk slides at <http://okmij.org/ftp/Streams.html#iteratee>). The+main implementation by Kiselyov and John Lato is simply called+/iteratee/ (<http://hackage.haskell.org/package/iteratee>). Another+realization of the iteratee concepts is the /enumerator/ package+(<http://hackage.haskell.org/package/enumerator>). IterIO is a+re-implementation of these concepts from scratch. This section+discusses the differences between previous packages and iterIO, both+as a means for motivating iterIO's design and as a set of suggestions+for improving other iteratee implementations.++* /Base abstraction/++The iterIO package represents an iteratee as a pure function from a+chunk of pending input data to an iteratee result of type 'IterR':++@+ newtype 'Iter' t m a = 'Iter' { runIter :: 'Chunk' t -> 'IterR' t m a }+@++An 'IterR' can yield a result and residual input, or it can ask for+more input, or it can request to have an action executed in the+underlying monad, or it can signal failure. The fact that all+iteratees are functions of input ensures that iteratees generally see+/all/ pending input. Thus, iteratees can do things like measure the+length of buffered input to subtract it from the current file offset+and determine the effective position in a file.++`IterR`'s division of iteratee results into different outcomes such as+needing input or needing monadic actions allows the library to+distinguish between pure iteratees and those with potential side+effects. The ability to know that a specific iteratee is a pure+function in many cases allows one to parse LL(*) grammars without+large amounts of input buffering for backtracking (see below).++In contrast, the iteratee package uses continuation passing style+(CPS), in which an iteratee is a function taking two continuation+functions--one to call when done, and a second to call when either+requesting more input or failing:++> -- From the iteratee package:+> newtype Iteratee s m a = Iteratee{ runIter :: forall r.+> -- First the "onDone" function:+> (a -> Stream s -> m r) ->+> -- Next the "onCont" function:+> ((Stream s -> Iteratee s m a) -> Maybe SomeException -> m r) ->+> m r}++CPS has the advantage of exposing the bind operator of the underlying+monad, making 'lift' cheap and simple. Moreover, splitting into two+continuations saves the first and most common one (i.e., \"onDone\")+from the overhead of checking whether an error condition or request+for more input has occurred. See+<http://haskell.org/haskellwiki/Performance/Monads#Use__Continuation_Passing_Style>+for a good discussion of the advantages of CPS.++Because of CPS, iteratee should be capable of delivering the best+performance of the three iteratee packages. A disadvantage of+iterIO's approach is that every invocation of 'lift' must be+propagated all the way up the call chain, where a small amount of+overhead is added for each enclosing 'catchI' or similar call. While+iterIO can handle most successful 'IterR' outcomes and caught+exceptions locally without popping back up the call stack, there is+also potentially overhead from actually checking that the outcome was+successful at each bind site. (GHC's inliner may be able to avoid the+check in some cases.)++However, iteratee lacks several features of iterIO; offering these+features would likely reduce the benefits of CPS and complicate code.+For instance, there is no way to execute a pure iteratee without+monadic actions (the benefit touted above and described below for+LL(*) parsing). Moreover, iteratee's exception mechanism discards the+current location in the input stream, making it unsuitable for failed+parse alternatives. IterIO provides a general control mechanism to+make arbitrary requests from enumerators (such as seek, tell,+getpeername, get SSL information, etc.); iteratee instead overloads+the exception mechanism for control purposes, which prevents control+operations from returning values. Thus, while iteratee can implement+seek, it cannot, for instance, implement tell.++The enumerator package's approach is closer to iterIO's, but makes+every iteratee into a monadic action in the underlying monad @m@:++> -- From the enumerator package:+> newtype Iteratee a m b = Iteratee { runIteratee :: m (Step a m b) }++Here @Step@ is similar to iterIO's 'IterR' type, but the @m@ wrapper+disallows iterIO's LL(*) parsing tricks. It also causes gratuitous+invocation of @m@'s bind function, which can be expensive when using+stacks of monad transformers. Furthermore, enumerator discards the+input state on all errors, making it impossible to resume from+failures that leave the input in a known state (such as a parsing+lookahead failure).++* /Uniformity of abstraction/++IterIO's abstractions were refined over many iterations to become+minimal yet highly expressive and familiar to Unix shell users. Thus,+we have 'Iter's, which are data sinks that consume input and produce a+result. Then we have 'Inum's, which are also 'Iter's. These two data+types and can combined through pipes (i.e., fusing) and concatenation,+both of which have direct analogues in the Unix @|@ (pipe) operator+and @cat@ command.++Basing everything around these few concepts makes the library easier+to learn and use. For instance, because all 'Inum's are 'Iter's,+there is only one set of 'Iter' building blocks to learn. 'Inum'+implementations invoke the same 'Iter's that are used to build other+'Iter's. Moreover, 'Inum's and 'Iter's use the same error handling+mechanism. Finally, because 'Onum's are also 'Inum's, one set of+fusing and concatenation operators works for both.++By contrast, both the iteratee and enumerator packages use enumerator+types that are not iteratees. Hence, constructing enumerators is+harder and requires a different error handing mechanism. The packages+must introduce a third, hybrid \"Enumeratee\" type for inner pipeline+stages, and fusing Enumerators to Enumeratees is a different function+from fusing Enumeratees together.++Funneling everything through a small number of abstractions also+ensures that the right thing happens in corner cases. In particular,+all enumerator application happens through the pipe operator. Though+there are two pipe operators, a left associative one and a right+associative one, they internally use the same function: @a '|.' b =+(a '.|') . b@. Similarly, the pipe application operators ('|$' and+'.|$') are defined in terms of '.|'.++'.|' guarantees that its right-hand argument will receive an EOF when+the left hand argument terminates (whether normally or through an+exception). This is crucial for managing resources such file+descriptors, and works no matter how convoluted the control structure+of your program.++Consider the following realistic scenario of a web server constructed+as an 'Inum' that translates from HTTP requests to HTTP responses.+(Such an 'Inum' is provided by the function 'inumHttpServer' in+"Data.IterIO.HTTP".) The server's accept loop would resemble the+following:++@+ loop = do+ (sock, _) <- Net.accept $ listen_socket+ _ <- forkIO $ do+ (iter, enum) <- 'iterStream' (sock)+ enum '|$' 'inumHttpServer' ('ioHttpServer' handler) '.|' iter+ loop+@++This code depends on the fact that 'iterStream' closes @sock@ after+both the @iter@ has received an EOF and the @enum@ has returned. One+level down, 'inumHttpServer' uses 'mkInumM' to construct an 'Inum',+and has code looking something like this:++@+ req <- 'httpReqI' -- parse HTTP request+ resp <- 'liftI' $ inumHttpBody .| handler req -- invoke handler+ 'irun' $ enumHttpResp resp Nothing -- send response to client+@++The @handler@ gets run on the body of the message, and might decide to+process an HTTP POST request by saving an uploaded file to disk, for+instance with code like this:++@+ let saveFile _ field+ | ffName field == S8.pack \"file\" = do+ h <- liftIO $ openBinaryFile \"upload\" WriteMode+ 'handleI' h ``finallyI`` liftIO (hClose h)+ | otherwise = return ()+ in foldForm req saveFile ()+@++@foldForm@ internally is invoking an 'Inum' that parses HTTP+multipart/form-data to pipe each field of the form to the @saveFile@+function.++Now suppose 'inumHttpBody' fails (most likely because it receives an+EOF before reading the number of bytes specified in the Content-Length+header). Because 'inumHttpBody' is fused to @handler@, the failure+will cause @handler@ to receive an EOF, which will cause @foldForm@ to+fail, which will cause 'handleI' to receive an EOF and return, which+will ensure 'hClose' runs and the file handle @h@ is not leaked.++Once the EOFs have been processed, the exception will propagate+upwards making 'inumHttpServer' fail, which in turn will send an EOF+to @iter@. Then the exception will cause @enum@ to fail, after which+@sock@ will be closed. In summary, despite the complex structure of+the web server, because all the components are fused together with+pipe operators, corner cases like this just work with no need to worry+about leaked file descriptors.++* /Uniform error-handling and simplified monad transformers/++The iterIO library provides a traditional throw and catch exception+mechanism using its own functions 'throwI' and 'catchI', but keeping+the standard library exception hierarchy from "Control.Exception".+All of the support routines are carefully crafted to ensure that this+single exception mechanism is the only one you ever need, so that you+don't end up having to integrate different components with different+error strategies, a situation summarized amusingly in the following+blog post:+<http://www.randomhacks.net/articles/2007/03/10/haskell-8-ways-to-report-errors>.++A key to uniform error handling is ensuring that errors can be+propagated cleanly across different monads and transformers. Thus,+for instance, the iterIO 'liftIO' function translates all uncaught IO+errors into 'Iter' errors.++More importantly, iterIO is designed to support the standard mtl monad+transformers while keeping 'Iter' as the outermost monadic type. For+instance, if deep in the middle of some @'Iter' t 'IO'@ computation+you need a state transformer monad, you can invoke one with+'runStateTI', which is the iterIO equivalent of 'runStateT'. As seen+by comparing their effective types, 'runStateTI' keeps the 'Iter'+monad on the outside, and thus can cleanly propagate failures out of+the 'StateT' subcomputation:++> runStateT :: StateT s m a -> s -> m (a, s)+>+> runStateTI :: Iter t (StateT s m) a -> s -> Iter t m (a, s)++Similarly, there is a function @'liftI' :: (MonadTrans t) => Iter+s m a -> Iter s (t m) a@ that can be used to execute a computation in+which a level of monad transformer is stripped off the inner monadic+type.++An equally important feature is the ability to distinguish 'Iter'+failures from 'Inum' failures, given that the former are often more+serious than the latter. As shown by the @grep@ example in the+tutorial above, when one in a series of concatenated 'Inum's fails,+you often want to keep going without losing the state of the 'Iter'.+The enumerator package does not appear to support this distinction.+The iteratee package might, but it is not clear how to implement the+iteratee equivalent of the @grep@ example above.++By contrast, iterIO's 'Inum' mechanism was designed to be intuitive.+If you wrap a pipeline of 'Inum's in an 'inumCatch' statement, then+you will catch exactly the errors thrown by those 'Inum's, not those+thrown by pipeline stages outside the scope of the 'inumCatch' call.++It is because of this unified error handling mechanism that examples+such as the HTTP server above can be guaranteed not to leak resources.++* /Parser combinators for LL(*) grammars/++IterIO's "Data.IterIO.Parse" module supports parsing of iteratee input+using combinators similar to those found in parsec. However, parsec+supports only LL(1) grammars, and can lead to confusing failures--for+instance the parser @string \"foo\" \<|\> string \"for\"@ would fail+on input @\"for\"@. IterIO, by contrast, supports full LL(*) parsing,+meaning a parser can look arbitrarily far ahead before failing.++LL(*) parsers are generally disfavored because of their potential to+consume arbitrarily large amounts of memory to remember input for+backtracking. However, iterIO offers two mechanisms that mitigate the+problem.++First, because 'Iter's are constructed in such a way as to+differentiate requests for more input from execution of monadic+actions, it is possible to run multiple parsers in parallel. Consider+a hypothetical parser such as the following, designed to recognize the+input format and parse either XML or JSON data:++@+ parser :: 'Iter' 'L.ByteString' m Value+ parser = ('string' \"\<!DOCTYPE\" >> parseXml)+ \<|\> ('char' \'{\' >> parseJson)+@++@\<|\>@ is an infix synonym for the iterIO function 'multiParse',+which attempts to run two parsers concurrently on input as it arrives.+Because 'string' and 'char' are both pure parser combinators with no+monadic side effects, it is possible to run them both concurrently+without fear that the second rule--if it fails--will nonetheless have+produced side effects. In fact, at least one of the 'string' or the+'char' action will fail almost immediately, likely on the first chunk+of data. After one of the two has signaled a parse error, there is no+longer any need to store input for backtracking. Note this works even+if the subsequent functions @parseXml@ and @parseJson@ have monadic+side effects, because 'multiParse' doesn't need to invoke those+monadic actions to determine that one of the two parsers has failed.++A second way to avoid large amounts of storage for backtracking is to+use iterIO's '\/' operator, which is an infix synonym for 'ifNoParse'.+The formulation @iter '\/' no $ yes@ splits a parser into three+components. @iter@ is executed with backtracking enabled. If it+succeeds, then the saved data is discarded, @iter@'s result is fed to+the function @yes@, and any further failures will not cause input to+be rewound. If, on the other hand, @iter@ fails, then input is+rewound and @no@ is executed. The '\/' operator is very convenient+for long folds whose individual elements do not consume a lot of+input. For example, to parse and sum a list of numbers (given a+parser @number@ that skips spaces then parses one number), you might+do something like this:++> parseAndSumIntegerList :: Iter String IO Int+> parseAndSumIntegerList = loop 0+> where loop n = number \/ return n $ \n' -> loop (n + n')++Regardless of the length of the list of numbers being parsed,+@sumNumbers@ only ever needs to backtrack over the input consumed by a+single iteration of @number@, which is likely a small amount of extra+memory to keep around.++If you do want an LL(1) parser combinator library, iterIO supports+seamless integration with the attoparsec package. The function 'atto'+in "Data.IterIO.Atto" turns an attoparsec @Parser@ into an 'Iter'+monad, treating an attoparsec failure as an 'Iter' exception that can+be handled in the usual way with 'ifParse' or 'multiParse', or just+caught with 'catchI'. (Attoparsec has the additional advantage of+solving the annoying @string \"foo\" \<|\> string \"for\"@ issue by+special-casing @string@ to have more lookahead.)++Preliminary testing suggests that attoparsec can be about three times+faster than "Data.IterIO.Parse" on parse-intensive workloads. The+limitation is that attoparsec parsers must be pure. A good compromise+may be to use IterIO for coarse-grained parsing, and attoparsec for+more complex data structures. For example, you might want to use+iterIO's parsing of HTTP multipart/form-data (so as to be able to pipe+files to disk in constant space), but for fields with JSON data, use+'atto' to pipe the contents to the excellent attoparsec-based aeson+package.++-}++{- $Acknowledgments++Daniel Giffin contributed numerous suggestions and improvements to+both the code and documentation. Deian Stefan and David Terei helped+with testing and improving the package, as well as understanding+various relevant aspects of Haskell and GHC. Mike Hamburg made the+key suggestion of defining 'Onum's as type-restricted 'Inum's. The+author is grateful to John Lato for helping him understand much of the+important design rationale behind the original iteratee package. This+work was funded by the DARPA Clean-Slate Design of Resilient,+Adaptive, Secure Hosts (CRASH) program, BAA-10-70.++-}++-- LocalWords: IterIO iteratee monad mtl Iter combinators zlib gzip SSL Inum+-- LocalWords: attoparsec parsers loopback monadic Iteratees ChunkData tIn kk+-- LocalWords: MonadIO iteratees tOut transcoding Inum's mkInum mkInumM Onum+-- LocalWords: transcode inumPure transcodes enum iterIO Haskell mempty lineI+-- LocalWords: headFile FilePath enumFile Iter's stdoutI handleI catFile EOF+-- LocalWords: ByteString enumfile MonadTrans liftIOexampleI liftIO putStrLn+-- LocalWords: takeI wc lineCountI safeLineI ByteStrings inumToLines Onum's+-- LocalWords: InumM throwEOFI inumGrep headI packedRe lengthI safeHeadI usr+-- LocalWords: whileNullI grepCount catchI inumCatch throwI throwIO enumStdin+-- LocalWords: linesOutI foldr enumLines IOError IterR hPutStrLn stderr mline+-- LocalWords: resumeI isDoesNotExistError reRunIter verboseResumeI Oleg Lato+-- LocalWords: Kiselyov iterIO's newtype runIter forall onDone onCont GHC's+-- LocalWords: SomeException inliner iteratee's getpeername runIteratee iter+-- LocalWords: lookahead Enumeratee Enumeratees inumHttpServer forkIO req GHC+-- LocalWords: iterStream ioHttpServer httpReqI liftI inumHttpBody irun EOFs+-- LocalWords: enumHttpResp saveFile ffName openBinaryFile WriteMode finallyI+-- LocalWords: hClose foldForm multipart monads runStateTI runStateT StateT+-- LocalWords: subcomputation enumeratee JSON DOCTYPE parseXml parseJson atto+-- LocalWords: multiParse ifNoParse parseAndSumIntegerList sumNumbers ifParse+-- LocalWords: combinator aeson Giffin Deian Terei DARPA
+ Data/IterIO/Atto.hs view
@@ -0,0 +1,52 @@+{-# LANGUAGE DeriveDataTypeable #-}++-- | This module contains an adapter function to run attoparsec+-- 'Parser's from within the 'Iter' monad.+module Data.IterIO.Atto where++import Control.Exception+import Data.Attoparsec as A+import Data.Typeable+import qualified Data.ByteString.Lazy as L+import qualified Data.ByteString as S++import Data.IterIO++-- | Class of types whose 'Iter's can be converted to strict+-- 'S.ByteString's. Basically just strict 'S.ByteString's and lazy+-- 'L.ByteString's. This class mostly exists so that the 'atto'+-- function can work with either type of ByteString.+class (ChunkData t) => IterStrictByteString t where+ fromIterStrictByteString :: (Monad m) => Iter S.ByteString m a -> Iter t m a++instance IterStrictByteString S.ByteString where+ {-# INLINE fromIterStrictByteString #-}+ fromIterStrictByteString = id++instance IterStrictByteString L.ByteString where+ {-# INLINE fromIterStrictByteString #-}+ fromIterStrictByteString = (inumLtoS .|)++-- | Run an attoparsec parser in an 'Iter' monad. Throws an+-- 'IterFail' exception with constructor 'IterParseErr' if the parse+-- fails. (This exception can be handled with 'multiParse' and+-- 'ifParse'.)+atto :: (IterStrictByteString t, Monad m) =>+ A.Parser a -> Iter t m a+atto parser = fromIterStrictByteString $+ data0I >>= A.parseWith data0I parser >>= check+ where check (A.Done t a) = ungetI t >> return a+ check (A.Fail t _ e) = ungetI t >> throwParseI e+ check _ = error $ "atto: Partial"++-- | Try running an attoparsec parser. Returns @'Right' a@ if the+-- parser succeeds with result @a@. Returns @'Left' err@ where @err@+-- is an error message otherwise. Note that the input stream will be+-- in an indeterminate state should the parser fail. (If you need to+-- keep parsing input from some known state, it may be better to use+-- 'atto' in conjunction with 'multiParse'.)+tryAtto :: (IterStrictByteString t, Monad m) =>+ A.Parser a -> Iter t m (Either String a)+tryAtto parser = either check Right `fmap` tryFI (atto parser)+ where check (IterParseErr e) = Left e+ check _ = error "tryAtto"
+ Data/IterIO/Extra.hs view
@@ -0,0 +1,223 @@+{-# LANGUAGE ForeignFunctionInterface #-}+{-# LANGUAGE FlexibleInstances #-}++-- | This module contains miscellaneous functions plus a few pieces of+-- functionality that are missing from the standard Haskell libraries.+module Data.IterIO.Extra+ ( -- * Miscellaneous+ iterLoop+ , inumSplit+ -- , fixIterPure+ -- * Functionality missing from system libraries+ , SendRecvString(..)+ , hShutdown+ -- * Debugging functions+ , traceInput, traceI+ ) where++import Control.Concurrent (myThreadId)+import Control.Concurrent.MVar+import Control.Monad+import Control.Monad.Trans+import Data.ByteString.Internal (inlinePerformIO)+import Data.Monoid+import Debug.Trace+import Foreign.C+import qualified Data.ByteString as S+import qualified Data.ByteString.Char8 as S8+import qualified Data.ByteString.Lazy as L+import Network.Socket+import Network.Socket.ByteString as S+import Network.Socket.ByteString.Lazy as L+import System.IO++import Data.IterIO.Iter+import Data.IterIO.Inum++import Data.Typeable+import System.IO.Error+import GHC.IO.FD (FD(..))+import GHC.IO.Handle.Types (Handle__(..))+import GHC.IO.Handle.Internals (wantWritableHandle)++foreign import ccall unsafe "sys/socket.h shutdown"+ c_shutdown :: CInt -> CInt -> IO CInt++-- | Create a loopback @('Iter', 'Onum')@ pair. The iteratee and+-- enumerator can be used in different threads. Any data fed into the+-- 'Iter' will in turn be fed by the 'Onum' into whatever 'Iter' it+-- is given. This is useful for testing a protocol implementation+-- against itself.+iterLoop :: (MonadIO m, ChunkData t, Show t) =>+ m (Iter t m (), Onum t m a)+iterLoop = do+ -- The loopback is implemented with an MVar (MVar Chunk). The+ -- enumerator waits on the inner MVar, while the iteratee uses the outer + -- MVar to avoid races when appending to the stored chunk.+ mv <- liftIO $ newEmptyMVar >>= newMVar+ return (iter mv, enum mv)+ where+ iter mv = do+ c@(Chunk _ eof) <- chunkI+ liftIO $ withMVar mv $ \p ->+ do mp <- tryTakeMVar p+ putMVar p $ case mp of+ Nothing -> c+ Just c' -> mappend c' c+ if eof then return () else iter mv++ -- Note the ifeed mempty, which is there in case the enum feeds+ -- an iter that starts with a liftIO or something, and the other+ -- half of the loopback interface waits for the result of that+ -- liftIO to start producing data.+ enum mv = mkInumM (ifeed mempty >> loop)+ where loop = do p <- liftIO $ readMVar mv+ Chunk t eof <- liftIO $ takeMVar p+ done <- ifeed t+ when (not $ eof || done) loop+ +-- | Returns an 'Iter' that always returns itself until a result is+-- produced. You can fuse @inumSplit@ to an 'Iter' to produce an+-- 'Iter' that can safely be fed (e.g., with 'enumPure') from multiple+-- threads.+inumSplit :: (MonadIO m, ChunkData t) => Inum t t m a+inumSplit iter1 = do+ mv <- liftIO $ newMVar $ IterF iter1+ iter mv+ where+ iter mv = do+ (Chunk t eof) <- chunkI+ rold <- liftIO $ takeMVar mv+ rnew <- runIterMC (passCtl pullupResid) (reRunIter rold) $ chunk t+ liftIO $ putMVar mv rnew+ case rnew of+ IterF _ | not eof -> iter mv+ _ -> return rnew++{- fixIterPure allows MonadFix instances, which support+ out-of-order name bindings in a "rec" block, provided your file+ has {-# LANGUAGE RecursiveDo #-} at the top. A contrived example+ would be:++fixtest :: IO Int+fixtest = enumPure [10] `cat` enumPure [1] |$ fixee+ where+ fixee :: Iter [Int] IO Int+ fixee = rec+ liftIO $ putStrLn "test #1"+ c <- return $ a + b+ liftIO $ putStrLn "test #2"+ a <- headI+ liftIO $ putStrLn "test #3"+ b <- headI+ liftIO $ putStrLn "test #4"+ return c++-- A very convoluted way of computing factorial+fixtest2 :: Int -> IO Int+fixtest2 i = do+ f <- enumPure [2] `cat` enumPure [1] |$ mfix fact+ run $ f i+ where+ fact :: (Int -> Iter [Int] IO Int)+ -> Iter [Int] IO (Int -> Iter [Int] IO Int)+ fact f = do+ ignore <- headI+ liftIO $ putStrLn $ "ignoring " ++ show ignore+ base <- headI+ liftIO $ putStrLn $ "base is " ++ show base+ return $ \n -> if n <= 0+ then return base+ else liftM (n *) (f $ n - 1)++-- | This is a fixed point combinator for iteratees over monads that+-- have no side effects. If you wish to use @rec@ with such a monad,+-- you can define an instance of 'MonadFix' in which+-- @'mfix' = fixIterPure@. However, be warned that this /only/ works+-- when computations in the monad have no side effects, as+-- @fixIterPure@ will repeatedly re-invoke the function passsed in+-- when more input is required (thereby also repeating side-effects).+-- For cases in which the monad may have side effects, if the monad is+-- in the 'MonadIO' class then there is already an 'mfix' instance+-- defined using 'fixMonadIO'.+fixIterPure :: (ChunkData t, MonadFix m) =>+ (a -> Iter t m a) -> Iter t m a+fixIterPure f = Iter $ \c ->+ let ff ~(Done a _) = check $ runIter (f a) c+ -- Warning: IterF case re-runs function, repeating side effects+ check (IterF _) = return $ IterF $ Iter $ \c' ->+ runIter (fixIterPure f) (mappend c c')+ check (IterM m) = m >>= check+ check r = return r+ in IterM $ mfix ff+-}+++--+-- Some utility functions for things that are made hard by the Haskell+-- libraries+--++-- | @SendRecvString@ is the class of string-like objects that can be+-- used with datagram sockets.+class (Show t) => SendRecvString t where+ genRecv :: Socket -> Int -> IO t+ genSend :: Socket -> t -> IO ()+ genRecvFrom :: Socket -> Int -> IO (t, SockAddr)+ genSendTo :: Socket -> t -> SockAddr -> IO ()++instance SendRecvString [Char] where+ genRecv s len = liftM S8.unpack $ S.recv s len+ genSend s str = S.sendAll s (S8.pack str)+ genRecvFrom s len = do (str, a) <- S.recvFrom s len+ return (S8.unpack str, a)+ genSendTo s str dest = S.sendAllTo s (S8.pack str) dest++instance SendRecvString S.ByteString where+ genRecv s len = S.recv s len+ genSend s str = S.sendAll s str+ genRecvFrom s len = S.recvFrom s len+ genSendTo s str dest = S.sendAllTo s str dest++instance SendRecvString L.ByteString where+ genRecv s len = do str <- S.recv s len+ return $ L.fromChunks [str]+ genSend s str = L.sendAll s str+ genRecvFrom s len = do (str, a) <- S.recvFrom s len+ return (L.fromChunks [str], a)+ genSendTo s str dest = S.sendManyTo s (L.toChunks str) dest++-- | Flushes a file handle and calls the /shutdown/ system call so as+-- to write an EOF to a socket while still being able to read from it.+-- This is very important when the same file handle is being used to+-- to read data in an 'Onum' and to write data in an 'Iter'. Proper+-- protocol functioning may require the 'Iter' to send an EOF (e.g., a+-- TCP FIN segment), but the 'Onum' may still be reading from the+-- socket in a different thread.+hShutdown :: Handle -> CInt -> IO Int+hShutdown h how = do+ hFlush h+ wantWritableHandle "hShutdown" h $ \Handle__ {haDevice = dev} ->+ case cast dev of+ Just (FD {fdFD = fd}) -> liftM fromEnum $ c_shutdown fd how+ Nothing -> ioError (ioeSetErrorString+ (mkIOError illegalOperationErrorType+ "hShutdown" (Just h) Nothing) + "handle is not a file descriptor")+ +--+-- Debugging+--++-- | For debugging, print a tag along with the current residual input.+-- Not referentially transparent.+traceInput :: (ChunkData t, Monad m) => String -> Iter t m ()+traceInput tag = Iter $ \c -> trace (tag ++ ": " ++ show c) $ Done () c++-- | For debugging. Print the current thread ID and a message. Not+-- referentially transparent.+traceI :: (ChunkData t, Monad m) => String -> Iter t m ()+traceI msg = Iter $ \c -> inlinePerformIO $ do+ tid <- myThreadId+ putTraceMsg $ show tid ++ ": " ++ msg+ return $ Done () c
+ Data/IterIO/Http.hs view
@@ -0,0 +1,1333 @@+{-# LANGUAGE DeriveDataTypeable #-}+{-# OPTIONS_GHC -fno-warn-unused-do-bind #-}++module Data.IterIO.Http (-- * HTTP Request support+ HttpReq(..), reqNormalPath+ , httpReqI, inumHttpBody+ , inumToChunks, inumFromChunks+ , http_fmt_time, dateI+ , FormField(..), foldForm+ -- , urlencodedFormI, multipartI, inumMultipart+ -- , foldUrlencoded, foldMultipart, foldQuery+ -- * HTTP Response support+ , HttpStatus(..)+ , stat100, stat200, stat301, stat302, stat303, stat304+ , stat400, stat401, stat403, stat404, stat405+ , stat500, stat501+ , HttpResp(..), defaultHttpResp+ , mkHttpHead, mkHtmlResp, mkContentLenResp, mkOnumResp+ , resp301, resp303, resp403, resp404, resp405, resp500+ , enumHttpResp+ -- * HTTP connection handling+ , HttpRequestHandler+ , HttpServerConf(..), nullHttpServer, ioHttpServer+ , inumHttpServer+ -- -- * For debugging+ -- , postReq, encReq, mptest, mptest'+ -- , formTestMultipart, formTestUrlencoded+ ) where++import Control.Exception (SomeException(..))+import Control.Monad+import Control.Monad.Identity+import Control.Monad.Trans+import Data.Array.Unboxed+import Data.Bits+import Data.Map (Map)+import qualified Data.Map as Map+import Data.Maybe+import qualified Data.ByteString as S+import qualified Data.ByteString.Char8 as S8+import qualified Data.ByteString.Lazy as L+import qualified Data.ByteString.Lazy.Char8 as L8+import Data.ByteString.Internal (w2c, c2w)+-- import Data.Bits+import Data.Char+import Data.Int+import Data.List+import Data.Time+import Data.Typeable+import Data.Word+import System.Locale (defaultTimeLocale)+import System.IO+import Text.Printf++import Data.IterIO+import Data.IterIO.Parse+import Data.IterIO.Search++-- import System.IO++type L = L8.ByteString+type S = S.ByteString++strictify :: L -> S+strictify = S.concat . L.toChunks++--+-- Basic pieces+--++-- | Secton 19.3 of RFC2616: "The line terminator for message-header+-- fields is the sequence CRLF. However, we recommend that+-- applications, when parsing such headers, recognize a single LF as a+-- line terminator and ignore the leading CR."+crlf :: (Monad m) => Iter L m Word8+crlf = char '\r' *> char '\n' <|> char '\n'++-- | Spaces and tabs+spaces :: (Monad m) => Iter L m ()+spaces = skipWhile1I (\c -> c == eord ' ' || c == eord '\t')+ <?> "spaces"++-- | Linear whitespace, defined as:+--+-- > LWS = [CRLF] 1*( SP | HT )+--+-- Parses as a single space+lws :: (Monad m) => Iter L m L+lws = optionalI crlf >> L8.singleton ' ' <$ spaces <?> "linear white space"++-- | @olws = 'optionalI' 'lws'@+olws :: (Monad m) => Iter L m ()+olws = optionalI lws++-- | non-control characters+noctl :: (Monad m) => Iter L m L+noctl = while1I (\c -> c >= 0x20 && c < 0x7f) <?> "non-control characters"++-- | TEXT = 1*(any OCTET except CTLs | LWS)+text :: (Monad m) => Iter L m L+text = concat1I (noctl <|> lws) <?> "text (Data.IterIO.Http)"++-- | 'text' excluding some list of except characters.+text_except :: (Monad m) => String -> Iter L m L+text_except except = concat1I (while1I ok <|> lws)+ where+ except' = fmap c2w except+ ok c = c >= 0x20 && c < 0x7f && not (c `elem` except')++-- | Parse one hex digit and return its value from 0-15.+hex :: (Monad m) => Iter L m Int+hex = headI >>= digit <?> "hex digit"+ where+ digit c | c > 127 = expectedI (show $ w2c c) "hex digit"+ | otherwise = case hexTab ! c of+ -1 -> expectedI (show $ w2c c) "hex digit"+ n -> return $ fromIntegral n+ hexTab :: UArray Word8 Int8+ hexTab = listArray (0,127) $ fmap digitval ['\0'..'\177']+ digitval c | isHexDigit c = toEnum $ digitToInt c+ | otherwise = -1++-- | Parse a raw hexadecimal number (no \"0x...\" prefix).+hexInt :: (Monad m) => Iter L m Int+hexInt = foldM1I digit 0 hex+ where+ maxok = maxBound `shiftR` 4+ digit n d | n > maxok = throwParseI "hex integer too large"+ | otherwise = return $ (n `shiftL` 4) .|. d++-- | 1*\<any CHAR except CTLs or separators\>+token :: (Monad m) => Iter L m S+token = strictify <$> token'++-- | Lazy 'L.ByteString' version of 'token'.+token' :: (Monad m) => Iter L m L+token' = while1I (\c -> c < 127 && tokenTab ! c) <?> "token"+ where+ tokenTab :: UArray Word8 Bool+ tokenTab = listArray (0,127) $ fmap isTokenChar [0..127]+ isTokenChar c = c > 0x20 && c < 0x7f && not (elem (chr c) separators)+ separators = "()<>@,;:\\\"/[]?={} \t\177"++-- | Percent-decode input for as long as the non percent-escaped+-- characters match some predicate.+percent_decode :: (Monad m) => (Word8 -> Bool) -> Iter L m L+percent_decode test = foldrI L.cons' L.empty getc+ where+ getc = do+ c <- headI+ case c of+ _ | c == eord '%' -> getval+ _ | test c -> return c+ _ -> expectedI (show c) "percent_decode predicate"+ getval = do hi <- hex; lo <- hex; return $ toEnum $ 16 * hi + lo++-- | Parse a backslash-escaped character.+quoted_pair :: (Monad m) => Iter L m L+quoted_pair = char '\\' <:> headI <:> nil++-- | 'text' and 'quoted_pair's surrounded by double quotes.+quoted_string :: (Monad m) => Iter L m S+quoted_string = do char '"'+ ret <- concatI (text_except "\"" <|> quoted_pair)+ char '"'+ return $ strictify ret++{-+-- | 'text' and 'quoted_pair's surrounded by parentheses.+comment :: (Monad m) => Iter L m L+comment = char '('+ <:> concatI (text_except "()" <|> quoted_pair <|> comment)+ <++> string ")"+ <?> "comment"++-- | Parses q=N where 0.000 <= N <= 1.000, and returns the result+-- multiplied by 1000 as an integer (i.e., 1.0 returns 1000).+qvalue :: (Monad m) => Iter L m Int+qvalue = do char 'q'; olws; char '='; olws; frac <|> one+ where+ frac = do char '0'+ char '.' \/ return 0 $ \_ ->+ whileMinMaxI 0 3 (isDigit . w2c) \/ return 0 $ readI+ one = do char '1'+ optionalI $ do char '.'+ optionalI $ whileMinMaxI 0 3 (== eord '0')+ return 1000+-}++parameter :: (Monad m) => Iter L m (S, S)+parameter = do+ olws+ k <- token+ olws; char '='; olws+ v <- token <|> quoted_string+ return (k, v)++--+-- Date/time+--++-- | Formats a time in the format specified by RFC 2616.+http_fmt_time :: UTCTime -> String+http_fmt_time = formatTime defaultTimeLocale "%a, %_d %b %Y %H:%M:%S GMT"++dowmap :: Map L Int+dowmap = Map.fromList $ flip zip ([0..6] ++ [0..6]) $+ map L8.pack ["Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat"+ , "Sunday", "Monday", "Tuesday", "Wednesday"+ , "Thursday", "Friday", "Saturday", "Sunday"]++weekdayI :: (Monad m) => Iter L.ByteString m Int+weekdayI = mapI dowmap <?> "Day of Week"++monmap :: Map L Int+monmap = Map.fromList $ flip zip [1..12] $+ map L8.pack ["Jan", "Feb", "Mar", "Apr", "May", "Jun"+ , "Jul", "Aug", "Sep", "Oct", "Nov", "Dec"]++monthI :: (Monad m) => Iter L.ByteString m Int+monthI = mapI monmap <?> "Month"++timeI :: (Monad m) => Iter L.ByteString m TimeOfDay+timeI = do+ hours <- whileMinMaxI 2 2 (isDigit . w2c) >>= readI <?> "Hours"+ char ':'+ minutes <- whileMinMaxI 2 2 (isDigit . w2c) >>= readI <?> "Minutes"+ char ':'+ seconds <- whileMinMaxI 2 2 (isDigit . w2c) >>= readI <?> "Seconds"+ when (hours >= 24 || minutes >= 60 || seconds > 62) $ -- 2 leap seconds+ throwParseI "timeI: Invalid hours/minutes/seconds"+ return $ TimeOfDay hours minutes (fromIntegral (seconds :: Int))++rfc822_time :: (Monad m) => Iter L m UTCTime+rfc822_time = do+ weekdayI+ char ','+ spaces+ mday <- whileMinMaxI 1 2 (isDigit . w2c) >>= readI <?> "Day of Month"+ spaces+ month <- monthI+ spaces+ year <- whileMinMaxI 4 5 (isDigit . w2c) >>= readI <?> "Year"+ spaces+ tod <- timeI+ spaces+ string "GMT"+ return $ localTimeToUTC utc LocalTime {+ localDay = fromGregorian year month mday+ , localTimeOfDay = tod+ }++rfc850_time :: (Monad m) => Iter L m UTCTime+rfc850_time = do+ weekdayI+ char ','+ spaces+ mday <- whileMinMaxI 2 2 (isDigit . w2c) >>= readI <?> "Day of Month"+ char '-'+ month <- monthI+ char '-'+ year <- do y2 <- whileMinMaxI 2 2 (isDigit . w2c) >>= readI <?> "Year"+ return $ if y2 < 70 then y2 + 2000 else y2 + 1900+ spaces+ tod <- timeI+ spaces+ string "GMT"+ return $ localTimeToUTC utc LocalTime {+ localDay = fromGregorian year month mday+ , localTimeOfDay = tod+ }++asctime_time :: (Monad m) => Iter L m UTCTime+asctime_time = do+ weekdayI+ spaces+ month <- monthI+ spaces+ mday <- whileMinMaxI 1 2 (isDigit . w2c) >>= readI <?> "Day of Month"+ spaces+ tod <- timeI+ spaces+ year <- whileMinMaxI 4 5 (isDigit . w2c) >>= readI <?> "Year"+ return $ localTimeToUTC utc LocalTime {+ localDay = fromGregorian year month mday+ , localTimeOfDay = tod+ }++-- | Parses a Date/Time string in any one of the three formats+-- specified by RFC 2616.+dateI :: (Monad m) => Iter L.ByteString m UTCTime+dateI = rfc822_time <|> rfc850_time <|> asctime_time <?> "HTTP date/time"++--+-- URI parsing (RFC 3986)+--++-- | RFC3986 syntax classes unreserved characters+rfc3986_unreserved :: Word8+rfc3986_unreserved = 0x1++rfc3986_gen_delims :: Word8+rfc3986_gen_delims = 0x2++rfc3986_sub_delims :: Word8+rfc3986_sub_delims = 0x4++rfc3986_schemechars :: Word8+rfc3986_schemechars = 0x8++rfc3986_addrchars :: Word8+rfc3986_addrchars = 0x10++rfc3986_pcharslash :: Word8+rfc3986_pcharslash = 0x20++rfc3986_syntax :: UArray Word8 Word8+rfc3986_syntax = listArray (0, 255) $ fmap bits ['\0'..'\377']+ where+ bits c = foldl' (.|.) 0 [+ if isAlphaNum c || c `elem` "-._~"+ then rfc3986_unreserved else 0+ , if c `elem` ":/?#[]@" then rfc3986_gen_delims else 0+ , if c `elem` "!$&'()*+,;=" then rfc3986_sub_delims else 0+ , if isAlphaNum c || c `elem` "+-."+ then rfc3986_schemechars else 0+ , if isAlphaNum c || c `elem` "-._~:!$&'()*+,;="+ then rfc3986_addrchars else 0+ , if isAlphaNum c || c `elem` "-._~!$&'()*+,;=:@/"+ then rfc3986_pcharslash else 0+ ]++rfc3986_test :: Word8 -> Word8 -> Bool+rfc3986_test mask c = rfc3986_syntax ! c .&. mask /= 0++{-+isUnreserved :: Word8 -> Bool+isUnreserved c = rfc3986_syntax ! c .&. rfc3986_unreserved /= 0+-}++hostI :: (Monad m) => Iter L m (S, Maybe Int)+hostI = (,) <$> host <*> (Just <$> port <|> return Nothing) <?> "host"+ where+ host = S8.map toLower <$> strictify <$>+ (bracketed <|> percent_decode regnamechar)+ port = do _ <- char ':'; whileI (isDigit . w2c) >>= readI+ regnamechar c = (rfc3986_syntax ! c+ .&. (rfc3986_unreserved .|. rfc3986_sub_delims)) /= 0+ addrchar c = 0 /= rfc3986_syntax ! c .&. rfc3986_addrchars+ bracketed = char '[' <:> percent_decode addrchar <++> char ']' <:> nil++pathI :: (Monad m) => Iter L m (S, S)+pathI = dopath <?> "path"+ where+ dopath = do+ path <- strictify <$>+ (ensureI (== eord '/')+ *> percent_decode (rfc3986_test rfc3986_pcharslash))+ <|> return (S8.pack "/")+ query <- char '?' *> (strictify <$> whileI qpcharslash) <|> nil+ return (path, query)+ qpcharslash c = rfc3986_test rfc3986_pcharslash c || c == eord '?'+ +-- | Returns (scheme, host, path, query)+absUri :: (Monad m) => Iter L m (S, S, Maybe Int, S, S)+absUri = do+ scheme <- strictify <$> satisfy (isAlpha . w2c)+ <:> while1I (rfc3986_test rfc3986_schemechars)+ string "://"+ optionalI $ userinfo >> string "@"+ authority <- hostI+ (path, query) <- pathI+ return (scheme, fst authority, snd authority, path, query)+ where+ userinfo = percent_decode $ \c ->+ rfc3986_test (rfc3986_unreserved .|. rfc3986_sub_delims) c+ || c == eord ':'+ +-- | Returns (scheme, host, path, query).+uri :: (Monad m) => Iter L m (S, S, Maybe Int, S, S)+uri = absUri+ <|> path+ <|> char '*' *> return (S.empty, S.empty, Nothing, S8.pack "*", S.empty)+ <?> "URI"+ where+ path = do (p, q) <- ensureI (== eord '/') *> pathI+ return (S.empty, S.empty, Nothing, p, q)++-- | Turn a path into a list of components+path2list :: S -> [S]+path2list path = runIdentity $ inumPure path |$ (slash [] <?> "absolute path")+ where+ slash acc = while1I (eord '/' ==) \/ eofI *> return (reverse acc) $+ const $ comp acc+ comp acc = while1I (eord '/' /=) \/ return (reverse acc) $ \n ->+ case () of+ () | n == S8.pack "." -> slash acc+ () | n == S8.pack ".." ->+ if null acc then slash [] else slash $ tail acc+ () -> slash $ n:acc++--+-- HTTP request and header parsing+--++-- | Data structure representing an HTTP request message.+data HttpReq = HttpReq {+ reqMethod :: !S.ByteString+ -- ^ Method (e.g., GET, POST, ...).+ , reqPath :: !S.ByteString+ -- ^ Raw path from the URL (not needed if you use @reqPathList@+ -- and @reqPathParams@).+ , reqPathLst :: ![S.ByteString]+ -- ^ URL request path, broken into a list of directory components,+ -- and normalized to remove @\".\"@ and process @\"..\"@.+ , reqPathParams :: ![S.ByteString]+ -- ^ Used by 'routeVar' to save pathname components that are+ -- variables (used as a stack, so the last variable saved is the+ -- first one in the list).+ , reqPathCtx :: ![S.ByteString]+ -- ^ Stores pathname components that have been stripped off of+ -- @reqPathLst@ during routing.+ , reqQuery :: !S.ByteString+ -- ^ The portion of the URL after the @?@ character (if any).+ , reqHost :: !S.ByteString+ -- ^ Lower-case host header (or the host from the request line, if+ -- the request is for an absolute URI).+ , reqPort :: !(Maybe Int)+ -- ^ Port number if supplied in Host header.+ , reqVers :: !(Int, Int)+ -- ^ HTTP version major and minor number from the request line.+ , reqHeaders :: ![(S.ByteString, S.ByteString)]+ -- ^ List of all header field names and values in the HTTP+ -- request. Field names are converted to lowercase to allow+ -- easier searching.+ , reqCookies :: ![(S.ByteString, S.ByteString)]+ -- ^ List of Cookies supplied in the request.+ , reqContentType :: !(Maybe (S.ByteString, [(S.ByteString,S.ByteString)]))+ -- ^ Parsed version of the Content-Type header, if any. The first+ -- 'S.ByteString' is the actual content type. Following this is a+ -- list of parameter names and values. The most useful parameter+ -- is @\"boundary\"@, used with the @multipart/form-data@ content+ -- type.+ , reqContentLength :: !(Maybe Int)+ -- ^ Value of the content-Length header, if any.+ , reqTransferEncoding :: ![S.ByteString]+ -- ^ A list of the encodings in the Transfer-Encoding header.+ , reqIfModifiedSince :: !(Maybe UTCTime)+ } deriving (Typeable, Show)++defaultHttpReq :: HttpReq+defaultHttpReq = HttpReq { reqMethod = S.empty+ , reqPath = S.empty+ , reqPathLst = []+ , reqPathParams = []+ , reqPathCtx = []+ , reqQuery = S.empty+ , reqHost = S.empty+ , reqPort = Nothing+ , reqVers = (0, 0)+ , reqHeaders = []+ , reqCookies = []+ , reqContentType = Nothing+ , reqContentLength = Nothing+ , reqTransferEncoding = []+ , reqIfModifiedSince = Nothing+ }++-- | Returns a normalized version of the full requested path+-- (including all context in 'reqCtx') in the URL (e.g., where @\".\"@+-- has been eliminated, @\"..\"@ has been processed, there is exactly+-- one @\'/\'@ between each directory component, and the query has+-- been stripped off).+reqNormalPath :: HttpReq -> S.ByteString+reqNormalPath rq =+ S.intercalate slash $ S.empty : reqPathCtx rq ++ reqPathLst rq+ where slash = S8.singleton '/'++hTTPvers :: (Monad m) => Iter L m (Int, Int)+hTTPvers = do+ string "HTTP/"+ major <- whileI (isDigit . w2c) >>= readI+ char '.'+ minor <- whileI (isDigit . w2c) >>= readI+ return (major, minor)++-- | HTTP request line, defined by RFC2616 as:+--+-- > Request-Line = Method SP Request-URI SP HTTP-Version CRLF+request_line :: (Monad m) => Iter L m HttpReq+request_line = do+ method <- strictify <$> while1I (isUpper . w2c)+ spaces+ (_, host, mport, path, query) <- uri+ spaces+ (major, minor) <- hTTPvers+ optionalI spaces+ skipI crlf+ return defaultHttpReq {+ reqMethod = method+ , reqPath = path+ , reqPathLst = path2list path+ , reqQuery = query+ , reqHost = host+ , reqPort = mport+ , reqVers = (major, minor)+ }++request_headers :: (Monad m) => Map S (HttpReq -> Iter L m HttpReq)+request_headers = Map.fromList $+ map (\(a, b) -> (S8.map toLower $ S8.pack a, b)) $+ [+ ("Host", host_hdr)+ , ("Cookie", cookie_hdr)+ , ("Content-Type", content_type_hdr)+ , ("Content-Length", content_length_hdr)+ , ("Transfer-Encoding", transfer_encoding_hdr)+ , ("If-Modified-Since", if_modified_since_hdr)+ ]++host_hdr :: (Monad m) => HttpReq -> Iter L m HttpReq+host_hdr req = do+ (host, mport) <- hostI+ return req { reqHost = host, reqPort = mport }++-- Cookie header (RFC 6265)+cookie_hdr :: (Monad m) => HttpReq -> Iter L m HttpReq+cookie_hdr req = ifParse cookiesI setCookies ignore+ where+ cookiesI = sepBy1 parameter sep <* eofI+ sep = do olws; char ';' <|> char ','+ setCookies cookies = return $ req { reqCookies = cookies }+ ignore = nullI >> return req++content_type_hdr :: (Monad m) => HttpReq -> Iter L m HttpReq+content_type_hdr req = do+ typ <- token <++> char '/' <:> token+ parms <- many $ olws >> char ';' >> parameter+ return req { reqContentType = Just (typ, parms) }++content_length_hdr :: (Monad m) => HttpReq -> Iter L m HttpReq+content_length_hdr req = do+ len <- olws >> (while1I (isDigit . w2c) >>= readI) <* olws+ return req { reqContentLength = Just len }++transfer_encoding_hdr :: (Monad m) => HttpReq -> Iter L m HttpReq+transfer_encoding_hdr req = do+ tclist <- many tc+ return req { reqTransferEncoding = tclist }+ where+ tc = do+ olws+ coding <- S8.map toLower <$> token+ skipMany $ olws >> char ';' >> parameter+ return coding++if_modified_since_hdr :: (Monad m) => HttpReq -> Iter L m HttpReq+if_modified_since_hdr req = do+ modtime <- dateI+ return req { reqIfModifiedSince = Just modtime }++hdr_field_val :: (Monad m) => Iter L m (S, S)+hdr_field_val = do+ field <- S8.map toLower <$> token+ char ':'+ olws+ val <- strictify <$> text+ crlf+ return (field, val)++any_hdr :: (Monad m) => HttpReq -> Iter L m HttpReq+any_hdr req = do+ (field, val) <- hdr_field_val+ let req' = req { reqHeaders = (field, val) : reqHeaders req }+ case Map.lookup field request_headers of+ Nothing -> return req'+ Just f -> do+ r <- inumPure (L.fromChunks [val]) .|$+ (f req' <* (optionalI spaces >> eofI)+ <?> (S8.unpack field ++ " header"))+ return r++-- | Parse an HTTP header, returning an 'HttpReq' data structure.+httpReqI :: Monad m => Iter L.ByteString m HttpReq+httpReqI = do+ -- Section 4.1 of RFC 2616: "In the interest of robustness, servers+ -- SHOULD ignore any empty line(s) received where a Request-Line is+ -- expected. In other words, if the server is reading the protocol+ -- stream at the beginning of a message and receives a CRLF first,+ -- it should ignore the CRLF."+ skipMany crlf+ (request_line >>= next_hdr) <* crlf+ where next_hdr req = seq req $ any_hdr req \/ return req $ next_hdr+++--+-- Chunk encoding and decoding (RFC 2616)+--++-- | An HTTP Chunk encoder (as specified by RFC 2616).+inumToChunks :: (Monad m) => Inum L.ByteString L.ByteString m a+inumToChunks = mkInumM loop+ where+ loop = do+ Chunk s eof <- chunkI+ let len = L8.length s+ chunksize = L8.pack $ printf "%x\r\n" len+ trailer = if eof && len > 0+ then L8.pack "\r\n0\r\n\r\n"+ else L8.pack "\r\n"+ ifeed $ L8.concat [chunksize, s, trailer]+ unless eof loop++-- | An HTTP Chunk decoder (as specified by RFC 2616).+inumFromChunks :: (Monad m) => Inum L.ByteString L.ByteString m a+inumFromChunks = mkInumM $ getchunk+ where+ osp = skipWhileI $ \c -> c == eord ' ' || c == eord '\t'+ chunk_ext_val = do char '='; osp; token <|> quoted_string; osp+ chunk_ext = do char ';'; osp; token; osp; optionalI chunk_ext_val+ getchunk = do+ size <- hexInt <* (osp >> skipMany chunk_ext >> crlf)+ if size > 0 then ipipe (inumTakeExact size) >> getchunk+ else do+ skipMany (noctl >> crlf)+ skipI crlf++-- | This 'Inum' reads to the end of an HTTP message body (and not+-- beyond) and decodes the Transfer-Encoding. It handles straight+-- content of a size specified by the Content-Length header and+-- chunk-encoded content.+inumHttpBody :: (Monad m) => HttpReq -> Inum L.ByteString L.ByteString m a+inumHttpBody req =+ case reqTransferEncoding req of+ lst | null lst || lst == [S8.pack "identity"] ->+ if hasclen then inumTakeExact (fromJust $ reqContentLength req)+ else inumNull -- No message body present+ lst | lst == [S8.pack "chunked"] -> inumFromChunks+ lst -> inumFromChunks |. tcfold (reverse lst)+ where+ hasclen = isJust $ reqContentLength req+ tcfold [] = inumNop+ tcfold (h:t) + | h == S8.pack "identity" = tcfold t+ | h == S8.pack "chunked" = tcfold t -- Has to be first one+ --- | h == S8.pack "gzip" = inumGunzip |. tcfold t+ | otherwise = mkInum $+ fail $ "unknown Transfer-Coding " ++ chunkShow h++{-+-- | This 'Inum' reads to the end of an HTTP message body (and not+-- beyond) and decodes the Transfer-Encoding. It handles straight+-- content of a size specified by the Content-Length header,+-- chunk-encoded content, and content that has been gzipped then+-- chunk-encoded.+inumHttpBodyZ :: (MonadIO m) => HttpReq -> Inum L.ByteString L.ByteString m a+inumHttpBodyZ req =+ case reqTransferEncoding req of+ lst | null lst || lst == [S8.pack "identity"] ->+ if hasclen then inumTakeExact (fromJust $ reqContentLength req)+ else return -- No message body present+ lst | lst == [S8.pack "chunked"] -> inumFromChunks+ lst -> inumFromChunks |. tcfold (reverse lst)+ where+ hasclen = isJust $ reqContentLength req+ tcfold [] = inumNop+ tcfold (h:t) + | h == S8.pack "identity" = tcfold t+ | h == S8.pack "chunked" = tcfold t -- Has to be first one+ | h == S8.pack "gzip" = inumGunzip |. tcfold t+ | otherwise = mkInum $ fail $ "unknown Transfer-Coding "+ ++ chunkShow h+-}++--+-- Support for decoding form data+--++-- | Data structure representing the name and metadata of a control in+-- a submitted form.+data FormField = FormField {+ ffName :: !S.ByteString+ -- ^ Name of the form control being processed+ , ffParams :: ![(S.ByteString, S.ByteString)]+ -- ^ Parameters from the @Content-Disposition:@ header. This only+ -- applies to @Content-Type: multipart/form-data@, and will be+ -- empty for forms of type application/x-www-form-urlencoded or+ -- forms submitted in the URL parameters of a GET request.+ , ffHeaders :: ![(S.ByteString, S.ByteString)]+ -- ^ Extra headers following the @Content-Disposition:@ header of+ -- a @multipart/form-data@ post. Empty for other kinds of form+ -- submission.+ } deriving (Show)++defaultFormField :: FormField+defaultFormField = FormField {+ ffName = S.empty+ , ffParams = []+ , ffHeaders = []+ }++-- | Parses a form, and folds a function over each control. The value+-- of each control is available through Iteratee input. Thus, you can+-- extract the submitted value with 'pureI', or redirect it elsewhere+-- by executing another 'Iter'. For example, to parse a form and+-- print it to standard output (without buffering possibly large file+-- uploads in memory):+--+-- > do let docontrol _ field = do+-- > liftIO $ putStrLn $+-- > "The value of " ++ (S8.unpack $ ffName field) ++ " is:"+-- > stdoutI -- Send form value to standard output+-- > liftIO $ putStrLn "\n"+-- > foldForm req docontrol ()+--+-- Or to produce a list of (field, value) pairs, you can say something+-- like:+-- +-- > do let docontrol acc field = do+-- > val <- pureI+-- > return $ (ffName field, val) : acc+-- > foldForm req docontrol []+--+-- Note that for POSTed forms of enctype+-- @application/x-www-form-urlencoded@, @foldForm@ will read to the+-- end of its input. Thus, it is important to ensure @foldForm@ is+-- called from within an 'inumHttpBody' enumerator (which is+-- guaranteed by 'inumHttpServer').+foldForm :: (Monad m) =>+ HttpReq+ -> (a -> FormField -> Iter L.ByteString m a)+ -> a+ -> Iter L.ByteString m a+foldForm req = case reqContentType req of+ Nothing -> foldQuery req+ Just (mt, _) | mt == urlencoded -> foldUrlencoded req+ Just (mt, _) | mt == multipart -> foldMultipart req+ _ -> \_ _ -> throwParseI "foldForm: invalid Content-Type"+++--+-- application/x-www-form-urlencoded decoding+--+-- The HTML 4.01 spec says:+--+-- This is the default content type. Forms submitted with this+-- content type must be encoded as follows:+-- +-- 1. Control names and values are escaped. Space characters are+-- replaced by `+', and then reserved characters are escaped as+-- described in [RFC1738], section 2.2: Non-alphanumeric characters+-- are replaced by `%HH', a percent sign and two hexadecimal digits+-- representing the ASCII code of the character. Line breaks are+-- represented as "CR LF" pairs (i.e., `%0D%0A').+-- +-- 2. The control names/values are listed in the order they appear in+-- the document. The name is separated from the value by `=' and+-- name/value pairs are separated from each other by `&'.+--+-- RFC 1738 says:+-- ...only alphanumerics, the special characters "$-_.+!*'(),", and+-- reserved characters used for their reserved purposes may be used+-- unencoded within a URL.+--+-- On the other hand, RFC 3986 says the following are reserved:+-- :/?#[]@!$&'()*+,;=+--+-- And that the only unreserved characters are:+-- unreserved = ALPHA / DIGIT / "-" / "." / "_" / "~"+--+-- In practice, browsers seem to encode everything (including "~"),+-- except for ALPHA, DIGIT, and the four characters:+-- -._*+--+-- Given the confusion, we'll just accept almost everything except '&'+-- and '='.++urlencoded :: S+urlencoded = S8.pack "application/x-www-form-urlencoded"++urlencTab :: UArray Word8 Bool+urlencTab = listArray (0, 127) $ fmap ok ['\0'..'\177']+ where ok c | c <= ' ' = False+ | c >= '\177' = False+ | c `elem` "%+&=" = False+ | otherwise = True++controlI :: (Monad m) => Iter L m (S, S)+controlI = flip (<?>) "form control NAME=VALUE" $ do+ name <- encval+ value <- (char '=' >> encval) <|> nil+ return (name, value)+ where+ encval = liftM strictify $ concatI $+ someI (percent_decode (urlencTab !))+ <|> L8.singleton ' ' <$ char '+'++{-+urlencodedFormI :: (Monad m) => Iter L m [(S,S)]+urlencodedFormI = sepBy controlI (char '&')+-}++inumBind :: (ChunkData t, Monad m) =>+ Iter t m a -> (a -> Iter t m a) -> Iter t m a+inumBind m k = tryRI m >>= either reRunIter k+infixl 1 `inumBind`++foldControls :: (Monad m) => (a -> FormField -> Iter L m a) -> a -> Iter L m a+foldControls f z =+ controlI \/ return z $ \(k, v) ->+ inumPure (L.fromChunks [v]) .|+ f z defaultFormField { ffName = k } `inumBind` \a ->+ char '&' \/ return a $ \_ -> foldControls f a++foldUrlencoded :: (Monad m) =>+ HttpReq -> (a -> FormField -> Iter L m a) -> a -> Iter L m a+foldUrlencoded _req f z = foldControls f z++foldQuery :: (Monad m) =>+ HttpReq -> (a -> FormField -> Iter L m a) -> a -> Iter L m a+foldQuery req f z = inumPure (L.fromChunks [reqQuery req]) .| foldControls f z++--+-- multipart/form-data decoding, as specified throughout the following:+--+-- RFC 2045 - MIME part 1, including Content-Type header grammar+-- RFC 2046 - MIME part 2, including multipart boundary grammar+-- RFC 2047 - (splitting up parameters - not implemented yet here)+-- RFC 2183 - The Content-Disposition header grammar+-- +-- Less useful, but normative:+--+-- RFC 2388 - multipart/form data spec (mostly references above)+--++{-+-- | Mime boundary characters+bcharTab :: UArray Word8 Bool+bcharTab = listArray (0,127) $ fmap isBChar ['\0'..'\177']+ where isBChar c = isAlphaNum c || elem c otherBChars+ otherBChars = "'()/+_,-./:=? "+-}++multipart :: S+multipart = S8.pack "multipart/form-data"++reqBoundary :: HttpReq -> Maybe S+reqBoundary req = case reqContentType req of+ Just (typ, parms) | typ == multipart ->+ lookup (S8.pack "boundary") parms+ _ -> Nothing++multipartI :: (Monad m) => HttpReq -> Iter L m (Maybe (FormField))+multipartI req = case reqBoundary req of+ Just b -> findpart $ S8.pack "--" `S8.append` b+ Nothing -> return Nothing+ where+ nextLine :: (Monad m) => Iter L m ()+ nextLine = skipWhileI (\c -> c `elem` map eord " \t\r") >>+ char '\n' >> return ()+ findpart b = do+ match $ L.fromChunks [b]+ done <- ((string "--" >> return True) <|> return False) <* nextLine+ if done then return Nothing else Just <$> parsepart+ parsepart = do+ cdhdr@(field, val) <- hdr_field_val+ inumPure field .|$ stringCase "Content-Disposition"+ parms <- inumPure (L.fromChunks [val]) .|$+ sepBy (parameter <|> (token >>= \t -> return (t, S.empty)))+ (olws >> char ';')+ hdrs <- many hdr_field_val+ crlf+ return FormField {+ ffName = maybe S.empty id $ lookup (S8.pack "name") parms+ , ffParams = parms+ , ffHeaders = cdhdr:hdrs+ }++inumMultipart :: (Monad m) => HttpReq -> Inum L L m a+inumMultipart req iter = flip mkInumM (iter <* nullI) $ do+ b <- bstr+ ipipe $ inumStopString b+ (crlf <?> chunkShow b)+ where+ bstr = case reqBoundary req of+ Just b -> return $ S8.pack "\r\n--" `S8.append` b+ Nothing -> throwParseI "inumMultipart: no parts"++foldMultipart :: (Monad m) =>+ HttpReq -> (a -> FormField -> Iter L m a) -> a -> Iter L m a+foldMultipart req f z = multipartI req >>= doPart+ where+ doPart Nothing = return z+ doPart (Just mp) =+ inumMultipart req .| (f z mp <* nullI) `inumBind` \a ->+ foldMultipart req f a+++--+-- HTTP Response support+--++-- | HTTP status code and text description of response, for the first+-- line of an HTTP response message. A bunch of pre-defined statuses+-- from RFC 2616 are supplied under the names 'stat200', 'stat404',+-- 'stat500', etc.+data HttpStatus = HttpStatus !Int !S.ByteString deriving Show++mkStat :: Int -> String -> HttpStatus+mkStat n s = HttpStatus n $ S8.pack s++fmtStat :: HttpStatus -> L+fmtStat (HttpStatus n s) = L.fromChunks [+ S8.pack $ "HTTP/1.1 " ++ show n ++ " "+ , s, S8.pack "\r\n"]++stat100, stat200+ , stat301, stat302, stat303, stat304+ , stat400, stat401, stat403, stat404, stat405+ , stat500, stat501 :: HttpStatus+stat100 = mkStat 100 "Continue"+stat200 = mkStat 200 "OK"+stat301 = mkStat 301 "Moved Permanently"+stat302 = mkStat 302 "Found"+stat303 = mkStat 303 "See Other"+stat304 = mkStat 304 "Not Modified"+stat400 = mkStat 400 "Bad Request"+stat401 = mkStat 401 "Unauthorized"+stat403 = mkStat 403 "Forbidden"+stat404 = mkStat 404 "Not Found"+stat405 = mkStat 405 "Method not allowed"+stat500 = mkStat 500 "Internal Server Error"+stat501 = mkStat 501 "Not Implemented"++-- | A data structure describing an HTTP response message to be sent,+-- parameterized by the Monad in which the response will be written to+-- the network.+data HttpResp m = HttpResp {+ respStatus :: !HttpStatus+ -- ^ The response status.+ , respHeaders :: ![S.ByteString]+ -- ^ Headers to send back+ , respChunk :: !Bool+ -- ^ True if the message body should be passed through+ -- 'inumToChunks' and a \"@Transfer-Encoding: chunked@\" header+ -- should be added. Generally this should be 'True' unless you+ -- have added a @Content-Length@ header, manually set up chunk+ -- encoding by fusing it in 'respBody', or are not returning a+ -- message body with the reply.+ , respBody :: !(Onum L.ByteString m (IterR L.ByteString m ()))+ -- ^ 'Onum' producing the message body. Use 'inumNull' (which is+ -- an empty 'Inum') to produce an empty body for responses that do+ -- not contain a body.+ }++respAddHeader :: S.ByteString -> HttpResp m -> HttpResp m+respAddHeader hdr resp = resp { respHeaders = hdr : respHeaders resp }++instance Show (HttpResp m) where+ showsPrec _ resp rest = "HttpResp (" ++ show (respStatus resp)+ ++ ") " ++ show (respHeaders resp) ++ rest++-- | An empty HTTP response, to which you must add headers and+-- possibly a message body.+defaultHttpResp :: (Monad m) => HttpResp m+defaultHttpResp = HttpResp { respStatus = stat200+ , respHeaders = []+ , respChunk = True+ , respBody = inumNull+ }++-- | Generate an 'HttpResp' without a body.+mkHttpHead :: (Monad m) => HttpStatus -> HttpResp m+mkHttpHead stat = HttpResp { respStatus = stat+ , respHeaders = []+ , respChunk = False+ , respBody = inumNull }++-- | Generate an 'HttpResp' with a body of type @text/html@.+mkHtmlResp :: (Monad m) =>+ HttpStatus+ -> L.ByteString -- ^ Body as a pure lazy 'L.ByteString'+ -> HttpResp m+mkHtmlResp stat html = resp+ where resp0 = mkHttpHead stat `asTypeOf` resp+ ctype = S8.pack "Content-Type: text/html"+ len = S8.pack $ "Content-Length: " ++ show (L8.length html)+ resp = resp0 { respHeaders = respHeaders resp0 ++ [ctype, len]+ , respBody = inumPure html+ }++-- | Make an 'HttpResp' of an arbitrary content-type based on a pure+-- lazy 'L.ByteString'. Since the result is pure, this function first+-- measures its length so as to set a Content-Length header instead of+-- using HTTP chunk encoding.+mkContentLenResp :: (Monad m)+ => HttpStatus+ -> String -- ^ Value for Content-Type: header+ -> L.ByteString -- ^ Contents of response body+ -> HttpResp m+mkContentLenResp stat ctype body =+ HttpResp { respStatus = stat+ , respHeaders = [contentType, contentLength]+ , respChunk = False+ , respBody = inumPure body }+ where+ contentType = S8.pack $ "Content-Type: " ++ ctype+ contentLength = S8.pack $ "Content-Length: " ++ show (L8.length body)++-- | Make an 'HttpResp' of an arbitrary content-type based on an+-- 'Onum' that will dynamically generate the message body. Since the+-- message body is generated dynamically, the reply will use an HTTP+-- chunk encoding.+mkOnumResp :: (Monad m)+ => HttpStatus+ -> String+ -- ^ Value for Content-Type header:+ -> Onum L.ByteString m (IterR L.ByteString m ())+ -- ^ 'Onum' that will generate reply body dynamically.+ -> HttpResp m+mkOnumResp stat ctype body =+ HttpResp { respStatus = stat+ , respHeaders = [contentType]+ , respChunk = True+ , respBody = body }+ where+ contentType = S8.pack $ "Content-Type: " ++ ctype++htmlEscapeChar :: Char -> Maybe String+htmlEscapeChar '<' = Just "<"+htmlEscapeChar '>' = Just ">"+htmlEscapeChar '&' = Just "&"+htmlEscapeChar '"' = Just """+htmlEscapeChar '\'' = Just "&"+htmlEscapeChar _ = Nothing++htmlEscape :: String -> L.ByteString+htmlEscape str = L8.unfoldr next (str, "")+ where+ next (s, h:t) = Just (h, (s, t))+ next (h:t, "") = maybe (Just (h, (t, ""))) (curry next t) $+ htmlEscapeChar h+ next ("", "") = Nothing++-- | Generate a 301 (redirect) response.+resp301 :: (Monad m) => String -> HttpResp m+resp301 target =+ respAddHeader (S8.pack $ "Location: " ++ target) $ mkHtmlResp stat301 html+ where html = L8.concat+ [L8.pack+ "<!DOCTYPE HTML PUBLIC \"-//IETF//DTD HTML 2.0//EN\">\n\+ \<HTML><HEAD>\n\+ \<TITLE>301 Moved Permanently</TITLE>\n\+ \</HEAD><BODY>\n\+ \<H1>Moved Permanently</H1>\n\+ \<P>The document has moved <A HREF=\""+ , htmlEscape target+ , L8.pack "\">here</A>.</P>\n"]++-- | Generate a 303 (see other) response.+resp303 :: (Monad m) => String -> HttpResp m+resp303 target =+ respAddHeader (S8.pack $ "Location: " ++ target) $ mkHtmlResp stat303 html+ where html = L8.concat+ [L8.pack+ "<!DOCTYPE HTML PUBLIC \"-//IETF//DTD HTML 2.0//EN\">\n\+ \<HTML><HEAD>\n\+ \<TITLE>303 See Other</TITLE>\n\+ \</HEAD><BODY>\n\+ \<H1>See Other</H1>\n\+ \<P>The document has moved <A HREF=\""+ , htmlEscape target+ , L8.pack "\">here</A>.</P>\n"]++-- | Generate a 403 (forbidden) response.+resp403 :: (Monad m) => HttpReq -> HttpResp m+resp403 req = mkHtmlResp stat403 html+ where html = L8.concat+ [L8.pack+ "<!DOCTYPE HTML PUBLIC \"-//IETF//DTD HTML 2.0//EN\">\n\+ \<HTML><HEAD>\n\+ \<TITLE>403 Forbidden</TITLE>\n\+ \</HEAD><BODY>\n\+ \<H1>Forbidden</H1>\n\+ \<P>You don't have permission to access "+ , htmlEscape $ S8.unpack (reqNormalPath req)+ , L8.pack " on this server.</P>\n\+ \</BODY></HTML>\n"]++-- | Generate a 404 (not found) response.+resp404 :: (Monad m) => HttpReq -> HttpResp m+resp404 req = mkHtmlResp stat404 html+ where html = L8.concat+ [L8.pack+ "<!DOCTYPE HTML PUBLIC \"-//IETF//DTD HTML 2.0//EN\">\n\+ \<HTML><HEAD>\n\+ \<TITLE>404 Not Found</TITLE>\n\+ \</HEAD><BODY>\n\+ \<H1>Not Found</H1>\n\+ \<P>The requested URL "+ , htmlEscape $ S8.unpack (reqNormalPath req)+ , L8.pack " was not found on this server.</P>\n\+ \</BODY></HTML>\n"]++-- | Generate a 405 (method not allowed) response.+resp405 :: (Monad m) => HttpReq -> HttpResp m+resp405 req = mkHtmlResp stat405 html+ where html = L8.concat+ [L8.pack+ "<!DOCTYPE HTML PUBLIC \"-//IETF//DTD HTML 2.0//EN\">\n\+ \<HTML><HEAD>\n\+ \<TITLE>405 Method Not Allowed</TITLE>\n\+ \</HEAD><BODY>\n\+ \<H1>Method Not Allowed</H1>\n\+ \<P>The requested method "+ , L.fromChunks [reqMethod req]+ , L8.pack " is not allowed for the URL "+ , htmlEscape $ S8.unpack (reqNormalPath req)+ , L8.pack ".</P>\n\+ \</BODY></HTML>\n"]++-- | Generate a 500 (internal server error) response.+resp500 :: (Monad m) => String -> HttpResp m+resp500 msg = mkHtmlResp stat500 html+ where html = L8.concat+ [L8.pack+ "<!DOCTYPE HTML PUBLIC \"-//IETF//DTD HTML 2.0//EN\">\n\+ \<HTML><HEAD>\n\+ \<TITLE>500 Internal Server Error</TITLE>\n\+ \</HEAD><BODY>\n\+ \<H1>Internal Server Error</H1>\n\+ \<P>"+ , htmlEscape msg+ , L8.pack "</P>\n</BODY></HTML>\n"]++-- | Format and enumerate a response header and body.+enumHttpResp :: (Monad m) =>+ HttpResp m+ -> Maybe UTCTime -- ^ Time for @Date:@ header (if desired)+ -> Onum L.ByteString m ()+enumHttpResp resp mdate = inumPure fmtresp `cat` (respBody resp |. maybeChunk)+ where+ fmtresp = L.append (fmtStat $ respStatus resp) hdrs+ hdrs = foldr (L.append . hdr) (L8.pack "\r\n") $+ (if respChunk resp+ then ((S8.pack "Transfer-Encoding: chunked") :)+ else id) $+ (maybe id (\t -> (S8.pack ("Date: " ++ http_fmt_time t) :)) mdate)+ (respHeaders resp)+ hdr h = L.fromChunks [h, S8.pack "\r\n"]+ maybeChunk = if respChunk resp then inumToChunks else inumNop++-- | Given the headers of an HTTP request, provides an iteratee that+-- will process the request body (if any) and return a response.+type HttpRequestHandler m = HttpReq -> Iter L.ByteString m (HttpResp m)++-- | Data structure describing the configuration of an HTTP server for+-- 'inumHttpServer'.+data HttpServerConf m = HttpServerConf {+ srvLogger :: !(String -> Iter L.ByteString m ())+ , srvDate :: !(Iter L.ByteString m (Maybe UTCTime))+ , srvHandler :: !(HttpRequestHandler m)+ }++-- | Generate a null 'HttpServerConf' structure with no logging and no+-- Date header.+nullHttpServer :: (Monad m) => HttpRequestHandler m -> HttpServerConf m+nullHttpServer handler = HttpServerConf {+ srvLogger = const $ return ()+ , srvDate = return Nothing+ , srvHandler = handler+ }++-- | Generate an 'HttpServerConf' structure that uses IO calls to log to+-- standard error and get the current time for the Date header.+ioHttpServer :: (MonadIO m) => HttpRequestHandler m -> HttpServerConf m+ioHttpServer handler = HttpServerConf {+ srvLogger = liftIO . hPutStrLn stderr+ , srvDate = liftIO $ Just `liftM` getCurrentTime+ , srvHandler = handler+ }++-- | An 'Inum' that behaves like an HTTP server. The file+-- @Examples/httptest.hs@ that comes with the iterIO distribution+-- gives an example of how to use this function.+inumHttpServer :: (Monad m) =>+ HttpServerConf m -- ^ Server configuration+ -> Inum L.ByteString L.ByteString m ()+inumHttpServer server = mkInumM loop+ where+ loop = do+ eof <- atEOFI+ unless eof doreq+ doreq = do+ req <- httpReqI+ let handler = srvHandler server req+ resp <- liftI $ inumHttpBody req .|+ (catchI handler errHandler <* nullI)+ now <- liftI $ srvDate server+ tryI (irun $ enumHttpResp resp now) >>=+ either (fatal . fst) (const loop)+ errHandler e@(SomeException _) _ = do+ srvLogger server $ "Response error: " ++ show e+ return $ resp500 $ show e+ fatal e@(SomeException _) = do+ liftI $ srvLogger server $ "Reply error: " ++ show e+ return ()+++{-++--+-- Everything below here is crap for testing+--++formTest :: L -> IO ()+formTest b = inumPure b |$ handleReq+ where+ handleReq = do+ req <- httpReqI+ parts <- foldForm req getPart []+ liftIO $ putStrLn $ "### Summary\n" ++ show parts+ getPart result mp = do+ liftIO $ do putStrLn $ "### Part " ++ show (length result); print mp; putStrLn ""+ stdoutI+ liftIO $ putStr "\n\n"+ return (mp:result)++formTestMultipart :: IO ()+formTestMultipart = formTest postReq++formTestUrlencoded :: IO ()+formTestUrlencoded = formTest postReqUrlencoded++{-+dumpCtl :: () -> Multipart -> Iter L IO ()+dumpCtl () mp = do+ liftIO $ S.putStr (ffName mp) >> putStrLn ":"+ stdoutI+ liftIO $ putStrLn "\n"++x :: L+x = L8.pack "p1=v1&p2=v2"+-}++mptest :: IO ()+mptest = inumPure postReq |$ (httpReqI >>= getHead)+ where+ getHead req = do+ mmp <- multipartI req+ case mmp of+ Nothing -> return ()+ Just mp -> do liftIO $ print mp+ (inumMultipart req ) .| stdoutI+ (inumMultipart req ) .| nullI+ (inumMultipart req ) .| nullI+ (inumMultipart req ) .| nullI+ (inumMultipart req ) .| nullI+ crlf+ liftIO $ putStr "\n\n"+ getHead req++mptest' :: IO ()+mptest' = inumPure postReq |$ (httpReqI >>= getParts 0)+ where+ getParts :: (MonadIO m) => Integer -> HttpReq -> Iter L m ()+ getParts n req = do+ mmp <- multipartI req+ case mmp of+ Nothing -> return ()+ Just mp -> do liftIO $ do+ putStrLn $ "### Part " ++ show n+ print mp+ putStrLn ""+ (inumMultipart req) .| stdoutI+ liftIO $ putStr "\n\n"+ getParts (n+1) req+ ++postReq :: L+postReq = L8.pack+ "POST /testSubmit HTTP/1.1\n\+ \Host: localhost:8000\n\+ \User-Agent: Mozilla/5.0 (X11; U; Linux i686 (x86_64); en-US; rv:1.9.2.8) Gecko/20100722 Firefox/3.6.8\n\+ \Accept: text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8\n\+ \Accept-Language: en-us,en;q=0.5\n\+ \Accept-Encoding: gzip,deflate\n\+ \Accept-Charset: ISO-8859-1,utf-8;q=0.7,*;q=0.7\n\+ \Keep-Alive: 115\n\+ \Connection: keep-alive\n\+ \Content-Type: multipart/form-data; boundary=---------------------------28986267117678495841915281966\n\+ \Content-Length: 561\n\+ \\n\+ \-----------------------------28986267117678495841915281966\n\+ \Content-Disposition: form-data; name=\"justatestkey\"\n\+ \\n\+ \nothing\r\n\+ \-----------------------------28986267117678495841915281966\n\+ \Content-Disposition: form-data; name=\"hate\"\n\+ \\n\+ \666\r\n\+ \-----------------------------28986267117678495841915281966\n\+ \Content-Disposition: form-data; name=\"file1\"; filename=\"x\"\n\+ \Content-Type: application/octet-stream\n\+ \\n\+ \search scs.stanford.edu uun.org\n\+ \nameserver 127.0.0.1\n\+ \nameserver 64.81.79.2\n\+ \nameserver 216.231.41.2\n\+ \\r\n\+ \-----------------------------28986267117678495841915281966--\n"++postReqUrlencoded :: L+postReqUrlencoded = L8.pack+ "POST /testSubmit HTTP/1.1\n\+ \Host: localhost:8000\n\+ \User-Agent: Mozilla/5.0 (X11; U; Linux i686 (x86_64); en-US; rv:1.9.2.8) Gecko/20100722 Firefox/3.6.8\n\+ \Accept: text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8\n\+ \Accept-Language: en-us,en;q=0.5\n\+ \Accept-Encoding: gzip,deflate\n\+ \Accept-Charset: ISO-8859-1,utf-8;q=0.7,*;q=0.7\n\+ \Keep-Alive: 115\n\+ \Connection: keep-alive\n\+ \Content-Type: application/x-www-form-urlencoded\n\+ \Content-Length: 11\n\+ \\n\+ \p1=v1&p2=v2"+++encReq :: L+encReq = L8.pack "justatestkey=nothing&hate=666&file1=mtab"++-}
+ Data/IterIO/HttpRoute.hs view
@@ -0,0 +1,380 @@++module Data.IterIO.HttpRoute+ (HttpRoute(..)+ , runHttpRoute, addHeader+ , routeConst, routeFn, routeReq+ , routeMethod, routeHost, routeTop+ , HttpMap, routeMap, routeMap', routeName, routePath, routeVar+ , mimeTypesI, dirRedir, routeFileSys, FileSystemCalls(..), routeGenFileSys+ ) where++import Control.Monad+import Control.Monad.Trans+import Data.Char (toLower)+import qualified Data.ByteString.Char8 as S8+import qualified Data.ByteString.Lazy as L+import qualified Data.Map as Map+import Data.Maybe+import Data.Monoid+import Data.Time (UTCTime)+import Data.Time.Clock.POSIX (posixSecondsToUTCTime)+import System.FilePath+import System.IO+import System.IO.Error (isDoesNotExistError)+import System.Posix.Files+import System.Posix.IO+import System.Posix.Types++import Data.IterIO+import Data.IterIO.Http+import Data.IterIO.Parse++--+-- Request routing+--++-- | Simple HTTP request routing structure for 'inumHttpServer'. This+-- is a wrapper around a function on 'HttpReq' structures. If the+-- function accepts the 'HttpReq', it returns 'Just' a response+-- action. Otherwise it returns 'Nothing'.+--+-- @HttpRoute@ is a 'Monoid', and hence can be concatenated with+-- 'mappend' or 'mconcat'. For example, you can say something like:+--+-- > simpleServer :: Iter L.ByteString IO () -- Output to web browser+-- > -> Onum L.ByteString IO () -- Input from web browser+-- > -> IO ()+-- > simpleServer iter enum = enum |$ inumHttpServer server .| iter+-- > where htdocs = "/var/www/htdocs"+-- > server = ioHttpServer $ runHttpRoute routing+-- > routing = mconcat [ routeTop $ routeConst $ resp301 "/start.html"+-- > , routeName "apps" $ routeMap apps+-- > , routeFileSys mimeMap "index.html" htdocs+-- > ]+-- > apps = [ ("app1", routeFn app1)+-- > , ("app2", routeFn app2) ]+-- > +-- > app1 :: (Monad m) => HttpReq -> Iter L.ByteString m (HttpResp m)+-- > app1 = ...+--+-- The above function will redirect requests for @/@ to the URL+-- @/start.html@ using an HTTP 301 (Moved Permanently) response. Any+-- request for a path under @/apps/@ will be redirected to the+-- functions @app1@, @app2@, etc. Finally, any other file name will+-- be served out of the file system under the @\"\/var\/www\/htdocs\"@+-- directory. (This example assumes @mimeMap@ has been constructed as+-- discussed for 'mimeTypesI'.)+newtype HttpRoute m =+ HttpRoute (HttpReq -> Maybe (Iter L.ByteString m (HttpResp m)))++runHttpRoute :: (Monad m) =>+ HttpRoute m -> HttpReq -> Iter L.ByteString m (HttpResp m)+runHttpRoute (HttpRoute route) rq = fromMaybe (return $ resp404 rq) $ route rq++instance Monoid (HttpRoute m) where+ mempty = HttpRoute $ const Nothing+ mappend (HttpRoute a) (HttpRoute b) =+ HttpRoute $ \req -> a req `mplus` b req++popPath :: Bool -> HttpReq -> HttpReq+popPath isParm req =+ case reqPathLst req of+ h:t -> req { reqPathLst = t+ , reqPathCtx = reqPathCtx req ++ [h]+ , reqPathParams = if isParm then h : reqPathParams req+ else reqPathParams req+ }+ _ -> error "Data.IterIO.Http.popPath: empty path"++-- | Prepend a header field to the response produced by an 'HttpRoute'+-- if that 'HttpRoute' is successful. For example, to let clients+-- cache static data for an hour, you might use:+--+-- @+-- addHeader ('S8.pack' \"Cache-control: max-age=3600\") $+-- 'routeFileSys' mime ('dirRedir' \"index.html\") \"\/var\/www\/htdocs\"+-- @+addHeader :: (Monad m) => S8.ByteString -> HttpRoute m -> HttpRoute m+addHeader h (HttpRoute r) = HttpRoute $ \req -> liftM (liftM addit) (r req)+ where addit resp = resp { respHeaders = h : respHeaders resp }++-- | Route all requests to a constant response action that does not+-- depend on the request. This route always succeeds, so anything+-- 'mappend'ed will never be used.+routeConst :: (Monad m) => HttpResp m -> HttpRoute m+routeConst resp = HttpRoute $ const $ Just $ return resp++-- | Route all requests to a particular function. This route always+-- succeeds, so anything 'mappend'ed will never be used.+routeFn :: (HttpReq -> Iter L.ByteString m (HttpResp m)) -> HttpRoute m+routeFn fn = HttpRoute $ Just . fn++-- | Select a route based on some arbitrary function of the request.+-- For most purposes, the existing predicates ('routeName',+-- 'routePath', etc.) should be fine, but occationally you might want+-- to define a custom predicate. For example, to reject methods other+-- then \"GET\" or \"POST\" at the top of your route, you could say:+--+-- @+-- myRoute = 'mconcat' [ rejectBadMethod+-- , otherRoute1+-- , ...+-- ]+-- ...+--+--rejectBadMethod :: 'HttpRoute' m+--rejectBadMethod =+-- routeReq $ \req ->+-- case 'reqMethod' req of+-- s | s == 'S8.pack' \"GET\" || s == 'S8.pack' \"PUT\" ->+-- 'mempty' {- reject route, falling through+-- to rest of myRoute -}+-- _ -> 'routeConst' $ 'resp405' req {- reject request -}+-- @+routeReq :: (HttpReq -> HttpRoute m) -> HttpRoute m+routeReq fn = HttpRoute $ \req ->+ let (HttpRoute route) = fn req+ in route req+++-- | Route the root directory (/).+routeTop :: HttpRoute m -> HttpRoute m+routeTop (HttpRoute route) = HttpRoute $ \req ->+ if null $ reqPathLst req then route req+ else Nothing++-- | Route requests whose \"Host:\" header matches a particular+-- string.+routeHost :: String -- ^ String to compare against host (must be lower-case)+ -> HttpRoute m -- ^ Target route to follow if host matches+ -> HttpRoute m+routeHost host (HttpRoute route) = HttpRoute check+ where shost = S8.pack $ map toLower host+ check req | reqHost req /= shost = Nothing+ | otherwise = route req++-- | Route based on the method (GET, POST, HEAD, etc.) in a request.+routeMethod :: String -- ^ String method should match+ -> HttpRoute m -- ^ Target route to take if method matches+ -> HttpRoute m+routeMethod method (HttpRoute route) = HttpRoute check+ where smethod = S8.pack method+ check req | reqMethod req /= smethod = Nothing+ | otherwise = route req++-- | Type alias for the argument of 'routeMap'.+type HttpMap m = [(String, HttpRoute m)]++-- | @routeMap@ builds an efficient map out of a list of+-- @(directory_name, 'HttpRoute')@ pairs. It matches all requests and+-- returns a 404 error if there is a request for a name not present in+-- the map.+routeMap :: (Monad m) => HttpMap m -> HttpRoute m+routeMap lst = routeMap' lst `mappend` routeFn (return . resp404)++-- | @routeMap'@ is like @routeMap@, but only matches names that exist+-- in the map. Thus, multiple @routeMap'@ results can be combined+-- with 'mappend'. By contrast, combining @routeMap@ results with+-- 'mappend' is useless--the first one will match all requests (and+-- return a 404 error for names that do not appear in the map).+routeMap' :: HttpMap m -> HttpRoute m+routeMap' lst = HttpRoute check+ where+ check req = case reqPathLst req of+ h:_ -> maybe Nothing+ (\(HttpRoute route) -> route $ popPath False req)+ (Map.lookup h rmap)+ _ -> Nothing+ packfirst (a, b) = (S8.pack a, b)+ rmap = Map.fromListWithKey nocombine $ map packfirst lst+ nocombine k _ _ = error $ "routeMap: duplicate key for " ++ S8.unpack k++-- | Routes a specific directory name, like 'routeMap' for a singleton+-- map.+routeName :: String -> HttpRoute m -> HttpRoute m+routeName name (HttpRoute route) = HttpRoute check+ where sname = S8.pack name+ headok (h:_) | h == sname = True+ headok _ = False+ check req | headok (reqPathLst req) = route $ popPath False req+ check _ = Nothing++-- | Routes a specific path, like 'routeName', except that the path+-- can include several directories.+routePath :: String -> HttpRoute m -> HttpRoute m+routePath path route = foldr routeName route dirs+ where dirs = case splitDirectories path of+ "/":t -> t+ t -> t++-- | Matches any directory name, but additionally pushes it onto the+-- front of the 'reqPathParams' list in the 'HttpReq' structure. This+-- allows the name to serve as a variable argument to the eventual+-- handling function.+routeVar :: HttpRoute m -> HttpRoute m+routeVar (HttpRoute route) = HttpRoute check+ where check req = case reqPathLst req of+ _:_ -> route $ popPath True req+ _ -> Nothing++--+-- Routing to Filesystem+--++-- | Parses @mime.types@ file data. Returns a function mapping file+-- suffixes to mime types. The argument is a default mime type for+-- suffixes to do not match any in the mime.types data. (Reasonable+-- defaults might be @\"text\/html\"@, @\"text\/plain\"@, or, more+-- pedantically but less usefully, @\"application\/octet-stream\"@.)+--+-- Since this likely doesn't change, it is convenient just to define+-- it once in your program, for instance with something like:+--+-- > mimeMap :: String -> S8.ByteString+-- > mimeMap = unsafePerformIO $ do+-- > path <- findMimeTypes ["mime.types"+-- > , "/etc/mime.types"+-- > , "/var/www/conf/mime.types"]+-- > enumFile path |$ mimeTypesI "application/octet-stream"+-- > where+-- > findMimeTypes (h:t) = do exist <- fileExist h+-- > if exist then return h else findMimeTypes t+-- > findMimeTypes [] = return "mime.types" -- cause error+mimeTypesI :: (Monad m) =>+ String+ -> Iter S8.ByteString m (String -> S8.ByteString)+mimeTypesI deftype = do+ mmap <- Map.fromList <$> concatI ((mimeLine <|> nil) <* eol)+ return $ \suffix -> maybe (S8.pack deftype) id $ Map.lookup suffix mmap+ where+ mimeLine = do+ typ <- word+ many $ do space; ext <- word; return (S8.unpack ext, typ)+ word = while1I $ \c -> c > eord ' ' && c <= eord '~'+ space = skipWhile1I $ \c -> c == eord ' ' || c == eord '\t'+ comment = char '#' >> skipWhileI (/= eord '\n')+ eol = do+ optionalI space+ optionalI comment+ optionalI (char '\r'); char '\n'++-- | An abstract representation of file system calls returning an+-- opaque handle type @h@ in an 'Iter' parameterized by an arbitrary+-- 'Monad' @m@. This representation allows one to use+-- 'routeGenFileSys' in a monad that is not an instance of 'MonadIO'.+data FileSystemCalls h m = FileSystemCalls {+ fs_stat :: !(FilePath -> Iter L.ByteString m FileStatus)+ -- ^ Return file attributes.+ , fs_open :: !(FilePath -> Iter L.ByteString m h)+ -- ^ Open file and return an opaque handle of type @h@.+ , fs_close :: !(h -> Iter L.ByteString m ())+ -- ^ Close an open file. You must call this unless you apply the+ -- enumerator returned by @fs_enum@.+ , fs_fstat :: !(h -> Iter L.ByteString m FileStatus)+ -- ^ Return the attributes of an open file.+ , fs_enum :: !(h -> Iter L.ByteString m+ (Onum L.ByteString m (IterR L.ByteString m ())))+ -- ^ Enumerate the contents of an open file, then close the file.+ -- If you apply the 'Onum' returned by @fs_enum@, you do not need+ -- to call @fs_close@.+ }++-- | Default file system calls for instances of the @MonadIO@ class.+defaultFileSystemCalls :: (MonadIO m) => FileSystemCalls Fd m+defaultFileSystemCalls = FileSystemCalls { fs_stat = liftIO . getFileStatus+ , fs_open = liftIO . pathToFd+ , fs_close = liftIO . closeFd+ , fs_fstat = liftIO . getFdStatus+ , fs_enum = liftIO . fdToOnum+ }+ where pathToFd path = openFd path ReadOnly Nothing defaultFileFlags+ fdToOnum fd = do h <- fdToHandle fd+ return $ enumHandle h `inumFinally` liftIO (hClose h)++-- | @dirRedir indexFileName@ redirects requests to the URL formed by+-- appending @\"/\" ++ indexFileName@ to the requested URL.+dirRedir :: (Monad m) => FilePath -> FilePath -> HttpRoute m+dirRedir index _path = routeFn $ \req -> return $+ resp301 $ S8.unpack (reqNormalPath req) ++ '/':index++modTimeUTC :: FileStatus -> UTCTime+modTimeUTC = posixSecondsToUTCTime . realToFrac . modificationTime++-- | Route a request to a directory tree in the file system. It gets+-- the Content-Length from the target file's attributes (after opening+-- the file). Thus, overwriting files on an active server could cause+-- problems, while renaming new files into place should be safe.+routeFileSys :: (MonadIO m) =>+ (String -> S8.ByteString)+ -- ^ Map of file suffixes to mime types (see 'mimeTypesI')+ -> (FilePath -> HttpRoute m)+ -- ^ Handler to invoke when the URL maps to a directory+ -- in the file system. Reasonable options include:+ --+ -- * @('const' 'mempty')@ to do nothing, which results in a+ -- 403 forbidden,+ --+ -- * @('dirRedir' \"index.html\")@ to redirect directory+ -- accesses to an index file, and+ --+ -- * a recursive invocation such as @(routeFileSys+ -- typemap . (++ \"/index.html\"))@ to re-route the+ -- request directly to an index file.+ -> FilePath+ -- ^ Pathname of directory to serve from file system+ -> HttpRoute m+routeFileSys = routeGenFileSys defaultFileSystemCalls++-- | A generalized version of 'routeFileSys' that takes a+-- 'FileSystemCalls' object and can therefore work outside of the+-- 'MonadIO' monad. Other than the 'FileSystemCalls' object, the+-- arguments and their meaning are identical to 'routeFileSys'.+routeGenFileSys :: (Monad m) =>+ FileSystemCalls h m+ -> (String -> S8.ByteString)+ -> (FilePath -> HttpRoute m)+ -> FilePath+ -> HttpRoute m+routeGenFileSys fs typemap index dir0 = HttpRoute $ Just . check+ where+ dir = if null dir0 then "." else dir0+ checkErr req e _ | isDoesNotExistError e = return $ resp404 req+ | otherwise = return $ resp500 (show e)+ check req = flip catchI (checkErr req) $ do+ let path = dir ++ concatMap (('/' :) . S8.unpack) (reqPathLst req)+ st <- fs_stat fs path+ case () of+ _ | isRegularFile st -> doFile req path st+ | not (isDirectory st) -> return $ resp404 req+ | otherwise -> runHttpRoute+ (index path `mappend` routeConst (resp403 req)) req+ doFile req path st+ | reqMethod req == S8.pack "GET"+ && maybe True (< (modTimeUTC st)) (reqIfModifiedSince req) = do+ fd <- fs_open fs path+ -- Use attributes from opened file in case file name changes+ st' <- fs_fstat fs fd `onExceptionI` fs_close fs fd+ if isRegularFile st'+ then do body <- fs_enum fs fd `onExceptionI` fs_close fs fd+ return $ resp { respHeaders = mkHeaders req st'+ , respBody = body }+ else do fs_close fs fd -- File no longer file -- re-try+ check req+ | reqMethod req == S8.pack "GET" =+ return $ resp { respStatus = stat304 }+ | reqMethod req == S8.pack "HEAD" =+ return $ resp { respStatus = stat200 }+ | otherwise = return $ resp405 req+ where resp = defaultHttpResp { respChunk = False+ , respHeaders = mkHeaders req st }++ mkHeaders req st =+ [ S8.pack $ "Last-Modified: " ++ (http_fmt_time $ modTimeUTC st)+ , S8.pack $ "Content-Length: " ++ (show $ fileSize st)+ , S8.pack "Content-Type: " `S8.append` typemap (fileExt req) ]+ fileExt req =+ drop 1 $ takeExtension $ case reqPathLst req of+ [] -> dir+ l -> S8.unpack $ last l++
+ Data/IterIO/Inum.hs view
@@ -0,0 +1,1114 @@+{-# LANGUAGE DeriveDataTypeable #-}++module Data.IterIO.Inum+ (-- * Base types+ Inum, Onum+ -- * Concatenation and fusing operators+ , (|$), (.|$), cat, lcat, (|.), (.|)+ -- * Exception functions+ , inumCatch, inumFinally, inumOnException+ , resumeI, verboseResumeI+ -- * Simple enumerator construction function+ -- $mkInumIntro+ , ResidHandler, CtlHandler+ , mkInumC, mkInum, mkInumP+ , inumBracket+ -- * Utilities+ , pullupResid+ , noCtl, passCtl, consCtl, mkCtl, mkFlushCtl+ , runIterM, runIterMC, runInum+ -- * Some basic Inums+ , inumNop, inumNull, inumPure, enumPure, inumRepeat+ -- * Enumerator construction monad+ -- $mkInumMIntro+ , InumM, mkInumM, mkInumAutoM+ , setCtlHandler, setAutoEOF, setAutoDone, addCleanup, withCleanup+ , ifeed, ifeed1, ipipe, irun, irepeat, ipopresid, idone+ ) where++import Prelude hiding (null)+import Control.Exception (Exception(..))+import Control.Monad+import Control.Monad.Trans+import Data.Maybe+import Data.Monoid+import Data.Typeable+import System.Environment (getProgName)+import System.IO++import Data.IterIO.Iter+import Data.IterIO.Trans++--+-- Enumerator types+--++-- | The type of an /iterator-enumerator/, which transcodes data from+-- some input type @tIn@ to some output type @tOut@. An @Inum@ acts+-- as an 'Iter' when consuming data, then acts as an enumerator when+-- feeding transcoded data to another 'Iter'.+--+-- At a high level, one can think of an @Inum@ as a function from+-- 'Iter's to 'IterR's, where an @Inum@'s input and output types are+-- different. A simpler-seeming alternative to @Inum@ might have+-- been:+--+-- > type Inum' tIn tOut m a = Iter tOut m a -> Iter tIn m a+--+-- In fact, given an @Inum@ object @inum@, it is possible to construct+-- a function of type @Inum'@ with @(inum '.|')@. But sometimes one+-- might like to concatenate @Inum@s. For instance, consider a+-- network protocol that changes encryption or compression modes+-- midstream. Transcoding is done by @Inum@s. To change transcoding+-- methods after applying an @Inum@ to an iteratee requires the+-- ability to \"pop\" the iteratee back out of the @Inum@ so as to be+-- able to hand it to another @Inum@. @Inum@'s return type (@Iter tIn+-- m (IterR tOut m a)@ as opposed to @Iter tIn m a@) allows the+-- monadic bind operator '>>=' to accomplish this popping in+-- conjunction with the 'tryRI' and 'reRunIter' functions.+--+-- All @Inum@s must obey the following two rules.+--+-- 1. /An/ @Inum@ /may never feed a chunk with the EOF flag set to/+-- /it's target/ 'Iter'. Instead, upon receiving EOF, the @Inum@+-- should simply return the state of the inner 'Iter' (this is how+-- \"popping\" the iteratee back out works--If the @Inum@ passed+-- the EOF through to the 'Iter', the 'Iter' would stop requesting+-- more input and could not be handed off to a new @Inum@).+--+-- 2. /An/ @Inum@ /must always return the state of its target/ 'Iter'.+-- This is true even when the @Inum@ fails, and is why the 'Fail'+-- state contains a @'Maybe' a@ field.+--+-- In addition to returning when it receives an EOF or fails, an+-- @Inum@ should return when the target 'Iter' returns a result or+-- fails. An @Inum@ may also unilaterally return the state of the+-- iteratee at any earlier point, for instance if it has reached some+-- logical message boundary (e.g., many protocols finish processing+-- headers upon reading a blank line).+--+-- @Inum@s are generally constructed with one of the 'mkInum' or+-- 'mkInumM' functions, which hide most of the error handling details+-- and ensure the above rules are obeyed. Most @Inum@s are+-- polymorphic in the last type, @a@, in order to work with iteratees+-- returning any type.+type Inum tIn tOut m a = Iter tOut m a -> Iter tIn m (IterR tOut m a)++-- | An @Onum t m a@ is just an 'Inum' in which the input is+-- @()@--i.e., @'Inum' () t m a@--so that there is no meaningful input+-- data to transcode. Such an enumerator is called an+-- /outer enumerator/, because it must produce the data it feeds to+-- 'Iter's by either executing actions in monad @m@, or from its own+-- internal pure state (as for 'enumPure').+--+-- As with 'Inum's, an @Onum@ should under no circumstances ever feed+-- a chunk with the EOF bit set to its 'Iter' argument. When the+-- @Onum@ runs out of data, it must simply return the current state of+-- the 'Iter'. This way more data from another source can still be+-- fed to the iteratee, as happens when enumerators are concatenated+-- with the 'cat' function.+--+-- @Onum@s should generally be constructed using the 'mkInum' or+-- 'mkInumM' function, just like 'Inum's, the only difference being+-- that for an @Onum@ the input type is @()@, so executing 'Iter's to+-- consume input will be of little use.+type Onum t m a = Inum () t m a++-- Concatenation and fusing functions++-- | Run an 'Onum' on an 'Iter'. This is the main way of actually+-- executing IO with 'Iter's. @|$@ is a type-restricted version of+-- the following code, in which @inum@ must be an 'Onum':+--+-- @+-- inum |$ iter = 'run' (inum .| iter)+-- infixr 2 |$+-- @+(|$) :: (ChunkData t, Monad m) => Onum t m a -> Iter t m a -> m a+(|$) inum iter = run (inum .| iter)+infixr 2 |$++-- | @.|$@ is a variant of '|$' that allows you to apply an 'Onum'+-- from within an 'Iter' monad. This is often useful in conjuction+-- with 'enumPure', if you want to parse at some coarse-granularity+-- (such as lines), and then re-parse the contents of some+-- coarser-grained parse unit. For example:+--+-- > rawcommand <- lineI+-- > command <- enumPure rawcommand .|$ parseCommandI+-- > return Request { cmd = command, rawcmd = rawcommand }+--+-- @.|$@ has the same fixity as @|$@, namely:+--+-- > infixr 2 .|$+--+-- Note the important distinction between @(.|$)@ and @('.|')@.+-- @(.|$)@ runs an 'Onum' and does not touch the current input, while+-- ('.|') pipes the current input through an 'Inum'. For instance, to+-- send the contents of a file to standard output (regardless of the+-- current input), you must say @'enumFile' \".signature\" .|$+-- 'stdoutI'@. But to take the current input, compress it, and send+-- the result to standard output, you must use '.|', as in @'inumGzip'+-- '.|' 'stdoutI'@.+--+-- As suggested by the types, @enum .|$ iter@ is sort of equivalent to+-- @'lift' (enum |$ iter)@, except that the latter will call 'throw'+-- on failures, causing language-level exceptions that cannot be+-- caught within the outer 'Iter'. Thus, it is better to use @.|$@+-- than @'lift' (... '|$' ...)@, though in the less general case of+-- the IO monad, @enum .|$ iter@ is equivalent to @'liftIO' (enum '|$'+-- iter)@ as illustrated by the following examples:+--+-- > -- Catches exception, because .|$ propagates failure through the outer+-- > -- Iter Monad, where it can still be caught.+-- > apply1 :: IO String+-- > apply1 = enumPure "test1" |$ iter `catchI` handler+-- > where+-- > iter = enumPure "test2" .|$ fail "error"+-- > handler (SomeException _) _ = return "caught error"+-- > +-- > -- Does not catch error. |$ turns the Iter failure into a language-+-- > -- level exception, which can only be caught in the IO Monad.+-- > apply2 :: IO String+-- > apply2 = enumPure "test1" |$ iter `catchI` handler+-- > where+-- > iter = lift (enumPure "test2" |$ fail "error")+-- > handler (SomeException _) _ = return "caught error"+-- > +-- > -- Catches the exception, because liftIO uses the IO catch function to+-- > -- turn language-level exceptions into monadic Iter failures. (By+-- > -- contrast, lift works in any Monad, so cannot do this in apply2.)+-- > -- This example illustrates how liftIO is not equivalent to lift.+-- > apply3 :: IO String+-- > apply3 = enumPure "test1" |$ iter `catchI` handler+-- > where+-- > iter = liftIO (enumPure "test2" |$ fail "error")+-- > handler (SomeException _) _ = return "caught error"+(.|$) :: (ChunkData tIn, ChunkData tOut, Monad m) =>+ Onum tOut m a -> Iter tOut m a -> Iter tIn m a+(.|$) enum iter = runI (enum .| iter)+infixr 2 .|$++-- | Concatenate the outputs of two enumerators. For example,+-- @'enumFile' \"file1\" \`cat\` 'enumFile' \"file2\"@ produces an+-- 'Onum' that outputs the concatenation of files \"file1\" and+-- \"file2\". Unless the first 'Inum' fails, @cat@ always invokes the+-- second 'Inum', as the second 'Inum' may have monadic side-effects+-- that must be executed even when the 'Iter' has already finished.+-- See 'lcat' if you want to stop when the 'Iter' no longer requires+-- input. If you want to continue executing even in the event of an+-- 'InumFail' condition, you can wrap the first 'Inum' with+-- 'inumCatch' and invoke 'resumeI' from within the exception handler.+--+-- @cat@ (and 'lcat', described below) are useful in right folds.+-- Say, for instance, that @files@ is a list of files you wish to+-- concatenate. You can use a construct such as:+--+-- @+-- catFiles :: ('MonadIO' m) => ['FilePath'] -> 'Onum' 'L.ByteString' m a+-- catFiles files = 'foldr' ('cat' . 'enumFile') 'inumNull' files+-- @+--+-- Note the use of 'inumNull' as the starting value for 'foldr'. This+-- is not to be confused with 'inumNop'. 'inumNull' acts as a no-op+-- for concatentation, producing no output analogously to+-- @\/dev\/null@. By contrast 'inumNop' is the no-op for fusing (see+-- '|.' and '.|' below) because it passes all data through untouched.+--+-- @cat@ has fixity:+--+-- > infixr 3 `cat`+cat :: (ChunkData tIn, ChunkData tOut, Monad m) =>+ Inum tIn tOut m a -- ^+ -> Inum tIn tOut m a+ -> Inum tIn tOut m a+cat a b iter = tryRI (runInum a iter) >>= either reRunIter (b . reRunIter)+-- Note this was carefully constructed to preserve the return value in+-- errors. Something like: cat a b iter = a iter >>= b . reRunIter+-- would turn a @('Fail' e ('Just' r) c)@ result from @a@ into+-- @('Fail' e 'Nothing' c)@; since the input and output types of >>=+-- do not have to be the same, >>= must convert error results to+-- 'Nothing'.+infixr 3 `cat`++-- | Lazy cat. Like 'cat', except that it does not run the second+-- 'Inum' if the 'Iter' is no longer active after completion of the+-- first 'Inum'. Also has fixity @infixr 3 \`lcat\`@.+lcat :: (ChunkData tIn, ChunkData tOut, Monad m) =>+ Inum tIn tOut m a -- ^+ -> Inum tIn tOut m a+ -> Inum tIn tOut m a+lcat a b iter = tryRI (runInum a iter) >>= either reRunIter check+ where check r = if isIterActive r then b $ reRunIter r else return r+infixr 3 `lcat`++-- | Transforms the result of an 'Inum' into the result of the 'Iter'+-- that it contains. Used by '|.' and '.|' to collapse their result+-- types.+--+-- Note that because the input type if the inner 'Iter', @tMid@, gets+-- squeezed out of the return type, @joinR@ will feed an EOF to the+-- inner 'Iter' if it is still active. This is what ensures that+-- active 'Iter's end up seeing an EOF, even though 'Inum's themselves+-- are never supposed to feed an EOF to the underlying 'Iter'. All+-- 'Iter's in right-hand arguments of '.|' and '|.' get fed an EOF by+-- @joinR@ (if they don't finish on their own), while the outermost+-- 'Inum' is fed an iter by the 'run' function (or by '|$' which+-- invokes 'run' internally).+joinR :: (ChunkData tIn, ChunkData tMid, Monad m) =>+ IterR tIn m (IterR tMid m a)+ -> IterR tIn m a+joinR (Done i c) = runIterR (runR i) c+joinR (Fail e Nothing c) = Fail e Nothing c+--+-- Note that 'runR' in the following function is serving two purposes,+-- one of them subtle. The obvious purpose is to preserve the state+-- of the non-failed target 'Iter' when an 'Inum' has failed.+-- However, a subtler, more important purpose is to guarantee that all+-- (non-failed) 'Iter's eventually receive EOF even when 'Inum's fail.+-- This is critical for things like EOF transmission and file+-- descriptor closing, and is how functions such as 'pairFinalizer'+-- can make sense.+joinR (Fail e (Just i) c) = flip onDoneR (runR i) $ \r ->+ case r of+ Done a _ -> Fail e (Just a) c+ Fail e' a _ -> Fail e' a c+ _ -> error "joinR"+joinR _ = error "joinR: not done"++-- | Left-associative pipe operator. Fuses two 'Inum's when the+-- output type of the first 'Inum' is the same as the input type of+-- the second. More specifically, if @inum1@ transcodes type @tIn@ to+-- @tOut@ and @inum2@ transcodes @tOut@ to @tOut2@, then @inum1+-- |. inum2@ produces a new 'Inum' that transcodes from @tIn@ to+-- @tOut2@.+--+-- Typically types @i@ and @iR@ are @'Iter' tOut2 m a@ and @'IterR'+-- tOut2 m a@, respectively, in which case the second argument and+-- result of @|.@ are also 'Inum's.+--+-- This function is equivalent to:+--+-- @+-- outer |. inner = \\iter -> outer '.|' inner iter+-- infixl 4 |.+-- @+--+-- But if you like point-free notation, think of it as @outer |. inner+-- = (outer '.|') . inner@, or better yet @(|.) = (.) . ('.|')@.+(|.) :: (ChunkData tIn, ChunkData tOut, Monad m) =>+ Inum tIn tOut m iR -- ^+ -> (i -> Iter tOut m iR)+ -> (i -> Iter tIn m iR)+(|.) outer inner = \iter -> onDone joinR $ outer $ inner iter+infixl 4 |.++-- | Right-associative pipe operator. Fuses an 'Inum' that transcodes+-- @tIn@ to @tOut@ with an 'Iter' taking input type @tOut@ to produce+-- an 'Iter' taking input type @tIn@. If the 'Iter' is still active+-- when the 'Inum' terminates (either normally or through an+-- exception), then @.|@ sends it an EOF.+--+-- Has fixity:+--+-- > infixr 4 .|+(.|) :: (ChunkData tIn, ChunkData tOut, Monad m) =>+ Inum tIn tOut m a -- ^+ -> Iter tOut m a+ -> Iter tIn m a+(.|) inum iter = onDone joinR $ inum iter+infixr 4 .|++--+-- Exception functions+--++-- | Catches errors thrown by an 'Inum', or a set of fused 'Inum's.+-- Note that only errors in 'Inum's that are lexically within the+-- scope of the argument to 'inumCatch' will be caught. For example:+--+-- > inumBad :: (ChunkData t, Monad m) => Inum t t m a+-- > inumBad = mkInum $ fail "inumBad"+-- > +-- > skipError :: (ChunkData tIn, MonadIO m) =>+-- > SomeException+-- > -> IterR tIn m (IterR tOut m a)+-- > -> Iter tIn m (IterR tOut m a)+-- > skipError e iter = do+-- > liftIO $ hPutStrLn stderr $ "skipping error: " ++ show e+-- > resumeI iter+-- >+-- > -- Throws an exception, because inumBad was fused outside the argument+-- > -- to inumCatch.+-- > test1 :: IO ()+-- > test1 = inumCatch (enumPure "test") skipError |. inumBad |$ nullI+-- > +-- > -- Does not throw an exception, because inumBad fused within the+-- > -- argument to inumCatch.+-- > test2 :: IO ()+-- > test2 = inumCatch (enumPure "test" |. inumBad) skipError |$ nullI+-- > +-- > -- Again no exception, because inumCatch is wrapped around inumBad.+-- > test3 :: IO ()+-- > test3 = enumPure "test" |. inumCatch inumBad skipError |$ nullI+--+-- Note that @\`inumCatch\`@ has the default infix precedence (@infixl+-- 9 \`inumcatch\`@), which binds more tightly than any concatenation+-- or fusing operators.+--+-- As noted for 'catchI', exception handlers receive both the+-- exception thrown and the failed 'IterR'. Particularly in the case+-- of @inumCatch@, it is important to re-throw exceptions by+-- re-executing the failed 'Iter' with 'reRunIter', not passing the+-- exception itself to 'throwI'. That way, if the exception is+-- re-caught, 'resumeI' will continue to work properly. For example,+-- to copy two files to standard output and ignore file not found+-- errors but re-throw any other kind of error, you could use the+-- following:+--+-- @+-- resumeTest :: IO ()+-- resumeTest = doFile \"file1\" ``cat`` doFile \"file2\" |$ 'stdoutI'+-- where+-- doFile path = inumCatch (`enumFile'` path) $ \\err r ->+-- if 'isDoesNotExistError' err+-- then 'verboseResumeI' r+-- else 'reRunIter' r+-- @+--+inumCatch :: (Exception e, ChunkData tIn, Monad m) =>+ Inum tIn tOut m a+ -- ^ 'Inum' that might throw an exception+ -> (e -> IterR tIn m (IterR tOut m a) -> Iter tIn m (IterR tOut m a))+ -- ^ Exception handler+ -> Inum tIn tOut m a+inumCatch enum handler iter = catchI (enum iter) check+ where check e r@(Fail _ (Just _) _) = handler e r+ check _ r = reRunIter r++-- | Execute some cleanup action when an 'Inum' finishes.+inumFinally :: (ChunkData tIn, Monad m) =>+ Inum tIn tOut m a -> Iter tIn m b -> Inum tIn tOut m a+inumFinally inum cleanup iter = inum iter `finallyI` cleanup++-- | Execute some cleanup action if an 'Inum' fails. Does not execute+-- the action if the 'Iter' (or some inner 'Inum') fails. Has the+-- same scoping rules as 'inumCatch'.+inumOnException :: (ChunkData tIn, Monad m) =>+ Inum tIn tOut m a -> Iter tIn m b -> Inum tIn tOut m a+inumOnException inum cleanup iter = inum iter `onExceptionI` cleanup++-- | Used in an exception handler, after an 'Inum' failure, to resume+-- processing of the 'Iter' by the next enumerator in a 'cat'ed+-- series. See 'inumCatch' for an example.+resumeI :: (ChunkData tIn, Monad m) =>+ IterR tIn m (IterR tOut m a) -> Iter tIn m (IterR tOut m a)+resumeI (Fail _ (Just a) _) = return a+resumeI _ = error "resumeI: not an Inum failure"++-- | Like 'resumeI', but if the 'Iter' is resumable, also prints an+-- error message to standard error before resuming.+verboseResumeI :: (ChunkData tIn, MonadIO m) =>+ IterR tIn m (IterR tOut m a) -> Iter tIn m (IterR tOut m a)+verboseResumeI (Fail e (Just a) _) = do+ liftIO $ do prog <- liftIO getProgName+ hPutStrLn stderr $ prog ++ ": " ++ show e+ return a+verboseResumeI _ = error "verboseResumeI: not an Inum failure"++--+-- Control handlers+--++-- | A @ResidHandler@ specifies how to handle residual data in an+-- 'Inum'. Typically, when an 'Inum' finishes executing, there are+-- two kinds of residual data. First, the 'Inum' itself (in its role+-- as an iteratee) may have left some unconsumed data. Second, the+-- target 'Iter' being fed by the 'Inum' may have some resitual data,+-- and this data may be of a different type. A @ResidHandler@ allows+-- this residual data to be adjusted by untranslating the residual+-- data of the target 'Iter' and sticking the result back into the+-- `Inum`'s residual data.+--+-- The two most common @ResidHandler@s are 'pullupResid' (to pull the+-- target `Iter`'s residual data back up to the 'Inum' as is), and+-- 'id' (to do no adjustment of residual data).+--+-- @ResidHandler@s are used by the 'mkInumC' function, and by the+-- 'passCtl' 'CtlHandler'.+type ResidHandler tIn tOut = (tIn, tOut) -> (tIn, tOut)++withResidHandler :: ResidHandler tIn tOut+ -> Chunk tOut+ -> (Chunk tOut -> Iter tIn mIn a)+ -> Iter tIn mIn a+withResidHandler adjust (Chunk tOut0 eofOut) cont =+ Iter $ \(Chunk tIn0 eofIn) ->+ case adjust (tIn0, tOut0) of+ (tIn, tOut) -> runIter (cont $ Chunk tOut eofOut) $ Chunk tIn eofIn++-- | A control handler maps control requests to 'IterR' results.+-- Generally the type parameter @m1@ is @'Iter' t' m@.+type CtlHandler m1 t m a = CtlArg t m a -> m1 (IterR t m a)++-- | Reject all control requests.+noCtl :: (Monad m1) => CtlHandler m1 t m a+noCtl (CtlArg _ n c) = return $ runIter (n CtlUnsupp) c++-- | Pass all control requests through to the enclosing 'Iter' monad.+-- The 'ResidHandler' argument says how to adjust residual data, in+-- case some enclosing 'CtlHandler' decides to flush pending input+-- data, it is advisable to un-translate any data in the output type+-- @tOut@ back to the input type @tIn@.+passCtl :: (Monad mIn) =>+ ResidHandler tIn tOut+ -> CtlHandler (Iter tIn mIn) tOut m a+passCtl adj (CtlArg a n c0) = withResidHandler adj c0 runn+ where runn c = do mcr <- safeCtlI a+ return $ runIter (n mcr) c++-- | Create a 'CtlHandler' given a function of a particular control+-- argument type and a fallback 'CtlHandler' to run if the argument+-- type does not match. @consCtl@ is used to chain handlers, with the+-- rightmost handler being either 'noCtl' or 'passCtl'.+--+-- For example, to create a control handler that implements seek on+-- @'SeekC'@ requests, returns the size of the file on @'SizeC'@+-- requests, and passes everything else out to the enclosing+-- enumerator (if any), you could use the following:+--+-- @+-- fileCtl :: (ChunkData t, MonadIO m) => Handle -> CtlHandler (Iter () m) t m a+-- fileCtl h = ('mkFlushCtl' $ \(SeekC mode pos) -> liftIO (hSeek h mode pos))+-- \`consCtl\` ('mkCtl' $ \SizeC -> liftIO (hFileSize h))+-- \`consCtl\` 'passCtl' 'id'+-- @+--+-- Has fixity:+--+-- > infixr 9 `consCtl`+consCtl :: (CtlCmd carg cres, ChunkData tIn, Monad mIn) =>+ (carg -> (cres -> Iter t m a) -> Chunk t+ -> Iter tIn mIn (IterR t m a))+ -> CtlHandler (Iter tIn mIn) t m a+ -> CtlHandler (Iter tIn mIn) t m a+consCtl fn fallback ca@(CtlArg a0 n c) = maybe (fallback ca) runfn $ cast a0+ where runfn a = fn a (n . fromJust . cast) c+ `catchI` \e _ -> return $ runIter (n $ CtlFail e) c+infixr 9 `consCtl`++-- | Make a control function suitable for use as the first argument to+-- 'consCtl'.+mkCtl :: (CtlCmd carg cres, Monad m1) =>+ (carg -> Iter t1 m1 cres)+ -> carg -> (cres -> Iter t m a) -> Chunk t -> Iter t1 m1 (IterR t m a)+mkCtl f a n c = do cres <- f a; return $ runIter (n cres) c++-- | Like 'mkCtl', except that it flushes all input and clears the EOF+-- flag in both 'Iter' monads after executing the control function.+mkFlushCtl :: (CtlCmd carg cres, Monad mIn, ChunkData tIn, ChunkData t) =>+ (carg -> Iter tIn mIn cres)+ -> carg -> (cres -> Iter t m a) -> Chunk t+ -> Iter tIn mIn (IterR t m a)+mkFlushCtl f a n _ = do cres <- onDone (flip setResid mempty) $ f a+ return $ runIter (n cres) mempty+ +--+-- Basic tools+--++-- $mkInumIntro+--+-- The 'mkInum' function allows you to create stateless 'Inum's out of+-- simple transcoding 'Iter's. As an example, suppose you are+-- processing a list of @L.ByteString@s representing packets, and want+-- to concatenate them all into one continuous stream of bytes. You+-- could implement an 'Inum' called @inumConcat@ to do this as+-- follows:+--+-- #mkInumExample#+--+-- @+--iterConcat :: (Monad m) => 'Iter' [L.ByteString] m L.ByteString+--iterConcat = L.concat ``liftM`` 'dataI'+--+--inumConcat :: (Monad m) => 'Inum' [L.ByteString] L.ByteString m a+--inumConcat = 'mkInum' iterConcat+-- @+--++-- | Like 'runIterMC', but only for 'IterM'--may return 'IterC'.+runIterM :: (Monad m, MonadTrans mt, Monad (mt m)) =>+ Iter t m a -> Chunk t -> mt m (IterR t m a)+runIterM iter c = check $ runIter iter c+ where check (IterM m) = lift m >>= check+ check r = return r++runIterRMC :: (Monad m) =>+ CtlHandler (Iter tIn m) tOut m a+ -> IterR tOut m a -> Iter tIn m (IterR tOut m a)+runIterRMC ch = check+ where check (IterM m) = lift m >>= check+ check (IterC ca) = ch ca >>= check+ check r = return r++-- | Run an 'Iter' just like 'runIter', but then keep stepping the+-- result for as long as it is in the 'IterM' or 'IterC' state (using+-- the supplied 'CtlHandler' for 'IterC' states). 'Inum's should+-- generally use this function or 'runIterM' in preference to+-- 'runIter', as it is convenient if 'Inum's avoid ever returning+-- 'IterR's in the 'IterM' state.+runIterMC :: (Monad m) =>+ CtlHandler (Iter tIn m) tOut m a+ -> Iter tOut m a -> Chunk tOut -> Iter tIn m (IterR tOut m a)+runIterMC ch iter c = runIterRMC ch $ runIter iter c++-- | Takes an 'Inum' that might return 'IterR's in the 'IterM' state+-- (which is considered impolite--see 'runIterMC') and transforms it+-- into an 'Inum' that never returns 'IterR's in the 'IterM' state.+runInum :: (ChunkData tIn, Monad m) =>+ Inum tIn tOut m a -> Inum tIn tOut m a+runInum inum = onDone check . inum+ where+ check (Done (IterM m) c) = IterM $ m >>= \r -> return $ check $ Done r c+ check r = r++-- | Create a stateless 'Inum' from a \"codec\" 'Iter' that transcodes+-- the input type to the output type. The codec is invoked repeately+-- until one of the following occurs: The codec returns 'null' data,+-- the codec throws an exception, or the underlying target 'Iter' is+-- no longer active. If the codec throws an exception of type+-- 'IterEOF', this is considered normal termination and the error is+-- not further propagated.+--+-- @mkInumC@ requires two other arguments before the codec. First, a+-- 'ResidHandler' allows residual data to be adjusted between the+-- input and output 'Iter' monads. Second, a 'CtlHandler' specifies a+-- handler for control requests. For example, to pass up control+-- requests and ensure no residual data is lost when the 'Inum' is+-- fused to an 'Iter', the @inumConcat@ function given previously for+-- 'mkInum' at <#mkInumExample> could be re-written:+--+-- > inumConcat :: (Monad m) => Inum [L.ByteString] L.ByteString m a+-- > inumConcat = mkInumC reList (passCtl reList) iterConcat+-- > where reList (a, b) = (b:a, mempty)+mkInumC :: (ChunkData tIn, ChunkData tOut, Monad m) =>+ ResidHandler tIn tOut+ -- ^ Adjust residual data (use 'id' for no adjustment)+ -> CtlHandler (Iter tIn m) tOut m a+ -- ^ Handle control requests (use 'noCtl' or 'passCtl' if+ -- 'Inum' shouldn't implement any specific control functions).+ -> Iter tIn m tOut+ -- ^ Generate transcoded data chunks+ -> Inum tIn tOut m a+mkInumC adj ch codec iter0 = doIter iter0+ where+ doIter iter = tryEOFI codec >>= maybe (return $ IterF iter) (doInput iter)+ doInput iter input = do+ r <- runIterMC ch iter (Chunk input False)+ case r of+ (IterF i) | not (null input) -> doIter i+ _ | isIterActive r -> return r+ _ -> withResidHandler adj (getResid r) $ return . setResid r++-- | Create an 'Inum' based on an 'Iter' that transcodes the input to+-- the output type. This is a simplified version of 'mkInumC' that+-- rejects all control requests and does not adjust residual data.+--+-- > mkInum = mkInumC id noCtl+mkInum :: (ChunkData tIn, ChunkData tOut, Monad m) =>+ Iter tIn m tOut -> Inum tIn tOut m a+mkInum = mkInumC id noCtl++-- | A simplified version of 'mkInum' that passes all control requests+-- to enclosing enumerators. It requires a 'ResidHandler' to describe+-- how to adjust residual data. (E.g., use 'pullupResid' when @tIn@+-- and @tOut@ are the same type.)+--+-- > mkInumP adj = mkInumC adj (passCtl adj)+mkInumP :: (ChunkData tIn, ChunkData tOut, Monad m) =>+ ResidHandler tIn tOut -> Iter tIn m tOut -> Inum tIn tOut m a+mkInumP adj = mkInumC adj (passCtl adj)++-- | @pullupResid (a, b) = (mappend a b, mempty)@. See 'ResidHandler'.+pullupResid :: (ChunkData t) => (t, t) -> (t, t)+pullupResid (a, b) = (mappend a b, mempty)++-- | Bracket an 'Inum' with a start and end function, which can be+-- used to acquire and release a resource, must like the IO monad's+-- @'bracket'@ function. For example:+--+-- > enumFile :: (MonadIO m, ChunkData t, LL.ListLikeIO t e) =>+-- > FilePath -> Onum t m a+-- > enumFile path = inumBracket (liftIO $ openBinaryFile path ReadMode)+-- > (liftIO . hClose)+-- > enumHandle+inumBracket :: (ChunkData tIn, Monad m) =>+ Iter tIn m b+ -- ^ Computation to run first+ -> (b -> Iter tIn m c)+ -- ^ Computation to run last+ -> (b -> Inum tIn tOut m a)+ -- ^ Inum to bracket+ -> Inum tIn tOut m a+inumBracket start end inum iter = tryFI start >>= check+ where check (Left e) = Iter $ Fail e (Just $ IterF iter) . Just+ check (Right b) = inum b iter `finallyI` end b++--+-- Basic Inums+--++-- | @inumNop@ passes all data through to the underlying 'Iter'. It+-- acts as a no-op when fused to other 'Inum's with '|.' or when fused+-- to 'Iter's with '.|'.+--+-- @inumNop@ is particularly useful for conditionally fusing 'Inum's+-- together. Even though most 'Inum's are polymorphic in the return+-- type, this library does not use the Rank2Types extension, which+-- means any given 'Inum' must have a specific return type. Here is+-- an example of incorrect code:+--+-- @+-- let enum = if debug then base_enum '|.' 'inumStderr' else base_enum -- Error+-- @+--+-- This doesn't work because @base_enum@ cannot have the same type as+-- @(base_enum |. inumStderr)@. Instead, you can use the following:+--+-- @+-- let enum = base_enum '|.' if debug then 'inumStderr' else inumNop+-- @+inumNop :: (ChunkData t, Monad m) => Inum t t m a+inumNop = mkInumP pullupResid dataI++-- | @inumNull@ feeds empty data to the underlying 'Iter'. It pretty+-- much acts as a no-op when concatenated to other 'Inum's with 'cat'+-- or 'lcat'.+--+-- There may be cases where @inumNull@ is required to avoid deadlock.+-- In an expression such as @enum '|$' iter@, if @enum@ immediately+-- blocks waiting for some event, and @iter@ immediately starts out+-- triggering that event before reading any input, then to break the+-- deadlock you can re-write the code as @cat inumNull enum '|$'+-- iter@.+inumNull :: (ChunkData tOut, Monad m) => Inum tIn tOut m a+inumNull = inumPure mempty++-- | Feed pure data to an 'Iter'.+inumPure :: (Monad m) => tOut -> Inum tIn tOut m a+inumPure t iter = runIterM iter $ chunk t++-- | Type-restricted version of 'inumPure'.+enumPure :: (Monad m) => tOut -> Onum tOut m a+enumPure = inumPure++-- | Repeat an 'Inum' until the input receives an EOF condition, the+-- 'Iter' no longer requires input, or the 'Iter' is in an unhandled+-- 'IterC' state (which presumably will continue to be unhandled by+-- the same 'Inum', so no point in executing it again).+inumRepeat :: (ChunkData tIn, Monad m) =>+ Inum tIn tOut m a -> Inum tIn tOut m a+inumRepeat inum iter0 = do+ er <- tryRI $ runInum inum iter0+ stop <- atEOFI+ case (stop, er) of+ (False, Right (IterF iter)) -> inumRepeat inum iter+ (_, Right r) -> return r+ (_, Left r) -> reRunIter r++--+-- Complex Inum creation+--++{- $mkInumMIntro++Complex 'Inum's that need state and non-trivial control flow can be+constructed using the 'mkInumM' function to produce an 'Inum' out of a+computation in the 'InumM' monad. The 'InumM' monad implicitly keeps+track of the state of the 'Iter' to which the 'Inum' is feeding data,+which we call the \"target\" 'Iter'.++'InumM' is an 'Iter' monad, and so can consume input by invoking+ordinary 'Iter' actions. However, to keep track of the state of the+target 'Iter', 'InumM' wraps its inner monadic type with an+'IterStateT' transformer. Specifically, when creating an enumerator+of type @'Inum' tIn tOut m a@, the 'InumM' action is of a type like+@'Iter' tIn ('IterStateT' (InumState ...) m) ()@. That means that to+execute actions of type @'Iter' tIn m a@ that are not polymorphic in+@m@, you have to transform them with the 'liftI' function.++Output can be fed to the target 'Iter' by means of the 'ifeed'+function. As an example, here is another version of the @inumConcat@+function given previously for 'mkInum' at <#mkInumExample>:++@+inumConcat :: (Monad m) => 'Inum' [L.ByteString] L.ByteString m a+inumConcat = 'mkInumM' loop+ where loop = do+ 'Chunk' t eof <- 'chunkI'+ done <- 'ifeed' $ L.concat t+ if not (eof || done)+ then loop+ else do resid <- 'ipopresid'+ 'ungetI' [resid]+@++There are several points to note about this function. It reads data+in 'Chunk's using 'chunkI', rather than just inputting data with+'dataI'. The choice of 'chunkI' rather than 'dataI' allows+@inumConcat@ to see the @eof@ flag and know when there is no more+input. 'chunkI' also avoids throwing an 'IterEOF' exception on end of+file, as 'dataI' would. In contrast to 'mkInum', which gracefully+interprets 'IterEOF' exceptions as an exit request, 'mkInumM' by+default treats such exceptions as an 'Inum' failure.++As previously mentioned, data is fed to the target 'Iter', which here+is of type @'Iter' L.ByteString m a@, using 'ifeed'. 'ifeed' returns+a 'Bool' that is @'True'@ when the 'Iter' is no longer active. This+brings us to another point--there is no implicit looping or+repetition. We explicitly loop via a tail-recursive call to @loop@ so+long as the @eof@ flag is clear and 'ifeed' returned @'False'@+indicating the target 'Iter' has not finished.++What happens when @eof@ or @done@ is set? One possibility is to do+nothing. This is often correct. Falling off the end of the 'InumM'+do-block causes the 'Inum' to return the current state of the 'Iter'.+However, it may be that the 'Inum' has been fused to the target+'Iter', in which case any left-over residual data fed to, but not+consumed by, the target 'Iter' will be discarded. We may instead want+to put the data back onto the input stream. The 'ipopresid' function+extracts any left-over data from the target 'Iter', while 'ungetI'+places data back in the input stream. Since here the input stream is+a list of @L.ByteString@s, we have to place @resid@ in a list. (After+doing this, the list element boundaries may be different, but all the+input bytes will be there.) Note that the version of @inumConcat@+implemented with 'mkInum' at <#mkInumExample> does not have this+input-restoring feature.++The code above looks much clumsier than the version based on 'mkInum',+but several of these steps can be made implicit. There is an+/AutoEOF/ flag, controlable with the 'setAutoEOF' function, that+causes 'IterEOF' exceptions to produce normal termination of the+'Inum', rather than failure (just as 'mkInum' handles such+exceptions). Another flag, /AutoDone/, is controlable with the+'setAutoDone' function and causes the 'Inum' to exit immediately when+the underlying 'Iter' is no longer active (i.e., the 'ifeed' function+returns @'True'@). Both of these flags are set at once by the+'mkInumAutoM' function, which yields the following simpler+implementation of @inumConcat@:++@+inumConcat = 'mkInumAutoM' $ do 'addCleanup' $ 'ipopresid' >>= 'ungetI' . (: [])+ loop+ where loop = do+ t <- 'dataI' -- AutoEOF flag will handle IterEOF err+ 'ifeed' $ L.concat t -- AutoDone flag will catch True result+ loop+@++The 'addCleanup' function registers actions that should always be+executed when the 'Inum' finishes. Here we use it to place residual+data from the target 'Iter' back into the `Inum`'s input stream.++Finally, there is a function 'irepeat' that automatically sets the+/AutoEOF/ and /AutoDone/ flags and then loops forever on an 'InumM'+computation. Using 'irepeat' to simplify further, we have:++@+'inumConcat' = 'mkInumM' $ 'withCleanup' ('ipopresid' >>= 'ungetI' . (: [])) $+ 'irepeat' $ 'dataI' >>= 'ifeed' . L.concat+@++'withCleanup', demonstrated here, is a variant of 'addCleanup' that+cleans up after a particular action, rather than at the end of the+`Inum`'s whole execution. (At the outermost level, as used here,+`withCleanup`'s effects are identical to `addCleanup`'s.)++In addition to 'ifeed', the 'ipipe' function invokes a different+'Inum' from within the 'InumM' monad, piping its output directly to+the target 'Iter'. As an example, consider an 'Inum' that processes a+mail message and appends a signature line, implemented as follows:++@+inumAddSig :: (Monad m) => 'Inum' L.ByteString L.ByteString m a+inumAddSig = 'mkInumM' $ do+ 'ipipe' 'inumNop'+ 'ifeed' $ L8.pack \"\\n--\\nSent from my Haskell interpreter.\\n\"+@++Here we start by using 'inumNop' to \"pipe\" all input to the target+'Iter' unmodified. On reading an end of file, 'inumNop' returns, at+which point we use 'ifeed' to append our signature.++A similar function 'irun' runs an 'Onum' (or 'Inum' of a different+type) on the target 'Iter'. For instance, to read the signature from+a file called @\".signature\"@, one could use:++@+inumAddSig :: ('MonadIO' m) => 'Inum' L.ByteString L.ByteString m a+inumAddSig = 'mkInumM' $ do+ 'ipipe' 'inumNop'+ 'irun' $ 'enumFile' \".signature\"+@++Of course, these examples are a bit contrived. An even simpler+implementation is:++@+inumAddSig = 'inumNop' ``cat`` 'runI' . 'enumFile' \".signature\"+@++The @.@ between 'runI' and @'enumFile'@ is because 'Inum's are+functions from 'Iter's to 'IterR's; we want to apply 'runI' to the+result of applying @'enumFile' \".signature\"@ to an 'Iter'. Spelled+out, the type of @'enumFile'@ is:++@+enumFile :: (MonadIO m, ChunkData t, ListLikeIO t e) =>+ FilePath+ -> 'Iter' t m a+ -> 'Iter' () m a ('IterR' t m a)+@++-}++-- | Internal data structure for the 'InumM' monad's state.+data InumState tIn tOut m a = InumState {+ insAutoEOF :: !Bool+ , insAutoDone :: !Bool+ , insCtl :: !(CtlHandler (Iter tIn m) tOut m a)+ , insIter :: !(IterR tOut m a)+ , insCleanup :: !(InumM tIn tOut m a ())+ , insCleaning :: !Bool+ }++defaultInumState :: (ChunkData tIn, Monad m) => InumState tIn tOut m a+defaultInumState = InumState {+ insAutoEOF = False+ , insAutoDone = False+ , insCtl = noCtl+ , insIter = IterF $ Iter $ const $ error "insIter"+ , insCleanup = return ()+ , insCleaning = False+ }++-- | A monad in which to define the actions of an @'Inum' tIn tOut m+-- a@. Note @InumM tIn tOut m a@ is a 'Monad' of kind @* -> *@, where+-- @a@ is the (almost always parametric) return type of the 'Inum'. A+-- fifth type argument is required for monadic computations of kind+-- @*@, e.g.:+--+-- > seven :: InumM tIn tOut m a Int+-- > seven = return 7+--+-- Another important thing to note about the 'InumM' monad, as+-- described in the documentation for 'mkInumM', is that you must call+-- @'lift'@ twice to execute actions in monad @m@, and you must use+-- the 'liftI' function to execute actions in monad @'Iter' t m a@.+type InumM tIn tOut m a = Iter tIn (IterStateT (InumState tIn tOut m a) m)++-- | Set the control handler an 'Inum' should use from within an+-- 'InumM' computation. (The default is 'noCtl'.)+setCtlHandler :: (ChunkData tIn, Monad m) =>+ CtlHandler (Iter tIn m) tOut m a+ -> InumM tIn tOut m a ()+setCtlHandler ch = imodify $ \s -> s { insCtl = ch }++-- | Set the /AutoEOF/ flag within an 'InumM' computation. If this+-- flag is 'True', handle 'IterEOF' exceptions like a normal but+-- immediate termination of the 'Inum'. If this flag is @'False'@+-- (the default), then 'IterEOF' exceptions must be manually caught or+-- they will terminate the thread.+setAutoEOF :: (ChunkData tIn, Monad m) => Bool -> InumM tIn tOut m a ()+setAutoEOF val = imodify $ \s -> s { insAutoEOF = val }++-- | Set the /AutoDone/ flag within an 'InumM' computation. When+-- @'True'@, the 'Inum' will immediately terminate as soon as the+-- 'Iter' it is feeding enters a non-active state (i.e., 'Done' or a+-- failure state). If this flag is @'False'@ (the default), the+-- 'InumM' computation will need to monitor the results of the+-- 'ifeed', 'ipipe', and 'irun' functions to ensure the 'Inum'+-- terminates when one of these functions returns @'False'@.+setAutoDone :: (ChunkData tIn, Monad m) => Bool -> InumM tIn tOut m a ()+setAutoDone val = imodify $ \s -> s { insAutoDone = val }++-- | Like imodify, but throws an error if the insCleaning flag is set.+ncmodify :: (ChunkData tIn, Monad m) =>+ (InumState tIn tOut m a -> InumState tIn tOut m a)+ -> InumM tIn tOut m a ()+ncmodify fn = imodify $ \s -> if insCleaning s+ then error "illegal call within Cleanup function"+ else fn s++-- | Add a cleanup action to be executed when the 'Inum' finishes, or,+-- if used in conjunction with the 'withCleanup' function, when the+-- innermost enclosing 'withCleanup' action finishes.+addCleanup :: (ChunkData tIn, Monad m) =>+ InumM tIn tOut m a () -> InumM tIn tOut m a ()+addCleanup clean = ncmodify $ \s -> s { insCleanup = clean >> insCleanup s }++-- | Run an 'InumM' with some cleanup action in effect. The cleanup+-- action specified will be executed when the main action returns,+-- whether normally, through an exception, because of the /AutoDone/+-- or /AutoEOF/ flags, or because 'idone' is invoked.+--+-- Note @withCleanup@ also defines the scope of actions added by the+-- 'addCleanup' function. In other words, given a call such as+-- @withCleanup cleaner1 main@, if @main@ invokes @'addCleanup'+-- cleaner2@, then both @cleaner1@ and @cleaner2@ will be executed+-- upon @main@'s return, even if the overall 'Inum' has not finished+-- yet.+withCleanup :: (ChunkData tIn, Monad m) =>+ InumM tIn tOut m a () -- ^ Cleanup action+ -> InumM tIn tOut m a b -- ^ Main action to execute+ -> InumM tIn tOut m a b+withCleanup clean action = do+ old <- igets insCleanup+ ncmodify $ \s -> s { insCleanup = clean }+ action `finallyI` do+ newclean <- igets insCleanup+ imodify $ \s -> s { insCleanup = old }+ newclean++-- | Convert an 'InumM' computation into an 'Inum', given some+-- @'InumState'@ to run on.+runInumM :: (ChunkData tIn, ChunkData tOut, Monad m) =>+ InumM tIn tOut m a b+ -- ^ Monadic computation defining the 'Inum'.+ -> InumState tIn tOut m a+ -- ^ State to run on+ -> Iter tIn m (IterR tOut m a)+runInumM inumm s0 = do+ (err1, s1) <- getErr =<< runIterStateT inumm s0+ (err2, s2) <- getErr =<< runIterStateT (insCleanup s1)+ s1 { insAutoDone = False, insCleaning = True }+ let r = insIter s2+ Iter $ maybe (Done r) (\e -> Fail e (Just r) . Just) $ mplus err2 err1+ where+ getErr (Fail (IterEOFErr _) _ _, s) | insAutoEOF s = return (Nothing, s)+ getErr (Fail e _ _, s) = return (Just e, s)+ getErr (_, s) = return (Nothing, s)++-- | A variant of 'mkInumM' that sets /AutoEOF/ and /AutoDone/ to+-- 'True' by default. (Equivalent to calling @'setAutoEOF' 'True' >>+-- 'setAutoDone' 'True'@ as the first thing inside 'mkInumM'.)+mkInumAutoM :: (ChunkData tIn, ChunkData tOut, Monad m) =>+ InumM tIn tOut m a b -> Inum tIn tOut m a+mkInumAutoM inumm iter0 =+ runInumM inumm defaultInumState { insIter = IterF iter0+ , insAutoEOF = True+ , insAutoDone = True+ }+++-- | Build an 'Inum' out of an 'InumM' computation. If you run+-- 'mkInumM' inside the @'Iter' tIn m@ monad (i.e., to create an+-- enumerator of type @'Inum' tIn tOut m a@), then the 'InumM'+-- computation will be in a Monad of type @'Iter' t tm@ where @tm@ is+-- a transformed version of @m@. This has the following two+-- consequences:+--+-- - If you wish to execute actions in monad @m@ from within your+-- 'InumM' computation, you will have to apply @'lift'@ twice (as+-- in @'lift' $ 'lift' action_in_m@) rather than just once.+--+-- - If you need to execute actions in the @'Iter' t m@ monad, you+-- will have to lift them with the 'liftI' function.+--+-- The 'InumM' computation you construct can feed output of type+-- @tOut@ to the target 'Iter' (which is implicitly contained in the+-- monad state), using the 'ifeed', 'ipipe', and 'irun' functions.+mkInumM :: (ChunkData tIn, ChunkData tOut, Monad m) =>+ InumM tIn tOut m a b -> Inum tIn tOut m a+mkInumM inumm iter0 =+ runInumM inumm defaultInumState { insIter = IterF iter0 }++-- | Used from within the 'InumM' monad to feed data to the target+-- 'Iter'. Returns @'False'@ if the target 'Iter' is still active and+-- @'True'@ if the iter has finished and the 'Inum' should also+-- return. (If the @autoDone@ flag is @'True'@, then @ifeed@,+-- @ipipe@, and @irun@ will never actually return @'True'@, but+-- instead just immediately run cleanup functions and exit the+-- 'Inum' when the target 'Iter' stops being active.)+ifeed :: (ChunkData tIn, ChunkData tOut, Monad m) =>+ tOut -> InumM tIn tOut m a Bool+ifeed = ipipe . inumPure++-- | A variant of 'ifeed' that throws an exception of type 'IterEOF'+-- if the data being fed is 'null'. Convenient when reading input+-- with a function (such as "Data.ListLike"'s @hget@) that returns 0+-- bytes instead of throwing an EOF exception to indicate end of file.+-- For instance, the main loop of @'enumFile'@ could be implemented+-- as:+--+-- @+-- 'irepeat' $ 'liftIO' ('LL.hGet' h 'defaultChunkSize') >>= 'ifeed1'+-- @+ifeed1 :: (ChunkData tIn, ChunkData tOut, Monad m) =>+ tOut -> InumM tIn tOut m a Bool+ifeed1 dat = if null dat then throwEOFI "ifeed1" else ifeed dat++-- | Apply another 'Inum' to the target 'Iter' from within the 'InumM'+-- monad. As with 'ifeed', returns @'True'@ when the 'Iter' is+-- finished.+--+-- Note that the applied 'Inum' must handle all control requests. (In+-- other words, ones it passes on are not caught by whatever handler+-- is installed by 'setCtlHandler', but if the 'Inum' returns the+-- 'IterR' in the 'IterC' state, as 'inumPure' does, then requests+-- will be handled.)+ipipe :: (ChunkData tIn, ChunkData tOut, Monad m) =>+ Inum tIn tOut m a -> InumM tIn tOut m a Bool+ipipe inum = do+ s <- iget+ r <- tryRI (liftI (inum $ reRunIter $ insIter s)) >>= getIter+ >>= liftI . runIterRMC (insCtl s)+ iput s { insIter = r }+ let done = not $ isIterActive r+ if done && insAutoDone s then idone else return done+ where+ getIter (Right i) = return i+ getIter (Left r@(Fail _ (Just i) _)) = do+ imodify $ \s -> s { insIter = i }+ reRunIter r+ getIter (Left r) = reRunIter r++-- | Apply an 'Onum' (or 'Inum' of an arbitrary, unused input type) to+-- the 'Iter' from within the 'InumM' monad. As with 'ifeed', returns+-- @'True'@ when the 'Iter' is finished.+irun :: (ChunkData tAny, ChunkData tIn, ChunkData tOut, Monad m) =>+ Inum tAny tOut m a -> InumM tIn tOut m a Bool+irun onum = ipipe $ runI . onum++-- | Repeats an action until the 'Iter' is done or an EOF error is+-- thrown. (Also stops if a different kind of exception is thrown, in+-- which case the exception propagates further and may cause the+-- 'Inum' to fail.) @irepeat@ sets both the /AutoEOF/ and+-- /AutoDone/ flags to @'True'@.+irepeat :: (ChunkData tIn, Monad m) =>+ InumM tIn tOut m a b -> InumM tIn tOut m a ()+irepeat action = do+ imodify $ \s -> s { insAutoEOF = True, insAutoDone = True }+ let loop = action >> loop in loop++-- | If the target 'Iter' being fed by the 'Inum' is no longer active+-- (i.e., if it is in the 'Done' state or in an error state), this+-- funciton pops the residual data out of the 'Iter' and returns it.+-- If the target is in any other state, returns 'mempty'.+ipopresid :: (ChunkData tIn, ChunkData tOut, Monad m) =>+ InumM tIn tOut m a tOut+ipopresid = do+ s <- iget+ case insIter s of+ r | isIterActive r -> return mempty+ | otherwise -> do let (Chunk t _) = getResid r+ iput s { insIter = setResid r mempty }+ return t++-- | Immediately perform a successful exit from an 'InumM' monad,+-- terminating the 'Inum' and returning the current state of the+-- target 'Iter'. Can be used to end an 'irepeat' loop. (Use+-- @'throwI' ...@ for an unsuccessful exit.)+idone :: (ChunkData tIn, Monad m) => InumM tIn tOut m a b+idone = setAutoEOF True >> throwEOFI "idone"
+ Data/IterIO/Iter.hs view
@@ -0,0 +1,1020 @@+{-# LANGUAGE DeriveDataTypeable #-}+{-# LANGUAGE ExistentialQuantification #-}+{-# LANGUAGE MultiParamTypeClasses #-}+{-# LANGUAGE FunctionalDependencies #-}++module Data.IterIO.Iter+ (-- * Base types+ ChunkData(..), Chunk(..), chunk, chunkEOF+ , Iter(..), CtlCmd, CtlRes(..), CtlArg(..), IterFail(..)+ , IterR(..), iterF+ , isIterActive, iterShows, iterShow+ -- * Execution+ , run, runI+ -- * Exception types+ , mkIterEOF+ , IterCUnsupp(..)+ -- * Exception-related functions+ , throwI, throwEOFI, throwParseI+ , catchI, tryI, tryFI, tryRI, tryEOFI+ , finallyI, onExceptionI+ , tryBI, tryFBI+ , ifParse, ifNoParse, multiParse+ -- * Some basic Iters+ , nullI, data0I, dataI, pureI, chunkI+ , whileNullI, peekI, atEOFI, ungetI+ , safeCtlI, ctlI+ -- * Internal functions+ , onDone, fmapI+ , onDoneR, stepR, stepR', runR, fmapR, reRunIter, runIterR+ , getResid, setResid+ ) where++import Prelude hiding (null)+import qualified Prelude+import Control.Applicative (Applicative(..), (<$), (<$>))+import Control.Exception (SomeException(..), ErrorCall(..), Exception(..)+ , try, throw)+import Control.Monad+import Control.Monad.Fix+import Control.Monad.Trans+import Data.IORef+import Data.Maybe+import Data.Monoid+import Data.Typeable+import qualified Data.ByteString as S+import qualified Data.ByteString.Char8 as S8+import qualified Data.ByteString.Lazy as L+import qualified Data.ByteString.Lazy.Char8 as L8+import System.IO.Error (mkIOError, eofErrorType, isEOFError)+import System.IO.Unsafe++--+-- Iteratee types and instances+--++-- | @ChunkData@ is the class of data types that can be output by an+-- enumerator and iterated on with an iteratee. A @ChunkData@ type+-- must be a 'Monoid', but must additionally provide a predicate,+-- @null@, for testing whether an object is equal to 'mempty'.+-- Feeding a @null@ chunk to an iteratee followed by any other chunk+-- should have the same effect as just feeding the second chunk. To+-- simplify debugging, there is an additional requirement that+-- @ChunkData@ be convertable to a String with the @chunkShow@ method.+--+-- Note that because the "Prelude" contains a function 'Prelude.null'+-- for lists, you may wish to include the import:+--+-- > import Prelude hiding (null)+--+class (Monoid t) => ChunkData t where+ null :: t -> Bool+ chunkShow :: t -> String+instance (Show a) => ChunkData [a] where+ {-# INLINE null #-}+ null = Prelude.null+ chunkShow = show+instance ChunkData L.ByteString where+ {-# INLINE null #-}+ null = L.null+ chunkShow = show . L8.unpack+instance ChunkData S.ByteString where+ {-# INLINE null #-}+ null = S.null+ chunkShow = show . S8.unpack+instance ChunkData () where+ {-# INLINE null #-}+ null _ = True+ chunkShow _ = "()"++-- | @Chunk@ is a wrapper around a 'ChunkData' type that also includes+-- an EOF flag that is 'True' if the data is followed by an+-- end-of-file condition. An 'Iter' that receives a @Chunk@ with EOF+-- 'True' must return a result (or failure); it is an error to demand+-- more data (return 'IterF') after an EOF.+data Chunk t = Chunk !t !Bool deriving (Eq, Typeable)++instance (ChunkData t) => Show (Chunk t) where+ showsPrec _ (Chunk t eof) rest =+ chunkShow t ++ if eof then "+EOF" ++ rest else rest++instance Functor Chunk where+ {-# INLINE fmap #-}+ fmap f (Chunk t eof) = Chunk (f t) eof++instance (ChunkData t) => Monoid (Chunk t) where+ {-# INLINE mempty #-}+ mempty = Chunk mempty False++ {-# INLINABLE mappend #-}+ mappend ca@(Chunk a eofa) cb@(Chunk b eofb)+ | eofa = error $ "mappend to EOF: " ++ show ca+ ++ " `mappend` " ++ show cb+ | null b = Chunk a eofb -- Just an optimization for case below+ | otherwise = Chunk (mappend a b) eofb++-- | A 'Chunk' is 'null' when its data is 'null' and its EOF flag is+-- 'False'.+instance (ChunkData t) => ChunkData (Chunk t) where+ {-# INLINE null #-}+ null (Chunk t False) = null t+ null (Chunk _ True) = False+ chunkShow = show++-- | Constructor function that builds a chunk containing data and a+-- 'False' EOF flag.+chunk :: t -> Chunk t+{-# INLINE chunk #-}+chunk t = Chunk t False++-- | An chunk with 'mempty' data and the EOF flag 'True'.+chunkEOF :: (Monoid t) => Chunk t+{-# INLINE chunkEOF #-}+chunkEOF = Chunk mempty True++-- | The basic Iteratee type is @Iter t m a@, where @t@ is the type of+-- input (in class 'ChunkData'), @m@ is a monad in which the iteratee+-- may execute actions (using the 'MonadTrans' 'lift' method), and @a@+-- is the result type of the iteratee.+--+-- Internally, an @Iter@ is a function from an input 'Chunk' to a+-- result of type 'IterR'.+newtype Iter t m a = Iter { runIter :: Chunk t -> IterR t m a }++-- | Builds an 'Iter' that keeps requesting input until it receives a+-- non-'null' 'Chunk'. In other words, the 'Chunk' fed to the+-- argument function is guaranteed either to contain data or to have+-- the EOF flag true (or both).+iterF :: (ChunkData t) => (Chunk t -> IterR t m a) -> Iter t m a+{-# INLINE iterF #-}+iterF f = loop+ where loop = Iter $ \c -> if null c then IterF $ loop else f c++-- | Class of control commands for enclosing enumerators. The class+-- binds each control argument type to a unique result type.+class (Typeable carg, Typeable cres) => CtlCmd carg cres | carg -> cres++-- | The outcome of an 'IterC' request.+data CtlRes a = CtlUnsupp+ -- ^ The request type was not supported by the enumerator.+ | CtlFail !SomeException+ -- ^ The request was supported, and executing it caused+ -- an exception to be thrown.+ | CtlDone !a+ -- ^ The result of the control operation.+ deriving (Typeable)++-- | Used when an 'Iter' is issuing a control request to an enclosing+-- enumerator. Note that unlike 'IterF' or 'IterM', control requests+-- expose the residual data, which is ordinarily fed right back to the+-- continuation upon execution of the request. This allows certain+-- control operations (such as seek and tell) to flush, check the+-- length of, or adjust the residual data.+data CtlArg t m a = forall carg cres. (CtlCmd carg cres) =>+ CtlArg !carg (CtlRes cres -> Iter t m a) (Chunk t)++-- | Contains information about a failed 'Iter'. Failures of type+-- 'IterException' must be caught by 'catchI' (or 'tryI', etc.).+-- However, any other type of failure is considered a parse error, and+-- will be caught by 'multiParse', 'ifParse', and 'mplus'.+data IterFail = IterException !SomeException+ -- ^ An actual error occured that is not a parse error,+ -- EOF, etc.+ | IterExpected [(String, String)]+ -- ^ List of @(input_seen, input_expected)@ pairs.+ | IterEOFErr IOError+ -- ^ An EOF error occurred, either in some IO action+ -- wrapped by 'liftIO', or in some 'Iter' that called+ -- 'throwEOFI'.+ | IterParseErr String+ -- ^ A miscellaneous parse error occured.+ | IterMzero+ -- ^ What you get from 'mzero'. Useful if you don't+ -- want to specify any information about the failure.+ deriving (Typeable)++instance Show IterFail where+ showsPrec _ (IterException e) rest = shows e rest+ showsPrec _ (IterExpected swl) rest =+ "Input failed to match all expectations\n" ++ fmt swl+ where fmt [] = rest+ fmt ((saw, expected):t) =+ " expected " ++ show expected ++ ", saw "+ ++ (take 50 $ show saw) ++ "\n" ++ fmt t+ showsPrec _ (IterEOFErr e) rest = "IterEOFErr: " ++ shows e rest+ showsPrec _ (IterParseErr e) rest = "IterParseErr: " ++ e ++ rest+ showsPrec _ IterMzero rest = "IterMZero" ++ rest++instance Exception IterFail where+ {-# INLINE toException #-}+ toException (IterException e) = e+ toException (IterEOFErr e) = toException e+ toException e = SomeException e++-- | An @IterR@ is the result of feeding a chunk of data to an 'Iter'.+-- An @IterR@ is in one of several states: it may require more input+-- ('IterF'), it may wish to execute monadic actions in the+-- transformed monad ('IterM'), it may have a control request for an+-- enclosing enumerator ('IterC'), it may have produced a result+-- ('Done'), or it may have failed ('Fail').+data IterR t m a = IterF !(Iter t m a)+ -- ^ The iteratee requires more input.+ | IterM !(m (IterR t m a))+ -- ^ The iteratee must execute monadic bind in monad @m@+ | IterC !(CtlArg t m a)+ -- ^ A control request (see 'CtlArg').+ | Done a (Chunk t)+ -- ^ Sufficient input was received; the 'Iter' is+ -- returning a result of type @a@. In adition, the+ -- 'IterR' has a 'Chunk' containing any residual+ -- input that was not consumed in producing the+ -- result.+ | Fail !IterFail !(Maybe a) !(Maybe (Chunk t))+ -- ^ The 'Iter' failed. If it was an enumerator, the+ -- target 'Iter' that the enumerator was feeding+ -- likely has not failed, in which case its current+ -- state is returned in the @Maybe a@. If it makes+ -- sense to preserve the state of the input stream+ -- (which it does for most errors except parse+ -- errors), then the third parameter includes the+ -- residual 'Chunk' at the time of the failure.++-- | True if an 'IterR' is requesting something from an+-- enumerator--i.e., the 'IterR' is not 'Done' or 'Fail'.+isIterActive :: IterR t m a -> Bool+{-# INLINE isIterActive #-}+isIterActive (IterF _) = True+isIterActive (IterM _) = True+isIterActive (IterC _) = True+isIterActive _ = False++-- | Show the current state of an 'IterR', prepending it to some+-- remaining input (the standard 'ShowS' optimization), when 'a' is in+-- class 'Show'. Note that if @a@ is not in 'Show', you can simply+-- use the 'shows' function.+iterShows :: (ChunkData t, Show a) => IterR t m a -> ShowS+iterShows (IterC (CtlArg a _ c)) rest =+ "IterC " ++ (shows (typeOf a) $ " _ " ++ shows c rest)+iterShows (Done a c) rest = "Done " ++ (shows a $ " " ++ shows c rest)+iterShows (Fail e a c) rest =+ "Fail " ++ (shows e $ " (" ++ (shows a $ ") " ++ shows c rest))+iterShows iter rest = shows iter rest++-- | Show the current state of an 'Iter' if type @a@ is in the 'Show'+-- class. (Otherwise, you can simply use the ordinary 'show'+-- function.)+iterShow :: (ChunkData t, Show a) => IterR t m a -> String+iterShow iter = iterShows iter ""++instance (ChunkData t) => Show (IterR t m a) where+ showsPrec _ (IterF _) rest = "IterF _" ++ rest+ showsPrec _ (IterM _) rest = "IterM _" ++ rest+ showsPrec _ (IterC (CtlArg a _ c)) rest =+ "IterC " ++ (shows (typeOf a) $ " _" ++ shows c rest)+ showsPrec _ (Done _ c) rest = "Done _ " ++ shows c rest+ showsPrec _ (Fail e a c) rest =+ "Fail " ++ (shows e $+ (if isJust a then " Just _ " else " Nothing ")+ ++ shows c rest)++iterTc :: TyCon+iterTc = mkTyCon "Iter"+instance (Typeable t, Typeable1 m) => Typeable1 (Iter t m) where+ typeOf1 iter = mkTyConApp iterTc [typeOf $ t iter, typeOf1 $ m iter]+ where t :: Iter t m a -> t; t _ = undefined+ m :: Iter t m a -> m (); m _ = undefined++instance (Typeable t, Typeable1 m, Typeable a) => Typeable (Iter t m a) where+ typeOf = typeOfDefault++instance (Monad m) => Functor (Iter t m) where+ {-# INLINE fmap #-}+ fmap = fmapI++-- | @fmapI@ is like 'liftM', but differs in one important respect:+-- it preserves the failed result of an enumerator (and in fact+-- applies the function to the non-failed target 'Iter' state). By+-- contrast, 'liftM', which is equivalent to @'liftM' f i = i '>>='+-- 'return' . f@, transforms the @'Maybe' a@ component of all 'Fail'+-- states to 'Nothing' because of its use of '>>='.+fmapI :: (Monad m) => (a -> b) -> Iter t m a -> Iter t m b+{-# INLINE fmapI #-}+fmapI = onDone . fmapR++-- | Maps the result of an 'IterR' like 'fmap', but only if the+-- 'IterR' is no longer active. It is an error to call this function+-- on an 'IterR' in the 'IterF', 'IterM', or 'IterC' state. Because+-- of this restriction, @fmapR@ does not require the input and output+-- 'Monad' types (@m1@ and @m2@) to be the same.+fmapR :: (a -> b) -> IterR t m1 a -> IterR t m2 b+{-# INLINE fmapR #-}+fmapR f (Done a c) = Done (f a) c+fmapR f (Fail e a c) = Fail e (fmap f a) c+fmapR _ (IterF _) = error "fmapR (IterF)"+fmapR _ (IterM _) = error "fmapR (IterM)"+fmapR _ (IterC _) = error "fmapR (IterC)"++instance (Monad m) => Functor (IterR t m) where+ fmap = onDoneR . fmapR++instance (Monad m) => Applicative (Iter t m) where+ pure = return+ (<*>) = ap+ (*>) = (>>)+ a <* b = do r <- a; b >> return r++instance (Monad m) => Monad (Iter t m) where+ {-# INLINE return #-}+ return a = Iter $ Done a++ {-# INLINE (>>=) #-}+ -- Because check calls itself and (>>=) recursively, IterF and+ -- IterM likely cannot be inlined. However, inlining Done and+ -- Fail (which can occur quite often for parsing failures) in the+ -- first part of this function seems to give about a 1-2% speedup.+ m >>= k = Iter $ \c0 -> case runIter m c0 of+ (Done a c) -> runIter (k a) c+ (Fail e _ c) -> Fail e Nothing c+ r -> check r+ where check (IterM mm) = IterM $ mm >>= return . check+ check (IterF i) = IterF $ i >>= k+ check (IterC (CtlArg a n c)) = IterC $ CtlArg a (n >=> k) c+ check (Fail e _ c) = Fail e Nothing c+ check (Done a c) = runIter (k a) c++ fail msg = Iter $ Fail (IterException $ toException $ ErrorCall msg)+ Nothing . Just++instance (ChunkData t, Monad m) => MonadPlus (Iter t m) where+ {-# INLINE mzero #-}+ mzero = Iter $ const $ Fail IterMzero Nothing Nothing+ mplus a b = ifParse a return b++instance MonadTrans (Iter t) where+ {-# INLINE lift #-}+ lift m = Iter $ \c -> IterM $ m >>= \a -> return $ Done a c++-- | The 'Iter' instance of 'MonadIO' handles errors specially. If+-- the lifted operation throws an exception, 'liftIO' catches the+-- exception and returns it as an 'IterFail' failure. If the+-- exception is an 'IOError' satisfying 'isEOFError', then the+-- exception is wrapped in the 'IterEOFErr' constructor; otherwise, it+-- is wrapped in 'IterException' otherwise. This approach allows+-- efficient testing for EOF errors without the need to invoke the+-- expensive 'cast' or 'fromException' operations. (Yes @liftIO@ uses+-- these expensive operations, but 'Iter's that invoke 'throwEOFI' do+-- not.)+--+-- One consequence of this exception handling is that with 'Iter',+-- unlike with most monad transformers, 'liftIO' is /not/ equivalent+-- to some number of nested calls to 'lift'. See the documentation of+-- '.|$' for an example.+instance (MonadIO m) => MonadIO (Iter t m) where+ liftIO m = Iter $ \c -> IterM $ liftIO $ do+ result <- try m + case result of+ Right ok -> return $ Done ok c+ Left se -> return $ case fromException se of+ Just e | isEOFError e -> Fail (IterEOFErr e) Nothing (Just c)+ _ -> Fail (IterException se) Nothing (Just c)++-- | This is a generalization of 'fixIO' for arbitrary members of the+-- 'MonadIO' class.+fixMonadIO :: (MonadIO m) => (a -> m a) -> m a+fixMonadIO f = do+ ref <- liftIO $ newIORef $ throw $ toException+ $ ErrorCall "fixMonadIO: non-termination"+ a <- liftIO $ unsafeInterleaveIO $ readIORef ref+ r <- f a+ liftIO $ writeIORef ref r+ return r++instance (MonadIO m) => MonadFix (Iter t m) where+ mfix = fixMonadIO++--+-- Core functions+--++-- | Feed an EOF to an 'Iter' and return the result. Throws an+-- exception if there has been a failure.+run :: (ChunkData t, Monad m) => Iter t m a -> m a+run i0 = check $ runIter i0 chunkEOF+ where check (Done a _) = return a+ check (IterF i) = run i+ check (IterM m) = m >>= check+ check (IterC (CtlArg _ n c)) = check $ runIter (n CtlUnsupp) c+ check (Fail (IterException e) _ _) = throw e+ check (Fail e _ _) = throw e++-- | The equivalent for 'runI' for 'IterR's.+runR :: (ChunkData t1, ChunkData t2, Monad m) => IterR t1 m a -> IterR t2 m a+{-# INLINABLE runR #-}+runR (Done a _) = Done a mempty+runR (IterF i) = runR $ runIter i chunkEOF+runR (IterM m) = IterM $ liftM runR m+runR (IterC (CtlArg _ n c)) = runR $ runIter (n CtlUnsupp) c+runR (Fail e a c) = Fail e a $ mempty <$ c++-- | Runs an 'Iter' from within a different 'Iter' monad. If+-- successful, @runI iter@ will produce the same result as @'lift'+-- ('run' iter)@. However, if @iter@ fails, 'run' throws a+-- language-level exception, which cannot be caught within other+-- 'Iter' monads. By contrast, @runI@ throws a monadic exception that+-- can be caught. In short, use @runI@ in preference to @run@ in+-- situations where both are applicable. See a more detailed+-- discussion of the same issue with examples in the documentation for+-- @'.|$'@ in "Data.IterIO.Inum".+runI :: (ChunkData t1, ChunkData t2, Monad m) => Iter t1 m a -> Iter t2 m a+{-# INLINABLE runI #-}+runI i = Iter $ runIterR (runR $ runIter i chunkEOF)++--+-- Exceptions+--++-- | Make an 'IterEOFErr' from a String.+mkIterEOF :: String -> IterFail+mkIterEOF loc = IterEOFErr $ mkIOError eofErrorType loc Nothing Nothing++-- | Exception thrown by 'CtlI' when the type of the control request+-- is not supported by the enclosing enumerator.+data IterCUnsupp = forall carg cres. (CtlCmd carg cres) =>+ IterCUnsupp carg deriving (Typeable)+instance Show IterCUnsupp where+ showsPrec _ (IterCUnsupp carg) rest =+ "Unsupported control request " ++ shows (typeOf carg) rest+instance Exception IterCUnsupp+++--+-- Exception functions+--+-- | Throw an exception from an Iteratee. The exception will be+-- propagated properly through nested Iteratees, which will allow it+-- to be categorized properly and avoid situations in which resources+-- such as file handles are not released. (Most iteratee code does+-- not assume the Monad parameter @m@ is in the 'MonadIO' class, and+-- hence cannot use 'catch' or @'onException'@ to clean up after+-- exceptions.) Use 'throwI' in preference to 'throw' whenever+-- possible.+--+-- Do not use @throwI@ to throw parse errors or EOF errors. Use+-- 'throwEOFI' and 'throwParseI' instead. For performance reasons,+-- the 'IterFail' type segregates EOF and parse errors from other+-- types of failures.+throwI :: (Exception e) => e -> Iter t m a+throwI e = Iter $ Fail (IterException $ toException e) Nothing . Just++-- | Throw an exception of type 'IterEOF'. This will be interpreted+-- by 'mkInum' as an end of file chunk when thrown by the codec. It+-- will also be interpreted by 'ifParse' and 'multiParse' as parsing+-- failure. If not caught within the 'Iter' monad, the exception will+-- be rethrown by 'run' (and hence '|$') as an 'IOError' of type EOF.+throwEOFI :: String -> Iter t m a+{-# INLINE throwEOFI #-}+throwEOFI s = Iter $ Fail (mkIterEOF s) Nothing . Just++-- | Throw a miscellaneous parse error (after which input is assumed+-- to be unsynchronized and thus is discarded). Parse errors may be+-- caught as exception type 'IterFail', but they can also be caught+-- more efficiently by the functions 'multiParse', 'ifParse', and+-- 'mplus'.+throwParseI :: String -> Iter t m a+{-# INLINE throwParseI #-}+throwParseI s = Iter $ \_ -> Fail (IterParseErr s) Nothing Nothing++-- | Run an 'Iter'. Catch any exception it throws (and return the+-- failing iter state). Transform successful results with a function.+--+-- This function is slightly more general than 'catchI'. For+-- instance, we can't implement 'tryI' in terms of just 'catchI'.+-- Something like+--+-- > tryI iter = catchI (iter >>= return . Right) ...+--+-- would remove the possibly unfailed 'Iter' state from failed 'Inum'+-- results, because the '>>=' operator has this effect. (I.e., if+-- @iter@ is @'Fail' e ('Just' i) c@, the expression @iter >>= return+-- . Right@ will be @'Fail' e Nothing c@.) This could be particularly+-- bad in cases where the exception is not even of a type caught by+-- the 'tryI' expression.+--+-- Similarly, trying to implement 'catchI' in terms of 'tryI' doesn't+-- quite work. Something like+--+-- > catchI iter handler = tryI iter >>= either (uncurry handler) return+--+-- would erase state from 'Inum' failures /not/ caught by the handler.+genCatchI :: (ChunkData t, Monad m, Exception e) =>+ Iter t m a+ -- ^ 'Iter' that might throw an exception+ -> (e -> IterR t m a -> Iter t m b) + -- ^ Exception handler+ -> (a -> b)+ -- ^ Conversion function for result and 'InumFail' errors.+ -> Iter t m b+genCatchI iter0 handler conv = onDone check iter0+ where check (Done a c) = Done (conv a) c+ check r@(Fail e0 _ _) =+ case fromException $ toException e0 of+ Just e -> runIter (handler e $ setResid r mempty) (getResid r)+ Nothing -> fmap conv r+ check _ = error "genCatchI"++-- | Catch an exception thrown by an 'Iter', including exceptions+-- thrown by any 'Inum's fused to the 'Iter' (or applied to it with+-- '.|$'). If you wish to catch just errors thrown within 'Inum's,+-- see the function @'inumCatch'@ in "Data.IterIO.Inum".+--+-- On exceptions, @catchI@ invokes a handler passing it both the+-- exception thrown and the state of the failing 'IterR', which may+-- contain more information than just the exception. In particular,+-- if the exception occured in an 'Inum', the returned 'IterR' will+-- also contain the 'IterR' being fed by that 'Inum', which likely+-- will not have failed. To avoid discarding this extra information,+-- you should not re-throw exceptions with 'throwI'. Rather, you+-- should re-throw an exception by re-executing the failed 'IterR'+-- with 'reRunIter'. For example, a possible definition of+-- 'onExceptionI' is:+--+-- @+-- onExceptionI iter cleanup =+-- iter \`catchI\` \\('SomeException' _) r -> cleanup >> 'reRunIter' r+-- @+--+-- Note that @catchI@ only works for /synchronous/ exceptions, such as+-- IO errors (thrown within 'liftIO' blocks), the monadic 'fail'+-- operation, and exceptions raised by 'throwI'. It is not possible+-- to catch /asynchronous/ exceptions, such as lazily evaluated+-- divide-by-zero errors, the 'throw' function, or exceptions raised+-- by other threads using @'throwTo'@ if those exceptions might arrive+-- anywhere outside of a 'liftIO' call.+--+-- @\`catchI\`@ has the default infix precedence (@infixl 9+-- \`catchI\`@), which binds more tightly than any concatenation or+-- fusing operators.+catchI :: (Exception e, ChunkData t, Monad m) =>+ Iter t m a+ -- ^ 'Iter' that might throw an exception+ -> (e -> IterR t m a -> Iter t m a)+ -- ^ Exception handler, which gets as arguments both the+ -- exception and the failing 'Iter' state.+ -> Iter t m a+{-# INLINE catchI #-}+catchI iter handler = genCatchI iter handler id++-- | If an 'Iter' succeeds and returns @a@, returns @'Right' a@. If+-- the 'Iter' fails and throws an exception @e@ (of type @e@), returns+-- @'Left' (e, r)@ where @r@ is the state of the failing 'Iter'.+tryI :: (ChunkData t, Monad m, Exception e) =>+ Iter t m a -> Iter t m (Either (e, IterR t m a) a)+{-# INLINE tryI #-}+tryI iter = genCatchI iter (curry $ return . Left) Right++-- | A version of 'tryI' that catches all exceptions. Instead of+-- returning the exception caught, it returns the failing 'IterR'+-- (from which you can extract the exception if you really want it).+-- The main use of this is for doing some kind of clean-up action,+-- then re-throwing the exception with 'reRunIter'.+--+-- For example, the following is a possible implementation of 'finallyI':+--+-- > finallyI iter cleanup = do+-- > er <- tryRI iter+-- > cleanup+-- > either reRunIter return er+tryRI :: (ChunkData t, Monad m) =>+ Iter t m a -> Iter t m (Either (IterR t m a) a)+tryRI = onDone check+ where check (Fail e a c) = Done (Left $ Fail e a $ Just mempty) $+ fromMaybe mempty c+ check r = Right <$> r++-- | A variant of 'tryI' that just catches EOF errors. Returns+-- 'Nothing' after an EOF error, and 'Just' the result otherwise.+-- Should be much faster than trying to catch an EOF error of type+-- 'Exception'.+tryEOFI :: (ChunkData t, Monad m) =>+ Iter t m a -> Iter t m (Maybe a)+tryEOFI = onDone check+ where check (Fail (IterEOFErr _) _ c) = Done Nothing $ fromMaybe mempty c+ check r = Just <$> r++-- | A varient of 'tryI' that returns the 'IterFail' state rather than+-- trying to match a particular exception.+tryFI :: (ChunkData t, Monad m) =>+ Iter t m a -> Iter t m (Either IterFail a)+tryFI = onDone check+ where check (Fail e _ c) = Done (Left e) $ fromMaybe mempty c+ check r = Right <$> r++-- | Execute an 'Iter', then perform a cleanup action regardless of+-- whether the 'Iter' threw an exception or not. Analogous to the+-- standard library function @'finally'@.+finallyI :: (ChunkData t, Monad m) =>+ Iter t m a -> Iter t m b -> Iter t m a+finallyI iter cleanup = do er <- tryRI iter+ cleanup >> either reRunIter return er++-- | Execute an 'Iter' and perform a cleanup action if the 'Iter'+-- threw an exception. Analogous to the standard library function+-- @'onException'@.+onExceptionI :: (ChunkData t, Monad m) =>+ Iter t m a -> Iter t m b -> Iter t m a+onExceptionI iter cleanup =+ catchI iter $ \(SomeException _) r -> cleanup >> reRunIter r++-- | Simlar to 'tryI', but saves all data that has been fed to the+-- 'Iter', and rewinds the input if the 'Iter' fails. (The @B@ in+-- @tryBI@ stands for \"backtracking\".) Thus, if @tryBI@ returns+-- @'Left' exception@, the next 'Iter' to be invoked will see the same+-- input that caused the previous 'Iter' to fail. (For this reason,+-- it makes no sense ever to call @'resumeI'@ on the 'Iter' you get+-- back from @tryBI@, which is why @tryBI@ does not return the failing+-- Iteratee the way 'tryI' does.)+--+-- Because @tryBI@ saves a copy of all input, it can consume a lot of+-- memory and should only be used when the 'Iter' argument is known to+-- consume a bounded amount of data.+tryBI :: (ChunkData t, Monad m, Exception e) =>+ Iter t m a -> Iter t m (Either e a)+tryBI = onDoneInput errToEither+ where errToEither (Done a c) _ = Done (Right a) c+ errToEither r@(Fail ie _ _) c =+ case fromException $ toException ie of+ Just e -> Done (Left e) c+ Nothing -> Right <$> r+ errToEither _ _ = error "tryBI"++-- | A variant of 'tryBI' that, also rewinds input on failure, but+-- returns the raw 'IterFail' structure, rather than mapping it to a+-- particular exception. This is much faster because it requires no+-- dynamic casts. However, the same warning applies that @tryFBI@+-- should not be applied to 'Iter's that could take unbounded input.+tryFBI :: (ChunkData t, Monad m) =>+ Iter t m a -> Iter t m (Either IterFail a)+tryFBI = onDoneInput check+ where check (Done a c) _ = Done (Right a) c+ check (Fail e _ _) c = Done (Left e) c+ check _ _ = error "tryFBI"++{-+mapIterFail :: (ChunkData t, Monad m) =>+ (IterFail -> IterFail) -> Iter t m a -> Iter t m a+mapIterFail f = onDone check+ where check (Fail e a c) = Fail (f e) a c+ check r = r+-}++-- | Run an Iteratee, and if it throws a parse error by calling+-- 'expectedI', then combine the exptected tokens with those of a+-- previous parse error.+combineExpected :: (ChunkData t, Monad m) =>+ IterFail+ -- ^ Previous parse error+ -> IterR t m a+ -- ^ Result of second 'Iter'--if it fails the error+ -- should be combined with the first error+ -> IterR t m a+combineExpected _ r@(Done _ _) = r+combineExpected (IterExpected l1) (Fail (IterExpected l2) _ _) =+ Fail (IterExpected $ l1 ++ l2) Nothing Nothing+combineExpected _ r@(Fail (IterExpected _) _ _) = r+combineExpected e _ = Fail e Nothing Nothing++-- | Try two Iteratees and return the result of executing the second+-- if the first one throws a parse, EOF, or 'mzero' error. Note that+-- "Data.IterIO.Parse" defines @'<|>'@ as an infix synonym for this+-- function.+--+-- The statement @multiParse a b@ is similar to @'ifParse' a return+-- b@, but the two functions operate differently. Depending on the+-- situation, only one of the two formulations may be correct.+-- Specifically:+-- +-- * @'ifParse' a f b@ works by first executing @a@, saving a copy of+-- all input consumed by @a@. If @a@ throws a parse error, the+-- saved input is used to backtrack and execute @b@ on the same+-- input that @a@ just rejected. If @a@ succeeds, @b@ is never+-- run; @a@'s result is fed to @f@, and the resulting action is+-- executed without backtracking (so any error thrown within @f@+-- will not be caught by this 'ifParse' expression).+--+-- * Instead of saving input, @multiParse a b@ executes both @a@ and+-- @b@ concurrently as input chunks arrive. If @a@ throws a parse+-- error, then the result of executing @b@ is returned. If @a@+-- either succeeds or throws an exception that is not a parse+-- error/EOF/'mzero', then the result of running @a@ is returned.+--+-- * With @multiParse a b@, if @b@ returns a value, executes a+-- monadic action via 'lift', or issues a control request via+-- 'ctlI', then further processing of @b@ will be suspended until+-- @a@ experiences a parse error, and thus the behavior will be+-- equivalent to @'ifParse' a return b@.+--+-- The main restriction on 'ifParse' is that @a@ must not consume+-- unbounded amounts of input, or the program may exhaust memory+-- saving the input for backtracking. Note that the second argument+-- to 'ifParse' (i.e., 'return' in @ifParse a return b@) is a+-- continuation for @a@ when @a@ succeeds.+--+-- The advantage of @multiParse@ is that it can avoid storing+-- unbounded amounts of input for backtracking purposes if both+-- 'Iter's consume data. Another advantage is that with an expression+-- such as @'ifParse' a f b@, sometimes it is not convenient to break+-- the parse target into an action to execute with backtracking (@a@)+-- and a continuation to execute without backtracking (@f@). The+-- equivalent @multiParse (a >>= f) b@ avoids the need to do this,+-- since it does not do backtracking.+--+-- However, it is important to note that it is still possible to end+-- up storing unbounded amounts of input with @multiParse@. For+-- example, consider the following code:+--+-- > total :: (Monad m) => Iter String m Int+-- > total = multiParse parseAndSumIntegerList (return -1) -- Bad+--+-- Here the intent is for @parseAndSumIntegerList@ to parse a+-- (possibly huge) list of integers and return their sum. If there is+-- a parse error at any point in the input, then the result is+-- identical to having defined @total = return -1@. But @return -1@+-- succeeds immediately, consuming no input, which means that @total@+-- must return all left-over input for the next action (i.e., @next@+-- in @total >>= next@). Since @total@ has to look arbitrarily far+-- into the input to determine that @parseAndSumIntegerList@ fails, in+-- practice @total@ will have to save all input until it knows that+-- @parseAndSumIntegerList@ succeeds.+--+-- A better approach might be:+--+-- @+-- total = multiParse parseAndSumIntegerList ('nullI' >> return -1)+-- @+--+-- Here 'nullI' discards all input until an EOF is encountered, so+-- there is no need to keep a copy of the input around. This makes+-- sense so long as @total@ is the last or only Iteratee run on the+-- input stream. (Otherwise, 'nullI' would have to be replaced with+-- an Iteratee that discards input up to some end-of-list marker.)+--+-- Another approach might be to avoid parsing combinators entirely and+-- use:+--+-- @+-- total = parseAndSumIntegerList ``catchI`` handler+-- where handler \('IterNoParse' _) _ = return -1+-- @+--+-- This last definition of @total@ may leave the input in some+-- partially consumed state. This is fine so long as @total@ is the+-- last 'Iter' executed on the input stream. Otherwise, before+-- throwing the parse error, @parseAndSumIntegerList@ would need to+-- ensure the input is at some reasonable boundary point for whatever+-- comes next. (The 'ungetI' function is sometimes helpful for this+-- purpose.)+multiParse :: (ChunkData t, Monad m) =>+ Iter t m a -> Iter t m a -> Iter t m a+multiParse a b = Iter $ \c -> check (runIter a c) (runIter b c)+ where+ check ra@(Done _ _) _ = ra+ check (IterF ia) (IterF ib) = IterF $ multiParse ia ib+ check (IterF ia) rb =+ IterF $ onDoneInput (\ra c -> check ra (runIterR rb c)) ia+ check ra rb = stepR ra (flip check rb) $ case ra of+ (Fail (IterException _) _ _) -> ra+ (Fail e _ _) -> onDoneR (combineExpected e) rb+ _ -> error "multiParse"++-- | @ifParse iter success failure@ runs @iter@, but saves a copy of+-- all input consumed using 'tryFBI'. (This means @iter@ must not+-- consume unbounded amounts of input! See 'multiParse' for such+-- cases.) If @iter@ succeeds, its result is passed to the function+-- @success@. If @iter@ throws a parse error (with 'throwParseI'),+-- throws an EOF error (with 'throwEOFI'), or executes 'mzero', then+-- @failure@ is executed with the input re-wound (so that @failure@ is+-- fed the same input that @iter@ was). If @iter@ throws any other+-- type of exception, @ifParse@ passes the exception back and does not+-- execute @failure@.+--+-- See "Data.IterIO.Parse" for a discussion of this function and the+-- related infix operator @\\/@ (which is a synonym for 'ifNoParse').+ifParse :: (ChunkData t, Monad m) =>+ Iter t m a+ -- ^ Iteratee @iter@ to run with backtracking+ -> (a -> Iter t m b)+ -- ^ @success@ function+ -> Iter t m b+ -- ^ @failure@ action+ -> Iter t m b+ -- ^ result+ifParse iter yes no = tryFBI iter >>= check+ where check (Right a) = yes a+ check (Left (IterException e)) = throwI e+ check (Left e) = onDone (combineExpected e) no+++-- | @ifNoParse@ is just 'ifParse' with the second and third arguments+-- reversed.+ifNoParse :: (ChunkData t, Monad m) =>+ Iter t m a -> Iter t m b -> (a -> Iter t m b) -> Iter t m b+{-# INLINE ifNoParse #-}+ifNoParse iter no yes = ifParse iter yes no+++--+-- Some super-basic Iteratees+--++-- | Sinks data like @\/dev\/null@, returning @()@ on EOF.+nullI :: (Monad m, ChunkData t) => Iter t m ()+nullI = Iter $ \(Chunk _ eof) ->+ if eof then Done () chunkEOF else IterF nullI++-- | Returns a non-empty amount of input data if there is any input+-- left. Returns 'mempty' on an EOF condition.+data0I :: (ChunkData t) => Iter t m t+{-# INLINE data0I #-}+data0I = iterF $ \(Chunk d eof) -> Done d (Chunk mempty eof)++-- | Like 'data0I', but always returns non-empty data. Throws an+-- exception on an EOF condition.+dataI :: (ChunkData t) => Iter t m t+{-# INLINE dataI #-}+dataI = iterF nextChunk+ where nextChunk c@(Chunk d True) | null d = Fail eoferr Nothing (Just c)+ nextChunk (Chunk d eof) = Done d (Chunk mempty eof)+ eoferr = mkIterEOF "dataI"++-- | A variant of 'data0I' that reads the whole input up to an EOF and+-- returns it.+pureI :: (Monad m, ChunkData t) => Iter t m t+pureI = do peekI nullI; Iter $ \(Chunk t _) -> Done t chunkEOF++-- | Returns the next 'Chunk' that either contains non-'null' data or+-- has the EOF bit set.+chunkI :: (Monad m, ChunkData t) => Iter t m (Chunk t)+{-# INLINE chunkI #-}+chunkI = iterF $ \c@(Chunk _ eof) -> Done c (Chunk mempty eof)++-- | Keep running an 'Iter' until either its output is not 'null' or+-- we have reached EOF. Return the the `Iter`'s value on the last+-- (i.e., usually non-'null') iteration.+whileNullI :: (ChunkData tIn, ChunkData tOut, Monad m) =>+ Iter tIn m tOut -> Iter tIn m tOut+whileNullI iter = loop+ where loop = do buf <- iter+ if null buf+ then do eof <- atEOFI+ if eof then return buf else loop+ else return buf++-- | Runs an 'Iter' then rewinds the input state, so that the effect+-- is to parse lookahead data. (See 'tryBI' if you want to rewind the+-- input only when the 'Iter' fails.)+peekI :: (ChunkData t, Monad m) => Iter t m a -> Iter t m a+peekI = onDoneInput setResid++-- | Does not actually consume any input, but returns 'True' if there+-- is no more input data to be had.+atEOFI :: (Monad m, ChunkData t) => Iter t m Bool+atEOFI = iterF $ \c@(Chunk t _) -> Done (null t) c++-- | Place data back onto the input stream, where it will be the next+-- data consumed by subsequent 'Iter's.+ungetI :: (ChunkData t) => t -> Iter t m ()+{-# INLINE ungetI #-}+ungetI t = Iter $ \c -> Done () (mappend (chunk t) c)++-- | Issue a control request. Returns 'CtlUnsupp' if the request type+-- is unsupported. Otherwise, returns 'CtlDone' with the result if+-- the request succeeds, or return @'CtlFail'@ if the request type is+-- supported but attempting to execute the request caused an+-- exception.+safeCtlI :: (CtlCmd carg cres, Monad m) =>+ carg -> Iter t m (CtlRes cres)+safeCtlI carg = Iter $ IterC . CtlArg carg return++-- | Issue a control request and return the result. Throws an+-- exception of type 'IterCUnsupp' if the operation type was not+-- supported by an enclosing enumerator.+ctlI :: (CtlCmd carg cres, ChunkData t, Monad m) =>+ carg -> Iter t m cres+ctlI carg = do+ res <- safeCtlI carg+ case res of+ CtlUnsupp -> throwI $ IterCUnsupp carg+ CtlFail e -> throwI e+ CtlDone cres -> return cres++--+-- Iter manipulation functions+--++-- | A variant of 'stepR' that only works for the 'IterF' and 'IterC'+-- states, not the 'IterM' state. (Because of this additional+-- restriction, the input and output 'Monad' types @m1@ and @m2@ do+-- not need to be the same.)+stepR' :: IterR t m1 a+ -- ^ The 'IterR' that needs to be stepped.+ -> (IterR t m1 a -> IterR t m2 b)+ -- ^ Transformation function if the 'IterR' is in the 'IterF'+ -- or 'IterC' state.+ -> IterR t m2 b+ -- ^ Fallback if the 'IterR' is no longer active.+ -> IterR t m2 b+{-# INLINE stepR' #-}+stepR' (IterF (Iter i)) f _ = IterF $ Iter $ f . i+stepR' (IterC (CtlArg a n c)) f _ =+ IterC $ CtlArg a (Iter . (f .) . runIter . n) c+stepR' (IterM _) _ _ = error "stepR' (IterM)"+stepR' _ _ notActive = notActive++-- | Step an active 'IterR' (i.e., one in the 'IterF', 'IterM', or+-- 'IterC' state) to its next state, and pass the result through a+-- function.+stepR :: (Monad m) =>+ IterR t m a+ -- ^ The 'Iter' that needs to be stepped+ -> (IterR t m a -> IterR t m b)+ -- ^ Function to pass the 'Iter' to after stepping it.+ -> IterR t m b+ -- ^ Fallback if the 'Iter' can no longer be stepped+ -> IterR t m b+{-# INLINE stepR #-}+stepR (IterM m) f _ = IterM $ liftM f m+stepR r f notActive = stepR' r f notActive++-- | The equivalent of 'onDone' for 'IterR's.+onDoneR :: (Monad m) =>+ (IterR t m a -> IterR t m b) -> IterR t m a -> IterR t m b+{-# INLINE onDoneR #-}+onDoneR f = check+ where check r = stepR r check $ f r++-- | Run an 'Iter' until it enters the 'Done' or 'Fail' state, then+-- use a function to transform the 'IterR'.+{-# INLINE onDone #-}+onDone :: (Monad m) =>+ (IterR t m a -> IterR t m b) -> Iter t m a -> Iter t m b+onDone f i = Iter $ onDoneR f . runIter i++-- | Like 'onDone', but also keeps a copy of all input consumed. (The+-- residual input on the 'IterR' returned will be a suffix of the+-- input returned.)+onDoneInput :: (ChunkData t, Monad m) =>+ (IterR t m a -> Chunk t -> IterR t m b)+ -> Iter t m a+ -> Iter t m b+{-# INLINABLE onDoneInput #-}+onDoneInput f = Iter . next id+ where next acc iter c =+ let check (IterF i) = IterF $ Iter $ next (acc . mappend c) i+ check r = stepR r check $ f r (acc c)+ in check $ runIter iter c++++-- | Get the residual data for an 'IterR' that is in no longer active+-- or that is in the 'IterC' state. (It is an error to call this+-- function on an 'IterR' in the 'IterF' or 'IterM' state.)+getResid :: (ChunkData t) => IterR t m a -> Chunk t+{-# INLINABLE getResid #-}+getResid (Done _ c) = c+getResid (Fail _ _ c) = fromMaybe mempty c+getResid (IterC (CtlArg _ _ c)) = c+getResid (IterF _) = error "getResid (IterF)"+getResid (IterM _) = error "getResid (IterM)"++-- | Set residual data for an 'IterR' that is not active. (It is an+-- error to call this on an 'IterR' in the 'Done', 'IterM', or 'IterC'+-- states.)+setResid :: IterR t1 m1 a -> Chunk t2 -> IterR t2 m2 a+{-# INLINABLE setResid #-}+setResid (Done a _) = Done a+setResid (Fail e a _) = Fail e a . Just+setResid (IterF _) = error "setResid (IterF)"+setResid (IterM _) = error "setResid (IterM)"+setResid (IterC _) = error "setResid (IterC)"++-- | Feed more input to an 'Iter' that has already been run (and hence+-- is already an 'IterR'). In the event that the 'IterR' is+-- requesting more input (i.e., is in the 'IterF' state), this is+-- straight forward. However, if the 'Iter' is in some other state+-- such as 'IterM', this function needs to save the input until such+-- time as the 'IterR' is stepped to a new state (e.g., with 'stepR'+-- or 'reRunIter').+runIterR :: (ChunkData t, Monad m) => IterR t m a -> Chunk t -> IterR t m a+{-# INLINABLE runIterR #-}+runIterR r c = if null c then r else check r+ where check (Done a c0) = Done a (mappend c0 c)+ check (IterF i) = runIter i c+ check (IterM m) = IterM $ liftM check m+ check (IterC (CtlArg a n c0)) = IterC $ CtlArg a n (mappend c0 c)+ check (Fail e a c0) = Fail e a $ fmap (`mappend` c) c0++-- | Turn an 'IterR' back into an 'Iter'.+reRunIter :: (ChunkData t, Monad m) => IterR t m a -> Iter t m a+{-# INLINE reRunIter #-}+reRunIter (IterF i) = i+reRunIter r = Iter $ runIterR r
+ Data/IterIO/ListLike.hs view
@@ -0,0 +1,487 @@+{-# LANGUAGE MultiParamTypeClasses #-}+{-# LANGUAGE DeriveDataTypeable #-}++-- | This module contains basic iteratees and enumerators for working+-- with strings, 'LL.ListLike' objects, file handles, and stream and+-- datagram sockets.+module Data.IterIO.ListLike+ ( -- * Iteratees+ putI, sendI+ , headLI, safeHeadLI+ , headI, safeHeadI+ , lineI, safeLineI+ , dataMaxI, data0MaxI, takeI+ , handleI, sockDgramI, sockStreamI+ , stdoutI+ -- * Control requests+ , SeekMode(..)+ , SizeC(..), SeekC(..), TellC(..), fileCtl+ , GetSocketC(..), socketCtl+ -- * Onums+ , enumDgram, enumDgramFrom, enumStream+ , enumHandle, enumHandle', enumNonBinHandle+ , enumFile, enumFile'+ , enumStdin+ -- * Inums+ , inumMax, inumTakeExact+ , inumLog, inumhLog, inumStderr+ , inumLtoS, inumStoL+ -- * Functions for Iter-Inum pairs+ , pairFinalizer, iterHandle, iterStream+ ) where++import Prelude hiding (null)+import Control.Concurrent+import Control.Exception (onException)+import Control.Monad+import Control.Monad.Trans+import qualified Data.ByteString as S+import qualified Data.ByteString.Lazy as L+import qualified Data.ByteString.Lazy.Internal as L+ (defaultChunkSize, chunk, ByteString(..))+import Data.Char+import Data.Monoid+import Data.Typeable+import Network.Socket+import System.IO++import qualified Data.ListLike as LL++import Data.IterIO.Iter+import Data.IterIO.Inum+import Data.IterIO.Extra+++echr :: (Enum e) => Char -> e+echr = toEnum . ord++--+-- Iters+--++-- | An Iteratee that puts data to a consumer function, then calls an+-- eof function. For instance, @'handleI'@ could be defined as:+--+-- @+-- handleI :: (MonadIO m) => 'Handle' -> 'Iter' 'L.ByteString' m ()+-- handleI h = putI ('liftIO' . 'L.hPut' h) ('liftIO' $ 'hShutdown' h 1)+-- @+putI :: (ChunkData t, Monad m) =>+ (t -> Iter t m a)+ -> Iter t m b+ -> Iter t m ()+putI putfn eoffn = doput `finallyI` eoffn+ where doput = do Chunk t eof <- chunkI+ unless (null t) $ putfn t >> return ()+ if eof then return () else doput++-- | Send datagrams using a supplied function. The datagrams are fed+-- as a list of packets, where each element of the list should be a+-- separate datagram. For example, to create an 'Iter' from a+-- connected UDP socket:+--+-- @+-- udpI :: ('SendRecvString' s, 'MonadIO' m) => 'Socket' -> 'Iter' s m ()+-- udpI sock = sendI $ 'liftIO' . 'genSend' sock+-- @+sendI :: (Show t, Monad m) =>+ (t -> Iter [t] m a)+ -> Iter [t] m ()+sendI sendfn = do+ dgram <- safeHeadI+ case dgram of+ Just pkt -> sendfn pkt >> sendI sendfn+ Nothing -> return ()++-- | Return the first element when the Iteratee data type is a list.+headLI :: (Show a, Monad m) => Iter [a] m a+{-# INLINABLE headLI #-}+headLI = iterF dohead+ where dohead (Chunk (a:as) eof) = Done a $ Chunk as eof+ dohead c = Fail err Nothing $ Just c+ err = mkIterEOF "headLI"++-- | Return 'Just' the first element when the Iteratee data type+-- is a list, or 'Nothing' on EOF.+safeHeadLI :: (Show a, Monad m) => Iter [a] m (Maybe a)+{-# INLINABLE safeHeadLI #-}+safeHeadLI = iterF $ dohead+ where dohead (Chunk (a:as) eof) = Done (Just a) $ Chunk as eof+ dohead _ = Done Nothing chunkEOF+++-- | Like 'headLI', but works for any 'LL.ListLike' data type.+headI :: (ChunkData t, LL.ListLike t e, Monad m) => Iter t m e+{-# INLINABLE headI #-}+headI = iterF $ \c@(Chunk t eof) ->+ if LL.null t then Fail err Nothing $ Just c+ else Done (LL.head t) $ Chunk (LL.tail t) eof+ where err = mkIterEOF "headI"++-- | Like 'safeHeadLI', but works for any 'LL.ListLike' data type.+safeHeadI :: (ChunkData t, LL.ListLike t e, Monad m) => Iter t m (Maybe e)+{-# INLINABLE safeHeadI #-}+safeHeadI = iterF $ \c@(Chunk t eof) ->+ if LL.null t then Done Nothing c+ else Done (Just $ LL.head t) $ Chunk (LL.tail t) eof++-- | Like 'lineI', but returns 'Nothing' on EOF.+safeLineI :: (ChunkData t, Monad m, LL.ListLike t e, Eq t, Enum e, Eq e) =>+ Iter t m (Maybe t)+safeLineI = iterF $ doline LL.empty+ where+ cr = LL.singleton $ echr '\r'+ nl = LL.singleton $ echr '\n'+ crnl = LL.append cr nl+ eol c = c == echr '\n' || c == echr '\r'+ doline acc (Chunk t eof) =+ let acc' = LL.append acc t+ (l, r) = LL.break eol acc'+ result = dolr eof l r+ in case result of+ Just (l', r') -> Done (Just l') (Chunk r' eof)+ Nothing | eof -> Done Nothing (Chunk acc' True)+ _ -> IterF $ iterF $ doline acc'+ dolr eof l r+ | LL.isPrefixOf nl r = Just (l, LL.drop (LL.length nl) r)+ | LL.isPrefixOf crnl r = Just (l, LL.drop (LL.length crnl) r)+ | LL.isPrefixOf cr r && (eof || r /= cr) =+ Just (l, LL.drop (LL.length cr) r)+ | otherwise = Nothing++-- | Return a line delimited by \\r, \\n, or \\r\\n.+lineI :: (Monad m, ChunkData t, LL.ListLike t e, Eq t, Enum e, Eq e) =>+ Iter t m t+lineI = do+ mline <- safeLineI+ case mline of+ Nothing -> throwEOFI "lineI"+ Just line -> return line++-- | Return 'LL.ListLike' data that is at most the number of elements+-- specified by the first argument, and at least one element unless+-- EOF is encountered or 0 elements are requested, in which case+-- 'mempty' is returned.+data0MaxI :: (ChunkData t, LL.ListLike t e, Monad m) => Int -> Iter t m t+data0MaxI maxlen | maxlen <= 0 = return mempty+ | otherwise = iterF $ \(Chunk s eof) ->+ case LL.splitAt maxlen s of+ (h, t) -> Done h $ Chunk t eof++-- | Return 'LL.ListLike' data that is at most the number of elements+-- specified by the first argument, and at least one element (as long+-- as a positive number is requested). Throws an exception if a+-- positive number of items is requested and an EOF is encountered.+dataMaxI :: (ChunkData t, LL.ListLike t e, Monad m) => Int -> Iter t m t+dataMaxI maxlen | maxlen <= 0 = return mempty+ | otherwise = iterF $ \c@(Chunk s eof) ->+ if LL.null s then Fail err Nothing $ Just c+ else case LL.splitAt maxlen s of+ (h, t) -> Done h $ Chunk t eof+ where err = mkIterEOF "dataMaxI"++-- | Return the next @len@ elements of a 'LL.ListLike' data stream,+-- unless an EOF is encountered, in which case fewer may be returned.+-- Note the difference from 'data0MaxI': @'takeI' n@ will keep+-- reading input until it has accumulated @n@ elements or seen an EOF,+-- then return the data; @'data0MaxI' n@ will keep reading only until+-- it has received any non-empty amount of data, even if the amount+-- received is less than @n@ elements and there is no EOF.+takeI :: (ChunkData t, LL.ListLike t e, Monad m) => Int -> Iter t m t+takeI len | len <= 0 = return mempty+ | otherwise = do+ t <- data0MaxI len+ let tlen = LL.length t+ if tlen == len || tlen == 0+ then return t+ else LL.append t `liftM` takeI (len - tlen)++-- | Puts strings (or 'LL.ListLikeIO' data) to a file 'Handle', then+-- writes an EOF to the handle.+--+-- Note that this does not put the handle into binary mode. To do+-- this, you may need to call @'hSetBinaryMode' h 'True'@ on the+-- handle before using it with @handleI@. Otherwise, Haskell by+-- default will treat the data as UTF-8. (On the other hand, if the+-- 'Handle' corresponds to a socket and the socket is being read in+-- another thread, calling 'hSetBinaryMode' can cause deadlock, so in+-- this case it is better to have the thread handling reads call+-- 'hSetBinaryMode'.)+--+-- Also note that Haskell by default buffers data written to+-- 'Handle's. For many network protocols this is a problem. Don't+-- forget to call @'hSetBuffering' h 'NoBuffering'@ before passing a+-- handle to 'handleI'.+handleI :: (MonadIO m, ChunkData t, LL.ListLikeIO t e) =>+ Handle+ -> Iter t m ()+handleI h = putI (liftIO . LL.hPutStr h) (liftIO $ hShutdown h 1)++-- | Sends a list of packets to a datagram socket.+sockDgramI :: (MonadIO m, SendRecvString t) =>+ Socket+ -> Maybe SockAddr+ -> Iter [t] m ()+sockDgramI s mdest = loop+ where sendit = case mdest of Nothing -> liftIO . genSend s+ Just dest -> liftIO . flip (genSendTo s) dest+ loop = safeHeadI >>= maybe (return ()) (\str -> sendit str >> loop)++-- | Sends output to a stream socket. Calls shutdown (e.g., to send a+-- TCP FIN packet) upon receiving EOF.+sockStreamI :: (ChunkData t, SendRecvString t, MonadIO m) =>+ Socket -> Iter t m ()+sockStreamI sock = putI (liftIO . genSend sock)+ (liftIO $ shutdown sock ShutdownSend)++-- | An 'Iter' that uses 'LL.hPutStr' to write all output to 'stdout'.+stdoutI :: (LL.ListLikeIO t e, ChunkData t, MonadIO m) => Iter t m ()+stdoutI = putI (liftIO . LL.hPutStr stdout) (return ())++--+-- Control functions+--++-- | A control command (issued with @'ctlI' SizeC@) requesting the+-- size of the current file being enumerated.+data SizeC = SizeC deriving (Typeable)+instance CtlCmd SizeC Integer++-- | A control command for seeking within a file, when a file is being+-- enumerated. Flushes the residual input data.+data SeekC = SeekC !SeekMode !Integer deriving (Typeable)+instance CtlCmd SeekC ()++-- | A control command for determining the current offset within a+-- file. Note that this subtracts the size of the residual input data+-- from the offset in the file. Thus, it will only be accurate when+-- all left-over input data is from the current file.+data TellC = TellC deriving (Typeable)+instance CtlCmd TellC Integer++-- | A handler function for the 'SizeC', 'SeekC', and 'TellC' control+-- requests. @fileCtl@ is used internally by 'enumFile' and+-- 'enumHandle', and is exposed for similar enumerators to use.+fileCtl :: (ChunkData t, LL.ListLike t e, MonadIO m) =>+ Handle+ -> CtlHandler (Iter () m) t m a+fileCtl h = (mkFlushCtl $ \(SeekC mode pos) -> liftIO (hSeek h mode pos))+ `consCtl` tryTellC+ `consCtl` (mkCtl $ \SizeC -> liftIO (hFileSize h))+ `consCtl` passCtl id+ where tryTellC TellC n c@(Chunk t _) = do+ offset <- liftIO $ hTell h+ return $ runIter (n $ offset - LL.genericLength t) c++-- | A control request that returns the 'Socket' from an enclosing+-- socket enumerator.+data GetSocketC = GetSocketC deriving (Typeable)+instance CtlCmd GetSocketC Socket++-- | A handler for the 'GetSocketC' control request.+socketCtl :: (ChunkData t, MonadIO m) =>+ Socket -> CtlHandler (Iter () m) t m a+socketCtl s = (mkCtl $ \GetSocketC -> return s)+ `consCtl` passCtl id++--+-- Onums+--++-- | Read datagrams (of up to 64KiB in size) from a socket and feed a+-- list of strings (one for each datagram) into an Iteratee.+enumDgram :: (MonadIO m, SendRecvString t) =>+ Socket+ -> Onum [t] m a+enumDgram sock = mkInumC id (socketCtl sock) $+ liftIO $ liftM (: []) $ genRecv sock 0x10000++-- | Read datagrams from a socket and feed a list of (Bytestring,+-- SockAddr) pairs (one for each datagram) into an Iteratee.+enumDgramFrom :: (MonadIO m, SendRecvString t) =>+ Socket+ -> Onum [(t, SockAddr)] m a+enumDgramFrom sock = mkInumC id (socketCtl sock) $+ liftIO $ liftM (: []) $ genRecvFrom sock 0x10000++-- | Read data from a stream (e.g., TCP) socket.+enumStream :: (MonadIO m, ChunkData t, SendRecvString t) =>+ Socket -> Onum t m a+enumStream sock = mkInumC id (socketCtl sock) $+ liftIO (genRecv sock L.defaultChunkSize)++-- | A variant of 'enumHandle' type restricted to input in the Lazy+-- 'L.ByteString' format.+enumHandle' :: (MonadIO m) => Handle -> Onum L.ByteString m a+enumHandle' = enumHandle++-- | Puts a handle into binary mode with 'hSetBinaryMode', then+-- enumerates data read from the handle to feed an 'Iter' with any+-- 'LL.ListLikeIO' input type.+enumHandle :: (MonadIO m, ChunkData t, LL.ListLikeIO t e) =>+ Handle+ -> Onum t m a+enumHandle h iter = tryFI (liftIO $ hSetBinaryMode h True) >>= check+ where check (Left e) = Iter $ Fail e (Just $ IterF iter) . Just+ check (Right _) = enumNonBinHandle h iter++-- | Feeds an 'Iter' with data from a file handle, using any input+-- type in the 'LL.ListLikeIO' class. Note that @enumNonBinHandle@+-- uses the handle as is, unlike 'enumHandle', and so can be used if+-- you want to read the data in non-binary form.+enumNonBinHandle :: (MonadIO m, ChunkData t, LL.ListLikeIO t e) =>+ Handle+ -> Onum t m a+enumNonBinHandle h =+ mkInumC id (fileCtl h) $+ liftIO (hWaitForInput h (-1) >> LL.hGetNonBlocking h L.defaultChunkSize)+-- Note that hGet can block when there is some (but not enough) data+-- available. Thus, we use hWaitForInput followed by hGetNonBlocking.+-- ByteString introduced the call hGetSome for this purpose, but it is+-- not supported by the ListLike package yet.++-- | Enumerate the contents of a file as a series of lazy+-- 'L.ByteString's. (This is a type-restricted version of+-- 'enumFile'.)+enumFile' :: (MonadIO m) => FilePath -> Onum L.ByteString m a+enumFile' = enumFile++-- | Enumerate the contents of a file for an 'Iter' taking input in+-- any 'LL.ListLikeIO' type. Note that the file is opened with+-- 'openBinaryFile' to ensure binary mode.+enumFile :: (MonadIO m, ChunkData t, LL.ListLikeIO t e) =>+ FilePath -> Onum t m a+enumFile path = inumBracket (liftIO $ openBinaryFile path ReadMode)+ (liftIO . hClose) enumNonBinHandle++-- | Enumerate standard input.+enumStdin :: (MonadIO m, ChunkData t, LL.ListLikeIO t e) => Onum t m a+enumStdin = enumHandle stdin++--+-- Inums+--++-- | Feed exactly some number of bytes to an 'Iter'. Throws an error+-- if that many bytes are not available.+inumTakeExact :: (ChunkData t, LL.ListLike t e, Monad m) => Int -> Inum t t m a+inumTakeExact = mkInumM . loop+ where loop n | n <= 0 = return ()+ | otherwise = do+ t <- dataI+ let (h, r) = LL.splitAt n t+ ungetI r+ _ <- ifeed h -- Keep feeding even if Done+ loop $ n - LL.length h++-- | Feed up to some number of list elements (bytes in the case of+-- 'L.ByteString's) to an 'Iter', or feed fewer if the 'Iter' returns+-- or an EOF is encountered. The formulation @inumMax n '.|' iter@+-- can be used to prevent @iter@ from consuming unbounded amounts of+-- input.+inumMax :: (ChunkData t, LL.ListLike t e, Monad m) => Int -> Inum t t m a+{-# SPECIALIZE inumMax :: (Monad m) =>+ Int -> Inum L.ByteString L.ByteString m a #-}+{-# SPECIALIZE inumMax :: (Monad m) =>+ Int -> Inum S.ByteString S.ByteString m a #-}+inumMax n0 i | n0 <= 0 = runner i mempty+ | otherwise = do+ (t, more) <- next n0+ r <- runner i $ chunk t+ case r of+ IterF i1 | more -> inumMax (n0 - LL.length t) i1+ _ | isIterActive r -> return r+ _ -> case getResid r of+ Chunk t1 _ -> ungetI t1 >> return (setResid r mempty) + where runner = runIterMC (passCtl pullupResid)+ next n = Iter $ \(Chunk t eof) ->+ case LL.splitAt n t of+ (t1, t2) -> Done (t1, not eof && LL.null t2) (Chunk t2 eof)+ +-- | This inner enumerator is like 'inumNop' in that it passes+-- unmodified 'Chunk's straight through to an iteratee. However, it+-- also logs the 'Chunk's to a file (which can optionally be truncated+-- or appended to, based on the second argument).+inumLog :: (MonadIO m, ChunkData t, LL.ListLikeIO t e) =>+ FilePath -- ^ Path to log to+ -> Bool -- ^ True to truncate file+ -> Inum t t m a+inumLog path trunc = inumBracket openLog (liftIO . hClose) inumhLog+ where openLog = liftIO $ do+ h <- openBinaryFile path (if trunc then WriteMode else AppendMode)+ hSetBuffering h NoBuffering+ return h++-- | Like 'inumLog', but takes a writeable file handle rather than a+-- file name. Does not close the handle when done.+inumhLog :: (MonadIO m, ChunkData t, LL.ListLikeIO t e) =>+ Handle -> Inum t t m a+inumhLog h = mkInumP pullupResid $ do+ buf <- data0I+ unless (null buf) $ liftIO $ LL.hPutStr h buf+ return buf++-- | Log a copy of everything to standard error. (@inumStderr =+-- 'inumhLog' 'stderr'@)+inumStderr :: (MonadIO m, ChunkData t, LL.ListLikeIO t e) =>+ Inum t t m a+inumStderr = inumhLog stderr++-- | An 'Inum' that converts input in the lazy 'L.ByteString' format+-- to strict 'S.ByteString's.+inumLtoS :: (Monad m) => Inum L.ByteString S.ByteString m a+{-# INLINABLE inumLtoS #-}+inumLtoS = mkInumP rh loop+ where rh (a, b) = (L.chunk b a, S.empty)+ loop = iterF $ \c@(Chunk lbs eof) ->+ case lbs of+ L.Chunk bs rest -> Done bs (Chunk rest eof)+ _ -> Done S.empty c++-- | The dual of 'inumLtoS'--converts input from strict+-- 'S.ByteString's to lazy 'L.ByteString's.+inumStoL :: (Monad m) => Inum S.ByteString L.ByteString m a+inumStoL = mkInumP rh loop+ where rh (a, b) = (S.concat (L.toChunks b ++ [a]), L.empty)+ loop = iterF $ \(Chunk bs eof) ->+ Done (L.chunk bs L.Empty) (Chunk S.empty eof)++--+-- Iter-Onum pairs+--++-- | Add a finalizer to run when an 'Iter' has received an EOF and an+-- 'Inum' has finished. This works regardless of the order in which+-- the two events happen.+pairFinalizer :: (ChunkData t, ChunkData t1, ChunkData t2+ , MonadIO m, MonadIO m1) =>+ Iter t m a+ -> Inum t1 t2 m1 b+ -> IO ()+ -- ^ Cleanup action+ -> IO (Iter t m a, Inum t1 t2 m1 b)+ -- ^ Cleanup action will run when these two are both done+pairFinalizer iter inum cleanup = do+ mc <- newMVar False+ let end = modifyMVar mc $ \cleanit ->+ when cleanit cleanup >> return (True, ())+ return (iter `finallyI` liftIO end+ , (inumNull `cat` inum) `inumFinally` liftIO end)++-- | \"Iterizes\" a file 'Handle' by turning into an 'Onum' (for+-- reading) and an 'Iter' (for writing). Uses 'pairFinalizer' to+-- 'hClose' the 'Handle' when both the 'Iter' and 'Onum' are finished.+-- Puts the handle into binary mode, but does not change the+-- buffering.+iterHandle :: (LL.ListLikeIO t e, ChunkData t, MonadIO m) =>+ Handle -> IO (Iter t m (), Onum t m a)+iterHandle h = do+ hSetBinaryMode h True `onException` hClose h+ pairFinalizer (handleI h) (enumNonBinHandle h) (hClose h)++-- | \"Iterizes\" a stream 'Socket' by turning into an 'Onum' (for+-- reading) and an 'Iter' (for writing). Uses 'pairFinalizer' to+-- 'sClose' the 'Socket' when both the 'Iter' and 'Onum' are finished.+iterStream :: (SendRecvString t, ChunkData t, MonadIO m) =>+ Socket -> IO (Iter t m (), Onum t m a)+iterStream s = pairFinalizer (sockStreamI s) (enumStream s) (sClose s)
+ Data/IterIO/Parse.hs view
@@ -0,0 +1,561 @@++-- | This module contains functions to help parsing input from within+-- 'Iter's. Many of the operators are either imported from+-- "Data.Applicative" or inspired by "Text.Parsec".++module Data.IterIO.Parse (-- * Iteratee combinators+ (<|>), (\/), orEmpty, (<?>), expectedI+ , someI, foldrI, foldr1I, foldrMinMaxI+ , foldlI, foldl1I, foldMI, foldM1I+ , skipI, optionalI, ensureI+ , eord+ , skipWhileI, skipWhile1I+ , whileI, while1I, whileMaxI, whileMinMaxI+ , concatI, concat1I, concatMinMaxI+ , readI, eofI+ -- * Applicative combinators+ , (<$>), (<$), ($>), (>$>), Applicative(..), (<**>)+ , (<++>), (<:>), nil+ -- * Parsing Iteratees+ -- $Parseclike+ , many, skipMany, sepBy, endBy, sepEndBy+ , many1, skipMany1, sepBy1, endBy1, sepEndBy1+ , satisfy, char, match, string, stringCase+ ) where++import Prelude hiding (null)+import Control.Applicative (Applicative(..), (<**>), liftA2)+import Control.Monad+import Data.Char+import Data.Functor ((<$>), (<$))+import qualified Data.ListLike as LL+import Data.Monoid++import Data.IterIO.Iter+import Data.IterIO.Inum+import Data.IterIO.ListLike++-- | An infix synonym for 'multiParse' that allows LL(*) parsing of+-- alternatives by executing both Iteratees on input chunks as they+-- arrive. This is similar to the @\<|>@ method of the+-- @'Alternative'@ class in "Control.Applicative", but the+-- @'Alternative'@ operator has left fixity, while for efficiency this+-- one has:+--+-- > infixr 3 <|>+(<|>) :: (ChunkData t, Monad m) =>+ Iter t m a -> Iter t m a -> Iter t m a+{-# INLINE (<|>) #-}+(<|>) = multiParse+infixr 3 <|>++-- | An infix synonym for 'ifNoParse' that allows LL(*) parsing of+-- alternatives by keeping a copy of input data consumed by the first+-- Iteratee so as to backtrack and execute the second Iteratee if the+-- first one fails. Returns a function that takes a continuation for+-- the first 'Iter', should it succeed. The code:+--+-- > iter1 \/ iter2 $ \iter1Result -> doSomethingWith iter1Result+--+-- Executes @iter1@ (saving a copy of the input for backtracking). If+-- @iter1@ fails with an exception of class 'IterNoParse', then the+-- input is re-wound and fed to @iter2@. On the other hand, if+-- @iter1@ succeeds and returns @iter1Result@, then the saved input is+-- discarded (as @iter2@ will not need to be run) and the result of+-- @iter1@ is fed to function @doSomethingWith@.+--+-- For example, to build up a list of results of executing @iter@, one+-- could implement a type-restricted version of 'many' as follows:+--+-- @+-- myMany :: (ChunkData t, Monad m) => Iter t m a -> Iter t m [a]+-- myMany iter = iter \\/ return [] '$' \\r -> 'fmap' ((:) r) (myMany iter)+-- @+--+-- In other words, @myMany@ tries running @iter@. If @iter@ fails,+-- then @myMany@ returns the empty list. If @iter@ succeeds, its+-- result @r@ is added to the head of the list returned by calling+-- @myMany@ recursively. This idiom of partially applying a binary+-- funciton to a result and then applying the resulting function to an+-- 'Iter' via 'fmap' is so common that there is an infix operator for+-- it, @'>$>'@. Thus, the above code can be written:+--+-- @+-- myMany iter = iter \\/ return [] '$' (:) '>$>' myMany iter+-- @+--+-- Of course, using 'fmap' is not the most efficient way to implement+-- @myMany@. If you are going to use this pattern for something+-- performance critical, you should use an accumulator rather than+-- build up long chains of 'fmap's. A faster implementation would be:+--+-- @+-- myMany iter = loop id+-- where loop ac = iter \\/ return (acc []) '$' \a -> loop (acc . (a :))+-- @+--+-- @\\/@ has fixity:+--+-- > infix 2 \/+--+(\/) :: (ChunkData t, Monad m) => + Iter t m a -> Iter t m b -> (a -> Iter t m b) -> Iter t m b+{-# INLINE (\/) #-}+(\/) = ifNoParse+infix 2 \/++-- | @(f >$> a) t@ is equivalent to @f t '<$>' a@ (where '<$>' is and+-- infix alias for 'fmap'). Particularly useful with infix+-- combinators such as '\/' and ``orEmpty`` when chaining parse+-- actions. See examples at '\/' and 'orEmpty'. Note 'fmap' is not+-- always the most efficient solution (see an example in the+-- description of '\/').+--+-- Has fixity:+--+-- > infixl 3 >$>+--+(>$>) :: (Functor f) => (t -> a -> b) -> f a -> t -> f b+{-# INLINE (>$>) #-}+(>$>) f a = \t -> f t <$> a+infixr 3 >$>++-- | @fa $> b = b <$ fa@ -- replaces the output value of a functor+-- with some pure value. Has the same fixity as '<$>' and '<$',+-- namely:+--+-- > infixl 4 $>+($>) :: (Functor f) => f a -> b -> f b+{-# INLINE ($>) #-}+a $> b = b <$ a+infixl 4 $>++-- | Defined as @orEmpty = ('\/' return 'mempty')@, and useful when+-- parse failures should just return an empty 'Monoid'. For example,+-- a type-restricted 'many' can be implemented as:+--+-- @+-- myMany :: (ChunkData t, Monad m) => Iter t m a -> Iter t m [a]+-- myMany iter = iter ``orEmpty`` (:) '>$>' myMany iter+-- @+--+-- Has fixity:+--+-- > infixr 3 `orEmpty`+--+orEmpty :: (ChunkData t, Monad m, Monoid b) =>+ Iter t m a -> (a -> Iter t m b) -> Iter t m b+{-# INLINE orEmpty #-}+orEmpty = (\/ nil)+infixr 3 `orEmpty`++-- | @iter \<?\> token@ replaces any kind of parse failure in @iter@+-- with an exception equivalent to calling @'expectedI' prefix token@+-- where @prefix@ is a prefix of the input that was fed to @iter@ and+-- caused it to fail.+--+-- Has fixity:+--+-- > infix 0 <?>+--+(<?>) :: (ChunkData t, Monad m) => Iter t m a -> String -> Iter t m a+{-# INLINE (<?>) #-}+(<?>) iter expected =+ Iter $ \c -> case runIter iter c of+ r@(Done _ _) -> r+ r@(Fail e _ _) -> case e of+ IterException _ -> r+ _ -> Fail (IterExpected [(show c, expected)])+ Nothing Nothing+ r -> slowPath (show c) expected r+ where+ {-# NOINLINE slowPath #-}+ slowPath saw exp1 = onDoneR $ \r0 ->+ case r0 of+ r@(Fail e _ _) -> case e of+ IterException _ -> r+ _ -> Fail (IterExpected [(saw, exp1)])+ Nothing Nothing+ r -> r+infix 0 <?>+ +-- | Throw an 'Iter' exception that describes expected input not+-- found.+expectedI :: (ChunkData t) =>+ String -- ^ Input actually received+ -> String -- ^ Description of input that was wanted+ -> Iter t m a+expectedI saw target =+ Iter $ \_ -> Fail (IterExpected [(saw, target)]) Nothing Nothing++-- | Takes an 'Iter' returning a 'LL.ListLike' type, executes the+-- 'Iter' once, and throws a parse error if the returned value is+-- 'LL.null'. (Note that this is quite different from the @'some'@+-- method of the @'Alternative'@ class in "Control.Applicative", which+-- executes a computation one /or more/ times. This library does not+-- use @'Alternative'@ because @`Alternative`@'s @\<|\>@ operator has+-- left instead of right fixity.)+someI :: (ChunkData t, Monad m, LL.ListLike a e) => Iter t m a -> Iter t m a+someI iter = (<?> "someI") $ do+ a <- iter+ if LL.null a then mzero else return a++-- | Repeatedly invoke an 'Iter' and right-fold a function over the+-- results.+foldrI :: (ChunkData t, Monad m) =>+ (a -> b -> b) -> b -> Iter t m a -> Iter t m b+foldrI = innerFoldrI id++innerFoldrI :: (ChunkData t, Monad m) =>+ (b -> b) -> (a -> b -> b) -> b -> Iter t m a -> Iter t m b+innerFoldrI acc0 f z iter = loop acc0+ where loop acc = iter \/ return (acc z) $ \a -> loop (acc . f a)++-- | A variant of 'foldrI' that requires the 'Iter' to succeed at+-- least once.+foldr1I :: (ChunkData t, Monad m) =>+ (a -> b -> b) -> b -> Iter t m a -> Iter t m b+foldr1I f z iter = iter >>= \a -> innerFoldrI (f a) f z iter++-- | A variant of 'foldrI' that requires the 'Iter' to succeed at+-- least a minimum number of items and stops parsing after executing+-- the 'Iter' some maximum number of times.+foldrMinMaxI :: (ChunkData t, Monad m) =>+ Int -- ^ Minimum number to parse+ -> Int -- ^ Maximum number to parse+ -> (a -> b -> b) -- ^ Folding function+ -> b -- ^ Rightmost value+ -> Iter t m a -- ^ Iteratee generating items to fold+ -> Iter t m b+foldrMinMaxI nmin0 nmax0 f z iter+ | nmin0 > nmax0 = throwParseI "foldrMinMaxI: min > max"+ | nmax0 < 0 = throwParseI "foldrMinMaxI: negative max"+ | otherwise = loop id nmin0 nmax0+ where+ loop acc nmin nmax+ | nmax == 0 = return $ acc z+ | nmin > 0 = iter >>= \a -> loop (acc . f a) (nmin - 1) (nmax - 1)+ | otherwise = iter \/ return (acc z) $ \a ->+ loop (acc . f a) 0 (nmax - 1)++-- | Strict left fold over an 'Iter' (until it throws an 'IterNoParse'+-- exception). @foldlI f z iter@ is sort of equivalent to:+--+-- > ... (f <$> (f <$> (f z <$> iter) <*> iter) <*> iter) ...+foldlI :: (ChunkData t, Monad m) =>+ (b -> a -> b) -> b -> Iter t m a -> Iter t m b+foldlI f z0 iter = foldNext z0+ where foldNext z = z `seq` iter \/ return z $ \a -> foldNext (f z a)++-- | A version of 'foldlI' that fails if the 'Iter' argument does not+-- succeed at least once.+foldl1I :: (ChunkData t, Monad m) =>+ (b -> a -> b) -> b -> Iter t m a -> Iter t m b+foldl1I f z iter = iter >>= \a -> foldlI f (f z a) iter++-- | @foldMI@ is a left fold in which the folding function can execute+-- monadic actions. Essentially @foldMI@ is to 'foldlI' as 'foldM' is+-- to @`foldl'`@ in the standard libraries.+foldMI :: (ChunkData t, Monad m) =>+ (b -> a -> Iter t m b) -> b -> Iter t m a -> Iter t m b+foldMI f z0 iter = foldNext z0+ where foldNext z = iter \/ return z $ f z >=> foldNext++-- | A variant of 'foldMI' that requires the 'Iter' to succeed at+-- least once.+foldM1I :: (ChunkData t, Monad m) =>+ (b -> a -> Iter t m b) -> b -> Iter t m a -> Iter t m b+foldM1I f z0 iter = iter >>= f z0 >>= \z -> foldMI f z iter+++-- | Discard the result of executing an Iteratee once. Throws an+-- error if the Iteratee fails. (Like @skip x = x >> return ()@.)+skipI :: Applicative f => f a -> f ()+skipI = (() <$)++-- | Execute an iteratee. Discard the result if it succeeds. Rewind+-- the input and suppress the error if it fails.+optionalI :: (ChunkData t, Monad m) => Iter t m a -> Iter t m ()+optionalI iter = ifParse iter (const $ return ()) (return ())++-- | Ensures the next input element satisfies a predicate or throws a+-- parse error. Does not consume any input.+ensureI :: (ChunkData t, LL.ListLike t e, Monad m) =>+ (e -> Bool) -> Iter t m ()+ensureI test =+ Iter $ \c@(Chunk t eof) ->+ if LL.null t+ then (if eof then eofFail else IterF (ensureI test))+ else (if test (LL.head t) then Done () c else testFail)+ where testFail = Fail (IterParseErr "ensureI test failed") Nothing Nothing+ eofFail = Fail (mkIterEOF "ensureI EOF") Nothing Nothing++-- | A variant of the standard library 'ord' function, but that+-- translates a 'Char' into any 'Enum' type, not just 'Int'.+-- Particularly useful for 'Iter's that must work with both 'String's+-- (which consist of 'Char's) and ASCII @'ByteString'@s (which consist+-- of @'Word8'@s). For example, to skip one or more space or TAB+-- characters, you can use:+--+-- @+-- skipSpace :: ('LL.ListLike' t e, ChunkData t, 'Eq' e, 'Enum' e, Monad m) =>+-- 'Iter' t m ()+-- skipSpace = 'skipWhile1I' (\\c -> c == eord ' ' || c == eord '\t')+-- @+eord :: (Enum e) => Char -> e+{-# INLINE eord #-}+eord = toEnum . ord++-- | Skip all input elements encountered until an element is found+-- that does not match the specified predicate.+skipWhileI :: (ChunkData t, LL.ListLike t e, Monad m) =>+ (e -> Bool) -> Iter t m ()+skipWhileI test = loop+ where loop = Iter $ \(Chunk t eof) ->+ case LL.dropWhile test t of+ t1 | LL.null t1 && not eof -> IterF loop+ t1 -> Done () $ Chunk t1 eof++-- | Like 'skipWhileI', but fails if at least one element does not+-- satisfy the predicate.+skipWhile1I :: (ChunkData t, LL.ListLike t e, Monad m) =>+ (e -> Bool) -> Iter t m ()+skipWhile1I test = ensureI test >> skipWhileI test <?> "skipWhile1I"++-- | Return all input elements up to the first one that does not match+-- the specified predicate.+whileI :: (ChunkData t, LL.ListLike t e, Monad m)+ => (e -> Bool) -> Iter t m t+whileI test = more id+ where+ more acc = Iter $ \(Chunk t eof) ->+ case LL.span test t of+ (t1, t2) | not (LL.null t2) || eof ->+ Done (acc t1) $ Chunk t2 eof+ (t1, _) -> IterF $ more (acc . LL.append t1)++-- | Like 'whileI', but fails if at least one element does not satisfy+-- the predicate.+while1I :: (ChunkData t, LL.ListLike t e, Monad m) =>+ (e -> Bool) -> Iter t m t+while1I test = ensureI test >> whileI test++-- | A variant of 'whileI' with a maximum number matches.+whileMaxI :: (ChunkData t, LL.ListLike t e, Monad m) =>+ Int -- ^ Maximum number to match+ -> (e -> Bool) -- ^ Predicate test+ -> Iter t m t+whileMaxI nmax test = inumMax nmax .| whileI test++-- | A variant of 'whileI' with a minimum and maximum number matches.+whileMinMaxI :: (ChunkData t, LL.ListLike t e, Monad m) =>+ Int -- ^ Minumum number+ -> Int -- ^ Maximum number+ -> (e -> Bool) -- ^ Predicate test+ -> Iter t m t+whileMinMaxI nmin nmax test = do+ result <- whileMaxI nmax test+ if LL.length result >= nmin+ then return result+ else expectedI "too few" "whileMinMaxI minimum"++-- | Repeatedly execute an 'Iter' returning a 'Monoid' and 'mappend'+-- all the results in a right fold.+concatI :: (ChunkData t, Monoid s, Monad m) =>+ Iter t m s -> Iter t m s+concatI iter = foldrI mappend mempty iter++-- | Like 'concatI', but fails if the 'Iter' doesn't return at least+-- once.+concat1I :: (ChunkData t, Monoid s, Monad m) =>+ Iter t m s -> Iter t m s+concat1I iter = foldr1I mappend mempty iter++-- | A version of 'concatI' that takes a minimum and maximum number of+-- items to parse.+concatMinMaxI :: (ChunkData t, Monoid s, Monad m) =>+ Int -- ^ Minimum number to parse+ -> Int -- ^ Maximum number to parse+ -> Iter t m s -- ^ 'Iter' whose results to concatenate+ -> Iter t m s+concatMinMaxI nmin nmax iter = foldrMinMaxI nmin nmax mappend mempty iter+ + +-- | This 'Iter' parses a 'LL.StringLike' argument. It does not+-- consume any Iteratee input. The only reason it is an Iteratee is+-- so that it can throw an Iteratee parse error should it fail to+-- parse the argument string (or should the argument yield an+-- ambiguous parse).+readI :: (ChunkData t, Monad m, LL.StringLike s, Read a) => + s -> Iter t m a+readI s' = let s = LL.toString s'+ in case [a | (a,"") <- reads s] of+ [a] -> return a+ [] -> throwParseI $ "readI can't parse: " ++ s+ _ -> throwParseI $ "readI ambiguous: " ++ s++-- | Ensures the input is at the end-of-file marker, or else throws an+-- exception.+eofI :: (ChunkData t, Monad m, Show t) => Iter t m ()+eofI = do+ Chunk t eof <- iterF $ \c -> Done c c+ if eof && null t+ then return ()+ else expectedI (chunkShow t) "EOF"++-- | 'mappend' the result of two 'Applicative' types returning+-- 'Monoid' types (@\<++> = 'liftA2' 'mappend'@). Has the same fixity+-- as '++', namely:+--+-- > infixr 5 <++>+(<++>) :: (Applicative f, Monoid t) => f t -> f t -> f t+(<++>) = liftA2 mappend+infixr 5 <++>++-- | 'LL.cons' an 'Applicative' type onto an an 'Applicative'+-- 'LL.ListLike' type (@\<:> = 'liftA2' 'LL.cons'@). Has the same+-- fixity as @:@, namely:+--+-- > infixr 5 <:>+(<:>) :: (LL.ListLike t e, Applicative f) => f e -> f t -> f t+{-# INLINE (<:>) #-}+(<:>) = liftA2 LL.cons+infixr 5 <:>++-- | @nil = 'pure' 'mempty'@--An empty 'Monoid' injected into an+-- 'Applicative' type.+nil :: (Applicative f, Monoid t) => f t+{-# INLINE nil #-}+nil = pure mempty++-- $Parseclike+--+-- These functions are intended to be similar to those supplied by+-- "Text.Parsec".++-- | Run an 'Iter' zero or more times (until it fails) and return a+-- list-like container of the results.+many :: (ChunkData t, LL.ListLike f a, Monad m) => Iter t m a -> Iter t m f+{-# INLINE many #-}+many = foldrI LL.cons LL.empty++-- | Repeatedly run an 'Iter' until it fails and discard all the+-- results.+skipMany :: (ChunkData t, Monad m) => Iter t m a -> Iter t m ()+skipMany = foldlI (\_ _ -> ()) ()++-- | Parses a sequence of the form+-- /Item1 Separator Item2 Separator ... Separator ItemN/+-- and returns the list @[@/Item1/@,@ /Item2/@,@ ...@,@ /ItemN/@]@+-- or a 'LL.ListLike' equivalent.+sepBy :: (ChunkData t, LL.ListLike f a, Monad m) =>+ Iter t m a -- ^ Item to parse+ -> Iter t m b -- ^ Separator between items+ -> Iter t m f -- ^ Returns 'LL.ListLike' list of items+sepBy item sep = item `orEmpty` \a ->+ innerFoldrI (LL.cons a) LL.cons LL.empty (sep *> item)++-- | Like 'sepBy', but expects a separator after the final item. In+-- other words, parses a sequence of the form+-- /Item1 Separator Item2 Separator ... Separator ItemN Separator/+-- and returns the list @[@/Item1/@,@ /Item2/@,@ ...@,@ /ItemN/@]@ or+-- a 'LL.ListLike' equivalent.+endBy :: (ChunkData t, LL.ListLike f a, Monad m) =>+ Iter t m a -- ^ Item to parse+ -> Iter t m b -- ^ Separator that must follow each item+ -> Iter t m f -- ^ Returns 'LL.ListLike' list of items+endBy item sep = foldrI LL.cons LL.empty (item <* sep)++-- | Accepts items that would be parsed by either 'sepBy' or 'endBy'.+-- Essentially a version of 'endBy' in which the final separator is+-- optional.+sepEndBy :: (ChunkData t, LL.ListLike f a, Monad m) =>+ Iter t m a -> Iter t m b -> Iter t m f+sepEndBy item sep = sepBy item sep <* optionalI sep+++-- | Run an 'Iter' one or more times (until it fails) and return a+-- list-like container of the results.+many1 :: (ChunkData t, LL.ListLike f a, Monad m) => Iter t m a -> Iter t m f+many1 = foldr1I LL.cons LL.empty++-- | A variant of 'skipMany' that throws a parse error if the 'Iter'+-- does not succeed at least once.+skipMany1 :: (ChunkData t, Monad m) => Iter t m a -> Iter t m ()+skipMany1 = foldl1I (\_ _ -> ()) ()++-- | A variant of 'sepBy' that throws a parse error if it cannot+-- return at least one item.+sepBy1 :: (ChunkData t, LL.ListLike f a, Monad m) =>+ Iter t m a -> Iter t m b -> Iter t m f+sepBy1 item sep = item >>= \a ->+ innerFoldrI (LL.cons a) LL.cons LL.empty (sep *> item)++-- | A variant of 'endBy' that throws a parse error if it cannot+-- return at least one item.+endBy1 :: (ChunkData t, LL.ListLike f a, Monad m) =>+ Iter t m a -> Iter t m b -> Iter t m f+endBy1 item sep = foldr1I LL.cons LL.empty (item <* sep)++-- | A variant of 'sepEndBy' that throws a parse error if it cannot+-- return at least one item.+sepEndBy1 :: (ChunkData t, LL.ListLike f a, Monad m) =>+ Iter t m a -> Iter t m b -> Iter t m f+sepEndBy1 item sep = sepBy1 item sep <* optionalI sep++ +-- | Read the next input element if it satisfies some predicate.+-- Otherwise throw an error.+satisfy :: (ChunkData t, LL.ListLike t e, Enum e, Monad m) =>+ (e -> Bool) -> Iter t m e+satisfy test =+ Iter $ \c@(Chunk t eof) ->+ if LL.null t+ then (if eof then eofFail else IterF (satisfy test))+ else case LL.head t of+ h | test h -> + Done h (Chunk (LL.tail t) eof)+ | otherwise ->+ Fail (IterExpected [(show $ chr $ fromEnum h+ , "satisfy predicate")])+ Nothing (Just c)+ where eofFail = Fail (mkIterEOF "satisfy: EOF") Nothing Nothing++-- | Read input that exactly matches a character.+char :: (ChunkData t, LL.ListLike t e, Eq e, Enum e, Monad m) =>+ Char -> Iter t m e+{-# INLINE char #-}+char target = satisfy (eord target ==) <?> show target++-- | Read input that exactly matches some target.+match :: (ChunkData t, LL.ListLike t e, Eq e, Monad m) =>+ t -> Iter t m t+match ft = doMatch ft+ where doMatch target | LL.null target = return ft+ | otherwise = do+ m <- data0MaxI $ LL.length target+ if not (LL.null m) && LL.isPrefixOf m target+ then doMatch $ LL.drop (LL.length m) target+ else expectedI (chunkShow m) $ chunkShow target++-- | Read input that exactly matches a string.+string :: (ChunkData t, LL.ListLike t e, LL.StringLike t, Eq e, Monad m) =>+ String -> Iter t m t+{-# INLINE string #-}+string = match . LL.fromString++-- | Read input that matches a string up to case.+stringCase :: (ChunkData t, LL.ListLike t e, Enum e, Eq e, Monad m) =>+ String -> Iter t m t+stringCase ft = doMatch LL.empty $ ft+ where+ prefix a b | LL.null a = True+ | otherwise =+ if toLower (chr $ fromEnum $ LL.head a) /= toLower (head b)+ then False else LL.tail a `prefix` LL.tail b+ doMatch acc target | LL.null target = return acc+ | otherwise = do+ m <- data0MaxI $ LL.length target+ if not (LL.null m) && m `prefix` target+ then doMatch (LL.append acc m) $ LL.drop (LL.length m) target+ else expectedI (chunkShow m) $ chunkShow target
+ Data/IterIO/SSL.hs view
@@ -0,0 +1,100 @@+{-# LANGUAGE MultiParamTypeClasses #-}+{-# LANGUAGE DeriveDataTypeable #-}++module Data.IterIO.SSL where++import Control.Exception (throwIO, ErrorCall(..), finally, onException)+import Control.Monad+import Control.Monad.Trans+import Data.ByteString as S+import qualified Data.ByteString.Lazy as L+import Data.ByteString.Lazy.Internal as L (defaultChunkSize)+import Data.Typeable+import qualified Network.Socket as Net+import qualified OpenSSL.Session as SSL+import System.Cmd+import System.Exit++import Data.IterIO.Iter+import Data.IterIO.Inum+import Data.IterIO.ListLike++-- | A wrapper around the type 'SSL.SSL' to make it an instance of the+-- 'Typeable' class.+newtype SslConnection = SslConnection { unSslConnection :: SSL.SSL }+ deriving (Typeable)++-- | Control request to fetch the 'SSL' object associated with an+-- enumerator.+data SslC = SslC deriving (Typeable)+instance CtlCmd SslC SslConnection++-- | Simple OpenSSL 'Onum'.+enumSsl :: (MonadIO m) => SSL.SSL -> Onum L.ByteString m a+enumSsl ssl = mkInumC id ch codec+ where ch = mkCtl (\SslC -> return $ SslConnection ssl)+ `consCtl` (socketCtl $ SSL.sslSocket ssl)+ codec = do buf <- liftIO (SSL.read ssl L.defaultChunkSize)+ if S.null buf+ then return L.empty+ else return $ L.fromChunks [buf]++-- | Simple OpenSSL 'Iter'. Does a uni-directional SSL shutdown when+-- it receives a 'Chunk' with the EOF bit 'True'.+sslI :: (MonadIO m) => SSL.SSL -> Iter L.ByteString m ()+sslI ssl = loop+ where loop = do+ Chunk t eof <- chunkI+ unless (L.null t) $ liftIO $ SSL.lazyWrite ssl t+ if eof then liftIO $ SSL.shutdown ssl SSL.Unidirectional else loop++-- | Turn a socket into an 'Iter' and 'Onum' that use OpenSSL to write+-- to and read from the socket, respectively. Does an SSL+-- bi-directional shutdown and closes the socket when both a) the enum+-- completes and b) the iter has received an EOF chunk.+--+-- If the SSL handshake fails, then @iterSSL@ closes the socket before+-- throwing an exception.+--+-- This funciton must only be invoked from within a call to+-- @withOpenSSL@.+iterSSL :: (MonadIO m) =>+ SSL.SSLContext+ -- ^ OpenSSL context+ -> Net.Socket+ -- ^ The socket+ -> Bool+ -- ^ 'True' for server handshake, 'False' for client+ -> IO (Iter L.ByteString m (), Onum L.ByteString m a)+iterSSL ctx sock server = do+ ssl <- SSL.connection ctx sock `onException` Net.sClose sock+ (if server then SSL.accept ssl else SSL.connect ssl)+ `onException` Net.sClose sock+ liftIO $ pairFinalizer (sslI ssl) (enumSsl ssl) $+ SSL.shutdown ssl SSL.Bidirectional `finally` Net.sClose sock++-- | Simplest possible SSL context, loads cert and unencrypted private+-- key from a single file.+simpleContext :: FilePath -> IO SSL.SSLContext+simpleContext keyfile = do+ ctx <- SSL.context+ SSL.contextSetDefaultCiphers ctx+ SSL.contextSetCertificateFile ctx keyfile+ SSL.contextSetPrivateKeyFile ctx keyfile+ -- SSL.contextSetVerificationMode ctx $ SSL.VerifyPeer False True+ SSL.contextSetVerificationMode ctx SSL.VerifyNone+ return ctx++-- | Quick and dirty funciton to generate a self signed certificate+-- for testing and stick it in a file. E.g.:+--+-- > genSelfSigned "testkey.pem" "localhost"+genSelfSigned :: FilePath -- ^ Filename in which to output key+ -> String -- ^ Common Name (usually domain name)+ -> IO ()+genSelfSigned file cn = do+ r <- rawSystem "openssl"+ [ "req", "-x509", "-nodes", "-days", "365", "-subj", "/CN=" ++ cn+ , "-newkey", "rsa:1024", "-keyout", file, "-out", file+ ]+ when (r /= ExitSuccess) $ throwIO $ ErrorCall "openssl failed"
+ Data/IterIO/Search.hs view
@@ -0,0 +1,114 @@++module Data.IterIO.Search (inumStopString+ , mapI, mapLI+ ) where++import qualified Data.ByteString.Char8 as S8+import qualified Data.ByteString.Lazy.Char8 as L8+import qualified Data.ByteString.Lazy.Search as Search+import qualified Data.ListLike as LL+import Data.Map (Map)+import qualified Data.Map as Map+import Data.Monoid++import Data.IterIO.Iter+import Data.IterIO.Inum++-- | Feeds input to an Iteratee until some boundary string is found.+-- The boundary string is neither consumed nor passed through to the+-- target 'Iter'. (Thus, if the input is at end-of-file after+-- inumStopString returns, it means the boundary string was never+-- encountered.)+inumStopString :: (Monad m) =>+ S8.ByteString+ -> Inum L8.ByteString L8.ByteString m a+inumStopString spat = mkInumM $ nextChunk L8.empty+ where+ lpat = L8.fromChunks [spat]+ plen = toEnum $ S8.length spat+ search = Search.breakOn spat+ nextChunk old = do+ (Chunk t eof) <- chunkI+ case search $ L8.append old t of+ (a, b) | not (L8.null b) -> ungetI b >> ifeed a+ (a, _) | eof -> ifeed a+ (a, _) -> checkEnd a+ checkEnd t = let tlen = L8.length t+ hlen = max 0 (tlen - plen - 1)+ ttail = L8.drop hlen t+ fpm = firstPossibleMatch 0 ttail+ rlen = hlen + fpm+ in if rlen == tlen+ then ifeed t >> nextChunk L8.empty+ else case L8.splitAt rlen t of+ (r, o) -> ifeed r >> nextChunk o+ firstPossibleMatch n t =+ if t `L8.isPrefixOf` lpat+ then n+ else firstPossibleMatch (n + 1) (L8.tail t)++longestCommonPrefix :: (LL.ListLike t e, Eq e) => t -> t -> t+longestCommonPrefix a0 = cmp 0 a0+ where+ cmp n a b | LL.null a || LL.null b = LL.take n a0+ cmp n a b | LL.head a == LL.head b = cmp (n + 1) (LL.tail a) (LL.tail b)+ cmp n _ _ = LL.take n a0++findLongestPrefix :: (LL.ListLike t e, Ord t, Eq e) =>+ Map t a -> t -> Maybe (t, a)+findLongestPrefix mp t = maybe ckprefix (\v1 -> Just (t, v1)) ma+ where+ (ltmap, ma, _) = Map.splitLookup t mp+ (k, v) = Map.findMax ltmap+ p = longestCommonPrefix k t+ ckprefix | Map.null mp || LL.null t = Nothing+ | k `LL.isPrefixOf` t = Just (k, v)+ | otherwise = findLongestPrefix ltmap p++-- | Reads input until it can uniquely determine the longest key in a+-- 'Map.Map' that is a prefix of the input. Consumes the input that+-- matches the key, and returns the corresponding value in the+-- 'Map.Map', along with the residual input that follows the key.+mapI :: (ChunkData t, LL.ListLike t e, Ord t, Eq e, Monad m) =>+ Map t a -> Iter t m a+mapI mp | Map.null mp = fail $ "mapI: null map"+ | otherwise = do+ c@(Chunk t eof) <- chunkI+ if not (eof) && more t+ then iterF (runIter (mapI mp) . mappend c)+ else case findLongestPrefix mp t of+ Nothing -> Iter $ \c' ->+ Fail (IterExpected $+ (show c+ , show (Map.size mp) ++ " keys including the following:")+ : map (\k -> ("", chunkShow k)) (take 5 $ Map.keys mp))+ Nothing (Just $ mappend c c')+ Just (k, v) -> ungetI (LL.drop (LL.length k) t) >> return v+ where+ gtmap t = snd $ Map.split t mp+ more t | Map.null $ gtmap t = False+ | otherwise = t `LL.isPrefixOf` (fst $ Map.findMin $ gtmap t)++-- | @mapLI@ is a variant of 'mapI' that takes a list of+-- @(key, value)@ pairs instead of a 'Map.Map'.+-- @mapLI = 'mapI' . 'Map.fromList'@.+mapLI :: (ChunkData t, LL.ListLike t e, Ord t, Eq e, Monad m) =>+ [(t, a)] -> Iter t m a+mapLI = mapI . Map.fromList+++++{-+main :: IO ()+main = enumStdin |$ do+ inumStopString end .| stdoutI+ match end+ liftIO $ putStrLn "\n\n*** We have reached THE END #1 ***\n\n"+ inumStopString end .| stdoutI+ match end+ liftIO $ putStrLn "\n\n*** We have reached THE END #2 ***\n\n"+ stdoutI+ where+ end = L8.pack "TheEnd"+-}
+ Data/IterIO/Trans.hs view
@@ -0,0 +1,394 @@+{-# LANGUAGE MultiParamTypeClasses #-}+{-# LANGUAGE FunctionalDependencies #-}++-- These extensions are only for MTL stuff where it is required+{-# LANGUAGE FlexibleInstances #-}+{-# LANGUAGE UndecidableInstances #-}+{-# OPTIONS_GHC -fno-warn-orphans #-}++-- | This module contains various helper functions and instances for+-- using 'Iter's of different 'Monad's together in the same pipeline.+-- For example, as-is the following code is illegal:+--+-- @+--iter1 :: 'Iter' String IO Bool+--iter1 = ...+-- +--iter2 :: 'Iter' String ('StateT' MyState IO) ()+--iter2 = do ...+-- s <- iter1 -- ILLEGAL: iter1 is in wrong monad+-- ...+-- @+--+-- You can't invoke @iter1@ from within @iter2@ because the 'Iter'+-- type is wrapped around a different 'Monad' in each case. However,+-- the function 'liftI' exactly solves this problem:+--+-- @+-- s <- liftI iter1+-- @+--+-- Conversely, you may be in a 'Monad' like @'Iter' String IO@ and+-- need to invoke a computation that requires some other monad+-- functionality, such as a reader. There are a number of+-- iteratee-specific runner functions that help you run other+-- 'MonadTrans' transformers inside the 'Iter' monad. These typically+-- use the names of the runner functions in the mtl library, but with+-- an @I@ appended--for instance 'runReaderTI', 'runStateTI',+-- 'runWriterTI'. Here's a fuller example of adapting the inner+-- 'Iter' 'Monad'. The example also illustrates that @'Iter' t m@ is+-- member any mtl classes (such as 'MonadReader' and 'MonadState')+-- that @m@ is.+--+-- @+--iter1 :: Iter String ('ReaderT' MyState IO) Bool+--iter1 = do+-- s <- 'ask'+-- liftIO $ ('putStrLn' ('show' s) >> return True)+-- ``catch`` \('SomeException' _) -> return False+--+--iter2 :: Iter String ('StateT' MyState IO) ()+--iter2 = do+-- s <- 'get'+-- ok <- 'liftI' $ 'runReaderTI' iter1 s+-- if ok then return () else fail \"iter1 failed\"+-- @+module Data.IterIO.Trans (-- * Adapters for Iters of mtl transformers+ liftI, liftIterIO+ , runContTI, runErrorTI, runListTI, runReaderTI+ , runRWSI, runRWSLI, runStateTI, runStateTLI+ , runWriterTI, runWriterTLI+ -- * Functions for building new monad adapters+ , adaptIter, adaptIterM+ -- * Iter-specific state monad transformer+ , IterStateT(..), runIterStateT+ , iget, igets, iput, imodify+ ) where++import Control.Monad.Cont+import Control.Monad.Error+import Control.Monad.List+import Control.Monad.Reader+import Control.Monad.RWS.Strict+import Control.Monad.State.Strict+import Control.Monad.Writer.Strict+import qualified Control.Monad.RWS.Lazy as Lazy+import qualified Control.Monad.State.Lazy as Lazy+import qualified Control.Monad.Writer.Lazy as Lazy+import Control.Monad+import Control.Monad.Trans++import Data.IterIO.Iter++--+-- IterStateT monad+--++-- | @IterStateT@ is a variant of the 'StateT' monad transformer+-- specifically designed for use inside 'Iter's. The 'IterStateT'+-- Monad itself is the same as 'StateT'. However, the 'runIterStateT'+-- function works differently from 'runStateT'--it returns an 'IterR'+-- and the result state separately. The advantage of this approach is+-- that you can still recover the state at the point of the excaption+-- even after an 'IterFail' or 'InumFail' condition.+newtype IterStateT s m a = IterStateT (s -> m (a, s))++instance (Monad m) => Monad (IterStateT s m) where+ return a = IterStateT $ \s -> return (a, s)+ (IterStateT mf) >>= k = IterStateT $ \s -> do (a, s') <- mf s+ let (IterStateT kf) = k a+ kf $! s'+ fail = IterStateT . const . fail++instance MonadTrans (IterStateT s) where+ lift m = IterStateT $ \s -> m >>= \a -> return (a, s)++instance (MonadIO m) => MonadIO (IterStateT s m) where+ liftIO = lift . liftIO++-- | Runs an @'IterStateT' s m@ computation on some state @s@.+-- Returns the result ('IterR') of the 'Iter' and the state of @s@ as+-- a pair. Pulls residual input up to the enclosing 'Iter' monad (as+-- with @'pullupResid'@ in "Data.IterIO.Inum").+runIterStateT :: (ChunkData t, Monad m) => + Iter t (IterStateT s m) a -> s -> Iter t m (IterR t m a, s)+runIterStateT i0 s0 = Iter $ adapt s0 . runIter i0+ where adapt s (IterM (IterStateT f)) =+ IterM $ liftM (uncurry $ flip adapt) (f s)+ adapt s r =+ stepR' r (adapt s) $ Done (setResid r mempty, s) (getResid r)+ +-- | Returns the state in an @'Iter' t ('IterStateT' s m)@ monad.+-- Analogous to @'get'@ for a @'StateT' s m@ monad.+iget :: (Monad m) => Iter t (IterStateT s m) s+iget = lift $ IterStateT $ \s -> return (s, s)++-- | Returns a particular field of the 'IterStateT' state, analogous+-- to @'gets'@ for @'StateT'@.+igets :: (Monad m) => (s -> a) -> Iter t (IterStateT s m) a+igets f = liftM f iget++-- | Sets the 'IterStateT' state. Analogous to @'put'@ for+-- @'StateT'@.+iput :: (Monad m) => s -> Iter t (IterStateT s m) ()+iput s = lift $ IterStateT $ \_ -> return ((), s)++-- | Modifies the 'IterStateT' state. Analogous to @'modify'@ for+-- @'StateT'@.+imodify :: (Monad m) => (s -> s) -> Iter t (IterStateT s m) ()+imodify f = lift $ IterStateT $ \s -> return ((), f s)++--+-- Adapter utility functions+--++-- | Adapt an 'Iter' from one monad to another. This function is the+-- lowest-level monad adapter function, upon which all of the other+-- adapters are built. @adaptIter@ requires two functions as+-- arguments. One adapts the result to a new type (if required). The+-- second adapts monadic computations from one monad to the other.+-- For example, 'liftI' could be implemented as:+--+-- @+-- liftI :: ('MonadTrans' t, Monad m, Monad (t m), 'ChunkData' s) =>+-- 'Iter' s m a -> 'Iter' s (t m) a+-- liftI = adaptIter 'id' (\\m -> 'lift' ('lift' m) >>= liftI)+-- @+--+-- Here @'lift' ('lift' m)@ executes a computation @m@ of type @m+-- ('Iter' s m a)@ from within the @'Iter' s (t m)@ monad. The+-- result, of type @'Iter' s m a@, can then be fed back into+-- @liftI@ recursively.+--+-- Note that in general a computation adapters must invoke the outer+-- adapter function recursively. @adaptIter@ is designed this way+-- because the result adapter function may need to change. An example+-- is 'runStateTI', which could be implemented as follows:+--+-- > runStateTI :: (ChunkData t, Monad m) =>+-- > Iter t (StateT s m) a -> s -> Iter t m (a, s)+-- > runStateTI iter s = adaptIter adaptResult adaptComputation iter+-- > where adaptResult a = (a, s)+-- > adaptComputation m = do (r', s') <- lift (runStateT m s)+-- > runStateTI r' s'+--+-- Here, after executing 'runStateT', the state may be modified.+-- Thus, @adaptComputation@ invokes @runStateTI@ recursively with the+-- modified state, @s'@, to ensure that subsequent 'IterM'+-- computations will be run on the latest state, and that eventually+-- @adaptResult@ will pair the result @a@ with the newest state.+adaptIter :: (ChunkData t, Monad m1) =>+ (a -> b) -- ^ How to adapt result values+ -> (m1 (Iter t m1 a) -> Iter t m2 b) -- ^ How to adapt computations+ -> Iter t m1 a -- ^ Input computation+ -> Iter t m2 b -- ^ Output computation+adaptIter f mf i = Iter $ check . runIter i+ where check (IterM m) = runIter (mf $ liftM (Iter . runIterR) m) mempty+ check r = stepR' r check $ fmapR f r++-- | Simplified adapter function to translate 'Iter' computations from+-- one monad to another. This only works on monads @m@ for which+-- running @m a@ returns a result of type @a@. For more complex+-- scenarios (such as 'ListT' or 'StateT'), you need to use the more+-- general 'adaptIter'.+--+-- As an example, the 'liftIterIO' function is implemented as follows:+--+-- @+-- liftIterIO :: (ChunkData t, 'MonadIO' m) => Iter t IO a -> Iter t m a+-- liftIterIO = adaptIterM 'liftIO'+-- @+adaptIterM :: (ChunkData t, Monad m1, Monad m2) =>+ (m1 (Iter t m1 a) -> m2 (Iter t m1 a)) -- ^ Conversion function+ -> Iter t m1 a -- ^ 'Iter' of input monad+ -> Iter t m2 a -- ^ Returns 'Iter' of output monad+adaptIterM f = adapt+ where adapt = adaptIter id $ lift . f >=> adapt++-- | Run an @'Iter' s m@ computation from witin the @'Iter' s (t m)@+-- monad, where @t@ is a 'MonadTrans'.+liftI :: (MonadTrans t, Monad m, Monad (t m), ChunkData s) =>+ Iter s m a -> Iter s (t m) a+liftI = adaptIterM lift++-- | Run an @'Iter' t IO@ computation from within an @'Iter' t m@+-- monad where @m@ is in class 'MonadIO'.+liftIterIO :: (ChunkData t, MonadIO m) =>+ Iter t IO a -> Iter t m a+liftIterIO = adaptIterM liftIO++--+-- mtl runner functions+--++-- | The type signature says it all. Just a slightly optimized+-- version of @joinlift = join . lift@.+joinlift :: (Monad m) => m (Iter t m a) -> Iter t m a+joinlift m = Iter $ \c -> IterM $ m >>= \i -> return $ runIter i c++-- | Turn a computation of type @'Iter' t ('ContT' ('Iter' t m a) m)+-- a@ into one of type @'Iter' t m a@. Note the continuation has to+-- return type @'Iter' t m a@ and not @a@ so that runContTI can call+-- itself recursively.+runContTI :: (ChunkData t, Monad m) =>+ Iter t (ContT (Iter t m a) m) a -> Iter t m a+runContTI = adaptIter id adapt+ where adapt m = joinlift $ runContT m $ return . runContTI+-- adapt :: ContT (Iter t m a) m (Iter t (ContT (Iter t m a) m) a)+-- -> Iter t m a++-- | Run a computation of type @'Iter' t ('ErrorT' e m)@ from within+-- the @'Iter' t m@ monad. This function is here for completeness,+-- but please consider using 'throwI' instead, since the 'Iter' monad+-- already has built-in exception handling and it's best to have a+-- single, uniform approach to error reporting.+runErrorTI :: (Monad m, ChunkData t, Error e) =>+ Iter t (ErrorT e m) a -> Iter t m (Either e a)+runErrorTI = adaptIter Right $ lift . runErrorT >=> next+ where next (Left e) = return $ Left e+ next (Right iter) = runErrorTI iter++-- | Run an @'Iter' t ('ListT' m)@ computation from within the @'Iter'+-- t m@ monad.+runListTI :: (Monad m, ChunkData t) =>+ Iter t (ListT m) a -> Iter t m [a]+runListTI = adaptIter (: []) $+ lift . runListT >=> liftM concat . runListTI . sequence++-- | Run an @'Iter' t ('ReaderT' r m)@ computation from within the+-- @'Iter' t m@ monad.+runReaderTI :: (ChunkData t, Monad m) =>+ Iter t (ReaderT r m) a -> r -> Iter t m a+runReaderTI m r = adaptIterM (flip runReaderT r) m++-- | Run an @'Iter' t ('RWST' r w s m)@ computation from within the+-- @'Iter' t m@ monad.+runRWSI :: (ChunkData t, Monoid w, Monad m) =>+ Iter t (RWST r w s m) a -- ^ Computation to transform+ -> r -- ^ Reader State+ -> s -- ^ Mutable State+ -> Iter t m (a, s, w) -- ^ Returns result, mutable state, writer+runRWSI iter0 r s0 = doRWS mempty s0 iter0+ where doRWS w s = adaptIter (\a -> (a, s, w)) $ \m -> do+ (iter, s', w') <- lift $ runRWST m r s+ doRWS (mappend w w') s' iter+ +-- | Run an @'Iter' t ('Lazy.RWST' r w s m)@ computation from within+-- the @'Iter' t m@ monad. Just like 'runRWSI', execpt this function+-- is for /Lazy/ 'Lazy.RWST' rather than strict 'RWST'.+runRWSLI :: (ChunkData t, Monoid w, Monad m) =>+ Iter t (Lazy.RWST r w s m) a+ -- ^ Computation to transform+ -> r -- ^ Reader State+ -> s -- ^ Mutable State+ -> Iter t m (a, s, w) -- ^ Returns result, mutable state, writer+runRWSLI iter0 r s0 = doRWS mempty s0 iter0+ where doRWS w s = adaptIter (\a -> (a, s, w)) $ \m -> do+ (iter, s', w') <- lift $ Lazy.runRWST m r s+ doRWS (mappend w w') s' iter++-- | Run an @'Iter' t ('StateT' m)@ computation from within the+-- @'Iter' t m@ monad.+runStateTI :: (ChunkData t, Monad m) =>+ Iter t (StateT s m) a -> s -> Iter t m (a, s)+runStateTI iter0 s0 = adaptIter (\a -> (a, s0)) adapt iter0+ where adapt m = lift (runStateT m s0) >>= uncurry runStateTI++-- | Run an @'Iter' t ('Lazy.StateT' m)@ computation from within the+-- @'Iter' t m@ monad. Just like 'runStateTI', except this function+-- works on /Lazy/ 'Lazy.StateT' rather than strict 'StateT'.+runStateTLI :: (ChunkData t, Monad m) =>+ Iter t (Lazy.StateT s m) a -> s -> Iter t m (a, s)+runStateTLI iter0 s0 = adaptIter (\a -> (a, s0)) adapt iter0+ where adapt m = lift (Lazy.runStateT m s0) >>= uncurry runStateTLI++-- | Run an @'Iter' t ('WriterT' w m)@ computation from within the+-- @'Iter' t m@ monad.+runWriterTI :: (ChunkData t, Monoid w, Monad m) =>+ Iter t (WriterT w m) a -> Iter t m (a, w)+runWriterTI = doW mempty+ where doW w = adaptIter (\a -> (a, w)) $+ lift . runWriterT >=> \(iter, w') -> doW (mappend w w') iter++-- | Run an @'Iter' t ('Lazy.WriterT' w m)@ computation from within+-- the @'Iter' t m@ monad. This is the same as 'runWriterT' but for+-- the /Lazy/ 'Lazy.WriterT', rather than the strict one.+runWriterTLI :: (ChunkData t, Monoid w, Monad m) =>+ Iter t (Lazy.WriterT w m) a -> Iter t m (a, w)+runWriterTLI = doW mempty+ where doW w = adaptIter (\a -> (a, w)) $+ lift . Lazy.runWriterT >=> \(iter, w') ->+ doW (mappend w w') iter++--+-- Below this line, we use FlexibleInstances and UndecidableInstances,+-- but only because this is required by mtl.+--++instance (ChunkData t, MonadCont m) => MonadCont (Iter t m) where+ callCC f = joinlift $ (callCC $ \cc -> return $ f (icont cc))+ where icont cc a = Iter $ \c -> IterM $ cc (Iter $ \_ -> Done a c)++instance (Error e, MonadError e m, ChunkData t) =>+ MonadError e (Iter t m) where+ throwError = lift . throwError+ catchError m0 h = adaptIter id (joinlift . runm) m0+ where runm m = do+ r <- catchError (liftM Right m) (return . Left . h)+ case r of+ Right iter -> return $ catchError iter h+ Left iter -> return iter++instance (MonadReader r m, ChunkData t) => MonadReader r (Iter t m) where+ ask = lift ask+ local f = adaptIterM $ local f++instance (MonadState s m, ChunkData t) => MonadState s (Iter t m) where+ get = lift get+ put = lift . put++instance (Monoid w, MonadWriter w m, ChunkData t) =>+ MonadWriter w (Iter t m) where+ tell = lift . tell+ listen = adapt mempty+ where adapt w = adaptIter (\a -> (a, w)) $+ lift . listen >=> \(iter, w') ->+ adapt (mappend w w') iter+ pass m = do+ ((a, f), w) <- adapt mempty m+ tell (f w)+ return a+ where+ adapt w = adaptIter (\af -> (af, w)) $+ lift . censor (const mempty) . listen >=> \(i, w') ->+ adapt (mappend w w') i++--+-- and instances for IterStateT (which are identical to StateT)+--++unIterStateT :: IterStateT s m a -> (s -> m (a, s))+unIterStateT (IterStateT f) = f++instance (MonadCont m) => MonadCont (IterStateT s m) where+ callCC f = IterStateT $ \s -> callCC $ \c ->+ unIterStateT (f (\a -> IterStateT $ \s' -> c (a, s'))) s++instance (MonadError e m) => MonadError e (IterStateT s m) where+ throwError = lift . throwError+ catchError m h = IterStateT $ \s ->+ unIterStateT m s `catchError` \e ->+ unIterStateT (h e) s++instance (MonadReader r m) => MonadReader r (IterStateT s m) where+ ask = lift ask+ local f m = IterStateT $ \s -> local f (unIterStateT m s)++instance (MonadWriter w m) => MonadWriter w (IterStateT s m) where+ tell = lift . tell+ listen m = IterStateT $ \s -> do+ ((a, s'), w) <- listen (unIterStateT m s)+ return ((a, w), s')+ pass m = IterStateT $ \s -> pass $ do+ ((a, f), s') <- unIterStateT m s+ return ((a, s'), f)
+ Data/IterIO/Zlib.hs view
@@ -0,0 +1,261 @@++module Data.IterIO.Zlib (-- * Codec and Inum functions+ ZState, deflateInit2, inflateInit2+ , inumZState, inumZlib, inumGzip, inumGunzip+ -- * Constants from zlib.h+ , max_wbits, max_mem_level, def_mem_level, zlib_version+ , z_DEFAULT_COMPRESSION+ , ZStrategy, z_FILTERED, z_HUFFMAN_ONLY, z_RLE+ , z_FIXED, z_DEFAULT_STRATEGY+ , ZMethod, z_DEFLATED+ ) where++import Prelude hiding (null)+import Control.Exception (throwIO, ErrorCall(..))+import Control.Monad.State.Strict+-- import qualified Data.ByteString as S+import qualified Data.ByteString.Internal as S+import qualified Data.ByteString.Lazy as L+import qualified Data.ByteString.Lazy.Internal as L+import Foreign+import Foreign.C++import Data.IterIO.Iter+import Data.IterIO.Inum+import Data.IterIO.ZlibInt++-- | State used by 'inumZState', the most generic zlib 'Inum'.+-- Create the state using 'deflateInit2' or 'inflateInit2'.+data ZState = ZState { zStream :: (ForeignPtr ZStream)+ , zOp :: (ZFlush -> IO CInt)+ , zFinish :: !ZFlush+ , zInChunk :: !(ForeignPtr Word8)+ , zOutChunk :: !(ForeignPtr Word8)+ , zOut :: L.ByteString -> L.ByteString+ }++defaultZState :: ZState+defaultZState = ZState { zStream = error "must allocate zStream"+ , zOp = error "must define zOp"+ , zFinish = z_FINISH+ , zInChunk = S.nullForeignPtr+ , zOutChunk = S.nullForeignPtr + , zOut = id+ }++newZStream :: (Ptr ZStream -> IO CInt) -> IO (ForeignPtr ZStream)+newZStream initfn = do+ zs <- mallocForeignPtrBytes z_stream_size+ withForeignPtr zs $ \ptr ->+ do _ <- S.memset (castPtr ptr) 0 z_stream_size+ err <- initfn ptr+ when (err /= z_OK) $ throwIO $ ErrorCall "newZStream: init failed"+ return zs++-- | Create a 'ZState' for compression. See the description of+-- @deflateInit2@ in the zlib.h C header file for a more detailed+-- description of the arguments. Note in particular that the value of+-- @windowBits@ determines the encapsulation format of the compressed+-- data:+--+-- * 8..15 = zlib format+--+-- * 24..31 = gzip format+--+-- * -8..-15 = means raw zlib format with no header+deflateInit2 :: CInt+ -- ^ Compression level (use 'z_DEFAULT_COMPRESSION' for default)+ -> ZMethod+ -- ^ Method (use 'z_DEFLATED')+ -> CInt+ -- ^ @windowBits@ (e.g., 'max_wbits')+ -> CInt+ -- ^ @memLevel@ (e.g., 'def_mem_level')+ -> ZStrategy+ -- ^ strategy (e.g., 'z_DEFAULT_STRATEGY')+ -> IO ZState+deflateInit2 level method windowBits memLevel strategy = do+ z <- newZStream $ \ptr -> (c_deflateInit2 ptr level method windowBits+ memLevel strategy zlib_version z_stream_size)+ addForeignPtrFinalizer c_deflateEnd z+ return defaultZState { zStream = z+ , zOp = \flush -> withForeignPtr z $ \zp ->+ c_deflate zp flush+ }++-- | Create a 'Zstate' for uncompression. See the description of+-- @inflateInit2@ in the zlib.h C header file for a more detailed+-- description of the arguments. Note in particular that the value of+-- @windowBits@ determines the encapsulation format of the compressed+-- data:+--+-- * 8..15 = zlib format+--+-- * 24..31 = gzip format+--+-- * 40..47 = automatically determine zlib/gzip format+--+-- * -8..-15 = means raw zlib format with no header+inflateInit2 :: CInt+ -- ^ windowBits+ -> IO ZState+inflateInit2 windowBits = do+ z <- newZStream $ \ptr -> (c_inflateInit2 ptr windowBits+ zlib_version z_stream_size)+ addForeignPtrFinalizer c_inflateEnd z+ return defaultZState { zStream = z+ , zOp = \flush -> withForeignPtr z $ \zp ->+ c_inflate zp flush+ , zFinish = z_NO_FLUSH+ -- Library documentation makes it sound like+ -- you don't need Z_FINISH for inflating, and+ -- it could cause problems if the output buffer+ -- is not large enough.+ }++type ZM = StateT ZState IO++withZFP :: (ZState -> ForeignPtr a) -> (Ptr a -> ZM b) -> ZM b+withZFP field k = StateT $ \zs ->+ withForeignPtr (field zs) $ \v -> (runStateT $ k v) zs++zPeek :: (Storable a) => (Ptr ZStream -> Ptr a) -> ZM a+zPeek f = withZFP zStream $ liftIO . peek . f++zPoke :: (Storable a) => (Ptr ZStream -> Ptr a) -> a -> ZM ()+zPoke f a = withZFP zStream $ liftIO . flip poke a . f++zPokeFP :: (Ptr ZStream -> Ptr (Ptr Word8)) -> ForeignPtr Word8 -> Int -> ZM ()+zPokeFP f fp offset = withZFP zStream $ \z ->+ liftIO $ withForeignPtr fp $ \p ->+ poke (f z) $ p `plusPtr` offset++zMinusPtr :: (Ptr ZStream -> Ptr (Ptr Word8))+ -> (ZState -> ForeignPtr Word8)+ -> ZM Int+zMinusPtr curf basef = withZFP basef $ \base ->+ if base == nullPtr+ then return 0+ else do+ cur <- zPeek curf+ return $ cur `minusPtr` base++zPushIn :: L.ByteString -> ZM L.ByteString+zPushIn s = do+ avail <- zPeek avail_in+ if avail > 0 then return s else pushit s+ where+ pushit (L.Chunk h t) = do+ let (fp, offset, len) = S.toForeignPtr h+ modify $ \zs -> zs { zInChunk = fp }+ zPokeFP next_in fp offset+ zPoke avail_in $ fromIntegral len+ return t+ pushit L.Empty = return L.Empty++zPopIn :: L.ByteString -> ZM L.ByteString+zPopIn s = do+ len <- zPeek avail_in+ if len <= 0+ then return s+ else do+ fptr <- gets zInChunk+ offset <- zMinusPtr next_in zInChunk+ zPoke avail_in 0+ return $ L.chunk (S.fromForeignPtr fptr offset $ fromIntegral len) s++zOutLen :: ZM Int+zOutLen = zMinusPtr next_out zOutChunk++zPopOut :: ZM ()+zPopOut = do+ len <- zOutLen+ when (len > 0) $ do+ ochunk <- liftM (\c -> S.fromForeignPtr c 0 len) $ gets zOutChunk+ out <- liftM (. L.chunk ochunk) $ gets zOut+ modify $ \zs -> zs { zOutChunk = S.nullForeignPtr+ , zOut = out }+ zPoke avail_out 0++zMkSpace :: ZM ()+zMkSpace = do+ avail <- zPeek avail_out+ when (avail <= 0) $ do+ zPopOut+ nchunk <- liftIO $ S.mallocByteString L.defaultChunkSize+ zPokeFP next_out nchunk 0+ zPoke avail_out $ fromIntegral L.defaultChunkSize+ modify $ \zs -> zs { zOutChunk = nchunk }++zExec :: ZFlush -> ZM CInt+zExec flush = do+ zMkSpace+ op <- gets zOp+ r <- withZFP zInChunk $ \_ -> liftIO $ op flush+ avail <- zPeek avail_out+ case () of+ _ | r == z_OK && avail == 0 -> zExec flush+ _ | r == z_NEED_DICT -> liftIO $ throwIO $ ErrorCall "zlib NEED_DICT"+ _ | r == z_STREAM_END -> do zPopOut+ return r+ _ | r < 0 -> do cm <- zPeek msg+ m <- if cm == nullPtr+ then return $ "zlib failed ("+ ++ show r ++ ")"+ else liftIO $ peekCString cm+ liftIO $ throwIO $ ErrorCall m+ _ | otherwise -> return r+++-- | The most general zlib 'Inum', which can take any 'ZState' created+-- by 'deflateInit2' or 'inflateInit2'.+inumZState :: (MonadIO m) =>+ ZState+ -> Inum L.ByteString L.ByteString m a+inumZState = mkInumM . loop+ where+ loop zs0 = do+ (Chunk dat eof) <- chunkI+ ((r, rest), zs) <- liftIO (runStateT (runz eof dat) zs0)+ ungetI rest+ done <- ifeed $ zOut zs L.empty+ unless (done || eof || r == z_STREAM_END) $ loop zs { zOut = id }++ runz False L.Empty = return (z_OK, L.Empty)+ runz eof s0 = do+ s <- zPushIn s0+ flush <- if eof && L.null s then gets zFinish else return z_NO_FLUSH+ r <- zExec flush+ if r == z_STREAM_END || L.null s+ then do s' <- zPopIn s; return (r, s')+ else runz eof s++-- | An 'Inum' that compresses in zlib format. To uncompress, use+-- 'inumGunzip'.+inumZlib :: (MonadIO m) => Inum L.ByteString L.ByteString m a+inumZlib iter = do+ zs <- liftIO (deflateInit2 z_DEFAULT_COMPRESSION z_DEFLATED max_wbits+ def_mem_level z_DEFAULT_STRATEGY)+ inumZState zs iter++-- | An 'Inum' that compresses in gzip format.+inumGzip :: (MonadIO m) => Inum L.ByteString L.ByteString m a+inumGzip iter = do+ zs <- liftIO (deflateInit2 z_DEFAULT_COMPRESSION z_DEFLATED (16 + max_wbits)+ def_mem_level z_DEFAULT_STRATEGY)+ inumZState zs iter++-- | An 'Inum' that uncompresses a data in either the zlib or gzip+-- format. Note that this only uncompresses one gzip stream. Thus,+-- if you feed in the concatenation of multiple gzipped files,+-- @inumGunzip@ will stop after the first one. If this is not what+-- you want, then use @'inumRepeat' inumGunzip@ to decode repeated+-- gzip streams.+inumGunzip :: (MonadIO m) => Inum L.ByteString L.ByteString m a+inumGunzip iter = do+ zs <- liftIO $ inflateInit2 (32 + max_wbits)+ inumZState zs iter++-- Local Variables:+-- haskell-program-name: "ghci -lz"+-- End:
+ Data/IterIO/ZlibInt.hsc view
@@ -0,0 +1,124 @@+{-# OPTIONS_GHC -fno-warn-deprecated-flags #-}+{-# LANGUAGE ForeignFunctionInterface #-}++-- | This module exposes the raw FFI interface to zlib C functions.+-- It is intended for internal use only, and should not be imported by+-- code outside the IterIO library.+module Data.IterIO.ZlibInt where++import Data.Word+import Foreign+import Foreign.C++#include "zlib.h"++foreign import ccall unsafe "zlib.h deflateInit_"+ c_deflateInit :: Ptr ZStream -> CInt -> CString -> CInt -> IO CInt+foreign import ccall unsafe "zlib.h deflateInit2_"+ c_deflateInit2 :: Ptr ZStream -> CInt -> ZMethod -> CInt -> CInt+ -> ZStrategy -> CString -> CInt -> IO CInt+foreign import ccall unsafe "zlib.h deflate"+ c_deflate :: Ptr ZStream -> ZFlush -> IO CInt+foreign import ccall unsafe "zlib.h &deflateEnd"+ c_deflateEnd :: FunPtr (Ptr ZStream -> IO ())++foreign import ccall unsafe "zlib.h inflateInit_"+ c_inflateInit :: Ptr ZStream -> CString -> CInt -> IO CInt+foreign import ccall unsafe "zlib.h inflateInit2_"+ c_inflateInit2 :: Ptr ZStream -> CInt -> CString -> CInt -> IO CInt+foreign import ccall unsafe "zlib.h inflate"+ c_inflate :: Ptr ZStream -> ZFlush -> IO CInt+foreign import ccall unsafe "zlib.h &inflateEnd"+ c_inflateEnd :: FunPtr (Ptr ZStream -> IO ())++-- | Use this value for zlib format. Add 16 for gzip format. Negate+-- for raw zlib format. When uncompressing, add 32 to determine+-- zlib/gzip format automatically.+max_wbits :: CInt+max_wbits = #const MAX_WBITS++max_mem_level :: CInt+max_mem_level = #const MAX_MEM_LEVEL++def_mem_level :: CInt+def_mem_level = #const MAX_MEM_LEVEL > 8 ? 8 : MAX_MEM_LEVEL++zlib_version :: CString+zlib_version = unsafePerformIO $ newCAString #const_str ZLIB_VERSION++z_stream_size :: (Num a) => a+z_stream_size = #size z_stream++#def struct zssz { z_stream z; char c; };+z_stream_alignment :: Int+z_stream_alignment = #const sizeof (struct zssz) - sizeof (z_stream)++data ZStream = ZStream++#{let zoffdef type, field =+ #field " :: Ptr ZStream -> Ptr (" #type ")\n"+ #field " zptr = zptr `plusPtr` %ld"+ , (long) offsetof (z_stream, field)}+#zoffdef Ptr Word8, next_in+#zoffdef CUInt, avail_in+#zoffdef CULong, total_in+#zoffdef Ptr Word8, next_out+#zoffdef CUInt, avail_out+#zoffdef CULong, total_out+#zoffdef CString, msg+#zoffdef FunPtr (Ptr a -> CUInt -> CUInt -> Ptr b), zalloc+#zoffdef FunPtr (Ptr a -> Ptr b -> ()), zfree+#zoffdef Ptr a, opaque+#zoffdef ZDataType, data_type+#zoffdef CULong, adler++newtype ZFlush = ZFlush CInt+#{enum ZFlush, ZFlush+ , z_NO_FLUSH = Z_NO_FLUSH+ , z_SYNC_FLUSH = Z_SYNC_FLUSH+ , z_FULL_FLUSH = Z_FULL_FLUSH+ , z_FINISH = Z_FINISH+ , z_BLOCK = Z_BLOCK+ }++#{enum CInt,+ , z_OK = Z_OK+ , z_STREAM_END = Z_STREAM_END+ , z_NEED_DICT = Z_NEED_DICT+ , z_ERRNO = Z_ERRNO+ , z_STREAM_ERROR = Z_STREAM_ERROR+ , z_DATA_ERROR = Z_DATA_ERROR+ , z_MEM_ERROR = Z_MEM_ERROR+ , z_BUF_ERROR = Z_BUF_ERROR+ , z_VERSION_ERROR = Z_VERSION_ERROR+ }++#{enum CInt,+ , z_DEFAULT_COMPRESSION = Z_DEFAULT_COMPRESSION+ }++newtype ZStrategy = ZStrategy CInt+#{enum ZStrategy, ZStrategy+ , z_FILTERED = Z_FILTERED+ , z_HUFFMAN_ONLY = Z_HUFFMAN_ONLY+ , z_RLE = Z_RLE+ , z_FIXED = Z_FIXED+ , z_DEFAULT_STRATEGY = Z_DEFAULT_STRATEGY+ }++newtype ZDataType = ZDataType CInt+#{enum ZDataType, ZDataType+ , z_BINARY = Z_BINARY+ , z_TEXT = Z_TEXT+ , z_UNKNOWN = Z_UNKNOWN+ }++newtype ZMethod = ZMethod CInt+#{enum ZMethod, ZMethod+ , z_DEFLATED = Z_DEFLATED+ }+++-- Local Variables:+-- haskell-program-name: "ghci -lz"+-- End:
+ Examples/fgrep.hs view
@@ -0,0 +1,59 @@++module Main where++import qualified Data.ByteString.Lazy.Char8 as L8+import qualified Data.ByteString.Lazy as L++import Control.Monad+import Control.Monad.Trans+import System.Environment+import System.Exit+import System.IO+-- import System.IO.Error++import Data.IterIO++filterLines :: (Monad m) =>+ String+ -> Inum L.ByteString [L.ByteString] m a+filterLines s = mkInum loop+ where+ loop = do line <- lineI+ if match line then return [line] else loop+ ls = L8.pack s+ match l | L.null l = False+ | otherwise = L.isPrefixOf ls l || match (L.tail l)++printLines :: (MonadIO m) => Iter [L.ByteString] m ()+printLines = do+ line <- safeHeadI+ case line of+ Just l -> do liftIO $ L.putStrLn l+ printLines+ Nothing -> return ()++enumFileCatchError :: (MonadIO m) => FilePath -> Onum L.ByteString m a+enumFileCatchError file = enumFile file `inumCatch` enumCatchIO+ where+ enumCatchIO :: (ChunkData t, MonadIO m) =>+ IOError+ -> IterR () m (IterR t m a)+ -> Iter () m (IterR t m a)+ enumCatchIO _ = verboseResumeI+ -- or to avoid the need for a type signature, you could say:+ -- enumCatchIO e = flip const (e :: IOError) verboseResumeI++main :: IO ()+main = do+ prog <- getProgName+ av <- getArgs+ unless (length av >= 1) $ do+ hPutStrLn stderr $ "usage: " ++ prog ++ " string [file ...]"+ exitFailure+ hSetBuffering stdout NoBuffering+ let pat = head av+ enum = if length av == 1+ then enumHandle stdin+ else foldr1 cat $ map enumFileCatchError $ tail av+ enum |. filterLines pat |$ printLines+ exitSuccess
+ Examples/httptest.hs view
@@ -0,0 +1,115 @@+{-# LANGUAGE ScopedTypeVariables #-}+{-# LANGUAGE OverloadedStrings #-}++module Main where++import Prelude hiding (catch, head, id, div)+import Control.Concurrent+import Control.Exception+import Control.Monad+import Control.Monad.Trans+import qualified Data.ByteString.Char8 as S8+import qualified Data.ByteString.Lazy as L+-- import qualified Data.ByteString.Lazy.Char8 as L8+import Data.Monoid+import qualified Network.Socket as Net+import qualified OpenSSL as SSL+import qualified OpenSSL.Session as SSL+import System.IO+import System.Posix.Files++import Data.IterIO+-- import Data.IterIO.Parse+import Data.IterIO.Http+import Data.IterIO.HttpRoute+import Data.IterIO.SSL+import System.Directory (getAppUserDataDirectory)+import System.IO.Unsafe (unsafePerformIO)++type L = L.ByteString++data HttpServer = HttpServer {+ hsListenSock :: !Net.Socket+ , hsSslCtx :: !(Maybe SSL.SSLContext)+ , hsLog :: !(Maybe Handle)+ }++myListen :: Net.PortNumber -> IO Net.Socket+myListen pn = do+ sock <- Net.socket Net.AF_INET Net.Stream Net.defaultProtocol+ Net.setSocketOption sock Net.ReuseAddr 1+ Net.bindSocket sock (Net.SockAddrInet pn Net.iNADDR_ANY)+ Net.listen sock Net.maxListenQueue+ return sock++mkServer :: Net.PortNumber -> Maybe SSL.SSLContext -> IO HttpServer+mkServer port mctx = do+ sock <- myListen port+ h <- openBinaryFile "http.log" WriteMode+ hSetBuffering h NoBuffering+ return $ HttpServer sock mctx (Just h)++mimeMap :: String -> S8.ByteString+mimeMap = unsafePerformIO $ do+ path <- findMimeTypes ["mime.types"+ , "/etc/mime.types"+ , "/var/www/conf/mime.types"]+ enumFile path |$ mimeTypesI "application/octet-stream"+ where+ findMimeTypes (h:t) = do exist <- fileExist h+ if exist then return h else findMimeTypes t+ findMimeTypes [] = return "mime.types" -- cause error++routeFS :: (MonadIO m) => FilePath -> HttpRoute m+routeFS = routeFileSys mimeMap (dirRedir "index.html")++cabal_dir :: String+cabal_dir = (unsafePerformIO $ getAppUserDataDirectory "cabal") ++ "/share/doc"++route :: (MonadIO m) => HttpRoute m+route = mconcat+ [ routeTop $ routeConst $ resp301 "/cabal"+ , routeMap' [ ("cabal", routeConst $ resp301 cabal_dir)+ , ("static", routeFS "static") -- directory ./static+ , ("favicon.ico"+ -- serve /favicon.ico from file ./static/favicon.ico,+ -- but tell browser to cache it for 1 day+ , addHeader "Cache-Control: max-age=86400" $+ routeFS "static/favicon.ico")+ ]+ , routePath cabal_dir $ routeFS cabal_dir+ , routePath "/usr/share/doc/ghc/html" $+ routeFS "/usr/share/doc/ghc/html"+ ]++accept_loop :: HttpServer -> IO ()+accept_loop srv = loop+ where+ loop = do+ (s, addr) <- Net.accept $ hsListenSock srv+ hPutStrLn stderr (show addr)+ _ <- forkIO $ server s+ loop+ server s = do+ (iter, enum) <- maybe (iterStream s) (\ctx -> iterSSL ctx s True)+ (hsSslCtx srv)+ let loger = maybe inumNop inumhLog $ hsLog srv+ enum |. loger |$ inumHttpServer (ioHttpServer handler) .| loger .| iter+ handler = runHttpRoute route++main :: IO ()+main = Net.withSocketsDo $ SSL.withOpenSSL $ do+ mctx <- if secure+ then do+ exists <- fileExist privkey+ unless exists $ genSelfSigned privkey "localhost"+ ctx <- simpleContext privkey+ return $ Just ctx+ else return Nothing+ srv <- mkServer (if secure then 4433 else 8000) mctx+ sem <- newQSem 0+ _ <- forkIO $ accept_loop srv `finally` signalQSem sem+ waitQSem sem+ where+ privkey = "testkey.pem"+ secure = True
+ Examples/zpipe.hs view
@@ -0,0 +1,24 @@++module Main where++import System.Environment+import System.IO++import Data.IterIO+import Data.IterIO.Zlib++main :: IO ()+main = do+ av <- getArgs+ hSetBuffering stdout NoBuffering+ case av of+ -- The decision to put inumG[un]zip to the left or to the right+ -- of |$ is arbitrary, so we do one of each.+ [] -> enumStdin |$ inumGzip .| stdoutI+ ["-d"] -> enumStdin |. inumRepeat inumGunzip |$ stdoutI+ _ -> do prog <- getProgName+ hPutStrLn stderr $ "usage: " ++ prog ++ " [-d]"++-- Local Variables:+-- haskell-program-name: "ghci -lz"+-- End:
+ GNUmakefile view
@@ -0,0 +1,68 @@++PKG = $(basename $(wildcard *.cabal))+TARGETS := $(basename $(shell find Examples -name '[a-z]*.hs' -print))+TESTS := $(basename $(shell find tests -name '[a-z]*.hs' -print))+HSCS := $(patsubst %.hsc,%.hs,$(shell find . -name '*.hsc' -print))+HSCCLEAN = $(patsubst %.hs,%_hsc.[ch],$(HSCS))++all: $(TARGETS) $(HSCS)++.PHONY: all always clean build dist doc browse install hsc++GHC = ghc $(WALL)+#GHC = ghc $(WALL) -prof -auto-all -caf-all -rtsopts=all -with-rtsopts=-xc+WALL = -Wall -Werror+LIBS = -lz++always:+ @:++Examples/%: always $(HSCS)+ $(GHC) --make -i$(dir $@) $@.hs $(LIBS)++tests/%: always $(HSCS)+ $(GHC) --make $@.hs++%.hs: %.hsc+ hsc2hs $<++hsc: $(HSCS)++Setup: Setup.hs+ $(GHC) --make Setup.hs++dist/setup-config: Setup $(PKG).cabal+ ./Setup configure --user++build: dist/setup-config+ ./Setup build++doc: dist/setup-config+ ./Setup haddock --hyperlink-source++dist: dist/setup-config+ ./Setup sdist++INDEXDOC = cd $(HOME)/.cabal/share/doc \+ && find . -name '*.haddock' -print \+ | sed -e 's/\.\/\(.*\)\/[^\/]*\.haddock/--read-interface=\1,&/' \+ | xargs -t haddock --gen-contents --gen-index --odir=.++install: build doc+ ./Setup install+ $(INDEXDOC)++uninstall: dist/setup-config+ ./Setup unregister --user+ rm -rf $(HOME)/.cabal/lib/$(PKG)-[0-9]*+ rm -rf $(HOME)/.cabal/share/doc/$(PKG)-[0-9]*+ $(INDEXDOC)++browse: doc+ xdg-open dist/doc/html/$(PKG)/index.html++clean:+ rm -rf dist+ rm -f Setup $(TARGETS) $(TESTS) $(HSCS) $(HSCCLEAN)+ find . \( -name '*~' -o -name '*.hi' -o -name '*.o' \) -print0 \+ | xargs -0 rm -f --
+ LICENSE view
@@ -0,0 +1,29 @@+Copyright (C) 2009 David Mazieres++Redistribution and use in source and binary forms, with or without+modification, are permitted provided that the following conditions are+met:++ 1. Redistributions of source code must retain the above copyright+ notice, this list of conditions, and the following disclaimer.++ 2. Redistributions in binary form must reproduce the above+ copyright notice, this list of conditions, and the following+ disclaimer in the documentation and/or other materials provided+ with the distribution.++ 3. The name of the author may not be used to endorse or promote+ products derived from this software without specific prior+ written permission.++THIS SOFTWARE IS PROVIDED BY THE AUTHOR ``AS IS'' AND ANY EXPRESS OR+IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED+WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE+DISCLAIMED. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY DIRECT,+INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES+(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR+SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION)+HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT,+STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING+IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE+POSSIBILITY OF SUCH DAMAGE.
+ README view
@@ -0,0 +1,11 @@++To build this package, install the Haskell platform+(http://hackage.haskell.org/platform/), then run the following+commands to install required packages:++ cabal update+ cabal install ListLike HSOpenSSL stringsearch attoparsec+ cabal install aeson blaze-html+ cabal install binary regex-posix xhtml utf8-string++Then run "make" or "gmake" to build the example programs.
+ Setup.hs view
@@ -0,0 +1,3 @@+import Distribution.Simple+main :: IO ()+main = defaultMain
+ iterIO.cabal view
@@ -0,0 +1,99 @@+Name: iterIO+Homepage: http://www.scs.stanford.edu/~dm/iterIO+Version: 0.1+Cabal-version: >= 1.6+build-type: Simple+License: BSD3+License-file: LICENSE+Author: David Mazieres+Stability: experimental+Maintainer: http://www.scs.stanford.edu/~dm/addr/+Category: System, Data, Enumerator+Synopsis: Iteratee-based IO with pipe operators+Extra-source-files:+ GNUmakefile, README,+ Examples/fgrep.hs, Examples/zpipe.hs, Examples/httptest.hs++Description:++ Iteratee-based IO is an alternative to lazy IO that offers+ better error handling, referential transparency, and+ convenient composition of protocol layers or parsers. This+ package provides iteratees based around /pipe/ operators for+ hooking together application components and directing data+ flow. New users should see the tutorial in the "Data.IterIO"+ module documentation. Highlights of the library include:++ .++ * Heavy emphasis on ease of use, ease of learning, and+ uniformity of mechanism.++ .++ * Copious documentation.++ .++ * Consistent EOF and error handling to avoid resource leaks+ and other issues in corner cases.++ .++ * A set of iteratee parsing combinators providing LL(*)+ parsing while generally not consuming large amounts of+ memory for backtracking.++ .++ * Seamless integration with attoparsec for LL(1) parsing.++ .++ See "Data.IterIO" for a discussion of the differences between+ iterIO and the two previous iteratee implementations (iteratee+ and enumerator).++Source-repository head+ Type: git+ Location: http://www.scs.stanford.edu/~dm/repos/iterIO.git++Library+ Build-depends: array >= 0.3.0.1 && < 2,+ base >= 4.3 && < 6,+ bytestring >= 0.9 && < 2,+ containers >= 0.3 && < 2,+ filepath >= 1.2 && < 2,+ HsOpenSSL >= 0.8 && < 2,+ ListLike >= 1.0 && < 4,+ mtl >= 1.1.0.2 && < 3,+ network >= 2.3 && < 3,+ old-locale >= 1.0.0.2 && < 2,+ attoparsec >= 0.8.5 && < 2,+ process >= 1.0.1.3 && < 2,+ stringsearch >= 0.3 && < 2,+ time >= 1.1.4 && < 2,+ unix >= 2.4 && < 3++ ghc-options: -Wall+ Exposed-modules:+ Data.IterIO,+ Data.IterIO.Atto,+ Data.IterIO.Iter,+ Data.IterIO.Extra,+ Data.IterIO.Http,+ Data.IterIO.HttpRoute,+ Data.IterIO.Inum,+ Data.IterIO.ListLike,+ Data.IterIO.Parse,+ Data.IterIO.SSL,+ Data.IterIO.Search,+ Data.IterIO.Trans,+ Data.IterIO.Zlib+ Other-modules:+ Data.IterIO.ZlibInt+ Extensions:+ ForeignFunctionInterface, DeriveDataTypeable,+ ExistentialQuantification, MultiParamTypeClasses,+ FunctionalDependencies, FlexibleInstances+ Extra-libraries: z