Как я искал (и нашел) разницу в двух побайтово идентичных файлах
Есть у нас одно .NET-приложение, которое умеет загружать и использовать плагины. Плагины — дело хорошее. Можно функционал расширять, можно оперативненько обновлять их со своего сайта, можно даже юзерам дать SDK и позволить писать свои плагины. Мы всё это и делали. Наши плагины представляли собой обычные .NET-сборки, которые нужно было подкинуть в определённую папку, откуда основное приложения их загружало и использовало. Ну, вы, наверное представляете как — Assembly.Load (), дальше ищем класс, реализующий необходимый интерфейс, создаём объект этого класса и т.д. Всё это работало давно, стабильно и ничто не предвещало беды. Но вдруг в какой-то момент появилась необходимость создать плагин, состоящий из нескольких файлов. В связи с этим было решено считать плагином не просто .NET-сборку (1 файл), а zip-архив, в котором может быть как одна сборка, так и несколько файлов. В связи с этим пришлось научить билд-сервер паковать плагины в архивы, а основное приложение — разархивировать их в нужное место. В общем-то задача на 10 строк кода. Ничто не предвещало беды. И вот скачиваю я с билд-сервера собранный архив с плагином, разархивирую его в нужную папку, запускаю приложение, и… не работает! Стоп, как не работает? Это ведь тот же плагин!
Дальше — больше. Прошу проделать ту же самую процедуру моего коллегу, на его компьютера. Он пробует — и у него всё работает! Но как же так? Одна версия приложения, один и тот же файл с билд-сервера. Какая-то разница в окружении? Сажусь за компьютер коллеги, пробую ещё раз — не работает! Он в этом время пробует на моём — работает! То есть получается, что файл «помнит», кто его разархивировал! Зовём третьего коллегу понаблюдать этот цирк. Последовательно, на одном и том же компьютере, по очереди делаем одни и те же действия: скачиваем архив с плагином, разархивируем в нужную папку, запускаем приложение. Когда это делаю я — программа не видит плагин, когда это делает коллега — всё работает. На третьем круге этих интересных экспериментов вдруг замечаем разницу в действиях: я разархивировал плагин стандартными средствами Windows, а мой коллега — с помощью 7-Zip. И то и другое вызывалось нами из контекстного меню архива, так что разницу в клик по не тому пункту вначале никто не замечал. Ну ок. Получается, файл, извлечённый из zip-архива с помощью 7-zip, отличается от того же файла из того же архива, извлечённого с помощью стандартного архиватора Windows?
Кстати, пока вы не открыли статью под катом, ответьте-ка сами для себя на вопрос, может ли такое быть, что содержимое файлов валидного zip-архива при разархивации 7-zip и через проводник Windows будет разным?
Ну, не будем гадать и сравним файлы с помощью WinMerge:
Получается, файлы одинаковые и должны одинаково загружаться и обрабатываться? Как бы не так! WinMerge врёт. Файлы разные. И загружаются они .NETом тоже по-разному.
А теперь будет страшная правда
При загрузке файла из интернета Windows ставит на него специальный «флаг», означающий зону доверия, соответствующую сайту, с которого он был загружен. Я думаю, многие видели при попытке запуска только что скачанного исполняемого файла предупреждения о том, что запускать его, возможно, не стоит, надо подумать, вот посмотрите сертификат и скажите, что делать. В зависимости от политик безопасности и происхождения файла уровень параноидальности этих предупреждений может быть разным — от полного их отсутствия (работаем под админом, UAC отключен, файл подписан) до блокировки запуска (корпоративное окружение, неподписанный файл). Есть и несколько промежуточных стадий, где надо один или несколько раз сказать «да, запускаем». Но это всё ведь работает только для exe-файлов, да? Нет! На скачанный из интернета dll-файл или архив тоже будет повешен данный флаг! С технической точки зрения он является альтернативным файловым потоком NTFS, который можно посмотреть, например, через утилиту AlternateStreamView ну или через команду:
more < Plugin.dll:Zone.Identifier
И вот здесь мы имеем стечение следующих обстоятельств:
- Браузер при загрузке создаёт для скачанного архива альтернативный файловый поток «Zone.Identifier» и пишет туда ID зоны, откуда пришел файл.
- Стандартный архиватор проводника Windows при разархивировании читает не только основной файловый поток, но и альтернативные, и добавляет их к каждому извлечённому файлу. (7-Zip этого не делает).
- Утилита WinMerge сравнивает только основные файловые потоки и говорит, что файлы, созданные 7-Zip и проводником идентичны.
- В .NET метод Assembly.Load () тоже читает альтернативные файловые потоки, находит идентификатор зоны с пониженным доверием — и отказывается загружать файл! При этом привычные пользователю сообщения с просьбой подтвердить запуск недоверенного приложения не показываются и мы получаем наш баг.
Бороться с проблемой достаточно просто — нужно проверять\удалять данный файловый поток. В Windows для этого можно вызвать свойства файла и нажать там кнопку Unblock (ну или делать это програмно).
Если вы сделаете это для архива до извлечения из него файлов — идентификатор зоны пропадёт и для всех извлеченных в последствии файлов.
Возможно, я тут рассказал банальные и всем известные вещи, однако тот факт, что из одного и того же архива различные архиваторы могут извлечь разные файлы, да ещё и так хитро разные, что WinMerge этой разницы не видит, а .NET — видит, лично для меня было интересным открытием.