четверг, 13 ноября 2014 г.

Об экранировании слеша в .NET

Блог переехал. Актуальная версия поста находится по адресу: http://aakinshin.net/ru/blog/dotnet/uri-escaping/.


Загадка на сегодня: что выведет код?

var uri = new Uri("http://localhost/%2F1");
Console.WriteLine(uri.OriginalString);
Console.WriteLine(uri.AbsoluteUri);

Правильный ответ: зависит. Давайте немножко поразбираемся.

Так что же он выведет?

Результат работы зависит от версии .NET:

// .NET 4.0
http://localhost/%2F1
http://localhost//1
// .NET 4.5
http://localhost/%2F1
http://localhost/%2F1

Да как же так-то?

Увы, до версии .NET 4.0 имела место неприятная бага с экранированием слеша (он же %2F). В 4.5 её решили пофиксить, чтобы поведение соответствовало RFC 3986. Сделали вроде бы правильно, но добавили дополнительной головной боли разработчикам, которые не знают про этот небольшой нюанс: теперь механизм экранирования зависит от версии Framework-а. Лучше всего использовать правильный механизм из .NET 4.5. Но что, если у нас нет .NET 4.5? Имеется путь починить поведение в .NET 4.0. Для этого необходимо добавить в *.config-файл вашего приложения магические строчки:

<configuration>
  <uri>
    <schemeSettings>
      <add name="http" 
           genericUriParserOptions="DontUnescapePathDotsAndSlashes" />
    </schemeSettings>
  </uri>
</configuration>

Работает приведённый фокус-покус начиная с .NET 4.0 beta 2. Т.е., скажем, в .NET 3.5 так сделать не получится. Так что придётся крутиться и вертеться. Например, на просторах интернета можно найти вот такой чудо-хак:

void ForceCanonicalPathAndQuery(Uri uri)
{
  string paq = uri.PathAndQuery; // need to access PathAndQuery
  FieldInfo flagsFieldInfo = typeof(Uri).GetField("m_Flags", 
    BindingFlags.Instance | BindingFlags.NonPublic);
  ulong flags = (ulong) flagsFieldInfo.GetValue(uri);
  flags &= ~((ulong) 0x30); // Flags.PathNotCanonical|Flags.QueryNotCanonical
  flagsFieldInfo.SetValue(uri, flags);
}

А что будет в Mono?

В Mono накосячили точно также. Починка осуществилась совсем недавно с выходом Mono 3.10.0 в октябре 2014. Так что если вы сидите на последней версии, то у вас уже всё должно быть хорошо. Но как же нам теперь переключаться между старым и новым поведением? Для этих целей в классе System.Uri имеется свойство IriParsing. Заглянем в код:

private static bool s_IriParsing;

internal static bool IriParsing {
    get { return s_IriParsing; }
    set { s_IriParsing = value; }
}

Выставляется свойство следующим образом:

static Uri ()
{
#if NET_4_5
    IriParsing = true;
#endif

    var iriparsingVar = 
        Environment.GetEnvironmentVariable ("MONO_URI_IRIPARSING");
    if (iriparsingVar == "true")
        IriParsing = true;
    else if (iriparsingVar == "false")
        IriParsing = false;
}

Т.е. выставить его проще всего через переменную окружения MONO_URI_IRIPARSING.

Заключение

Бага не особо приятная и может стоить вам многих часов душевного спокойствия, если вы на неё случайно наткнётесь. Поэтому я решил оформить такую вот небольшую заметку, чтобы побольше людей было в курсе. Помните о неоднозначности экранирования некоторых URI и пишите стабильный код.

Ссылки по теме