packages feed

katt 0.1.0.0 → 0.2.0.0

raw patch · 14 files changed

+1220/−1065 lines, 14 filesdep +kattdep −regex-posixdep ~basedep ~bytestringdep ~mtl

Dependencies added: katt

Dependencies removed: regex-posix

Dependency ranges changed: base, bytestring, mtl

Files

+ katt-cli/src/Main.hs view
@@ -0,0 +1,78 @@+{-# Language OverloadedStrings #-}++--------------------------------------------------------------------+-- |+-- Module : Main+--+-- Entry point of the program.+--+-- Begins by locating a global configuration file, and upon success+-- parses the given arguments and executes the corresponding submodule.+--++module Main(main) where++import qualified Utils.Katt.Configuration as C+import Control.Monad.State+import qualified Data.ByteString.Char8 as B+import Data.Monoid ((<>))+import Utils.Katt.Init+import Network.Http.Client+import OpenSSL (withOpenSSL)+import System.Environment+import System.Exit (exitFailure)+import Utils.Katt.Upload+import Utils.Katt.Utils++-- | Main loads the global config and runs argument parsing.+main :: IO ()+main = do+  conf <- C.loadGlobalConfig+  conf' <- case conf of+    Left err -> do+      B.putStrLn $ "Kattis configuration error: " <> err+      printHelp+      exitFailure+    Right c -> return c++  withOpenSSL $ parseArgs conf'++-- | Given some configuration state, parse arguments and+--   run the appropiate submodule.+--   Output help text upon failure to parse arguments.+parseArgs :: ConfigState -> IO ()+parseArgs conf = getArgs >>= parse+  where+  parse :: [String] -> IO ()+  parse ("init" : problem : []) = withConn conf . initializeProblem True True . ProblemName $ B.pack problem+  parse ("init-session" : session : []) = withConn conf . initializeSession True $ read session+  parse ("submit" : filterList) = withConn conf $ makeSubmission filterList+  parse _ = printHelp++-- | Print help text.+printHelp :: IO ()+printHelp = putStrLn $+  "The following commands are available:\n\n" <>+  "init <problem>\n" <>+  "  Create the corresponding directory and download any available tests.\n\n" <>+  "init-session <session>\n" <>+  "  Initialize all problems associated to the problem session, given as an integer id.\n\n" <>+  "submit [+add_file] [-skip_file]\n" <>+  "  Make a submission using the problem name read from a local config.\n" <>+  "  Defaults to recursively including all source and header files that can be found.\n" <>+  "  Use the optional filter arguments to include or exclude files.\n\n" <>+  "Note that you will need a valid configuration file (placed in ~/.kattisrc), such as:\n" <>+  "https://www.kattis.com/download/kattisrc\n"++-- | Given some action, initiate a connection and run it.+--   Connections are layered using StateT since they may be reestablished.+withConn :: ConfigState -> ConnEnv IO a -> IO a+withConn conf action = do+  B.putStrLn $ "Connecting to host: " <> host conf+  conn <- establishConnection (host conf)+  ((res, conn'), _) <- runStateT (withConf conn) conf+  closeConnection conn'+  return res++  where+  withConf = runStateT (terminateOnFailure "Failed to run command" action)
+ katt-lib/src/Utils/Katt/Configuration.hs view
@@ -0,0 +1,121 @@+{-# Language OverloadedStrings, NoMonomorphismRestriction #-}++--------------------------------------------------------------------+-- |+-- Module : Utils.Katt.Configuration+--+-- Implements loading and saving of global and local configurations.+-- All configurations are stored in the 'ConfigFile' format, which is+-- fully compatible with the official Kattis configuration file.+--+-- The global configuration file corresponds to /kattisrc/, which holds+-- information regarding authentication and hosts.+--+-- The local configuration holds project-specific information and+-- is created by the 'Init' submodule.+-- Currently only the problem name is stored.++module Utils.Katt.Configuration+(loadGlobalConfig, projectConfigExists, loadProjectConfig, saveProjectConfig)+where++import Control.Error hiding (tryIO)+import Control.Monad+import qualified Control.Monad.State as S+import Control.Monad.Trans (liftIO, lift)+import qualified Data.ByteString.Char8 as B+import Data.ConfigFile+import Data.Monoid ((<>))+import qualified Network.URL as U+import System.Directory (getHomeDirectory)+import System.IO+import System.IO.Error (catchIOError)+import Utils.Katt.Utils++-- | Path to global configuration file, relative home directory.+globalConfigFile :: FilePath+globalConfigFile  = ".kattisrc"++-- | Path to project-specific (local) configuration file.+projectConfigFile :: FilePath+projectConfigFile = B.unpack $ configDir <> "/" <> "config"++-- | Serialize error description into bytestring.+convertErrorDesc :: (Show a) => (a, String) -> B.ByteString+convertErrorDesc (errorData, str) = B.pack (show errorData) <> B.pack str++-- | Retrieve a field from the configuration state using a key and section,+--   possibly failing.+get' :: (Monad m, Get_C r) => ConfigParser -> SectionSpec -> String -> EitherT B.ByteString m r+get' conf section key = fmapLT+  (const . B.pack $ "Failed to parse " <> key <> " field")+  (get conf section key)++-- | Load global configuration file and parse the configuration state.+--   Ensures that all fields are present and validates the URLs.+loadGlobalConfig :: IO (Either ErrorDesc ConfigState)+loadGlobalConfig = runEitherT $ do+  home <- tryIO getHomeDirectory+  let filePath = home <> "/" <> globalConfigFile++  fileHandle <- tryIOMsg ("Failed to open " <> B.pack filePath) $+    openFile filePath ReadMode+  conf <- fmapLT convertErrorDesc $ join . liftIO $ readhandle emptyCP fileHandle++  user' <- get' conf "user" "username"+  apiKey' <- get' conf "user" "token"++  loginURL <- get' conf "kattis" "loginurl"+  loginParsed <- extract "Failed to parse loginurl field" loginURL++  submitURL <- get' conf "kattis" "submissionurl"+  submitParsed <- extract "Failed to parse submissionurl field" submitURL++  host' <- parseHost loginParsed++  tryIOMsg "Failed to close configuration file handle" $ hClose fileHandle++  return $ ConfigState+    (B.pack user')+    (B.pack apiKey')+    (B.pack $ "https://" <> host')+    (B.pack $ U.url_path loginParsed)+    (B.pack $ U.url_path submitParsed)+    Nothing++  where+  extract msg = noteT msg . hoistMaybe . U.importURL+  parseHost (U.URL (U.Absolute host') _ _) = return $ U.host host'+  parseHost _ = left "Invalid URL format"++-- | Check if a project-specific configuration file exists and can be read.+projectConfigExists :: IO Bool+projectConfigExists = catchIOError open . const $ return False+  where+  open = withFile projectConfigFile ReadMode . const $ return True++-- | Load a project-specific configuration based on the current directory.+loadProjectConfig :: ConfigEnv IO ()+loadProjectConfig = do+  conf <- fmapLT convertErrorDesc $ join . liftIO $ readfile emptyCP projectConfigFile+  problem <- get' conf "problem" "problemname"+  lift . S.modify $ \s -> s { project = Just . ProblemName $ B.pack problem}++-- | Save a project-specific configuration file to disk.+saveProjectConfig :: ConnEnv IO ()+saveProjectConfig = do+  project' <- lift . lift $ S.gets project+  state <- noteT "Tried to save an empty project configuration." . hoistMaybe $ project'+  problemName <- retrieveProblemName state+  serialized <- serialize problemName+  tryIOMsg (B.pack $+            "Failed to save project configuration file (path: "+            <> projectConfigFile <> ")") $+    writeFile projectConfigFile (to_string serialized)++  where+  serialize problemName = EitherT . return . fmapL convertErrorDesc $ do+    let conf = emptyCP+    conf' <- add_section conf "problem"+    conf'' <- add_section conf' "submissions"+    set conf'' "problem" "problemname" $ B.unpack problemName
+ katt-lib/src/Utils/Katt/Init.hs view
@@ -0,0 +1,205 @@+{-# Language OverloadedStrings, ScopedTypeVariables,+    NoMonomorphismRestriction #-}++--------------------------------------------------------------------+-- |+-- 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 Codec.Archive.Zip+import Control.Arrow ((***))+import qualified Control.Monad.State as S+import qualified Utils.Katt.Configuration as C+import Control.Error hiding (tryIO)+import qualified Control.Exception as E+import Control.Monad.Reader+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 System.Directory+import System.IO (stderr)+import Text.Parsec hiding (token)+import Text.Parsec.ByteString+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 "<a href='"+  endLink name = string ">" >> string name >> string "</a>"+  endParser = manyTill anyChar (char '\'') <* endLink "Download"++-- | Try to parse test cases embedded into HTML data.+--   Currently only table-style test cases are supported (e.g. problem 'friends').+parseEmbedded :: GenParser Char st TestParser+parseEmbedded = TestContents <$> 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 "<table class=\"sample\" summary=\"sample data\">"+  endTable  = void $ string "</table>"++  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 -> ConnEnv IO TestContent+downloadTestArchive url = do+  reestablishConnection+  zipFile <- BL.fromChunks . return <$> retrievePublicPage url+  zipEntries <- tryIOMsg "Failed to unpack zip file: corrupt archive" $+    E.evaluate (zEntries $ toArchive zipFile)++  let filterFiles suffix = filter (isSuffixOf suffix . 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 . fromEntry++-- | Retrieve test cases, which fall into either one of the three categories.+retrieveTestFiles :: KattisProblem -> ConnEnv IO TestContent+retrieveTestFiles problem = do+  problemName <- 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 "<a href='problems/"+  skip = manyTill anyChar (void (try beginLink) <|> eof)+  tag = do+    problem <- manyTill (letter <|> digit) (char '\'')+    return . ProblemName $ B.pack problem++-- | Given a problem session id, initialize all the corresponding problems.+initializeSession :: Bool -> ProblemSession -> ConnEnv 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+      reestablishConnection+      initializeProblem True retrieveTests problem+      restoreDir+    )+    problems+  where+  parse' contents = parse (parseProblemList) "Problem list parser" contents+  restoreDir = liftIO $ setCurrentDirectory ".."+  -- TODO: restore to previously used directory++-- | Given a problem identifier, setup directory structures and+--   optionally download test cases.+initializeProblem :: Bool -> Bool -> KattisProblem -> ConnEnv IO ()+initializeProblem mkDir retrieveTests problem = do+  liftIO . putStrLn $ "Initializing problem: " <> show problem+  problemName <- retrieveProblemName problem+  tryIO . when mkDir $ do+    createDirectoryIfMissing False (B.unpack problemName)+    setCurrentDirectory (B.unpack problemName)++  tryIO $ createDirectoryIfMissing False (B.unpack configDir)++  fileExists <- liftIO C.projectConfigExists+  tryAssert+    "Project configuration file already exists, please remove it in order to continue."+    (not fileExists)++  lift . lift . S.modify $ \s -> s { project = Just $ ProblemName problemName }+  C.saveProjectConfig++  when retrieveTests $ do+    tryIO $ 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
+ katt-lib/src/Utils/Katt/SourceHandler.hs view
@@ -0,0 +1,116 @@+{-# Language OverloadedStrings #-}++--------------------------------------------------------------------+-- |+-- Module : Utils.Katt.SourceHandler+--+-- Provides searching of source code files and language identification.+--+-- Language identification is required in order to detect any+-- inconsistencies (e.g. combining Java and C), and to tag submissions.+--+-- Java also requires identifying which file provides the main method.++module Utils.Katt.SourceHandler+(parseFilter, findFiles, determineLanguage, findMainClass, languageKattisName, languageContentType)+where++import Control.Applicative ((<$>))+import Control.Arrow ((***))+import Control.Monad+import qualified Data.ByteString.Char8 as B+import Data.List+import qualified Data.Set as Set+import System.Directory+import System.FilePath (takeBaseName, takeExtension)+import Text.Parsec+import Text.Parsec.ByteString+import Utils.Katt.Utils++-- | All supported source file extensions, per language.+supported :: KattisLanguage -> Set.Set FilePath+supported LangCplusplus = Set.fromList [".cc", ".cpp", ".hpp", ".h"]+supported LangC         = Set.fromList [".c", ".h"]+supported LangJava      = Set.fromList [".java"]++-- | Parse an argument list from the +file1 -file2 style into+--   two lists of file paths (included and ignored files).+parseFilter :: [String] -> Maybe ([FilePath], [FilePath])+parseFilter input = (filter' *** filter') <$> mapAndUnzipM go input+  where+  go ('+' : file) = Just (file, "")+  go ('-' : file) = Just ("", file)+  go _ = Nothing+  filter' = filter (not . null)++-- | Locate all source files recursively from the current directory.+findFiles :: IO [FilePath]+findFiles = explore "" "."+  where+  explore prefix dir = do+    contents <- (\\ [".", ".."]) <$> getDirectoryContents dir+    let withPrefix = map (prefix++) contents++    dirs <- filterM doesDirectoryExist withPrefix+    let sourceFiles = filter isValidSourceFile (withPrefix \\ dirs)+    nextDepth <- mapM exploreDir dirs+    return $ sourceFiles ++ concat nextDepth++  isValidSourceFile file = any (`isSuffixOf` file)+    (Set.toList . Set.unions $ map supported [LangCplusplus, LangC, LangJava])+  exploreDir dir = explore (dir ++ "/") dir++-- | Determine source code language by studying file extensions.+--   There is an implicit priority ordering, since C is a subset of C++.+determineLanguage :: [FilePath] -> Maybe KattisLanguage+determineLanguage files+  | is LangC = Just LangC+  | is LangCplusplus = Just LangCplusplus+  | is LangJava = Just LangJava+  | otherwise = Nothing+  where+  fileSet = Set.fromList $ map takeExtension files+  is lang = fileSet `Set.isSubsetOf` (supported lang)++-- | Locate main class based on source file contents.+--   C++ and C solutions do not need to be specified, returns an empty string.+--+--   In the Java case all souce code files are parsed.+--   All occurences of a /main/ method defined with /public static void/ are located.+--+--   Will return 'Data.Maybe.Nothing' if result is ambiguous.+findMainClass :: ([FilePath], KattisLanguage) -> IO (Maybe FilePath)+findMainClass ([], _)            = return Nothing+findMainClass (_, LangCplusplus) = return $ Just ""+findMainClass (_, LangC)         = return $ Just ""+findMainClass (files, LangJava)  = survey <$> filterM containsMain files+  where+  containsMain file = do+    parseResult <- parseFromFile mainParser file+    case parseResult of+      Right _ -> return True+      Left _ -> return False++  mainParser = manyTill+    (lineComment <|> blockComment <|> stringData <|> void anyChar)+    mainFunc+  blockComment = void $ string "/*" >> manyTill anyChar (try $ string "*/")+  lineComment = void . try $ string "//" >> manyTill anyChar newline+  stringData = void $ char '"' >> manyTill anyChar (char '"')+  mainFunc = try $ mapM_ keyWord ["public", "static", "void", "main"]+  keyWord str = void $ string str >> spaces++  survey [singleton] = Just $ takeBaseName singleton+  survey _ = Nothing++-- | Determine content type of submission language.+languageContentType :: KattisLanguage -> B.ByteString+languageContentType LangCplusplus = "text/x-c++src"+languageContentType LangJava = "text/x-c++src"+languageContentType LangC = "text/x-c++src"++-- | Determine Kattis language string identifier.+languageKattisName :: KattisLanguage -> B.ByteString+languageKattisName LangCplusplus = "C++"+languageKattisName LangJava = "Java"+languageKattisName LangC = "C"
+ katt-lib/src/Utils/Katt/Upload.hs view
@@ -0,0 +1,325 @@+{-# Language OverloadedStrings, ScopedTypeVariables #-}++--------------------------------------------------------------------+-- |+-- Module : Utils.Katt.Upload+--+-- Upload submodule providing submissions of solutions and parsing of results.+--+-- A submission is done by including all recursively found files and filtering+-- using a file filter given as an argument.+-- This is followed by polling for a submission result until some final+-- submission state has been reached (e.g. accepted).+--+-- Currently multipart data upload is implemented since https-streams+-- (the HTTP client being used) does not support it (yet?).++module Utils.Katt.Upload+(makeSubmission)+where++import Control.Applicative ((<$>))+import Blaze.ByteString.Builder (fromByteString)+import qualified Utils.Katt.Configuration as C+import Control.Concurrent (threadDelay)+import Control.Error hiding (tryIO)+import Control.Monad.Reader+import qualified Control.Monad.State as S+import qualified Data.ByteString.Char8 as B+import Data.List ((\\), union, findIndex)+import Data.Maybe (fromJust)+import Data.Monoid ((<>))+import Network.Http.Client+import Utils.Katt.SourceHandler+import System.IO.Streams (write)+import Text.Parsec hiding (token)+import Text.Parsec.ByteString+import Utils.Katt.Utils++-- | Line separator used in HTTP headers. +crlf :: B.ByteString+crlf = "\r\n"++-- | Multipart field consisting of an option or file.+data MultiPartField+  -- | Option built using a list of assignments and a payload.+  = Option [B.ByteString] B.ByteString+  -- | File to submitted, path given.+  | File FilePath++-- | Serialize a chunk based on a file to be submitted.+buildChunk :: B.ByteString -> MultiPartField -> IO B.ByteString+buildChunk langStr (File path) = do+  file <- B.readFile path+  return $ B.intercalate crlf [headerLine, "Content-Type: " <> langStr, "", file, ""]+  where+    headerLine = B.intercalate "; " ["Content-Disposition: form-data", "name=\"sub_file[]\"",+                                     B.concat ["filename=\"", B.pack path, "\""]]++-- | Serialize a chunk based on an option.+buildChunk _ (Option fields payload) = return $ B.intercalate crlf [headerLine, "", payload, ""]+  where+    headerLine = B.intercalate "; " fieldList+    fieldList = "Content-Disposition: form-data" : fields++-- | Submission page URL, relative 'Utils.host', from which specific submission can be requested.+submissionPage :: B.ByteString+submissionPage = "submission"++-- | Possible states of a submission, with unknowns being grouped into 'Other'.+data SubmissionState+  -- | Submission is queued.+  = Queued+  -- | Submission is compiling.+  | Compiling+  -- | Submission is running.+  | Running+  -- | Wrong answer.+  | WrongAnswer+  -- | Time limit exceeded.+  | TimeLimitExceeded+  -- | Submission was accepted (only success state).+  | Accepted+  -- | Compile error.+  | CompileError+  -- | Run time error.+  | RunTimeError+  -- | Some other, unmatched error code. Only used when parsing fails.+  | Other+  deriving (Eq, Show)++-- | Possible states of a single test case, i.e. an (input, output) data pair.+data TestCase+  -- | Test case passed.+  = TestPassed+  -- | Test case failed (state /= Accepted)+  | TestFailed+  -- | Test case has not been executed.+  | NotTested+  deriving (Eq, Show)++-- | Check if a given state is final, i.e. won't transition into some other.+--   Note that 'Other' is listed as final.+finalSubmissionState :: SubmissionState -> Bool+finalSubmissionState s = elem s+  [WrongAnswer, TimeLimitExceeded, Accepted, CompileError, RunTimeError, Other]++-- | Make a submission of the project in the working directory.+--   Accepts a list of filters on the form /+file1 -file2 ../, which are+--   taken into account when locating all the source files.+--   /+file/ implies adding the specified file.+--   /-file/ implies removing the specified file.+--+--   In addition to the filters, all recursively found source code files+--   will be included in the submission.+makeSubmission :: [String] -> ConnEnv IO ()+makeSubmission filterArguments = do+  exists <- liftIO C.projectConfigExists+  tryAssert "No project configuration could be found."+    exists++  unWrapTrans C.loadProjectConfig+  problem <- lift . lift $ (fromJust <$> S.gets project)++  -- Locate all source files, filter based on filter list.+  files <- tryIOMsg "Failed to locate source files" findFiles+  let adjusted = adjust (parseFilter filterArguments) files++  liftIO $ mapM_ (putStrLn . ("Adding file: "++)) adjusted++  -- Authenticate, submit files, and retrieve submission id.+  token <- authenticate ++  submission <- EitherT $ runReaderT+    (runEitherT $ submitSolution (problem, adjusted)) token++  tryIO . putStrLn $ "Made submission: " <> show submission+  tryIO $ threadDelay initialTimeout++  -- Reauthenticate to allow future requests.+  reestablishConnection+  token' <- authenticate ++  -- Poll submission page until completion.+  reestablishConnection+  EitherT $ runReaderT+    (runEitherT $ checkSubmission submission) token'++  where+  adjust Nothing files = files+  adjust (Just (add, sub)) files = union (files \\ sub) add++  -- Initial timeout before requesting updates is 2 s.+  initialTimeout = 2000000++-- | Poll kattis for updates on a submission.+--  This function returns when the submission has reached one of the final states.+--  TODO: Consider exponential back-off and timeout+checkSubmission :: SubmissionId -> AuthEnv IO ()+checkSubmission submission = do+  page <- retrievePrivatePage $+    "/" <> submissionPage <> "?id=" <> B.pack (show submission)+  let (state, tests) = parseSubmission page++  if finalSubmissionState state+    then+      tryIO $ printResult tests state+    else do+      tryIO $ putStrLn "Waiting for completion.." >> threadDelay interval+      unWrapTrans reestablishConnection+      checkSubmission submission+   +  where+  -- Default poll interval is 1 s.+  interval = 1000000++-- | Parse the supplied submission page into:+--   (1) Current submission state+--   (2) Status of all test cases+parseSubmission :: B.ByteString -> (SubmissionState, [TestCase])+parseSubmission contents =+  case res of+    Left err' -> error $ "Internal parser error" <> show err'+    Right res' -> res'+  where+  res = parse parser "Submission parser" contents+  parser = liftM2 (,) parseStatus parseTestCases++-- | String separator parser.+strSep :: GenParser Char st ()+strSep = void (char '\'' <|> char '"')++-- | End-of-tag parser, ignores everything up to the end of the current tag.+endTag :: GenParser Char st ()+endTag = void $ manyTill anyChar (char '>')++-- | Parse the submission status field, beginning from any offset in the page data.+parseStatus :: GenParser Char st SubmissionState+parseStatus = skip >> status+  where+  beginStatus = do+    void $ string "<td class="+    strSep >> string "status" >> strSep >> endTag+    void $ string "<span class=" >> strSep++  -- Skip to the appropiate <td> tag.+  skip = manyTill anyChar (void (try beginStatus) <|> eof)++  -- Parse contents in <td>...</td>.+  -- TODO: check if manyTill can be rewritten to the endTag pattern+  status = do+    void $ manyTill anyChar strSep+    endTag+    statusStr <- manyTill (letter <|> space) (char '<')+    return $ conv statusStr++  conv "Time Limit Exceeded" = TimeLimitExceeded+  conv "Wrong Answer" = WrongAnswer+  conv "Accepted" = Accepted+  conv "Memory Limit Exceeded" = Other+  conv "Compiling" = Compiling+  conv "Running" = Running+  conv "Compile Error" = CompileError+  conv "Run Time Error" = RunTimeError+  conv _ = Other++-- | Parse the status of all test cases, beginning from any offset in the page data.+--   May return zero test cases when a submission fails+--   with certain status values, e.g. /Compile Error/.+parseTestCases :: GenParser Char st [TestCase] +parseTestCases = skip >> tests+  where+  beginTests = do+    void $ string "<div class="+    strSep >> string "testcases" >> strSep+    endTag++  -- Locate surrounding div tag.+  skip = manyTill anyChar (void (try beginTests) <|> eof)++  -- Parse all test cases.+  tests = many testCase++  -- Each test case is basically <span [class="status"]>...</span> +  -- where a missing class attribute implies that it hasn't been executed.+  testCase = do+    void . try $ string "<span "+    classResult <- optionMaybe $ do+      string "class=" >> strSep+      manyTill anyChar strSep++    void . manyTill anyChar $ string "</span>"+    fromMaybe (return NotTested) (mapResult <$> classResult)++  mapResult "accepted" = return TestPassed+  mapResult "rejected" = return TestFailed+  mapResult _ = parserZero++-- | Print the result of a submission.+--   Will also take care of the special case when no test cases were parsed.+printResult :: [TestCase] -> SubmissionState -> IO ()+printResult tests state+  | state == Accepted = putStrLn $ "Accepted, " <> numTests <> " test(s) passed."+  | null tests = putStrLn resultStr+  | otherwise = putStrLn $ resultStr <> testCaseStr+  where+  numTests = show $ length tests+  firstFailed = show . (+1) . fromMaybe 0 $ findIndex (/= TestPassed) tests+  resultStr = "Result: " <> show state+  testCaseStr = ", failed on test case " <> firstFailed <> " of " <> numTests+++-- | Submit a solution, given problem name and source code files.+submitSolution :: Submission -> AuthEnv IO SubmissionId+submitSolution (problem, files) = do+  -- Determine language in submission.+  language <- noteT ("\nFailed to decide submission language\n" <>+                    "Please use either Java or some union of C++ and C")+    . hoistMaybe $ determineLanguage files+  let languageStr = languageKattisName language++  mainClassStr <- join . liftIO $+    (noteT "Failed to locate the \"public static void main\" method - is there any?" . hoistMaybe)+      <$> findMainClass (files, language)++  -- Build HTTP headers and form.+  let multiPartSeparator = "separator"++  conf <- lift . lift $ lift S.get+  header <- makeSignedRequest $ do+    http POST ("/" <> submitPage conf)+    defaultRequest+    setContentType $ B.append "multipart/form-data; boundary=" multiPartSeparator++  problemName <- unWrapTrans $ retrieveProblemName problem++  let postFields = [Option ["name=\"submit\""] "true"]+                <> [Option ["name=\"submit_ctr\""] "2"]+                <> [Option ["name=\"language\""] languageStr]+                <> [Option ["name=\"mainclass\""] (B.pack mainClassStr)]+                <> [Option ["name=\"problem\""] problemName]+                <> [Option ["name=\"tag\""] ""]+                <> [Option ["name=\"script\""] "true"]+                <> map File files++  -- Send request. Connection is reestablished.+  unWrapTrans reestablishConnection+  conn <- lift . lift $ S.get++  tryIO $ sendRequest conn header (\o -> do+    mapM_ (\part -> do+        serialized <- buildChunk (languageContentType language) part+        write (Just . fromByteString $ B.concat ["--", multiPartSeparator, crlf, serialized]) o)+      postFields++    write (Just . fromByteString $ B.concat ["--", multiPartSeparator, "--", crlf]) o+    )++  -- Receive server response and parse submission ID.+  reply <- tryIO $ receiveResponse conn concatHandler+  (EitherT . return  . fmapL (B.pack . show)) $+    parse parseSubmissionId "Submission ID parser" reply++  where+  parseSubmissionId = manyTill anyChar (lookAhead identifier) >> identifier+  identifier = read <$> many1 digit
+ katt-lib/src/Utils/Katt/Utils.hs view
@@ -0,0 +1,231 @@+{-# Language OverloadedStrings, ScopedTypeVariables,+    NoMonomorphismRestriction #-}++--------------------------------------------------------------------+-- |+-- Module : Utils.Katt.Utils+--+-- Contains shared type declarations and various utility functions.+--++module+Utils.Katt.Utils+where++import Control.Applicative ((<$>))+import Control.Error hiding (tryIO)+import qualified Control.Exception as E+import Control.Monad.Reader+import qualified Control.Monad.State as S+import qualified Data.ByteString.Char8 as B+import Data.Monoid ((<>))+import Network.Http.Client+import System.Exit (exitFailure)+import System.IO (stderr)+import System.IO.Streams (readExactly)++-- | Configuration layer consisting of configuration state.+type ConfigEnvInternal m = S.StateT ConfigState m+-- | Configuration layer wrapped with error handling.+type ConfigEnv m = EitherT ErrorDesc (ConfigEnvInternal m)++-- | Connection layer with connection state layered on the configuration layer.+type ConnEnvInternal m = S.StateT Connection (ConfigEnvInternal m)+-- | Connection layer wrapped with error handling.+type ConnEnv m = EitherT ErrorDesc (ConnEnvInternal m)++-- | Authentication layer with token state and error handling,+--   wrapping the connection layer.+type AuthEnv m = EitherT ErrorDesc (ReaderT B.ByteString (ConnEnvInternal m))++-- | Submissions consist of a problem identifier and a set of file paths.+type Submission = (KattisProblem, [FilePath])++-- | Error description alias.+type ErrorDesc = B.ByteString++-- | Submissions are identified with an integer id.+type SubmissionId = Integer+--+-- | Problem sessions are identified with an integer id.+type ProblemSession = Integer++-- | Project-specific state consists of the problem name.+type ProjectState = (KattisProblem)++-- | Global configuration, initialized from the /.kattisrc/ file.+data ConfigState =+  ConfigState {+    -- | Username.+    user :: B.ByteString,+    -- | API token (hash).+    apiKey :: B.ByteString,+    -- | Host to be considered as service.+    host :: B.ByteString,+    -- | URL to login page, relative 'host'.+    loginPage :: B.ByteString,+    -- | URL to submit page, relative 'host'.+    submitPage :: B.ByteString,+    -- | Project-specific state, optionally loaded.+    project :: Maybe ProjectState+  }+  deriving Show++-- | A Kattis problem.+data KattisProblem+  -- | Problem ID, unique.+  = ProblemId Integer+  -- | Associated name of the problem.+  | ProblemName B.ByteString+  deriving (Eq, Show)++-- | Language used in submission.+data KattisLanguage+  -- | C++.+  = LangCplusplus+  -- | Java.+  | LangJava+  -- | C.+  | LangC+  deriving (Eq, Show)++-- | Server response indicating successful login.+loginSuccess :: B.ByteString+loginSuccess = "Login successful"++-- | Extension of input test files.+inputTestExtension :: FilePath+inputTestExtension = ".in"++-- | Extension of reference ouput test files.+outputTestExtension :: FilePath+outputTestExtension = ".ans"++-- | Name of this program.+programName :: B.ByteString+programName = "katt"++-- | Relative path to project-specific configuration directory.+configDir :: B.ByteString+configDir = "." <> programName++-- | Relative path to folder containing tests.+testFolder :: FilePath+testFolder = "tests"++-- | URL to page with problem information, relative to 'host'.+problemAddress :: B.ByteString+problemAddress = "/problems/"++-- | Lift some error monad one layer.+unWrapTrans :: (Monad m, MonadTrans t) => EitherT e m a -> EitherT e (t m) a+unWrapTrans = EitherT . lift . runEitherT++-- | Execute an IO action and catch any exceptions.+tryIO :: MonadIO m => IO a -> EitherT ErrorDesc m a+tryIO = EitherT . liftIO . liftM (fmapL (B.pack . show)) . +  (E.try :: (IO a -> IO (Either E.SomeException a)))++-- | Execute an IO action and catch any exceptions, tagged with description.+tryIOMsg :: MonadIO m => B.ByteString -> IO a -> EitherT ErrorDesc m a+tryIOMsg msg = EitherT . liftIO . liftM (fmapL $ const msg) . +  (E.try :: (IO a -> IO (Either E.SomeException a)))++-- | Evaluate an error action and terminate process upon failure.+terminateOnFailure :: MonadIO m => ErrorDesc -> EitherT ErrorDesc m a -> m a+terminateOnFailure msg state = do+  res <- runEitherT state+  liftIO $ case res of+    Left errorMessage -> do+      B.hPutStrLn stderr $ msg <> ", error: " <> errorMessage+      exitFailure+    Right success -> return success++-- | Sign an existing HTTP request with a temporary token.+makeSignedRequest :: RequestBuilder () -> AuthEnv IO Request+makeSignedRequest req = do+  key <- lift $ liftM (setHeader "Cookie") ask+  liftIO . buildRequest $ req >> key++-- | Default HTTP request.+defaultRequest :: RequestBuilder ()+defaultRequest = do+  setHeader "User-Agent" programName+  setHeader "Connection" "keep-alive"++-- | Reestablish an existing connection.+--   Useful in order to avoid timeouts related to keep-alive.+reestablishConnection :: ConnEnv IO ()+reestablishConnection = do+  conn <- lift S.get+  tryIOMsg "Failed to close connection" $ closeConnection conn+  host' <- host <$> lift (lift S.get)+  conn' <- tryIOMsg "Failed to reestablish connection" $ establishConnection host'+  lift $ S.put conn'++-- | Retrieve a publically available page, using HTTP GET.+retrievePublicPage :: B.ByteString -> ConnEnv IO B.ByteString+retrievePublicPage page = do+  header <- tryIO . buildRequest $ http GET page >> defaultRequest+  makeRequest header++-- | Retrieve a page requiring authentication, using HTTP GET.+retrievePrivatePage :: B.ByteString -> AuthEnv IO B.ByteString+retrievePrivatePage page = do+  header <- makeSignedRequest $ do+    http GET page+    defaultRequest+  unWrapTrans $ makeRequest header++-- | Make a HTTP request and retrieve the server response body.+makeRequest :: Request -> ConnEnv IO B.ByteString+makeRequest header = do+  conn <- lift S.get+  tryIO $ sendRequest conn header emptyBody+  tryIO $ receiveResponse conn concatHandler++-- | Extract correct temporary token from cookie header string.+extractSessionHeader :: B.ByteString -> Maybe B.ByteString+extractSessionHeader headerStr +  | B.null match = Nothing+  | otherwise =+    case extractSessionHeader (B.tail match) of+      Just match' -> Just match'+      Nothing -> Just $ B.takeWhile (/= ';') match+  where+  (_, match) = B.breakSubstring "PHPSESSID" headerStr++-- | Authenticate an existing connection, returns a temporary token.+--   Basically the API token is used to acquire a session-specific token.+authenticate :: ConnEnv IO B.ByteString+authenticate = do+  conf <- lift $ lift S.get++  header <- tryIO . buildRequest $ do+    http POST ("/" <> loginPage conf)+    defaultRequest+    setContentType "application/x-www-form-urlencoded"++  let formData = [("token", apiKey conf), ("user", user conf), ("script", "true")] +  conn <- S.get+  tryIO . sendRequest conn header $ encodedFormBody formData++  (headers, response) <- tryIO $ receiveResponse conn (\headers stream -> do+    response <- readExactly (B.length loginSuccess) stream+    return (headers, response))++  tryAssert ("Login failure. Server returned: '" <> response <> "'")+    (response == loginSuccess)+    +  noteT "Failed to parse login cookie" . hoistMaybe $ +    getHeader headers "Set-Cookie" >>= extractSessionHeader++-- | Retrieve problem ID of a Kattis problem.+retrieveProblemId :: KattisProblem -> ConnEnv IO Integer+retrieveProblemId (ProblemId id') = return id'+retrieveProblemId (ProblemName _) = undefined++-- | Retrieve problem name of a Kattis problem.+retrieveProblemName :: KattisProblem -> ConnEnv IO B.ByteString+retrieveProblemName (ProblemId _) = undefined+retrieveProblemName (ProblemName name) = return name
+ katt-tests/src/TestSourceHandler.hs view
@@ -0,0 +1,56 @@+module Main where++import Control.Applicative ((<$>))+import Control.Monad+import Data.List (subsequences)+import Data.Monoid ((<>))+import System.Exit (exitFailure, exitSuccess)+import System.Directory (getDirectoryContents)+import System.IO+import Utils.Katt.SourceHandler+import Utils.Katt.Utils++main :: IO ()+main = do+  res <- verifyLanguages+  if res then exitSuccess else exitFailure++-- Given a set of source files, categorized into folders corresponding to all supported languages.+-- Test the following things:+-- (1) That the corresponding languages are actually identified correctly, also when intermixed.+-- (2) That all main method classes are identified correctly in the case of java and python++languages :: [(KattisLanguage, String)]+languages = [(LangCplusplus, "c++"), (LangC, "c"), (LangJava, "java")]++testDir :: String+testDir = "katt-tests/data/sample_submissions/"++verifyLanguages :: IO Bool+verifyLanguages = and <$> mapM (\lang -> liftM2 (&&) (verifyLanguage lang) (verifyMain lang)) languages+  where+  verifyMain info@(LangJava, folder) = do+    tests <- getFileNamesSimple $ testDir <> folder+    res <- mapM (\test -> findMainClass ([test], fst info)) tests+    if Nothing `notElem` res then return True else do+      hPutStrLn stderr $ "Failed to verify SourceHandler: main class for language " <> show (fst info)+      return False++  verifyMain _ = return True++  verifyLanguage (identifier, folder)= do+    tests <- getFileNames $ testDir <> folder+    let res = map determineLanguage tests+    if Nothing `notElem` res then return True else do+      hPutStrLn stderr $ "Failed to verify SourceHandler: language " <> show identifier+      return False++  filterNonFiles = filter (`notElem` [".", ".."])+  addFolder folder = map ((folder <> "/") <>)++  getFileNames folder = map (addFolder folder) <$> files+    where files = map filterNonFiles . drop 1 . subsequences+                          <$> getDirectoryContents folder++  getFileNamesSimple folder = addFolder folder <$> files+    where files = filterNonFiles <$> getDirectoryContents folder
katt.cabal view
@@ -1,5 +1,5 @@ name:                katt-version:             0.1.0.0+version:             0.2.0.0 stability:           experimental  synopsis:            Client for the Kattis judge system.@@ -31,7 +31,17 @@                      .                      Please note that the beta release is limited to C, C++ and Java.                      It also only supports running on unix.+                     .+                     Changes since last release:+                     .+                       * Fix issues related to to new keep-alive handling+                     .+                       * New test suite+                     .+                       * Confirmed working with open kattis (www.kattis.com)+                     . + homepage:            https://github.com/davnils/katt bug-reports:         https://github.com/davnils/katt/issues @@ -46,20 +56,21 @@ build-type:          Simple cabal-version:       >=1.8 +-- Support building on earlier GHC, at least 7.4.1 and 7.4.2.+-- The current solution does not scale very well since dependencies+-- are listed under each section (lib, exe, test1, test2, ..)+-- see https://github.com/haskell/cabal/issues/417+-- and the related discussion. -executable katt-  hs-source-dirs:    src-  main-is:           Main.hs-  other-modules:     Init, Upload, Configuration, Utils, SourceHandler-  ghc-options:       -O2 -threaded -Wall+library+  hs-source-dirs:    katt-lib/src+  exposed-modules:   Utils.Katt.Configuration,+                     Utils.Katt.Init,+                     Utils.Katt.SourceHandler,+                     Utils.Katt.Upload,+                     Utils.Katt.Utils -  -- Support building on earlier GHC, at least 7.4.1 and 7.4.2-  if impl(ghc <= 7.4.1)-    build-depends:  -                     regex-posix == 0.95.1-  else-    build-depends:  -                     regex-posix == 0.95.*+  ghc-options:       -Wall    if impl(ghc <= 7.4.2)     build-depends:  @@ -81,11 +92,74 @@                      http-streams == 0.6.*,                      HsOpenSSL >= 0.10.3 && < 0.11,                      io-streams,-                     mtl == 2.1.*,+                     mtl >= 2.0 && < 2.2,                      parsec == 3.1.*,                      unix == 2.6.*,                      url == 2.1.*,                      zip-archive == 0.1.*++executable katt+  hs-source-dirs:    katt-cli/src+  main-is:           Main.hs+  ghc-options:       -O2 -threaded -Wall++  if impl(ghc <= 7.4.2)+    build-depends:  +                     base == 4.5.*,+                     bytestring >= 0.9.2.1+  else+    build-depends:  +                     base == 4.6.*,+                     bytestring == 0.10.*+  +  build-depends:     +                     http-streams == 0.6.*,+                     HsOpenSSL >= 0.10.3 && < 0.11,+                     katt,+                     mtl >= 2.0 && < 2.2++-- Test-Suite test-katt-init+--   type:              exitcode-stdio-1.0+--   hs-source-dirs:    katt-tests/src+--   main-is:           TestInit.hs+--   ghc-options:       -O2 -threaded -Wall+-- +--   if impl(ghc <= 7.4.2)+--     build-depends:  +--                      base == 4.5.*,+--                      bytestring >= 0.9.2.1+--   else+--     build-depends:  +--                      base == 4.6.*,+--                      bytestring == 0.10.*+--   +--   build-depends:     +--                      http-streams == 0.6.*,+--                      HsOpenSSL >= 0.10.3 && < 0.11,+--                      katt,+--                      mtl >= 2.0 && < 2.2++Test-Suite test-katt-sourcehandler+  type:              exitcode-stdio-1.0+  hs-source-dirs:    katt-tests/src+  main-is:           TestSourceHandler.hs+  ghc-options:       -O2 -threaded -Wall++  if impl(ghc <= 7.4.2)+    build-depends:  +                     base == 4.5.*,+                     bytestring >= 0.9.2.1+  else+    build-depends:  +                     base == 4.6.*,+                     bytestring == 0.10.*+  +  build-depends:     +                     directory == 1.2.*,+                     http-streams == 0.6.*,+                     HsOpenSSL >= 0.10.3 && < 0.11,+                     katt,+                     mtl >= 2.0 && < 2.2  source-repository head   type: git
− src/Configuration.hs
@@ -1,119 +0,0 @@-{-# Language OverloadedStrings, NoMonomorphismRestriction #-}------------------------------------------------------------------------- |--- Module : Configuration------ Implements loading and saving of global and local configurations.--- All configurations are stored in the 'ConfigFile' format, which is--- fully compatible with the official Kattis configuration file.------ The global configuration file corresponds to /kattisrc/, which holds--- information regarding authentication and hosts.------ The local configuration holds project-specific information and--- is created by the 'Init' submodule.--- Currently only the problem name is stored.--module Configuration (loadGlobalConfig, projectConfigExists, loadProjectConfig, saveProjectConfig) where--import Control.Error hiding (tryIO)-import Control.Monad-import qualified Control.Monad.State as S-import Control.Monad.Trans (liftIO, lift)-import qualified Data.ByteString.Char8 as B-import Data.ConfigFile-import Data.Monoid ((<>))-import qualified Network.URL as U-import System.Directory (getHomeDirectory)-import System.IO-import System.IO.Error (catchIOError)-import Utils---- | Path to global configuration file, relative home directory.-globalConfigFile :: FilePath-globalConfigFile  = ".kattisrc"---- | Path to project-specific (local) configuration file.-projectConfigFile :: FilePath-projectConfigFile = B.unpack $ configDir <> "/" <> "config"---- | Serialize error description into bytestring.-convertErrorDesc :: (Show a) => (a, String) -> B.ByteString-convertErrorDesc (errorData, str) = B.pack (show errorData) <> B.pack str---- | Retrieve a field from the configuration state using a key and section,---   possibly failing.-get' :: (Monad m, Get_C r) => ConfigParser -> SectionSpec -> String -> EitherT B.ByteString m r-get' conf section key = fmapLT-  (const . B.pack $ "Failed to parse " <> key <> " field")-  (get conf section key)---- | Load global configuration file and parse the configuration state.---   Ensures that all fields are present and validates the URLs.-loadGlobalConfig :: IO (Either ErrorDesc ConfigState)-loadGlobalConfig = runEitherT $ do-  home <- tryIO getHomeDirectory-  let filePath = home <> "/" <> globalConfigFile--  fileHandle <- tryIOMsg ("Failed to open " <> B.pack filePath) $-    openFile filePath ReadMode-  conf <- fmapLT convertErrorDesc $ join . liftIO $ readhandle emptyCP fileHandle--  user' <- get' conf "user" "username"-  apiKey' <- get' conf "user" "token"--  loginURL <- get' conf "kattis" "loginurl"-  loginParsed <- extract "Failed to parse loginurl field" loginURL--  submitURL <- get' conf "kattis" "submissionurl"-  submitParsed <- extract "Failed to parse submissionurl field" submitURL--  host' <- parseHost loginParsed--  tryIOMsg "Failed to close configuration file handle" $ hClose fileHandle--  return $ ConfigState-    (B.pack user')-    (B.pack apiKey')-    (B.pack $ "https://" <> host')-    (B.pack $ U.url_path loginParsed)-    (B.pack $ U.url_path submitParsed)-    Nothing--  where-  extract msg = noteT msg . hoistMaybe . U.importURL-  parseHost (U.URL (U.Absolute host') _ _) = return $ U.host host'-  parseHost _ = left "Invalid URL format"---- | Check if a project-specific configuration file exists and can be read.-projectConfigExists :: IO Bool-projectConfigExists = catchIOError open . const $ return False-  where-  open = withFile projectConfigFile ReadMode . const $ return True---- | Load a project-specific configuration based on the current directory.-loadProjectConfig :: ConfigEnv IO ()-loadProjectConfig = do-  conf <- fmapLT convertErrorDesc $ join . liftIO $ readfile emptyCP projectConfigFile-  problem <- get' conf "problem" "problemname"-  lift . S.modify $ \s -> s { project = Just . ProblemName $ B.pack problem}---- | Save a project-specific configuration file to disk.-saveProjectConfig :: ConnEnv IO ()-saveProjectConfig = do-  project' <- lift . lift $ S.gets project-  state <- noteT "Tried to save an empty project configuration." . hoistMaybe $ project'-  problemName <- retrieveProblemName state-  serialized <- serialize problemName-  tryIOMsg (B.pack $-            "Failed to save project configuration file (path: "-            <> projectConfigFile <> ")") $-    writeFile projectConfigFile (to_string serialized)--  where-  serialize problemName = EitherT . return . fmapL convertErrorDesc $ do-    let conf = emptyCP-    conf' <- add_section conf "problem"-    conf'' <- add_section conf' "submissions"-    set conf'' "problem" "problemname" $ B.unpack problemName
− src/Init.hs
@@ -1,197 +0,0 @@-{-# Language OverloadedStrings, ScopedTypeVariables,-    NoMonomorphismRestriction #-}------------------------------------------------------------------------- |--- Module : 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 Init (initializeProblem, initializeSession) where--import Control.Applicative ((<$>), (<*))-import Codec.Archive.Zip-import Control.Arrow ((***))-import qualified Control.Monad.State as S-import qualified Configuration as C-import Control.Error hiding (tryIO)-import qualified Control.Exception as E-import Control.Monad.Reader-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 System.Directory-import System.IO (stderr)-import Text.Parsec hiding (token)-import Text.Parsec.ByteString-import 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 "<a href='"-  endLink name = string ">" >> string name >> string "</a>"-  endParser = manyTill anyChar (char '\'') <* endLink "Download"---- | Try to parse test cases embedded into HTML data.---   Currently only table-style test cases are supported (e.g. problem 'friends').-parseEmbedded :: GenParser Char st TestParser-parseEmbedded = TestContents <$> 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 "<table class=\"sample\" summary=\"sample data\">"-  endTable  = void $ string "</table>"--  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 -> ConnEnv IO TestContent-downloadTestArchive url = do-  zipFile <- BL.fromChunks . return <$> retrievePublicPage url-  zipEntries <- tryIOMsg "Failed to unpack zip file: corrupt archive" $-    E.evaluate (zEntries $ toArchive zipFile)--  let filterFiles suffix = filter (isSuffixOf suffix . 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 . fromEntry---- | Retrieve test cases, which fall into either one of the three categories.-retrieveTestFiles :: KattisProblem -> ConnEnv IO TestContent-retrieveTestFiles problem = do-  problemName <- 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 "<a href='problems/"-  skip = manyTill anyChar (void (try beginLink) <|> eof)-  tag = do-    problem <- manyTill (letter <|> digit) (char '\'')-    return . ProblemName $ B.pack problem---- | Given a problem session id, initialize all the corresponding problems.-initializeSession :: Bool -> ProblemSession -> ConnEnv IO ()-initializeSession retrieveTests session = do-  contents <- retrievePublicPage $ sessionPage <> (B.pack $ show session)-  problems <- nub <$> (EitherT . return  . fmapL (B.pack . show) $ parse' contents)-  mapM_ (\problem -> initializeProblem True retrieveTests problem >> restoreDir) problems-  where-  parse' contents = parse (parseProblemList) "Problem list parser" contents-  restoreDir = liftIO $ setCurrentDirectory ".."-  -- TODO: restore to previously used directory---- | Given a problem identifier, setup directory structures and---   optionally download test cases.-initializeProblem :: Bool -> Bool -> KattisProblem -> ConnEnv IO ()-initializeProblem mkDir retrieveTests problem = do-  liftIO . putStrLn $ "Initializing problem: " <> show problem-  problemName <- retrieveProblemName problem-  tryIO . when mkDir $ do-    createDirectoryIfMissing False (B.unpack problemName)-    setCurrentDirectory (B.unpack problemName)--  tryIO $ createDirectoryIfMissing False (B.unpack configDir)--  fileExists <- liftIO C.projectConfigExists-  tryAssert-    "Project configuration file already exists, please remove it in order to continue."-    (not fileExists)--  lift . lift . S.modify $ \s -> s { project = Just $ ProblemName problemName }-  C.saveProjectConfig--  when retrieveTests $ do-    tryIO $ 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
− src/Main.hs
@@ -1,78 +0,0 @@-{-# Language OverloadedStrings #-}------------------------------------------------------------------------- |--- Module : Main------ Entry point of the program.------ Begins by locating a global configuration file, and upon success--- parses the given arguments and executes the corresponding submodule.-----module Main(main) where--import qualified Configuration as C-import Control.Monad.State-import qualified Data.ByteString.Char8 as B-import Data.Monoid ((<>))-import Init-import Network.Http.Client-import OpenSSL (withOpenSSL)-import System.Environment-import System.Exit (exitFailure)-import Upload-import Utils---- | Main loads the global config and runs argument parsing.-main :: IO ()-main = do-  conf <- C.loadGlobalConfig-  conf' <- case conf of-    Left err -> do-      B.putStrLn $ "Kattis configuration error: " <> err-      printHelp-      exitFailure-    Right c -> return c--  withOpenSSL $ parseArgs conf'---- | Given some configuration state, parse arguments and---   run the appropiate submodule.---   Output help text upon failure to parse arguments.-parseArgs :: ConfigState -> IO ()-parseArgs conf = getArgs >>= parse-  where-  parse :: [String] -> IO ()-  parse ("init" : problem : []) = withConn conf . initializeProblem True True . ProblemName $ B.pack problem-  parse ("init-session" : session : []) = withConn conf . initializeSession True $ read session-  parse ("submit" : filterList) = withConn conf $ makeSubmission filterList-  parse _ = printHelp---- | Print help text.-printHelp :: IO ()-printHelp = putStrLn $-  "The following commands are available:\n\n" <>-  "init <problem>\n" <>-  "  Create the corresponding directory and download any available tests.\n\n" <>-  "init-session <session>\n" <>-  "  Initialize all problems associated to the problem session, given as an integer id.\n\n" <>-  "submit [+add_file] [-skip_file]\n" <>-  "  Make a submission using the problem name read from a local config.\n" <>-  "  Defaults to recursively including all source and header files that can be found.\n" <>-  "  Use the optional filter arguments to include or exclude files.\n\n" <>-  "Note that you will need a valid configuration file (placed in ~/.kattisrc), such as:\n" <>-  "https://kth.kattis.scrool.se/download/kattisrc\n"---- | Given some action, initiate a connection and run it.---   Connections are layered using StateT since they may be reestablished.-withConn :: ConfigState -> ConnEnv IO a -> IO a-withConn conf action = do-  B.putStrLn $ "Connecting to host: " <> host conf-  conn <- establishConnection (host conf)-  ((res, conn'), _) <- runStateT (withConf conn) conf-  closeConnection conn'-  return res--  where-  withConf = runStateT (terminateOnFailure "Failed to run command" action)
− src/SourceHandler.hs
@@ -1,114 +0,0 @@-{-# Language OverloadedStrings #-}------------------------------------------------------------------------- |--- Module : SourceHandler------ Provides searching of source code files and language identification.------ Language identification is required in order to detect any--- inconsistencies (e.g. combining Java and C), and to tag submissions.------ Java also requires identifying which file provides the main method.--module SourceHandler (parseFilter, findFiles, determineLanguage, findMainClass, languageKattisName, languageContentType) where--import Control.Applicative ((<$>))-import Control.Arrow ((***))-import Control.Monad-import qualified Data.ByteString.Char8 as B-import Data.List-import qualified Data.Set as Set-import System.Directory-import System.FilePath (takeBaseName, takeExtension)-import Text.Parsec-import Text.Parsec.ByteString-import Utils---- | All supported source file extensions, per language.-supported :: KattisLanguage -> Set.Set FilePath-supported LangCplusplus = Set.fromList [".cc", ".cpp", ".hpp", ".h"]-supported LangC         = Set.fromList [".c", ".h"]-supported LangJava      = Set.fromList [".java"]---- | Parse an argument list from the +file1 -file2 style into---   two lists of file paths (included and ignored files).-parseFilter :: [String] -> Maybe ([FilePath], [FilePath])-parseFilter input = (filter' *** filter') <$> mapAndUnzipM go input-  where-  go ('+' : file) = Just (file, "")-  go ('-' : file) = Just ("", file)-  go _ = Nothing-  filter' = filter (not . null)---- | Locate all source files recursively from the current directory.-findFiles :: IO [FilePath]-findFiles = explore "" "."-  where-  explore prefix dir = do-    contents <- (\\ [".", ".."]) <$> getDirectoryContents dir-    let withPrefix = map (prefix++) contents--    dirs <- filterM doesDirectoryExist withPrefix-    let sourceFiles = filter isValidSourceFile (withPrefix \\ dirs)-    nextDepth <- mapM exploreDir dirs-    return $ sourceFiles ++ concat nextDepth--  isValidSourceFile file = any (`isSuffixOf` file)-    (Set.toList . Set.unions $ map supported [LangCplusplus, LangC, LangJava])-  exploreDir dir = explore (dir ++ "/") dir---- | Determine source code language by studying file extensions.---   There is an implicit priority ordering, since C is a subset of C++.-determineLanguage :: [FilePath] -> Maybe KattisLanguage-determineLanguage files-  | is LangC = Just LangC-  | is LangCplusplus = Just LangCplusplus-  | is LangJava = Just LangJava-  | otherwise = Nothing-  where-  fileSet = Set.fromList $ map takeExtension files-  is lang = fileSet `Set.isSubsetOf` (supported lang)---- | Locate main class based on source file contents.---   C++ and C solutions do not need to be specified, returns an empty string.------   In the Java case all souce code files are parsed.---   All occurences of a /main/ method defined with /public static void/ are located.------   Will return 'Data.Maybe.Nothing' if result is ambiguous.-findMainClass :: ([FilePath], KattisLanguage) -> IO (Maybe FilePath)-findMainClass ([], _)            = return Nothing-findMainClass (_, LangCplusplus) = return $ Just ""-findMainClass (_, LangC)         = return $ Just ""-findMainClass (files, LangJava)  = survey <$> filterM containsMain files-  where-  containsMain file = do-    parseResult <- parseFromFile mainParser file-    case parseResult of-      Right _ -> return True-      Left _ -> return False--  mainParser = manyTill-    (lineComment <|> blockComment <|> stringData <|> void anyChar)-    mainFunc-  blockComment = void $ string "/*" >> manyTill anyChar (try $ string "*/")-  lineComment = void . try $ string "//" >> manyTill anyChar newline-  stringData = void $ char '"' >> manyTill anyChar (char '"')-  mainFunc = try $ mapM_ keyWord ["public", "static", "void", "main"]-  keyWord str = void $ string str >> spaces--  survey [singleton] = Just $ takeBaseName singleton-  survey _ = Nothing---- | Determine content type of submission language.-languageContentType :: KattisLanguage -> B.ByteString-languageContentType LangCplusplus = "text/x-c++src"-languageContentType LangJava = "text/x-c++src"-languageContentType LangC = "text/x-c++src"---- | Determine Kattis language string identifier.-languageKattisName :: KattisLanguage -> B.ByteString-languageKattisName LangCplusplus = "C++"-languageKattisName LangJava = "Java"-languageKattisName LangC = "C"
− src/Upload.hs
@@ -1,314 +0,0 @@-{-# Language OverloadedStrings, ScopedTypeVariables #-}------------------------------------------------------------------------- |--- Module : Upload------ Upload submodule providing submissions of solutions and parsing of results.------ A submission is done by including all recursively found files and filtering--- using a file filter given as an argument.--- This is followed by polling for a submission result until some final--- submission state has been reached (e.g. accepted).------ Currently multipart data upload is implemented since https-streams--- (the HTTP client being used) does not support it (yet?).--module Upload (makeSubmission) where--import Control.Applicative ((<$>))-import Blaze.ByteString.Builder (fromByteString)-import qualified Configuration as C-import Control.Concurrent (threadDelay)-import Control.Error hiding (tryIO)-import Control.Monad.Reader-import qualified Control.Monad.State as S-import qualified Data.ByteString.Char8 as B-import Data.List ((\\), union, findIndex)-import Data.Maybe (fromJust)-import Data.Monoid ((<>))-import Network.Http.Client-import SourceHandler-import System.IO.Streams (write)-import Text.Parsec hiding (token)-import Text.Parsec.ByteString-import Text.Regex.Posix-import Utils---- | Line separator used in HTTP headers. -crlf :: B.ByteString-crlf = "\r\n"---- | Multipart field consisting of an option or file.-data MultiPartField-  -- | Option built using a list of assignments and a payload.-  = Option [B.ByteString] B.ByteString-  -- | File to submitted, path given.-  | File FilePath---- | Serialize a chunk based on a file to be submitted.-buildChunk :: B.ByteString -> MultiPartField -> IO B.ByteString-buildChunk langStr (File path) = do-  file <- B.readFile path-  return $ B.intercalate crlf [headerLine, "Content-Type: " <> langStr, "", file, ""]-  where-    headerLine = B.intercalate "; " ["Content-Disposition: form-data", "name=\"sub_file[]\"",-                                     B.concat ["filename=\"", B.pack path, "\""]]---- | Serialize a chunk based on an option.-buildChunk _ (Option fields payload) = return $ B.intercalate crlf [headerLine, "", payload, ""]-  where-    headerLine = B.intercalate "; " fieldList-    fieldList = "Content-Disposition: form-data" : fields---- | Submission page URL, relative 'Utils.host', from which specific submission can be requested.-submissionPage :: B.ByteString-submissionPage = "submission"---- | Possible states of a submission, with unknowns being grouped into 'Other'.-data SubmissionState-  -- | Submission is queued.-  = Queued-  -- | Submission is compiling.-  | Compiling-  -- | Submission is running.-  | Running-  -- | Wrong answer.-  | WrongAnswer-  -- | Time limit exceeded.-  | TimeLimitExceeded-  -- | Submission was accepted (only success state).-  | Accepted-  -- | Compile error.-  | CompileError-  -- | Run time error.-  | RunTimeError-  -- | Some other, unmatched error code. Only used when parsing fails.-  | Other-  deriving (Eq, Show)---- | Possible states of a single test case, i.e. an (input, output) data pair.-data TestCase-  -- | Test case passed.-  = TestPassed-  -- | Test case failed (state /= Accepted)-  | TestFailed-  -- | Test case has not been executed.-  | NotTested-  deriving (Eq, Show)---- | Check if a given state is final, i.e. won't transition into some other.---   Note that 'Other' is listed as final.-finalSubmissionState :: SubmissionState -> Bool-finalSubmissionState s = elem s-  [WrongAnswer, TimeLimitExceeded, Accepted, CompileError, RunTimeError, Other]---- | Make a submission of the project in the working directory.---   Accepts a list of filters on the form /+file1 -file2 ../, which are---   taken into account when locating all the source files.---   /+file/ implies adding the specified file.---   /-file/ implies removing the specified file.------   In addition to the filters, all recursively found source code files---   will be included in the submission.-makeSubmission :: [String] -> ConnEnv IO ()-makeSubmission filterArguments = do-  exists <- liftIO C.projectConfigExists-  tryAssert "No project configuration could be found."-    exists--  unWrapTrans C.loadProjectConfig-  problem <- lift . lift $ (fromJust <$> S.gets project)--  -- Locate all source files, filter based on filter list.-  files <- tryIOMsg "Failed to locate source files" findFiles-  let adjusted = adjust (parseFilter filterArguments) files--  liftIO $ mapM_ (putStrLn . ("Adding file: "++)) adjusted--  -- Authenticate, submit files, and retrieve submission id.-  token <- authenticate --  submission <- EitherT $ runReaderT-    (runEitherT $ submitSolution (problem, adjusted)) token--  tryIO . putStrLn $ "Made submission: " <> show submission-  tryIO $ threadDelay initialTimeout-  reestablishConnection--  -- Poll submission page until completion.-  token' <- authenticate -  EitherT $ runReaderT-    (runEitherT $ checkSubmission submission) token'--  where-  adjust Nothing files = files-  adjust (Just (add, sub)) files = union (files \\ sub) add--  -- Initial timeout before requesting updates is 2 s.-  initialTimeout = 2000000---- | Poll kattis for updates on a submission.---  This function returns when the submission has reached one of the final states.---  TODO: Consider exponential back-off and timeout-checkSubmission :: SubmissionId -> AuthEnv IO ()-checkSubmission submission = do-  page <- retrievePrivatePage $-    "/" <> submissionPage <> "?id=" <> B.pack (show submission)-  let (state, tests) = parseSubmission page--  if finalSubmissionState state-    then-      tryIO $ printResult tests state-    else do-      tryIO $ putStrLn "Waiting for completion.." >> threadDelay interval-      unWrapTrans reestablishConnection-      checkSubmission submission-   -  where-  -- Default poll interval is 1 s.-  interval = 1000000---- | Parse the supplied submission page into:---   (1) Current submission state---   (2) Status of all test cases-parseSubmission :: B.ByteString -> (SubmissionState, [TestCase])-parseSubmission contents =-  case res of-    Left err' -> error $ "Internal parser error" <> show err'-    Right res' -> res'-  where-  res = parse parser "Submission parser" contents-  parser = liftM2 (,) parseStatus parseTestCases---- | String separator parser.-strSep :: GenParser Char st ()-strSep = void (char '\'' <|> char '"')---- | End-of-tag parser, ignores everything up to the end of the current tag.-endTag :: GenParser Char st ()-endTag = void $ manyTill anyChar (char '>')---- | Parse the submission status field, beginning from any offset in the page data.-parseStatus :: GenParser Char st SubmissionState-parseStatus = skip >> status-  where-  beginStatus = do-    void $ string "<td class="-    strSep >> string "status" >> strSep >> endTag-    void $ string "<span class=" >> strSep--  -- Skip to the appropiate <td> tag.-  skip = manyTill anyChar (void (try beginStatus) <|> eof)--  -- Parse contents in <td>...</td>.-  -- TODO: check if manyTill can be rewritten to the endTag pattern-  status = do-    void $ manyTill anyChar strSep-    endTag-    statusStr <- manyTill (letter <|> space) (char '<')-    return $ conv statusStr--  conv "Time Limit Exceeded" = TimeLimitExceeded-  conv "Wrong Answer" = WrongAnswer-  conv "Accepted" = Accepted-  conv "Memory Limit Exceeded" = Other-  conv "Compiling" = Compiling-  conv "Running" = Running-  conv "Compile Error" = CompileError-  conv "Run Time Error" = RunTimeError-  conv _ = Other---- | Parse the status of all test cases, beginning from any offset in the page data.---   May return zero test cases when a submission fails---   with certain status values, e.g. /Compile Error/.-parseTestCases :: GenParser Char st [TestCase] -parseTestCases = skip >> tests-  where-  beginTests = do-    void $ string "<div class="-    strSep >> string "testcases" >> strSep-    endTag--  -- Locate surrounding div tag.-  skip = manyTill anyChar (void (try beginTests) <|> eof)--  -- Parse all test cases.-  tests = many testCase--  -- Each test case is basically <span [class="status"]>...</span> -  -- where a missing class attribute implies that it hasn't been executed.-  testCase = do-    void . try $ string "<span "-    classResult <- optionMaybe $ do-      string "class=" >> strSep-      manyTill anyChar strSep--    void . manyTill anyChar $ string "</span>"-    fromMaybe (return NotTested) (mapResult <$> classResult)--  mapResult "accepted" = return TestPassed-  mapResult "rejected" = return TestFailed-  mapResult _ = parserZero---- | Print the result of a submission.---   Will also take care of the special case when no test cases were parsed.-printResult :: [TestCase] -> SubmissionState -> IO ()-printResult tests state-  | state == Accepted = putStrLn $ "Accepted, " <> numTests <> " test(s) passed."-  | null tests = putStrLn resultStr-  | otherwise = putStrLn $ resultStr <> testCaseStr-  where-  numTests = show $ length tests-  firstFailed = show . (+1) . fromMaybe 0 $ findIndex (/= TestPassed) tests-  resultStr = "Result: " <> show state-  testCaseStr = ", failed on test case " <> firstFailed <> " of " <> numTests----- | Submit a solution, given problem name and source code files.-submitSolution :: Submission -> AuthEnv IO SubmissionId-submitSolution (problem, files) = do-  -- Determine language in submission.-  language <- noteT ("\nFailed to decide submission language\n" <>-                    "Please use either Java or some union of C++ and C")-    . hoistMaybe $ determineLanguage files-  let languageStr = languageKattisName language--  mainClassStr <- join . liftIO $-    (noteT "Failed to locate the \"public static void main\" method - is there any?" . hoistMaybe)-      <$> findMainClass (files, language)--  -- Build HTTP headers and form.-  let multiPartSeparator = "separator"--  conf <- lift . lift $ lift S.get-  header <- makeSignedRequest $ do-    http POST ("/" <> submitPage conf)-    defaultRequest-    setContentType $ B.append "multipart/form-data; boundary=" multiPartSeparator--  problemName <- unWrapTrans $ retrieveProblemName problem--  let postFields = [Option ["name=\"submit\""] "true"]-                <> [Option ["name=\"submit_ctr\""] "2"]-                <> [Option ["name=\"language\""] languageStr]-                <> [Option ["name=\"mainclass\""] (B.pack mainClassStr)]-                <> [Option ["name=\"problem\""] problemName]-                <> [Option ["name=\"tag\""] ""]-                <> [Option ["name=\"script\""] "true"]-                <> map File files--  -- Send request.-  conn <- lift . lift $ S.get-  tryIO $ sendRequest conn header (\o -> do-    mapM_ (\part -> do-        serialized <- buildChunk (languageContentType language) part-        write (Just . fromByteString $ B.concat ["--", multiPartSeparator, crlf, serialized]) o)-      postFields--    write (Just . fromByteString $ B.concat ["--", multiPartSeparator, "--", crlf]) o-    )--  -- Receive server response.-  reply <- tryIO $ receiveResponse conn concatHandler-  tryRead "Failed to parse submission id from server" . B.unpack $ reply =~ ("[0-9]+" :: B.ByteString)
− src/Utils.hs
@@ -1,229 +0,0 @@-{-# Language OverloadedStrings, ScopedTypeVariables,-    NoMonomorphismRestriction #-}------------------------------------------------------------------------- |--- Module : Utils------ Contains shared type declarations and various utility functions.-----module Utils where--import Control.Applicative ((<$>))-import Control.Error hiding (tryIO)-import qualified Control.Exception as E-import Control.Monad.Reader-import qualified Control.Monad.State as S-import qualified Data.ByteString.Char8 as B-import Data.Monoid ((<>))-import Network.Http.Client-import System.Exit (exitFailure)-import System.IO (stderr)-import System.IO.Streams (readExactly)---- | Configuration layer consisting of configuration state.-type ConfigEnvInternal m = S.StateT ConfigState m--- | Configuration layer wrapped with error handling.-type ConfigEnv m = EitherT ErrorDesc (ConfigEnvInternal m)---- | Connection layer with connection state layered on the configuration layer.-type ConnEnvInternal m = S.StateT Connection (ConfigEnvInternal m)--- | Connection layer wrapped with error handling.-type ConnEnv m = EitherT ErrorDesc (ConnEnvInternal m)---- | Authentication layer with token state and error handling,---   wrapping the connection layer.-type AuthEnv m = EitherT ErrorDesc (ReaderT B.ByteString (ConnEnvInternal m))---- | Submissions consist of a problem identifier and a set of file paths.-type Submission = (KattisProblem, [FilePath])---- | Error description alias.-type ErrorDesc = B.ByteString---- | Submissions are identified with an integer id.-type SubmissionId = Integer------ | Problem sessions are identified with an integer id.-type ProblemSession = Integer---- | Project-specific state consists of the problem name.-type ProjectState = (KattisProblem)---- | Global configuration, initialized from the /.kattisrc/ file.-data ConfigState =-  ConfigState {-    -- | Username.-    user :: B.ByteString,-    -- | API token (hash).-    apiKey :: B.ByteString,-    -- | Host to be considered as service.-    host :: B.ByteString,-    -- | URL to login page, relative 'host'.-    loginPage :: B.ByteString,-    -- | URL to submit page, relative 'host'.-    submitPage :: B.ByteString,-    -- | Project-specific state, optionally loaded.-    project :: Maybe ProjectState-  }-  deriving Show---- | A Kattis problem.-data KattisProblem-  -- | Problem ID, unique.-  = ProblemId Integer-  -- | Associated name of the problem.-  | ProblemName B.ByteString-  deriving (Eq, Show)---- | Language used in submission.-data KattisLanguage-  -- | C++.-  = LangCplusplus-  -- | Java.-  | LangJava-  -- | C.-  | LangC-  deriving (Eq, Show)---- | Server response indicating successful login.-loginSuccess :: B.ByteString-loginSuccess = "Login successful"---- | Extension of input test files.-inputTestExtension :: FilePath-inputTestExtension = ".in"---- | Extension of reference ouput test files.-outputTestExtension :: FilePath-outputTestExtension = ".ans"---- | Name of this program.-programName :: B.ByteString-programName = "katt"---- | Relative path to project-specific configuration directory.-configDir :: B.ByteString-configDir = "." <> programName---- | Relative path to folder containing tests.-testFolder :: FilePath-testFolder = "tests"---- | URL to page with problem information, relative to 'host'.-problemAddress :: B.ByteString-problemAddress = "/problems/"---- | Lift some error monad one layer.-unWrapTrans :: (Monad m, MonadTrans t) => EitherT e m a -> EitherT e (t m) a-unWrapTrans = EitherT . lift . runEitherT---- | Execute an IO action and catch any exceptions.-tryIO :: MonadIO m => IO a -> EitherT ErrorDesc m a-tryIO = EitherT . liftIO . liftM (fmapL (B.pack . show)) . -  (E.try :: (IO a -> IO (Either E.SomeException a)))---- | Execute an IO action and catch any exceptions, tagged with description.-tryIOMsg :: MonadIO m => B.ByteString -> IO a -> EitherT ErrorDesc m a-tryIOMsg msg = EitherT . liftIO . liftM (fmapL $ const msg) . -  (E.try :: (IO a -> IO (Either E.SomeException a)))---- | Evaluate an error action and terminate process upon failure.-terminateOnFailure :: MonadIO m => ErrorDesc -> EitherT ErrorDesc m a -> m a-terminateOnFailure msg state = do-  res <- runEitherT state-  liftIO $ case res of-    Left errorMessage -> do-      B.hPutStrLn stderr $ msg <> ", error: " <> errorMessage-      exitFailure-    Right success -> return success---- | Sign an existing HTTP request with a temporary token.-makeSignedRequest :: RequestBuilder () -> AuthEnv IO Request-makeSignedRequest req = do-  key <- lift $ liftM (setHeader "Cookie") ask-  liftIO . buildRequest $ req >> key---- | Default HTTP request.-defaultRequest :: RequestBuilder ()-defaultRequest = do-  setHeader "User-Agent" programName-  setHeader "Connection" "keep-alive"---- | Reestablish an existing connection.---   Useful in order to avoid timeouts related to keep-alive.-reestablishConnection :: ConnEnv IO ()-reestablishConnection = do-  conn <- lift S.get-  tryIOMsg "Failed to close connection" $ closeConnection conn-  host' <- host <$> lift (lift S.get)-  conn' <- tryIOMsg "Failed to reestablish connection" $ establishConnection host'-  lift $ S.put conn'---- | Retrieve a publically available page, using HTTP GET.-retrievePublicPage :: B.ByteString -> ConnEnv IO B.ByteString-retrievePublicPage page = do-  header <- tryIO . buildRequest $ http GET page >> defaultRequest-  makeRequest header---- | Retrieve a page requiring authentication, using HTTP GET.-retrievePrivatePage :: B.ByteString -> AuthEnv IO B.ByteString-retrievePrivatePage page = do-  header <- makeSignedRequest $ do-    http GET page-    defaultRequest-  unWrapTrans $ makeRequest header---- | Make a HTTP request and retrieve the server response body.-makeRequest :: Request -> ConnEnv IO B.ByteString-makeRequest header = do-  conn <- lift S.get-  tryIO $ sendRequest conn header emptyBody-  tryIO $ receiveResponse conn concatHandler---- | Extract correct temporary token from cookie header string.-extractSessionHeader :: B.ByteString -> Maybe B.ByteString-extractSessionHeader headerStr -  | B.null match = Nothing-  | otherwise =-    case extractSessionHeader (B.tail match) of-      Just match' -> Just match'-      Nothing -> Just $ B.takeWhile (/= ';') match-  where-  (_, match) = B.breakSubstring "PHPSESSID" headerStr---- | Authenticate an existing connection, returns a temporary token.---   Basically the API token is used to acquire a session-specific token.-authenticate :: ConnEnv IO B.ByteString-authenticate = do-  conf <- lift $ lift S.get--  header <- tryIO . buildRequest $ do-    http POST ("/" <> loginPage conf)-    defaultRequest-    setContentType "application/x-www-form-urlencoded"--  let formData = [("token", apiKey conf), ("user", user conf), ("script", "true")] -  conn <- S.get-  tryIO . sendRequest conn header $ encodedFormBody formData--  (headers, response) <- tryIO $ receiveResponse conn (\headers stream -> do-    response <- readExactly (B.length loginSuccess) stream-    return (headers, response))--  tryAssert ("Login failure. Server returned: '" <> response <> "'")-    (response == loginSuccess)-    -  noteT "Failed to parse login cookie" . hoistMaybe $ -    getHeader headers "Set-Cookie" >>= extractSessionHeader---- | Retrieve problem ID of a Kattis problem.-retrieveProblemId :: KattisProblem -> ConnEnv IO Integer-retrieveProblemId (ProblemId id') = return id'-retrieveProblemId (ProblemName _) = undefined---- | Retrieve problem name of a Kattis problem.-retrieveProblemName :: KattisProblem -> ConnEnv IO B.ByteString-retrieveProblemName (ProblemId _) = undefined-retrieveProblemName (ProblemName name) = return name