Skip to content

Commit daff97d

Browse files
authored
Better escaping of String literals (#126)
* Whitespace encoding: be comprehensive * Handle slashes, quotes, and dollar signs * renamed json literal file in test
1 parent 5f3977d commit daff97d

File tree

6 files changed

+124
-40
lines changed

6 files changed

+124
-40
lines changed

json_serializable/CHANGELOG.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,8 @@
1515
* Throw an exception if a private field or an ignored field is referenced by a
1616
required constructor argument.
1717

18+
* More comprehensive escaping of string literals.
19+
1820
### `package:json_serializable/type_helper.dart`
1921

2022
* **Breaking** The `nullable` parameter on `TypeHelper.serialize` and

json_serializable/lib/src/utils.dart

Lines changed: 73 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -13,39 +13,85 @@ import 'package:source_gen/source_gen.dart';
1313

1414
/// Returns a quoted String literal for [value] that can be used in generated
1515
/// Dart code.
16-
// TODO: still need handle triple singe/double quotes within `value`
1716
String escapeDartString(String value) {
18-
if (value.contains('\n')) {
19-
return "r'''\n$value'''";
17+
var hasSingleQuote = false;
18+
var hasDoubleQuote = false;
19+
var hasDollar = false;
20+
var canBeRaw = true;
21+
22+
value = value.replaceAllMapped(_escapeRegExp, (match) {
23+
var value = match[0];
24+
if (value == "'") {
25+
hasSingleQuote = true;
26+
return value;
27+
} else if (value == '"') {
28+
hasDoubleQuote = true;
29+
return value;
30+
} else if (value == r'$') {
31+
hasDollar = true;
32+
return value;
33+
}
34+
35+
canBeRaw = false;
36+
return _escapeMap[value] ?? _getHexLiteral(value);
37+
});
38+
39+
if (!hasDollar) {
40+
if (hasSingleQuote) {
41+
if (!hasDoubleQuote) {
42+
return '"$value"';
43+
}
44+
// something
45+
} else {
46+
// trivial!
47+
return "'$value'";
48+
}
2049
}
2150

22-
var containsDollar = value.contains(r'$');
23-
24-
if (value.contains("'")) {
25-
if (value.contains('"')) {
26-
// `value` contains both single and double quotes.
27-
// The only safe way to wrap the content is to escape all of the
28-
// problematic characters.
29-
var string = value
30-
.replaceAll(r'$', r'\$')
31-
.replaceAll('"', r'\"')
32-
.replaceAll("'", r"\'");
33-
return "'$string'";
34-
} else if (containsDollar) {
35-
// `value` contains "'" and "$", but not '"'.
36-
// Safely wrap it in a raw string within double-quotes.
37-
return 'r"$value"';
51+
if (hasDollar && canBeRaw) {
52+
if (hasSingleQuote) {
53+
if (!hasDoubleQuote) {
54+
// quote it with single quotes!
55+
return 'r"$value"';
56+
}
57+
} else {
58+
// quote it with single quotes!
59+
return "r'$value'";
3860
}
39-
return '"$value"';
40-
} else if (containsDollar) {
41-
// `value` contains "$", but no "'"
42-
// wrap it in a raw string using single quotes
43-
return "r'$value'";
4461
}
4562

46-
// `value` contains no problematic characters - except for '"' maybe.
47-
// Wrap it in standard single-quotes.
48-
return "'$value'";
63+
// The only safe way to wrap the content is to escape all of the
64+
// problematic characters - `$`, `'`, and `"`
65+
var string = value.replaceAll(_dollarQuoteRegexp, r'\');
66+
return "'$string'";
67+
}
68+
69+
final _dollarQuoteRegexp = new RegExp(r"""(?=[$'"])""");
70+
71+
/// A [Map] between whitespace characters & `\` and their escape sequences.
72+
const _escapeMap = const {
73+
'\b': r'\b', // 08 - backspace
74+
'\t': r'\t', // 09 - tab
75+
'\n': r'\n', // 0A - new line
76+
'\v': r'\v', // 0B - vertical tab
77+
'\f': r'\f', // 0C - form feed
78+
'\r': r'\r', // 0D - carriage return
79+
'\x7F': r'\x7F', // delete
80+
r'\': r'\\' // backslash
81+
};
82+
83+
final _escapeMapRegexp = _escapeMap.keys.map(_getHexLiteral).join();
84+
85+
/// A [RegExp] that matches whitespace characters that should be escaped and
86+
/// single-quote, double-quote, and `$`
87+
final _escapeRegExp =
88+
new RegExp('[\$\'"\\x00-\\x07\\x0E-\\x1F$_escapeMapRegexp]');
89+
90+
/// Given single-character string, return the hex-escaped equivalent.
91+
String _getHexLiteral(String input) {
92+
var rune = input.runes.single;
93+
var value = rune.toRadixString(16).toUpperCase().padLeft(2, '0');
94+
return '\\x$value';
4995
}
5096

5197
String commonNullPrefix(

json_serializable/test/json_literal_test.dart

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -14,13 +14,13 @@ import 'test_utils.dart';
1414
main() {
1515
test('literal round-trip', () {
1616
var dataFilePath =
17-
p.join(getPackagePath(), 'test', 'test_files', 'data.json');
17+
p.join(getPackagePath(), 'test', 'test_files', 'json_literal.json');
1818
var dataFile = new File(dataFilePath);
1919

2020
var dataString = loudEncode(json.decode(dataFile.readAsStringSync()));
2121
// FYI: nice to re-write the test data when it's changed to keep it pretty
2222
// ... but not a good idea to ship this
23-
// dataFile.writeAsStringSync(dataString);
23+
// dataFile.writeAsStringSync(dataString.replaceAll('\u007F', '\\u007F'));
2424
var dartString = loudEncode(data);
2525

2626
expect(dartString, dataString);

json_serializable/test/test_files/json_literal.dart

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,8 +5,8 @@
55
import 'package:json_annotation/json_annotation.dart';
66
part 'json_literal.g.dart';
77

8-
@JsonLiteral('data.json')
8+
@JsonLiteral('json_literal.json')
99
List get data => _$dataJsonLiteral;
1010

11-
@JsonLiteral('data.json', asConst: true)
11+
@JsonLiteral('json_literal.json', asConst: true)
1212
List get asConst => _$asConstJsonLiteral;

json_serializable/test/test_files/json_literal.g.dart

Lines changed: 30 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -11,10 +11,15 @@ part of 'json_literal.dart';
1111
// **************************************************************************
1212

1313
final _$dataJsonLiteral = [
14-
r'''
15-
16-
new lines are fun!
17-
''',
14+
{
15+
'backspace': '\b',
16+
'tab': '\t',
17+
'new line': '\n',
18+
'vertical tab': '\v',
19+
'form feed': '\r',
20+
'carriage return': '\r',
21+
'delete': '\x7F'
22+
},
1823
'simple string',
1924
"'string with single quotes'",
2025
'"string with double quotes"',
@@ -25,6 +30,12 @@ new lines are fun!
2530
'""hello""',
2631
r'""$double quotes and dollar signs""',
2732
'\$scary with \'single quotes\' and triple-doubles \"\"\"oh no!',
33+
'Dollar signs: \$ vs \\\$ vs \\\\\$',
34+
'Slashes \\nice slash\\',
35+
'slashes \\ and dollars \$ with white \n space',
36+
"'''triple quoted strings should be\nfine!'''",
37+
'"""as with triple-double-quotes"""',
38+
'\"\"\"as with triple-double-quotes even when \'mixed\'\"\"\"',
2839
null,
2940
true,
3041
false,
@@ -42,10 +53,15 @@ new lines are fun!
4253
}
4354
];
4455
const _$asConstJsonLiteral = const [
45-
r'''
46-
47-
new lines are fun!
48-
''',
56+
const {
57+
'backspace': '\b',
58+
'tab': '\t',
59+
'new line': '\n',
60+
'vertical tab': '\v',
61+
'form feed': '\r',
62+
'carriage return': '\r',
63+
'delete': '\x7F'
64+
},
4965
'simple string',
5066
"'string with single quotes'",
5167
'"string with double quotes"',
@@ -56,6 +72,12 @@ new lines are fun!
5672
'""hello""',
5773
r'""$double quotes and dollar signs""',
5874
'\$scary with \'single quotes\' and triple-doubles \"\"\"oh no!',
75+
'Dollar signs: \$ vs \\\$ vs \\\\\$',
76+
'Slashes \\nice slash\\',
77+
'slashes \\ and dollars \$ with white \n space',
78+
"'''triple quoted strings should be\nfine!'''",
79+
'"""as with triple-double-quotes"""',
80+
'\"\"\"as with triple-double-quotes even when \'mixed\'\"\"\"',
5981
null,
6082
true,
6183
false,

json_serializable/test/test_files/data.json renamed to json_serializable/test/test_files/json_literal.json

Lines changed: 15 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,13 @@
11
[
2-
"\nnew lines are fun!\n",
2+
{
3+
"backspace": "\b",
4+
"tab": "\t",
5+
"new line": "\n",
6+
"vertical tab": "\u000b",
7+
"form feed": "\r",
8+
"carriage return": "\r",
9+
"delete": "\u007F"
10+
},
311
"simple string",
412
"'string with single quotes'",
513
"\"string with double quotes\"",
@@ -10,6 +18,12 @@
1018
"\"\"hello\"\"",
1119
"\"\"$double quotes and dollar signs\"\"",
1220
"$scary with 'single quotes' and triple-doubles \"\"\"oh no!",
21+
"Dollar signs: $ vs \\$ vs \\\\$",
22+
"Slashes \\nice slash\\",
23+
"slashes \\ and dollars $ with white \n space",
24+
"'''triple quoted strings should be\nfine!'''",
25+
"\"\"\"as with triple-double-quotes\"\"\"",
26+
"\"\"\"as with triple-double-quotes even when 'mixed'\"\"\"",
1327
null,
1428
true,
1529
false,

0 commit comments

Comments
 (0)