Проверка потокового CRC-32 для iOS на облачном Mac
Если контрольная сумма одного и того же файла проходит проверку при обработке целиком, но не сходится при чтении по блокам, причина обычно не в сети облачного Mac, а в сбросе состояния CRC после каждого блока. Когда распределённая команда включает обработку файлов в непрерывное тестирование, нужно зафиксировать не только входной файл, но и вариант алгоритма, способ разбиения на блоки и формат результата. Ниже мы напишем на Swift приёмочный тест, который можно сразу запустить на облачном Mac, а затем подключим те же проверки к проекту iOS.
Сначала зафиксируйте правила вычисления CRC-32
В статье используется CRC-32/ISO-HDLC: полином в обычном представлении — 0x04C11DB7, в отражённой реализации — 0xEDB88320, начальное значение и значение для финального XOR — 0xFFFFFFFF. Для входа и выхода применяется отражение битов. Не ограничивайтесь обозначением «CRC32» в спецификации интерфейса: иначе стороны могут выбрать разные варианты алгоритма.
Для стандартной тестовой строки 123456789 результат должен быть CBF43926, а для пустого входа — 00000000. Здесь строка преобразуется в байты UTF-8 без перевода строки. Если готовите образец в терминале, учитывайте, что некоторые команды вывода автоматически добавляют перевод строки.
CRC-32 предназначен для обнаружения случайных повреждений и не гарантирует защиту от подмены. Чтобы подтвердить происхождение файла, проверяйте доверенную цифровую подпись. Значение CRC, приложенное к файлу, нельзя считать свидетельством его безопасности.
В протоколе передачи также нужно указать, что представляет собой результат: восьмизначный шестнадцатеричный текст или четырёхбайтовое целое число. Во втором случае отдельно согласуйте порядок байтов. «Отражение» в алгоритме не означает, что при передаче по сети используется little-endian.
Напишите реализацию, сохраняющую внутреннее состояние
Сохраните следующий код в файл crc-check.swift. В примере используется побитовая обработка, чтобы параметры было проще проверить. Это эталонная реализация для проверки корректности, а не вариант для высокой пропускной способности. Если в рабочем коде вы перейдёте на табличную реализацию, сохраните приведённые далее тесты.
import Foundation
struct CRC32 {
private var register: UInt32 = 0xFFFFFFFF
mutating func update<S: Sequence>(_ bytes: S)
where S.Element == UInt8 {
for byte in bytes {
register ^= UInt32(byte)
for _ in 0..<8 {
let lowBit = register & 1
register >>= 1
if lowBit != 0 {
register ^= 0xEDB88320
}
}
}
}
var value: UInt32 {
register ^ 0xFFFFFFFF
}
}
func checksum(_ bytes: [UInt8]) -> UInt32 {
var crc = CRC32()
crc.update(bytes)
return crc.value
}
func require(_ condition: Bool, _ name: String) {
if !condition {
fatalError(name)
}
}
let vector = Array("123456789".utf8)
require(checksum(vector) == 0xCBF43926, "reference-vector")
require(checksum([]) == 0, "empty-input")
for split in 0...vector.count {
var crc = CRC32()
crc.update(vector[..<split])
let interim = crc.value
require(interim == checksum(Array(vector[..<split])),
"intermediate-value")
crc.update([])
crc.update(vector[split...])
require(crc.value == 0xCBF43926, "split-invariance")
}
Свойство value лишь возвращает вычисленное из внутреннего регистра значение без возможности записи. Чтение промежуточного результата не влияет на дальнейшие вычисления. Не изменяйте регистр внутри этого свойства и не используйте результат после финального XOR как начальное состояние для следующего блока. Создавать новый экземпляр для каждого файла понятнее, чем предоставлять интерфейс сброса, которым легко воспользоваться неправильно.
Дополните фиксированный вектор тестами разбиения на блоки
Девятибайтовый вектор проверяет лишь небольшую часть поведения. Добавим детерминированные входные данные, содержащие все возможные значения байта: так тесты охватят байты с установленным старшим битом, нулевые байты и неполный последний блок.
let payload = (0..<4097).map {
UInt8(truncatingIfNeeded: $0)
}
let expected = checksum(payload)
for size in [1, 7, 64, 1024, 4096, 8192] {
var crc = CRC32()
var offset = 0
while offset < payload.count {
let end = min(offset + size, payload.count)
crc.update(payload[offset..<end])
offset = end
}
require(crc.value == expected, "chunk-size-\(size)")
}
var first = CRC32()
first.update(vector)
var second = CRC32()
second.update(payload)
require(first.value == 0xCBF43926, "independent-state")
require(second.value == expected, "second-stream")
Здесь проверяется, что одна и та же реализация даёт одинаковый результат при разных способах разбиения. Само по себе это не доказывает корректность алгоритма, поэтому тест со стандартным вектором нужно обязательно сохранить. При подключении к реальному протоколу добавьте бинарные образцы, полученные независимой реализацией и проверенные отдельно.
Не утверждайте в тестах, что любые два разных входа обязательно имеют разные CRC. Множество контрольных сумм конечно, поэтому коллизии существуют. Отсутствие коллизий на случайных файлах не доказывает надёжность реализации.
Проверьте обработку ошибок при чтении файлов
Добавьте функцию чтения в тот же файл. Она последовательно получает блоки через FileHandle и каждый раз передаёт только текущие данные одному и тому же экземпляру CRC, не загружая файл целиком в память.
func checksumFile(_ url: URL) throws -> UInt32 {
let handle = try FileHandle(forReadingFrom: url)
defer { try? handle.close() }
var crc = CRC32()
while true {
let data = try handle.read(upToCount: 64 * 1024) ?? Data()
if data.isEmpty {
break
}
crc.update(data)
}
return crc.value
}
if CommandLine.arguments.count == 2 {
let url = URL(fileURLWithPath: CommandLine.arguments[1])
let result = try checksumFile(url)
Swift.print(String(format: "%08X", result))
}
Команда xcrun swift crc-check.swift запускает все предыдущие проверки. Если добавить аргументом путь к доступному для чтения файлу, программа также выведет его контрольную сумму. Ошибка чтения передаётся вызывающему коду, а не интерпретируется как пустой файл. На уровне прикладной логики следует отдельно регистрировать ошибки открытия, ошибки чтения и несовпадение результата.
Для чтения нужна стабильная копия, запись в которую уже завершена, а записывающий дескриптор закрыт. Если во время скачивания изменять файл по тому же пути и отдельно открыть его для расчёта, можно прочитать смесь разных версий. Завершение расчёта также не доказывает, что после него не было новых записей. При приёмочной проверке сначала зафиксируйте входные данные, затем сравнивайте длину и CRC.
При интеграции проверьте, кому принадлежит состояние
Поместите реализацию в модуль, доступный модульным тестам, и сохраните четыре группы проверок: стандартный вектор, независимость результата от разбиения на блоки, изоляция экземпляров и передача файловых ошибок вызывающему коду. Для файловых тестов используйте отдельный временный каталог, создавайте в нём пустой и бинарный файлы и удаляйте их после завершения.
Интерфейс iOS должен только показывать прогресс и результат; чтение файлов не должно занимать главный поток. Каждой задаче передачи нужен собственный экземпляр CRC. Блоки одной задачи необходимо обрабатывать в порядке их расположения в файле: параллельные обратные вызовы не должны напрямую изменять регистр.
Если задача отменена до завершения, отбросьте её экземпляр CRC. Для продолжения расчёта после возобновления одной финальной контрольной суммы недостаточно: нужно заново прочитать сохранённую начальную часть файла либо предусмотреть контрольные точки внутреннего состояния с ограничениями по совместимости версий. Во втором случае также обязательно проверять смещение и идентичность файла — сохранить одно целое число недостаточно.
Наконец, включайте в артефакты тестирования версию инструментальной цепочки, длину образца, схему разбиения и имя проверки, завершившейся ошибкой. Тогда при следующей смене узла или оптимизации алгоритма вы сможете отличить изменение параметров от пропущенных блоков и ошибочного совместного использования состояния, а не передавать файл заново в надежде на удачу.
Часто задаваемые вопросы
Доказывает ли CRC-32, что файл не подменили намеренно?
Нет. CRC-32 обнаруживает случайное повреждение, но злоумышленник может изменить и файл, и контрольную сумму. Для защиты от подмены нужна цифровая подпись из доверенного источника либо подходящий протокол аутентификации с ключом.
Можно ли получить CRC всего файла, применив XOR к CRC отдельных блоков?
Нет. При последовательном чтении обновляйте одно внутреннее состояние и получайте итоговое значение после обработки всех байтов. Для объединения независимых результатов нужен специальный алгоритм, учитывающий длину блоков.
Арендуйте Mac mini в удобном для проекта ритме
Выберите выделенный физический узел с оплатой за день, неделю, месяц или квартал. В пиковый сезон можно продлить аренду; по окончании срока без продления она прекращается. Фактический статус доступности смотрите в консоли.