Məzmuna keç
Tez Base

Как хранить внешние обработки 1С, чтобы через год понимать, что в папке

· Dərc edilib:

Права и безопасность

Сколько у вас файлов с именем вида “Обработка1.epf”? А сколько копий одной и той же загрузки лежит в подпапках под разные базы? Обычно ответа нет ни на один вопрос, и это нормально: архив внешних обработок растёт годами, а разбирать его некогда. Проблема всплывает в неудобный момент. Заказчик просит вернуть печатную форму “как было в прошлом году”, и приходится открывать один файл за другим.

Ниже порядок, по которому мы раскладываем такой архив. Каждый шаг работает отдельно, код на встроенном языке приведён целиком и не требует никаких наших обработок.

Что ломается в обычном архиве

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

Шаг 1. Опознавать файл по содержимому

Единственное, что не меняется при переименовании и копировании, это сами байты. Значит, ключ записи в каталоге должен считаться из них. SHA-256 встроенный язык считает штатно:

Хеш = Новый ХешированиеДанных(ХешФункция.SHA256);
Хеш.ДобавитьФайл(ПолныйПуть);
Ключ = НРег(ПолучитьHexСтрокуИзДвоичныхДанных(Хеш.ХешСумма));

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

Это дёшево. На нашем наборе из 85 файлов (4,5 МБ) обход с подсчётом хешей занял 108 мс. На 595 файлах вышло 1,7 с. При повторных обходах хеш можно не пересчитывать у файлов, где не сменились размер и дата: повторный обход тех же 85 файлов шёл 89 мс.

Есть один подвох. Если открыть обработку в конфигураторе и сохранить без единой правки, байты всё равно поменяются, и хеш будет другой. Для каталога это новый файл. Чтобы видеть его как версию старого, версии связывают по имени объекта, которое записано внутри файла, а хеш оставляют ключом конкретного экземпляра.

Шаг 2. Узнать, что делает файл, и не выполнить его

Соблазн понятный: подключить обработку через ВнешниеОбработки.Подключить и прочитать её метаданные. Подвох в том, что при подключении исполняется тело модуля объекта, то есть всё, что написано вне процедур и функций. Мы это проверили на образце, который в теле модуля создаёт файл на диске. После обычного подключения файл появился, хотя ни одной формы мы не открывали.

Вариантов у разработчика четыре:

СпособКод выполняется?Что получаемМинус
обычное подключениеда, целикомвсе метаданныеможно запустить что угодно
подключение в безопасном режимеда, до первого запрещённого действияметаданные, если модуль собралсястарые обработки на обычных формах часто не собираются на сервере
выгрузка конфигуратором в файлынетисходники в формате XML-выгрузки14,5 с на файл с запуском конфигуратора
чтение контейнера своим кодомнеттекст модулейнадо знать формат файла

Безопасный режим включается третьим параметром, а флаг в описании защиты снимает вопрос пользователю на каждом файле:

Адрес = ПоместитьВоВременноеХранилище(Новый ДвоичныеДанные(ПолныйПуть), Новый УникальныйИдентификатор);
Защита = Новый ОписаниеЗащитыОтОпасныхДействий;
Защита.ПредупреждатьОбОпасныхДействиях = Ложь;
Имя = ВнешниеОбработки.Подключить(Адрес, , Истина, Защита); // Истина - безопасный режим

Наш образец в этом режиме падает на попытке записи с ошибкой “Установлен безопасный режим”, файла на диске нет. Но запрещены только действия: запись файлов, запуск программ, COM, интернет. Всё, что модуль успевает сделать до них, он делает. И подключение идёт на сервере, а там нет Предупреждение, Состояние и ДиалогВыбораФайла. Модуль старой обработки, который их вызывает, на сервере просто не скомпилируется, и описать её этим способом не выйдет.

Выгрузка конфигуратором надёжна и удобна для Git, но медленная. Команда для одного файла, нужна любая база, хоть пустая файловая:

"C:\Program Files\1cv8\<версия>\bin\1cv8.exe" DESIGNER /F "<папка пустой базы>" /DisableStartupDialogs /DumpExternalDataProcessorOrReportToFiles "<куда>\Обработка.xml" "<файл>.epf"

Модуль объекта окажется в Ext\ObjectModule.bsl, модули форм в Forms\<имя формы>\Ext\Form\Module.bsl. Сотня файлов при наших 14,5 с на штуку это около 24 минут.

Остаётся четвёртый путь, самый быстрый: разобрать файл самому.

Шаг 3. Прочитать контейнер .epf

Внешняя обработка и внешний отчёт хранятся в одном формате, это собственный контейнер платформы. По смыслу он похож на ZIP: оглавление и набор именованных элементов. Разбирается он так.

  1. Шапка, 16 байт. Начинается с FF FF FF 7F. Если этих байтов нет, дальше читать бессмысленно: файл битый или вообще не обработка.
  2. Документы из блоков. Каждый документ записан цепочкой блоков. Перед блоком стоит текстовый заголовок длиной 31 байт: перевод строки, три шестнадцатеричных числа по восемь цифр через пробел и снова перевод строки. Числа означают размер документа, размер этого блока и адрес следующего. 7FFFFFFF на месте адреса означает конец цепочки.
  3. Оглавление. Первый документ после шапки. На каждый элемент в нём 12 байт: где лежит заголовок элемента, где его данные, и завершающие 7FFFFFFF.
  4. Элемент. В заголовке, начиная с 20-го байта, имя в UTF-16. Данные верхнего уровня сжаты голым deflate, без обёртки архива.

Внутри распакованных данных могут лежать вложенные контейнеры того же формата, только уже несжатые. Что в них искать, видно по составу нашей обработки на обычных формах:

ЭлементСодержимое
rootссылка на главное описание объекта
главное описание (имя-идентификатор)имя, синоним, комментарий, списки реквизитов, табличных частей, форм и макетов
идентификатор формызаголовок формы, в первой строке в кавычках её имя
идентификатор формы с суффиксом .0обычная форма: вложенный контейнер с элементами form и module
идентификатор модуля с суффиксом .0модуль объекта: вложенный контейнер с элементами info и text
идентификатор модуля с суффиксом .1встроенная справка, HTML в base64
version, versions, copyinfoслужебное, для чтения кода не нужно

У управляемой формы элемент с суффиксом .0 не контейнер, а скобочный текст, и модуль формы лежит в нём строкой сразу после описания формы.

На этом мы споткнулись сами. Макет в контейнере выглядит ровно как форма: элемент с идентификатором и заголовком, где первая строка в кавычках это имя. Отличить их можно только по спискам в главном описании. Первая версия нашего разбора эти списки не читала и на наборе из 71 файла разошлась с метаданными, полученными через подключение, в 26 местах. После того как форму стали определять по списку форм, расхождений не осталось.

Ниже функция, которая возвращает текст модуля объекта. Обработку она не подключает, для неё файл это просто массив байтов. Нужен доступ к файлу и к каталогу временных файлов, то есть сервер или толстый клиент. Проверена на 8.3.27.

// Текст модуля объекта из .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 мс, файла-метки на диске нет. Два испорченных файла, набор случайных байтов и обработка, обрезанная пополам, закончились внятным исключением, без зависания.

Если модуля в контейнере нет, он пустой или обработку поставили без исходного кода. Читать там нечего, и в каталоге это стоит отметить отдельно.

Шаг 4. Найти опасное в тексте

С текстом модуля на руках опасные места ищутся поиском по словам. Что стоит отмечать в первую очередь:

  • ЗапуститьПриложение и КомандаСистемы: запуск внешних программ;
  • Новый COMОбъект: управление чем угодно на машине, где выполняется код;
  • HTTPСоединение, FTPСоединение, ИнтернетПочта: обмен с внешним миром;
  • Выполнить и Вычислить с переменной: код, которого в файле не видно;
  • ПодключитьВнешнююКомпоненту: чужой машинный код внутри процесса 1С;
  • УстановитьПривилегированныйРежим(Истина): работа в обход прав пользователя;
  • запись и удаление объектов базы.

Функция выше для краткости берёт только модуль объекта. Для поиска опасного читайте и модули форм: у обычной формы это элемент module во вложенном контейнере, у управляемой строка внутри текста формы. Само по себе слово в коде ещё не приговор, загрузка из интернета для того и пишется. Смысл в том, чтобы знать о нём до запуска.

Шаг 5. Забрать то, что живёт только в базах

Часть обработок на диске не найти: их когда-то загрузили в справочник дополнительных отчётов и обработок, и последняя версия лежит только там. Привязываться к конфигурации не обязательно, справочник с обработками узнаётся по данным. У него есть реквизит типа ХранилищеЗначения, и в хранилище лежат байты, которые начинаются с FF FF FF 7F. Так находится и справочник БСП с реквизитом ХранилищеОбработки, и старый справочник ВнешниеОбработки в конфигурациях без БСП.

Если справочник иерархический, у групп хранилище возвращает Null. Наша первая проверка смотрела на первую строку, попадала в группу и делала вывод, что обработок в справочнике нет. Пустые хранилища надо пропускать и идти дальше.

Как поставить сбор на расписание, зависит от базы. С БСП всё штатно: внешняя обработка регистрируется как дополнительная с серверной командой, расписание задаётся в её карточке. Без БСП остаётся планировщик Windows и запуск 1С из командной строки с параметром:

"<путь к 1cv8.exe>" ENTERPRISE /S "<сервер 1С>\<имя базы>" /N "<пользователь>" /P "%ONEC_PWD%" /DisableStartupDialogs /DisableStartupMessages /Execute "<путь к обработке>.epf" /C "Sbor;Lib=<папка библиотеки>" /Out "%TEMP%\sbor.log"

Строка только запускает 1С:Предприятие с внешней обработкой и передаёт ей текст после /C. Всю работу делает сама обработка: в обработчике открытия формы она читает ПараметрЗапуска, видит команду, обходит справочник, раскладывает файлы по папке, пишет итог в журнал регистрации и закрывает сеанс. Подойдёт любая своя обработка, которая так же разбирает параметр.

Три практических условия для такого запуска:

  1. Отдельный пользователь с правом только читать справочник обработок. У него придётся выключить защиту от опасных действий, иначе запуск остановится на предупреждении.
  2. Пароль в переменной окружения, а не в тексте задания планировщика.
  3. Папка, которую видит сервер. В клиент-серверной базе файлы пишет служба сервера 1С, значит нужен сетевой путь с правом записи для её учётной записи.

Где держать результат

Описание, собранное по шагам выше, удобно хранить файлом прямо в корне архива, рядом с самими обработками. Тогда оно переезжает вместе с папкой, его можно положить в Git или облачный диск, и оно не зависит от конкретной базы. Если команда держит в репозитории ещё и конфигурацию с расширениями, описание едет туда же. Как поставить такой процесс команде, у которой пока только хранилище конфигурации, рассказываем на странице внедрения git в команде 1С. Для коллег, у которых нет 1С под рукой, пригодится выгрузка в HTML: один файл с поиском открывается в любом браузере.

Всё это мы собрали в одну обработку: Библиотекарь внешних обработок 1С. Она держит каталог по хешу в library.json, описывает файлы в безопасном режиме или разбором контейнера, ставит пометки риска, собирает обработки из справочников баз по расписанию и выгружает каталог в HTML и XLSX. Когда каталог собран, код самих обработок можно прогнать через анализатор кода, а перед обновлением типовой проверить, какие вызовы из обработок сломает новый релиз.

Если баз много и разбирать их обработки некогда, напишите нам в WhatsApp: соберём обработки со всех баз в один каталог и отметим те, что требуют внимания.

Внешние обработки разбросаны по папкам и базам, и никто не знает, какая версия где стоит

Соберём обработки со всех ваших баз в один каталог, опишем их и отметим файлы с опасными вызовами

Qiyməti
700 000 ₸-dənvergilərsiz; son qiymət - bazalar parkının auditindən sonra
Müddət
от 2 дней

Yazmaq tezdir? Özünüz ölçün

Библиотекарь внешних обработок 1С: каталог папки и сбор из всех баз

Emalı aç

INFOSTART TECH EVENT 2026: конференция для 1с-специалистов. если собираетесь, регистрируйтесь по нашей ссылке. Регистрация →

Bunu da oxuyun

Защита тиражной обработки 1С от копирования: три уровня и где у каждого потолок

Файл .epf копируется мышкой, и договор это не останавливает. Защиту обычно начинают с проверки ИНН в коде - и зря: она снимается за минуту, потому что логика лежит рядом открытым текстом. Разложили защиту на три уровня по убыванию дыр: от бесполезной проверки ИНН через шифрование тела процедуры под ИНН покупателя до настоящей лицензии на асимметричной криптографии. По каждому - что он реально закрывает, чего не закрывает и когда его стоит брать.

Шифровальщик зашифровал базы 1С вместе с бэкапами: порядок действий, когда ключа нет

Утром базы 1С не открываются, рядом с каждым файлом записка с требованием выкупа, сетевые бэкапы зашифрованы тоже. Разбираем по шагам, что делать: что не трогать в первый час, как по самому файлу понять, сколько зашифровано, в каком порядке искать копии и как собрать базу 1С из уцелевших страниц бэкапа, если шифровальщик испортил только начало. На примере реальной атаки: бухгалтерская база 23 ГБ собрана за 6 часов.

База 1С не открывается после обновления конфигурации: порядок действий, когда копии нет

Конфигуратор пишет "При обновлении данных, после последней реструктуризации, произошла критическая ошибка", пользователи в базу не попадают, копии до обновления нет. Разбираем по шагам: что сохранить первым, как по журналу регистрации найти этап, на котором упало, какие действия только добавляют ошибок, когда бросать починку старой базы и как через ibcmd выгрузить .dt из базы, которая не открывается, собрать новую и тихо подменить её в кластере. В конце - задания резервного копирования, после которых такая авария превращается в откат на минуту до обновления.