Проверка безопасного чтения архивов iOS на облачном Mac
Самый опасный исход чтения архива старой версии приложения в новой — не ошибка, а возврат пустых данных с последующим сохранением поверх исходного файла. Проверьте этот сценарий на облачном Mac: сначала запустите тест декодирования на чистом Foundation, затем прочитайте те же образцы в тестовом таргете iOS. Цель не в том, чтобы один раз успешно записать и прочитать данные, а в том, чтобы убедиться: неверные типы, неизвестные версии и повреждённые входные данные явно отклоняются.
Сначала определите границы чтения
Здесь рассматриваются архивы объектов, созданные с помощью NSKeyedArchiver, а не шифрование. Пример допускает только один корневой объект с номером версии и текстовым полем. Перед проверкой зафиксируйте три условия:
- Корневой объект должен иметь тип
Memo: нельзя принимать произвольный объект и лишь затем приводить его к нужному типу. - Версия должна совпадать с поддерживаемой; текстовое поле должно присутствовать и иметь ограниченную длину.
- При ошибке нельзя перезаписывать исходный файл или выдавать пустую запись за успешный результат.
supportsSecureCoding — это декларация типа, а не автоматическая проверка прикладных полей. На стороне чтения всё равно нужно включить requiresSecureCoding, а каждое объектное поле декодировать методом с ограничением допустимых типов.
Безопасное декодирование ограничивает типы объектов, экземпляры которых могут быть созданы. Оно не подтверждает происхождение данных, не обеспечивает конфиденциальность и не ограничивает размер файла. Сначала ограничьте объём входных данных и только потом передавайте их декодеру.
Напишите декодер, который отклоняет некорректные данные
Сохраните следующий код в archive-check.swift. Он зависит только от Foundation; сначала убедитесь, что команда xcrun swift --version выполняется. Номер версии в примере относится к формату архива, а не к версии операционной системы.
import Foundation
final class Memo: NSObject, NSSecureCoding {
static var supportsSecureCoding: Bool { true }
let schema: Int
let text: String
init(schema: Int = 1, text: String) {
self.schema = schema
self.text = text
super.init()
}
required init?(coder: NSCoder) {
guard coder.containsValue(forKey: "schema"),
coder.decodeInteger(forKey: "schema") == 1,
let value = coder.decodeObject(
of: NSString.self, forKey: "text"
),
value.length <= 4096 else {
return nil
}
schema = 1
text = value as String
super.init()
}
func encode(with coder: NSCoder) {
coder.encode(schema, forKey: "schema")
coder.encode(text as NSString, forKey: "text")
}
}
enum ArchiveFailure: Error {
case invalidSize
case invalidObject
}
func readMemo(_ data: Data) throws -> Memo {
guard !data.isEmpty, data.count <= 1_048_576 else {
throw ArchiveFailure.invalidSize
}
let reader = try NSKeyedUnarchiver(forReadingFrom: data)
reader.requiresSecureCoding = true
reader.decodingFailurePolicy = .setErrorAndReturn
let memo = reader.decodeObject(
of: Memo.self, forKey: NSKeyedArchiveRootObjectKey
)
reader.finishDecoding()
if let error = reader.error {
throw error
}
guard let memo else {
throw ArchiveFailure.invalidObject
}
return memo
}
func pack(_ value: Any) throws -> Data {
try NSKeyedArchiver.archivedData(
withRootObject: value, requiringSecureCoding: true
)
}
Сначала код проверяет наличие поля версии: иначе значение по умолчанию при декодировании целого числа могло бы скрыть отсутствие ключа. Длина текста ограничивается в кодовых единицах UTF-16, в соответствии с NSString.length. В рабочей реализации выбирайте предел по требованиям формата данных продукта, а не считайте порог из примера универсальным стандартом безопасности.
Политику setErrorAndReturn необходимо установить до чтения объекта. Не рассчитывайте, что внешний do/catch перехватит любые сбои декодирования: обработка некоторых недопустимых объектов определяется политикой ошибок декодера и не сводится к обычному механизму выбрасывания ошибок Swift.
Соберите минимальный регрессионный набор из четырёх входов
Добавьте следующий код в тот же файл. Корректный образец должен вернуть исходный текст; остальные три должны быть отклонены. При любом отклонении от ожидаемого результата проверочная программа должна завершаться с ненулевым кодом.
func expectRejection(_ data: Data) {
do {
_ = try readMemo(data)
} catch {
return
}
fatalError("Unexpected archive acceptance")
}
let valid = try pack(Memo(text: "draft"))
let restored = try readMemo(valid)
guard restored.text == "draft" else {
fatalError("Round-trip mismatch")
}
expectRejection(try pack("wrong root" as NSString))
expectRejection(try pack(Memo(schema: 2, text: "future")))
expectRejection(Data(valid.prefix(12)))
Сначала скомпилируйте программу, затем запустите её: так среда интерпретируемого выполнения не повлияет на имена типов в образцах, используемых далее.
set -eu
xcrun swiftc archive-check.swift -o archive-check
./archive-check
При обработке некорректных образцов Foundation может вывести диагностику в стандартный поток ошибок. Само по себе это не означает провала проверки. Ориентируйтесь на код завершения процесса и проверяемые условия, а не на полное отсутствие сообщений в журнале. Если корневой объект неверного типа был принят, исправьте список разрешённых классов, а не удаляйте негативный сценарий ради зелёного теста.
Добавьте вложенные объекты и исторические образцы
Задавайте разрешённые типы для каждого поля
Если реальный архив содержит массивы, разрешить только NSArray недостаточно, чтобы считать их элементы безопасными. Используйте перегрузку decodeObject(of:forKey:), принимающую набор классов: включите в него и контейнер, и ожидаемые классы элементов. Затем проверьте количество элементов, длину полей и связи между объектами. Не добавляйте слишком общий NSObject только для того, чтобы обойти ошибку.
Ограничения типов в архиве не заменяют контроль сложности графа объектов. Ограничение размера входных данных тоже не гарантирует ограниченного потребления памяти после декодирования. Для недоверенных данных дополнительно используйте формат с жёсткими ограничениями или отдельный изолированный контур обработки.
Сохраните реальные файлы старых версий
Добавьте обезличенные архивы старых версий в репозиторий как тестовые ресурсы. Укажите версию приложения, создавшего архив, ожидаемые поля и допустимые результаты миграции. Динамически создаваемые образцы подходят для проверки логики отклонения, но не доказывают, что реальные байты, записанные старой версией, по-прежнему читаются.
Имя класса Swift во время выполнения может измениться при смене модуля. При переносе типа или переименовании модуля сначала проверьте исторические образцы, а затем настройте сопоставление классов по явно заданным старым именам. Нельзя считывать произвольные имена классов из входных данных и автоматически разрешать их. После успешной проверки в командной строке выполните ту же проверку с тем же набором файлов в тестовом таргете iOS, чтобы учесть различия целевой среды выполнения.
Остановите ошибку на этапе чтения, не допуская её до сохранения
Представляйте результат декодирования как успех либо явную ошибку; избегайте конструкции try? с подстановкой пустого объекта. При ошибке чтения сохраняйте исходный файл и показывайте в интерфейсе способ восстановления. Заменять старый файл можно только после завершения миграции и успешного проверочного чтения новых данных.
Перед слиянием проверьте пять пунктов: поля корректного образца совпадают с исходными; неверный тип корневого объекта отклоняется; неизвестная версия отклоняется; усечённые данные не приводят к сохранению как при успешном чтении; исторические образцы проходят проверку в таргете iOS. Каждый негативный сценарий оформите отдельным тестом, чтобы при сбое сразу было понятно, какое условие нарушено.
Программа командной строки на облачном Mac даёт быструю обратную связь, а тесты iOS подтверждают поведение на целевой платформе. Только успешное прохождение обоих уровней даёт воспроизводимое подтверждение корректности пути чтения архивов, а не просто один удачный запуск на машине разработчика.
Часто задаваемые вопросы
Достаточно ли свойства supportsSecureCoding?
Нет. При чтении включите requiresSecureCoding, ограничьте классы через типизированные методы decodeObject и отдельно проверьте версию и значения полей.
Можно ли вернуть пустой список при ошибке чтения?
Не выдавайте ошибку за успешный пустой результат. Сохраните исходный файл и верните явную ошибку, чтобы последующая запись не уничтожила данные.
Выделенный облачный Mac для следующей сборки
Выделенный физический Mac mini, не виртуальная машина. Выберите срок аренды: день, неделя, месяц или квартал. Конфигурация и фактическая доступность указаны в консоли.