1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
|
(* Copyright (C) 2017 Ryan Kavanagh <rkavanagh@cs.cmu.edu> *)
(* 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
|