Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
61 changes: 50 additions & 11 deletions lib/src/utils.dart
Original file line number Diff line number Diff line change
Expand Up @@ -214,19 +214,58 @@ final class Utils {
final int c = str.codeUnitAt(i);

if (c == 0x25) {
if (str[i + 1] == 'u') {
buffer.writeCharCode(
int.parse(str.slice(i + 2, i + 6), radix: 16),
);
i += 6;
// '%'
// Ensure there's at least one character after '%'
if (i + 1 < str.length) {
if (str[i + 1] == 'u') {
// Check that there are at least 6 characters for "%uXXXX"
if (i + 6 <= str.length) {
try {
final int charCode =
int.parse(str.substring(i + 2, i + 6), radix: 16);
buffer.writeCharCode(charCode);
i += 6;
continue;
} on FormatException {
// Not a valid %u escape: treat '%' as literal.
buffer.write(str[i]);
i++;
continue;
}
} else {
// Not enough characters for a valid %u escape: treat '%' as literal.
buffer.write(str[i]);
i++;
continue;
}
} else {
// For %XX escape: check that there are at least 3 characters.
if (i + 3 <= str.length) {
try {
final int charCode =
int.parse(str.substring(i + 1, i + 3), radix: 16);
buffer.writeCharCode(charCode);
i += 3;
continue;
} on FormatException {
// Parsing failed: treat '%' as literal.
buffer.write(str[i]);
i++;
continue;
}
} else {
// Not enough characters for a valid %XX escape: treat '%' as literal.
buffer.write(str[i]);
i++;
continue;
}
}
} else {
// '%' is the last character; treat it as literal.
buffer.write(str[i]);
i++;
continue;
}

buffer.writeCharCode(
int.parse(str.slice(i + 1, i + 3), radix: 16),
);
i += 3;
continue;
}

buffer.write(str[i]);
Expand Down
93 changes: 83 additions & 10 deletions test/unit/utils_test.dart
Original file line number Diff line number Diff line change
Expand Up @@ -128,23 +128,50 @@ void main() {
});

test('escape', () {
// Basic alphanumerics (remain unchanged)
expect(
Utils.escape(
'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789@*_+-./',
),
equals(
'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789@*_+-./',
),
);
// Basic alphanumerics (remain unchanged)
expect(Utils.escape('abc123'), equals('abc123'));
// Accented characters (Latin-1 range uses %XX)
expect(Utils.escape('äöü'), equals('%E4%F6%FC'));
// Non-ASCII that falls outside Latin-1 uses %uXXXX
expect(Utils.escape('ć'), equals('%u0107'));
// special characters
// Characters that are defined as safe
expect(Utils.escape('@*_+-./'), equals('@*_+-./'));
// Parentheses: in RFC3986 they are encoded
expect(Utils.escape('('), equals('%28'));
expect(Utils.escape(')'), equals('%29'));
// Space character
expect(Utils.escape(' '), equals('%20'));
// Tilde is safe
expect(Utils.escape('~'), equals('%7E'));

// Punctuation that is not safe: exclamation and comma
expect(Utils.escape('!'), equals('%21'));
expect(Utils.escape(','), equals('%2C'));
// Mixed safe and unsafe characters
expect(Utils.escape('hello world!'), equals('hello%20world%21'));
// Multiple spaces are each encoded
expect(Utils.escape('a b c'), equals('a%20b%20c'));
// A string with various punctuation
expect(Utils.escape('Hello, World!'), equals('Hello%2C%20World%21'));
// Null character should be encoded
expect(Utils.escape('\x00'), equals('%00'));
// Emoji (e.g. 😀 U+1F600)
expect(Utils.escape('😀'), equals('%uD83D%uDE00'));
// Test RFC1738 format: Parentheses are safe (left unchanged)
expect(Utils.escape('(', format: Format.rfc1738), equals('('));
expect(Utils.escape(')', format: Format.rfc1738), equals(')'));
// Mixed test with RFC1738: other unsafe characters are still encoded
expect(
Utils.escape(
'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789@*_+-./',
),
equals(
'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789@*_+-./',
),
Utils.escape('(hello)!', format: Format.rfc1738),
equals('(hello)%21'),
);
});

Expand All @@ -154,16 +181,24 @@ void main() {
});

test('unescape', () {
// No escapes.
expect(Utils.unescape('abc123'), equals('abc123'));
// Hex escapes with uppercase hex digits.
expect(Utils.unescape('%E4%F6%FC'), equals('äöü'));
// Hex escapes with lowercase hex digits.
expect(Utils.unescape('%e4%f6%fc'), equals('äöü'));
// Unicode escape.
expect(Utils.unescape('%u0107'), equals('ć'));
// special characters
// Unicode escape with lowercase digits.
expect(Utils.unescape('%u0061'), equals('a'));
// Characters that do not need escaping.
expect(Utils.unescape('@*_+-./'), equals('@*_+-./'));
// Hex escapes for punctuation.
expect(Utils.unescape('%28'), equals('('));
expect(Utils.unescape('%29'), equals(')'));
expect(Utils.unescape('%20'), equals(' '));
expect(Utils.unescape('%7E'), equals('~'));

// A long string with only safe characters.
expect(
Utils.unescape(
'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789@*_+-./',
Expand All @@ -172,6 +207,44 @@ void main() {
'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789@*_+-./',
),
);
// A mix of Unicode and hex escapes.
expect(Utils.unescape('%u0041%20%42'), equals('A B'));
// A mix of literal text and hex escapes.
expect(Utils.unescape('hello%20world'), equals('hello world'));
// A literal percent sign that is not followed by a valid escape remains unchanged.
expect(Utils.unescape('100% sure'), equals('100% sure'));
// Mixed Unicode and hex escapes.
expect(Utils.unescape('%u0041%65'), equals('Ae'));
// Escaped percent signs that do not form a valid escape remain unchanged.
expect(Utils.unescape('50%% off'), equals('50%% off'));
// Consecutive escapes producing multiple spaces.
expect(Utils.unescape('%20%u0020'), equals(' '));
// An invalid escape sequence should remain unchanged.
expect(Utils.unescape('abc%g'), equals('abc%g'));

// The input "%uZZZZ" is 6 characters long so it passes the length check.
// However, "ZZZZ" is not valid hex so int.parse will throw a FormatException.
// In that case, the catch block writes the literal '%' and increments i by 1.
// The remainder of the string is then processed normally.
// For input "%uZZZZ", the processing is:
// - At i = 0, encounter '%', then since i+1 is 'u' and there are 6 characters, try block is entered.
// - int.parse("ZZZZ", radix: 16) fails, so the catch writes '%' and i becomes 1.
// - Then the rest of the string ("uZZZZ") is appended as literal.
// The expected result is "%uZZZZ".
expect(Utils.unescape('%uZZZZ'), equals('%uZZZZ'));

// Input "%u12" has only 4 characters.
// For a valid %u escape we need 6 characters.
// Thus, the branch "Not enough characters for a valid %u escape" is triggered,
// which writes the literal '%' and increments i.
// The remainder of the string ("u12") is then appended as literal.
// Expected output is "%u12".
expect(Utils.unescape('%u12'), equals('%u12'));

// When "%" is the last character of the string (with no following characters),
// the code writes it as literal.
// For example, "abc%" should remain "abc%".
expect(Utils.unescape('abc%'), equals('abc%'));
});

test('unescape huge string', () {
Expand Down
Loading