Bug 1454134 [wpt PR 8735] - [wptserve] Add on-demand file hash computation, a=testonly
authorMike Pennisi <mike@mikepennisi.com>
Mon, 23 Apr 2018 18:32:19 +0000
changeset 461282 ab11deb7d6b33f8dde1f15816b2177e2100285c1
parent 461281 51cffe1745dd12db96f0be639f21ca8a04353505
child 461283 fd611937fde85767f9b5a8ffb903807bbdbcde48
push id166
push userfmarier@mozilla.com
push dateThu, 10 May 2018 00:43:18 +0000
reviewerstestonly
bugs1454134
milestone61.0a1
Bug 1454134 [wpt PR 8735] - [wptserve] Add on-demand file hash computation, a=testonly Automatic update from web-platform-tests[wptserve] Add tests -- [wptserve] Correct typo in documentation -- [wptserve] Expand syntax to support fn invocation -- [wptserve] Add substitution to calculate file hash Additionally, update the two Web Platform Tests which previously specified static values for the expected hash of shared resource files. -- wpt-commits: cc236aa5904d4131ef725762a2f91f564de86615, 18ee2b46ab4c3c25053a01ded402834b65b75565, dd3f71885bd5039f7ba6f357ac2a43fc510ee23f, e0ea063afe0759e5c4928b0ca0e2c6a561ef7eaa wpt-pr: 8735
testing/web-platform/meta/MANIFEST.json
testing/web-platform/tests/html/semantics/scripting-1/the-script-element/module/dynamic-import/string-compilation-integrity-classic.html
testing/web-platform/tests/html/semantics/scripting-1/the-script-element/module/dynamic-import/string-compilation-integrity-classic.sub.html
testing/web-platform/tests/html/semantics/scripting-1/the-script-element/module/dynamic-import/string-compilation-integrity-module.html
testing/web-platform/tests/html/semantics/scripting-1/the-script-element/module/dynamic-import/string-compilation-integrity-module.sub.html
testing/web-platform/tests/tools/wptserve/tests/functional/docroot/sub_file_hash.sub.txt
testing/web-platform/tests/tools/wptserve/tests/functional/docroot/sub_file_hash_subject.txt
testing/web-platform/tests/tools/wptserve/tests/functional/docroot/sub_file_hash_unrecognized.sub.txt
testing/web-platform/tests/tools/wptserve/tests/functional/docroot/sub_location.sub.txt
testing/web-platform/tests/tools/wptserve/tests/functional/docroot/sub_url_base.sub.txt
testing/web-platform/tests/tools/wptserve/tests/functional/docroot/sub_uuid.sub.txt
testing/web-platform/tests/tools/wptserve/tests/functional/docroot/sub_var.sub.txt
testing/web-platform/tests/tools/wptserve/tests/functional/test_pipes.py
testing/web-platform/tests/tools/wptserve/tests/test_replacement_tokenizer.py
testing/web-platform/tests/tools/wptserve/wptserve/pipes.py
--- a/testing/web-platform/meta/MANIFEST.json
+++ b/testing/web-platform/meta/MANIFEST.json
@@ -338669,25 +338669,25 @@
     ]
    ],
    "html/semantics/scripting-1/the-script-element/module/dynamic-import/string-compilation-classic.html": [
     [
      "/html/semantics/scripting-1/the-script-element/module/dynamic-import/string-compilation-classic.html",
      {}
     ]
    ],
-   "html/semantics/scripting-1/the-script-element/module/dynamic-import/string-compilation-integrity-classic.html": [
-    [
-     "/html/semantics/scripting-1/the-script-element/module/dynamic-import/string-compilation-integrity-classic.html",
-     {}
-    ]
-   ],
-   "html/semantics/scripting-1/the-script-element/module/dynamic-import/string-compilation-integrity-module.html": [
-    [
-     "/html/semantics/scripting-1/the-script-element/module/dynamic-import/string-compilation-integrity-module.html",
+   "html/semantics/scripting-1/the-script-element/module/dynamic-import/string-compilation-integrity-classic.sub.html": [
+    [
+     "/html/semantics/scripting-1/the-script-element/module/dynamic-import/string-compilation-integrity-classic.sub.html",
+     {}
+    ]
+   ],
+   "html/semantics/scripting-1/the-script-element/module/dynamic-import/string-compilation-integrity-module.sub.html": [
+    [
+     "/html/semantics/scripting-1/the-script-element/module/dynamic-import/string-compilation-integrity-module.sub.html",
      {}
     ]
    ],
    "html/semantics/scripting-1/the-script-element/module/dynamic-import/string-compilation-module.html": [
     [
      "/html/semantics/scripting-1/the-script-element/module/dynamic-import/string-compilation-module.html",
      {}
     ]
@@ -574274,22 +574274,22 @@
   "html/semantics/scripting-1/the-script-element/module/dynamic-import/string-compilation-base-url-inline-module.html": [
    "1daf837d2d9ee258dfc5c9648b1a0f5b0b6e93e4",
    "testharness"
   ],
   "html/semantics/scripting-1/the-script-element/module/dynamic-import/string-compilation-classic.html": [
    "e22e6f200162000f043d114c89def6667097d13d",
    "testharness"
   ],
-  "html/semantics/scripting-1/the-script-element/module/dynamic-import/string-compilation-integrity-classic.html": [
-   "0295db34d953b716151726188ac467a948f0d955",
-   "testharness"
-  ],
-  "html/semantics/scripting-1/the-script-element/module/dynamic-import/string-compilation-integrity-module.html": [
-   "cfc2375f7a8abe67eae4213b2f0c5224ba25337d",
+  "html/semantics/scripting-1/the-script-element/module/dynamic-import/string-compilation-integrity-classic.sub.html": [
+   "68b065c3462e663b10d09e140a733e01271cf834",
+   "testharness"
+  ],
+  "html/semantics/scripting-1/the-script-element/module/dynamic-import/string-compilation-integrity-module.sub.html": [
+   "cbf05c10d041abe6b3769b474e0be58f4b56e627",
    "testharness"
   ],
   "html/semantics/scripting-1/the-script-element/module/dynamic-import/string-compilation-module.html": [
    "b332499d43e0768b5ddf1d5dbb5cd8ca702c3c64",
    "testharness"
   ],
   "html/semantics/scripting-1/the-script-element/module/dynamic-import/string-compilation-nonce-classic.html": [
    "20a6c6de42fe72fe375ccd8c9c8763191afa78f9",
deleted file mode 100644
--- a/testing/web-platform/tests/html/semantics/scripting-1/the-script-element/module/dynamic-import/string-compilation-integrity-classic.html
+++ /dev/null
@@ -1,52 +0,0 @@
-<!DOCTYPE html>
-<meta charset="utf-8">
-<title>import() doesn't have any integrity metadata when initiated by compiled strings inside a classic script</title>
-<link rel="author" title="Domenic Denicola" href="mailto:d@domenic.me">
-<meta http-equiv="Content-Security-Policy" content="require-sri-for script">
-
-<script src="/resources/testharness.js" integrity="sha384-4Nybydhnr3tOpv1yrTkDxu3RFpnxWAxlU5kGn7c8ebKvh1iUdfVMjqP6jf0dacrV"></script>
-<script src="/resources/testharnessreport.js" integrity="sha384-GOnHxuyo+nnsFAe4enY+RAl4/+w5NPMJPCQiDroTjxtR7ndRz7Uan8vNbM2qWKmU"></script>
-
-<div id="dummy"></div>
-
-<script>
-function createTestPromise() {
-  return new Promise((resolve, reject) => {
-    window.continueTest = resolve;
-    window.errorTest = reject;
-  });
-}
-
-const dummyDiv = document.querySelector("#dummy");
-
-const evaluators = {
-  eval,
-  setTimeout,
-  "the Function constructor"(x) {
-    Function(x)();
-  },
-  "reflected inline event handlers"(x) {
-    dummyDiv.setAttribute("onclick", x);
-    dummyDiv.onclick();
-  },
-  "inline event handlers triggered via UA code"(x) {
-    dummyDiv.setAttribute("onclick", x);
-    dummyDiv.click(); // different from .**on**click()
-  }
-};
-
-for (const [label, evaluator] of Object.entries(evaluators)) {
-  promise_test(t => {
-    t.add_cleanup(() => {
-      dummyDiv.removeAttribute("onclick");
-      delete window.evaluated_imports_a;
-    });
-
-    const promise = createTestPromise();
-
-    evaluator(`import('../imports-a.js?label=${label}').then(window.continueTest, window.errorTest);`);
-
-    return promise_rejects(t, new TypeError(), promise);
-  }, label + " should fail to import");
-};
-</script>
new file mode 100644
--- /dev/null
+++ b/testing/web-platform/tests/html/semantics/scripting-1/the-script-element/module/dynamic-import/string-compilation-integrity-classic.sub.html
@@ -0,0 +1,52 @@
+<!DOCTYPE html>
+<meta charset="utf-8">
+<title>import() doesn't have any integrity metadata when initiated by compiled strings inside a classic script</title>
+<link rel="author" title="Domenic Denicola" href="mailto:d@domenic.me">
+<meta http-equiv="Content-Security-Policy" content="require-sri-for script">
+
+<script src="/resources/testharness.js" integrity="sha384-{{file_hash(sha384, resources/testharness.js)}}"></script>
+<script src="/resources/testharnessreport.js" integrity="sha384-{{file_hash(sha384, resources/testharnessreport.js)}}"></script>
+
+<div id="dummy"></div>
+
+<script>
+function createTestPromise() {
+  return new Promise((resolve, reject) => {
+    window.continueTest = resolve;
+    window.errorTest = reject;
+  });
+}
+
+const dummyDiv = document.querySelector("#dummy");
+
+const evaluators = {
+  eval,
+  setTimeout,
+  "the Function constructor"(x) {
+    Function(x)();
+  },
+  "reflected inline event handlers"(x) {
+    dummyDiv.setAttribute("onclick", x);
+    dummyDiv.onclick();
+  },
+  "inline event handlers triggered via UA code"(x) {
+    dummyDiv.setAttribute("onclick", x);
+    dummyDiv.click(); // different from .**on**click()
+  }
+};
+
+for (const [label, evaluator] of Object.entries(evaluators)) {
+  promise_test(t => {
+    t.add_cleanup(() => {
+      dummyDiv.removeAttribute("onclick");
+      delete window.evaluated_imports_a;
+    });
+
+    const promise = createTestPromise();
+
+    evaluator(`import('../imports-a.js?label=${label}').then(window.continueTest, window.errorTest);`);
+
+    return promise_rejects(t, new TypeError(), promise);
+  }, label + " should fail to import");
+};
+</script>
deleted file mode 100644
--- a/testing/web-platform/tests/html/semantics/scripting-1/the-script-element/module/dynamic-import/string-compilation-integrity-module.html
+++ /dev/null
@@ -1,52 +0,0 @@
-<!DOCTYPE html>
-<meta charset="utf-8">
-<title>import() doesn't have any integrity metadata when initiated by compiled strings inside a module script</title>
-<link rel="author" title="Domenic Denicola" href="mailto:d@domenic.me">
-<meta http-equiv="Content-Security-Policy" content="require-sri-for script">
-
-<script src="/resources/testharness.js" integrity="sha384-4Nybydhnr3tOpv1yrTkDxu3RFpnxWAxlU5kGn7c8ebKvh1iUdfVMjqP6jf0dacrV"></script>
-<script src="/resources/testharnessreport.js" integrity="sha384-GOnHxuyo+nnsFAe4enY+RAl4/+w5NPMJPCQiDroTjxtR7ndRz7Uan8vNbM2qWKmU"></script>
-
-<div id="dummy"></div>
-
-<script type="module">
-function createTestPromise() {
-  return new Promise((resolve, reject) => {
-    window.continueTest = resolve;
-    window.errorTest = reject;
-  });
-}
-
-const dummyDiv = document.querySelector("#dummy");
-
-const evaluators = {
-  eval,
-  setTimeout,
-  "the Function constructor"(x) {
-    Function(x)();
-  },
-  "reflected inline event handlers"(x) {
-    dummyDiv.setAttribute("onclick", x);
-    dummyDiv.onclick();
-  },
-  "inline event handlers triggered via UA code"(x) {
-    dummyDiv.setAttribute("onclick", x);
-    dummyDiv.click(); // different from .**on**click()
-  }
-};
-
-for (const [label, evaluator] of Object.entries(evaluators)) {
-  promise_test(t => {
-    t.add_cleanup(() => {
-      dummyDiv.removeAttribute("onclick");
-      delete window.evaluated_imports_a;
-    });
-
-    const promise = createTestPromise();
-
-    evaluator(`import('../imports-a.js?label=${label}').then(window.continueTest, window.errorTest);`);
-
-    return promise_rejects(t, new TypeError(), promise);
-  }, label + " should fail to import");
-};
-</script>
new file mode 100644
--- /dev/null
+++ b/testing/web-platform/tests/html/semantics/scripting-1/the-script-element/module/dynamic-import/string-compilation-integrity-module.sub.html
@@ -0,0 +1,52 @@
+<!DOCTYPE html>
+<meta charset="utf-8">
+<title>import() doesn't have any integrity metadata when initiated by compiled strings inside a module script</title>
+<link rel="author" title="Domenic Denicola" href="mailto:d@domenic.me">
+<meta http-equiv="Content-Security-Policy" content="require-sri-for script">
+
+<script src="/resources/testharness.js" integrity="sha384-{{file_hash(sha384, resources/testharness.js)}}"></script>
+<script src="/resources/testharnessreport.js" integrity="sha384-{{file_hash(sha384, resources/testharnessreport.js)}}"></script>
+
+<div id="dummy"></div>
+
+<script type="module">
+function createTestPromise() {
+  return new Promise((resolve, reject) => {
+    window.continueTest = resolve;
+    window.errorTest = reject;
+  });
+}
+
+const dummyDiv = document.querySelector("#dummy");
+
+const evaluators = {
+  eval,
+  setTimeout,
+  "the Function constructor"(x) {
+    Function(x)();
+  },
+  "reflected inline event handlers"(x) {
+    dummyDiv.setAttribute("onclick", x);
+    dummyDiv.onclick();
+  },
+  "inline event handlers triggered via UA code"(x) {
+    dummyDiv.setAttribute("onclick", x);
+    dummyDiv.click(); // different from .**on**click()
+  }
+};
+
+for (const [label, evaluator] of Object.entries(evaluators)) {
+  promise_test(t => {
+    t.add_cleanup(() => {
+      dummyDiv.removeAttribute("onclick");
+      delete window.evaluated_imports_a;
+    });
+
+    const promise = createTestPromise();
+
+    evaluator(`import('../imports-a.js?label=${label}').then(window.continueTest, window.errorTest);`);
+
+    return promise_rejects(t, new TypeError(), promise);
+  }, label + " should fail to import");
+};
+</script>
new file mode 100644
--- /dev/null
+++ b/testing/web-platform/tests/tools/wptserve/tests/functional/docroot/sub_file_hash.sub.txt
@@ -0,0 +1,6 @@
+md5: {{file_hash(md5, sub_file_hash_subject.txt)}}
+sha1: {{file_hash(sha1, sub_file_hash_subject.txt)}}
+sha224: {{file_hash(sha224, sub_file_hash_subject.txt)}}
+sha256: {{file_hash(sha256, sub_file_hash_subject.txt)}}
+sha384: {{file_hash(sha384, sub_file_hash_subject.txt)}}
+sha512: {{file_hash(sha512, sub_file_hash_subject.txt)}}
new file mode 100644
--- /dev/null
+++ b/testing/web-platform/tests/tools/wptserve/tests/functional/docroot/sub_file_hash_subject.txt
@@ -0,0 +1,2 @@
+This file is used to verify expected behavior of the `file_hash` "sub"
+function.
new file mode 100644
--- /dev/null
+++ b/testing/web-platform/tests/tools/wptserve/tests/functional/docroot/sub_file_hash_unrecognized.sub.txt
@@ -0,0 +1,1 @@
+{{file_hash(sha007, sub_file_hash_subject.txt)}}
new file mode 100644
--- /dev/null
+++ b/testing/web-platform/tests/tools/wptserve/tests/functional/docroot/sub_location.sub.txt
@@ -0,0 +1,8 @@
+host: {{location[host]}}
+hostname: {{location[hostname]}}
+path: {{location[path]}}
+pathname: {{location[pathname]}}
+port: {{location[port]}}
+query: {{location[query]}}
+scheme: {{location[scheme]}}
+server: {{location[server]}}
new file mode 100644
--- /dev/null
+++ b/testing/web-platform/tests/tools/wptserve/tests/functional/docroot/sub_url_base.sub.txt
@@ -0,0 +1,1 @@
+Before {{url_base}} After
new file mode 100644
--- /dev/null
+++ b/testing/web-platform/tests/tools/wptserve/tests/functional/docroot/sub_uuid.sub.txt
@@ -0,0 +1,1 @@
+Before {{uuid()}} After
new file mode 100644
--- /dev/null
+++ b/testing/web-platform/tests/tools/wptserve/tests/functional/docroot/sub_var.sub.txt
@@ -0,0 +1,1 @@
+{{$first:host}} {{$second:ports[http][0]}} A {{$second}} B {{$first}} C
--- a/testing/web-platform/tests/tools/wptserve/tests/functional/test_pipes.py
+++ b/testing/web-platform/tests/tools/wptserve/tests/functional/test_pipes.py
@@ -1,10 +1,11 @@
 import os
 import unittest
+import urllib2
 import time
 import json
 
 import pytest
 
 wptserve = pytest.importorskip("wptserve")
 from .base import TestUsingServer, doc_root
 
@@ -53,26 +54,69 @@ class TestSlice(TestUsingServer):
         self.assertEqual(resp.read(), expected[:10])
 
 class TestSub(TestUsingServer):
     def test_sub_config(self):
         resp = self.request("/sub.txt", query="pipe=sub")
         expected = "localhost localhost %i" % self.server.port
         self.assertEqual(resp.read().rstrip(), expected)
 
+    def test_sub_file_hash(self):
+        resp = self.request("/sub_file_hash.sub.txt")
+        expected = """
+md5: JmI1W8fMHfSfCarYOSxJcw==
+sha1: nqpWqEw4IW8NjD6R375gtrQvtTo=
+sha224: RqQ6fMmta6n9TuA/vgTZK2EqmidqnrwBAmQLRQ==
+sha256: G6Ljg1uPejQxqFmvFOcV/loqnjPTW5GSOePOfM/u0jw=
+sha384: lkXHChh1BXHN5nT5BYhi1x67E1CyYbPKRKoF2LTm5GivuEFpVVYtvEBHtPr74N9E
+sha512: r8eLGRTc7ZznZkFjeVLyo6/FyQdra9qmlYCwKKxm3kfQAswRS9+3HsYk3thLUhcFmmWhK4dXaICz
+JwGFonfXwg=="""
+        self.assertEqual(resp.read().rstrip(), expected.strip())
+
+    def test_sub_file_hash_unrecognized(self):
+        with self.assertRaises(urllib2.HTTPError):
+            self.request("/sub_file_hash_unrecognized.sub.txt")
+
     def test_sub_headers(self):
         resp = self.request("/sub_headers.txt", query="pipe=sub", headers={"X-Test": "PASS"})
         expected = "PASS"
         self.assertEqual(resp.read().rstrip(), expected)
 
+    def test_sub_location(self):
+        resp = self.request("/sub_location.sub.txt?query_string")
+        expected = """
+host: localhost:{0}
+hostname: localhost
+path: /sub_location.sub.txt
+pathname: /sub_location.sub.txt
+port: {0}
+query: ?query_string
+scheme: http
+server: http://localhost:{0}""".format(self.server.port)
+        self.assertEqual(resp.read().rstrip(), expected.strip())
+
     def test_sub_params(self):
         resp = self.request("/sub_params.txt", query="test=PASS&pipe=sub")
         expected = "PASS"
         self.assertEqual(resp.read().rstrip(), expected)
 
+    def test_sub_url_base(self):
+        resp = self.request("/sub_url_base.sub.txt")
+        self.assertEqual(resp.read().rstrip(), "Before / After")
+
+    def test_sub_uuid(self):
+        resp = self.request("/sub_uuid.sub.txt")
+        self.assertRegexpMatches(resp.read().rstrip(), r"Before [a-f0-9-]+ After")
+
+    def test_sub_var(self):
+        resp = self.request("/sub_var.sub.txt")
+        port = self.server.port
+        expected = "localhost %s A %s B localhost C" % (port, port)
+        self.assertEqual(resp.read().rstrip(), expected)
+
 class TestTrickle(TestUsingServer):
     def test_trickle(self):
         #Actually testing that the response trickles in is not that easy
         t0 = time.time()
         resp = self.request("/document.txt", query="pipe=trickle(1:d2:5:d1:r2)")
         t1 = time.time()
         expected = open(os.path.join(doc_root, "document.txt"), 'rb').read()
         self.assertEqual(resp.read(), expected)
--- a/testing/web-platform/tests/tools/wptserve/tests/test_replacement_tokenizer.py
+++ b/testing/web-platform/tests/tools/wptserve/tests/test_replacement_tokenizer.py
@@ -1,17 +1,17 @@
 import pytest
 
 from wptserve.pipes import ReplacementTokenizer
 
 @pytest.mark.parametrize(
     "content,expected",
     [
         ["aaa", [('ident', 'aaa')]],
-        ["bbb()", [('ident', 'bbb()')]],
+        ["bbb()", [('ident', 'bbb'), ('arguments', [])]],
         ["$ccc:ddd", [('var', '$ccc'), ('ident', 'ddd')]],
         ["$eee", [('ident', '$eee')]],
         ["fff[0]", [('ident', 'fff'), ('index', 0)]],
         ["ggg[hhh]", [('ident', 'ggg'), ('index', u'hhh')]],
         ["[iii]", [('index', u'iii')]],
         ["jjj['kkk']", [('ident', 'jjj'), ('index', u"'kkk'")]],
         ["lll[]", [('ident', 'lll'), ('index', u"")]],
         ["111", [('ident', u'111')]],
--- a/testing/web-platform/tests/tools/wptserve/wptserve/pipes.py
+++ b/testing/web-platform/tests/tools/wptserve/wptserve/pipes.py
@@ -1,10 +1,12 @@
 from cgi import escape
 import gzip as gzip_module
+import hashlib
+import os
 import re
 import time
 import types
 import uuid
 from cStringIO import StringIO
 
 from six import text_type
 
@@ -272,16 +274,20 @@ def slice(request, response, start, end=
                 the file.
     """
     content = resolve_content(response)
     response.content = content[start:end]
     return response
 
 
 class ReplacementTokenizer(object):
+    def arguments(self, token):
+        unwrapped = token[1:-1]
+        return ("arguments", re.split(r",\s*", token[1:-1]) if unwrapped else [])
+
     def ident(self, token):
         return ("ident", token)
 
     def index(self, token):
         token = token[1:-1]
         try:
             token = int(token)
         except ValueError:
@@ -291,18 +297,19 @@ class ReplacementTokenizer(object):
     def var(self, token):
         token = token[:-1]
         return ("var", token)
 
     def tokenize(self, string):
         return self.scanner.scan(string)[0]
 
     scanner = re.Scanner([(r"\$\w+:", var),
-                          (r"\$?\w+(?:\(\))?", ident),
-                          (r"\[[^\]]*\]", index)])
+                          (r"\$?\w+", ident),
+                          (r"\[[^\]]*\]", index),
+                          (r"\([^)]*\)", arguments)])
 
 
 class FirstWrapper(object):
     def __init__(self, params):
         self.params = params
 
     def __getitem__(self, key):
         try:
@@ -334,42 +341,80 @@ def sub(request, response, escape_type="
       'server' is scheme://host:port, 'host' is hostname:port, and query
        includes the leading '?', but other delimiters are omitted.
     headers
       A dictionary of HTTP headers in the request.
     GET
       A dictionary of query parameters supplied with the request.
     uuid()
       A pesudo-random UUID suitable for usage with stash
+    file_hash(algorithm, filepath)
+      The cryptographic hash of a file. Supported algorithms: md5, sha1,
+      sha224, sha256, sha384, and sha512. For example:
+
+        {{file_hash(md5, dom/interfaces.html)}}
 
     So for example in a setup running on localhost with a www
     subdomain and a http server on ports 80 and 81::
 
       {{host}} => localhost
       {{domains[www]}} => www.localhost
       {{ports[http][1]}} => 81
 
 
     It is also possible to assign a value to a variable name, which must start with
     the $ character, using the ":" syntax e.g.
 
-    {{$id:uuid()}
+    {{$id:uuid()}}
 
     Later substitutions in the same file may then refer to the variable
     by name e.g.
 
     {{$id}}
     """
     content = resolve_content(response)
 
     new_content = template(request, content, escape_type=escape_type)
 
     response.content = new_content
     return response
 
+class SubFunctions(object):
+    @staticmethod
+    def uuid(request):
+        return str(uuid.uuid4())
+
+    # Maintain a whitelist of supported algorithms, restricted to those that
+    # are available on all platforms [1]. This ensures that test authors do not
+    # unknowingly introduce platform-specific tests.
+    #
+    # [1] https://docs.python.org/2/library/hashlib.html
+    supported_algorithms = ("md5", "sha1", "sha224", "sha256", "sha384", "sha512")
+
+    @staticmethod
+    def file_hash(request, algorithm, path):
+        if algorithm not in SubFunctions.supported_algorithms:
+            raise ValueError("Unsupported encryption algorithm: '%s'" % algorithm)
+
+        hash_obj = getattr(hashlib, algorithm)()
+        absolute_path = os.path.join(request.doc_root, path)
+
+        try:
+            with open(absolute_path) as f:
+                hash_obj.update(f.read())
+        except IOError:
+            # In this context, an unhandled IOError will be interpreted by the
+            # server as an indication that the template file is non-existent.
+            # Although the generic "Exception" is less precise, it avoids
+            # triggering a potentially-confusing HTTP 404 error in cases where
+            # the path to the file to be hashed is invalid.
+            raise Exception('Cannot open file for hash computation: "%s"' % absolute_path)
+
+        return hash_obj.digest().encode('base64').strip()
+
 def template(request, content, escape_type="html"):
     #TODO: There basically isn't any error handling here
     tokenizer = ReplacementTokenizer()
 
     variables = {}
 
     def config_replacement(match):
         content, = match.groups()
@@ -377,22 +422,25 @@ def template(request, content, escape_ty
         tokens = tokenizer.tokenize(content)
 
         if tokens[0][0] == "var":
             variable = tokens[0][1]
             tokens = tokens[1:]
         else:
             variable = None
 
-        assert tokens[0][0] == "ident" and all(item[0] == "index" for item in tokens[1:]), tokens
+        assert tokens[0][0] == "ident", tokens
+        assert all(item[0] in ("index", "arguments") for item in tokens[1:]), tokens
 
         field = tokens[0][1]
 
         if field in variables:
             value = variables[field]
+        elif hasattr(SubFunctions, field):
+            value = getattr(SubFunctions, field)
         elif field == "headers":
             value = request.headers
         elif field == "GET":
             value = FirstWrapper(request.GET)
         elif field == "domains":
             if ('not_domains' in request.server.config and
                     tokens[1][1] in request.server.config['not_domains']):
                 value = request.server.config['not_domains']
@@ -409,25 +457,26 @@ def template(request, content, escape_ty
                      "scheme": request.url_parts.scheme,
                      "host": "%s:%s" % (request.url_parts.hostname,
                                         request.url_parts.port),
                      "hostname": request.url_parts.hostname,
                      "port": request.url_parts.port,
                      "path": request.url_parts.path,
                      "pathname": request.url_parts.path,
                      "query": "?%s" % request.url_parts.query}
-        elif field == "uuid()":
-            value = str(uuid.uuid4())
         elif field == "url_base":
             value = request.url_base
         else:
             raise Exception("Undefined template variable %s" % field)
 
         for item in tokens[1:]:
-            value = value[item[1]]
+            if item[0] == "index":
+                value = value[item[1]]
+            else:
+                value = value(request, *item[1])
 
         assert isinstance(value, (int,) + types.StringTypes), tokens
 
         if variable is not None:
             variables[variable] = value
 
         escape_func = {"html": lambda x:escape(x, quote=True),
                        "none": lambda x:x}[escape_type]