Проверка переполнения целых в расчётах ёмкости для iOS

Проверка переполнения целых в расчётах ёмкости для iOS

Модуль загрузки резервирует место по формуле «число блоков × размер блока», а обработчик изображений выделяет буфер как «число строк × число байтов в строке». Обычно такие расчёты проходят без проблем, но повреждённый файл или некорректные данные от 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 и командной строкой. Модель, срок аренды и узел указаны в подтверждении заказа.