XML-файл, который без проблем читается локально, может заблокировать автоматизированный конвейер: в нём могут оказаться объявления внешних сущностей, слишком глубокая вложенность элементов или одноимённые поля из разных пространств имён. Задача облачного Mac — не пытаться «исправить по возможности» такие входные данные, а стабильно отклонять документы, нарушающие заданные ограничения, и сохранять образцы, на которых можно воспроизвести сбой.
Сначала определите контракт входных данных, затем вызывайте парсер
Рассмотрим импорт конфигурации в iOS. Заранее установим правила: принимается только UTF-8, DTD запрещён; размер одного входного файла — не более 256 КиБ, глубина вложенности элементов — не более 16 уровней, суммарный объём данных во всех текстовых обратных вызовах — не более 32 КиБ. Это примерные лимиты для этой статьи, а не требования стандарта XML и не системные значения по умолчанию. Их следует адаптировать к задачам приложения.
Ошибки формата, превышение лимитов и ошибки в полях приложения нужно регистрировать отдельно. Успешный результат парсера ещё не означает, что можно принять документ с неизвестным корневым элементом, повторяющимся полем, которое должно встречаться один раз, или отсутствующими обязательными полями.
Отключение разрешения внешних сущностей не запрещает DTD и не обеспечивает полной изоляции ресурсов. Ограничения формата, настройки парсера и тайм-аут на уровне процесса нужно проверять по отдельности.
Запустите минимальную проверку безопасности на облачном Mac
Сохраните следующий код в xml-guard.swift. Он проверяет только синтаксис и ресурсные ограничения, но не создаёт модель данных приложения. Скрипт намеренно использует консервативный подход: он отклоняет даже маркер объявления DTD внутри комментария. Так простая текстовая проверка не выдаётся за полноценный лексический анализ XML.
import Foundation
final class GuardDelegate: NSObject, XMLParserDelegate {
var depth = 0
var textBytes = 0
var exceeded = false
func stop(_ parser: XMLParser) {
exceeded = true
parser.abortParsing()
}
func parser(
_ parser: XMLParser,
didStartElement elementName: String,
namespaceURI: String?,
qualifiedName qName: String?,
attributes attributeDict: [String: String] = [:]
) {
depth += 1
if depth > 16 { stop(parser) }
}
func parser(
_ parser: XMLParser,
didEndElement elementName: String,
namespaceURI: String?,
qualifiedName qName: String?
) {
depth -= 1
}
func parser(_ parser: XMLParser, foundCharacters string: String) {
textBytes += string.utf8.count
if textBytes > 32 * 1024 { stop(parser) }
}
func parser(_ parser: XMLParser, foundCDATA data: Data) {
textBytes += data.count
if textBytes > 32 * 1024 { stop(parser) }
}
func parser(
_ parser: XMLParser,
resolveExternalEntityName name: String,
systemID: String?
) -> Data? {
nil
}
}
func validate(_ path: String) throws -> Bool {
let handle = try FileHandle(forReadingFrom: URL(fileURLWithPath: path))
defer { try? handle.close() }
var data = Data()
while true {
guard let chunk = try handle.read(upToCount: 8192),
!chunk.isEmpty else { break }
data.append(chunk)
if data.count > 256 * 1024 { return false }
}
guard let source = String(data: data, encoding: .utf8),
!source.contains("\0"),
!source.contains("<!DOCTYPE") else {
return false
}
let delegate = GuardDelegate()
let parser = XMLParser(data: data)
parser.delegate = delegate
parser.shouldProcessNamespaces = true
parser.shouldResolveExternalEntities = false
parser.externalEntityResolvingPolicy = .never
let parsed = parser.parse()
return parsed && !delegate.exceeded && delegate.depth == 0
}
guard CommandLine.arguments.count == 2 else { exit(2) }
do {
exit(try validate(CommandLine.arguments[1]) ? 0 : 1)
} catch {
exit(2)
}
Скомпилируйте скрипт в терминале с уже выбранным набором инструментов, затем запустите его на корректном образце:
xcrun swiftc xml-guard.swift -o xml-guard
printf '%s\n' '<settings><name>demo</name></settings>' > valid.xml
./xml-guard valid.xml
Коды завершения определены так: 0 — проверка пройдена, 1 — входные данные отклонены, 2 — ошибка вызова или чтения файла. Конвейер должен классифицировать результаты по этим кодам, а не разбирать тексты ошибок, зависящие от языка системы. На вход следует передавать обычный файл, временно сохранённый самой задачей, а не произвольный путь к устройству.
Покройте сценарии отказа фиксированными образцами
Храните как минимум шесть категорий файлов: корректный документ, документ с незакрытым тегом, документ с DTD, вложенность в 17 уровней, превышение лимита текста и превышение лимита входных байтов. Следующие команды создают два пограничных образца; для их выполнения нужен Python 3.
python3 - <<'PY'
from pathlib import Path
Path("deep.xml").write_text(
"<n>" * 17 + "</n>" * 17, encoding="utf-8"
)
Path("text-limit.xml").write_text(
"<n>" + "x" * (32 * 1024 + 1) + "</n>", encoding="utf-8"
)
PY
./xml-guard deep.xml
./xml-guard text-limit.xml
Оба запуска должны завершиться с кодом 1. Дополнительно подготовьте пару образцов: один точно на границе лимита, другой — на один байт больше. Это поможет обнаружить ошибку в операторе сравнения. Образец с внешней сущностью должен отклоняться до начала разбора. Если нужно отдельно проверить настройки доступа парсера к внешним ресурсам, используйте контролируемую тестовую цель и явно проверяйте, что обращений к ней не было. Пустой результат сам по себе не доказывает безопасность.
Обрабатывайте пространства имён и текстовые обратные вызовы с учётом структуры
При включённой обработке пространств имён поле приложения нужно идентифицировать по паре «URI пространства имён + локальное имя», а не только по префиксу или elementName. Префикс может меняться, а name в разных URI может обозначать разные понятия. Тесты должны проверять оба случая: смена префикса не мешает приёму документа, а смена URI обязательно приводит к отказу.
foundCharacters не гарантирует передачу всего содержимого поля за один вызов. Декодер приложения должен вести стек элементов, последовательно накапливать текст, а при закрытии элемента проверять длину, формат и повторение полей. Для CDATA предусмотрен отдельный обратный вызов; его данные тоже нужно учитывать. Не следует и безусловно удалять начальные и конечные пробелы во всех полях: для отображаемого имени и основного текста с сохранением пробелов могут действовать разные контракты.
Не сохраняйте частичный результат
Сначала помещайте все записи во временную модель, доступную только текущей задаче. Выполняйте единую операцию фиксации лишь после успешной проверки синтаксиса, лимитов, корневого элемента и правил приложения. Намеренно добавьте ошибочный тег в конец документа и убедитесь, что даже уже прочитанные записи не попадают в базу данных.
Включите приёмочные проверки в сборку, не ограничивайтесь ручным тестом
На облачном Mac OSXBuild сначала запустите образцы из командной строки, затем добавьте те же файлы в тестовый таргет iOS и вызовите декодер, который действительно использует приложение. Контракт у обеих проверок общий, но успешный запуск настольного скрипта не заменяет приёмочную проверку реализации в симуляторе.
В результатах регрессионных проверок достаточно сохранять имя образца, ожидаемый код завершения, фактический результат и версию набора инструментов. Полное содержимое импортируемого документа выводить не нужно. Ограничивайте аномальные задачи тайм-аутом конвейера: лимиты в обратных вызовах сдерживают только объём уже переданных данных и не задают абсолютного потолка внутреннего потребления памяти парсером.
В завершение проверьте каждый пункт: корректные файлы импортируются, документы за пределами ограничений стабильно отклоняются, пространства имён не приводят к смешению полей, а после ошибок не остаётся частичных результатов. Тогда и при следующем изменении логики разбора основанием для приёмки будут воспроизводимые входные данные и явные утверждения в тестах, а не наблюдение «в этот раз не зависло».
Часто задаваемые вопросы
Достаточно ли отключить внешние сущности?
Нет. Это не запрещает внутренний DTD. В описанном формате принимается только UTF-8 XML без DTD, а проверка перед запуском парсера консервативно отклоняет его объявление.
Возвращает ли foundCharacters текст поля целиком?
Нет. Текст одного поля может поступать несколькими вызовами. Его нужно накапливать до закрытия элемента, а результат сохранять только после проверки всего документа.
Выделенный облачный Mac для сборок
Вся физическая машина — только в вашем распоряжении, без виртуализации. Выберите модель и срок аренды под объём сборок, а архивы, подписи и кэш храните на своём узле.
Выбрать облачный Mac