(* Copyright (C) 2017 Ryan Kavanagh *) (* Distributed under the ISC license, see COPYING for details. *) functor Helper (structure C : sig val handinPath : string end) :> HELPER = struct exception MissingFile of string datatype checks = Check of string * (unit -> unit) | Problem of string * (unit -> real) val handinPath = C.handinPath fun printLn s = print (s ^ "\n") (* Puts strings in boxes... *) fun stringsInBox strs = let fun repeatChar c n = String.implode (List.tabulate (n, fn _ => c)) val maxLength = List.foldl (fn (s,themax) => Int.max (String.size s, themax)) 0 strs val edge = repeatChar #"#" (maxLength + 4) in edge :: (List.foldr (fn (s,thelist) => ("# " ^ s ^ (repeatChar #" " (maxLength - (String.size s))) ^ " #") :: thelist) [edge] strs) end (* Prints strings in boxes... *) fun printInBox strs = List.app printLn (stringsInBox strs) (* Generates an AutoLab json score string *) fun scoresToString (scores, scoreboard) = let val scoreStrings = map (fn (problem, score) => "\"" ^ problem ^ "\": " ^ (Real.toString score)) scores val scores = String.concatWith ", " scoreStrings val scoreboard = case scoreboard of SOME l => ", \"scoreboard\": [" ^ (String.concatWith ", " (map Int.toString l)) ^ "]" | NONE => "" in "{\"scores\": {" ^ scores ^ "}" ^ scoreboard ^ "}" end (* Aborts the program by printing strs, and gives an empty score. *) fun abortWithMessage strs = let val _ = List.app printLn strs val _ = printLn (scoresToString ([],NONE)) in OS.Process.exit OS.Process.success end (* Reads the lines of a file into a list *) (* Each string in the file will always be contain a newline (#"\n") at the end. *) fun readLines filename = let val inFile = TextIO.openIn filename fun readlines ins = case TextIO.inputLine ins of SOME ln => ln :: readlines ins | NONE => [] val lines = readlines inFile val _ = TextIO.closeIn inFile in lines end (* Check if the file exists *) fun checkFileExists (name : string) : unit = if OS.FileSys.access (name, [OS.FileSys.A_READ]) then () else raise MissingFile name fun joinHandinPath file = OS.Path.concat (handinPath, file) fun stripHandinPath path = if String.isPrefix handinPath path then String.extract (path, String.size handinPath + 1, NONE) else path (* Takes in a list of filenames, and checks if those files *) (* exist in the handinPath directory. *) (* Exits catastrophically if a file is missing. *) fun checkFilesExist filenames = List.app (checkFileExists o joinHandinPath) filenames handle MissingFile name => (abortWithMessage o stringsInBox) [ "File " ^ (stripHandinPath name) ^ " missing." , "Please make sure you included all required files and resubmit."] fun runCmd cmd = (printLn cmd; OS.Process.system cmd) (* Reads from fd in n byte chunks and treats it all as strings. *) fun readAllFDAsString (fd, n) = let val v = Posix.IO.readVec (fd, n) in if Word8Vector.length v = 0 then "" else (Byte.bytesToString v) ^ (readAllFDAsString (fd, n)) end (* Runs a command c (command and argument list) using Posix.Process.execp. *) (* Return the program's output as a string, along with its exit status. *) fun execpOutput (c : string * string list) : string * Posix.Process.exit_status = let val { infd = infd, outfd = outfd } = Posix.IO.pipe () in case Posix.Process.fork () of NONE => (* Child *) (( Posix.IO.close infd ; Posix.IO.dup2 { old = outfd, new = Posix.FileSys.stdout } ; Posix.IO.dup2 { old = outfd, new = Posix.FileSys.stderr } ; Posix.Process.execp c) handle OS.SysErr (err, _) => ( print ("Fatal error in child: " ^ err ^ "\n") ; OS.Process.exit OS.Process.failure )) | SOME pid => (* Parent *) let val _ = Posix.IO.close outfd val (_, status) = Posix.Process.waitpid (Posix.Process.W_CHILD pid, []) val output = readAllFDAsString (infd, 100) val _ = Posix.IO.close infd in (output, status) end end (* Check if a submitted file is a valid PDF using ghostscript. *) fun checkPDF pdf = let val spdf = joinHandinPath pdf in case ( runCmd ("gs -o/dev/null -sDEVICE=nullpage " ^ spdf) , Posix.FileSys.ST.size (Posix.FileSys.stat spdf) ) of (_,0) => let val _ = printInBox [ "Warning: The empty file " ^ pdf ^ " is not a valid PDF document." , "Please make sure to resubmit with a valid PDF document in its place." ] in () end | (0,_) => () | _ => (abortWithMessage o stringsInBox) [ "The file " ^ pdf ^ " is not a valid PDF document." , "Please resubmit with a valid PDF document (or an empty file) in its place." , "If you are convinced you submitted a valid PDF, please contact the course staff." ] end (* Runs all of the checks and grades all of the problems in "checks". *) fun runChecks (checks : checks list) = List.foldl (fn (cs,results) => case cs of (Check (n, c)) => let val _ = printLn ("\n\nRunning check " ^ n ^ "...") val _ = c () val _ = printLn " Success.\n" in results end | (Problem (n, c)) => let val _ = printLn ("\n\nChecking problem " ^ n ^ "...") val res = c () val _ = printLn (" Score: " ^ (Real.toString res) ^ ".\n") in (n, res) :: results end) [] checks (* Returns a score of zero for all problems. *) (* Useful when you need to abort but still provide a score. *) fun failAll (checks : checks list) = List.foldr (fn (cs,results) => case cs of Problem (n, c) => (n, 0) :: results | _ => results) [] checks structure RE = RegExpFn(structure P = AwkSyntax structure E = ThompsonEngine) fun matchesAwkRegex (r, s) = let val r = RE.find (RE.compileString r) in Option.isSome (StringCvt.scanString r s) end end