[ffigen] Fixed errors due to extended ASCII and control characters in macros (#106)
diff --git a/pkgs/ffigen/CHANGELOG.md b/pkgs/ffigen/CHANGELOG.md index 2ae8d46..a59311b 100644 --- a/pkgs/ffigen/CHANGELOG.md +++ b/pkgs/ffigen/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/pkgs/ffigen/lib/src/header_parser/sub_parsers/macro_parser.dart b/pkgs/ffigen/lib/src/header_parser/sub_parsers/macro_parser.dart index 8884dd5..16cf4a1 100644 --- a/pkgs/ffigen/lib/src/header_parser/sub_parsers/macro_parser.dart +++ b/pkgs/ffigen/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/pkgs/ffigen/pubspec.yaml b/pkgs/ffigen/pubspec.yaml index 04ca960..578efe9 100644 --- a/pkgs/ffigen/pubspec.yaml +++ b/pkgs/ffigen/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/pkgs/ffigen/test/header_parser_tests/macros.h b/pkgs/ffigen/test/header_parser_tests/macros.h index d75bcca..e58d4ac 100644 --- a/pkgs/ffigen/test/header_parser_tests/macros.h +++ b/pkgs/ffigen/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/pkgs/ffigen/test/header_parser_tests/macros_test.dart b/pkgs/ffigen/test/header_parser_tests/macros_test.dart index e358873..9b279e7 100644 --- a/pkgs/ffigen/test/header_parser_tests/macros_test.dart +++ b/pkgs/ffigen/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\\'"), ], ); }