Skip to content

Commit 7eca036

Browse files
committed
🐛 fix Utils.unescape for '%' characters
1 parent 9f6a43b commit 7eca036

File tree

2 files changed

+109
-21
lines changed

2 files changed

+109
-21
lines changed

lib/src/utils.dart

Lines changed: 50 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -214,19 +214,58 @@ final class Utils {
214214
final int c = str.codeUnitAt(i);
215215

216216
if (c == 0x25) {
217-
if (str[i + 1] == 'u') {
218-
buffer.writeCharCode(
219-
int.parse(str.slice(i + 2, i + 6), radix: 16),
220-
);
221-
i += 6;
217+
// '%'
218+
// Ensure there's at least one character after '%'
219+
if (i + 1 < str.length) {
220+
if (str[i + 1] == 'u') {
221+
// Check that there are at least 6 characters for "%uXXXX"
222+
if (i + 6 <= str.length) {
223+
try {
224+
final int charCode =
225+
int.parse(str.substring(i + 2, i + 6), radix: 16);
226+
buffer.writeCharCode(charCode);
227+
i += 6;
228+
continue;
229+
} on FormatException {
230+
// Not a valid %u escape: treat '%' as literal.
231+
buffer.write(str[i]);
232+
i++;
233+
continue;
234+
}
235+
} else {
236+
// Not enough characters for a valid %u escape: treat '%' as literal.
237+
buffer.write(str[i]);
238+
i++;
239+
continue;
240+
}
241+
} else {
242+
// For %XX escape: check that there are at least 3 characters.
243+
if (i + 3 <= str.length) {
244+
try {
245+
final int charCode =
246+
int.parse(str.substring(i + 1, i + 3), radix: 16);
247+
buffer.writeCharCode(charCode);
248+
i += 3;
249+
continue;
250+
} on FormatException {
251+
// Parsing failed: treat '%' as literal.
252+
buffer.write(str[i]);
253+
i++;
254+
continue;
255+
}
256+
} else {
257+
// Not enough characters for a valid %XX escape: treat '%' as literal.
258+
buffer.write(str[i]);
259+
i++;
260+
continue;
261+
}
262+
}
263+
} else {
264+
// '%' is the last character; treat it as literal.
265+
buffer.write(str[i]);
266+
i++;
222267
continue;
223268
}
224-
225-
buffer.writeCharCode(
226-
int.parse(str.slice(i + 1, i + 3), radix: 16),
227-
);
228-
i += 3;
229-
continue;
230269
}
231270

232271
buffer.write(str[i]);

test/unit/utils_test.dart

Lines changed: 59 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -128,23 +128,50 @@ void main() {
128128
});
129129

130130
test('escape', () {
131+
// Basic alphanumerics (remain unchanged)
132+
expect(
133+
Utils.escape(
134+
'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789@*_+-./',
135+
),
136+
equals(
137+
'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789@*_+-./',
138+
),
139+
);
140+
// Basic alphanumerics (remain unchanged)
131141
expect(Utils.escape('abc123'), equals('abc123'));
142+
// Accented characters (Latin-1 range uses %XX)
132143
expect(Utils.escape('äöü'), equals('%E4%F6%FC'));
144+
// Non-ASCII that falls outside Latin-1 uses %uXXXX
133145
expect(Utils.escape('ć'), equals('%u0107'));
134-
// special characters
146+
// Characters that are defined as safe
135147
expect(Utils.escape('@*_+-./'), equals('@*_+-./'));
148+
// Parentheses: in RFC3986 they are encoded
136149
expect(Utils.escape('('), equals('%28'));
137150
expect(Utils.escape(')'), equals('%29'));
151+
// Space character
138152
expect(Utils.escape(' '), equals('%20'));
153+
// Tilde is safe
139154
expect(Utils.escape('~'), equals('%7E'));
140-
155+
// Punctuation that is not safe: exclamation and comma
156+
expect(Utils.escape('!'), equals('%21'));
157+
expect(Utils.escape(','), equals('%2C'));
158+
// Mixed safe and unsafe characters
159+
expect(Utils.escape('hello world!'), equals('hello%20world%21'));
160+
// Multiple spaces are each encoded
161+
expect(Utils.escape('a b c'), equals('a%20b%20c'));
162+
// A string with various punctuation
163+
expect(Utils.escape('Hello, World!'), equals('Hello%2C%20World%21'));
164+
// Null character should be encoded
165+
expect(Utils.escape('\x00'), equals('%00'));
166+
// Emoji (e.g. 😀 U+1F600)
167+
expect(Utils.escape('😀'), equals('%uD83D%uDE00'));
168+
// Test RFC1738 format: Parentheses are safe (left unchanged)
169+
expect(Utils.escape('(', format: Format.rfc1738), equals('('));
170+
expect(Utils.escape(')', format: Format.rfc1738), equals(')'));
171+
// Mixed test with RFC1738: other unsafe characters are still encoded
141172
expect(
142-
Utils.escape(
143-
'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789@*_+-./',
144-
),
145-
equals(
146-
'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789@*_+-./',
147-
),
173+
Utils.escape('(hello)!', format: Format.rfc1738),
174+
equals('(hello)%21'),
148175
);
149176
});
150177

@@ -154,16 +181,24 @@ void main() {
154181
});
155182

156183
test('unescape', () {
184+
// No escapes.
157185
expect(Utils.unescape('abc123'), equals('abc123'));
186+
// Hex escapes with uppercase hex digits.
158187
expect(Utils.unescape('%E4%F6%FC'), equals('äöü'));
188+
// Hex escapes with lowercase hex digits.
189+
expect(Utils.unescape('%e4%f6%fc'), equals('äöü'));
190+
// Unicode escape.
159191
expect(Utils.unescape('%u0107'), equals('ć'));
160-
// special characters
192+
// Unicode escape with lowercase digits.
193+
expect(Utils.unescape('%u0061'), equals('a'));
194+
// Characters that do not need escaping.
161195
expect(Utils.unescape('@*_+-./'), equals('@*_+-./'));
196+
// Hex escapes for punctuation.
162197
expect(Utils.unescape('%28'), equals('('));
163198
expect(Utils.unescape('%29'), equals(')'));
164199
expect(Utils.unescape('%20'), equals(' '));
165200
expect(Utils.unescape('%7E'), equals('~'));
166-
201+
// A long string with only safe characters.
167202
expect(
168203
Utils.unescape(
169204
'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789@*_+-./',
@@ -172,6 +207,20 @@ void main() {
172207
'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789@*_+-./',
173208
),
174209
);
210+
// A mix of Unicode and hex escapes.
211+
expect(Utils.unescape('%u0041%20%42'), equals('A B'));
212+
// A mix of literal text and hex escapes.
213+
expect(Utils.unescape('hello%20world'), equals('hello world'));
214+
// A literal percent sign that is not followed by a valid escape remains unchanged.
215+
expect(Utils.unescape('100% sure'), equals('100% sure'));
216+
// Mixed Unicode and hex escapes.
217+
expect(Utils.unescape('%u0041%65'), equals('Ae'));
218+
// Escaped percent signs that do not form a valid escape remain unchanged.
219+
expect(Utils.unescape('50%% off'), equals('50%% off'));
220+
// Consecutive escapes producing multiple spaces.
221+
expect(Utils.unescape('%20%u0020'), equals(' '));
222+
// An invalid escape sequence should remain unchanged.
223+
expect(Utils.unescape('abc%g'), equals('abc%g'));
175224
});
176225

177226
test('unescape huge string', () {

0 commit comments

Comments
 (0)