{-# Language OverloadedStrings, ScopedTypeVariables #-} -------------------------------------------------------------------- -- | -- Module : Utils.Katt.Init -- -- Init submodule providing initialization of problems -- and entire problem sessions. -- -- Problems are initialized by creating a directory, configuration file, -- and optionally downloading all test files available. -- Both zip-based test data and embedded HTML tables are supported. -- -- Problem sessions are initialized by parsing the list of problems and -- initializing each problem separately. module Utils.Katt.Init (initializeProblem, initializeSession) where import Control.Applicative ((<$>), (<*)) import qualified Codec.Archive.Zip as Z import Control.Arrow ((***)) import Control.Monad (liftM, liftM2, void, when) import qualified Control.Monad.State as S import Control.Error hiding (tryIO) import qualified Control.Exception as E import qualified Data.ByteString.Char8 as B import qualified Data.ByteString.Lazy.Char8 as BL import Data.Foldable (fold) import Data.List (isSuffixOf, nub) import Data.Monoid ((<>)) import qualified System.Directory as D import System.IO (stderr) import Text.Parsec hiding (token) import Text.Parsec.ByteString import qualified Utils.Katt.Configuration as C import Utils.Katt.Utils -- | Parsed test cases associated with a problem. type TestContent = [(B.ByteString, B.ByteString)] -- | Possible test case scenarios. data TestParser -- | No tests available. = NoTestsAvailable -- | Test content available in zip file, given as URL. | TestAddress B.ByteString -- | Embedded test content. | TestContents TestContent deriving Show -- | Parse the possible different test file cases, given the problem page. -- Any zip download links are preferred over embedded test data. parseProblemPage :: B.ByteString -> TestParser parseProblemPage contents = case res of Left _ -> NoTestsAvailable Right test -> test where res = parse (try parseAddress <|> parseEmbedded) "Test parser" contents -- | Try to parse a download URL from the supplied page data. parseAddress :: GenParser Char st TestParser parseAddress = do void $ manyTill anyChar (try $ startLink >> lookAhead endParser) TestAddress . B.cons '/' . B.pack <$> endParser where startLink = string " tests where sp = skipMany $ space <|> newline <|> tab beginTag tag = void $ char '<' >> sp >> string tag >> sp >> char '>' endTag tag = void $ char '<' >> sp >> char '/' >> string tag >> sp >> char '>' htmlTag tag p = do sp >> beginTag tag >> sp manyTill p $ try $ endTag tag tr = htmlTag "tr" td = htmlTag "td" pre = htmlTag "pre" tests = manyTill anyChar (lookAhead startTable) >> endBy1 testTable sp startTable = void . try $ string "" endTable = void $ string "
" testTable = do startTable inner <- tableBody sp >> endTable return inner tableBody = do void $ tr anyChar try (fold <$> tr testCase) <* sp innerTestData = do sp B.pack <$> pre anyChar <* sp testData = liftM fold $ td innerTestData <* sp testCase = liftM2 (,) testData testData -- | Retrieve the zip archive located at the specified URL and unzip the contents. -- Matches input and output file pairs, producing a list of tuples. downloadTestArchive :: B.ByteString -> ConfigEnv IO TestContent downloadTestArchive url = do zipFile <- BL.fromChunks . return <$> retrievePublicPage url zipEntries <- tryIOMsg "Failed to unpack zip file: corrupt archive" $ E.evaluate (Z.zEntries $ Z.toArchive zipFile) let filterFiles suffix = filter (isSuffixOf suffix . Z.eRelativePath) zipEntries inFiles = filterFiles inputTestExtension outFiles = filterFiles outputTestExtension tryAssert "Failed to unpack zip file: no test files found" $ not (null zipEntries) tryAssert "Failed to unpack zip file: input and reference count doesn't match" $ length inFiles == length outFiles return $ zipWith (curry convertEntry) inFiles outFiles where convertEntry = getData *** getData getData = fold . BL.toChunks . Z.fromEntry -- | Retrieve test cases, which fall into either one of the three categories. retrieveTestFiles :: KattisProblem -> ConfigEnv IO TestContent retrieveTestFiles problem = do problemName <- tryIO $ retrieveProblemName problem problemPage <- retrievePublicPage $ problemAddress <> problemName case parseProblemPage problemPage of NoTestsAvailable -> do tryIO $ B.hPutStrLn stderr "No tests available" return [] TestAddress addr -> downloadTestArchive addr TestContents list -> return list -- | Page listing all problems associated with a problem session, relative 'Utils.host'. sessionPage :: B.ByteString sessionPage = "/standings/?sid=" -- | Parse a problem session page, locating all the associated problem names. parseProblemList :: GenParser Char st [KattisProblem] parseProblemList = skip >> endBy1 tag skip where beginLink = string "
ProblemSession -> ConfigEnv IO () initializeSession retrieveTests session = do contents <- retrievePublicPage $ sessionPage <> B.pack (show session) problems <- nub <$> (EitherT . return . fmapL (B.pack . show) $ parse' contents) mapM_ (\problem -> do initializeProblem True retrieveTests problem restoreDir ) problems where parse' = parse parseProblemList "Problem list parser" restoreDir = S.liftIO $ D.setCurrentDirectory ".." -- | Given a problem identifier, setup directory structures and -- optionally download test cases. initializeProblem :: Bool -> Bool -> KattisProblem -> ConfigEnv IO () initializeProblem mkDir retrieveTests problem = do S.liftIO . putStrLn $ "Initializing problem: " <> show problem problemName <- tryIO $ retrieveProblemName problem tryIO . when mkDir $ do D.createDirectoryIfMissing False (B.unpack problemName) D.setCurrentDirectory (B.unpack problemName) tryIO $ D.createDirectoryIfMissing False (B.unpack configDir) fileExists <- S.liftIO C.projectConfigExists tryAssert "Project configuration file already exists, please remove it in order to continue." (not fileExists) S.modify $ \s -> s { project = Just $ ProblemName problemName } C.saveProjectConfig when retrieveTests $ do tryIO $ D.createDirectory testFolder files <- zip [1..] <$> retrieveTestFiles problem mapM_ (\(n :: Integer, (input, output)) -> do let fileName = testFolder <> "/" <> B.unpack problemName <> "-" <> show n tryIO $ B.writeFile (fileName <> inputTestExtension) input tryIO $ B.writeFile (fileName <> outputTestExtension) output) files