diff --git a/lib/src/utils.dart b/lib/src/utils.dart index c685d73..0fb40e2 100644 --- a/lib/src/utils.dart +++ b/lib/src/utils.dart @@ -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]); diff --git a/test/unit/utils_test.dart b/test/unit/utils_test.dart index e3ce5b9..4a45b6d 100644 --- a/test/unit/utils_test.dart +++ b/test/unit/utils_test.dart @@ -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'), ); }); @@ -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@*_+-./', @@ -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', () {