Improve dict validation (#140)

* Refactor dict validation using regular expression for simplification and improvement.
After the change, the validation will:

 - Disallow `"\"`, `""`, and any non-space suffix after the entry ends (e.g. `"entry"suffix`).
 - Disallow unesacped '"' in enties. Raw '"' can indeed be used in libFuzzer due to implementation details,
   but it is undocumented (https://llvm.org/docs/LibFuzzer.html#dictionaries), implementation-dependent, and confusing.

* Fix a bad entry in example dictionaries.

* Show file paths and line numbers with errors.
diff --git a/examples/dictionaries/valid_part1.dict b/examples/dictionaries/valid_part1.dict
index 913cbad..29b6d96 100644
--- a/examples/dictionaries/valid_part1.dict
+++ b/examples/dictionaries/valid_part1.dict
@@ -6,7 +6,7 @@
 # Use \\ for backslash and \" for quotes.
 kw2="\"ac\\dc\""
 # Use \xAB for hex values
-kw3="\xF7\xF8""
+kw3="\xF7\xF8"
 # the name of the keyword followed by '=' may be omitted:
 "foo\x0Abar"
-"ab\""
\ No newline at end of file
+"ab\""
diff --git a/fuzzing/tools/dict_validation.py b/fuzzing/tools/dict_validation.py
index 5a169e1..db3c89b 100644
--- a/fuzzing/tools/dict_validation.py
+++ b/fuzzing/tools/dict_validation.py
@@ -17,47 +17,22 @@
 Validates the fuzzing dictionary.
 """
 
-from string import hexdigits
+import re
 
-
-def validate_entry(entry):
-    """Validates a single fuzzing dictionary entry.
-
-    Args:
-        entry: a string containing a single entry.
-
-    Returns:
-        True if the argument is a valid fuzzing dictionary entry, 
-        otherwise False.
-    """
-
-    # Use set to contain hex digits to decrease the query time complexity
-    hex_set = set(hexdigits)
-    pos, end = 0, len(entry) - 1
-    while pos < end:
-        pos += 1
-        chr = entry[pos]
-
-        if not (chr.isprintable() or chr.isspace()):
-            return False
-
-        # Handle '\\'
-        if chr == '\\':
-            if pos + 1 <= end and (entry[pos + 1] == '\\' or
-                                   entry[pos + 1] == '"'):
-                pos += 1
-                continue
-
-            # Handle '\xAB'
-            if pos + 3 <= end and entry[pos + 1] == 'x' and entry[
-                    pos + 2] in hex_set and entry[pos + 3] in hex_set:
-                pos += 3
-                continue
-
-            return False
-
-    return True
-
+_DICTIONARY_LINE_RE = re.compile(
+    r'''[^"]*  # Skip an arbitrary prefix (not used by libFuzzer).
+        "      # Must be enclosed in quotes.
+        (
+         [^\\\"]  # One or more non-escape characters...
+        |
+         \\(      # ...or an escape sequence...
+            [\\\"]  # ...consisting of either `\` or `"`...
+           |
+            x[0-9a-f]{2}  # ...or a hexa number, e.g. '\x0f'
+           )
+        )+
+        "''',
+    flags=re.IGNORECASE | re.VERBOSE)
 
 def validate_line(line):
     """Validates a single line in the fuzzing dictionary entry.
@@ -72,15 +47,5 @@
     line = line.strip()
     if not line or line.startswith('#'):
         return True
-    if len(line) < 2 or line[-1] != '"':
-        return False
-
-    left = 0
-    # Find the opening "
-    while left < len(line) - 1 and line[left] != '"':
-        left += 1
-
-    if left >= len(line) - 1:
-        return False
-
-    return validate_entry(line[left:])
+    else:
+        return re.fullmatch(_DICTIONARY_LINE_RE, line) is not None
diff --git a/fuzzing/tools/dict_validation_test.py b/fuzzing/tools/dict_validation_test.py
index 47eb7f4..078f8c5 100644
--- a/fuzzing/tools/dict_validation_test.py
+++ b/fuzzing/tools/dict_validation_test.py
@@ -28,7 +28,6 @@
         self.assertTrue(validate_line('":path"'))
         self.assertTrue(validate_line('"keep-alive"'))
         self.assertTrue(validate_line('"te"'))
-        self.assertTrue(validate_line('"ab""'))
 
     def test_escaped_words(self):
         self.assertTrue(validate_line('kw2="\\"ac\\\\dc\\""'))
@@ -38,9 +37,23 @@
     def test_invalid_escaped_words(self):
         self.assertFalse(validate_line('"\\A"'))
 
+    def test_unfinished_escape(self):
+        self.assertFalse(validate_line('"\\"'))
+        self.assertFalse(validate_line('"\\x"'))
+        self.assertFalse(validate_line('"\\x1"'))
+
+    def test_invalid_unescaped_words(self):
+        self.assertFalse(validate_line('"""'))
+
     def test_comment(self):
         self.assertTrue(validate_line('# valid dictionary entries'))
 
+    def test_suffix_after_entry(self):
+        self.assertFalse(validate_line('"entry"suffix'))
+
+    def test_empty_entry(self):
+        self.assertFalse(validate_line('""'))
+
     def test_empty_string(self):
         self.assertTrue(validate_line(''))
 
diff --git a/fuzzing/tools/validate_dict.py b/fuzzing/tools/validate_dict.py
index 28f9065..d561e68 100644
--- a/fuzzing/tools/validate_dict.py
+++ b/fuzzing/tools/validate_dict.py
@@ -33,10 +33,11 @@
 
 def validate_dict(dict_path, output_stream):
     with open(dict_path, 'r') as dict:
-        for line in dict.readlines():
+        for index, line in enumerate(dict.readlines()):
             line = line.strip()
             if not validate_line(line):
-                print("ERROR: invalid dictionary entry '%s'" % line,
+                print("ERROR: invalid dictionary entry '%s' in %s:%d" %
+                      (line, dict_path, index + 1),
                       file=stderr)
                 return False
             if output_stream: