Модуль загрузки резервирует место по формуле «число блоков × размер блока», а обработчик изображений выделяет буфер как «число строк × число байтов в строке». Обычно такие расчёты проходят без проблем, но повреждённый файл или некорректные данные от API могут сначала вызвать переполнение целого числа, а затем — аварийное завершение приложения. На облачном Mac чистую арифметическую логику можно вынести в отдельную исполняемую программу на Swift и организовать надёжные регрессионные проверки граничных случаев для iOS-проекта без запуска симулятора.
Сначала определим контракт расчёта ёмкости
В этой статье рассматриваются только неотрицательные целочисленные значения ёмкости, без приближённых вычислений с плавающей точкой. Установим три правила: отрицательные числа считаются недопустимым вводом; выход за диапазон Int должен возвращать ошибку переполнения; делитель при делении с округлением вверх должен быть строго положительным. Нулевая ёмкость допустима, нулевой делитель — нет.
Не преобразуйте значение в беззнаковый тип до проверки. Обычное преобразование отрицательного числа в беззнаковое может сразу вызвать аварийное завершение, а преобразование с усечением — превратить его в огромное положительное число. Если входные данные поступают в виде строки, сначала убедитесь, что её удалось преобразовать в Int: подменять ошибку разбора нулём нельзя.
Если результат арифметической операции представим в выбранном типе, это ещё не означает, что такой объём ресурсов можно выделить. Проверка диапазона целого числа и проверка прикладной квоты — два независимых барьера. Запрашивать ресурсы можно только после прохождения обоих.
Используйте отдельные имена для «количества», «числа байтов на элемент» и «общего числа байтов», чтобы не смешивать разные единицы измерения. Ошибка также должна указывать этап вычисления, но исходные конфиденциальные данные не следует целиком записывать в журнал.
Вместо циклического переполнения используем операции с флагом
Обычные операторы + и * при переполнении вызывают ошибку времени выполнения. Операторы &+ и &*, напротив, явно допускают циклическое переполнение, из-за чего огромный объём может превратиться в маленькое значение. Для проверки ёмкости нужны операции, возвращающие и результат, и флаг переполнения: сначала проверяем флаг и только затем используем результат.
Сохраните следующий код в файле arithmetic.swift. Он не зависит от проекта приложения и не выполняет реального выделения больших объёмов памяти.
enum CapacityError: Error, Equatable {
case invalidInput
case overflow
}
func checkedAdd(_ a: Int, _ b: Int) throws -> Int {
guard a >= 0, b >= 0 else {
throw CapacityError.invalidInput
}
let result = a.addingReportingOverflow(b)
guard !result.overflow else {
throw CapacityError.overflow
}
return result.partialValue
}
func checkedMultiply(_ a: Int, _ b: Int) throws -> Int {
guard a >= 0, b >= 0 else {
throw CapacityError.invalidInput
}
let result = a.multipliedReportingOverflow(by: b)
guard !result.overflow else {
throw CapacityError.overflow
}
return result.partialValue
}
func ceilingDivide(_ n: Int, by d: Int) throws -> Int {
guard n >= 0, d > 0 else {
throw CapacityError.invalidInput
}
let q = n / d
return n % d == 0 ? q : try checkedAdd(q, 1)
}
func require(_ condition: Bool) {
guard condition else {
fatalError("Boundary check failed")
}
}
func expectError(
_ expected: CapacityError,
_ operation: () throws -> Int
) {
do {
_ = try operation()
fatalError("Expected capacity error")
} catch let error as CapacityError {
require(error == expected)
} catch {
fatalError("Unexpected error type")
}
}
require(try checkedAdd(Int.max, 0) == Int.max)
require(try checkedMultiply(Int.max, 1) == Int.max)
require(try checkedMultiply(0, Int.max) == 0)
require(try ceilingDivide(0, by: 4) == 0)
require(try ceilingDivide(8, by: 4) == 2)
require(try ceilingDivide(9, by: 4) == 3)
require(try ceilingDivide(Int.max, by: 1) == Int.max)
require(try ceilingDivide(Int.max, by: 2) == Int.max / 2 + 1)
expectError(.overflow) {
try checkedAdd(Int.max, 1)
}
expectError(.overflow) {
try checkedMultiply(Int.max, 2)
}
expectError(.invalidInput) {
try checkedMultiply(-1, 0)
}
expectError(.invalidInput) {
try ceilingDivide(1, by: 0)
}
expectError(.invalidInput) {
try ceilingDivide(Int.min, by: -1)
}
Умножение отрицательного числа на ноль здесь намеренно проверяет порядок валидации. Если реализация сначала применяет сокращённый путь «множитель равен нулю — вернуть ноль», недопустимый ввод обойдёт контракт. Тест должен проверять конкретный тип ошибки, а не просто подтверждать, что «какая-то ошибка произошла».
При делении с округлением вверх не прибавляйте делитель заранее
Число блоков часто вычисляют по формуле (n + d - 1) / d. Её математическая корректность не гарантирует безопасности для целых чисел фиксированной разрядности: даже если итоговое частное невелико, предшествующее сложение может выйти за допустимый диапазон.
В примере сначала вычисляются частное и остаток, а единица прибавляется только при ненулевом остатке. Ограничив делимое неотрицательными значениями, а делитель — положительными, мы также исключаем особый случай переполнения при делении минимального знакового целого на минус один.
Тесты должны как минимум охватывать ноль, деление без остатка, остаток в единицу, максимальное целое и нулевой делитель. Не заменяйте Int.max жёстко заданной десятичной константой. Используйте границу, предоставляемую самим типом: так проверяется разрядность целого на фактической целевой платформе сборки, а не представление разработчика о ней.
Запускаем проверку в двух режимах сборки
На облачном Mac должен быть выбран набор инструментов разработки. Сначала убедитесь, что компилятор доступен, затем скомпилируйте и запустите программу в обоих режимах:
set -eu
xcrun swiftc --version
mkdir -p .arithmetic-check
xcrun swiftc -Onone arithmetic.swift \
-o .arithmetic-check/debug
.arithmetic-check/debug
xcrun swiftc -O arithmetic.swift \
-o .arithmetic-check/optimized
.arithmetic-check/optimized
При успехе программа завершается без вывода. Любая проваленная проверка или необработанная ошибка приводит к сбою шага. Не используйте конвейер так, чтобы код завершения программы подменялся успешным статусом утилиты журналирования.
В примере не используется assert, поскольку в оптимизированной сборке такие проверки могут быть удалены. Собственная функция require использует явное условие и fatalError, поэтому проверки сохраняются в обоих выбранных режимах. Не включайте дополнительные нестандартные настройки оптимизации, ослабляющие проверки безопасности во время выполнения.
При интеграции с приложением проверяем лимиты ресурсов
Эта отдельная программа — минимальная проверка арифметического контракта. При полноценной интеграции приложение и тесты должны обращаться к одной и той же реализации, используемой в рабочем коде, а не к двум похожим наборам функций, которые приходится поддерживать параллельно.
Проверьте цепочку вызовов в следующем порядке:
- При ошибке разбора исходных данных сразу возвращается ошибка; выполнение не продолжается с ёмкостью по умолчанию.
- Умножение и сложение с проверкой переполнения выполняются только после согласования единиц измерения, чтобы промежуточные выражения не успели переполниться.
- Перед выделением ресурсов результат сравнивается с установленной для продукта квотой на одну операцию.
- При переходе к целочисленному типу меньшей разрядности используется точное преобразование, способное завершиться неудачей; усечение не допускается.
- В сценариях отказа проверяется, что не были созданы файлы, выделены буферы или отправлены последующие задачи.
В завершение добавьте пример, в котором арифметический результат допустим, но превышает прикладную квоту, и убедитесь, что он обрабатывается отдельно от ошибки переполнения. Тогда при диагностике можно будет чётко различать недопустимый ввод, результат за пределами диапазона типа и отказ по политике использования ресурсов, а не сводить все граничные случаи к нехватке памяти.
Часто задаваемые вопросы
Можно ли при переполнении возвращать Int.max?
По умолчанию следует возвращать явную ошибку. Насыщение скрывает разницу между допустимым большим результатом и сбоем. Используйте его только при явно заданном контракте насыщаемой арифметики.
Нужен ли симулятор iOS?
Нет. Пример использует только стандартную библиотеку Swift и выполняется непосредственно на облачном Mac. Интерфейс, платформенные типы и реальные выделения памяти требуют отдельных проверок.
Передайте задачи сборки на выделенный облачный Mac
Выберите выделенный узел Apple Silicon и получите в своё распоряжение всю машину — с нативным графическим интерфейсом macOS и командной строкой. Модель, срок аренды и узел указаны в подтверждении заказа.