From 7b2b2db168cb6b9a027981800b45e3bbb7425e33 Mon Sep 17 00:00:00 2001 From: Adam Chlipala Date: Fri, 5 Aug 2016 17:12:23 -0400 Subject: Return to working version mode --- configure.ac | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/configure.ac b/configure.ac index 196d597c..a35f13b1 100644 --- a/configure.ac +++ b/configure.ac @@ -1,5 +1,5 @@ AC_INIT([urweb], [20160805]) -WORKING_VERSION=0 +WORKING_VERSION=1 AC_USE_SYSTEM_EXTENSIONS # automake 1.12 requires this, but automake 1.11 doesn't recognize it -- cgit v1.2.3 From 06cb0a88e1df0d4c84427786a9d04f7a75856854 Mon Sep 17 00:00:00 2001 From: Benjamin Barenblat Date: Fri, 5 Aug 2016 21:31:07 -0400 Subject: Export fewer symbols Closes https://github.com/urweb/urweb/issues/43. --- src/c/Makefile.am | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/src/c/Makefile.am b/src/c/Makefile.am index d117d018..f4d9bef8 100644 --- a/src/c/Makefile.am +++ b/src/c/Makefile.am @@ -8,9 +8,14 @@ liburweb_static_la_SOURCES = static.c AM_CPPFLAGS = -I$(srcdir)/../../include/urweb $(OPENSSL_INCLUDES) AM_CFLAGS = -Wimplicit -Wall -Werror -Wno-format-security -Wno-deprecated-declarations -U_FORTIFY_SOURCE $(PTHREAD_CFLAGS) -liburweb_la_LDFLAGS = $(AM_LDFLAGS) $(OPENSSL_LDFLAGS) +liburweb_la_LDFLAGS = $(AM_LDFLAGS) $(OPENSSL_LDFLAGS) \ + -export-symbols-regex '^(client_pruner|pthread_create_big|strcmp_nullsafe|uw_.*)' liburweb_la_LIBADD = $(PTHREAD_LIBS) -lm $(OPENSSL_LIBS) liburweb_http_la_LIBADD = liburweb.la +liburweb_http_la_LDFLAGS = -export-symbols-regex '^(main|uw_.*)' liburweb_cgi_la_LIBADD = liburweb.la +liburweb_cgi_la_LDFLAGS = -export-symbols-regex '^(main|uw_.*)' liburweb_fastcgi_la_LIBADD = liburweb.la +liburweb_fastcgi_la_LDFLAGS = -export-symbols-regex '^(main|uw_.*)' liburweb_static_la_LIBADD = liburweb.la +liburweb_static_la_LDFLAGS = -export-symbols-regex '^(main|uw_.*)' -- cgit v1.2.3 From 8bd305270a83f8794859108b3e8eb200971a7521 Mon Sep 17 00:00:00 2001 From: Adam Chlipala Date: Mon, 8 Aug 2016 13:38:01 -0400 Subject: Avoid repopulating dead nodes; otherwise, we might wind up killing a node multiple times, screwing up the free list of available closure IDs (with duplicates), so that the same ID gets allocated for multiple functions later --- lib/js/urweb.js | 2 ++ 1 file changed, 2 insertions(+) diff --git a/lib/js/urweb.js b/lib/js/urweb.js index 68e7979d..b871b8e5 100644 --- a/lib/js/urweb.js +++ b/lib/js/urweb.js @@ -731,6 +731,8 @@ function flattenLocal(s) { // Dynamic tree management function populate(node) { + if (node.dead) return; + var s = node.signal; var oldSources = node.sources; try { -- cgit v1.2.3 From 3bcc7ed859aecb758742b0cf23cae98214938012 Mon Sep 17 00:00:00 2001 From: Adam Chlipala Date: Mon, 8 Aug 2016 13:53:17 -0400 Subject: Remove a check that was actually always passing because it was on the wrong object; this helps explain why the last change was necessary --- lib/js/urweb.js | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/lib/js/urweb.js b/lib/js/urweb.js index b871b8e5..95fd6756 100644 --- a/lib/js/urweb.js +++ b/lib/js/urweb.js @@ -762,8 +762,7 @@ function sv(s, v) { s.data = v; for (var ls = s.dyns; ls; ls = ls.next) - if (!ls.dead) - populate(ls.data); + populate(ls.data); } } function sg(s) { -- cgit v1.2.3 From 0b38f30883241b7dc45f8e2b8ff1a9c52a8b1536 Mon Sep 17 00:00:00 2001 From: Adam Chlipala Date: Wed, 24 Aug 2016 12:14:14 -0400 Subject: Plug a reactive-DOM memory leak (thanks to Saulo Araujo for spotting the problem and proposing a slightly different fix) --- lib/js/urweb.js | 16 +++++++++++----- 1 file changed, 11 insertions(+), 5 deletions(-) diff --git a/lib/js/urweb.js b/lib/js/urweb.js index 95fd6756..222a8322 100644 --- a/lib/js/urweb.js +++ b/lib/js/urweb.js @@ -1100,12 +1100,18 @@ function active(s) { function input(x, s, recreate, type, name) { if (name) x.name = name; if (type) x.type = type; - x.dead = false; - x.signal = ss(s); - x.sources = null; - x.recreate = recreate(x); addNode(x); - populate(x); + + var sc = document.createElement("script"); + sc.dead = false; + sc.signal = ss(s); + sc.sources = null; + sc.recreate = recreate(x); + + if (x.parentNode) + x.parentNode.insertBefore(sc, x); + + populate(sc); return x; } -- cgit v1.2.3 From 4330e7007078d680c1a3092d77831ed0c0fcf9b7 Mon Sep 17 00:00:00 2001 From: Adam Chlipala Date: Fri, 26 Aug 2016 12:41:25 -0400 Subject: Manual: explain the cooperative multithreading model --- doc/manual.tex | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/doc/manual.tex b/doc/manual.tex index 76f69330..8c12102e 100644 --- a/doc/manual.tex +++ b/doc/manual.tex @@ -2095,6 +2095,12 @@ Configure the policy for meta names with the \texttt{allow} and \texttt{deny} \t Ur/Web supports running code on web browsers, via automatic compilation to JavaScript. +The concurrency model is \emph{cooperative multithreading}. Like with, say, POSIX threads, which uses the \emph{preemptive multithreading} model, there may be multiple threads of control active at a time. However, unlike with preemptive multithreading, the currently running thread gets to run interrupted until a well-defined \emph{context-switch} point. Specifically, four functions defined below are the context-switch points. They are $\mt{sleep}$, $\mt{rpc}$, $\mt{tryRpc}$, and $\mt{recv}$. (We explain their purposes as we come to them below.) Additional functions added via the foreign function interface might also have context-switching behavior. In any case, it is guaranteed that a running thread ``owns the processor'' until it calls a context-switching function, at which time we may switch to running a different thread instead. + +This concurrency paradigm has many nice properties. For instance, there is almost never any need for locking or other synchronization between threads. + +Readers used to the standard JavaScript model may recognize this style as the natural one that we obtain by imposing a thread-based perspective on top of the usual JavaScript callback-based API. Indeed, every context-switching Ur/Web function is implemented with an underlying JavaScript call that asks for some callback to be triggered when an event happens. + \subsubsection{The Basics} All of the functions in this subsection are client-side only. @@ -2245,7 +2251,7 @@ The $\mt{channel}$ and $\mt{send}$ operations may only be executed on the server Clients and channels live only as long as the web browser page views that they are associated with. When a user surfs away, his client and its channels will be garbage-collected, after that user is not heard from for the timeout period. Garbage collection deletes any database row that contains a client or channel directly. Any reference to one of these types inside an $\mt{option}$ is set to $\mt{None}$ instead. Both kinds of handling have the flavor of weak pointers, and that is a useful way to think about clients and channels in the database. -\emph{Note}: Currently, there are known concurrency issues with multi-threaded applications that employ message-passing on top of database engines that don't support true serializable transactions. Postgres 9.1 is the only supported engine that does this properly. +\emph{Note}: Currently, there are known concurrency issues with multi-threaded applications that employ message-passing on top of database engines that don't support true serializable transactions. Postgres (versions 9.1 and up) is the only supported engine that does this properly. \section{Ur/Web Syntax Extensions} -- cgit v1.2.3 From f0c60c01bb078da639efd3aaf33bc2f0ff996169 Mon Sep 17 00:00:00 2001 From: Adam Chlipala Date: Fri, 26 Aug 2016 19:26:49 -0400 Subject: Import Json module from old Meta library (thanks to Edward Z. Yang for code contributions) --- lib/ur/json.ur | 345 +++++++++++++++++++++++++++++++++++++++++++++++++++++ lib/ur/json.urs | 31 +++++ tests/jsonTest.ur | 6 + tests/jsonTest.urp | 7 ++ 4 files changed, 389 insertions(+) create mode 100644 lib/ur/json.ur create mode 100644 lib/ur/json.urs create mode 100644 tests/jsonTest.ur create mode 100644 tests/jsonTest.urp diff --git a/lib/ur/json.ur b/lib/ur/json.ur new file mode 100644 index 00000000..bd74f299 --- /dev/null +++ b/lib/ur/json.ur @@ -0,0 +1,345 @@ +con json a = {ToJson : a -> string, + FromJson : string -> a * string} + +fun mkJson [a] (x : {ToJson : a -> string, + FromJson : string -> a * string}) = x + +fun skipSpaces s = + let + val len = String.length s + + fun skip i = + if i >= len then + "" + else + let + val ch = String.sub s i + in + if Char.isSpace ch then + skip (i+1) + else + String.substring s {Start = i, Len = len-i} + end + in + skip 0 + end + +fun toJson [a] (j : json a) : a -> string = j.ToJson +fun fromJson' [a] (j : json a) : string -> a * string = j.FromJson + +fun fromJson [a] (j : json a) (s : string) : a = + let + val (v, s') = j.FromJson (skipSpaces s) + in + if String.all Char.isSpace s' then + v + else + error Extra content at end of JSON record: {[s']} + end + +fun escape s = + let + fun esc s = + case s of + "" => "\"" + | _ => + let + val ch = String.sub s 0 + in + (if ch = #"\"" || ch = #"\\" then + "\\" ^ String.str ch + else + String.str ch) ^ esc (String.suffix s 1) + end + in + "\"" ^ esc s + end + +fun unescape s = + let + val len = String.length s + + fun findEnd i = + if i >= len then + error JSON unescape: string ends before quote: {[s]} + else + let + val ch = String.sub s i + in + case ch of + #"\"" => i + | #"\\" => + if i+1 >= len then + error JSON unescape: Bad escape sequence: {[s]} + else + findEnd (i+2) + | _ => findEnd (i+1) + end + + val last = findEnd 1 + + fun unesc i = + if i >= last then + "" + else + let + val ch = String.sub s i + in + case ch of + #"\\" => + if i+1 >= len then + error JSON unescape: Bad escape sequence: {[s]} + else + String.str (String.sub s (i+1)) ^ unesc (i+2) + | _ => String.str ch ^ unesc (i+1) + end + in + if len = 0 || String.sub s 0 <> #"\"" then + error JSON unescape: String doesn't start with double quote: {[s]} + else + (unesc 1, String.substring s {Start = last+1, Len = len-last-1}) + end + +val json_string = {ToJson = escape, + FromJson = unescape} + +fun numIn [a] (_ : read a) s : a * string = + let + val len = String.length s + + fun findEnd i = + if i >= len then + i + else + let + val ch = String.sub s i + in + if Char.isDigit ch || ch = #"-" || ch = #"." || ch = #"E" || ch = #"e" then + findEnd (i+1) + else + i + end + + val last = findEnd 0 + in + (readError (String.substring s {Start = 0, Len = last}), String.substring s {Start = last, Len = len-last}) + end + +fun json_num [a] (_ : show a) (_ : read a) : json a = {ToJson = show, + FromJson = numIn} + +val json_int = json_num +val json_float = json_num + +val json_bool = {ToJson = fn b => if b then "true" else "false", + FromJson = fn s => if String.isPrefix {Full = s, Prefix = "true"} then + (True, String.substring s {Start = 4, Len = String.length s - 4}) + else if String.isPrefix {Full = s, Prefix = "false"} then + (False, String.substring s {Start = 5, Len = String.length s - 5}) + else + error JSON: bad boolean string: {[s]}} + +fun json_option [a] (j : json a) : json (option a) = + {ToJson = fn v => case v of + None => "null" + | Some v => j.ToJson v, + FromJson = fn s => if String.isPrefix {Full = s, Prefix = "null"} then + (None, String.substring s {Start = 4, Len = String.length s - 4}) + else + let + val (v, s') = j.FromJson s + in + (Some v, s') + end} + +fun json_list [a] (j : json a) : json (list a) = + let + fun toJ' (ls : list a) : string = + case ls of + [] => "" + | x :: ls => "," ^ toJson j x ^ toJ' ls + + fun toJ (x : list a) : string = + case x of + [] => "[]" + | x :: [] => "[" ^ toJson j x ^ "]" + | x :: ls => "[" ^ toJson j x ^ toJ' ls ^ "]" + + fun fromJ (s : string) : list a * string = + let + fun fromJ' (s : string) : list a * string = + if String.length s = 0 then + error JSON list doesn't end with ']' + else + let + val ch = String.sub s 0 + in + case ch of + #"]" => ([], String.substring s {Start = 1, Len = String.length s - 1}) + | _ => + let + val (x, s') = j.FromJson s + val s' = skipSpaces s' + val s' = if String.length s' = 0 then + error JSON list doesn't end with ']' + else if String.sub s' 0 = #"," then + skipSpaces (String.substring s' {Start = 1, Len = String.length s' - 1}) + else + s' + + val (ls, s'') = fromJ' s' + in + (x :: ls, s'') + end + end + in + if String.length s = 0 || String.sub s 0 <> #"[" then + error JSON list doesn't start with '[': {[s]} + else + fromJ' (skipSpaces (String.substring s {Start = 1, Len = String.length s - 1})) + end + in + {ToJson = toJ, + FromJson = fromJ} + end + +fun json_record [ts ::: {Type}] (fl : folder ts) (jss : $(map json ts)) (names : $(map (fn _ => string) ts)) : json $ts = + {ToJson = fn r => "{" ^ @foldR3 [json] [fn _ => string] [ident] [fn _ => string] + (fn [nm ::_] [t ::_] [r ::_] [[nm] ~ r] (j : json t) name v acc => + escape name ^ ":" ^ j.ToJson v ^ (case acc of + "" => "" + | acc => "," ^ acc)) + "" fl jss names r ^ "}", + FromJson = fn s => + let + fun fromJ s (r : $(map option ts)) : $(map option ts) * string = + if String.length s = 0 then + error JSON object doesn't end in brace + else if String.sub s 0 = #"}" then + (r, String.substring s {Start = 1, Len = String.length s - 1}) + else let + val (name, s') = unescape s + val s' = skipSpaces s' + val s' = if String.length s' = 0 || String.sub s' 0 <> #":" then + error No colon after JSON object field name + else + skipSpaces (String.substring s' {Start = 1, Len = String.length s' - 1}) + + val (r, s') = @foldR2 [json] [fn _ => string] [fn ts => $(map option ts) -> $(map option ts) * string] + (fn [nm ::_] [t ::_] [r ::_] [[nm] ~ r] (j : json t) name' acc r => + if name = name' then + let + val (v, s') = j.FromJson s' + in + (r -- nm ++ {nm = Some v}, s') + end + else + let + val (r', s') = acc (r -- nm) + in + (r' ++ {nm = r.nm}, s') + end) + (fn _ => error Unknown JSON object field name {[name]}) + fl jss names r + + val s' = skipSpaces s' + val s' = if String.length s' <> 0 && String.sub s' 0 = #"," then + skipSpaces (String.substring s' {Start = 1, Len = String.length s' - 1}) + else + s' + in + fromJ s' r + end + in + if String.length s = 0 || String.sub s 0 <> #"{" then + error JSON record doesn't begin with brace + else + let + val (r, s') = fromJ (skipSpaces (String.substring s {Start = 1, Len = String.length s - 1})) + (@map0 [option] (fn [t ::_] => None) fl) + in + (@map2 [option] [fn _ => string] [ident] (fn [t] (v : option t) name => + case v of + None => error Missing JSON object field {[name]} + | Some v => v) fl r names, s') + end + end} + +fun destrR [K] [f :: K -> Type] [fr :: K -> Type] [t ::: Type] + (f : p :: K -> f p -> fr p -> t) + [r ::: {K}] (fl : folder r) (v : variant (map f r)) (r : $(map fr r)) : t = + match v + (@Top.mp [fr] [fn p => f p -> t] + (fn [p] (m : fr p) (v : f p) => f [p] v m) + fl r) + +fun json_variant [ts ::: {Type}] (fl : folder ts) (jss : $(map json ts)) (names : $(map (fn _ => string) ts)) : json (variant ts) = + {ToJson = fn r => let val jnames = @map2 [json] [fn _ => string] [fn x => json x * string] + (fn [t] (j : json t) (name : string) => (j, name)) fl jss names + in @destrR [ident] [fn x => json x * string] + (fn [p ::_] (v : p) (j : json p, name : string) => + "{" ^ escape name ^ ":" ^ j.ToJson v ^ "}") fl r jnames + end, + FromJson = fn s => + if String.length s = 0 || String.sub s 0 <> #"{" then + error JSON variant doesn't begin with brace + else + let + val (name, s') = unescape (skipSpaces (String.suffix s 1)) + val s' = skipSpaces s' + val s' = if String.length s' = 0 || String.sub s' 0 <> #":" then + error No colon after JSON object field name + else + skipSpaces (String.substring s' {Start = 1, Len = String.length s' - 1}) + + val (r, s') = (@foldR2 [json] [fn _ => string] + [fn ts => ts' :: {Type} -> [ts ~ ts'] => variant (ts ++ ts') * string] + (fn [nm ::_] [t ::_] [rest ::_] [[nm] ~ rest] (j : json t) name' + (acc : ts' :: {Type} -> [rest ~ ts'] => variant (rest ++ ts') * string) [fwd ::_] [[nm = t] ++ rest ~ fwd] => + if name = name' + then + let val (v, s') = j.FromJson s' + in (make [nm] v, s') + end + else acc [fwd ++ [nm = t]] + ) + (fn [fwd ::_] [[] ~ fwd] => error Unknown JSON object variant name {[name]}) + fl jss names) [[]] ! + + val s' = skipSpaces s' + val s' = if String.length s' <> 0 && String.sub s' 0 = #"," then + skipSpaces (String.substring s' {Start = 1, Len = String.length s' - 1}) + else + s' + in + if String.length s' = 0 then + error JSON object doesn't end in brace + else if String.sub s' 0 = #"}" then + (r, String.substring s' {Start = 1, Len = String.length s' - 1}) + else error Junk after JSON value in object + end + } + +val json_unit : json unit = json_record {} {} + +functor Recursive (M : sig + con t :: Type -> Type + val json_t : a ::: Type -> json a -> json (t a) + end) = struct + open M + + datatype r = Rec of t r + + fun rTo (Rec x) = (json_t {ToJson = rTo, + FromJson = fn _ => error Tried to FromJson in ToJson!}).ToJson x + + fun rFrom s = + let + val (x, s') = (json_t {ToJson = fn _ => error Tried to ToJson in FromJson!, + FromJson = rFrom}).FromJson s + in + (Rec x, s') + end + + val json_r = {ToJson = rTo, FromJson = rFrom} +end diff --git a/lib/ur/json.urs b/lib/ur/json.urs new file mode 100644 index 00000000..b4bd6350 --- /dev/null +++ b/lib/ur/json.urs @@ -0,0 +1,31 @@ +(** The JSON text-based serialization format *) + +class json + +val toJson : a ::: Type -> json a -> a -> string +val fromJson : a ::: Type -> json a -> string -> a +val fromJson' : a ::: Type -> json a -> string -> a * string + +val mkJson : a ::: Type -> {ToJson : a -> string, + FromJson : string -> a * string} -> json a + +val json_string : json string +val json_int : json int +val json_float : json float +val json_bool : json bool +val json_option : a ::: Type -> json a -> json (option a) +val json_list : a ::: Type -> json a -> json (list a) + +val json_record : ts ::: {Type} -> folder ts -> $(map json ts) -> $(map (fn _ => string) ts) -> json $ts +val json_variant : ts ::: {Type} -> folder ts -> $(map json ts) -> $(map (fn _ => string) ts) -> json (variant ts) + +val json_unit : json unit + +functor Recursive (M : sig + con t :: Type -> Type + val json_t : a ::: Type -> json a -> json (t a) + end) : sig + datatype r = Rec of M.t r + + val json_r : json r +end diff --git a/tests/jsonTest.ur b/tests/jsonTest.ur new file mode 100644 index 00000000..97898de8 --- /dev/null +++ b/tests/jsonTest.ur @@ -0,0 +1,6 @@ +open Json + +fun main () : transaction page = return + {[fromJson "[1, 2, 3]" : list int]}
+ {[toJson ("hi" :: "bye\"" :: "hehe" :: [])]} +
diff --git a/tests/jsonTest.urp b/tests/jsonTest.urp new file mode 100644 index 00000000..0b606fa3 --- /dev/null +++ b/tests/jsonTest.urp @@ -0,0 +1,7 @@ +rewrite all JsonTest/* + +$/char +$/string +$/list +$/json +jsonTest -- cgit v1.2.3 From de0a0f3c917f66646d1982281fae750c49f261d6 Mon Sep 17 00:00:00 2001 From: Adam Chlipala Date: Sat, 27 Aug 2016 10:51:38 -0400 Subject: Json: parse records with extra fields --- lib/ur/json.ur | 44 +++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 43 insertions(+), 1 deletion(-) diff --git a/lib/ur/json.ur b/lib/ur/json.ur index bd74f299..9288a6dd 100644 --- a/lib/ur/json.ur +++ b/lib/ur/json.ur @@ -203,6 +203,48 @@ fun json_list [a] (j : json a) : json (list a) = FromJson = fromJ} end +fun skipOne s = + let + fun skipOne s dquote squote brace bracket = + if String.length s = 0 then + s + else + let + val ch = String.sub s 0 + val rest = String.suffix s 1 + in + case ch of + #"\"" => skipOne rest (not dquote) squote brace bracket + | #"'" => skipOne rest dquote (not squote) brace bracket + | #"\\" => if String.length s >= 2 then + skipOne (String.suffix s 2) dquote squote brace bracket + else + "" + | #"{" => skipOne rest dquote squote (brace + 1) bracket + | #"}" => if brace = 0 then + s + else + skipOne rest dquote squote (brace - 1) bracket + + | #"[" => skipOne rest dquote squote brace (bracket + 1) + | #"]" => + if bracket = 0 then + s + else + skipOne rest dquote squote brace (bracket - 1) + + | #"," => + if not dquote && not squote && brace = 0 && bracket = 0 then + s + else + skipOne rest dquote squote brace bracket + + | _ => skipOne rest dquote squote brace bracket + end + in + skipOne s False False 0 0 + end + fun json_record [ts ::: {Type}] (fl : folder ts) (jss : $(map json ts)) (names : $(map (fn _ => string) ts)) : json $ts = {ToJson = fn r => "{" ^ @foldR3 [json] [fn _ => string] [ident] [fn _ => string] (fn [nm ::_] [t ::_] [r ::_] [[nm] ~ r] (j : json t) name v acc => @@ -239,7 +281,7 @@ fun json_record [ts ::: {Type}] (fl : folder ts) (jss : $(map json ts)) (names : in (r' ++ {nm = r.nm}, s') end) - (fn _ => error Unknown JSON object field name {[name]}) + (fn r => (r, skipOne s')) fl jss names r val s' = skipSpaces s' -- cgit v1.2.3 From 4daa34c90be6aebb62abaec75006e19f6666b5c6 Mon Sep 17 00:00:00 2001 From: Adam Chlipala Date: Thu, 1 Sep 2016 11:13:39 -0400 Subject: Emacs mode: switch font-lock-cvariable-face to inherit from font-lock-type-face, to avoid hardcoding of colors --- src/elisp/urweb-mode.el | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/elisp/urweb-mode.el b/src/elisp/urweb-mode.el index d1eec2a1..69b0e23c 100644 --- a/src/elisp/urweb-mode.el +++ b/src/elisp/urweb-mode.el @@ -281,7 +281,7 @@ See doc for the variable `urweb-mode-info'." "Face name to use for SQL keywords.") (defface font-lock-cvariable-face - '((t (:foreground "dark blue"))) + '((t (:inherit font-lock-type-face))) "Font Lock mode face used to highlight capitalized identifiers." :group 'font-lock-highlighting-faces) (defvar font-lock-cvariable-face 'font-lock-cvariable-face -- cgit v1.2.3 From c49ad46cc190e63f2395fcef03deff4386845877 Mon Sep 17 00:00:00 2001 From: Adam Chlipala Date: Thu, 1 Sep 2016 11:16:41 -0400 Subject: Remove 'Value' attributes for AJAX-y UI widgets, because they should use 'Source' instead --- lib/ur/basis.urs | 20 ++++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/lib/ur/basis.urs b/lib/ur/basis.urs index 1163daed..8b0d4faa 100644 --- a/lib/ur/basis.urs +++ b/lib/ur/basis.urs @@ -1075,23 +1075,23 @@ val curl : ctext val ctel : ctext val ccolor : ctext -val cnumber : cformTag ([Source = source float, Value = float, Min = float, Max = float, Step = float, Size = int, Onchange = transaction unit] ++ boxAttrs ++ inputAttrs) [] -val crange : cformTag ([Source = source float, Value = float, Min = float, Max = float, Size = int, Onchange = transaction unit] ++ boxAttrs ++ inputAttrs) [] -val cdate : cformTag ([Source = source string, Value = string, Min = string, Max = string, Size = int, Onchange = transaction unit] ++ boxAttrs ++ inputAttrs) [] -val cdatetime : cformTag ([Source = source string, Value = string, Min = string, Max = string, Size = int, Onchange = transaction unit] ++ boxAttrs ++ inputAttrs) [] -val cdatetime_local : cformTag ([Source = source string, Value = string, Min = string, Max = string, Size = int, Onchange = transaction unit] ++ boxAttrs ++ inputAttrs) [] -val cmonth : cformTag ([Source = source string, Value = string, Min = string, Max = string, Size = int, Onchange = transaction unit] ++ boxAttrs ++ inputAttrs) [] -val cweek : cformTag ([Source = source string, Value = string, Min = string, Max = string, Size = int, Onchange = transaction unit] ++ boxAttrs ++ inputAttrs) [] -val ctime : cformTag ([Source = source string, Value = string, Min = string, Max = string, Size = int, Onchange = transaction unit] ++ boxAttrs ++ inputAttrs) [] +val cnumber : cformTag ([Source = source float, Min = float, Max = float, Step = float, Size = int, Onchange = transaction unit] ++ boxAttrs ++ inputAttrs) [] +val crange : cformTag ([Source = source float, Min = float, Max = float, Size = int, Onchange = transaction unit] ++ boxAttrs ++ inputAttrs) [] +val cdate : cformTag ([Source = source string, Min = string, Max = string, Size = int, Onchange = transaction unit] ++ boxAttrs ++ inputAttrs) [] +val cdatetime : cformTag ([Source = source string, Min = string, Max = string, Size = int, Onchange = transaction unit] ++ boxAttrs ++ inputAttrs) [] +val cdatetime_local : cformTag ([Source = source string, Min = string, Max = string, Size = int, Onchange = transaction unit] ++ boxAttrs ++ inputAttrs) [] +val cmonth : cformTag ([Source = source string, Min = string, Max = string, Size = int, Onchange = transaction unit] ++ boxAttrs ++ inputAttrs) [] +val cweek : cformTag ([Source = source string, Min = string, Max = string, Size = int, Onchange = transaction unit] ++ boxAttrs ++ inputAttrs) [] +val ctime : cformTag ([Source = source string, Min = string, Max = string, Size = int, Onchange = transaction unit] ++ boxAttrs ++ inputAttrs) [] val button : cformTag ([Value = string] ++ boxAttrs) [] -val ccheckbox : cformTag ([Value = bool, Size = int, Source = source bool, Onchange = transaction unit] ++ boxAttrs ++ inputAttrs) [] +val ccheckbox : cformTag ([Size = int, Source = source bool, Onchange = transaction unit] ++ boxAttrs ++ inputAttrs) [] val cselect : cformTag ([Source = source string, Onchange = transaction unit] ++ boxAttrs) [Cselect] val coption : unit -> tag [Value = string, Selected = bool] [Cselect, Body] [] [] [] -val ctextarea : cformTag ([Value = string, Rows = int, Cols = int, Source = source string, Onchange = transaction unit, +val ctextarea : cformTag ([Rows = int, Cols = int, Source = source string, Onchange = transaction unit, Ontext = transaction unit] ++ boxAttrs ++ inputAttrs) [] (*** Tables *) -- cgit v1.2.3 From 51ac19f565fa935eec6d9dd1f7119c6227383a3c Mon Sep 17 00:00:00 2001 From: Jacob Mitchell Date: Wed, 7 Sep 2016 13:19:13 -0700 Subject: HTTP Last-Modified: latest modtime of source files See issue #38. --- src/cjr_print.sml | 35 +++++++++++++++++------------------ src/compiler.sml | 8 ++++---- src/demo.sml | 4 ++-- src/elab_err.sml | 2 +- src/fileio.sig | 9 +++++++++ src/fileio.sml | 34 ++++++++++++++++++++++++++++++++++ src/globals.sig | 7 +++++++ src/globals.sml | 7 +++++++ src/jscomp.sml | 2 +- src/main.mlton.sml | 5 +++-- src/settings.sig | 1 + src/settings.sml | 9 +++++---- src/tutorial.sml | 4 ++-- 13 files changed, 93 insertions(+), 34 deletions(-) create mode 100644 src/fileio.sig create mode 100644 src/fileio.sml create mode 100644 src/globals.sig create mode 100644 src/globals.sml diff --git a/src/cjr_print.sml b/src/cjr_print.sml index b2c85a54..2e969c46 100644 --- a/src/cjr_print.sml +++ b/src/cjr_print.sml @@ -3306,8 +3306,7 @@ fun p_file env (ds, ps) = val onError = ListUtil.search (fn (DOnError n, _) => SOME n | _ => NONE) ds - val now = Time.now () - val nowD = Date.fromTimeUniv now + val lastMod = Date.fromTimeUniv (FileIO.mostRecentModTime ()) val rfcFmt = "%a, %d %b %Y %H:%M:%S GMT" fun hexifyByte (b : Word8.word) : string = @@ -3496,26 +3495,26 @@ fun p_file env (ds, ps) = string "static void uw_handle(uw_context ctx, char *request) {", newline, + string "uw_Basis_string ims = uw_Basis_requestHeader(ctx, \"If-modified-since\");", + newline, + string ("if (ims && !strcmp(ims, \"" ^ Date.fmt rfcFmt lastMod ^ "\")) {"), + newline, + box [string "uw_clear_headers(ctx);", + newline, + string "uw_write_header(ctx, uw_supports_direct_status ? \"HTTP/1.1 304 Not Modified\\r\\n\" : \"Status: 304 Not Modified\\r\\n\");", + newline, + string "return;", + newline], + string "}", + newline, + newline, string "if (!strcmp(request, \"", string app_js, string "\")) {", newline, - box [string "uw_Basis_string ims = uw_Basis_requestHeader(ctx, \"If-modified-since\");", - newline, - string ("if (ims && !strcmp(ims, \"" ^ Date.fmt rfcFmt nowD ^ "\")) {"), - newline, - box [string "uw_clear_headers(ctx);", - newline, - string "uw_write_header(ctx, uw_supports_direct_status ? \"HTTP/1.1 304 Not Modified\\r\\n\" : \"Status: 304 Not Modified\\r\\n\");", - newline, - string "return;", - newline], - string "}", - newline, - newline, - string "uw_write_header(ctx, \"Content-Type: text/javascript\\r\\n\");", + box [string "uw_write_header(ctx, \"Content-Type: text/javascript\\r\\n\");", newline, - string ("uw_write_header(ctx, \"Last-Modified: " ^ Date.fmt rfcFmt nowD ^ "\\r\\n\");"), + string ("uw_write_header(ctx, \"Last-Modified: " ^ Date.fmt rfcFmt lastMod ^ "\\r\\n\");"), newline, string ("uw_write_header(ctx, \"Cache-Control: max-age=31536000, public\\r\\n\");"), newline, @@ -3538,7 +3537,7 @@ fun p_file env (ds, ps) = string (String.toCString ct), string "\\r\\n\");", newline]), - string ("uw_write_header(ctx, \"Last-Modified: " ^ Date.fmt rfcFmt (Date.fromTimeUniv (#LastModified r)) ^ "\\r\\n\");"), + string ("uw_write_header(ctx, \"Last-Modified: " ^ Date.fmt rfcFmt lastMod ^ "\\r\\n\");"), newline, string ("uw_write_header(ctx, \"Content-Length: " ^ Int.toString (Word8Vector.length (#Bytes r)) ^ "\\r\\n\");"), newline, diff --git a/src/compiler.sml b/src/compiler.sml index dccda06d..87d175ed 100644 --- a/src/compiler.sml +++ b/src/compiler.sml @@ -212,7 +212,7 @@ val parseUrs = val fname = OS.FileSys.tmpName () val outf = TextIO.openOut fname val () = TextIO.output (outf, "sig\n") - val inf = TextIO.openIn filename + val inf = FileIO.txtOpenIn filename fun loop () = case TextIO.inputLine inf of NONE => () @@ -225,7 +225,7 @@ val parseUrs = val () = (ErrorMsg.resetErrors (); ErrorMsg.resetPositioning filename; Lex.UserDeclarations.initialize ()) - val file = TextIO.openIn fname + val file = FileIO.txtOpenIn fname fun get _ = TextIO.input file fun parseerror (s, p1, p2) = ErrorMsg.errorAt' (p1, p2) s val lexer = LrParser.Stream.streamify (Lex.makeLexer get) @@ -251,7 +251,7 @@ val parseUr = { val () = (ErrorMsg.resetErrors (); ErrorMsg.resetPositioning filename; Lex.UserDeclarations.initialize ()) - val file = TextIO.openIn filename + val file = FileIO.txtOpenIn filename fun get _ = TextIO.input file fun parseerror (s, p1, p2) = ErrorMsg.errorAt' (p1, p2) s val lexer = LrParser.Stream.streamify (Lex.makeLexer get) @@ -478,7 +478,7 @@ fun parseUrp' accLibs fname = val thisPath = OS.Path.dir filename val dir = OS.Path.dir filename - fun opener () = TextIO.openIn (OS.Path.joinBaseExt {base = filename, ext = SOME "urp"}) + fun opener () = FileIO.txtOpenIn (OS.Path.joinBaseExt {base = filename, ext = SOME "urp"}) val inf = opener () diff --git a/src/demo.sml b/src/demo.sml index 0d9f0f4f..47d22395 100644 --- a/src/demo.sml +++ b/src/demo.sml @@ -33,7 +33,7 @@ fun make' {prefix, dirname, guided} = let val prose = OS.Path.joinDirFile {dir = dirname, file = "prose"} - val inf = TextIO.openIn prose + val inf = FileIO.txtOpenIn prose val outDir = OS.Path.concat (dirname, "out") @@ -351,7 +351,7 @@ fun make' {prefix, dirname, guided} = SOME "urp" => doit (fn (src, html) => let - val inf = TextIO.openIn src + val inf = FileIO.txtOpenIn src val out = TextIO.openOut html fun loop () = diff --git a/src/elab_err.sml b/src/elab_err.sml index 33daa118..385caca3 100644 --- a/src/elab_err.sml +++ b/src/elab_err.sml @@ -275,7 +275,7 @@ fun p_decl env d = fun readFromFile () = let - val inf = TextIO.openIn fname + val inf = FileIO.txtOpenIn fname fun loop acc = case TextIO.inputLine inf of diff --git a/src/fileio.sig b/src/fileio.sig new file mode 100644 index 00000000..37b3b529 --- /dev/null +++ b/src/fileio.sig @@ -0,0 +1,9 @@ +signature FILE_IO = sig + + (* When was a source file last modified (excluding files produced after [getResetTime])? *) + val mostRecentModTime : unit -> Time.time + + val txtOpenIn : string -> TextIO.instream + val binOpenIn : string -> BinIO.instream + +end diff --git a/src/fileio.sml b/src/fileio.sml new file mode 100644 index 00000000..72e72f6d --- /dev/null +++ b/src/fileio.sml @@ -0,0 +1,34 @@ +structure FileIO :> FILE_IO = struct + +val mostRecentModTimeRef = ref (Time.zeroTime) + +fun checkFileModTime fname = + let val mtime = OS.FileSys.modTime fname in + if Time.compare (mtime, !mostRecentModTimeRef) = GREATER andalso + Time.compare (mtime, Globals.getResetTime ()) = LESS + then mostRecentModTimeRef := mtime + else () + end + +fun mostRecentModTime () = + if Time.compare (!mostRecentModTimeRef, Time.zeroTime) = EQUAL + then Globals.getResetTime () + else !mostRecentModTimeRef + +fun txtOpenIn fname = + let + val inf = TextIO.openIn fname + val () = checkFileModTime fname + in + inf + end + +fun binOpenIn fname = + let + val inf = BinIO.openIn fname + val () = checkFileModTime fname + in + inf + end + +end diff --git a/src/globals.sig b/src/globals.sig new file mode 100644 index 00000000..0cff65b5 --- /dev/null +++ b/src/globals.sig @@ -0,0 +1,7 @@ +signature GLOBALS = sig + + (* When was the Ur/Web compiler started or reset? *) + val setResetTime : unit -> unit + val getResetTime : unit -> Time.time + +end diff --git a/src/globals.sml b/src/globals.sml new file mode 100644 index 00000000..fafc0438 --- /dev/null +++ b/src/globals.sml @@ -0,0 +1,7 @@ +structure Globals :> GLOBALS = struct + +val resetTime = ref (Time.zeroTime) +fun setResetTime () = resetTime := Time.now () +fun getResetTime () = !resetTime + +end diff --git a/src/jscomp.sml b/src/jscomp.sml index 65a0fa3a..dedcb554 100644 --- a/src/jscomp.sml +++ b/src/jscomp.sml @@ -1334,7 +1334,7 @@ fun process (file : file) = maxName = U.File.maxName file + 1} (#1 file) - val inf = TextIO.openIn (OS.Path.joinDirFile {dir = Settings.libJs (), file = "urweb.js"}) + val inf = FileIO.txtOpenIn (OS.Path.joinDirFile {dir = Settings.libJs (), file = "urweb.js"}) fun lines acc = case TextIO.inputLine inf of NONE => String.concat (rev acc) diff --git a/src/main.mlton.sml b/src/main.mlton.sml index 6d368106..fb1a1723 100644 --- a/src/main.mlton.sml +++ b/src/main.mlton.sml @@ -254,7 +254,8 @@ fun send (sock, s) = send (sock, String.extract (s, n, NONE)) end -val () = case CommandLine.arguments () of +val () = (Globals.setResetTime (); + case CommandLine.arguments () of ["daemon", "start"] => (case Posix.Process.fork () of SOME _ => () @@ -376,4 +377,4 @@ val () = case CommandLine.arguments () of else (OS.FileSys.remove socket; raise OS.SysErr ("", NONE)) - end handle OS.SysErr _ => OS.Process.exit (oneRun args) + end handle OS.SysErr _ => OS.Process.exit (oneRun args)) diff --git a/src/settings.sig b/src/settings.sig index dd135bda..05ab5e23 100644 --- a/src/settings.sig +++ b/src/settings.sig @@ -302,4 +302,5 @@ signature SETTINGS = sig val addJsFile : string (* filename *) -> unit val listJsFiles : unit -> {Filename : string, Content : string} list + end diff --git a/src/settings.sml b/src/settings.sml index b72789df..f4d399dc 100644 --- a/src/settings.sml +++ b/src/settings.sml @@ -844,7 +844,7 @@ fun noMime () = fun readMimeTypes () = let - val inf = TextIO.openIn "/etc/mime.types" + val inf = FileIO.txtOpenIn "/etc/mime.types" fun loop m = case TextIO.inputLine inf of @@ -914,7 +914,7 @@ fun addFile {Uri, LoadFromFilename} = ErrorMsg.error ("Two different files requested for URI " ^ Uri ^ " ( " ^ path' ^ " vs. " ^ path ^ ")") | NONE => let - val inf = BinIO.openIn path + val inf = FileIO.binOpenIn path in files := SM.insert (!files, Uri, @@ -937,7 +937,7 @@ val jsFiles = ref (SM.empty : {Filename : string, Content : string} SM.map) fun addJsFile LoadFromFilename = let val path = OS.Path.concat (!filePath, LoadFromFilename) - val inf = TextIO.openIn path + val inf = FileIO.txtOpenIn path in jsFiles := SM.insert (!jsFiles, path, @@ -952,7 +952,8 @@ fun addJsFile LoadFromFilename = fun listJsFiles () = SM.listItems (!jsFiles) fun reset () = - (urlPrefixFull := "/"; + (Globals.setResetTime (); + urlPrefixFull := "/"; urlPrefix := "/"; urlPrePrefix := ""; timeout := 0; diff --git a/src/tutorial.sml b/src/tutorial.sml index dd2d3f7d..0c2f908f 100644 --- a/src/tutorial.sml +++ b/src/tutorial.sml @@ -38,7 +38,7 @@ fun readAll inf = before TextIO.closeIn inf end -val readAllFile = readAll o TextIO.openIn +val readAllFile = readAll o FileIO.txtOpenIn fun fixupFile (fname, title) = let @@ -154,7 +154,7 @@ fun fixupFile (fname, title) = fun doUr fname = let - val inf = TextIO.openIn fname + val inf = FileIO.txtOpenIn fname val title = case TextIO.inputLine inf of NONE => raise Fail ("No title comment at start of " ^ fname) -- cgit v1.2.3 From 40f5ea3c4242f8f92d511596778f980671972d7f Mon Sep 17 00:00:00 2001 From: Jacob Mitchell Date: Thu, 8 Sep 2016 11:09:45 -0700 Subject: Use hash instead of timestamp in app.js filename Makes Ur/Web project builds reproducible without sacrificing HTTP caching. Uses a public domain SHA1 implementation by tom7. See #38. --- src/cjr_print.sml | 28 +++--- src/sha1.sig | 31 +++++++ src/sha1.sml | 264 ++++++++++++++++++++++++++++++++++++++++++++++++++++++ src/sources | 9 ++ 4 files changed, 320 insertions(+), 12 deletions(-) create mode 100644 src/sha1.sig create mode 100644 src/sha1.sml diff --git a/src/cjr_print.sml b/src/cjr_print.sml index 2e969c46..688b3e4d 100644 --- a/src/cjr_print.sml +++ b/src/cjr_print.sml @@ -55,6 +55,8 @@ structure CM = BinaryMapFn(struct val debug = ref false +val app_js = ref "" + val dummyTyp = (TDatatype (Enum, 0, ref []), ErrorMsg.dummySpan) val ident = String.translate (fn #"'" => "PRIME" @@ -2509,9 +2511,15 @@ fun p_decl env (dAll as (d, loc) : decl) = | DDatabase _ => box [] | DPreparedStatements _ => box [] - | DJavaScript s => box [string "static char jslib[] = \"", - string (Prim.toCString s), - string "\";"] + | DJavaScript s => + let + val () = app_js := OS.Path.joinDirFile {dir = Settings.getUrlPrefix (), + file = "app." ^ SHA1.bintohex (SHA1.hash s) ^ ".js"} + in + box [string "static char jslib[] = \"", + string (Prim.toCString s), + string "\";"] + end | DCookie s => box [string "/*", space, string "cookie", @@ -2948,15 +2956,11 @@ fun p_file env (ds, ps) = newline] end - val timestamp = LargeInt.toString (Time.toMilliseconds (Time.now ())) - val app_js = OS.Path.joinDirFile {dir = Settings.getUrlPrefix (), - file = "app." ^ timestamp ^ ".js"} - - val allScripts = + fun allScripts () = foldl (fn (x, scripts) => scripts ^ "\\n") - "" (Settings.getScripts () @ [app_js]) + "" (Settings.getScripts () @ [!app_js]) fun p_page (ek, s, n, ts, ran, side, dbmode, tellSig) = let @@ -3098,7 +3102,7 @@ fun p_file env (ds, ps) = val scripts = case side of ServerOnly => "" - | _ => allScripts + | _ => allScripts () in string scripts end, @@ -3509,7 +3513,7 @@ fun p_file env (ds, ps) = newline, newline, string "if (!strcmp(request, \"", - string app_js, + string (!app_js), string "\")) {", newline, box [string "uw_write_header(ctx, \"Content-Type: text/javascript\\r\\n\");", @@ -3633,7 +3637,7 @@ fun p_file env (ds, ps) = newline, if !hasJs then box [string "uw_set_script_header(ctx, \"", - string allScripts, + string (allScripts ()), string "\");", newline] else diff --git a/src/sha1.sig b/src/sha1.sig new file mode 100644 index 00000000..7fda97f5 --- /dev/null +++ b/src/sha1.sig @@ -0,0 +1,31 @@ + +(* Implementation the SHA-1 hash function. + Written by Tom 7 in 2004; code in the public domain. *) + +signature SHA1 = +sig + + (* Perform the SHA-1 hash function on a message. + Returns the 160 bit (20 byte) hash. + + recall that string = CharVector.vector. + The input string may contain non-ascii data; + the output certainly will. *) + + val hash : string -> string + + (* pass in a stream as stateful function that returns + SOME s for some non-empty prefix of the remainder of + the stream, or NONE when the stream has ended. *) + val hash_stream : (unit -> string option) -> string + + (* XXX move to hashutil *) + (* convert a binary string to one built of hex digits *) + val bintohex : string -> string + + (* Parse a hexadecimal SHA-1 string. Uppercase and lowercase + are permitted. If the string is not the right length or + contains invalid characters, returns NONE. *) + val parse_hex : string -> string option + +end diff --git a/src/sha1.sml b/src/sha1.sml new file mode 100644 index 00000000..d962c4e0 --- /dev/null +++ b/src/sha1.sml @@ -0,0 +1,264 @@ + +(* RFC-3174 (SHA-1) hashing function. + By Tom 7, 2004: Code placed in the public domain. +*) + +structure SHA1 :> SHA1 = +struct + exception Unimplemented + + val xorb = Word32.xorb + val andb = Word32.andb + val orb = Word32.orb + val << = Word32.<< + val >> = Word32.>> + val notb = Word32.notb + val ++ = Word32.+ + + type w32 = word + infix xorb andb orb << >> ++ + + (* workaround for andb bug in MLton 20010706 *) + fun mkbyte w = Word32.mod (w, 0w256) + + fun ROL(X, N : Word.word) = (X << N) orb (X >> (0w32-N)) + + fun wc hi lo = (hi << 0w16) orb lo + + fun w2b w = map chr + [Word32.toInt (mkbyte (w >> 0w24)), + Word32.toInt (mkbyte (w >> 0w16)), + Word32.toInt (mkbyte (w >> 0w8)), + Word32.toInt (mkbyte w)] + + (* the length (arg in bytes, output in bits) + as a 64-bit quantity, big-endian *) + fun lenbits l = + implode (List.tabulate (4, fn _ => chr 0)) ^ + implode (w2b (Word32.fromInt (l * 8))) + + + (* executes f for each index lo..hi-1 inclusive *) + fun for lo hi f = + if lo >= hi then () + else (ignore (f lo); for (lo + 1) hi f) + + fun ford lo hi b f = + if lo >= hi then b + else + let + val b = f (lo, b) + in + (ford (lo + 1) hi b f) + end + + fun doblock (aa, bb, cc, dd, ee) msg = + let + val K0 = wc 0wx5A82 0wx7999 + val K1 = wc 0wx6ED9 0wxEBA1 + val K2 = wc 0wx8F1B 0wxBCDC + val K3 = wc 0wxCA62 0wxC1D6 + + fun mb n = Word32.fromInt (ord (CharVector.sub(msg, n))) + + val W = Array.array(80, 0w0) + fun Ws x = Array.sub(W, x) + + val _ = + for 0 16 + (fn t => + let in + Array.update(W, t, + (mb (t * 4 ) << 0w24) orb + (mb (t * 4 + 1) << 0w16) orb + (mb (t * 4 + 2) << 0w8) orb + (mb (t * 4 + 3))) + end) + + val _ = + for 16 80 + (fn t => + let + val n = + Ws (t-3) xorb + Ws (t-8) xorb + Ws (t-14) xorb + Ws (t-16) + val zz = ROL(n, 0w1) + in + Array.update(W, t, zz) + end) + + + val (A, B, C, D, E) = (aa, bb, cc, dd, ee) + + + fun round lo hi f k ctxt = + ford lo hi ctxt + (fn (t, ctxt as (A, B, C, D, E)) => + let + val temp = ROL(A, 0w5) ++ (f ctxt) ++ E ++ Ws t ++ k + val E = D; + val D = C; + val C = ROL(B, 0w30) + val B = A + val A = temp + in + (A, B, C, D, E) + end) + + val (A, B, C, D, E) = + round 0 20 (fn (A, B, C, D, E) => + ((B andb C) orb ((notb B) andb D))) + K0 (A, B, C, D, E) + + val (A, B, C, D, E) = + round 20 40 (fn (A, B, C, D, E) => + (B xorb C xorb D)) + K1 (A, B, C, D, E) + + val (A, B, C, D, E) = + round 40 60 (fn (A, B, C, D, E) => + ((B andb C) orb (B andb D) orb (C andb D))) + K2 (A, B, C, D, E) + + val (A, B, C, D, E) = + round 60 80 (fn (A, B, C, D, E) => + (B xorb C xorb D)) + K3 (A, B, C, D, E) + + in + (aa ++ A, bb ++ B, cc ++ C, dd ++ D, ee ++ E) + end + + datatype 'a stream = + Cons of ('a * (unit -> 'a stream)) + | Nil + + (* turn a stream of oddly chunked strings into + one with 512-bit blocks *) + fun chunk_512 s = + let + + (* the padding required to make a message of length l (bytes) + a proper SHA-1 input. Returns either one or two Cons cells. + tail is the end of the input (63 bytes or less) + l is the total length of the input, *including* the length of the + tail end *) + fun padding tail l = + let val v = l mod 64 in + if v < 56 then + let val p = 56 - v + val padding = implode (List.tabulate (p - 1, fn _ => chr 0)) + in Cons (tail ^ str (chr 0x80) ^ padding ^ lenbits l, + fn _ => Nil) + end + else if v < 64 then + let val p = 64 - v + val padding1 = implode (List.tabulate (p - 1, fn _ => chr 0)) + val padding2 = implode (List.tabulate (56, fn _ => chr 0)) + in Cons (tail ^ str (chr 0x80) ^ padding1, + fn _ => Cons (padding2 ^ lenbits l, fn _ => Nil)) + end + else raise Unimplemented (* Impossible? *) + end + + (* n is the bytes we've already output. + cur is a string (of 64 bytes or less) that will + be our next chunk. + rest,sofar is a string and index indicating the + next bit of data. *) + (* PERF Could be more efficient by using an + accumulating array instead of a string for cur *) + fun ch n cur sofar startat () = + (* if we already have 64 bytes, return it *) + if size cur = 64 + then + let in + Cons(cur, ch (n + 64) "" sofar startat) + end + else + (* do we have any in 'sofar'? *) + if startat < size sofar + then let + val get = Int.min(size sofar - startat, + 64 - size cur) + in + (* be eager, since we need to return something now *) + ch n (cur ^ String.substring(sofar, startat, get)) + sofar (startat + get) () + end + else + (* sofar has been exhausted, + so get some from input stream *) + (case s () of + (* eager, again *) + SOME ss => ch n cur ss 0 () + | NONE => + (* no more data. *) + padding cur (n + size cur)) + in + ch 0 "" "" 0 + end + + fun hash_stream orig_stream = + let + + val stream512 = chunk_512 orig_stream + + (* gets hash context, length of string so far (bytes), + and tail of stream *) + fun hash_rest stream ctxt = + (case stream() of + Cons (s, stream) => + let val ctxt = doblock ctxt s + in hash_rest stream ctxt + end + | Nil => ctxt) + + val init = + (wc 0wx6745 0wx2301, + wc 0wxefcd 0wxab89, + wc 0wx98ba 0wxdcfe, + wc 0wx1032 0wx5476, + wc 0wxc3d2 0wxe1f0) + + val (a, b, c, d, e) = hash_rest stream512 init + in + implode (w2b a @ w2b b @ w2b c @ w2b d @ w2b e) + end + + fun hash m = + hash_stream + (let val r = ref true + in (fn () => + if !r + then (r := false; SOME m) + else NONE) + end) + + val digits = "0123456789ABCDEF" + fun bintohex s = + String.translate (fn c => + implode [CharVector.sub (digits, ord c div 16), + CharVector.sub (digits, ord c mod 16)]) s + + (* ASCII trick: (ch | 4400) % 55 *) + fun hexvalue ch = + SysWord.toInt (SysWord.orb(SysWord.fromInt(ord ch), SysWord.fromInt 4400)) mod 55 + + fun parse_hex s = + if size s <> 40 + orelse not (CharVector.all (fn c => (ord c >= ord #"0" andalso + ord c <= ord #"9") orelse + (ord c >= ord #"a" andalso + ord c <= ord #"f") orelse + (ord c >= ord #"A" andalso + ord c <= ord #"F")) s) + then NONE + else SOME (CharVector.tabulate(20, + (fn i => + chr(hexvalue (String.sub(s, i * 2)) * 16 + + hexvalue (String.sub(s, i * 2 + 1)))))) + +end diff --git a/src/sources b/src/sources index 1a09e7e8..52b1bdd7 100644 --- a/src/sources +++ b/src/sources @@ -1,6 +1,9 @@ $(SRC)/config.sig config.sml +$(SRC)/globals.sig +$(SRC)/globals.sml + $(SRC)/search.sig $(SRC)/search.sml @@ -16,6 +19,9 @@ $(SRC)/errormsg.sml $(SRC)/print.sig $(SRC)/print.sml +$(SRC)/fileio.sig +$(SRC)/fileio.sml + $(SRC)/settings.sig $(SRC)/settings.sml @@ -227,6 +233,9 @@ $(SRC)/sigcheck.sml $(SRC)/mono_inline.sml +$(SRC)/sha1.sig +$(SRC)/sha1.sml + $(SRC)/cjr.sml $(SRC)/postgres.sig -- cgit v1.2.3 From 316befb960fea2009ed1b8bed26b4060f04673ba Mon Sep 17 00:00:00 2001 From: Marvin Sielenkemper Date: Fri, 16 Sep 2016 21:35:36 +0200 Subject: Add IPv6 support and a new '-A' option to specify an IPv6 adress to bind --- src/c/http.c | 32 ++++++++++++++++++++++---------- 1 file changed, 22 insertions(+), 10 deletions(-) diff --git a/src/c/http.c b/src/c/http.c index 9059746f..5752fc6f 100644 --- a/src/c/http.c +++ b/src/c/http.c @@ -325,8 +325,8 @@ static void sigint(int signum) { int main(int argc, char *argv[]) { // The skeleton for this function comes from Beej's sockets tutorial. int sockfd; // listen on sock_fd - struct sockaddr_in my_addr; - struct sockaddr_in their_addr; // connector's address information + struct sockaddr_in6 my_addr; + struct sockaddr_in6 their_addr; // connector's address information socklen_t sin_size; int yes = 1, uw_port = 8080, nthreads = 1, i, *names, opt; int recv_timeout_sec = 5; @@ -334,10 +334,9 @@ int main(int argc, char *argv[]) { signal(SIGINT, sigint); signal(SIGPIPE, SIG_IGN); - my_addr.sin_addr.s_addr = INADDR_ANY; // auto-fill with my IP - memset(my_addr.sin_zero, '\0', sizeof my_addr.sin_zero); + my_addr.sin6_addr = in6addr_any; // auto-fill with my IP - while ((opt = getopt(argc, argv, "hp:a:t:kqT:")) != -1) { + while ((opt = getopt(argc, argv, "hp:a:A:t:kqT:")) != -1) { switch (opt) { case '?': fprintf(stderr, "Unknown command-line option\n"); @@ -358,8 +357,21 @@ int main(int argc, char *argv[]) { break; case 'a': - if (!inet_pton(AF_INET, optarg, &my_addr.sin_addr)) { - fprintf(stderr, "Invalid IP address\n"); + { + char *buf = alloca(strlen(optarg) + 8); + strcpy(buf, "::FFFF:"); + strcpy(buf + 7, optarg); + if (!inet_pton(AF_INET6, buf, &my_addr.sin6_addr)) { + fprintf(stderr, "Invalid IPv4 address\n"); + help(argv[0]); + return 1; + } + } + break; + + case 'A': + if (!inet_pton(AF_INET6, optarg, &my_addr.sin6_addr)) { + fprintf(stderr, "Invalid IPv6 address\n"); help(argv[0]); return 1; } @@ -401,7 +413,7 @@ int main(int argc, char *argv[]) { names = calloc(nthreads, sizeof(int)); - sockfd = socket(PF_INET, SOCK_STREAM, 0); // do some error checking! + sockfd = socket(AF_INET6, SOCK_STREAM, 0); // do some error checking! if (sockfd < 0) { fprintf(stderr, "Listener socket creation failed\n"); @@ -413,8 +425,8 @@ int main(int argc, char *argv[]) { return 1; } - my_addr.sin_family = AF_INET; // host byte order - my_addr.sin_port = htons(uw_port); // short, network byte order + my_addr.sin6_family = AF_INET6; // host byte order + my_addr.sin6_port = htons(uw_port); // short, network byte order if (bind(sockfd, (struct sockaddr *)&my_addr, sizeof my_addr) < 0) { fprintf(stderr, "Listener socket bind failed\n"); -- cgit v1.2.3 From c70444b4dd41960554f5f283d22e911df6447737 Mon Sep 17 00:00:00 2001 From: Marvin Sielenkemper Date: Fri, 16 Sep 2016 22:19:07 +0200 Subject: Add tests for IPv6 --- Makefile.am | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/Makefile.am b/Makefile.am index 9ab31acd..8e945a47 100644 --- a/Makefile.am +++ b/Makefile.am @@ -121,4 +121,11 @@ test: (curl -s 'http://localhost:8080/Demo/Hello/main' | diff tests/hello.html -) || (kill `cat $(TESTPID)`; echo "Test 'Hello' failed"; /bin/false) (curl -s 'http://localhost:8080/Demo/Crud1/create?A=1&B=2&C=3&D=4' | diff tests/crud1.html -) || (kill `cat $(TESTPID)`; echo "Test 'Crud1' failed"; /bin/false) kill `cat $(TESTPID)` + rm -f $(TESTDB) + sqlite3 $(TESTDB) < demo/demo.sql + demo/demo.exe & echo $$! > $(TESTPID) + sleep 1 + (curl -s 'http://[::1]:8080/Demo/Hello/main' | diff tests/hello.html -) || (kill `cat $(TESTPID)`; echo "Test 'Hello' failed"; /bin/false) + (curl -s 'http://[::1]:8080/Demo/Crud1/create?A=1&B=2&C=3&D=4' | diff tests/crud1.html -) || (kill `cat $(TESTPID)`; echo "Test 'Crud1' failed"; /bin/false) + kill `cat $(TESTPID)` echo Tests succeeded. -- cgit v1.2.3 From 13a90684dc583427ad1d59a10d8bb00928fb6687 Mon Sep 17 00:00:00 2001 From: Marvin Sielenkemper Date: Fri, 16 Sep 2016 22:38:08 +0200 Subject: Amend the tests to check if the new option works --- Makefile.am | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Makefile.am b/Makefile.am index 8e945a47..3eb0a377 100644 --- a/Makefile.am +++ b/Makefile.am @@ -116,14 +116,14 @@ test: bin/urweb -boot -noEmacs -dbms sqlite -db $(TESTDB) -demo /Demo demo rm -f $(TESTDB) sqlite3 $(TESTDB) < demo/demo.sql - demo/demo.exe & echo $$! > $(TESTPID) + demo/demo.exe -a 127.0.0.1 & echo $$! > $(TESTPID) sleep 1 (curl -s 'http://localhost:8080/Demo/Hello/main' | diff tests/hello.html -) || (kill `cat $(TESTPID)`; echo "Test 'Hello' failed"; /bin/false) (curl -s 'http://localhost:8080/Demo/Crud1/create?A=1&B=2&C=3&D=4' | diff tests/crud1.html -) || (kill `cat $(TESTPID)`; echo "Test 'Crud1' failed"; /bin/false) kill `cat $(TESTPID)` rm -f $(TESTDB) sqlite3 $(TESTDB) < demo/demo.sql - demo/demo.exe & echo $$! > $(TESTPID) + demo/demo.exe -A ::1 & echo $$! > $(TESTPID) sleep 1 (curl -s 'http://[::1]:8080/Demo/Hello/main' | diff tests/hello.html -) || (kill `cat $(TESTPID)`; echo "Test 'Hello' failed"; /bin/false) (curl -s 'http://[::1]:8080/Demo/Crud1/create?A=1&B=2&C=3&D=4' | diff tests/crud1.html -) || (kill `cat $(TESTPID)`; echo "Test 'Crud1' failed"; /bin/false) -- cgit v1.2.3 From 10baddb6ee0fcd645ef0da096ef1dd5b6d4e09a4 Mon Sep 17 00:00:00 2001 From: Marvin Sielenkemper Date: Sat, 17 Sep 2016 08:22:12 +0200 Subject: Explicitly cleares IPV6_V6ONLY to ensure IPv4 availability --- src/c/http.c | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/src/c/http.c b/src/c/http.c index 5752fc6f..b3ada18a 100644 --- a/src/c/http.c +++ b/src/c/http.c @@ -11,6 +11,8 @@ #include #include #include +#include +#include #include @@ -328,7 +330,7 @@ int main(int argc, char *argv[]) { struct sockaddr_in6 my_addr; struct sockaddr_in6 their_addr; // connector's address information socklen_t sin_size; - int yes = 1, uw_port = 8080, nthreads = 1, i, *names, opt; + int yes = 1, no = 0, uw_port = 8080, nthreads = 1, i, *names, opt; int recv_timeout_sec = 5; signal(SIGINT, sigint); @@ -425,6 +427,11 @@ int main(int argc, char *argv[]) { return 1; } + if (setsockopt(sockfd, IPPROTO_IPV6, IPV6_V6ONLY, &no, sizeof(int)) < 0) { + fprintf(stderr, "Listener IPV6_V6ONLY option resetting failed\n"); + return 1; + } + my_addr.sin6_family = AF_INET6; // host byte order my_addr.sin6_port = htons(uw_port); // short, network byte order -- cgit v1.2.3 From 6bd95b0236f8142181aae369c93e00d15bb3c83f Mon Sep 17 00:00:00 2001 From: Marvin Sielenkemper Date: Sat, 17 Sep 2016 09:33:39 +0200 Subject: Check if the loopback interface has an IPv6 address and only run IPv6 test tests if this is the case --- Makefile.am | 20 +++++++++++++------- 1 file changed, 13 insertions(+), 7 deletions(-) diff --git a/Makefile.am b/Makefile.am index 3eb0a377..0b9a5957 100644 --- a/Makefile.am +++ b/Makefile.am @@ -121,11 +121,17 @@ test: (curl -s 'http://localhost:8080/Demo/Hello/main' | diff tests/hello.html -) || (kill `cat $(TESTPID)`; echo "Test 'Hello' failed"; /bin/false) (curl -s 'http://localhost:8080/Demo/Crud1/create?A=1&B=2&C=3&D=4' | diff tests/crud1.html -) || (kill `cat $(TESTPID)`; echo "Test 'Crud1' failed"; /bin/false) kill `cat $(TESTPID)` - rm -f $(TESTDB) - sqlite3 $(TESTDB) < demo/demo.sql - demo/demo.exe -A ::1 & echo $$! > $(TESTPID) - sleep 1 - (curl -s 'http://[::1]:8080/Demo/Hello/main' | diff tests/hello.html -) || (kill `cat $(TESTPID)`; echo "Test 'Hello' failed"; /bin/false) - (curl -s 'http://[::1]:8080/Demo/Crud1/create?A=1&B=2&C=3&D=4' | diff tests/crud1.html -) || (kill `cat $(TESTPID)`; echo "Test 'Crud1' failed"; /bin/false) - kill `cat $(TESTPID)` + if (ifconfig lo | grep -q inet6); \ + then \ + echo "Running IPv6 tests."; \ + rm -f $(TESTDB); \ + sqlite3 $(TESTDB) < demo/demo.sql; \ + demo/demo.exe -A ::1 & echo $$! > $(TESTPID); \ + sleep 1; \ + (curl -s 'http://[::1]:8080/Demo/Hello/main' | diff tests/hello.html -) || (kill `cat $(TESTPID)`; echo "Test 'Hello' failed"; /bin/false); \ + (curl -s 'http://[::1]:8080/Demo/Crud1/create?A=1&B=2&C=3&D=4' | diff tests/crud1.html -) || (kill `cat $(TESTPID)`; echo "Test 'Crud1' failed"; /bin/false); \ + kill `cat $(TESTPID)`; \ + else \ + echo "Skipped IPv6 tests."; \ + fi echo Tests succeeded. -- cgit v1.2.3 From 8c7115af98f01fc6bc3683f20be252412cb75d1c Mon Sep 17 00:00:00 2001 From: Marvin Sielenkemper Date: Sat, 17 Sep 2016 09:40:56 +0200 Subject: Remove accidentally commited unused includes which do not exist on MacOS --- src/c/http.c | 2 -- 1 file changed, 2 deletions(-) diff --git a/src/c/http.c b/src/c/http.c index b3ada18a..2c409c04 100644 --- a/src/c/http.c +++ b/src/c/http.c @@ -11,8 +11,6 @@ #include #include #include -#include -#include #include -- cgit v1.2.3 From 62ac943afa64fd4fc16f47c2f23fd90f7c5fff81 Mon Sep 17 00:00:00 2001 From: Marvin Sielenkemper Date: Sat, 17 Sep 2016 12:42:00 +0200 Subject: Try adding an IPv6 address to interface lo --- .travis.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.travis.yml b/.travis.yml index df4e4abc..32f1e3ee 100644 --- a/.travis.yml +++ b/.travis.yml @@ -24,5 +24,6 @@ before_install: - if command -v brew &>/dev/null; then brew install libtool; fi - if command -v brew &>/dev/null; then brew install openssl mlton; fi - if command -v brew &>/dev/null; then export CONFIGURE_ARGS="--with-openssl=/usr/local/opt/openssl"; fi + - if command -v ifconfig &>/dev/null; then sudo ifconfig lo add ::1; fi script: ./autogen.sh && ./configure $CONFIGURE_ARGS && make && make test -- cgit v1.2.3 From 0d9b72d8806df969a7a4f3dbf0813eb68c52fae0 Mon Sep 17 00:00:00 2001 From: Marvin Sielenkemper Date: Sat, 17 Sep 2016 12:44:57 +0200 Subject: Revert "Try adding an IPv6 address to interface lo" This reverts commit 62ac943afa64fd4fc16f47c2f23fd90f7c5fff81. --- .travis.yml | 1 - 1 file changed, 1 deletion(-) diff --git a/.travis.yml b/.travis.yml index 32f1e3ee..df4e4abc 100644 --- a/.travis.yml +++ b/.travis.yml @@ -24,6 +24,5 @@ before_install: - if command -v brew &>/dev/null; then brew install libtool; fi - if command -v brew &>/dev/null; then brew install openssl mlton; fi - if command -v brew &>/dev/null; then export CONFIGURE_ARGS="--with-openssl=/usr/local/opt/openssl"; fi - - if command -v ifconfig &>/dev/null; then sudo ifconfig lo add ::1; fi script: ./autogen.sh && ./configure $CONFIGURE_ARGS && make && make test -- cgit v1.2.3 From 3afe16344c02881e4ace9e8965f48154b5225638 Mon Sep 17 00:00:00 2001 From: Adam Chlipala Date: Mon, 19 Sep 2016 15:52:05 -0400 Subject: make test: add extra 'curl' flags to make IPv6 work in Ubuntu 14.04 --- Makefile.am | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Makefile.am b/Makefile.am index 0b9a5957..83a08171 100644 --- a/Makefile.am +++ b/Makefile.am @@ -128,8 +128,8 @@ test: sqlite3 $(TESTDB) < demo/demo.sql; \ demo/demo.exe -A ::1 & echo $$! > $(TESTPID); \ sleep 1; \ - (curl -s 'http://[::1]:8080/Demo/Hello/main' | diff tests/hello.html -) || (kill `cat $(TESTPID)`; echo "Test 'Hello' failed"; /bin/false); \ - (curl -s 'http://[::1]:8080/Demo/Crud1/create?A=1&B=2&C=3&D=4' | diff tests/crud1.html -) || (kill `cat $(TESTPID)`; echo "Test 'Crud1' failed"; /bin/false); \ + (curl -g -6 -s 'http://[::1]:8080/Demo/Hello/main' | diff tests/hello.html -) || (kill `cat $(TESTPID)`; echo "Test 'Hello' failed"; /bin/false); \ + (curl -g -6 -s 'http://[::1]:8080/Demo/Crud1/create?A=1&B=2&C=3&D=4' | diff tests/crud1.html -) || (kill `cat $(TESTPID)`; echo "Test 'Crud1' failed"; /bin/false); \ kill `cat $(TESTPID)`; \ else \ echo "Skipped IPv6 tests."; \ -- cgit v1.2.3 From 2f81ca80930450cbbb12dba7abd112e7b296f1f6 Mon Sep 17 00:00:00 2001 From: Jacob Mitchell Date: Mon, 19 Sep 2016 22:34:11 -0700 Subject: Use 1s precision when comparing file mtimes HTTP Last-Modified caching depends on mtimes of files read during compilation. Only mtimes before the compiler's start (reset) time are considered. On filesystems with lower mtime precision than Standard ML's Time.now, a temporary file generated by the compiler may have an mtime preceding the reset time even though it was modified after. The fix here is to compare times using 1s precision, the most granular used by filesystems. --- src/fileio.sml | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/src/fileio.sml b/src/fileio.sml index 72e72f6d..cab9d8a3 100644 --- a/src/fileio.sml +++ b/src/fileio.sml @@ -3,9 +3,14 @@ structure FileIO :> FILE_IO = struct val mostRecentModTimeRef = ref (Time.zeroTime) fun checkFileModTime fname = - let val mtime = OS.FileSys.modTime fname in - if Time.compare (mtime, !mostRecentModTimeRef) = GREATER andalso - Time.compare (mtime, Globals.getResetTime ()) = LESS + let + val mtime = OS.FileSys.modTime fname + val mostRecentMod = !mostRecentModTimeRef + val resetTime = Globals.getResetTime () + fun lessThan (a, b) = LargeInt.compare (Time.toSeconds a, Time.toSeconds b) = LESS + infix lessThan + in + if mostRecentMod lessThan mtime andalso mtime lessThan resetTime then mostRecentModTimeRef := mtime else () end -- cgit v1.2.3 From 6720a4578afab577f9721485fbb8e810104d6249 Mon Sep 17 00:00:00 2001 From: Adam Chlipala Date: Tue, 20 Sep 2016 14:26:58 -0400 Subject: Make HTML5 the default and add 'xhtml' .urp directive --- doc/manual.tex | 3 ++- src/compiler.sml | 3 +++ src/settings.sml | 2 +- 3 files changed, 6 insertions(+), 2 deletions(-) diff --git a/doc/manual.tex b/doc/manual.tex index 8c12102e..b65809d0 100644 --- a/doc/manual.tex +++ b/doc/manual.tex @@ -151,7 +151,7 @@ Here is the complete list of directive forms. ``FFI'' stands for ``foreign func \item \texttt{exe FILENAME} sets the filename to which to write the output executable. The default for file \texttt{P.urp} is \texttt{P.exe}. \item \texttt{file URI FILENAME} asks for the application executable to respond to requests for \texttt{URI} by serving a snapshot of the contents of \texttt{FILENAME} as of compile time. That is, the file contents are baked into the executable. System file \texttt{/etc/mime.types} is consulted (again, at compile time) to figure out the right MIME type to suggest in the HTTP response. \item \texttt{ffi FILENAME} reads the file \texttt{FILENAME.urs} to determine the interface to a new FFI module. The name of the module is calculated from \texttt{FILENAME} in the same way as for normal source files. See the files \texttt{include/urweb/urweb\_cpp.h} and \texttt{src/c/urweb.c} for examples of C headers and implementations for FFI modules. In general, every type or value \texttt{Module.ident} becomes \texttt{uw\_Module\_ident} in C. -\item \texttt{html5} activates work-in-progress support for generating HTML5 instead of XHTML. For now, this option only affects the first few tokens on any page, which are always the same. +\item \texttt{html5} asks to generate HTML5 code, which primarily affects the first few lines of the output documents, like the \texttt{DOCTYPE}. This option is on by default. \item \texttt{include FILENAME} adds \texttt{FILENAME} to the list of files to be \texttt{\#include}d in C sources. This is most useful for interfacing with new FFI modules. \item \texttt{jsFile FILENAME} asks to serve the contents of a file as JavaScript. All such content is concatenated into a single file, included via a \texttt{