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 472380 ab11deb7d6b33f8dde1f15816b2177e2100285c1
parent 472379 51cffe1745dd12db96f0be639f21ca8a04353505
child 472381 fd611937fde85767f9b5a8ffb903807bbdbcde48
push id1728
push userjlund@mozilla.com
push dateMon, 18 Jun 2018 21:12:27 +0000
treeherdermozilla-release@c296fde26f5f [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewerstestonly
bugs1454134
milestone61.0a1
first release with
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
last release without
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
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]