Как через год вспомнить, что лежит в папке с внешними обработками
Задачи пользователя - Менеджеры внешних отчетов
У любого, кто пишет на 1С дольше пары лет, есть папка с внешними обработками. В ней "ВнешняяОбработка1.epf", три версии одной загрузки в разных подпапках и печатная форма, которую когда-то забрали из базы заказчика. Через год никто не помнит, что из этого рабочее и что делает файл с именем "111.epf".
Чтобы навести там порядок, надо ответить на три вопроса: как опознать файл после переименования, как узнать, что внутри, не запуская его код, и как собрать в одно место обработки, которые живут только в базах. Ниже разбор всех трёх. Код из статьи работает сам по себе, без нашей обработки.
Ключ - содержимое файла, путь не годится
Ручной реестр в Excel держится на пути к файлу. Первое переименование рвёт связь: строка есть, файла по этому пути нет. Имя и дата изменения тоже врут: "Загрузка.epf" и "Загрузка (2).epf" бывают одним файлом, а скачивание из почты делает трёхлетний файл вчерашним.
Надёжный ключ один - хеш содержимого. SHA-256 платформа считает сама:
Хеш = Новый ХешированиеДанных(ХешФункция.SHA256); Хеш.ДобавитьФайл(ПолныйПуть); Ключ = НРег(ПолучитьHexСтрокуИзДвоичныхДанных(Хеш.ХешСумма));
Описание и теги храните по этому ключу, а путь держите просто полем. Мы так проверяли на папке из 85 файлов: переименовали один, перенесли второй, скопировали третий, удалили четвёртый, пятый заменили новой сборкой. Повторный обход нашёл все пять изменений, а у переименованного и перенесённого описание осталось на месте. Хеши для всей папки посчитались за 108 мс.
Открыть чужой .epf значит выполнить его код
Самый простой способ узнать, что за обработка, это подключить её и прочитать метаданные. Но при подключении выполняется тело модуля объекта, всё написанное вне процедур. Мы собрали образец, который в теле модуля пишет файл-метку на диск, подключили его обычным способом и получили метку раньше, чем успели посмотреть, что это за обработка.
Если метаданные всё-таки нужны через подключение, подключайте в безопасном режиме и без предупреждения на каждый файл:
Адрес = ПоместитьВоВременноеХранилище(Новый ДвоичныеДанные(ПолныйПуть), Новый УникальныйИдентификатор); Защита = Новый ОписаниеЗащитыОтОпасныхДействий; Защита.ПредупреждатьОбОпасныхДействиях = Ложь; Имя = ВнешниеОбработки.Подключить(Адрес, , Истина, Защита); // третий параметр - безопасный режим
Тот же образец так падает с "Установлен безопасный режим", и метки на диске нет. Безопасный режим запрещает запись файлов, запуск программ, COM и интернет, но само тело модуля всё равно выполняется до первого запрещённого действия. Подключение при этом идёт на сервере, и у старых обработок на обычных формах модуль там часто не собирается: Предупреждение, Состояние и ДиалогВыбораФайла на сервере недоступны. Такой файл не опишешь вовсе. Поэтому надёжнее совсем не подключать файл и прочитать его как архив.
Что лежит внутри файла .epf
Файл внешней обработки или отчёта (.erf устроен так же) это контейнер 1С: набор именованных элементов, примерно как ZIP, только формат свой.
- Шапка - первые 16 байт, файл начинается с байтов FF FF FF 7F. Если их нет, перед вами не тот контейнер, который разбирает код ниже: битый файл, другой формат или вовсе не обработка.
- Блоки. Всё остальное хранится документами, документ - цепочкой блоков. У каждого блока заголовок из 31 байта прямо текстом: символы ВК и ПС, три числа по 8 шестнадцатеричных цифр через пробел (размер документа, размер блока, адрес следующего блока), пробел и снова ВК ПС. Адрес следующего 7FFFFFFF значит "блок последний".
- Оглавление - первый документ, сразу за шапкой. На каждый элемент по 12 байт: адрес заголовка элемента, адрес его данных и 7FFFFFFF.
- Элемент - заголовок и данные. Имя элемента записано в заголовке со смещения 20 в UTF-16. Данные верхнего уровня сжаты deflate без обёртки ZIP.
Так выглядит оглавление нашей обработки на обычных формах со встроенной справкой (идентификаторы сокращены):
| Элемент | Что внутри |
|---|---|
| root | {2,443d4be3-...,} - идентификатор главного описания, с него начинается разбор |
| 443d4be3-... | главное описание в скобочном формате 1С: имя, синоним, комментарий и списки реквизитов, табличных частей, форм и макетов |
| a38e7543-... | заголовок формы, первая строка в кавычках - её имя ("Форма") |
| a38e7543-....0 | сама обычная форма: вложенный контейнер из двух элементов, form (описание) и module (модуль формы) |
| 27717259-....0 | модуль объекта: вложенный контейнер из info и text, в text исходник модуля |
| 27717259-....1 | встроенная справка: HTML-страница в base64 |
| version, versions, copyinfo | служебные элементы, для чтения кода не нужны |
Вложенные контейнеры внутри распакованных данных лежат уже без сжатия, их оглавление читается тем же кодом. Модуль объекта от обычной формы отличается составом: у модуля внутри text, у формы module. У управляемой формы элемент с суффиксом .0 вложенным контейнером не является: это скобочный текст вида {4,{описание формы},"текст модуля",...}, модуль формы стоит в нём строкой сразу после описания.
Здесь есть грабля, на которую мы наступили сами. Макеты лежат в контейнере точно так же, как формы: элемент с идентификатором вместо имени и заголовком, где первая строка в кавычках - имя. По одному элементу форму от макета не отличить. Отличает только список форм в главном описании. Первая версия нашего разбора этот список не читала, и на отдельном наборе из 71 файла дала 26 расхождений с метаданными, прочитанными через подключение. После правки расхождений ноль.
Если модуля в контейнере нет совсем, он либо пустой, либо обработку поставили без исходного кода. Прочитать в ней нечего, и это тоже ответ.
Как достать модуль, не запуская обработку
Путей два. Первый штатный, через конфигуратор. Нужна любая база, хоть пустая файловая:
"C:\Program Files\1cv8\<версия>\bin\1cv8.exe" DESIGNER /F "<папка пустой базы>" /DisableStartupDialogs /DumpExternalDataProcessorOrReportToFiles "<куда>\Обработка.xml" "<файл>.epf"
Конфигуратор кладёт рядом с Обработка.xml папку с исходниками: модуль объекта в Ext\ObjectModule.bsl, каждую форму в Forms\<Форма>.xml и Forms\<Форма>\Ext\Form.xml, модуль формы в Ext\Form\Module.bsl. Формат тот же, что у выгрузки конфигурации в файлы, так что результат удобно класть в Git. Код обработки при выгрузке не выполняется. Минус в скорости: у нас вышло 14,5 с на файл вместе с запуском конфигуратора, сотня файлов - около 24 минут.
Второй путь - прочитать контейнер своим кодом. Функция ниже возвращает текст модуля объекта и ничего не подключает: файл для неё просто байты. Работает на сервере или в толстом клиенте, нужен доступ к файлу и к каталогу временных файлов.
// Текст модуля объекта из .epf/.erf без подключения обработки: файл читается как байты. // Нужен доступ к файлу и к временному каталогу (сервер или толстый клиент). Проверено на 8.3.27. Функция МодульОбъектаИзEPF(ПутьКФайлу) Экспорт Буфер = ПолучитьБуферДвоичныхДанныхИзДвоичныхДанных(Новый ДвоичныеДанные(ПутьКФайлу)); Если Буфер.Размер < 60 Или Буфер.ПрочитатьЦелое32(0) <> 2147483647 Тогда // FF FF FF 7F ВызватьИсключение "Это не контейнер 1С"; КонецЕсли; Для Каждого Элемент Из ОглавлениеКонтейнера(Буфер) Цикл Если Прав(Элемент.Имя, 2) <> ".0" Тогда Продолжить; КонецЕсли; Данные = РаспаковатьDeflate(Элемент.Данные); // Модуль объекта - вложенный контейнер из info и text, обычная форма - из form и module. // Управляемая форма лежит под таким же суффиксом, но это текст, а не контейнер. Если Данные.Размер < 4 Или Данные.ПрочитатьЦелое32(0) <> 2147483647 Тогда Продолжить; КонецЕсли; Для Каждого Вложенный Из ОглавлениеКонтейнера(Данные) Цикл Если Вложенный.Имя = "text" Тогда Текст = ПолучитьСтрокуИзБуфераДвоичныхДанных(Вложенный.Данные, "UTF-8"); Возврат ?(Лев(Текст, 1) = Символ(65279), Сред(Текст, 2), Текст); // без BOM КонецЕсли; КонецЦикла; КонецЦикла; Возврат Неопределено; // модуль пустой или обработку поставили без исходников КонецФункции // Оглавление лежит документом сразу за 16 байтами шапки файла: // по 12 байт на элемент - адрес заголовка, адрес данных, 7FFFFFFF. Функция ОглавлениеКонтейнера(Буфер) Оглавление = ДокументКонтейнера(Буфер, 16); Элементы = Новый Массив; Поз = 0; Пока Поз + 12 <= Оглавление.Размер Цикл Заголовок = ДокументКонтейнера(Буфер, Оглавление.ПрочитатьЦелое32(Поз)); Данные = ДокументКонтейнера(Буфер, Оглавление.ПрочитатьЦелое32(Поз + 4)); // Имя элемента - UTF-16 с 20-го байта заголовка, до нулевого символа Имя = ""; Смещение = 20; Пока Смещение + 1 < Заголовок.Размер И Заголовок.ПрочитатьЦелое16(Смещение) <> 0 Цикл Имя = Имя + Символ(Заголовок.ПрочитатьЦелое16(Смещение)); Смещение = Смещение + 2; КонецЦикла; Элементы.Добавить(Новый Структура("Имя, Данные", Имя, Данные)); Поз = Поз + 12; КонецЦикла; Возврат Элементы; КонецФункции // Документ - цепочка блоков. Заголовок блока 31 байт текстом: // ВК ПС, размер документа, размер блока, адрес следующего блока // (по 8 шестнадцатеричных цифр через пробел), ВК ПС. 7FFFFFFF - блок последний. Функция ДокументКонтейнера(Буфер, Адрес) Осталось = -1; // размер документа берём из первого блока Части = Новый Массив; Шаг = 0; Пока Осталось <> 0 И Адрес <> 2147483647 И Шаг < 100000 Цикл // страж от битого файла Шаг = Шаг + 1; Если Адрес < 0 Или Адрес + 31 > Буфер.Размер Тогда ВызватьИсключение "Файл повреждён: блок за концом файла"; КонецЕсли; Шапка = ПолучитьСтрокуИзБуфераДвоичныхДанных(Буфер.Прочитать(Адрес, 31)); Если Осталось < 0 Тогда Осталось = ЧислоИзШестнадцатеричнойСтроки("0x" + Сред(Шапка, 3, 8)); КонецЕсли; Взять = Мин(Осталось, ЧислоИзШестнадцатеричнойСтроки("0x" + Сред(Шапка, 12, 8)), Буфер.Размер - Адрес - 31); Если Взять <= 0 Тогда Прервать; КонецЕсли; Части.Добавить(Буфер.Прочитать(Адрес + 31, Взять)); Осталось = Осталось - Взять; Адрес = ЧислоИзШестнадцатеричнойСтроки("0x" + Сред(Шапка, 21, 8)); КонецЦикла; Если Части.Количество() = 0 Тогда Возврат Новый БуферДвоичныхДанных(0); КонецЕсли; Возврат СоединитьБуферыДвоичныхДанных(Части); КонецФункции // Распаковщика deflate в платформе нет, а ЧтениеZipФайла его понимает. // Поток заворачивается в ZIP из одного файла с CRC 0 и размером с запасом: // платформа ругается на контрольную сумму, но файл к этому моменту уже записан. Функция РаспаковатьDeflate(Сжатое) Разделитель = ПолучитьРазделительПути(); Каталог = КаталогВременныхФайлов() + Строка(Новый УникальныйИдентификатор); СоздатьКаталог(Каталог); Объявлено = Сжатое.Размер * 40 + 65536; Имя = ПолучитьБуферДвоичныхДанныхИзСтроки("d", "UTF-8"); Локальный = Новый БуферДвоичныхДанных(30); Локальный.ЗаписатьЦелое32(0, 67324752); // PK 03 04 Локальный.ЗаписатьЦелое16(4, 20); Локальный.ЗаписатьЦелое16(8, 8); // метод deflate Локальный.ЗаписатьЦелое32(18, Сжатое.Размер); Локальный.ЗаписатьЦелое32(22, Объявлено); Локальный.ЗаписатьЦелое16(26, 1); // длина имени "d" Центральный = Новый БуферДвоичныхДанных(46); Центральный.ЗаписатьЦелое32(0, 33639248); // PK 01 02 Центральный.ЗаписатьЦелое16(4, 20); Центральный.ЗаписатьЦелое16(6, 20); Центральный.ЗаписатьЦелое16(10, 8); Центральный.ЗаписатьЦелое32(20, Сжатое.Размер); Центральный.ЗаписатьЦелое32(24, Объявлено); Центральный.ЗаписатьЦелое16(28, 1); Конец = Новый БуферДвоичныхДанных(22); Конец.ЗаписатьЦелое32(0, 101010256); // PK 05 06 Конец.ЗаписатьЦелое16(8, 1); Конец.ЗаписатьЦелое16(10, 1); Конец.ЗаписатьЦелое32(12, 47); Конец.ЗаписатьЦелое32(16, 31 + Сжатое.Размер); Части = Новый Массив; Части.Добавить(Локальный); Части.Добавить(Имя); Части.Добавить(Сжатое); Части.Добавить(Центральный); Части.Добавить(Имя); Части.Добавить(Конец); ПутьZip = Каталог + Разделитель + "z.zip"; ПолучитьДвоичныеДанныеИзБуфераДвоичныхДанных(СоединитьБуферыДвоичныхДанных(Части)).Записать(ПутьZip); Попытка Чтение = Новый ЧтениеZipФайла(ПутьZip); Попытка Чтение.ИзвлечьВсе(Каталог, РежимВосстановленияПутейФайловZIP.НеВосстанавливать); Исключение // ожидаемо: "Ошибка контрольной суммы", судим по файлу КонецПопытки; Чтение.Закрыть(); Исключение // архив не открылся: ниже вернётся пустой буфер КонецПопытки; Файл = Новый Файл(Каталог + Разделитель + "d"); Результат = ?(Файл.Существует(), ПолучитьБуферДвоичныхДанныхИзДвоичныхДанных(Новый ДвоичныеДанные(Файл.ПолноеИмя)), Новый БуферДвоичныхДанных(0)); УдалитьФайлы(Каталог); Если Результат.Размер >= Объявлено Тогда ВызватьИсключение "Запаса размера не хватило, увеличьте множитель"; КонецЕсли; Возврат Результат; КонецФункции
Единственное неочевидное место - распаковка. Отдельной функции для deflate в платформе нет, но ЧтениеZipФайла его понимает. Поэтому сжатый поток заворачивается в ZIP-архив из одного файла. Контрольную сумму посчитать нечем, и платформа честно ругается "Ошибка контрольной суммы", только файл к этому моменту уже распакован целиком. Это поведение нигде не описано, мы его проверяли на 8.3.27, и после обновления платформы его стоит перепроверить одним прогоном. Несжатый размер в обёртке надо объявить с запасом: если объявить меньше настоящего, файл обрежется ровно по нему. В наших файлах модули сжаты в 5-6 раз, так что множитель 40 с большим запасом, а если его не хватит, функция скажет об этом исключением.
Мы собрали этот код во внешнюю обработку и прогнали на семи файлах. Пять настоящих, включая один .erf, от образца с меткой в теле модуля (13 строк) до обработки на 4 770 строк: каждый модуль прочитан за 2-7 мс. Метки на диске после прогона нет. Два битых файла (случайные байты и обрезанная наполовину обработка) дали понятное исключение вместо зависания. Дальше с текстом можно искать ЗапуститьПриложение, COMОбъект, HTTPСоединение и Выполнить из переменной, сравнивать версии построчно, класть в Git. Для поиска опасных вызовов читайте и модули форм: функция для краткости берёт только модуль объекта, а модуль формы лежит рядом, как описано выше.
Одна оговорка про хеш из первого раздела. Обработка, пересохранённая в конфигураторе без единой правки, получит другой хеш. Для каталога это новая версия, поэтому версии одной обработки связывайте по имени из главного описания, а хеш оставьте ключом конкретного файла.
Обработки, которые живут в базах
Часть обработок на диске не лежит вовсе: они в справочнике дополнительных отчётов и обработок рабочих баз, и там бывают версии, которых нет больше нигде. Найти их можно по форме данных, без привязки к конфигурации: справочник, у которого есть реквизит типа ХранилищеЗначения, и в хранилище лежат байты, начинающиеся с FF FF FF 7F. Так находится и справочник БСП (реквизит ХранилищеОбработки), и старый справочник ВнешниеОбработки в конфигурациях без БСП. Одна грабля: у групп иерархического справочника хранилище приходит Null. Наша первая проверка спотыкалась на первой же группе и решала, что обработок в справочнике нет. Строки с Null надо пропускать и смотреть дальше.
В базах с БСП сбор вешается на расписание штатно, через карточку дополнительной обработки. В базах без БСП остаётся планировщик Windows и запуск из командной строки:
"<путь к 1cv8.exe>" ENTERPRISE /S "<сервер 1С>\<имя базы>" /N "<пользователь>" /P "%ONEC_PWD%" /DisableStartupDialogs /DisableStartupMessages /Execute "<путь к обработке>.epf" /C "Sbor;Lib=<папка библиотеки>" /Out "%TEMP%\sbor.log"
Эта команда только обвязка. Она запускает 1С:Предприятие, открывает в нём внешнюю обработку ключом /Execute и передаёт ей строку /C. Обработка в ПриОткрытии формы читает ПараметрЗапуска, видит "Sbor", сама читает справочник, копирует файлы в папку, пишет итог в журнал регистрации и закрывает 1С. Вся логика внутри .epf. Сама команда ни справочник не читает, ни файлы не копирует, без обработки в /Execute ей нечего запускать. Подставить в /Execute можно любую свою обработку, которая так же разбирает ПараметрЗапуска. Пароль лучше держать в переменной окружения. У пользователя для такого запуска придётся выключить защиту от опасных действий, иначе платформа остановится на предупреждении, поэтому заведите под сбор отдельного пользователя с правом только читать справочник обработок. Путь к папке должен видеть сервер 1С: в клиент-серверной базе это сетевой путь с правом записи для службы сервера.
Где это собрано вместе
Всё описанное выше мы собрали в одну обработку: каталог с памятью по хешу, описание в безопасном режиме или разбором контейнера, сбор из баз по расписанию, выгрузка списка в HTML и XLSX. Файлы она не удаляет, данные базы не меняет.
Обработка из этой статьи: Библиотекарь внешних обработок 1С.
Другие наши инструменты:
- Анализ кода внешних обработок 1С - техдолг, производительность и находки с подсветкой по каждой обработке.
- Проверка внешних обработок и печатных форм при обновлении типовой - какие вызванные обработками методы типовой пропадут в новом релизе.
- ИИ-анализ кода 1С - движок находит место, модель объясняет и предлагает правку.
Вопрос к тем, кто дочитал
Где у вас живут внешние обработки: в папке, в Git, в справочнике каждой базы или во всех трёх местах сразу? И как вы узнаёте, что в базе стоит не та версия, что лежит на диске?
Вступайте в нашу телеграмм-группу Инфостарт
Вы можете заказать платную адаптацию этой статьи под ваши задачи на «Бирже заказов».
- 0% комиссии — оплата напрямую исполнителю;
- Исполнители любого масштаба — от отдельных специалистов до команд под проект;
- Прямой обмен контактами между заказчиком и исполнителем;
- Безопасная сделка — при необходимости;
- Рейтинги, кейсы и прозрачная система откликов.