Проверка строгого hex-кодека iOS на облачном Mac

Проверка строгого hex-кодека iOS на облачном Mac

Проверка строгого hex-кодека iOS на облачном Mac

Когда бизнес-логика уже написана в Windows, а iOS-проект перенесён на облачный Mac для сборки, вместе с кодом нужно проверить и шестнадцатеричные поля интерфейсов. Такое представление часто используется в сообщениях устройств, дайджестах и двоичных идентификаторах. Строка может выглядеть корректно, но это ещё не означает, что при декодировании получатся правильные байты. Особенно важно исключить пропуск недопустимых символов, потерю ведущих нулей и автоматическое дополнение неполного ввода, которое превращает его в другие данные.

В этой статье мы зададим контракт преобразования байтов с помощью отдельного сценария Swift, а затем перенесём проверки в тесты проекта. Сценарий не зависит от сетевых сервисов и не требует материалов для подписи, поэтому его удобно запускать перед созданием архива.

Определяем контракт входных и выходных данных

Декодер принимает только ASCII-символы 0–9, a–f и A–F: каждые два символа представляют один байт. Кодер всегда выдаёт строчные буквы и ровно два символа на байт. Регистр букв может различаться, но пробелы, переводы строк, префикс 0x и полноширинные символы в этот протокол не входят.

На низком уровне пустая строка означает пустые данные. Если бизнес-поле не может быть пустым, его должна отклонять бизнес-валидация: не стоит неявно возлагать правила конкретного поля на универсальный декодер. Верхний предел задаётся числом байтов после декодирования; само граничное значение должно явно считаться допустимым.

Строгое декодирование и подготовка пользовательского ввода — разные задачи. Если нужно поддержать вставку данных с пробелами, сначала явно преобразуйте их на уровне адаптации интерфейса, а затем передавайте строгому декодеру. В машинных интерфейсах не должно быть автоматического исправления по умолчанию.

Кроме того, шестнадцатеричная запись — лишь способ представления байтов. Она не обеспечивает шифрование, целостность или проверку подлинности источника. Успешное декодирование не означает, что сообщению можно доверять.

Реализуем декодирование с отклонением ошибок и кодирование фиксированной ширины

Сохраните следующий код в HexContract.swift. Реализация сначала проверяет длину и лимит, а уже затем выделяет память под результат. Символы проверяются побайтово, без compactMap, который мог бы пропустить элементы, не прошедшие преобразование. Отдельный тип ошибок позволяет тестам различать причины отказа.

import Foundation

enum HexError: Error, Equatable {
    case invalidLimit
    case oddLength
    case tooLarge
    case invalidCharacter
}

func nibble(_ c: UInt8) throws -> UInt8 {
    switch c {
    case 48...57: return c - 48
    case 65...70: return c - 65 + 10
    case 97...102: return c - 97 + 10
    default: throw HexError.invalidCharacter
    }
}

func decodeHex(_ text: String, maxBytes: Int) throws -> Data {
    guard maxBytes >= 0 else { throw HexError.invalidLimit }
    let count = text.utf8.count
    guard count % 2 == 0 else { throw HexError.oddLength }
    guard count / 2 <= maxBytes else { throw HexError.tooLarge }

    var iterator = text.utf8.makeIterator()
    var output = Data()
    output.reserveCapacity(count / 2)

    while let high = iterator.next() {
        guard let low = iterator.next() else {
            throw HexError.oddLength
        }
        let a = try nibble(high)
        let b = try nibble(low)
        output.append((a << 4) | b)
    }
    return output
}

func encodeHex(_ data: Data) -> String {
    let alphabet = Array("0123456789abcdef".utf8)
    var output = [UInt8]()
    for byte in data {
        output.append(alphabet[Int(byte >> 4)])
        output.append(alphabet[Int(byte & 15)])
    }
    return String(decoding: output, as: UTF8.self)
}

Здесь используется число байтов UTF-8, а не количество видимых пользователю символов. Любой ввод вне ASCII в итоге должен быть отклонён. Порядок проверок тоже является частью контракта: сначала отрицательный лимит, затем нечётная длина, далее превышение объёма и лишь после этого проверка допустимых символов.

При проверке верхнего предела не вычисляется maxBytes * 2, поэтому умножение лимита не может вызвать переполнение. Эта проверка ограничивает выделение памяти под результат, а не приём запроса. Размер тела запроса по-прежнему нужно ограничивать при чтении: нельзя сначала принять строку неограниченной длины, а затем рассчитывать, что декодер сэкономит память.

Добавляем фиксированные векторы и примеры для отклонения

Добавьте следующие тесты в конец того же файла. Фиксированные векторы проверяют ведущие нули и регистр. Для всех однобайтовых значений независимым эталоном служит результат стандартного форматирования: это исключает ситуацию, когда кодер и декодер допускают согласованную ошибку, но проверка обратного преобразования всё равно проходит.

func reject(
    _ text: String,
    limit: Int = 16,
    expected: HexError
) {
    do {
        _ = try decodeHex(text, maxBytes: limit)
        preconditionFailure("Expected rejection")
    } catch let error as HexError {
        precondition(error == expected, "Unexpected error")
    } catch {
        preconditionFailure("Unexpected error type")
    }
}

let fixed = try decodeHex("000aFf", maxBytes: 3)
precondition(fixed == Data([0, 10, 255]))
precondition(encodeHex(fixed) == "000aff")

let empty = try decodeHex("", maxBytes: 0)
precondition(empty.isEmpty)

for value in 0...255 {
    let reference = String(format: "%02x", value)
    let decoded = try decodeHex(reference, maxBytes: 1)
    precondition(decoded == Data([UInt8(value)]))
    precondition(encodeHex(Data([UInt8(value)])) == reference)
}

reject("f", expected: .oddLength)
reject("0g", expected: .invalidCharacter)
reject("0x00", expected: .invalidCharacter)
reject("00\n0", expected: .invalidCharacter)
reject("00", expected: .invalidCharacter)
reject("0000", limit: 1, expected: .tooLarge)
reject("", limit: -1, expected: .invalidLimit)

let sequence = Data((0...255).map { UInt8($0) })
let restored = try decodeHex(encodeHex(sequence), maxBytes: 256)
precondition(restored == sequence)

Образцы нечётной длины должны однозначно отклоняться: нельзя угадывать, означает ли f значение 0f или f0. Также нужно проверить случай с недопустимым символом в конце строки, чтобы убедиться, что реализация не возвращает уже декодированную начальную часть. Для полей, значимых с точки зрения безопасности, ошибка должна означать отказ всей операции, а не выдачу частичного результата, который можно использовать дальше.

Запускаем проверки на облачном Mac и подключаем их к проекту

Сначала убедитесь, что текущие инструменты разработки доступны, затем выполните сценарий:

xcrun --find swift
xcrun swift HexContract.swift

При успешном выполнении сценарий ничего не выводит и завершается с нулевым кодом. Нарушение проверяемого условия или необработанная ошибка приводит к ненулевому коду завершения. Не запускайте этот сценарий в режиме оптимизации, отключающем проверки, и не добавляйте в конвейер обработку, игнорирующую код завершения.

Отдельный сценарий проверяет текущую реализацию Swift для macOS. При полноценном подключении к проекту поместите кодер и декодер в общий модуль приложения, перенесите приведённые векторы в XCTest, замените проверки на XCTAssertEqual и XCTAssertThrowsError и запустите их в существующем тестовом таргете iOS. Только так можно одновременно выявить проблемы с включением файлов в таргет, условной компиляцией и реальными путями вызова.

Если реализация уже существует, не поддерживайте ещё одну копию специально для тестов: тесты должны напрямую импортировать рабочий модуль. Иначе сценарий может пройти успешно, а приложение продолжит использовать старый декодер.

Завершаем регрессионную проверку списком граничных случаев

Перед слиянием последовательно проверьте: кодируется ли нулевой байт как 00, соответствует ли пустой ввод правилам поля, принимается ли максимально допустимый объём, отклоняется ли превышение на один байт и не возвращаются ли вообще никакие данные при некорректном вводе. Добавьте также детерминированные длинные последовательности вместо того, чтобы полагаться только на случайные данные.

Смешанный регистр допустим, но исходную строку нельзя напрямую использовать как уникальный ключ: aa и AA представляют одинаковые байты. Для нормализованного хранения сначала выполните строгое декодирование, затем кодирование в едином формате. Не подменяйте проверку байтов преобразованием регистра строки до валидации.

В итоге достаточно сохранить версию инструментальной цепочки, названия не прошедших проверку образцов и коды завершения тестов. Если работа связана с реальными сообщениями устройств или токенами, не записывайте полный ввод в журнал: воспроизводите ситуацию на фиксированных синтетических векторах. Цель проверки — не преобразовать любую строку, а обеспечить точное восстановление допустимых байтов после кодирования и явный отказ для ввода, выходящего за установленные ограничения.

Часто задаваемые вопросы

Нужно ли удалять пробелы и дополнять строку нечётной длины?

На машинном интерфейсе такой ввод следует отклонять. Очистку ручного ввода, если она нужна, выполняйте отдельным адаптером до строгого декодирования.

Достаточно ли проверки кодирования с обратным декодированием?

Нет: обе функции могут содержать одинаковую ошибку. Нужны фиксированные векторы, все однобайтовые значения, ошибочный ввод и ведущие нули.

Арендуйте выделенный Mac для сборки приложений.

Выберите ближайший узел, затем проверьте, сколько памяти и места на диске требуется проекту, и подберите период оплаты. В вашем распоряжении будет отдельный физический Mac, а не виртуальная машина. Актуальную доступность можно проверить в панели управления.

Подготовьтесь к сборке заранее.

Перед заказом проверьте конфигурацию и период оплаты. Инструкции по подключению, переносу среды и первой сборке доступны на странице поддержки.