Расширение и настоящий тип файла во встроенном языке 1С

· 9 мин

Содержание

Расширение файла в 1С получают одной строкой: Новый Файл(ПолноеИмя).Расширение. Свойство вернёт часть имени после последней точки вместе с точкой, например .pdf. Сам файл для этого существовать не обязан: платформа просто разбирает строку пути.

Этого хватает, когда имени можно доверять. Если файл пришёл от пользователя, из почты или по HTTP, расширение может врать или отсутствовать. Тогда тип файла определяют по содержимому — по первым байтам, — и по заголовкам ответа сервера.

Как получить расширение файла через объект Файл

Объект Файл доступен и на клиенте, и на сервере. Конструктор принимает полный путь или просто имя.

Файл = Новый Файл("C:\Выгрузка\Акт сверки.pdf");

Сообщить(Файл.Расширение);         // .pdf
Сообщить(Файл.ИмяБезРасширения);   // Акт сверки
Сообщить(Файл.Имя);                // Акт сверки.pdf
Сообщить(Файл.Путь);               // C:\Выгрузка\

Что полезно знать про эти свойства:

  • 📌 Расширение содержит точку. Сравнение Файл.Расширение = "pdf" всегда даст Ложь.
  • 📌 Регистр сохраняется как в имени: Скан.PDF вернёт .PDF. Перед сравнением приводите к одному регистру через НРег().
  • 📌 Если точки в имени нет, свойство вернёт пустую строку.
  • 📌 Свойства считаются по строке. Проверка наличия файла — отдельно, методом Существует().

Новый Файл() можно создать на сервере по пути, который пользователь выбрал на своём компьютере. Свойства Имя и Расширение разберутся верно, а вот Существует() проверит уже диск сервера.

Расширение из строки: своя функция без объекта Файл

Объект Файл неудобен в общих модулях, где путь приходит из внешней системы, или когда нужно расширение без точки и сразу в нижнем регистре. Тогда пишут функцию на строковых методах. СтрНайти() с поиском с конца работает в платформе 8.3.6 и новее.

// Возвращает расширение без точки в нижнем регистре.
// Пустая строка, если расширения нет.
Функция РасширениеФайла(ИмяФайла) Экспорт

    // Оставляем только имя: отрезаем путь с любым разделителем
    Имя = СтрЗаменить(ИмяФайла, "\", "/");
    ПозицияСлеша = СтрНайти(Имя, "/", НаправлениеПоиска.СКонца);
    Если ПозицияСлеша > 0 Тогда
        Имя = Сред(Имя, ПозицияСлеша + 1);
    КонецЕсли;

    ПозицияТочки = СтрНайти(Имя, ".", НаправлениеПоиска.СКонца);

    // 0 — точки нет; 1 — имя вида ".env"; точка в конце — расширения нет
    Если ПозицияТочки <= 1 Или ПозицияТочки = СтрДлина(Имя) Тогда
        Возврат "";
    КонецЕсли;

    Возврат НРег(Сред(Имя, ПозицияТочки + 1));

КонецФункции

Отрезать путь нужно обязательно. Иначе в строке C:\Обмен.2024\выгрузка функция найдёт точку в имени каталога и вернёт 2024\выгрузка.

Вариант через СтрРазделить(Имя, ".") и последний элемент массива короче, но с ловушкой. Для имени без точки массив состоит из одного элемента — самого имени, и оно ошибочно станет «расширением». Проверяйте Части.Количество() > 1.

В конфигурациях на Библиотеке стандартных подсистем есть готовая функция разбора полного имени файла в общем модуле ОбщегоНазначенияКлиентСервер (поищите по слову «РазложитьПолноеИмяФайла»). Если она есть в вашем релизе, используйте её вместо своей.

Имена с несколькими точками и другие пограничные случаи

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

Имя файлаФайл.РасширениеЧто на самом деле
Отчет.v2.final.xlsx.xlsxвсё верно
backup.tar.gz.gzсоставное расширение tar.gz
Счет.pdf.exe.exeисполняемый файл под видом PDF
READMEпустая строкарасширения нет
Скан.JPG.JPGрегистр нужно нормализовать

Составные расширения распознают по списку известных окончаний, а не по второй точке. Иначе Отчет.v2.xlsx превратится в «v2.xlsx».

Функция ПолноеРасширение(ИмяФайла) Экспорт

    ИмяНРег = НРег(ИмяФайла);
    Для Каждого Составное Из СтрРазделить("tar.gz,tar.bz2,tar.xz", ",") Цикл
        Если СтрЗаканчиваетсяНа(ИмяНРег, "." + Составное) Тогда
            Возврат Составное;
        КонецЕсли;
    КонецЦикла;

    Возврат РасширениеФайла(ИмяФайла);

КонецФункции

Случай Счет.pdf.exe — повод для проверки безопасности. Файл с двойным расширением, где последнее — exe, bat, cmd, js или vbs, не открывайте и не сохраняйте в общую папку. Почтовые вложения с такими именами — классический способ доставки вредоносной программы.

Определение типа файла по содержимому: сигнатуры

У большинства форматов первые байты фиксированы. Их называют сигнатурой, или «магическим числом». По ним видно, что Договор.pdf на самом деле картинка, а выгрузка.xls — HTML-страница.

ФорматПервые байты (hex)Как читается
PDF25 50 44 46%PDF
PNG89 50 4E 47 0D 0A 1A 0A.PNG и служебные байты
JPEGFF D8 FF—
GIF47 49 46 38GIF8
ZIP, а также docx, xlsx, odt50 4B 03 04PK
Старые doc и xlsD0 CF 11 E0 A1 B1 1A E1контейнер OLE
RAR52 61 72 21Rar!
7z37 7A BC AF 27 1C7z и служебные байты
GZIP1F 8B—
BMP42 4DBM
XML в UTF-8 с BOMEF BB BF 3C 3FBOM, затем <?

Читать весь файл в память не нужно, достаточно восьми байт. Объект ЧтениеДанных работает с двоичными данными, потоком или файлом на диске.

Функция ТипПоСигнатуре(ДвоичныеДанные) Экспорт

    Чтение = Новый ЧтениеДанных(ДвоичныеДанные);
    Начало = Чтение.Прочитать(8).ПолучитьДвоичныеДанные();
    Чтение.Закрыть();

    Hex = ВРег(ПолучитьHexСтрокуИзДвоичныхДанных(Начало));

    Если СтрНачинаетсяС(Hex, "25504446") Тогда
        Возврат "pdf";
    ИначеЕсли СтрНачинаетсяС(Hex, "89504E47") Тогда
        Возврат "png";
    ИначеЕсли СтрНачинаетсяС(Hex, "FFD8FF") Тогда
        Возврат "jpg";
    ИначеЕсли СтрНачинаетсяС(Hex, "47494638") Тогда
        Возврат "gif";
    ИначеЕсли СтрНачинаетсяС(Hex, "504B0304") Тогда
        Возврат "zip"; // уточнить: docx, xlsx или архив
    ИначеЕсли СтрНачинаетсяС(Hex, "D0CF11E0") Тогда
        Возврат "ole"; // doc, xls или другой старый формат Office
    ИначеЕсли СтрНачинаетсяС(Hex, "52617221") Тогда
        Возврат "rar";
    ИначеЕсли СтрНачинаетсяС(Hex, "377ABCAF") Тогда
        Возврат "7z";
    ИначеЕсли СтрНачинаетсяС(Hex, "1F8B") Тогда
        Возврат "gz";
    КонецЕсли;

    Возврат "";

КонецФункции

Пустая строка значит «сигнатура неизвестна». Так будет для текстовых форматов: txt, csv, json и XML без BOM у сигнатуры нет. Их узнают по первым символам: <?xml, {, [, или доверяют расширению.

Как отличить docx и xlsx от обычного архива

Офисные форматы Open XML — это ZIP с известной структурой. Загляните в оглавление архива: документ Word содержит word/document.xml, книга Excel — xl/workbook.xml.

Функция УточнитьТипZip(ПолноеИмяФайла) Экспорт

    Архив = Новый ЧтениеZipФайла(ПолноеИмяФайла);
    Результат = "zip";

    Для Каждого Элемент Из Архив.Элементы Цикл
        ИмяВАрхиве = НРег(СтрЗаменить(Элемент.ПолноеИмя, "\", "/"));
        Если ИмяВАрхиве = "word/document.xml" Тогда
            Результат = "docx";
            Прервать;
        ИначеЕсли ИмяВАрхиве = "xl/workbook.xml" Тогда
            Результат = "xlsx";
            Прервать;
        КонецЕсли;
    КонецЦикла;

    Архив.Закрыть();
    Возврат Результат;

КонецФункции

Разделитель в именах элементов приводим к /, чтобы код одинаково работал на сервере под Windows и Linux.

Расширение файла из HTTP-запроса и внешних источников

Файл, скачанный через HTTPСоединение, часто приходит без понятного имени. Источников подсказки три, проверяйте их по порядку.

  1. Заголовок Content-Disposition. Если сервер отдаёт вложение, там будет filename="report.pdf" или filename*=UTF-8''... с кириллицей в URL-кодировке.
  2. Путь в адресе. /files/act-15.pdf?token=abc — расширение есть, но сначала отрежьте всё после ? и #.
  3. Заголовок Content-Type. MIME-тип переводится в расширение по таблице ниже.
Content-TypeРасширение
application/pdfpdf
image/jpegjpg
image/pngpng
application/zipzip
application/xml, text/xmlxml
application/jsonjson
text/csvcsv
application/vnd.openxmlformats-officedocument.spreadsheetml.sheetxlsx
application/vnd.openxmlformats-officedocument.wordprocessingml.documentdocx
application/octet-streamничего не говорит, смотрите сигнатуру

Имена заголовков разные серверы пишут в разном регистре, а Ответ.Заголовки — соответствие с точным совпадением ключа. Поэтому заголовок ищут перебором:

Функция ЗначениеЗаголовка(Заголовки, ИмяЗаголовка)

    Для Каждого КлючИЗначение Из Заголовки Цикл
        Если НРег(КлючИЗначение.Ключ) = НРег(ИмяЗаголовка) Тогда
            Возврат КлючИЗначение.Значение;
        КонецЕсли;
    КонецЦикла;
    Возврат "";

КонецФункции

Функция РасширениеИзАдреса(АдресРесурса)

    Путь = АдресРесурса;
    Для Каждого Символ Из СтрРазделить("?,#", ",") Цикл
        Позиция = СтрНайти(Путь, Символ);
        Если Позиция > 0 Тогда
            Путь = Лев(Путь, Позиция - 1);
        КонецЕсли;
    КонецЦикла;
    Возврат РасширениеФайла(Путь);

КонецФункции

Сам ответ получают так: Ответ = Соединение.Получить(Запрос), тело — Ответ.ПолучитьТелоКакДвоичныеДанные(). Эти двоичные данные сразу можно передать в ТипПоСигнатуре().

Если Content-Type равен text/html, а вы ждали PDF, скорее всего сервер вернул страницу ошибки или авторизации. Проверьте код состояния Ответ.КодСостояния до сохранения файла.

Файл от пользователя: клиент, сервер и веб-клиент

Когда пользователь выбирает файл в форме, расширение берут из имени, которое вернул диалог. Сам файл при этом передают на сервер через временное хранилище.

&НаКлиенте
Процедура ЗагрузитьФайл(Команда)
    Оповещение = Новый ОписаниеОповещения("ПослеПомещенияФайла", ЭтотОбъект);
    НачатьПомещениеФайла(Оповещение, , , Истина, УникальныйИдентификатор);
КонецПроцедуры

&НаКлиенте
Процедура ПослеПомещенияФайла(Результат, Адрес, ВыбранноеИмяФайла, ДополнительныеПараметры) Экспорт
    Если Не Результат Тогда
        Возврат; // пользователь отказался от выбора
    КонецЕсли;
    ОбработатьФайлНаСервере(Адрес, ВыбранноеИмяФайла);
КонецПроцедуры

&НаСервере
Процедура ОбработатьФайлНаСервере(Адрес, ИмяФайла)
    Расширение = РасширениеФайла(ИмяФайла);
    Данные = ПолучитьИзВременногоХранилища(Адрес);
    ТипПоДанным = ТипПоСигнатуре(Данные);
    // дальше сравниваем Расширение и ТипПоДанным
КонецПроцедуры

В новых релизах платформы есть асинхронные методы помещения файлов. Имя файла там приходит в свойствах описания помещённого файла, логика та же: имя — для расширения, адрес — для данных.

В веб-клиенте браузер не отдаёт полный путь к файлу на компьютере пользователя. Имя с расширением при этом доступно, так что разбор по имени работает.

Чтобы сразу ограничить выбор, задайте фильтр диалога: "Документы PDF (*.pdf)|*.pdf|Все файлы (*.*)|*.*". Но фильтр — это удобство, а не защита: пользователь переключит его на «Все файлы».

Как сменить расширение и проверить, допустимо ли оно

Сменить расширение — значит переименовать файл . Новое имя собирают из пути и имени без расширения, а переносят методом ПереместитьФайл().

Процедура СменитьРасширение(ПолноеИмя, НовоеРасширение)
    Файл = Новый Файл(ПолноеИмя);
    НовоеИмя = Файл.Путь + Файл.ИмяБезРасширения + "." + НовоеРасширение;
    ПереместитьФайл(ПолноеИмя, НовоеИмя);
КонецПроцедуры

Переименование не меняет формат. Файл отчет.xls, переименованный в отчет.xlsx, Excel откроет с ошибкой. Для конвертации табличного документа используйте его запись в нужном формате, а не смену имени.

Для временного файла расширение задаётся сразу: ПолучитьИмяВременногоФайла("xml"). Параметр передают без точки.

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

Функция РасширениеДопустимо(ИмяФайла, ДвоичныеДанные) Экспорт

    Разрешенные = СтрРазделить("pdf,jpg,jpeg,png,xlsx,docx,xml", ",");
    Расширение = РасширениеФайла(ИмяФайла);
    Если Разрешенные.Найти(Расширение) = Неопределено Тогда
        Возврат Ложь;
    КонецЕсли;

    // Для двоичных форматов сверяем имя с содержимым
    ПоДанным = ТипПоСигнатуре(ДвоичныеДанные);
    Если Расширение = "pdf" И ПоДанным <> "pdf" Тогда
        Возврат Ложь;
    КонецЕсли;
    Возврат Истина;

КонецФункции

Сверку с сигнатурой расширяйте под свои форматы. Для jpeg ожидайте jpg, для docx и xlsx — zip с уточнением по структуре.

Перед приёмом файла в базу
Выполнено: 0 / 5

Подводные камни и ограничения платформы

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

  • 🐞 Сравнение с точкой и без. Файл.Расширение возвращает .pdf, своя функция — pdf. Смешение двух подходов в одном модуле — частая причина, по которой проверка «никогда не срабатывает».
  • 🐞 Регистр на сервере Linux. В Linux Акт.PDF и Акт.pdf — два разных файла. Код, который ищет файл по имени с «исправленным» регистром, не найдёт его.
  • 🐞 Точка в имени каталога. Строковый поиск без отрезания пути находит точку в папке Обмен.2024.
  • 🐞 Пробелы и точки в конце имени. Windows отбрасывает их при сохранении, поэтому Отчет.pdf. на диске станет Отчет.pdf. Перед разбором уберите хвост через СокрП().
  • 🐞 Недопустимые символы. Имя из внешнего источника может содержать \ / : * ? " < > |. Заменяйте их до записи на диск, иначе запись на диск или ПереместитьФайл() закончатся ошибкой.
  • 🐞 Диск клиента против диска сервера. Код на сервере видит файловую систему сервера. Путь C:\Users\..., полученный с клиента, на сервере не существует.

Для внешних обработок, подключаемых в безопасном режиме, работа с файловой системой может быть запрещена. Разбор имени при этом работает, а чтение файла — нет. Передавайте данные через временное хранилище.

Частые вопросы

Где хранится расширение у присоединённых файлов в типовых конфигурациях?
В справочниках присоединённых файлов на БСП расширение лежит в отдельном реквизите, без точки. Разбирать имя заново не нужно, достаточно прочитать этот реквизит.
Как узнать кодировку текстового файла, раз сигнатуры нет?
Надёжно — только по BOM: EF BB BF означает UTF-8, FF FE — UTF-16 LE. Файл без BOM может быть и в UTF-8, и в Windows-1251, тогда кодировку согласуют с источником данных.
Почему файл с расширением xls не открывается как табличный документ?
Часто это HTML или CSV, сохранённые сайтом под именем xls. Проверьте первые байты: у настоящего старого xls они начинаются с D0 CF 11 E0.
Можно ли определить расширение файла прямо в запросе к базе?
Удобнее сделать это в коде после выборки. В языке запросов нет поиска подстроки с конца, поэтому расширение из имени там выделять неудобно.