Repository: tornadoweb/tornado
Pull request #1432 (2015-04-24 14:50:41)

Modify xhtml_unescape to handle hex numeric refs
While the existing code handles references like &#32; properly,
it doesn't handle the valid case where the number is specified
in hex. For example 'foo&#32;bar', 'foo&#x20;bar' and 'foo&#X20;bar'
should all decode to 'foo bar'. The changes in escape.py check
for the 'x' and decode appropriately. Also added unit tests for
unescaping strings with numeric references.


Patch:
diff --git a/docs/releases/next.rst b/docs/releases/next.rst
index 313006e6..821d8bd9 100644
--- a/docs/releases/next.rst
+++ b/docs/releases/next.rst
@@ -46,11 +46,9 @@ Toro's ``Semaphore.wait`` allowed a coroutine to wait for the semaphore to
 be unlocked *without* acquiring it. This encouraged unorthodox patterns; in
 Tornado, just use `~.Semaphore.acquire`.
 
-Toro's ``Event.wait`` raised a ``Timeout`` exception after a timeout. In
-Tornado, `.Event.wait` raises `tornado.gen.TimeoutError`.
-
-Toro's ``Condition.wait`` also raised ``Timeout``, but in Tornado, the `.Future`
-returned by `.Condition.wait` resolves to False after a timeout::
+Toro's ``Condition.wait`` raised a ``Timeout`` exception after a timeout. But in
+Tornado, the `.Future` returned by `.Condition.wait` resolves to False after a
+timeout::
 
     @gen.coroutine
     def await_notification():
@@ -59,6 +57,7 @@ returned by `.Condition.wait` resolves to False after a timeout::
         else:
             print('condition is true')
 
+
 In lock and queue methods, wherever Toro accepted ``deadline`` as a keyword
 argument, Tornado names the argument ``timeout`` instead.
 
diff --git a/tornado/escape.py b/tornado/escape.py
index 2852cf51..2f04b468 100644
--- a/tornado/escape.py
+++ b/tornado/escape.py
@@ -378,7 +378,10 @@ def linkify(text, shorten=False, extra_params="",
 def _convert_entity(m):
     if m.group(1) == "#":
         try:
-            return unichr(int(m.group(2)))
+            if m.group(2)[:1].lower() == 'x':
+                return unichr(int(m.group(2)[1:], 16))
+            else:
+                return unichr(int(m.group(2)))
         except ValueError:
             return "&#%s;" % m.group(2)
     try:
diff --git a/tornado/gen.py b/tornado/gen.py
index 4cc578eb..a6e9c2dd 100644
--- a/tornado/gen.py
+++ b/tornado/gen.py
@@ -305,7 +305,7 @@ class WaitIterator(object):
           except Exception as e:
               print("Error {} from {}".format(e, wait_iterator.current_future))
           else:
-              print("Result {} received from {} at {}".format(
+              print("Result {} recieved from {} at {}".format(
                   result, wait_iterator.current_future,
                   wait_iterator.current_index))
 


Test patch:
diff --git a/tornado/test/escape_test.py b/tornado/test/escape_test.py
index 98a23463..65765b68 100644
--- a/tornado/test/escape_test.py
+++ b/tornado/test/escape_test.py
@@ -154,6 +154,19 @@ class EscapeTestCase(unittest.TestCase):
             self.assertEqual(utf8(xhtml_escape(unescaped)), utf8(escaped))
             self.assertEqual(utf8(unescaped), utf8(xhtml_unescape(escaped)))
 
+    def test_xhtml_unescape_numeric(self):
+        tests = [
+            ('foo&#32;bar', 'foo bar'),
+            ('foo&#x20;bar', 'foo bar'),
+            ('foo&#X20;bar', 'foo bar'),
+            ('foo&#xabc;bar', u('foo\u0abcbar')),
+            ('foo&#xyz;bar', 'foo&#xyz;bar'),  # invalid encoding
+            ('foo&#;bar', 'foo&#;bar'),        # invalid encoding
+            ('foo&#x;bar', 'foo&#x;bar'),      # invalid encoding
+        ]
+        for escaped, unescaped in tests:
+            self.assertEqual(unescaped, xhtml_unescape(escaped))
+
     def test_url_escape_unicode(self):
         tests = [
             # byte strings are passed through as-is