Чтение пакетов .ospx и манифестов opm-metadata.xml для OneScript: разбор
и запись манифеста, безопасное извлечение содержимого пакета и валидация.
Библиотека рассчитана на приёмную сторону — хаб, зеркало, линтер, — которая
разбирает чужой архив и не имеет права ему верить.
Публичный интерфейс — один модуль-фасад ПакетыOspx. Модель манифеста,
парсер, сериализатор, читатель и валидатор лежат в src/internal и меняются
без оглядки на обратную совместимость.
Пакет = ПакетыOspx.ПрочитатьПакет("semver-1.1.1.ospx"); // манифест + README + файлы
Итог = ПакетыOspx.ПроверитьПакет("подозрительный.ospx"); // разбор без исключенийЗависимостей времени выполнения нет — только стандартная библиотека OneScript.
opm install ospx-tools.ЗависитОт("ospx-tools", "0.2.0")Zip-архив, в корне которого лежат ровно два файла:
opm-metadata.xml— манифест пакета (пространство имёнhttp://oscript.io/schemas/opm-metadata/1.0): элементыname,version,author,author-email,description,repo-address,engine-version,manifest-version, повторяющиеся<depends-on name="…" version="…" version-max="…" dev="true|false"/>и<executable>путь</executable>;content.zip— вложенный zip с файлами пакета:lib.configв корне, исходники и, если автор включил его в поставку,README.md.
#Использовать ospx-tools
Пакет = ПакетыOspx.ПрочитатьПакет("semver-1.1.1.ospx");
Сообщить(Пакет.Манифест().Имя()); // semver
Сообщить(Пакет.Манифест().Версия()); // 1.1.1
Если Пакет.ЕстьREADME() Тогда
Сообщить(Пакет.ТекстREADME());
КонецЕсли;
Итог = ПакетыOspx.ПроверитьПакет("подозрительный.ospx");
Если Не Итог.Корректен() Тогда
Сообщить(Итог.ОписаниеОшибок());
КонецЕсли;Чтение бросает исключение на любом отказе, проверка — не бросает никогда:
ПроверитьПакет ловит отказ чтения и кладёт его в список ошибок. Первое —
для своих файлов, второе — для чужих.
| Метод | Назначение |
|---|---|
ПрочитатьПакет(Путь, Ограничения = Неопределено) |
манифест, README и список файлов content.zip |
ПрочитатьМанифестПакета(Путь, Ограничения = Неопределено) |
только манифест — content.zip не разбирается |
НовыеОграничения() |
лимиты распаковки со значениями по умолчанию |
НовыйМанифест(Имя = "", Версия = "") |
пустой манифест |
ПрочитатьМанифестИзСтроки(ТекстXML) |
разбор opm-metadata.xml из строки (BOM допускается) |
ЗаписатьМанифестВСтроку(Манифест) |
сериализация в XML формата opm |
ЗаписатьМанифестВФайл(Манифест, Путь) |
то же в файл, UTF-8 |
ПроверитьПакет(Путь, Ограничения = Неопределено) |
целостность архива + корректность манифеста |
ПроверитьМанифест(Манифест) |
обязательные поля, имена, версии, дубли зависимостей |
ИмяПакетаКорректно(Имя) |
латиница, цифры, -, _, .; первый символ — буква или цифра; до 100 символов |
ВерсияПакетаКорректна(Версия) |
semver до 4 числовых компонент, prerelease/build, SNAPSHOT |
Пакет (ПрочитатьПакет): Манифест(), ЕстьREADME(), ТекстREADME(),
Файлы() — пути внутри content.zip.
Манифест: Имя(), Версия(), Автор(), АдресАвтора(), Описание(),
АдресРепозитория(), ВерсияДвижка(), ВерсияМанифеста(), Зависимости(),
Зависимость(Имя) — поиск без учёта регистра, ИсполняемыеФайлы().
Зависимость: Имя(), Версия(), МаксимальнаяВерсия(), ДляРазработки().
Методы Установить… и Добавить… возвращают сам манифест и допускают цепочки:
Манифест = ПакетыOspx.НовыйМанифест("mylib", "0.1.0")
.УстановитьОписание("Моя библиотека")
.УстановитьВерсиюДвижка("2.0.0")
.ДобавитьЗависимость("logos", "1.7.1")
.ДобавитьЗависимость("asserts", "1.6.0", , Истина); // dev-зависимость
XML = ПакетыOspx.ЗаписатьМанифестВСтроку(Манифест);Результат проверки: Корректен(), Ошибки() — массив строк,
ОписаниеОшибок() — все ошибки одной строкой. Проверка собирает все
найденные ошибки, а не останавливается на первой.
Ограничения = ПакетыOspx.НовыеОграничения()
.УстановитьМаксимальныйОбщийРазмер(10 * 1024 * 1024)
.УстановитьМаксимальноеКоличествоФайлов(500);
Пакет = ПакетыOspx.ПрочитатьПакет("подозрительный.ospx", Ограничения);| Лимит | По умолчанию | Метод |
|---|---|---|
| Суммарный размер распакованного | 256 МБ | УстановитьМаксимальныйОбщийРазмер |
| Количество файлов в архиве | 10 000 | УстановитьМаксимальноеКоличествоФайлов |
Глубина путей внутри content.zip |
32 сегмента | УстановитьМаксимальнуюГлубинуПутей |
Размер opm-metadata.xml |
1 МБ | УстановитьМаксимальныйРазмерМанифеста |
Размер README.md |
2 МБ | УстановитьМаксимальныйРазмерREADME |
| Потолок транзиторной записи на диск | 256 МБ | УстановитьМаксимальнуюТранзиторнуюЗапись |
Превышение любого лимита прерывает чтение исключением
«Превышено ограничение распаковки: …».
Заявленным размерам из central directory библиотека не доверяет: их пишет тот, кто собрал архив. Защита работает эшелонами:
- До извлечения — гейты по заявленному размеру, количеству файлов и глубине путей. Отсекают честно заявленную бомбу.
- Перед извлечением — гейт по сжатому размеру записи. Сжатый размер
занизить нельзя: он физически ограничен размером самого файла. Для манифеста
и README требуется
сжатый размер × 1032(физический максимум расширения deflate)<= МаксимальнаяТранзиторнаяЗапись. Гейт ограничивает возможный объём записи на диск, а не допуск файла, — поэтому честно сжатые большие файлы проходят, а решение о допуске принимает шаг 3. Дляcontent.zipсжатый размер не должен превышать общий лимит. - После извлечения — сверка фактического размера с лимитом, fail-closed: отсутствие извлечённого файла — тоже ошибка. Временный каталог вычищается всегда: и на успехе, и на любом отказе.
Дополнительно читатель отвергает небезопасные пути записей (../, абсолютные —
zip-slip), архивы zip64 и многотомные, а также дубли имён записей в central
directory: такой архив неоднозначен — разные распаковщики возьмут разные копии.
Собственный разбор оглавления сверяется с числом записей, которое видит
zip-библиотека, — защита от parser differential.
Вложенные в content.zip архивы не распаковываются: кроме README, записи
content.zip на диск вообще не извлекаются, поэтому их лживые размеры вреда
не наносят.
Остаточный риск. У самого content.zip — уже сжатого архива, типичный
коэффициент повторного сжатия ~1 — фактический объём распаковки известен
только после извлечения. Транзиторная запись во временный каталог ограничена
физическим пределом deflate и немедленно вычищается при отказе. Принимающей
стороне рекомендуется дополнительно ограничивать размер загружаемого файла.
Наружу выставлен только фасад; всё, что ниже, — деталь реализации:
src/Модули/ПакетыOspx.os публичный фасад (#Использовать "../internal")
src/internal/Классы/ ЧитательOspx, ВалидаторПакета, ПарсерМанифеста,
СериализаторМанифеста, МанифестПакета,
ЗависимостьПакета, ПакетOspx,
ОграниченияРаспаковки, РезультатВалидации
oneunit execute70 тестов на 7 наборов. Фикстуры в tests/fixtures — настоящие пакеты
с hub.oscript.io (semver, logos, asserts) и специально подготовленные
некорректные архивы: битые, без манифеста, zip-бомбы, zip-slip, лгущие
о размерах, с дублями записей. Тестам внутренностей доступен
#Использовать "../src/internal".
Набор Тесты_ЗахватИмён держит границу фасада: соседняя библиотека делает
глобальным модуль с распространённым именем (как oscript-yaml —
Сериализатор), после чего дочерний oscript вызывает все методы фасада.
MIT — см. LICENSE.