Fixed errors due to extended ASCII and control characters in macros (#106)

diff --git a/CHANGELOG.md b/CHANGELOG.md
index 2ae8d46..a59311b 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,3 +1,6 @@
+# 1.0.3
+- Fixed errors due to extended ASCII and control characters in macro strings.
+
 # 1.0.2
 - Fix indentation for pub's readme.
 
diff --git a/lib/src/header_parser/sub_parsers/macro_parser.dart b/lib/src/header_parser/sub_parsers/macro_parser.dart
index 8884dd5..16cf4a1 100644
--- a/lib/src/header_parser/sub_parsers/macro_parser.dart
+++ b/lib/src/header_parser/sub_parsers/macro_parser.dart
@@ -4,6 +4,7 @@
 
 import 'dart:ffi';
 import 'dart:io';
+import 'dart:typed_data';
 
 import 'package:path/path.dart' as p;
 import 'package:ffi/ffi.dart';
@@ -131,17 +132,16 @@
           );
           break;
         case clang_types.CXEvalResultKind.CXEval_StrLiteral:
-          var value = Utf8.fromUtf8(clang.clang_EvalResult_getAsStr(e).cast());
-          // Escape $ character.
-          value = value.replaceAll(r'$', r'\$');
-          // Escape ' character, because our strings are enclosed with '.
-          value = value.replaceAll("'", r"\'");
+          final rawValue = _getWrittenRepresentation(
+            macroName,
+            clang.clang_EvalResult_getAsStr(e),
+          );
           constant = Constant(
             usr: savedMacros[macroName].usr,
             originalName: savedMacros[macroName].originalName,
             name: macroName,
             rawType: 'String',
-            rawValue: "'${value}'",
+            rawValue: "'${rawValue}'",
           );
           break;
       }
@@ -241,3 +241,84 @@
     return s.substring(nameStart, nameStart + len);
   }
 }
+
+/// Gets a written representation string of a C string.
+///
+/// E.g- For a string "Hello\nWorld", The new line character is converted to \n.
+/// Note: The string is considered to be Utf8, but is treated as Extended ASCII,
+/// if the conversion fails.
+String _getWrittenRepresentation(String macroName, Pointer<Int8> strPtr) {
+  final sb = StringBuffer();
+  try {
+    // Consider string to be Utf8 encoded by default.
+    sb.clear();
+    // This throws a Format Exception if string isn't Utf8 so that we handle it
+    // in the catch block.
+    final result = Utf8.fromUtf8(strPtr.cast());
+    for (final s in result.runes) {
+      sb.write(_getWritableChar(s));
+    }
+  } catch (e) {
+    // Handle string if it isn't Utf8. String is considered to be
+    // Extended ASCII in this case.
+    _logger.warning(
+        "Couldn't decode Macro string '$macroName' as Utf8, using ASCII instead.");
+    sb.clear();
+    final length = Utf8.strlen(strPtr.cast());
+    final charList = Uint8List.view(
+        strPtr.cast<Uint8>().asTypedList(length).buffer, 0, length);
+
+    for (final char in charList) {
+      sb.write(_getWritableChar(char, utf8: false));
+    }
+  }
+
+  return sb.toString();
+}
+
+/// Creates a writable char from [char] code.
+///
+/// E.g- `\` is converted to `\\`.
+String _getWritableChar(int char, {bool utf8 = true}) {
+  /// Handle control characters.
+  if (char >= 0 && char < 32 || char == 127) {
+    /// Handle these - `\b \t \n \v \f \r` as special cases.
+    switch (char) {
+      case 8: // \b
+        return r'\b';
+      case 9: // \t
+        return r'\t';
+      case 10: // \n
+        return r'\n';
+      case 11: // \v
+        return r'\v';
+      case 12: // \f
+        return r'\f';
+      case 13: // \r
+        return r'\r';
+      default:
+        final h = char.toRadixString(16).toUpperCase().padLeft(2, '0');
+        return '\\x${h}';
+    }
+  }
+
+  /// Handle characters - `$ ' \` these need to be escaped when writing to file.
+  switch (char) {
+    case 36: // $
+      return r'\$';
+    case 39: // '
+      return r"\'";
+    case 92: // \
+      return r'\\';
+  }
+
+  /// In case encoding is not Utf8, we know all characters will fall in [0..255]
+  /// Print range [128..255] as `\xHH`.
+  if (!utf8) {
+    final h = char.toRadixString(16).toUpperCase().padLeft(2, '0');
+    return '\\x${h}';
+  }
+
+  /// In all other cases, simply convert to string.
+  return String.fromCharCode(char);
+}
diff --git a/pubspec.yaml b/pubspec.yaml
index 04ca960..578efe9 100644
--- a/pubspec.yaml
+++ b/pubspec.yaml
@@ -3,7 +3,7 @@
 # BSD-style license that can be found in the LICENSE file.
 
 name: ffigen
-version: 1.0.2
+version: 1.0.3
 homepage: https://github.com/dart-lang/ffigen
 description: Experimental generator for FFI bindings, using LibClang to parse C header files.
 
diff --git a/test/header_parser_tests/macros.h b/test/header_parser_tests/macros.h
index d75bcca..e58d4ac 100644
--- a/test/header_parser_tests/macros.h
+++ b/test/header_parser_tests/macros.h
@@ -15,6 +15,11 @@
 
 #define TEST8 5,2,3
 
-// These tests that special characters are escaped properly.
+// These test that special characters are escaped properly.
 #define TEST9 "$dollar"
 #define TEST10 "test's"
+
+// These test that extended ASCII and control characters are handled properly.
+#define TEST11 "\x80"
+#define TEST12 "hello\n\t\r\v\b"
+#define TEST13 "test\\"
diff --git a/test/header_parser_tests/macros_test.dart b/test/header_parser_tests/macros_test.dart
index e358873..9b279e7 100644
--- a/test/header_parser_tests/macros_test.dart
+++ b/test/header_parser_tests/macros_test.dart
@@ -72,6 +72,18 @@
       expect(actual.getBindingAsString('TEST10'),
           expected.getBindingAsString('TEST10'));
     });
+    test('TEST11', () {
+      expect(actual.getBindingAsString('TEST11'),
+          expected.getBindingAsString('TEST11'));
+    });
+    test('TEST12', () {
+      expect(actual.getBindingAsString('TEST12'),
+          expected.getBindingAsString('TEST12'));
+    });
+    test('TEST13', () {
+      expect(actual.getBindingAsString('TEST13'),
+          expected.getBindingAsString('TEST13'));
+    });
   });
 }
 
@@ -88,6 +100,10 @@
       Constant(name: 'TEST8', rawType: 'int', rawValue: '5'),
       Constant(name: 'TEST9', rawType: 'String', rawValue: r"'\$dollar'"),
       Constant(name: 'TEST10', rawType: 'String', rawValue: r"'test\'s'"),
+      Constant(name: 'TEST11', rawType: 'String', rawValue: r"'\x80'"),
+      Constant(
+          name: 'TEST12', rawType: 'String', rawValue: r"'hello\n\t\r\v\b'"),
+      Constant(name: 'TEST13', rawType: 'String', rawValue: r"'test\\'"),
     ],
   );
 }