Bug 229827: escape unprintable characters in CSS parser diagnostics. r=dbaron
authorZack Weinberg <zackw@panix.com>
Fri, 16 Nov 2012 21:53:38 -0500
changeset 113597 b11550b854e8a42046248d6301766e5dc5e8e703
parent 113596 1cb8097025bf59bf68f8860d7dbee2f1a30a8089
child 113598 5078cf4f60a31fa50745d8506a104908fe284524
push id23875
push userryanvm@gmail.com
push dateSat, 17 Nov 2012 13:04:27 +0000
treeherdermozilla-central@5242359612d2 [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersdbaron
bugs229827
milestone19.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 229827: escape unprintable characters in CSS parser diagnostics. r=dbaron
layout/style/ErrorReporter.cpp
layout/style/nsCSSScanner.cpp
layout/style/nsRuleNode.cpp
layout/style/nsStyleUtil.cpp
layout/style/nsStyleUtil.h
layout/style/test/Makefile.in
layout/style/test/property_database.js
layout/style/test/test_parser_diagnostics_unprintables.html
--- a/layout/style/ErrorReporter.cpp
+++ b/layout/style/ErrorReporter.cpp
@@ -12,16 +12,17 @@
 #include "nsCSSScanner.h"
 #include "nsCSSStyleSheet.h"
 #include "nsIConsoleService.h"
 #include "nsIDocument.h"
 #include "nsIFactory.h"
 #include "nsIScriptError.h"
 #include "nsIServiceManager.h"
 #include "nsIStringBundle.h"
+#include "nsStyleUtil.h"
 #include "nsThreadUtils.h"
 
 #ifdef CSS_REPORT_PARSE_ERRORS
 
 using mozilla::Preferences;
 namespace services = mozilla::services;
 
 namespace {
@@ -247,17 +248,20 @@ ErrorReporter::ReportUnexpected(const ch
 }
 
 void
 ErrorReporter::ReportUnexpected(const char *aMessage,
                                 const nsString &aParam)
 {
   if (!ShouldReportErrors()) return;
 
-  const PRUnichar *params[1] = { aParam.get() };
+  nsAutoString qparam;
+  nsStyleUtil::AppendEscapedCSSIdent(aParam, qparam);
+  const PRUnichar *params[1] = { qparam.get() };
+
   nsAutoString str;
   sStringBundle->FormatStringFromName(NS_ConvertASCIItoUTF16(aMessage).get(),
                                       params, ArrayLength(params),
                                       getter_Copies(str));
   AddToError(str);
 }
 
 void
--- a/layout/style/nsCSSScanner.cpp
+++ b/layout/style/nsCSSScanner.cpp
@@ -4,16 +4,17 @@
  * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
 
 
 /* tokenization of CSS style sheets */
 
 #include <math.h> // must be first due to symbol conflicts
 
 #include "nsCSSScanner.h"
+#include "nsStyleUtil.h"
 #include "mozilla/css/ErrorReporter.h"
 #include "mozilla/Likely.h"
 #include "mozilla/Util.h"
 
 using mozilla::ArrayLength;
 
 static const uint8_t IS_HEX_DIGIT  = 0x01;
 static const uint8_t START_IDENT   = 0x02;
@@ -139,93 +140,111 @@ nsCSSToken::nsCSSToken()
 {
   mType = eCSSToken_Symbol;
 }
 
 void
 nsCSSToken::AppendToString(nsString& aBuffer) const
 {
   switch (mType) {
+    case eCSSToken_Ident:
+      nsStyleUtil::AppendEscapedCSSIdent(mIdent, aBuffer);
+      break;
+
     case eCSSToken_AtKeyword:
-      aBuffer.Append(PRUnichar('@')); // fall through intentional
-    case eCSSToken_Ident:
-    case eCSSToken_WhiteSpace:
+      aBuffer.Append('@');
+      nsStyleUtil::AppendEscapedCSSIdent(mIdent, aBuffer);
+      break;
+
+    case eCSSToken_ID:
+    case eCSSToken_Ref:
+      aBuffer.Append('#');
+      nsStyleUtil::AppendEscapedCSSIdent(mIdent, aBuffer);
+      break;
+
     case eCSSToken_Function:
-    case eCSSToken_HTMLComment:
-    case eCSSToken_URange:
-      aBuffer.Append(mIdent);
-      if (mType == eCSSToken_Function)
-        aBuffer.Append(PRUnichar('('));
+      nsStyleUtil::AppendEscapedCSSIdent(mIdent, aBuffer);
+      aBuffer.Append('(');
       break;
+
     case eCSSToken_URL:
     case eCSSToken_Bad_URL:
       aBuffer.AppendLiteral("url(");
       if (mSymbol != PRUnichar(0)) {
-        aBuffer.Append(mSymbol);
-      }
-      aBuffer.Append(mIdent);
-      if (mSymbol != PRUnichar(0)) {
-        aBuffer.Append(mSymbol);
+        nsStyleUtil::AppendEscapedCSSString(mIdent, aBuffer, mSymbol);
+      } else {
+        aBuffer.Append(mIdent);
       }
       if (mType == eCSSToken_URL) {
         aBuffer.Append(PRUnichar(')'));
       }
       break;
+
     case eCSSToken_Number:
       if (mIntegerValid) {
         aBuffer.AppendInt(mInteger, 10);
-      }
-      else {
+      } else {
         aBuffer.AppendFloat(mNumber);
       }
       break;
+
     case eCSSToken_Percentage:
       NS_ASSERTION(!mIntegerValid, "How did a percentage token get this set?");
       aBuffer.AppendFloat(mNumber * 100.0f);
       aBuffer.Append(PRUnichar('%'));
       break;
+
     case eCSSToken_Dimension:
       if (mIntegerValid) {
         aBuffer.AppendInt(mInteger, 10);
-      }
-      else {
+      } else {
         aBuffer.AppendFloat(mNumber);
       }
-      aBuffer.Append(mIdent);
+      nsStyleUtil::AppendEscapedCSSIdent(mIdent, aBuffer);
       break;
+
+    case eCSSToken_Bad_String:
+      nsStyleUtil::AppendEscapedCSSString(mIdent, aBuffer, mSymbol);
+      // remove the trailing quote character
+      aBuffer.Truncate(aBuffer.Length() - 1);
+      break;
+
     case eCSSToken_String:
-      aBuffer.Append(mSymbol);
-      aBuffer.Append(mIdent); // fall through intentional
+      nsStyleUtil::AppendEscapedCSSString(mIdent, aBuffer, mSymbol);
+      break;
+
     case eCSSToken_Symbol:
       aBuffer.Append(mSymbol);
       break;
-    case eCSSToken_ID:
-    case eCSSToken_Ref:
-      aBuffer.Append(PRUnichar('#'));
+
+    case eCSSToken_WhiteSpace:
+      aBuffer.Append(' ');
+      break;
+
+    case eCSSToken_HTMLComment:
+    case eCSSToken_URange:
       aBuffer.Append(mIdent);
       break;
+
     case eCSSToken_Includes:
       aBuffer.AppendLiteral("~=");
       break;
     case eCSSToken_Dashmatch:
       aBuffer.AppendLiteral("|=");
       break;
     case eCSSToken_Beginsmatch:
       aBuffer.AppendLiteral("^=");
       break;
     case eCSSToken_Endsmatch:
       aBuffer.AppendLiteral("$=");
       break;
     case eCSSToken_Containsmatch:
       aBuffer.AppendLiteral("*=");
       break;
-    case eCSSToken_Bad_String:
-      aBuffer.Append(mSymbol);
-      aBuffer.Append(mIdent);
-      break;
+
     default:
       NS_ERROR("invalid token type");
       break;
   }
 }
 
 nsCSSScanner::nsCSSScanner(const nsAString& aBuffer, uint32_t aLineNumber)
   : mReadPointer(aBuffer.BeginReading())
--- a/layout/style/nsRuleNode.cpp
+++ b/layout/style/nsRuleNode.cpp
@@ -367,18 +367,19 @@ static nscoord CalcLengthWith(const nsCS
     aStyleFont ? aStyleFont : aStyleContext->GetStyleFont();
   if (aFontSize == -1) {
     // XXX Should this be styleFont->mSize instead to avoid taking minfontsize
     // prefs into account?
     aFontSize = styleFont->mFont.size;
   }
   switch (aValue.GetUnit()) {
     case eCSSUnit_EM: {
+      // CSS2.1 specifies that this unit scales to the computed font
+      // size, not the em-width in the font metrics, despite the name.
       return ScaleCoord(aValue, float(aFontSize));
-      // XXX scale against font metrics height instead?
     }
     case eCSSUnit_XHeight: {
       nsRefPtr<nsFontMetrics> fm =
         GetMetricsFor(aPresContext, aStyleContext, styleFont,
                       aFontSize, aUseUserFontSet);
       return ScaleCoord(aValue, float(fm->XHeight()));
     }
     case eCSSUnit_Char: {
--- a/layout/style/nsStyleUtil.cpp
+++ b/layout/style/nsStyleUtil.cpp
@@ -50,103 +50,99 @@ bool nsStyleUtil::DashMatchCompare(const
     }
     else {
       result = StringBeginsWith(aAttributeValue, aSelectorValue, aComparator);
     }
   }
   return result;
 }
 
-void nsStyleUtil::AppendEscapedCSSString(const nsString& aString,
-                                         nsAString& aReturn)
+void nsStyleUtil::AppendEscapedCSSString(const nsAString& aString,
+                                         nsAString& aReturn,
+                                         PRUnichar quoteChar)
 {
-  aReturn.Append(PRUnichar('"'));
+  NS_PRECONDITION(quoteChar == '\'' || quoteChar == '"',
+                  "CSS strings must be quoted with ' or \"");
+  aReturn.Append(quoteChar);
 
-  const nsString::char_type* in = aString.get();
-  const nsString::char_type* const end = in + aString.Length();
-  for (; in != end; in++)
-  {
-    if (*in < 0x20)
-    {
-     // Escape all characters below 0x20 numerically.
-   
-     /*
-      This is the buffer into which snprintf should write. As the hex. value is,
-      for numbers below 0x20, max. 2 characters long, we don't need more than 5
-      characters ("\XX "+NUL).
-     */
-     PRUnichar buf[5];
-     nsTextFormatter::snprintf(buf, ArrayLength(buf), NS_LITERAL_STRING("\\%hX ").get(), *in);
-     aReturn.Append(buf);
-   
-    } else switch (*in) {
-      // Special characters which should be escaped: Quotes and backslash
-      case '\\':
-      case '\"':
-      case '\'':
-       aReturn.Append(PRUnichar('\\'));
-      // And now, after the eventual escaping character, the actual one.
-      default:
-       aReturn.Append(PRUnichar(*in));
+  const PRUnichar* in = aString.BeginReading();
+  const PRUnichar* const end = aString.EndReading();
+  for (; in != end; in++) {
+    if (*in < 0x20 || (*in >= 0x7F && *in < 0xA0)) {
+      // Escape U+0000 through U+001F and U+007F through U+009F numerically.
+      aReturn.AppendPrintf("\\%hX ", *in);
+    } else {
+      if (*in == '"' || *in == '\'' || *in == '\\') {
+        // Escape backslash and quote characters symbolically.
+        // It's not technically necessary to escape the quote
+        // character that isn't being used to delimit the string,
+        // but we do it anyway because that makes testing simpler.
+        aReturn.Append(PRUnichar('\\'));
+      }
+      aReturn.Append(*in);
     }
   }
 
-  aReturn.Append(PRUnichar('"'));
+  aReturn.Append(quoteChar);
 }
 
 /* static */ void
-nsStyleUtil::AppendEscapedCSSIdent(const nsString& aIdent, nsAString& aReturn)
+nsStyleUtil::AppendEscapedCSSIdent(const nsAString& aIdent, nsAString& aReturn)
 {
   // The relevant parts of the CSS grammar are:
   //   ident    [-]?{nmstart}{nmchar}*
   //   nmstart  [_a-z]|{nonascii}|{escape}
   //   nmchar   [_a-z0-9-]|{nonascii}|{escape}
   //   nonascii [^\0-\177]
   //   escape   {unicode}|\\[^\n\r\f0-9a-f]
   //   unicode  \\[0-9a-f]{1,6}(\r\n|[ \n\r\t\f])?
   // from http://www.w3.org/TR/CSS21/syndata.html#tokenization
 
-  const nsString::char_type* in = aIdent.get();
-  const nsString::char_type* const end = in + aIdent.Length();
+  const PRUnichar* in = aIdent.BeginReading();
+  const PRUnichar* const end = aIdent.EndReading();
 
-  // Deal with the leading dash separately so we don't need to
-  // unnecessarily escape digits.
-  if (in != end && *in == '-') {
+  if (in == end)
+    return;
+
+  // A leading dash does not need to be escaped as long as it is not the
+  // *only* character in the identifier.
+  if (in + 1 != end && *in == '-') {
     aReturn.Append(PRUnichar('-'));
     ++in;
   }
 
-  bool first = true;
-  for (; in != end; ++in, first = false)
-  {
-    if (*in < 0x20 || (first && '0' <= *in && *in <= '9'))
-    {
-      // Escape all characters below 0x20, and digits at the start
-      // (including after a dash), numerically.  If we didn't escape
-      // digits numerically, they'd get interpreted as a numeric escape
-      // for the wrong character.
+  // Escape a digit at the start (including after a dash),
+  // numerically.  If we didn't escape it numerically, it would get
+  // interpreted as a numeric escape for the wrong character.
+  // A second dash immediately after a leading dash must also be
+  // escaped, but this may be done symbolically.
+  if (in != end && (*in == '-' ||
+                    ('0' <= *in && *in <= '9'))) {
+    if (*in == '-') {
+      aReturn.Append(PRUnichar('\\'));
+      aReturn.Append(PRUnichar('-'));
+    } else {
+      aReturn.AppendPrintf("\\%hX ", *in);
+    }
+    ++in;
+  }
 
-      /*
-       This is the buffer into which snprintf should write. As the hex.
-       value is, for numbers below 0x7F, max. 2 characters long, we
-       don't need more than 5 characters ("\XX "+NUL).
-      */
-      PRUnichar buf[5];
-      nsTextFormatter::snprintf(buf, ArrayLength(buf),
-                                NS_LITERAL_STRING("\\%hX ").get(), *in);
-      aReturn.Append(buf);
+  for (; in != end; ++in) {
+    PRUnichar ch = *in;
+    if (ch < 0x20 || (0x7F <= ch && ch < 0xA0)) {
+      // Escape U+0000 through U+001F and U+007F through U+009F numerically.
+      aReturn.AppendPrintf("\\%hX ", *in);
     } else {
-      PRUnichar ch = *in;
-      if (!((ch == PRUnichar('_')) ||
-            (PRUnichar('A') <= ch && ch <= PRUnichar('Z')) ||
-            (PRUnichar('a') <= ch && ch <= PRUnichar('z')) ||
-            PRUnichar(0x80) <= ch ||
-            (!first && ch == PRUnichar('-')) ||
-            (PRUnichar('0') <= ch && ch <= PRUnichar('9')))) {
-        // Character needs to be escaped
+      // Escape ASCII non-identifier printables as a backslash plus
+      // the character.
+      if (ch < 0x7F &&
+          ch != '_' && ch != '-' &&
+          (ch < '0' || '9' < ch) &&
+          (ch < 'A' || 'Z' < ch) &&
+          (ch < 'a' || 'z' < ch)) {
         aReturn.Append(PRUnichar('\\'));
       }
       aReturn.Append(ch);
     }
   }
 }
 
 /* static */ void
--- a/layout/style/nsStyleUtil.h
+++ b/layout/style/nsStyleUtil.h
@@ -20,24 +20,27 @@ class nsIContent;
 
 // Style utility functions
 class nsStyleUtil {
 public:
 
  static bool DashMatchCompare(const nsAString& aAttributeValue,
                                 const nsAString& aSelectorValue,
                                 const nsStringComparator& aComparator);
-                                
-  // Append a quoted (with "") and escaped version of aString to aResult.
-  static void AppendEscapedCSSString(const nsString& aString,
-                                     nsAString& aResult);
+
+  // Append a quoted (with 'quoteChar') and escaped version of aString
+  // to aResult.  'quoteChar' must be ' or ".
+  static void AppendEscapedCSSString(const nsAString& aString,
+                                     nsAString& aResult,
+                                     PRUnichar quoteChar = '"');
+
   // Append the identifier given by |aIdent| to |aResult|, with
   // appropriate escaping so that it can be reparsed to the same
   // identifier.
-  static void AppendEscapedCSSIdent(const nsString& aIdent,
+  static void AppendEscapedCSSIdent(const nsAString& aIdent,
                                     nsAString& aResult);
 
   // Append a bitmask-valued property's value(s) (space-separated) to aResult.
   static void AppendBitmaskCSSValue(nsCSSProperty aProperty,
                                     int32_t aMaskedValue,
                                     int32_t aFirstMask,
                                     int32_t aLastMask,
                                     nsAString& aResult);
--- a/layout/style/test/Makefile.in
+++ b/layout/style/test/Makefile.in
@@ -113,16 +113,17 @@ MOCHITEST_FILES =	test_acid3_test46.html
 		test_media_queries_dynamic_xbl.html \
 		test_media_query_list.html \
 		test_moz_device_pixel_ratio.html \
 		test_namespace_rule.html \
 		test_of_type_selectors.xhtml \
 		test_parse_ident.html \
 		test_parse_rule.html \
 		test_parse_url.html \
+		test_parser_diagnostics_unprintables.html \
 		test_pixel_lengths.html \
 		test_pointer-events.html \
 		test_property_database.html \
 		test_priority_preservation.html \
 		test_property_syntax_errors.html \
 		test_rem_unit.html \
 		test_rule_serialization.html \
 		test_rules_out_of_sheets.html \
--- a/layout/style/test/property_database.js
+++ b/layout/style/test/property_database.js
@@ -2310,25 +2310,25 @@ var gCSSProperties = {
 		other_values: [ '""', "''", '"hello"', "url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAACAAAAAgCAIAAAD8GO2jAAAAKElEQVR42u3NQQ0AAAgEoNP+nTWFDzcoQE1udQQCgUAgEAgEAsGTYAGjxAE/G/Q2tQAAAABJRU5ErkJggg==)", "url('data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAACAAAAAgCAIAAAD8GO2jAAAAKElEQVR42u3NQQ0AAAgEoNP+nTWFDzcoQE1udQQCgUAgEAgEAsGTYAGjxAE/G/Q2tQAAAABJRU5ErkJggg==')", 'url("data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAACAAAAAgCAIAAAD8GO2jAAAAKElEQVR42u3NQQ0AAAgEoNP+nTWFDzcoQE1udQQCgUAgEAgEAsGTYAGjxAE/G/Q2tQAAAABJRU5ErkJggg==")', 'counter(foo)', 'counter(bar, upper-roman)', 'counters(foo, ".")', "counters(bar, '-', lower-greek)", "'-' counter(foo) '.'", "attr(title)", "open-quote", "close-quote", "no-open-quote", "no-close-quote", "close-quote attr(title) counters(foo, '.', upper-alpha)", "counter(foo, none)", "counters(bar, '.', none)", "attr(\\32)", "attr(\\2)", "attr(-\\2)", "attr(-\\32)", "counter(\\2)", "counters(\\32, '.')", "counter(-\\32, upper-roman)", "counters(-\\2, '-', lower-greek)", "counter(\\()", "counters(a\\+b, '.')", "counter(\\}, upper-alpha)", "-moz-alt-content" ],
 		invalid_values: [ 'counters(foo)', 'counter(foo, ".")', 'attr("title")', "attr('title')", "attr(2)", "attr(-2)", "counter(2)", "counters(-2, '.')", "-moz-alt-content 'foo'", "'foo' -moz-alt-content" ]
 	},
 	"counter-increment": {
 		domProp: "counterIncrement",
 		inherited: false,
 		type: CSS_TYPE_LONGHAND,
 		initial_values: [ "none" ],
-		other_values: [ "foo 1", "bar", "foo 3 bar baz 2", "\\32  1", "-\\32  1", "-c 1", "\\32 1", "-\\32 1", "\\2  1", "-\\2  1", "-c 1", "\\2 1", "-\\2 1" ],
+		other_values: [ "foo 1", "bar", "foo 3 bar baz 2", "\\32  1", "-\\32  1", "-c 1", "\\32 1", "-\\32 1", "\\2  1", "-\\2  1", "-c 1", "\\2 1", "-\\2 1", "-\\7f \\9e 1" ],
 		invalid_values: []
 	},
 	"counter-reset": {
 		domProp: "counterReset",
 		inherited: false,
 		type: CSS_TYPE_LONGHAND,
 		initial_values: [ "none" ],
-		other_values: [ "foo 1", "bar", "foo 3 bar baz 2", "\\32  1", "-\\32  1", "-c 1", "\\32 1", "-\\32 1", "\\2  1", "-\\2  1", "-c 1", "\\2 1", "-\\2 1" ],
+		other_values: [ "foo 1", "bar", "foo 3 bar baz 2", "\\32  1", "-\\32  1", "-c 1", "\\32 1", "-\\32 1", "\\2  1", "-\\2  1", "-c 1", "\\2 1", "-\\2 1", "-\\7f \\9e 1" ],
 		invalid_values: []
 	},
 	"cursor": {
 		domProp: "cursor",
 		inherited: true,
 		type: CSS_TYPE_LONGHAND,
 		initial_values: [ "auto" ],
 		other_values: [ "crosshair", "default", "pointer", "move", "e-resize", "ne-resize", "nw-resize", "n-resize", "se-resize", "sw-resize", "s-resize", "w-resize", "text", "wait", "help", "progress", "copy", "alias", "context-menu", "cell", "not-allowed", "col-resize", "row-resize", "no-drop", "vertical-text", "all-scroll", "nesw-resize", "nwse-resize", "ns-resize", "ew-resize", "none", "-moz-grab", "-moz-grabbing", "-moz-zoom-in", "-moz-zoom-out", "url(foo.png), move", "url(foo.png) 5 7, move", "url(foo.png) 12 3, url(bar.png), no-drop", "url(foo.png), url(bar.png) 7 2, wait", "url(foo.png) 3 2, url(bar.png) 7 9, pointer" ],
new file mode 100644
--- /dev/null
+++ b/layout/style/test/test_parser_diagnostics_unprintables.html
@@ -0,0 +1,219 @@
+<!doctype html>
+<html>
+<head>
+  <meta charset="utf-8">
+  <title>Test for CSS parser diagnostics escaping unprintable
+         characters correctly</title>
+  <script src="/tests/SimpleTest/SimpleTest.js"></script>
+  <link rel="stylesheet" href="/tests/SimpleTest/test.css">
+</head>
+<body>
+<a target="_blank"
+   href="https://bugzilla.mozilla.org/show_bug.cgi?id=229827"
+>Mozilla Bug 229827</a>
+<style id="testbench"></style>
+<script type="application/javascript;version=1.8">
+// This test has intimate knowledge of how to get the CSS parser to
+// emit diagnostics that contain text under control of the user.
+// That's not the point of the test, though; the point is only that
+// *that text* is properly escaped.
+
+// There is one "pattern" for each code path through the error reporter
+// that might need to escape some kind of user-supplied text.
+// Each "pattern" is tested once with each of the "substitution"s below:
+// <t>, <i>, and <s> are replaced by the t:, i:, and s: fields of
+// each substitution object in turn.
+const patterns = [
+  // REPORT_UNEXPECTED_P (only ever used in contexts where identifier-like
+  // escaping is appropriate)
+  { i: "<t>|x{}",                 o: "prefix '<i>'" },
+  // REPORT_UNEXPECTED_TOKEN with:
+  // _Ident
+  { i: "@namespace fnord <t>;",    o: "within @namespace: '<i>'" },
+  // _Ref
+  { i: "@namespace fnord #<t>;",   o: "within @namespace: '#<i>'" },
+  // _Function
+  { i: "@namespace fnord <t>();",  o: "within @namespace: '<i>('" },
+  // _Dimension
+  { i: "@namespace fnord 14<t>;",  o: "within @namespace: '14<i>'" },
+  // _AtKeyword
+  { i: "x{@<t>: }",        o: "declaration but found '@<i>'." },
+  // _String
+  { i: "x{ '<t>'}" ,       o: "declaration but found ''<s>''." },
+  // _Bad_String
+  { i: "x{ '<t>\n}",      o: "declaration but found ''<s>'." },
+  // _URL
+  { i: "x{ url('<t>')}",   o: "declaration but found 'url('<s>')'." },
+  // _Bad_URL
+  { i: "x{ url('<t>'.)}" , o: "declaration but found 'url('<s>''." }
+];
+
+// Blocks of characters to test, and how they should be escaped when
+// they appear in identifiers and string constants.
+const substitutions = [
+  // ASCII printables that _can_ normally appear in identifiers,
+  // so should of course _not_ be escaped.
+  { t: "-_0123456789",               i: "-_0123456789",
+                                     s: "-_0123456789" },
+  { t: "abcdefghijklmnopqrstuvwxyz", i: "abcdefghijklmnopqrstuvwxyz",
+                                     s: "abcdefghijklmnopqrstuvwxyz" },
+  { t: "ABCDEFGHIJKLMNOPQRSTUVWXYZ", i: "ABCDEFGHIJKLMNOPQRSTUVWXYZ",
+                                     s: "ABCDEFGHIJKLMNOPQRSTUVWXYZ" },
+
+  // ASCII printables that are not normally valid as the first character
+  // of an identifier, or the character immediately after a leading dash,
+  // but can be forced into that position with escapes.
+  { t: "\\-",    i: "\\-",    s: "-"  },
+  { t: "\\30 ",  i: "\\30 ",  s: "0"  },
+  { t: "\\31 ",  i: "\\31 ",  s: "1"  },
+  { t: "\\32 ",  i: "\\32 ",  s: "2"  },
+  { t: "\\33 ",  i: "\\33 ",  s: "3"  },
+  { t: "\\34 ",  i: "\\34 ",  s: "4"  },
+  { t: "\\35 ",  i: "\\35 ",  s: "5"  },
+  { t: "\\36 ",  i: "\\36 ",  s: "6"  },
+  { t: "\\37 ",  i: "\\37 ",  s: "7"  },
+  { t: "\\38 ",  i: "\\38 ",  s: "8"  },
+  { t: "\\39 ",  i: "\\39 ",  s: "9"  },
+  { t: "-\\-",   i: "-\\-",   s: "--" },
+  { t: "-\\30 ", i: "-\\30 ", s: "-0" },
+  { t: "-\\31 ", i: "-\\31 ", s: "-1" },
+  { t: "-\\32 ", i: "-\\32 ", s: "-2" },
+  { t: "-\\33 ", i: "-\\33 ", s: "-3" },
+  { t: "-\\34 ", i: "-\\34 ", s: "-4" },
+  { t: "-\\35 ", i: "-\\35 ", s: "-5" },
+  { t: "-\\36 ", i: "-\\36 ", s: "-6" },
+  { t: "-\\37 ", i: "-\\37 ", s: "-7" },
+  { t: "-\\38 ", i: "-\\38 ", s: "-8" },
+  { t: "-\\39 ", i: "-\\39 ", s: "-9" },
+
+  // ASCII printables that must be escaped in identifiers.
+  // Most of these should not be escaped in strings.
+  { t: "\\!\\\"\\#\\$",   i: "\\!\\\"\\#\\$",   s: "!\\\"#$" },
+  { t: "\\%\\&\\'\\(",    i: "\\%\\&\\'\\(",    s: "%&\\'(" },
+  { t: "\\)\\*\\+\\,",    i: "\\)\\*\\+\\,",    s: ")*+," },
+  { t: "\\.\\/\\:\\;",    i: "\\.\\/\\:\\;",    s: "./:;" },
+  { t: "\\<\\=\\>\\?",    i: "\\<\\=\\>\\?",    s: "<=>?", },
+  { t: "\\@\\[\\\\\\]",   i: "\\@\\[\\\\\\]",   s: "@[\\\\]" },
+  { t: "\\^\\`\\{\\}\\~", i: "\\^\\`\\{\\}\\~", s: "^`{}~" },
+
+  // U+0000 - U+0020 (C0 controls, space)
+  // U+000A LINE FEED, U+000C FORM FEED, and U+000D CARRIAGE RETURN
+  // cannot be put into a CSS token as escaped literal characters, so
+  // we do them with hex escapes instead.
+  { t: "\\\x00\\\x01\\\x02\\\x03",       i: "\\0 \\1 \\2 \\3 ",
+                                         s: "\\0 \\1 \\2 \\3 " },
+  { t: "\\\x04\\\x05\\\x06\\\x07",       i: "\\4 \\5 \\6 \\7 ",
+                                         s: "\\4 \\5 \\6 \\7 " },
+  { t: "\\\x08\\\x09\\000A\\\x0B",       i: "\\8 \\9 \\A \\B ",
+                                         s: "\\8 \\9 \\A \\B " },
+  { t: "\\000C\\000D\\\x0E\\\x0F",       i: "\\C \\D \\E \\F ",
+                                         s: "\\C \\D \\E \\F " },
+  { t: "\\\x10\\\x11\\\x12\\\x13",       i: "\\10 \\11 \\12 \\13 ",
+                                         s: "\\10 \\11 \\12 \\13 " },
+  { t: "\\\x14\\\x15\\\x16\\\x17",       i: "\\14 \\15 \\16 \\17 ",
+                                         s: "\\14 \\15 \\16 \\17 " },
+  { t: "\\\x18\\\x19\\\x1A\\\x1B",       i: "\\18 \\19 \\1A \\1B ",
+                                         s: "\\18 \\19 \\1A \\1B " },
+  { t: "\\\x1C\\\x1D\\\x1E\\\x1F\\ ",    i: "\\1C \\1D \\1E \\1F \\ ",
+                                         s: "\\1C \\1D \\1E \\1F  " },
+
+  // U+007F (DELETE) and U+0080 - U+009F (C1 controls)
+  { t: "\\\x7f\\\x80\\\x81\\\x82",       i: "\\7F \\80 \\81 \\82 ",
+                                         s: "\\7F \\80 \\81 \\82 " },
+  { t: "\\\x83\\\x84\\\x85\\\x86",       i: "\\83 \\84 \\85 \\86 ",
+                                         s: "\\83 \\84 \\85 \\86 " },
+  { t: "\\\x87\\\x88\\\x89\\\x8A",       i: "\\87 \\88 \\89 \\8A ",
+                                         s: "\\87 \\88 \\89 \\8A " },
+  { t: "\\\x8B\\\x8C\\\x8D\\\x8E",       i: "\\8B \\8C \\8D \\8E ",
+                                         s: "\\8B \\8C \\8D \\8E " },
+  { t: "\\\x8F\\\x90\\\x91\\\x92",       i: "\\8F \\90 \\91 \\92 ",
+                                         s: "\\8F \\90 \\91 \\92 " },
+  { t: "\\\x93\\\x94\\\x95\\\x96",       i: "\\93 \\94 \\95 \\96 ",
+                                         s: "\\93 \\94 \\95 \\96 " },
+  { t: "\\\x97\\\x98\\\x99\\\x9A",       i: "\\97 \\98 \\99 \\9A ",
+                                         s: "\\97 \\98 \\99 \\9A " },
+  { t: "\\\x9B\\\x9C\\\x9D\\\x9E\\\x9F", i: "\\9B \\9C \\9D \\9E \\9F ",
+                                         s: "\\9B \\9C \\9D \\9E \\9F " },
+
+  // CSS doesn't bother with the full Unicode rules for identifiers,
+  // instead declaring that any code point greater than or equal to
+  // U+00A0 is a valid identifier character.  Test a small handful
+  // of both basic and astral plane characters.
+
+  // Arabic (caution to editors: there is a possibly-invisible U+200E
+  // LEFT-TO-RIGHT MARK in each string, just before the close quote)
+  { t: "أبجدهوزحطيكلمنسعفصقرشتثخذضظغ‎",
+    i: "أبجدهوزحطيكلمنسعفصقرشتثخذضظغ‎",
+    s: "أبجدهوزحطيكلمنسعفصقرشتثخذضظغ‎" },
+
+  // Box drawing
+  { t: "─│┌┐└┘├┤┬┴┼╭╮╯╰╴╵╶╷",
+    i: "─│┌┐└┘├┤┬┴┼╭╮╯╰╴╵╶╷",
+    s: "─│┌┐└┘├┤┬┴┼╭╮╯╰╴╵╶╷" },
+
+  // CJK Unified Ideographs
+  { t: "一丁丂七丄丅丆万丈三上下丌不与丏",
+    i: "一丁丂七丄丅丆万丈三上下丌不与丏",
+    s: "一丁丂七丄丅丆万丈三上下丌不与丏" },
+
+  // CJK Unified Ideographs Extension B (astral)
+  { t: "𠀀𠀁𠀂𠀃𠀄𠀅𠀆𠀇𠀈𠀉𠀊𠀋𠀌𠀍𠀎𠀏",
+    i: "𠀀𠀁𠀂𠀃𠀄𠀅𠀆𠀇𠀈𠀉𠀊𠀋𠀌𠀍𠀎𠀏",
+    s: "𠀀𠀁𠀂𠀃𠀄𠀅𠀆𠀇𠀈𠀉𠀊𠀋𠀌𠀍𠀎𠀏" },
+
+  // Devanagari
+  { t: "कखगघङचछजझञटठडढणतथदधनपफबभमयरलळवशषसह",
+    i: "कखगघङचछजझञटठडढणतथदधनपफबभमयरलळवशषसह",
+    s: "कखगघङचछजझञटठडढणतथदधनपफबभमयरलळवशषसह" },
+
+  // Emoticons (astral)
+  { t: "😁😂😃😄😅😆😇😈😉😊😋😌😍😎😏😐",
+    i: "😁😂😃😄😅😆😇😈😉😊😋😌😍😎😏😐",
+    s: "😁😂😃😄😅😆😇😈😉😊😋😌😍😎😏😐" },
+
+  // Greek
+  { t: "αβγδεζηθικλμνξοπρςστυφχψω",
+    i: "αβγδεζηθικλμνξοπρςστυφχψω",
+    s: "αβγδεζηθικλμνξοπρςστυφχψω" }
+];
+
+const npatterns = patterns.length;
+const nsubstitutions = substitutions.length;
+
+function quotemeta(str) {
+  return str.replace(/[-[\]{}()*+?.,\\^$|#\s]/g, "\\$&");
+}
+function subst(str, sub) {
+  return str.replace("<t>", sub.t)
+    .replace("<i>", sub.i)
+    .replace("<s>", sub.s);
+}
+
+var curpat = 0;
+var cursubst = -1;
+var testbench = document.getElementById("testbench");
+
+function nextTest() {
+  cursubst++;
+  if (cursubst == nsubstitutions) {
+    curpat++;
+    cursubst = 0;
+  }
+  if (curpat == npatterns) {
+    SimpleTest.finish();
+    return;
+  }
+
+  let css = subst(patterns[curpat].i, substitutions[cursubst]);
+  let msg = quotemeta(subst(patterns[curpat].o, substitutions[cursubst]));
+
+  SimpleTest.expectConsoleMessages(function () { testbench.innerHTML = css },
+                                   [{ errorMessage: new RegExp(msg) }],
+                                   nextTest);
+}
+
+SimpleTest.waitForExplicitFinish();
+nextTest();
+</script>
+</body>
+</html>