Keyboard shortcuts

Press ← or → to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Корректность на уровне типов в Rust

О проекте

  • Курс основан на оригинальных материалах Rust Training. Лицензии: MIT (код) и CC-BY-4.0 (текст).
  • Русский перевод и адаптация выполнены участниками проекта RustTraining-Rus.
  • Замечания и исправления присылайте через Issues и Pull Requests репозитория.

Это практическое руководство по использованию системы типов Rust, чтобы целые классы ошибок становились невозможными для компиляции. Сопутствующая книга Паттерны Rust объясняет механику (трейты, ассоциированные типы, typestate), а это руководство показывает, как применять эту механику к реальным задачам: диагностике аппаратного обеспечения, криптографии, валидации протоколов и встраиваемым системам.

Все паттерны здесь следуют одному принципу: переносите инварианты из проверок во время выполнения в систему типов, чтобы компилятор обеспечивал их соблюдение.

Как работать с книгой

Уровни сложности

СимволУровеньДля кого
🟢ВводныйУверенно владеете владением и трейтами
🟡СреднийЗнакомы с обобщениями и ассоциированными типами
🔴ПродвинутыйГотовы к typestate, phantom-типам и сессионным типам

Рекомендации по темпу

ЦельМаршрутВремя
Быстрый обзоргл. 01, гл. 13 (шпаргалка)30 мин
Разработчик IPMI / BMCгл. 02, 05, 07, 10, 172,5 ч
Разработчик GPU / PCIeгл. 02, 06, 09, 10, 152,5 ч
Реализация Redfishгл. 02, 05, 07, 08, 17, 183 ч
Фреймворки и инфраструктурагл. 04, 08, 11, 14, 182,5 ч
Новичок в корректности по построениюгл. 01 → 10 по порядку, затем упражнения гл. 124 ч
Полное погружениеВсе главы по порядку7 ч

Аннотированное оглавление

Гл.НазваниеСложностьКлючевая идея
1Философия: почему типы лучше тестов🟢Три уровня корректности; типы как гарантии, которые проверяет компилятор
2Типизированные командные интерфейсы🟡Ассоциированные типы связывают запрос с ответом
3Одноразовые типы🟡Семантика перемещения как линейные типы для криптографии
4Capability-токены🟡Токены-доказательства полномочий нулевого размера
5Автоматы состояний протоколов🔴Typestate для сессий IPMI и LTSSM в PCIe
6Анализ размерностей🟢Обёртки newtype не дают перепутать единицы измерения
7Проверенные границы🟡Разбор данных один раз на границе, доказательство хранится в типах
8Capability-миксины🟡Трейты-ингредиенты и blanket impl
9Phantom-типы🟡PhantomData для ширины регистров и направления DMA
10Собираем всё вместе🟡Все 7 паттернов в одной платформе диагностики
11Четырнадцать проверенных приёмов🟡Sentinel → Option, sealed-трейты, билдеры и др.
12Упражнения🟡Шесть итоговых задач с решениями
13Краткая шпаргалка—Каталог паттернов и блок-схема выбора
14Тестирование гарантий на уровне типов🟡trybuild, proptest, cargo-show-asm
15Const fn: доказательства корректности на этапе компиляции🟠Карты памяти, регистры и битовые поля
16Send и Sync: доказательства корректности конкурентности🟠Проверки конкурентности на этапе компиляции
17Пошаговый разбор: клиент Redfish🟡Восемь паттернов в типобезопасном клиенте Redfish
18Пошаговый разбор: сервер Redfish🟡Typestate билдера, токены источников, агрегация состояния здоровья, миксины

Что желательно знать заранее

КонцепцияГде изучить
Владение и заимствованиеПаттерны Rust, гл. 01
Трейты и ассоциированные типыПаттерны Rust, гл. 02
Newtype и typestateПаттерны Rust, гл. 03
PhantomDataПаттерны Rust, гл. 04
Обобщения и ограничения трейтовПаттерны Rust, гл. 01

Спектр корректности по построению

← Менее безопасно                                             Более безопасно →

Проверки в рантайме  Модульные тесты   Property-тесты        Корректность по построению
───────────────────  ────────────────  ──────────────        ──────────────────────────

if temp > 100 {      #[test]           proptest! {           struct Celsius(f64);
  panic!("слишком    fn test_temp() {    |t in 0..200| {     // Нельзя перепутать с Rpm
  жарко");               assert!(          assert!(...)       // на уровне типов
}                      check(42));     }
                     }                 }
                                                            Некорректная программа?
Неверный код?        Неверный код?     Неверный код?         Не скомпилируется.
Падает в проде.      Падает в CI.      Падает в CI           Её не существует.
                                       (вероятностно).

Это руководство работает в самой правой позиции — там, где ошибок не бывает, потому что система типов не может их выразить.


Философия: почему типы лучше тестов 🟢

Что вы узнаете: три уровня корректности на этапе компиляции (значения, состояния, протоколы), то, как обобщённые сигнатуры функций работают как гарантии, которые проверяет компилятор, и то, когда паттерны корректности по построению оправданы, а когда — нет.

Перекрёстные ссылки: гл. 02 (типизированные команды), гл. 05 (typestate), гл. 13 (шпаргалка)

Цена проверок во время выполнения

Рассмотрим типичную проверку во время выполнения в кодовой базе диагностики:

fn read_sensor(sensor_type: &str, raw: &[u8]) -> f64 {
    match sensor_type {
        "temperature" => raw[0] as i8 as f64,          // знаковый байт
        "fan_speed"   => u16::from_le_bytes([raw[0], raw[1]]) as f64,
        "voltage"     => u16::from_le_bytes([raw[0], raw[1]]) as f64 / 1000.0,
        _             => panic!("неизвестный тип датчика: {sensor_type}"),
    }
}

У этой функции есть четыре режима отказа, которые компилятор не может поймать:

  1. Опечатка: "temperture" → panic во время выполнения
  2. Неверная длина raw: fan_speed с одним байтом → panic во время выполнения
  3. Вызывающий код использует возвращённое f64 как обороты (RPM), хотя это градусы Цельсия → логическая ошибка, которая никак не проявляется
  4. Добавлен новый тип датчика, но этот match не обновлён → panic во время выполнения

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

Три уровня корректности

Уровень 1 — Корректность значений

Сделайте недопустимые значения непредставимыми.

// ❌ Любой u16 может быть «портом» — 0 недопустим, но код компилируется
fn connect(port: u16) { /* ... */ }

// ✅ Существуют только проверенные порты
pub struct Port(u16);  // приватное поле

impl TryFrom<u16> for Port {
    type Error = &'static str;
    fn try_from(v: u16) -> Result<Self, Self::Error> {
        if v > 0 { Ok(Port(v)) } else { Err("порт должен быть больше 0") }
    }
}

fn connect(port: Port) { /* ... */ }
// Port(0) никогда не может быть создан — инвариант выполняется везде

Пример из аппаратного обеспечения: SensorId(u8) — оборачивает номер датчика сырым значением и проверяет, что он находится в диапазоне SDR.

Уровень 2 — Корректность состояний

Сделайте недопустимые переходы непредставимыми.

use std::marker::PhantomData;

struct Disconnected;
struct Connected;

struct Socket<State> {
    fd: i32,
    _state: PhantomData<State>,
}

impl Socket<Disconnected> {
    fn connect(self, addr: &str) -> Socket<Connected> {
        // ... логика подключения ...
        Socket { fd: self.fd, _state: PhantomData }
    }
}

impl Socket<Connected> {
    fn send(&mut self, data: &[u8]) { /* ... */ }
    fn disconnect(self) -> Socket<Disconnected> {
        Socket { fd: self.fd, _state: PhantomData }
    }
}

// У Socket<Disconnected> нет метода send() — если его вызвать, будет ошибка компиляции

Пример из аппаратного обеспечения: режимы пинов GPIO — у Pin<Input> есть read(), но нет write().

Уровень 3 — Корректность протоколов

Сделайте недопустимые взаимодействия непредставимыми.

use std::io;

trait IpmiCmd {
    type Response;
    fn parse_response(&self, raw: &[u8]) -> io::Result<Self::Response>;
}

// Упрощено для наглядности — полный трейт с net_fn(), cmd_byte(), payload() и
// parse_response() см. в гл. 02.

struct ReadTemp { sensor_id: u8 }
impl IpmiCmd for ReadTemp {
    type Response = Celsius;
    fn parse_response(&self, raw: &[u8]) -> io::Result<Celsius> {
        Ok(Celsius(raw[0] as i8 as f64))
    }
}

#[derive(Debug)] struct Celsius(f64);

fn execute<C: IpmiCmd>(cmd: &C, raw: &[u8]) -> io::Result<C::Response> {
    cmd.parse_response(raw)
}
// ReadTemp всегда возвращает Celsius — нельзя случайно получить Rpm

Пример из аппаратного обеспечения: IPMI, Redfish, административные команды NVMe — тип запроса определяет тип ответа.

Типы как гарантии, которые проверяет компилятор

Когда вы пишете:

fn execute<C: IpmiCmd>(cmd: &C) -> io::Result<C::Response>

вы не просто пишете функцию, а формулируете гарантию: «для любого типа команды C, реализующего IpmiCmd, выполнение даёт ровно C::Response». Компилятор проверяет эту гарантию при каждой сборке вашего кода. Если типы не сходятся, программа не скомпилируется.

Именно поэтому система типов Rust так мощна: она не просто ловит ошибки, а обеспечивает корректность на этапе компиляции.

Когда НЕ стоит использовать эти паттерны

Корректность по построению не всегда оправдана:

СитуацияРекомендация
Критичная к безопасности граница (управление питанием, криптография)✅ Всегда: ошибка здесь может расплавить железо или утечь секреты
Публичный API между модулями✅ Обычно: неправильное использование должно приводить к ошибке компиляции
Автомат состояний с тремя и более состояниями✅ Обычно: typestate предотвращает неверные переходы
Внутренний помощник в одной функции на 50 строк❌ Избыточно: достаточно простого assert!
Прототипирование и изучение неизвестного оборудования❌ Сначала сырые типы; уточняйте их, когда поведение станет понятно
Разбор пользовательского CLI⚠️ clap + TryFrom на границе, внутри допустимы сырые типы

Ключевой вопрос: «Если эта ошибка случится в продакшене, насколько она опасна?»

  • Вентилятор остановился → GPU выходит из строя → используйте типы
  • Неверная запись DER → клиент получает некорректные данные → используйте типы
  • Немного неверное debug-сообщение в логе → используйте assert!

Ключевые выводы

  1. Три уровня корректности — значения (newtype), состояния (typestate), протоколы (ассоциированные типы) — каждый уровень устраняет более широкий класс ошибок.
  2. Типы как гарантии — каждая сигнатура обобщённой функции — это контракт, который компилятор проверяет при каждой сборке.
  3. Вопрос о цене — «если эта ошибка уйдёт в продакшен, насколько она опасна?» — определяет, что уместнее: типы или тесты.
  4. Типы дополняют тесты — типы устраняют целые категории ошибок, а тесты проверяют конкретные значения и граничные случаи.
  5. Знайте, когда остановиться — внутренние помощники и одноразовые прототипы редко нуждаются в принудительной проверке на уровне типов.

Типизированные командные интерфейсы: запрос определяет ответ 🟡

Что вы узнаете: как ассоциированные типы в трейте команды создают связь между запросом и ответом на этапе компиляции, устраняя рассогласованный разбор данных, путаницу единиц измерения и тихое приведение типов в протоколах IPMI, Redfish и NVMe.

Перекрёстные ссылки: гл. 01 (философия), гл. 06 (размерные типы), гл. 07 (проверенные границы), гл. 10 (интеграция)

Неструктурированное болото

Большинство стеков управления аппаратным обеспечением — IPMI, Redfish, NVMe Admin, PLDM — начинаются с модели сырые байты на входе → сырые байты на выходе. Это порождает класс ошибок, которые тесты могут найти лишь частично:

use std::io;

struct BmcRaw { /* дескриптор ipmitool */ }

impl BmcRaw {
    fn raw_command(&self, net_fn: u8, cmd: u8, data: &[u8]) -> io::Result<Vec<u8>> {
        // ... вызывает ipmitool ...
        Ok(vec![0x00, 0x19, 0x00]) // заглушка
    }
}

fn diagnose_thermal(bmc: &BmcRaw) -> io::Result<()> {
    let raw = bmc.raw_command(0x04, 0x2D, &[0x20])?;
    let cpu_temp = raw[0] as f64;        // 🤞 а байт 0 — точно показание?

    let raw = bmc.raw_command(0x04, 0x2D, &[0x30])?;
    let fan_rpm = raw[0] as u32;         // 🐛 скорость вентилятора — 2 байта, little-endian

    let raw = bmc.raw_command(0x04, 0x2D, &[0x40])?;
    let voltage = raw[0] as f64;         // 🐛 нужно поделить на 1000

    if cpu_temp > fan_rpm as f64 {       // 🐛 сравниваем °C с об/мин
        println!("упс");
    }

    log_temp(voltage);                   // 🐛 передаём вольты как температуру
    Ok(())
}

fn log_temp(t: f64) { println!("Температура: {t}°C"); }
№ОшибкаКогда обнаружена
1Скорость вентилятора разобрана как 1 байт вместо 2В продакшене, в 3 часа ночи
2Напряжение не масштабированоКаждый блок питания помечен как перенапряжение
3Сравнение °C с об/минВозможно, никогда
4Вольты переданы в логгер температурыЧерез полгода, при чтении исторических данных

Первопричина: всё превращено в Vec<u8> → f64 → и молимся.

Паттерн типизированной команды

Шаг 1 — Доменные newtype

#[derive(Debug, Clone, Copy, PartialEq, PartialOrd)]
pub struct Celsius(pub f64);

#[derive(Debug, Clone, Copy, PartialEq, PartialOrd)]
pub struct Rpm(pub u32);  // u32: сырое целочисленное показание датчика IPMI (об/мин)

#[derive(Debug, Clone, Copy, PartialEq, PartialOrd)]
pub struct Volts(pub f64);

#[derive(Debug, Clone, Copy, PartialEq, PartialOrd)]
pub struct Watts(pub f64);

Примечание о Rpm(u32) и Rpm(f64): в этой главе внутренний тип — u32, потому что показания датчиков IPMI — целые значения. В гл. 06 (анализ размерностей) Rpm использует f64, чтобы поддерживать арифметические операции (усреднение, масштабирование). Оба варианта допустимы: newtype предотвращает путаницу между единицами независимо от внутреннего типа.

Шаг 2 — Трейт команды (диспетчеризация по индексу типа)

Ассоциированный тип Response — ключевой элемент: он связывает каждую структуру команды с её возвращаемым типом. Каждая реализующая структура фиксирует Response на конкретном доменном типе, поэтому execute() всегда возвращает ровно нужный тип:

pub trait IpmiCmd {
    /// «Индекс типа» — определяет, что возвращает execute().
    type Response;

    fn net_fn(&self) -> u8;
    fn cmd_byte(&self) -> u8;
    fn payload(&self) -> Vec<u8>;

    /// Разбор инкапсулирован здесь — каждая команда знает свою раскладку байтов.
    fn parse_response(&self, raw: &[u8]) -> io::Result<Self::Response>;
}

Шаг 3 — Одна структура на команду

pub struct ReadTemp { pub sensor_id: u8 }
impl IpmiCmd for ReadTemp {
    type Response = Celsius;
    fn net_fn(&self) -> u8 { 0x04 }
    fn cmd_byte(&self) -> u8 { 0x2D }
    fn payload(&self) -> Vec<u8> { vec![self.sensor_id] }
    fn parse_response(&self, raw: &[u8]) -> io::Result<Celsius> {
        if raw.is_empty() {
            return Err(io::Error::new(io::ErrorKind::InvalidData, "пустой ответ"));
        }
        // Примечание: нетипизированный пример из гл. 01 использует `raw[0] as i8 as f64`
        // (со знаком), потому что та функция демонстрировала обобщённый разбор без
        // метаданных SDR. Здесь мы используем беззнаковое значение (`as f64`), так как
        // формула линеаризации SDR из раздела 35.5 спецификации IPMI преобразует
        // беззнаковое сырое показание в калиброванное значение. В продакшене примените
        // полную формулу SDR: result = (M × raw + B) × 10^(R_exp).
        Ok(Celsius(raw[0] as f64))  // беззнаковый сырой байт, преобразуется по формуле SDR
    }
}

pub struct ReadFanSpeed { pub fan_id: u8 }
impl IpmiCmd for ReadFanSpeed {
    type Response = Rpm;
    fn net_fn(&self) -> u8 { 0x04 }
    fn cmd_byte(&self) -> u8 { 0x2D }
    fn payload(&self) -> Vec<u8> { vec![self.fan_id] }
    fn parse_response(&self, raw: &[u8]) -> io::Result<Rpm> {
        if raw.len() < 2 {
            return Err(io::Error::new(io::ErrorKind::InvalidData,
                format!("для скорости вентилятора нужно 2 байта, получено {}", raw.len())));
        }
        Ok(Rpm(u16::from_le_bytes([raw[0], raw[1]]) as u32))
    }
}

pub struct ReadVoltage { pub rail: u8 }
impl IpmiCmd for ReadVoltage {
    type Response = Volts;
    fn net_fn(&self) -> u8 { 0x04 }
    fn cmd_byte(&self) -> u8 { 0x2D }
    fn payload(&self) -> Vec<u8> { vec![self.rail] }
    fn parse_response(&self, raw: &[u8]) -> io::Result<Volts> {
        if raw.len() < 2 {
            return Err(io::Error::new(io::ErrorKind::InvalidData,
                format!("для напряжения нужно 2 байта, получено {}", raw.len())));
        }
        Ok(Volts(u16::from_le_bytes([raw[0], raw[1]]) as f64 / 1000.0))
    }
}

Шаг 4 — Исполнитель (без dyn, мономорфизированный)

pub struct BmcConnection { pub timeout_secs: u32 }

impl BmcConnection {
    pub fn execute<C: IpmiCmd>(&self, cmd: &C) -> io::Result<C::Response> {
        let raw = self.raw_send(cmd.net_fn(), cmd.cmd_byte(), &cmd.payload())?;
        cmd.parse_response(&raw)
    }

    fn raw_send(&self, _nf: u8, _cmd: u8, _data: &[u8]) -> io::Result<Vec<u8>> {
        Ok(vec![0x19, 0x00]) // заглушка
    }
}

Шаг 5 — Все четыре ошибки становятся ошибками компиляции

fn diagnose_thermal_typed(bmc: &BmcConnection) -> io::Result<()> {
    let cpu_temp: Celsius = bmc.execute(&ReadTemp { sensor_id: 0x20 })?;
    let fan_rpm:  Rpm     = bmc.execute(&ReadFanSpeed { fan_id: 0x30 })?;
    let voltage:  Volts   = bmc.execute(&ReadVoltage { rail: 0x40 })?;

    // Ошибка №1 — НЕВОЗМОЖНА: разбор находится в ReadFanSpeed::parse_response
    // Ошибка №2 — НЕВОЗМОЖНА: масштабирование единиц находится в ReadVoltage::parse_response

    // Ошибка №3 — ОШИБКА КОМПИЛЯЦИИ:
    // if cpu_temp > fan_rpm { }
    //    ^^^^^^^^   ^^^^^^^ Celsius и Rpm → "mismatched types" ❌

    // Ошибка №4 — ОШИБКА КОМПИЛЯЦИИ:
    // log_temperature(voltage);
    //                 ^^^^^^^ Volts, ожидается Celsius ❌

    if cpu_temp > Celsius(85.0) { println!("Перегрев CPU: {:?}", cpu_temp); }
    if fan_rpm < Rpm(4000)      { println!("Вентилятор слишком медленный: {:?}", fan_rpm); }

    Ok(())
}

fn log_temperature(t: Celsius) { println!("Температура: {:?}", t); }
fn log_voltage(v: Volts)       { println!("Напряжение: {:?}", v); }

IPMI: показания датчиков, которые нельзя перепутать

Добавление нового датчика — это одна структура и одна реализация, без разбросанного по коду разбора:

pub struct ReadPowerDraw { pub domain: u8 }
impl IpmiCmd for ReadPowerDraw {
    type Response = Watts;
    fn net_fn(&self) -> u8 { 0x04 }
    fn cmd_byte(&self) -> u8 { 0x2D }
    fn payload(&self) -> Vec<u8> { vec![self.domain] }
    fn parse_response(&self, raw: &[u8]) -> io::Result<Watts> {
        if raw.len() < 2 {
            return Err(io::Error::new(io::ErrorKind::InvalidData,
                format!("для потребляемой мощности нужно 2 байта, получено {}", raw.len())));
        }
        Ok(Watts(u16::from_le_bytes([raw[0], raw[1]]) as f64))
    }
}

// Любой вызов bmc.execute(&ReadPowerDraw { domain: 0 }) автоматически возвращает Watts —
// никакого кода разбора в других местах

Тестирование каждой команды изолированно

#[cfg(test)]
mod tests {
    use super::*;

    struct StubBmc {
        responses: std::collections::HashMap<u8, Vec<u8>>,
    }

    impl StubBmc {
        fn execute<C: IpmiCmd>(&self, cmd: &C) -> io::Result<C::Response> {
            let key = cmd.payload()[0];
            let raw = self.responses.get(&key)
                .ok_or_else(|| io::Error::new(io::ErrorKind::NotFound, "нет заглушки"))?;
            cmd.parse_response(raw)
        }
    }

    #[test]
    fn read_temp_parses_raw_byte() {
        let bmc = StubBmc {
            responses: [(0x20, vec![0x19])].into(), // 25 в десятичной = 0x19
        };
        let temp = bmc.execute(&ReadTemp { sensor_id: 0x20 }).unwrap();
        assert_eq!(temp, Celsius(25.0));
    }

    #[test]
    fn read_fan_parses_two_byte_le() {
        let bmc = StubBmc {
            responses: [(0x30, vec![0x00, 0x19])].into(), // 0x1900 = 6400
        };
        let rpm = bmc.execute(&ReadFanSpeed { fan_id: 0x30 }).unwrap();
        assert_eq!(rpm, Rpm(6400));
    }

    #[test]
    fn read_voltage_scales_millivolts() {
        let bmc = StubBmc {
            responses: [(0x40, vec![0xE8, 0x2E])].into(), // 0x2EE8 = 12008 мВ
        };
        let v = bmc.execute(&ReadVoltage { rail: 0x40 }).unwrap();
        assert!((v.0 - 12.008).abs() < 0.001);
    }
}

Redfish: REST-эндпоинты с типизированной схемой

Redfish подходит ещё лучше: каждый эндпоинт возвращает JSON-схему, определённую DMTF:

use serde::Deserialize;

#[derive(Debug, Deserialize)]
pub struct ThermalResponse {
    #[serde(rename = "Temperatures")]
    pub temperatures: Vec<RedfishTemp>,
    #[serde(rename = "Fans")]
    pub fans: Vec<RedfishFan>,
}

#[derive(Debug, Deserialize)]
pub struct RedfishTemp {
    #[serde(rename = "Name")]
    pub name: String,
    #[serde(rename = "ReadingCelsius")]
    pub reading: f64,
    #[serde(rename = "UpperThresholdCritical")]
    pub critical_hi: Option<f64>,
    #[serde(rename = "Status")]
    pub status: RedfishHealth,
}

#[derive(Debug, Deserialize)]
pub struct RedfishFan {
    #[serde(rename = "Name")]
    pub name: String,
    #[serde(rename = "Reading")]
    pub rpm: u32,
    #[serde(rename = "Status")]
    pub status: RedfishHealth,
}

#[derive(Debug, Deserialize)]
pub struct PowerResponse {
    #[serde(rename = "Voltages")]
    pub voltages: Vec<RedfishVoltage>,
    #[serde(rename = "PowerSupplies")]
    pub psus: Vec<RedfishPsu>,
}

#[derive(Debug, Deserialize)]
pub struct RedfishVoltage {
    #[serde(rename = "Name")]
    pub name: String,
    #[serde(rename = "ReadingVolts")]
    pub reading: f64,
    #[serde(rename = "Status")]
    pub status: RedfishHealth,
}

#[derive(Debug, Deserialize)]
pub struct RedfishPsu {
    #[serde(rename = "Name")]
    pub name: String,
    #[serde(rename = "PowerOutputWatts")]
    pub output_watts: Option<f64>,
    #[serde(rename = "Status")]
    pub status: RedfishHealth,
}

#[derive(Debug, Deserialize)]
pub struct ProcessorResponse {
    #[serde(rename = "Model")]
    pub model: String,
    #[serde(rename = "TotalCores")]
    pub cores: u32,
    #[serde(rename = "Status")]
    pub status: RedfishHealth,
}

#[derive(Debug, Deserialize)]
pub struct RedfishHealth {
    #[serde(rename = "State")]
    pub state: String,
    #[serde(rename = "Health")]
    pub health: Option<String>,
}

/// Типизированный эндпоинт Redfish — каждый знает тип своего ответа.
pub trait RedfishEndpoint {
    type Response: serde::de::DeserializeOwned;
    fn method(&self) -> &'static str;
    fn path(&self) -> String;
}

pub struct GetThermal { pub chassis_id: String }
impl RedfishEndpoint for GetThermal {
    type Response = ThermalResponse;
    fn method(&self) -> &'static str { "GET" }
    fn path(&self) -> String {
        format!("/redfish/v1/Chassis/{}/Thermal", self.chassis_id)
    }
}

pub struct GetPower { pub chassis_id: String }
impl RedfishEndpoint for GetPower {
    type Response = PowerResponse;
    fn method(&self) -> &'static str { "GET" }
    fn path(&self) -> String {
        format!("/redfish/v1/Chassis/{}/Power", self.chassis_id)
    }
}

pub struct GetProcessor { pub system_id: String, pub proc_id: String }
impl RedfishEndpoint for GetProcessor {
    type Response = ProcessorResponse;
    fn method(&self) -> &'static str { "GET" }
    fn path(&self) -> String {
        format!("/redfish/v1/Systems/{}/Processors/{}", self.system_id, self.proc_id)
    }
}

pub struct RedfishClient {
    pub base_url: String,
    pub auth_token: String,
}

impl RedfishClient {
    pub fn execute<E: RedfishEndpoint>(&self, endpoint: &E) -> io::Result<E::Response> {
        let url = format!("{}{}", self.base_url, endpoint.path());
        let json_bytes = self.http_request(endpoint.method(), &url)?;
        serde_json::from_slice(&json_bytes)
            .map_err(|e| io::Error::new(io::ErrorKind::InvalidData, e))
    }

    fn http_request(&self, _method: &str, _url: &str) -> io::Result<Vec<u8>> {
        Ok(vec![]) // заглушка — реальная реализация использует reqwest/hyper
    }
}

// Использование — полностью типизировано, код говорит сам за себя
fn redfish_pre_flight(client: &RedfishClient) -> io::Result<()> {
    let thermal: ThermalResponse = client.execute(&GetThermal {
        chassis_id: "1".into(),
    })?;
    let power: PowerResponse = client.execute(&GetPower {
        chassis_id: "1".into(),
    })?;

    // ❌ Ошибка компиляции — нельзя передать PowerResponse в проверку температуры:
    // check_thermals(&power);  → "expected ThermalResponse, found PowerResponse"

    for temp in &thermal.temperatures {
        if let Some(crit) = temp.critical_hi {
            if temp.reading > crit {
                println!("КРИТИЧНО: {} при {}°C (порог: {}°C)",
                    temp.name, temp.reading, crit);
            }
        }
    }
    Ok(())
}

NVMe Admin: Identify не возвращает журнальные страницы

Команды NVMe Admin устроены так же. Контроллер различает коды команд (opcode), но в C вызывающий код должен знать, какую структуру наложить на 4-килобайтный буфер завершения. Паттерн типизированной команды делает такую ошибку невозможной:

use std::io;

/// Трейт команды NVMe Admin — той же формы, что IpmiCmd.
pub trait NvmeAdminCmd {
    type Response;
    fn opcode(&self) -> u8;
    fn parse_completion(&self, data: &[u8]) -> io::Result<Self::Response>;
}

// ── Identify (opcode 0x06) ──

#[derive(Debug, Clone)]
pub struct IdentifyResponse {
    pub model_number: String,   // байты 24–63
    pub serial_number: String,  // байты 4–23
    pub firmware_rev: String,   // байты 64–71
    pub total_capacity_gb: u64,
}

pub struct Identify {
    pub nsid: u32, // 0 = контроллер, >0 = пространство имён
}

impl NvmeAdminCmd for Identify {
    type Response = IdentifyResponse;
    fn opcode(&self) -> u8 { 0x06 }
    fn parse_completion(&self, data: &[u8]) -> io::Result<IdentifyResponse> {
        if data.len() < 4096 {
            return Err(io::Error::new(io::ErrorKind::InvalidData, "короткий ответ identify"));
        }
        Ok(IdentifyResponse {
            serial_number: String::from_utf8_lossy(&data[4..24]).trim().to_string(),
            model_number: String::from_utf8_lossy(&data[24..64]).trim().to_string(),
            firmware_rev: String::from_utf8_lossy(&data[64..72]).trim().to_string(),
            total_capacity_gb: u64::from_le_bytes(
                data[280..288].try_into().unwrap()
            ) / (1024 * 1024 * 1024),
        })
    }
}

// ── Get Log Page (opcode 0x02) ──

#[derive(Debug, Clone)]
pub struct SmartLog {
    pub critical_warning: u8,
    pub temperature_kelvin: u16,
    pub available_spare_pct: u8,
    pub data_units_read: u128,
}

pub struct GetLogPage {
    pub log_id: u8, // 0x02 = SMART/Health
}

impl NvmeAdminCmd for GetLogPage {
    type Response = SmartLog;
    fn opcode(&self) -> u8 { 0x02 }
    fn parse_completion(&self, data: &[u8]) -> io::Result<SmartLog> {
        if data.len() < 512 {
            return Err(io::Error::new(io::ErrorKind::InvalidData, "короткая страница журнала"));
        }
        Ok(SmartLog {
            critical_warning: data[0],
            temperature_kelvin: u16::from_le_bytes([data[1], data[2]]),
            available_spare_pct: data[3],
            data_units_read: u128::from_le_bytes(data[32..48].try_into().unwrap()),
        })
    }
}

// ── Исполнитель ──

pub struct NvmeController { /* дескриптор, BAR и т. д. */ }

impl NvmeController {
    pub fn admin_cmd<C: NvmeAdminCmd>(&self, cmd: &C) -> io::Result<C::Response> {
        let raw = self.submit_and_wait(cmd.opcode())?;
        cmd.parse_completion(&raw)
    }

    fn submit_and_wait(&self, _opcode: u8) -> io::Result<Vec<u8>> {
        Ok(vec![0u8; 4096]) // заглушка — реальная реализация записывает doorbell и ждёт запись в CQ
    }
}

// ── Использование ──

fn nvme_health_check(ctrl: &NvmeController) -> io::Result<()> {
    let id: IdentifyResponse = ctrl.admin_cmd(&Identify { nsid: 0 })?;
    let smart: SmartLog = ctrl.admin_cmd(&GetLogPage { log_id: 0x02 })?;

    // ❌ Ошибка компиляции: Identify возвращает IdentifyResponse, а не SmartLog:
    // let smart: SmartLog = ctrl.admin_cmd(&Identify { nsid: 0 })?;

    println!("{} (FW {}): {}°C, {}% резерва",
        id.model_number, id.firmware_rev,
        smart.temperature_kelvin.saturating_sub(273),
        smart.available_spare_pct);

    Ok(())
}

Трёхпротокольная прогрессия теперь следует постепенной дуге (та же техника, что в гл. 07 для проверенных границ):

ЭтапПротоколСложностьЧто добавляет
1IPMIПросто: ID датчика → показаниеБазовый паттерн: trait + associated type
2RedfishREST: эндпоинт → типизированный JSONИнтеграция с serde, типизированные ответы по схеме
3NVMeБинарный: код операции → наложение структуры на 4 КБРазбор сырого буфера, данные завершения из нескольких структур

Расширение: макро-DSL для сценариев команд

/// Выполняет серию типизированных команд IPMI и возвращает кортеж результатов.
macro_rules! diag_script {
    ($bmc:expr; $($cmd:expr),+ $(,)?) => {{
        ( $( $bmc.execute(&$cmd)?, )+ )
    }};
}

fn full_pre_flight(bmc: &BmcConnection) -> io::Result<()> {
    let (temp, rpm, volts) = diag_script!(bmc;
        ReadTemp     { sensor_id: 0x20 },
        ReadFanSpeed { fan_id:    0x30 },
        ReadVoltage  { rail:      0x40 },
    );
    // Тип: (Celsius, Rpm, Volts) — выводится полностью; перестановка даёт ошибку компиляции
    assert!(temp  < Celsius(95.0), "ЦП слишком горячий");
    assert!(rpm   > Rpm(3000),     "Вентилятор слишком медленный");
    assert!(volts > Volts(11.4),   "Линия 12 В просела");
    Ok(())
}

Расширение: диспетчеризация через enum для динамических сценариев

Когда команды приходят из JSON-конфигурации во время выполнения:

pub enum AnyReading {
    Temp(Celsius),
    Rpm(Rpm),
    Volt(Volts),
    Watt(Watts),
}

pub enum AnyCmd {
    Temp(ReadTemp),
    Fan(ReadFanSpeed),
    Voltage(ReadVoltage),
    Power(ReadPowerDraw),
}

impl AnyCmd {
    pub fn execute(&self, bmc: &BmcConnection) -> io::Result<AnyReading> {
        match self {
            AnyCmd::Temp(c)    => Ok(AnyReading::Temp(bmc.execute(c)?)),
            AnyCmd::Fan(c)     => Ok(AnyReading::Rpm(bmc.execute(c)?)),
            AnyCmd::Voltage(c) => Ok(AnyReading::Volt(bmc.execute(c)?)),
            AnyCmd::Power(c)   => Ok(AnyReading::Watt(bmc.execute(c)?)),
        }
    }
}

fn run_dynamic_script(bmc: &BmcConnection, script: &[AnyCmd]) -> io::Result<Vec<AnyReading>> {
    script.iter().map(|cmd| cmd.execute(bmc)).collect()
}

Семейство паттернов

Этот паттерн применим к каждому протоколу управления аппаратным обеспечением:

ПротоколТип запросаТип ответа
Показание датчика IPMIReadTempCelsius
Redfish RESTGetThermalThermalResponse
NVMe AdminIdentifyIdentifyResponse
PLDMGetFwParamsFwParamsResponse
MCTPGetEidEidResponse
Конфигурационное пространство PCIeReadCapabilityCapabilityHeader
SMBIOS/DMIReadType17MemoryDeviceInfo

Тип запроса определяет тип ответа — компилятор обеспечивает это везде.

Поток типизированных команд

flowchart LR
    subgraph "Этап компиляции"
        RT["ReadTemp"] -->|"type Response = Celsius"| C[Celsius]
        RF["ReadFanSpeed"] -->|"type Response = Rpm"| R[Rpm]
        RV["ReadVoltage"] -->|"type Response = Volts"| V[Volts]
    end
    subgraph "Рантайм"
        E["bmc.execute(&cmd)"] -->|"мономорфизировано"| P["cmd.parse_response(raw)"]
    end
    style RT fill:#e1f5fe,color:#000
    style RF fill:#e1f5fe,color:#000
    style RV fill:#e1f5fe,color:#000
    style C fill:#c8e6c9,color:#000
    style R fill:#c8e6c9,color:#000
    style V fill:#c8e6c9,color:#000
    style E fill:#fff3e0,color:#000
    style P fill:#fff3e0,color:#000

Упражнение: типизированные команды PLDM

Спроектируйте трейт PldmCmd (той же формы, что IpmiCmd) для двух команд PLDM:

  • GetFwParams → FwParamsResponse { active_version: String, pending_version: Option<String> }
  • QueryDeviceIds → DeviceIdResponse { descriptors: Vec<Descriptor> }

Требования: статическая диспетчеризация, parse_response возвращает io::Result<Self::Response>.

Решение
use std::io;

pub trait PldmCmd {
    type Response;
    fn pldm_type(&self) -> u8;
    fn command_code(&self) -> u8;
    fn parse_response(&self, raw: &[u8]) -> io::Result<Self::Response>;
}

#[derive(Debug, Clone)]
pub struct FwParamsResponse {
    pub active_version: String,
    pub pending_version: Option<String>,
}

pub struct GetFwParams;
impl PldmCmd for GetFwParams {
    type Response = FwParamsResponse;
    fn pldm_type(&self) -> u8 { 0x05 } // Обновление прошивки
    fn command_code(&self) -> u8 { 0x02 }
    fn parse_response(&self, raw: &[u8]) -> io::Result<FwParamsResponse> {
        // Упрощённо — реальная реализация декодирует поля спецификации PLDM FW Update
        if raw.len() < 4 {
            return Err(io::Error::new(io::ErrorKind::InvalidData, "слишком коротко"));
        }
        Ok(FwParamsResponse {
            active_version: String::from_utf8_lossy(&raw[..4]).to_string(),
            pending_version: None,
        })
    }
}

#[derive(Debug, Clone)]
pub struct Descriptor { pub descriptor_type: u16, pub data: Vec<u8> }

#[derive(Debug, Clone)]
pub struct DeviceIdResponse { pub descriptors: Vec<Descriptor> }

pub struct QueryDeviceIds;
impl PldmCmd for QueryDeviceIds {
    type Response = DeviceIdResponse;
    fn pldm_type(&self) -> u8 { 0x05 }
    fn command_code(&self) -> u8 { 0x04 }
    fn parse_response(&self, raw: &[u8]) -> io::Result<DeviceIdResponse> {
        Ok(DeviceIdResponse { descriptors: vec![] }) // заглушка
    }
}

Ключевые выводы

  1. Ассоциированный тип — это контракт на этапе компиляции — type Response в трейте команды жёстко связывает каждый запрос ровно с одним типом ответа.
  2. Разбор инкапсулирован — знание о раскладке байтов живёт в parse_response, а не разбросано по вызывающему коду.
  3. Диспетчеризация без накладных расходов — обобщённая функция execute<C: IpmiCmd> мономорфизируется в прямые вызовы без vtable.
  4. Один паттерн, много протоколов — IPMI, Redfish, NVMe, PLDM и MCTP укладываются в одну и ту же форму trait Cmd { type Response; }.
  5. Диспетчеризация через enum связывает статику и динамику — оборачивайте типизированные команды в enum для сценариев, управляемых во время выполнения, не теряя типобезопасности внутри каждой ветки.
  6. Постепенное усложнение укрепляет интуицию — IPMI (ID датчика → показание), Redfish (эндпоинт → JSON-схема) и NVMe (код операции → наложение структуры на 4 КБ) используют одну и ту же форму трейта, но каждый этап добавляет ещё один слой сложности разбора.

Одноразовые типы: криптографические гарантии через владение 🟡

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

Перекрёстные ссылки: гл. 01 (философия), гл. 04 (capability-токены), гл. 05 (typestate), гл. 14 (тестирование compile-fail)

Катастрофа повторного использования nonce

В аутентифицированном шифровании (AES-GCM, ChaCha20-Poly1305) повторное использование nonce с тем же ключом — катастрофа: оно раскрывает XOR двух открытых текстов и часто сам ключ аутентификации. Это не теоретическая проблема:

  • 2016: Forbidden Attack на AES-GCM в TLS — повторное использование nonce позволяло восстановить открытый текст
  • 2020: в нескольких системах обновления прошивок IoT обнаружили повторное использование nonce из-за слабого генератора случайных чисел

В C/C++ nonce — это просто uint8_t[12]. Ничто не мешает использовать его дважды.

// C — ничто не мешает повторно использовать nonce
uint8_t nonce[12];
generate_nonce(nonce);
encrypt(key, nonce, msg1, out1);   // ✅ первое использование
encrypt(key, nonce, msg2, out2);   // 🐛 КАТАСТРОФА: тот же nonce

Семантика перемещения как линейные типы

Система владения Rust по сути является системой линейных типов: значение можно использовать ровно один раз (переместив его), если только тип не реализует Copy. Крейт ring использует это:

// ring::aead::Nonce:
// - НЕ Clone
// - НЕ Copy
// - потребляется по значению при использовании
pub struct Nonce(/* приватное */);

impl Nonce {
    pub fn try_assume_unique_for_key(value: &[u8]) -> Result<Self, Unspecified> {
        // ...
    }
    // Нет Clone, нет Copy — использовать можно только один раз
}

Когда вы передаёте Nonce в seal_in_place(), он перемещается:

// Псевдокод, повторяющий форму API ring
fn seal_in_place(
    key: &SealingKey,
    nonce: Nonce,       // ← перемещается, а не заимствуется
    data: &mut Vec<u8>,
) -> Result<(), Error> {
    // ... шифруем данные на месте ...
    // nonce потреблён — использовать его снова нельзя
    Ok(())
}

Попытка использовать его повторно:

fn bad_encrypt(key: &SealingKey, data1: &mut Vec<u8>, data2: &mut Vec<u8>) {
    // .unwrap() безопасен — массив из 12 байт всегда является корректным nonce.
    let nonce = Nonce::try_assume_unique_for_key(&[0u8; 12]).unwrap();
    seal_in_place(key, nonce, data1).unwrap();  // ✅ nonce перемещён здесь
    // seal_in_place(key, nonce, data2).unwrap();
    //                    ^^^^^ ОШИБКА: использование перемещённого значения ❌
}

Компилятор доказывает, что каждый nonce используется ровно один раз. Тесты для этого не нужны.

Разбор: nonce в ring

Крейт ring идёт дальше с NonceSequence — трейтом, который генерирует nonce и тоже не может быть клонирован:

/// Последовательность уникальных nonce.
/// Не Clone — после привязки к ключу не может быть продублирована.
pub trait NonceSequence {
    fn advance(&mut self) -> Result<Nonce, Unspecified>;
}

/// SealingKey оборачивает NonceSequence — каждый seal() автоматически продвигает её.
pub struct SealingKey<N: NonceSequence> {
    key: UnboundKey,   // потребляется при создании
    nonce_seq: N,
}

impl<N: NonceSequence> SealingKey<N> {
    pub fn new(key: UnboundKey, nonce_seq: N) -> Self {
        // UnboundKey перемещается — нельзя использовать и для шифрования, и для расшифровки
        SealingKey { key, nonce_seq }
    }

    pub fn seal_in_place_append_tag(
        &mut self,       // &mut — эксклюзивный доступ
        aad: Aad<&[u8]>,
        in_out: &mut Vec<u8>,
    ) -> Result<(), Unspecified> {
        let nonce = self.nonce_seq.advance()?; // автоматически генерируем уникальный nonce
        // ... шифруем с nonce ...
        Ok(())
    }
}
pub struct UnboundKey;
pub struct Aad<T>(T);
pub struct Unspecified;

Цепочка владения предотвращает:

  1. Повторное использование nonce — Nonce не Clone и потребляется при каждом вызове
  2. Дублирование ключа — UnboundKey перемещается в SealingKey, поэтому из него нельзя также создать OpeningKey
  3. Дублирование последовательности — NonceSequence не Clone, поэтому два ключа не могут делить один счётчик

Ни одна из этих гарантий не требует проверок во время выполнения. Компилятор обеспечивает все три.

Разбор: эфемерное согласование ключей

Эфемерные ключи Диффи — Хеллмана должны использоваться ровно один раз (именно это и означает «эфемерный»). ring обеспечивает это:

/// Эфемерный приватный ключ. Не Clone, не Copy.
/// Потребляется в agree_ephemeral().
pub struct EphemeralPrivateKey { /* ... */ }

/// Вычисляет общий секрет — потребляет приватный ключ.
pub fn agree_ephemeral(
    my_private_key: EphemeralPrivateKey,  // ← перемещается
    peer_public_key: &UnparsedPublicKey,
    error_value: Unspecified,
    kdf: impl FnOnce(&[u8]) -> Result<SharedSecret, Unspecified>,
) -> Result<SharedSecret, Unspecified> {
    // ... вычисление DH ...
    // my_private_key потреблён — переиспользовать его невозможно
    kdf(&[])
}
pub struct UnparsedPublicKey;
pub struct SharedSecret;
pub struct Unspecified;

После вызова agree_ephemeral() приватный ключ больше не существует в памяти (он уничтожен). Разработчику на C++ пришлось бы не забыть вызвать memset(key, 0, len) и надеяться, что компилятор не оптимизирует этот вызов. В Rust ключ просто исчезает.

Аппаратное применение: однократное программирование предохранителей

Серверные платформы содержат OTP-предохранители (one-time programmable) для ключей безопасности, серийных номеров плат и битов функций. Запись в предохранитель необратима: если сделать это дважды с разными данными, плата выходит из строя. Это идеально подходит для семантики перемещения:

use std::io;

/// Полезная нагрузка для записи в предохранитель. Не Clone, не Copy.
/// Потребляется при программировании предохранителя.
pub struct FusePayload {
    address: u32,
    data: Vec<u8>,
    // приватный конструктор — создаётся только через проверенный builder
}

/// Доказательство того, что программатор предохранителей находится в корректном состоянии.
pub struct FuseController {
    /* дескриптор аппаратуры */
}

impl FuseController {
    /// Программирует предохранитель — потребляет полезную нагрузку, исключая двойную запись.
    pub fn program(
        &mut self,
        payload: FusePayload,  // ← перемещается — нельзя использовать дважды
    ) -> io::Result<()> {
        // ... запись в OTP-аппаратуру ...
        // payload потреблён — повторное программирование с тем же payload
        // является ошибкой компиляции
        Ok(())
    }
}

/// Builder с валидацией — единственный способ создать FusePayload.
pub struct FusePayloadBuilder {
    address: Option<u32>,
    data: Option<Vec<u8>>,
}

impl FusePayloadBuilder {
    pub fn new() -> Self {
        FusePayloadBuilder { address: None, data: None }
    }

    pub fn address(mut self, addr: u32) -> Self {
        self.address = Some(addr);
        self
    }

    pub fn data(mut self, data: Vec<u8>) -> Self {
        self.data = Some(data);
        self
    }

    pub fn build(self) -> Result<FusePayload, &'static str> {
        let address = self.address.ok_or("требуется адрес")?;
        let data = self.data.ok_or("требуются данные")?;
        if data.len() > 32 { return Err("данные предохранителя слишком длинные"); }
        Ok(FusePayload { address, data })
    }
}

// Использование:
fn program_board_serial(ctrl: &mut FuseController) -> io::Result<()> {
    let payload = FusePayloadBuilder::new()
        .address(0x100)
        .data(b"SN12345678".to_vec())
        .build()
        .map_err(|e| io::Error::new(io::ErrorKind::InvalidInput, e))?;

    ctrl.program(payload)?;      // ✅ payload потреблён

    // ctrl.program(payload);    // ❌ ОШИБКА: использование перемещённого значения
    //              ^^^^^^^ значение используется после перемещения

    Ok(())
}

Аппаратное применение: одноразовый токен калибровки

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

/// Выдаётся один раз при включении питания. Не Clone, не Copy.
pub struct CalibrationToken {
    _private: (),
}

pub struct SensorController {
    calibrated: bool,
}

impl SensorController {
    /// Вызывается один раз при включении питания — возвращает токен калибровки.
    pub fn power_on() -> (Self, CalibrationToken) {
        (
            SensorController { calibrated: false },
            CalibrationToken { _private: () },
        )
    }

    /// Калибрует датчик — потребляет токен.
    pub fn calibrate(&mut self, _token: CalibrationToken) -> io::Result<()> {
        // ... выполняем последовательность калибровки ...
        self.calibrated = true;
        Ok(())
    }

    /// Читает датчик — имеет смысл только после калибровки.
    ///
    /// **Ограничение:** гарантия семантики перемещения здесь *частичная*. Вызывающий код
    /// может сделать `drop(cal_token)`, не вызывая `calibrate()`: токен будет уничтожен,
    /// но калибровка не выполнится. Аннотация `#[must_use]` (см. ниже) выдаёт предупреждение,
    /// но не жёсткую ошибку.
    ///
    /// Проверка `self.calibrated` во время выполнения — это **страховочная сетка** для
    /// этого пробела. Полностью решить задачу на этапе компиляции позволяет паттерн
    /// typestate из гл. 05, где `send_command()` существует только у `IpmiSession<Active>`.
    pub fn read(&self) -> io::Result<f64> {
        if !self.calibrated {
            return Err(io::Error::new(io::ErrorKind::Other, "датчик не откалиброван"));
        }
        Ok(25.0) // заглушка
    }
}

fn sensor_workflow() -> io::Result<()> {
    let (mut ctrl, cal_token) = SensorController::power_on();

    // Токен нужно где-то использовать: он не Copy, поэтому отбрасывание без потребления
    // даёт предупреждение (или ошибку при #[must_use])
    ctrl.calibrate(cal_token)?;

    // Теперь чтение работает:
    let temp = ctrl.read()?;
    println!("Температура: {temp}°C");

    // Повторно калибровать нельзя — токен потреблён:
    // ctrl.calibrate(cal_token);  // ❌ использование перемещённого значения

    Ok(())
}

Когда использовать одноразовые типы

СценарийИспользовать семантику одноразового (перемещаемого) значения?
Криптографические nonce✅ Всегда: повторное использование nonce катастрофично
Эфемерные ключи (DH, ECDH)✅ Всегда: повторное использование ослабляет прямую секретность
Запись в OTP-предохранители✅ Всегда: двойная запись выводит аппаратуру из строя
Коды активации лицензий✅ Обычно: предотвращает двойную активацию
Токены калибровки✅ Обычно: обеспечивает однократность за сессию
Дескрипторы записи файлов⚠️ Иногда: зависит от протокола
Дескрипторы транзакций БД⚠️ Иногда: commit/rollback выполняются однократно
Общие буферы данных❌ Их нужно переиспользовать: используйте &mut [u8]

Поток одноразового владения

flowchart LR
    N["Nonce::new()"] -->|перемещение| E["encrypt(nonce, msg)"]
    E -->|потреблён| X["❌ nonce уничтожен"]
    N -.->|"попытка повторного использования"| ERR["ОШИБКА КОМПИЛЯЦИИ:<br/>use of moved value"]
    style N fill:#e1f5fe,color:#000
    style E fill:#c8e6c9,color:#000
    style X fill:#ffcdd2,color:#000
    style ERR fill:#ffcdd2,color:#000

Упражнение: одноразовый токен подписи прошивки

Спроектируйте SigningToken, который можно использовать ровно один раз для подписи образа прошивки:

  • SigningToken::issue(key_id: &str) -> SigningToken (не Clone, не Copy)
  • sign(token: SigningToken, image: &[u8]) -> SignedImage (потребляет токен)
  • Попытка подписать дважды должна приводить к ошибке компиляции.
Решение
pub struct SigningToken {
    key_id: String,
    // НЕ Clone, НЕ Copy
}

pub struct SignedImage {
    pub signature: Vec<u8>,
    pub key_id: String,
}

impl SigningToken {
    pub fn issue(key_id: &str) -> Self {
        SigningToken { key_id: key_id.to_string() }
    }
}

pub fn sign(token: SigningToken, _image: &[u8]) -> SignedImage {
    // Токен потреблён перемещением — переиспользовать его нельзя
    SignedImage {
        signature: vec![0xDE, 0xAD],  // заглушка
        key_id: token.key_id,
    }
}

// ✅ Компилируется:
// let tok = SigningToken::issue("release-key");
// let signed = sign(tok, &firmware_bytes);
//
// ❌ Ошибка компиляции:
// let signed2 = sign(tok, &other_bytes);  // ОШИБКА: использование перемещённого значения

Ключевые выводы

  1. Перемещение = линейное использование — тип, который не является ни Clone, ни Copy, можно потребить ровно один раз; это обеспечивает компилятор.
  2. Повторное использование nonce катастрофично — система владения Rust предотвращает его структурно, а не дисциплиной.
  3. Паттерн выходит за рамки криптографии — OTP-предохранители, токены калибровки, записи аудита: всё, что должно произойти не более одного раза.
  4. Эфемерные ключи получают прямую секретность бесплатно — значение ключевого соглашения перемещается в производный секрет и исчезает.
  5. Если сомневаетесь, уберите Clone — его всегда можно добавить позже; удаление из опубликованного API — ломающее изменение.

Capability-токены: доказательство полномочий без накладных расходов 🟡

Что вы узнаете: как типы нулевого размера (ZST) выступают токенами-доказательствами на этапе компиляции и обеспечивают иерархию привилегий, порядок подачи питания и отзываемые полномочия, причём без накладных расходов во время выполнения.

Перекрёстные ссылки: гл. 03 (одноразовые типы), гл. 05 (typestate), гл. 08 (миксины), гл. 10 (интеграция)

Проблема: кому что разрешено?

В аппаратной диагностике некоторые операции опасны:

  • Прошивка BMC
  • Сброс линков PCIe
  • Запись OTP-предохранителей
  • Включение тестовых режимов с высоким напряжением

В C/C++ такие операции защищают проверками во время выполнения:

// C — проверка прав во время выполнения
int reset_pcie_link(bmc_handle_t bmc, int slot) {
    if (!bmc->is_admin) {        // проверка во время выполнения
        return -EPERM;
    }
    if (!bmc->link_trained) {    // ещё одна проверка во время выполнения
        return -EINVAL;
    }
    // ... выполняем опасное действие ...
    return 0;
}

Каждая функция, которая делает что-то опасное, должна повторять эти проверки. Забудьте одну — и у вас ошибка эскалации привилегий.

Типы нулевого размера как токены-доказательства

Capability-токен — это тип нулевого размера (ZST), который доказывает, что у вызывающего кода есть полномочия на действие. Во время выполнения он не занимает ни одного байта: он существует только в системе типов:

use std::marker::PhantomData;

/// Доказательство того, что вызывающий обладает правами администратора.
/// Нулевой размер — полностью исчезает при компиляции.
/// Не Clone, не Copy — должен передаваться явно.
pub struct AdminToken {
    _private: (),   // запрещает создание вне этого модуля
}

/// Доказательство того, что линк PCIe обучен и готов.
pub struct LinkTrainedToken {
    _private: (),
}

pub struct BmcController { /* ... */ }

impl BmcController {
    /// Аутентификация как администратор — возвращает capability-токен.
    /// Это ЕДИНСТВЕННЫЙ способ создать AdminToken.
    pub fn authenticate_admin(
        &mut self,
        credentials: &[u8],
    ) -> Result<AdminToken, &'static str> {
        // ... проверяем учётные данные ...
        let valid = true;
        if valid {
            Ok(AdminToken { _private: () })
        } else {
            Err("аутентификация не пройдена")
        }
    }

    /// Обучает линк PCIe — возвращает доказательство того, что он обучен.
    pub fn train_link(&mut self) -> Result<LinkTrainedToken, &'static str> {
        // ... выполняем обучение линка ...
        Ok(LinkTrainedToken { _private: () })
    }

    /// Сбрасывает линк PCIe — требует ОБА доказательства: администратора и обученного линка.
    /// Никаких проверок во время выполнения не нужно: сами токены и есть доказательство.
    pub fn reset_pcie_link(
        &mut self,
        _admin: &AdminToken,         // zero-cost доказательство полномочий
        _trained: &LinkTrainedToken,  // zero-cost доказательство состояния
        slot: u32,
    ) -> Result<(), &'static str> {
        println!("Сброс линка PCIe в слоте {slot}");
        Ok(())
    }
}

Использование: система типов обеспечивает соблюдение порядка действий:

fn maintenance_workflow(bmc: &mut BmcController) -> Result<(), &'static str> {
    // Шаг 1: аутентификация — получаем доказательство администратора
    let admin = bmc.authenticate_admin(b"secret")?;

    // Шаг 2: обучение линка — получаем доказательство обучения
    let trained = bmc.train_link()?;

    // Шаг 3: сброс — компилятор требует оба токена
    bmc.reset_pcie_link(&admin, &trained, 0)?;

    Ok(())
}

// Это НЕ скомпилируется:
fn unprivileged_attempt(bmc: &mut BmcController) -> Result<(), &'static str> {
    let trained = bmc.train_link()?;
    // bmc.reset_pcie_link(???, &trained, 0)?;
    //                     ^^^ нет AdminToken — вызвать эту функцию нельзя
    Ok(())
}

AdminToken и LinkTrainedToken занимают ноль байт в скомпилированном бинарнике. Они существуют только во время проверки типов. Сигнатура fn reset_pcie_link(&mut self, _admin: &AdminToken, ...) — это обязательство доказательства («вызвать эту функцию можно, только если вы можете получить AdminToken»), а получить его можно только через authenticate_admin().

Полномочия на управление питанием

Последовательность подачи питания на сервере строго определена: дежурное питание → вспомогательное → основное → CPU. Нарушение порядка может повредить аппаратуру. Capability-токены обеспечивают соблюдение порядка:

/// Токены состояния — каждый доказывает, что предыдущий шаг завершён.
pub struct StandbyOn { _p: () }
pub struct AuxiliaryOn { _p: () }
pub struct MainOn { _p: () }
pub struct CpuPowered { _p: () }

pub struct PowerController { /* ... */ }

impl PowerController {
    /// Шаг 1: включить дежурное питание. Предусловий нет.
    pub fn enable_standby(&mut self) -> Result<StandbyOn, &'static str> {
        println!("Дежурное питание ВКЛ");
        Ok(StandbyOn { _p: () })
    }

    /// Шаг 2: включить вспомогательное питание — требует доказательство дежурного.
    pub fn enable_auxiliary(
        &mut self,
        _standby: &StandbyOn,
    ) -> Result<AuxiliaryOn, &'static str> {
        println!("Вспомогательное питание ВКЛ");
        Ok(AuxiliaryOn { _p: () })
    }

    /// Шаг 3: включить основное питание — требует доказательство вспомогательного.
    pub fn enable_main(
        &mut self,
        _aux: &AuxiliaryOn,
    ) -> Result<MainOn, &'static str> {
        println!("Основное питание ВКЛ");
        Ok(MainOn { _p: () })
    }

    /// Шаг 4: подать питание на CPU — требует доказательство основного питания.
    pub fn power_cpu(
        &mut self,
        _main: &MainOn,
    ) -> Result<CpuPowered, &'static str> {
        println!("Питание CPU ВКЛ");
        Ok(CpuPowered { _p: () })
    }
}

fn power_on_sequence(ctrl: &mut PowerController) -> Result<CpuPowered, &'static str> {
    let standby = ctrl.enable_standby()?;
    let aux = ctrl.enable_auxiliary(&standby)?;
    let main = ctrl.enable_main(&aux)?;
    let cpu = ctrl.power_cpu(&main)?;
    Ok(cpu)
}

// Попытка пропустить шаг:
// fn wrong_order(ctrl: &mut PowerController) {
//     ctrl.power_cpu(???);  // ❌ нельзя получить MainOn без enable_main()
// }

Иерархические полномочия

В реальных системах есть иерархии: администратор может всё, что может пользователь, и даже больше. Смоделируем это иерархией трейтов:

/// Базовое полномочие — любой прошедший аутентификацию.
pub trait Authenticated {
    fn token_id(&self) -> u64;
}

/// Оператор может читать датчики и запускать неразрушающую диагностику.
pub trait Operator: Authenticated {}

/// Администратор может всё, что оператор, плюс разрушающие операции.
pub trait Admin: Operator {}

// Конкретные токены:
pub struct UserToken { id: u64 }
pub struct OperatorToken { id: u64 }
pub struct AdminCapToken { id: u64 }

impl Authenticated for UserToken { fn token_id(&self) -> u64 { self.id } }
impl Authenticated for OperatorToken { fn token_id(&self) -> u64 { self.id } }
impl Operator for OperatorToken {}
impl Authenticated for AdminCapToken { fn token_id(&self) -> u64 { self.id } }
impl Operator for AdminCapToken {}
impl Admin for AdminCapToken {}

pub struct Bmc { /* ... */ }

impl Bmc {
    /// Любой аутентифицированный может читать датчики.
    pub fn read_sensor(&self, _who: &impl Authenticated, id: u32) -> f64 {
        42.0 // заглушка
    }

    /// Запускать диагностику могут только операторы и выше.
    pub fn run_diag(&mut self, _who: &impl Operator, test: &str) -> bool {
        true // заглушка
    }

    /// Прошивать прошивку могут только администраторы.
    pub fn flash_firmware(&mut self, _who: &impl Admin, image: &[u8]) -> Result<(), &'static str> {
        Ok(()) // заглушка
    }
}

AdminCapToken можно передать в любую функцию: он удовлетворяет Authenticated, Operator и Admin. UserToken может вызывать только read_sensor(). Компилятор обеспечивает всю модель привилегий без накладных расходов во время выполнения.

Capability-токены, ограниченные временем жизни

Иногда полномочие должно быть ограниченным по области действия, то есть действительным только в пределах определённого времени жизни. Borrow checker решает эту задачу естественным образом:

/// Ограниченная административная сессия. Токен заимствует сессию,
/// поэтому он не может её пережить.
pub struct AdminSession {
    _active: bool,
}

pub struct ScopedAdminToken<'session> {
    _session: &'session AdminSession,
}

impl AdminSession {
    pub fn begin(credentials: &[u8]) -> Result<Self, &'static str> {
        // ... аутентификация ...
        Ok(AdminSession { _active: true })
    }

    /// Создаёт ограниченный токен — он живёт ровно столько, сколько сессия.
    pub fn token(&self) -> ScopedAdminToken<'_> {
        ScopedAdminToken { _session: self }
    }
}

fn scoped_example() -> Result<(), &'static str> {
    let session = AdminSession::begin(b"credentials")?;
    let token = session.token();

    // Используем токен в этой области видимости...
    // Когда session уничтожается, токен становится недействительным благодаря borrow checker.
    // Проверки истечения срока во время выполнения не нужны.

    // drop(session);
    // ❌ Ошибка компилятора: cannot move out of `session` because it is borrowed
    //    (by `token`, который хранит &session)
    //
    // Даже если пропустить drop() и попытаться использовать `token` после того, как
    // session выйдет из области видимости, — будет та же ошибка: несоответствие времён жизни.

    Ok(())
}

Когда использовать capability-токены

СценарийПаттерн
Привилегированные операции с аппаратуройZST-токен-доказательство (AdminToken)
Многошаговая последовательностьЦепочка токенов состояния (StandbyOn → AuxiliaryOn → …)
Управление доступом на основе ролей (RBAC)Иерархия трейтов (Authenticated → Operator → Admin)
Привилегии с ограничением по времениТокены с ограниченным временем жизни (ScopedAdminToken<'a>)
Полномочия между модулямиПубличный тип токена, приватный конструктор

Сводка по стоимости

ЧтоСтоимость во время выполнения
ZST-токен в памяти0 байт
Передача токена как параметраОптимизируется LLVM
Диспетчеризация по иерархии трейтовСтатическая (мономорфизация)
Проверка времени жизниТолько на этапе компиляции

Итоговые накладные расходы во время выполнения: ноль. Модель привилегий существует только в системе типов.

Иерархия capability-токенов

flowchart TD
    AUTH["authenticate(user, pass)"] -->|возвращает| AT["AdminToken"]
    AT -->|"&AdminToken"| FW["firmware_update()"]
    AT -->|"&AdminToken"| RST["reset_pcie_link()"]
    AT -->|понижение| OP["OperatorToken"]
    OP -->|"&OperatorToken"| RD["read_sensors()"]
    OP -.->|"попытка firmware_update"| ERR["❌ Ошибка компиляции"]
    style AUTH fill:#e1f5fe,color:#000
    style AT fill:#c8e6c9,color:#000
    style OP fill:#fff3e0,color:#000
    style FW fill:#e8f5e9,color:#000
    style RST fill:#e8f5e9,color:#000
    style RD fill:#fff3e0,color:#000
    style ERR fill:#ffcdd2,color:#000

Упражнение: многоуровневые диагностические полномочия

Спроектируйте трёхуровневую систему capability: ViewerToken, TechToken, EngineerToken.

  • Наблюдатели могут вызывать read_status()
  • Техники могут также вызывать run_quick_diag()
  • Инженеры могут также вызывать flash_firmware()
  • Более высокие уровни могут делать всё, что могут низшие (используйте ограничения трейтов или преобразование токенов).
Решение
// Токены — нулевого размера, с приватными конструкторами
pub struct ViewerToken { _private: () }
pub struct TechToken { _private: () }
pub struct EngineerToken { _private: () }

// Трейты capability — иерархические
pub trait CanView {}
pub trait CanDiag: CanView {}
pub trait CanFlash: CanDiag {}

impl CanView for ViewerToken {}
impl CanView for TechToken {}
impl CanView for EngineerToken {}
impl CanDiag for TechToken {}
impl CanDiag for EngineerToken {}
impl CanFlash for EngineerToken {}

pub fn read_status(_tok: &impl CanView) -> String {
    "статус: OK".into()
}

pub fn run_quick_diag(_tok: &impl CanDiag) -> String {
    "диагностика: PASS".into()
}

pub fn flash_firmware(_tok: &impl CanFlash, _image: &[u8]) {
    // Сюда попадают только инженеры
}

Ключевые выводы

  1. ZST-токены не занимают байтов — они существуют только в системе типов; LLVM полностью их оптимизирует.
  2. Приватные конструкторы делают токены неподделываемыми — токен может создать только authenticate() вашего модуля.
  3. Иерархии трейтов моделируют уровни разрешений — CanFlash: CanDiag: CanView повторяет реальный RBAC.
  4. Токены с ограниченным временем жизни отзываются автоматически — ScopedAdminToken<'session> не может пережить сессию.
  5. Сочетайте с typestate (гл. 05) для протоколов, которые требуют одновременно аутентификации и упорядоченных операций.

Автоматы состояний протоколов: typestate для реального оборудования 🔴

Что вы узнаете: как кодирование через typestate превращает нарушения протокола (команды в неверном порядке, использование после закрытия) в ошибки компиляции. Применим к жизненному циклу сессий IPMI и обучению линка PCIe.

Перекрёстные ссылки: гл. 01 (уровень 2: корректность состояний), гл. 04 (токены), гл. 09 (phantom-типы), гл. 11 (приём 4: typestate-билдер, приём 8: async typestate)

Проблема: нарушения протокола

У аппаратных протоколов строгие автоматы состояний. У сессии IPMI есть состояния: не аутентифицирована (Idle) → аутентифицирована (Authenticated) → активна (Active) → закрыта (Closed). Обучение линка PCIe проходит через фазы Detect → Polling → Configuration → L0. Отправка команды в неверном состоянии повреждает сессию или зависает шину.

Автомат состояний сессии IPMI:

stateDiagram-v2
    [*] --> Idle
    Idle --> Authenticated : authenticate(user, pass)
    Authenticated --> Active : activate_session()
    Active --> Active : send_command(cmd)
    Active --> Closed : close()
    Closed --> [*]

    note right of Active : send_command() существует только здесь
    note right of Idle : send_command() → ошибка компиляции

Автомат состояний обучения линка PCIe (LTSSM):

stateDiagram-v2
    [*] --> Detect
    Detect --> Polling : приёмник обнаружен
    Polling --> Configuration : блокировка битов + блокировка символов
    Configuration --> L0 : номер линка + линии назначены
    L0 --> L0 : send_tlp() / receive_tlp()
    L0 --> Recovery : порог ошибок
    Recovery --> L0 : переобучен
    Recovery --> Detect : переобучение не удалось

    note right of L0 : передача TLP только в L0

На C/C++ состояние отслеживают с помощью enum и проверок во время выполнения:

typedef enum { IDLE, AUTHENTICATED, ACTIVE, CLOSED } session_state_t;

typedef struct {
    session_state_t state;
    uint32_t session_id;
    // ...
} ipmi_session_t;

int ipmi_send_command(ipmi_session_t *s, uint8_t cmd, uint8_t *data, int len) {
    if (s->state != ACTIVE) {        // проверка во время выполнения — её легко забыть
        return -EINVAL;
    }
    // ... отправляем команду ...
    return 0;
}

Паттерн typestate

С typestate каждое состояние протокола — это отдельный тип. Переходы — это методы, которые потребляют одно состояние и возвращают другое. Компилятор не даёт вызывать методы в неверном состоянии, потому что этих методов у такого типа просто нет.

Разбор: жизненный цикл сессии IPMI

use std::marker::PhantomData;

// Состояния — маркерные типы нулевого размера
pub struct Idle;
pub struct Authenticated;
pub struct Active;
pub struct Closed;

/// Сессия IPMI, параметризованная текущим состоянием.
/// Состояние существует ТОЛЬКО в системе типов (PhantomData занимает ноль байт).
pub struct IpmiSession<State> {
    transport: String,     // например, "192.168.1.100"
    session_id: Option<u32>,
    _state: PhantomData<State>,
}

// Переход: Idle → Authenticated
impl IpmiSession<Idle> {
    pub fn new(host: &str) -> Self {
        IpmiSession {
            transport: host.to_string(),
            session_id: None,
            _state: PhantomData,
        }
    }

    pub fn authenticate(
        self,              // ← потребляет сессию в состоянии Idle
        user: &str,
        pass: &str,
    ) -> Result<IpmiSession<Authenticated>, String> {
        println!("Аутентификация {user} на {}", self.transport);
        Ok(IpmiSession {
            transport: self.transport,
            session_id: Some(42),
            _state: PhantomData,
        })
    }
}

// Переход: Authenticated → Active
impl IpmiSession<Authenticated> {
    pub fn activate(self) -> Result<IpmiSession<Active>, String> {
        // session_id гарантированно Some благодаря переходу между состояниями типа.
        println!("Активация сессии {}", self.session_id.unwrap());
        Ok(IpmiSession {
            transport: self.transport,
            session_id: self.session_id,
            _state: PhantomData,
        })
    }
}

// Операции доступны ТОЛЬКО в состоянии Active
impl IpmiSession<Active> {
    pub fn send_command(&mut self, netfn: u8, cmd: u8, data: &[u8]) -> Vec<u8> {
        // session_id гарантированно Some в состоянии Active.
        println!("Отправка команды 0x{cmd:02X} в сессии {}", self.session_id.unwrap());
        vec![0x00] // заглушка: код завершения OK
    }

    pub fn close(self) -> IpmiSession<Closed> {
        // session_id гарантированно Some в состоянии Active.
        println!("Закрытие сессии {}", self.session_id.unwrap());
        IpmiSession {
            transport: self.transport,
            session_id: None,
            _state: PhantomData,
        }
    }
}

fn ipmi_workflow() -> Result<(), String> {
    let session = IpmiSession::new("192.168.1.100");

    // session.send_command(0x04, 0x2D, &[]);
    //  ^^^^^^ ОШИБКА: у IpmiSession<Idle> нет метода send_command ❌

    let session = session.authenticate("admin", "password")?;

    // session.send_command(0x04, 0x2D, &[]);
    //  ^^^^^^ ОШИБКА: у IpmiSession<Authenticated> нет метода send_command ❌

    let mut session = session.activate()?;

    // ✅ ТЕПЕРЬ send_command доступна:
    let response = session.send_command(0x04, 0x2D, &[1]);

    let _closed = session.close();

    // _closed.send_command(0x04, 0x2D, &[]);
    //  ^^^^^^ ОШИБКА: у IpmiSession<Closed> нет метода send_command ❌

    Ok(())
}

Нет ни одной проверки состояния во время выполнения. Компилятор обеспечивает:

  • аутентификацию перед активацией
  • активацию перед отправкой команд
  • отсутствие команд после закрытия

Автомат состояний обучения линка PCIe

Обучение линка PCIe — многофазный протокол, описанный в спецификации PCIe. Typestate не даёт отправить данные до того, как линк готов:

use std::marker::PhantomData;

// Состояния LTSSM PCIe (упрощённо)
pub struct Detect;
pub struct Polling;
pub struct Configuration;
pub struct L0;         // полностью работоспособен
pub struct Recovery;

pub struct PcieLink<State> {
    slot: u32,
    width: u8,          // согласованная ширина (x1, x4, x8, x16)
    speed: u8,          // Gen1=1, Gen2=2, Gen3=3, Gen4=4, Gen5=5
    _state: PhantomData<State>,
}

impl PcieLink<Detect> {
    pub fn new(slot: u32) -> Self {
        PcieLink {
            slot, width: 0, speed: 0,
            _state: PhantomData,
        }
    }

    pub fn detect_receiver(self) -> Result<PcieLink<Polling>, String> {
        println!("Слот {}: приёмник обнаружен", self.slot);
        Ok(PcieLink {
            slot: self.slot, width: 0, speed: 0,
            _state: PhantomData,
        })
    }
}

impl PcieLink<Polling> {
    pub fn poll_compliance(self) -> Result<PcieLink<Configuration>, String> {
        println!("Слот {}: опрос завершён, переход к конфигурации", self.slot);
        Ok(PcieLink {
            slot: self.slot, width: 0, speed: 0,
            _state: PhantomData,
        })
    }
}

impl PcieLink<Configuration> {
    pub fn negotiate(self, width: u8, speed: u8) -> Result<PcieLink<L0>, String> {
        println!("Слот {}: согласовано x{width} Gen{speed}", self.slot);
        Ok(PcieLink {
            slot: self.slot, width, speed,
            _state: PhantomData,
        })
    }
}

impl PcieLink<L0> {
    /// Отправить TLP — возможно только тогда, когда линк полностью обучен (L0).
    pub fn send_tlp(&mut self, tlp: &[u8]) -> Vec<u8> {
        println!("Слот {}: отправка TLP из {} байт", self.slot, tlp.len());
        vec![0x00] // заглушка
    }

    /// Войти в восстановление — возврат в состояние Recovery.
    pub fn enter_recovery(self) -> PcieLink<Recovery> {
        PcieLink {
            slot: self.slot, width: self.width, speed: self.speed,
            _state: PhantomData,
        }
    }

    pub fn link_info(&self) -> String {
        format!("x{} Gen{}", self.width, self.speed)
    }
}

impl PcieLink<Recovery> {
    pub fn retrain(self, speed: u8) -> Result<PcieLink<L0>, String> {
        println!("Слот {}: переобучен на Gen{speed}", self.slot);
        Ok(PcieLink {
            slot: self.slot, width: self.width, speed,
            _state: PhantomData,
        })
    }
}

fn pcie_workflow() -> Result<(), String> {
    let link = PcieLink::new(0);

    // link.send_tlp(&[0x01]);  // ❌ нет метода send_tlp у PcieLink<Detect>

    let link = link.detect_receiver()?;
    let link = link.poll_compliance()?;
    let mut link = link.negotiate(16, 5)?; // x16 Gen5

    // ✅ ТЕПЕРЬ можно отправлять TLP:
    let _resp = link.send_tlp(&[0x00, 0x01, 0x02]);
    println!("Линк: {}", link.link_info());

    // Восстановление и переобучение:
    let recovery = link.enter_recovery();
    let mut link = recovery.retrain(4)?;  // понижаем до Gen4
    let _resp = link.send_tlp(&[0x03]);

    Ok(())
}

Комбинирование typestate с capability-токенами

Typestate и capability-токены естественно сочетаются. Пример: диагностика, которой нужны одновременно активная сессия IPMI и права администратора:

use std::marker::PhantomData;
pub struct Active;
pub struct AdminToken { _p: () }
pub struct IpmiSession<S> { _s: PhantomData<S> }
impl IpmiSession<Active> {
    pub fn send_command(&mut self, _nf: u8, _cmd: u8, _d: &[u8]) -> Vec<u8> { vec![] }
}

/// Запускает обновление прошивки — требует:
/// 1. Активную сессию IPMI (typestate)
/// 2. Права администратора (capability-токен)
pub fn firmware_update(
    session: &mut IpmiSession<Active>,   // доказывает, что сессия активна
    _admin: &AdminToken,                 // доказывает, что вызывающий — администратор
    image: &[u8],
) -> Result<(), String> {
    // Никаких проверок во время выполнения не нужно — сама сигнатура и есть проверка
    session.send_command(0x2C, 0x01, image);
    Ok(())
}

Вызывающий код должен:

  1. Создать сессию (Idle)
  2. Аутентифицировать её (Authenticated)
  3. Активировать её (Active)
  4. Получить AdminToken
  5. И только затем вызвать firmware_update()

Всё это проверяется на этапе компиляции, без накладных расходов во время выполнения.

Этап 3: обновление прошивки — многофазный FSM с композицией

У жизненного цикла обновления прошивки больше состояний, чем у сессии, и он сочетает одновременно capability-токены и одноразовые типы (гл. 03). Это самый сложный пример typestate в книге: если вы в нём разобрались, паттерн освоен.

stateDiagram-v2
    [*] --> Idle
    Idle --> Uploading : begin_upload(admin, image)
    Uploading --> Verifying : finish_upload()
    Uploading --> Idle : abort()
    Verifying --> Verified : verify_ok()
    Verifying --> Idle : verify_fail()
    Verified --> Applying : apply(одноразовый токен VerifiedImage)
    Applying --> WaitingReboot : apply_complete()
    WaitingReboot --> [*] : reboot()

    note right of Verified : токен VerifiedImage потребляется в apply()
    note right of Uploading : abort() возвращает в Idle (безопасно)
use std::marker::PhantomData;

// ── Состояния ──
pub struct Idle;
pub struct Uploading;
pub struct Verifying;
pub struct Verified;
pub struct Applying;
pub struct WaitingReboot;

// ── Одноразовое доказательство того, что образ прошёл проверку (гл. 03) ──
pub struct VerifiedImage {
    _private: (),
    pub digest: [u8; 32],
}

// ── Capability-токен: инициировать обновление могут только администраторы (гл. 04) ──
pub struct FirmwareAdminToken { _private: () }

pub struct FwUpdate<S> {
    version: String,
    _state: PhantomData<S>,
}

impl FwUpdate<Idle> {
    pub fn new() -> Self {
        FwUpdate { version: String::new(), _state: PhantomData }
    }

    /// Начать загрузку — требует права администратора.
    pub fn begin_upload(
        self,
        _admin: &FirmwareAdminToken,
        version: &str,
    ) -> FwUpdate<Uploading> {
        println!("Загрузка прошивки v{version}...");
        FwUpdate { version: version.to_string(), _state: PhantomData }
    }
}

impl FwUpdate<Uploading> {
    pub fn finish_upload(self) -> FwUpdate<Verifying> {
        println!("Загрузка завершена, проверяем v{}...", self.version);
        FwUpdate { version: self.version, _state: PhantomData }
    }

    /// Прерывание возвращает в Idle — безопасно в любой момент загрузки.
    pub fn abort(self) -> FwUpdate<Idle> {
        println!("Загрузка прервана.");
        FwUpdate { version: String::new(), _state: PhantomData }
    }
}

impl FwUpdate<Verifying> {
    /// При успехе возвращает одноразовый токен VerifiedImage.
    pub fn verify_ok(self, digest: [u8; 32]) -> (FwUpdate<Verified>, VerifiedImage) {
        println!("Проверка v{} пройдена", self.version);
        (
            FwUpdate { version: self.version, _state: PhantomData },
            VerifiedImage { _private: (), digest },
        )
    }

    pub fn verify_fail(self) -> FwUpdate<Idle> {
        println!("Проверка не пройдена — возврат в состояние ожидания.");
        FwUpdate { version: String::new(), _state: PhantomData }
    }
}

impl FwUpdate<Verified> {
    /// Apply ПОТРЕБЛЯЕТ токен VerifiedImage — применить дважды нельзя.
    pub fn apply(self, proof: VerifiedImage) -> FwUpdate<Applying> {
        println!("Применяем v{} (дайджест: {:02x?})", self.version, &proof.digest[..4]);
        // proof перемещён — переиспользовать его нельзя
        FwUpdate { version: self.version, _state: PhantomData }
    }
}

impl FwUpdate<Applying> {
    pub fn apply_complete(self) -> FwUpdate<WaitingReboot> {
        println!("Применение завершено — ожидаем перезагрузки.");
        FwUpdate { version: self.version, _state: PhantomData }
    }
}

impl FwUpdate<WaitingReboot> {
    pub fn reboot(self) {
        println!("Перезагрузка в v{}...", self.version);
    }
}

// ── Использование ──

fn firmware_workflow() {
    let fw = FwUpdate::new();

    // fw.finish_upload();  // ❌ нет метода finish_upload у FwUpdate<Idle>

    let admin = FirmwareAdminToken { _private: () }; // из системы аутентификации
    let fw = fw.begin_upload(&admin, "2.10.1");
    let fw = fw.finish_upload();

    let digest = [0xAB; 32]; // вычисляется во время проверки
    let (fw, token) = fw.verify_ok(digest);

    let fw = fw.apply(token);
    // fw.apply(token);  // ❌ ошибка компиляции: use of moved value: `token`

    let fw = fw.apply_complete();
    fw.reboot();
}

Что вместе иллюстрируют три этапа:

ЭтапПротоколСостоянияКомпозиция
1Сессия IPMI4Чистый typestate
2LTSSM PCIe5Typestate + ветка восстановления
3Обновление прошивки6Typestate + capability-токены (гл. 04) + одноразовое доказательство (гл. 03)

С каждым этапом добавляется слой сложности. К этапу 3 компилятор обеспечивает порядок состояний, права администратора и однократное применение: три класса ошибок устранены в одном автомате состояний.

Когда использовать typestate

ПротоколСтоит ли использовать typestate?
Жизненный цикл сессии IPMI✅ Да: аутентификация → активация → команда → закрытие
Обучение линка PCIe✅ Да: detect → poll → configure → L0
TLS-рукопожатие✅ Да: ClientHello → ServerHello → Finished
Перечисление USB✅ Да: Attached → Powered → Default → Addressed → Configured
Простой запрос/ответ⚠️ Скорее нет: всего 2 состояния
Сообщения «отправил и забыл»❌ Нет: нет состояния, которое нужно отслеживать

Упражнение: typestate для перечисления USB-устройства

Смоделируйте USB-устройство, которое должно пройти путь: Attached → Powered → Default → Addressed → Configured. Каждый переход должен потреблять предыдущее состояние и возвращать следующее. Метод send_data() должен быть доступен только в Configured.

Решение
use std::marker::PhantomData;

pub struct Attached;
pub struct Powered;
pub struct Default;
pub struct Addressed;
pub struct Configured;

pub struct UsbDevice<State> {
    address: u8,
    _state: PhantomData<State>,
}

impl UsbDevice<Attached> {
    pub fn new() -> Self {
        UsbDevice { address: 0, _state: PhantomData }
    }
    pub fn power_on(self) -> UsbDevice<Powered> {
        UsbDevice { address: self.address, _state: PhantomData }
    }
}

impl UsbDevice<Powered> {
    pub fn reset(self) -> UsbDevice<Default> {
        UsbDevice { address: self.address, _state: PhantomData }
    }
}

impl UsbDevice<Default> {
    pub fn set_address(self, addr: u8) -> UsbDevice<Addressed> {
        UsbDevice { address: addr, _state: PhantomData }
    }
}

impl UsbDevice<Addressed> {
    pub fn configure(self) -> UsbDevice<Configured> {
        UsbDevice { address: self.address, _state: PhantomData }
    }
}

impl UsbDevice<Configured> {
    pub fn send_data(&self, _data: &[u8]) {
        // Доступно только в состоянии Configured
    }
}

Ключевые выводы

  1. Typestate делает вызовы в неверном порядке невозможными — методы существуют только в тех состояниях, где они допустимы.
  2. Каждый переход потребляет self — нельзя сохранить старое состояние после перехода.
  3. Сочетайте с capability-токенами — firmware_update() требует и Session<Active>, и AdminToken.
  4. Три этапа, нарастающая сложность — IPMI (чистый FSM), LTSSM PCIe (ветки восстановления) и обновление прошивки (FSM + токены + одноразовые доказательства) показывают, что паттерн масштабируется от простого к сложно составленному.
  5. Не переусердствуйте — протоколы «запрос/ответ» с двумя состояниями проще без typestate.
  6. Паттерн распространяется на полные сценарии Redfish — гл. 17 применяет typestate к жизненному циклу сессий Redfish, а гл. 18 использует typestate билдера для построения ответов.

Анализ размерностей: заставляем компилятор проверять единицы измерения 🟢

Что вы узнаете: как newtype-обёртки и крейт uom превращают компилятор в движок проверки единиц измерения и предотвращают класс ошибок, из-за которого был потерян космический аппарат стоимостью 328 млн долларов.

Перекрёстные ссылки: гл. 02 (типизированные команды используют эти типы), гл. 07 (проверенные границы), гл. 10 (интеграция)

Mars Climate Orbiter

В 1999 году был потерян марсианский зонд NASA Mars Climate Orbiter: одна команда передавала данные о тяге в фунт-силах на секунду, а команда навигации ожидала ньютон-секунды. Аппарат вошёл в атмосферу на высоте 57 км вместо 226 км и разрушился. Стоимость: 327,6 млн долларов.

Первопричина: оба значения были double. Компилятор не мог их различить.

Тот же класс ошибок скрывается в любой аппаратной диагностике, которая работает с физическими величинами:

// C — все double, никакой проверки единиц
double read_temperature(int sensor_id);   // Цельсий? Фаренгейт? Кельвин?
double read_voltage(int channel);          // Вольты? Милливольты?
double read_fan_speed(int fan_id);         // об/мин? радианы в секунду?

// Ошибка: сравнение Цельсия с Фаренгейтом
if (read_temperature(0) > read_temperature(1)) { ... }  // единицы могут различаться!

Newtype для физических величин

Самый простой корректный по построению подход: обернуть каждую единицу измерения в отдельный тип.

use std::fmt;

/// Температура в градусах Цельсия.
#[derive(Debug, Clone, Copy, PartialEq, PartialOrd)]
pub struct Celsius(pub f64);

/// Температура в градусах Фаренгейта.
#[derive(Debug, Clone, Copy, PartialEq, PartialOrd)]
pub struct Fahrenheit(pub f64);

/// Напряжение в вольтах.
#[derive(Debug, Clone, Copy, PartialEq, PartialOrd)]
pub struct Volts(pub f64);

/// Напряжение в милливольтах.
#[derive(Debug, Clone, Copy, PartialEq, PartialOrd)]
pub struct Millivolts(pub f64);

/// Скорость вентилятора в об/мин.
#[derive(Debug, Clone, Copy, PartialEq, PartialOrd)]
pub struct Rpm(pub f64);

// Преобразования явные:
impl From<Celsius> for Fahrenheit {
    fn from(c: Celsius) -> Self {
        Fahrenheit(c.0 * 9.0 / 5.0 + 32.0)
    }
}

impl From<Fahrenheit> for Celsius {
    fn from(f: Fahrenheit) -> Self {
        Celsius((f.0 - 32.0) * 5.0 / 9.0)
    }
}

impl From<Volts> for Millivolts {
    fn from(v: Volts) -> Self {
        Millivolts(v.0 * 1000.0)
    }
}

impl From<Millivolts> for Volts {
    fn from(mv: Millivolts) -> Self {
        Volts(mv.0 / 1000.0)
    }
}

impl fmt::Display for Celsius {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        write!(f, "{:.1}°C", self.0)
    }
}

impl fmt::Display for Rpm {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        write!(f, "{:.0} об/мин", self.0)
    }
}

Теперь компилятор ловит несоответствия единиц:

#[derive(Debug, Clone, Copy, PartialEq, PartialOrd)]
pub struct Celsius(pub f64);
#[derive(Debug, Clone, Copy, PartialEq, PartialOrd)]
pub struct Volts(pub f64);

fn check_thermal_limit(temp: Celsius, limit: Celsius) -> bool {
    temp > limit  // ✅ одинаковые единицы — компилируется
}

// fn bad_comparison(temp: Celsius, voltage: Volts) -> bool {
//     temp > voltage  // ❌ ОШИБКА: mismatched types — Celsius vs Volts
// }

Нулевые накладные расходы во время выполнения: newtype компилируются в обычные значения f64. Обёртка существует чисто как понятие системы типов.

Макрос newtype для аппаратных величин

Писать newtype вручную утомительно. Макрос устраняет шаблонный код:

/// Генерирует newtype для физической величины.
macro_rules! quantity {
    ($Name:ident, $unit:expr) => {
        #[derive(Debug, Clone, Copy, PartialEq, PartialOrd)]
        pub struct $Name(pub f64);

        impl $Name {
            pub fn new(value: f64) -> Self { $Name(value) }
            pub fn value(self) -> f64 { self.0 }
        }

        impl std::fmt::Display for $Name {
            fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
                write!(f, "{:.2} {}", self.0, $unit)
            }
        }

        impl std::ops::Add for $Name {
            type Output = Self;
            fn add(self, rhs: Self) -> Self { $Name(self.0 + rhs.0) }
        }

        impl std::ops::Sub for $Name {
            type Output = Self;
            fn sub(self, rhs: Self) -> Self { $Name(self.0 - rhs.0) }
        }
    };
}

// Использование:
quantity!(Celsius, "°C");
quantity!(Fahrenheit, "°F");
quantity!(Volts, "V");
quantity!(Millivolts, "мВ");
quantity!(Rpm, "об/мин");
quantity!(Watts, "Вт");
quantity!(Amperes, "А");
quantity!(Pascals, "Па");
quantity!(Hertz, "Гц");
quantity!(Bytes, "Б");

Каждая строка порождает полноценный тип с Display, Add, Sub и операторами сравнения. Всё это без накладных расходов во время выполнения.

Физическая оговорка: макрос генерирует Add для всех величин, включая Celsius. Сложение абсолютных температур (25°C + 30°C = 55°C) физически бессмысленно: для разностей температур нужен отдельный тип, например TemperatureDelta. Крейт uom (см. ниже) обрабатывает это корректно. Для простой диагностики датчиков, где температуры только сравниваются и выводятся, можно убрать Add/Sub из типов температур и оставить их там, где сложение имеет смысл (ватты, вольты, байты). Если нужна арифметика разностей, определите newtype CelsiusDelta(f64) с impl Add<CelsiusDelta> for Celsius.

Прикладной пример: конвейер датчиков

Типичная диагностика считывает сырые значения АЦП, переводит их в физические единицы и сравнивает с порогами. С размерными типами каждый шаг проверяется системой типов:

macro_rules! quantity {
    ($Name:ident, $unit:expr) => {
        #[derive(Debug, Clone, Copy, PartialEq, PartialOrd)]
        pub struct $Name(pub f64);
        impl $Name {
            pub fn new(value: f64) -> Self { $Name(value) }
            pub fn value(self) -> f64 { self.0 }
        }
        impl std::fmt::Display for $Name {
            fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
                write!(f, "{:.2} {}", self.0, $unit)
            }
        }
    };
}
quantity!(Celsius, "°C");
quantity!(Volts, "V");
quantity!(Rpm, "об/мин");

/// Сырое показание АЦП — ещё не физическая величина.
#[derive(Debug, Clone, Copy)]
pub struct AdcReading {
    pub channel: u8,
    pub raw: u16,   // 12-битное значение АЦП (0–4095)
}

/// Коэффициенты калибровки для перевода АЦП → физическая единица.
pub struct TemperatureCalibration {
    pub offset: f64,
    pub scale: f64,   // °C на отсчёт АЦП
}

pub struct VoltageCalibration {
    pub reference_mv: f64,
    pub divider_ratio: f64,
}

impl TemperatureCalibration {
    /// Перевод сырого АЦП → Celsius. Тип результата гарантирует, что выход — Celsius.
    pub fn convert(&self, adc: AdcReading) -> Celsius {
        Celsius::new(adc.raw as f64 * self.scale + self.offset)
    }
}

impl VoltageCalibration {
    /// Перевод сырого АЦП → Volts. Тип результата гарантирует, что выход — Volts.
    pub fn convert(&self, adc: AdcReading) -> Volts {
        Volts::new(adc.raw as f64 * self.reference_mv / 4096.0 / self.divider_ratio / 1000.0)
    }
}

/// Проверка порогов — компилируется, только если единицы совпадают.
pub struct Threshold<T: PartialOrd> {
    pub warning: T,
    pub critical: T,
}

#[derive(Debug, PartialEq)]
pub enum ThresholdResult {
    Normal,
    Warning,
    Critical,
}

impl<T: PartialOrd> Threshold<T> {
    pub fn check(&self, value: &T) -> ThresholdResult {
        if *value >= self.critical {
            ThresholdResult::Critical
        } else if *value >= self.warning {
            ThresholdResult::Warning
        } else {
            ThresholdResult::Normal
        }
    }
}

fn sensor_pipeline_example() {
    let temp_cal = TemperatureCalibration { offset: -50.0, scale: 0.0625 };
    let temp_threshold = Threshold {
        warning: Celsius::new(85.0),
        critical: Celsius::new(100.0),
    };

    let adc = AdcReading { channel: 0, raw: 2048 };
    let temp: Celsius = temp_cal.convert(adc);

    let result = temp_threshold.check(&temp);
    println!("Температура: {temp}, статус: {result:?}");

    // Это не скомпилируется: нельзя сравнить показание Celsius с порогом в Volts:
    // let volt_threshold = Threshold {
    //     warning: Volts::new(11.4),
    //     critical: Volts::new(10.8),
    // };
    // volt_threshold.check(&temp);  // ❌ ОШИБКА: expected &Volts, found &Celsius
}

Весь конвейер статически проверяется на соответствие типов:

  • показания АЦП — это сырые отсчёты, а не единицы измерения
  • калибровка даёт типизированные величины (Celsius, Volts)
  • пороги обобщены по типу величины
  • сравнение Celsius с Volts — ошибка компиляции

Крейт uom

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

// Cargo.toml: uom = { version = "0.36", features = ["f64"] }
//
// use uom::si::f64::*;
// use uom::si::thermodynamic_temperature::degree_celsius;
// use uom::si::electric_potential::volt;
// use uom::si::power::watt;
//
// let temp = ThermodynamicTemperature::new::<degree_celsius>(85.0);
// let voltage = ElectricPotential::new::<volt>(12.0);
// let power = Power::new::<watt>(250.0);
//
// // temp + voltage;  // ❌ ошибка компиляции — нельзя сложить температуру и напряжение
// // power > temp;    // ❌ ошибка компиляции — нельзя сравнить мощность с температурой

Используйте uom, когда нужна автоматическая поддержка производных единиц (например, Ватты = Вольты × Амперы). Используйте самописные newtype, когда нужны только простые величины без арифметики производных единиц.

Когда использовать размерные типы

СценарийРекомендация
Показания датчиков (температура, напряжение, вентилятор)✅ Всегда: предотвращает путаницу единиц
Сравнение с порогами✅ Всегда: обобщённый Threshold<T>
Обмен данными между подсистемами✅ Всегда: закрепляйте контракты на границах API
Внутренние вычисления (одна единица во всём коде)⚠️ По желанию: меньше риска ошибок
Форматирование строк и вывод❌ Используйте реализацию Display у типа величины

Поток типов в конвейере датчиков

flowchart LR
    RAW["raw: &[u8]"] -->|разбор| C["Celsius(f64)"]
    RAW -->|разбор| R["Rpm(u32)"]
    RAW -->|разбор| V["Volts(f64)"]
    C -->|проверка порога| TC["Threshold<Celsius>"]
    R -->|проверка порога| TR["Threshold<Rpm>"]
    C -.->|"C + R"| ERR["❌ mismatched types"]
    style RAW fill:#e1f5fe,color:#000
    style C fill:#c8e6c9,color:#000
    style R fill:#fff3e0,color:#000
    style V fill:#e8eaf6,color:#000
    style TC fill:#c8e6c9,color:#000
    style TR fill:#fff3e0,color:#000
    style ERR fill:#ffcdd2,color:#000

Упражнение: калькулятор энергетического бюджета

Создайте newtype Watts(f64) и Amperes(f64). Реализуйте:

  • Watts::from_vi(volts: Volts, amps: Amperes) -> Watts (P = V × I)
  • PowerBudget, который суммирует ватты и отклоняет добавления, превышающие заданный лимит.
  • Попытка Watts + Celsius должна приводить к ошибке компиляции.
Решение
#[derive(Debug, Clone, Copy, PartialEq, PartialOrd)]
pub struct Watts(pub f64);

#[derive(Debug, Clone, Copy, PartialEq, PartialOrd)]
pub struct Amperes(pub f64);

#[derive(Debug, Clone, Copy, PartialEq, PartialOrd)]
pub struct Volts(pub f64);

#[derive(Debug, Clone, Copy, PartialEq, PartialOrd)]
pub struct Celsius(pub f64);

impl Watts {
    pub fn from_vi(volts: Volts, amps: Amperes) -> Self {
        Watts(volts.0 * amps.0)
    }
}

impl std::ops::Add for Watts {
    type Output = Watts;
    fn add(self, rhs: Watts) -> Watts {
        Watts(self.0 + rhs.0)
    }
}

pub struct PowerBudget {
    total: Watts,
    limit: Watts,
}

impl PowerBudget {
    pub fn new(limit: Watts) -> Self {
        PowerBudget { total: Watts(0.0), limit }
    }
    pub fn add(&mut self, w: Watts) -> Result<(), String> {
        let new_total = Watts(self.total.0 + w.0);
        if new_total > self.limit {
            return Err(format!("бюджет превышен: {:?} > {:?}", new_total, self.limit));
        }
        self.total = new_total;
        Ok(())
    }
}

// ❌ Ошибка компиляции: Watts + Celsius → "mismatched types"
// let bad = Watts(100.0) + Celsius(50.0);

Ключевые выводы

  1. Newtype предотвращает путаницу единиц без накладных расходов — Celsius и Rpm внутри оба f64, но компилятор считает их разными типами.
  2. Ошибка Mars Climate Orbiter становится невозможной — передать фунт-силы там, где ожидаются ньютоны, — ошибка компиляции.
  3. Макрос quantity! уменьшает объём шаблонного кода — он генерирует Display, арифметику и логику порогов для каждой единицы.
  4. Крейт uom обрабатывает производные единицы — используйте его, когда нужно автоматически получать Watts = Volts × Amperes.
  5. Threshold обобщён по величине — Threshold<Celsius> нельзя случайно сравнить с Threshold<Rpm>.

Проверенные границы: parse, don’t validate 🟡

Что вы узнаете: как проверить данные ровно один раз на границе системы, хранить доказательство их корректности в отдельном типе и никогда не проверять повторно. Применяем к записям IPMI FRU (плоские байты), JSON Redfish (структурированные документы) и записям IPMI SEL (полиморфные бинарные данные с вложенной диспетчеризацией) со сквозным пошаговым разбором.

Перекрёстные ссылки: гл. 02 (типизированные команды), гл. 06 (размерные типы), гл. 11 (приём 2: sealed-трейты, приём 3: #[non_exhaustive], приём 5: FromStr), гл. 14 (proptest)

Проблема: разбросанная валидация

В типичном коде проверки разбросаны повсюду. Каждая функция, которая получает данные, перепроверяет их «на всякий случай»:

// C — проверки разбросаны по всей кодовой базе
int process_fru_data(uint8_t *data, int len) {
    if (data == NULL) return -1;          // проверка: не NULL
    if (len < 8) return -1;              // проверка: минимальная длина
    if (data[0] != 0x01) return -1;      // проверка: версия формата
    if (checksum(data, len) != 0) return -1; // проверка: контрольная сумма

    // ... ещё 10 функций, которые повторяют те же проверки ...
}

Этот паттерн («дробовик» проверок) имеет две проблемы:

  1. Избыточность: одни и те же проверки встречаются в десятках мест
  2. Неполнота: забудете одну проверку в одной функции — и получите ошибку

Parse, Don’t Validate: разбирай, а не проверяй

Корректный по построению подход: проверить один раз на границе, а затем хранить доказательство корректности в типе.

/// Сырые байты из линии — ещё не проверены.
#[derive(Debug)]
pub struct RawFruData(Vec<u8>);

Разбор: данные IPMI FRU

#[derive(Debug)]
pub struct RawFruData(Vec<u8>);

/// Проверенные данные IPMI FRU. Создать можно только через TryFrom,
/// который обеспечивает все инварианты. Имея ValidFru, вы гарантированно
/// имеете корректные данные.
#[derive(Debug)]
pub struct ValidFru {
    format_version: u8,
    internal_area_offset: u8,
    chassis_area_offset: u8,
    board_area_offset: u8,
    product_area_offset: u8,
    data: Vec<u8>,
}

#[derive(Debug)]
pub enum FruError {
    TooShort { actual: usize, minimum: usize },
    BadFormatVersion(u8),
    ChecksumMismatch { expected: u8, actual: u8 },
    InvalidAreaOffset { area: &'static str, offset: u8 },
}

impl std::fmt::Display for FruError {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        match self {
            Self::TooShort { actual, minimum } =>
                write!(f, "данные FRU слишком короткие: {actual} байт (минимум {minimum})"),
            Self::BadFormatVersion(v) =>
                write!(f, "неподдерживаемая версия формата FRU: {v}"),
            Self::ChecksumMismatch { expected, actual } =>
                write!(f, "контрольная сумма не совпадает: ожидалось 0x{expected:02X}, получено 0x{actual:02X}"),
            Self::InvalidAreaOffset { area, offset } =>
                write!(f, "некорректное смещение области {area}: {offset}"),
        }
    }
}

impl TryFrom<RawFruData> for ValidFru {
    type Error = FruError;

    fn try_from(raw: RawFruData) -> Result<Self, FruError> {
        let data = raw.0;

        // 1. Проверка длины
        if data.len() < 8 {
            return Err(FruError::TooShort {
                actual: data.len(),
                minimum: 8,
            });
        }

        // 2. Версия формата
        if data[0] != 0x01 {
            return Err(FruError::BadFormatVersion(data[0]));
        }

        // 3. Контрольная сумма (заголовок — первые 8 байт, контрольная сумма в байте 7)
        let checksum: u8 = data[..8].iter().fold(0u8, |acc, &b| acc.wrapping_add(b));
        if checksum != 0 {
            return Err(FruError::ChecksumMismatch {
                expected: 0,
                actual: checksum,
            });
        }

        // 4. Смещения областей должны быть в пределах данных
        for (name, idx) in [
            ("internal", 1), ("chassis", 2),
            ("board", 3), ("product", 4),
        ] {
            let offset = data[idx];
            if offset != 0 && (offset as usize * 8) >= data.len() {
                return Err(FruError::InvalidAreaOffset {
                    area: name,
                    offset,
                });
            }
        }

        // Все проверки пройдены — создаём проверенный тип
        Ok(ValidFru {
            format_version: data[0],
            internal_area_offset: data[1],
            chassis_area_offset: data[2],
            board_area_offset: data[3],
            product_area_offset: data[4],
            data,
        })
    }
}

impl ValidFru {
    /// Проверка не нужна: тип гарантирует корректность.
    pub fn board_area(&self) -> Option<&[u8]> {
        if self.board_area_offset == 0 {
            return None;
        }
        let start = self.board_area_offset as usize * 8;
        Some(&self.data[start..])  // безопасно: границы проверены при разборе
    }

    pub fn product_area(&self) -> Option<&[u8]> {
        if self.product_area_offset == 0 {
            return None;
        }
        let start = self.product_area_offset as usize * 8;
        Some(&self.data[start..])
    }

    pub fn format_version(&self) -> u8 {
        self.format_version
    }
}

Любая функция, которая принимает &ValidFru, знает, что данные корректны. Повторных проверок не нужно:

pub struct ValidFru { board_area_offset: u8, data: Vec<u8> }
impl ValidFru {
    pub fn board_area(&self) -> Option<&[u8]> { None }
}

/// Эта функция НЕ должна проверять данные FRU.
/// Сигнатура типа гарантирует, что они уже корректны.
fn extract_board_serial(fru: &ValidFru) -> Option<String> {
    let board = fru.board_area()?;
    // ... разбираем серийный номер из области платы ...
    // Проверки границ не нужны: ValidFru гарантирует, что смещения в пределах
    Some("ABC123".to_string()) // заглушка
}

fn extract_board_manufacturer(fru: &ValidFru) -> Option<String> {
    let board = fru.board_area()?;
    // Проверка здесь тоже не нужна: та же гарантия
    Some("Acme Corp".to_string()) // заглушка
}

Проверенный JSON Redfish

Тот же паттерн применим к ответам API Redfish. Разбираем один раз, а валидность храним в типе:

use std::collections::HashMap;

/// Сырая JSON-строка из эндпоинта Redfish.
pub struct RawRedfishResponse(pub String);

/// Проверенный ответ Redfish Thermal.
/// Все обязательные поля гарантированно присутствуют и находятся в допустимом диапазоне.
#[derive(Debug)]
pub struct ValidThermalResponse {
    pub temperatures: Vec<ValidTemperatureReading>,
    pub fans: Vec<ValidFanReading>,
}

#[derive(Debug)]
pub struct ValidTemperatureReading {
    pub name: String,
    pub reading_celsius: f64,     // гарантированно не NaN, в пределах диапазона датчика
    pub upper_critical: f64,
    pub status: HealthStatus,
}

#[derive(Debug)]
pub struct ValidFanReading {
    pub name: String,
    pub reading_rpm: u32,        // гарантированно > 0 для установленных вентиляторов
    pub status: HealthStatus,
}

#[derive(Debug, Clone, Copy, PartialEq)]
pub enum HealthStatus {
    Ok,
    Warning,
    Critical,
}

#[derive(Debug)]
pub enum RedfishValidationError {
    MissingField(&'static str),
    OutOfRange { field: &'static str, value: f64 },
    InvalidStatus(String),
}

impl std::fmt::Display for RedfishValidationError {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        match self {
            Self::MissingField(name) => write!(f, "отсутствует обязательное поле: {name}"),
            Self::OutOfRange { field, value } =>
                write!(f, "поле {field} вне диапазона: {value}"),
            Self::InvalidStatus(s) => write!(f, "некорректный статус здоровья: {s}"),
        }
    }
}

// Имея проверенные данные, нижележащий код никогда не проверяет их повторно:
fn check_thermal_health(thermal: &ValidThermalResponse) -> bool {
    // Не нужно проверять отсутствие полей и значения NaN.
    // ValidThermalResponse гарантирует, что все показания разумны.
    thermal.temperatures.iter().all(|t| {
        t.reading_celsius < t.upper_critical && t.status != HealthStatus::Critical
    }) && thermal.fans.iter().all(|f| {
        f.reading_rpm > 0 && f.status != HealthStatus::Critical
    })
}

Полиморфная валидация: записи SEL IPMI

В первых двух разобранных примерах проверялись плоские структуры: фиксированная раскладка байтов (FRU) и известная JSON-схема (Redfish). Реальные данные часто полиморфны: смысл последующих байтов зависит от предыдущих. Классический пример — записи журнала системных событий IPMI (System Event Log, SEL).

Суть проблемы

Каждая запись SEL занимает ровно 16 байт. Но то, что означают эти байты, определяется цепочкой диспетчеризации:

Байт 2: Тип записи
  ├─ 0x02 → Системное событие
  │    Байт 10[6:4]: Тип события
  │      ├─ 0x01       → Событие порога (показание + порог в байтах данных 2–3)
  │      ├─ 0x02-0x0C  → Дискретное событие (бит в поле смещения)
  │      └─ 0x6F       → Специфичное для датчика (смысл зависит от типа датчика в байте 7)
  │           Байт 7: Тип датчика
  │             ├─ 0x01 → События температуры
  │             ├─ 0x02 → События напряжения
  │             ├─ 0x04 → События вентилятора
  │             ├─ 0x07 → События процессора
  │             ├─ 0x0C → События памяти
  │             ├─ 0x08 → События блока питания
  │             └─ ...  → (42 типа датчиков в IPMI 2.0, таблица 42-3)
  ├─ 0xC0-0xDF → OEM с меткой времени
  └─ 0xE0-0xFF → OEM без метки времени

В C это switch внутри switch внутри switch, и каждый уровень использует один и тот же указатель uint8_t *data. Забыли один уровень, неверно прочитали таблицу спецификации или взяли не тот байт — ошибка проходит незамеченной.

// C — проблема полиморфного разбора
void process_sel_entry(uint8_t *data, int len) {
    if (data[2] == 0x02) {  // системное событие
        uint8_t event_type = (data[10] >> 4) & 0x07;
        if (event_type == 0x01) {  // порог
            uint8_t reading = data[11];   // 🐛 или всё-таки data[13]?
            uint8_t threshold = data[12]; // 🐛 в спецификации байт 12 — это триггер, а не порог
            printf("Температура: %d пересекла %d\n", reading, threshold);
        } else if (event_type == 0x6F) {  // специфичное для датчика
            uint8_t sensor_type = data[7];
            if (sensor_type == 0x0C) {  // память
                // 🐛 забыли проверить биты смещения event data 1
                printf("Ошибка ECC памяти\n");
            }
            // 🐛 нет else — молча отбрасываются 30+ других типов датчиков
        }
    }
    // 🐛 записи OEM молча игнорируются
}

Шаг 1 — разбор внешнего уровня

Первый TryFrom диспетчеризует по типу записи — самому внешнему слою объединения:

/// Сырая 16-байтная запись SEL, прямо из `Get SEL Entry` (команда IPMI 0x43).
pub struct RawSelRecord(pub [u8; 16]);

/// Проверенная запись SEL — тип записи продиспетчеризован, все поля проверены.
pub enum ValidSelRecord {
    SystemEvent(SystemEventRecord),
    OemTimestamped(OemTimestampedRecord),
    OemNonTimestamped(OemNonTimestampedRecord),
}

#[derive(Debug)]
pub struct OemTimestampedRecord {
    pub record_id: u16,
    pub timestamp: u32,
    pub manufacturer_id: [u8; 3],
    pub oem_data: [u8; 6],
}

#[derive(Debug)]
pub struct OemNonTimestampedRecord {
    pub record_id: u16,
    pub oem_data: [u8; 13],
}

#[derive(Debug)]
pub enum SelParseError {
    UnknownRecordType(u8),
    UnknownSensorType(u8),
    UnknownEventType(u8),
    InvalidEventData { reason: &'static str },
}

impl std::fmt::Display for SelParseError {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        match self {
            Self::UnknownRecordType(t) => write!(f, "неизвестный тип записи: 0x{t:02X}"),
            Self::UnknownSensorType(t) => write!(f, "неизвестный тип датчика: 0x{t:02X}"),
            Self::UnknownEventType(t) => write!(f, "неизвестный тип события: 0x{t:02X}"),
            Self::InvalidEventData { reason } => write!(f, "некорректные данные события: {reason}"),
        }
    }
}

impl TryFrom<RawSelRecord> for ValidSelRecord {
    type Error = SelParseError;

    fn try_from(raw: RawSelRecord) -> Result<Self, SelParseError> {
        let d = &raw.0;
        let record_id = u16::from_le_bytes([d[0], d[1]]);

        match d[2] {
            0x02 => {
                let system = parse_system_event(record_id, d)?;
                Ok(ValidSelRecord::SystemEvent(system))
            }
            0xC0..=0xDF => {
                Ok(ValidSelRecord::OemTimestamped(OemTimestampedRecord {
                    record_id,
                    timestamp: u32::from_le_bytes([d[3], d[4], d[5], d[6]]),
                    manufacturer_id: [d[7], d[8], d[9]],
                    oem_data: [d[10], d[11], d[12], d[13], d[14], d[15]],
                }))
            }
            0xE0..=0xFF => {
                Ok(ValidSelRecord::OemNonTimestamped(OemNonTimestampedRecord {
                    record_id,
                    oem_data: [d[3], d[4], d[5], d[6], d[7], d[8], d[9],
                               d[10], d[11], d[12], d[13], d[14], d[15]],
                }))
            }
            other => Err(SelParseError::UnknownRecordType(other)),
        }
    }
}

После этой границы каждый потребитель выполняет сопоставление с enum. Компилятор требует обработать все три типа записей: вы не можете «забыть» OEM-записи.

Шаг 2 — разбор системного события: тип датчика → типизированное событие

Внутренняя диспетчеризация превращает байты данных события в сумму типов, индексированную типом датчика. Вложенный switch из C здесь становится вложенным enum:

#[derive(Debug)]
pub struct SystemEventRecord {
    pub record_id: u16,
    pub timestamp: u32,
    pub generator: GeneratorId,
    pub sensor_type: SensorType,
    pub sensor_number: u8,
    pub event_direction: EventDirection,
    pub event: TypedEvent,      // ← ключевое: данные события ТИПИЗИРОВАНЫ
}

#[derive(Debug)]
pub enum GeneratorId {
    Software(u8),
    Ipmb { slave_addr: u8, channel: u8, lun: u8 },
}

#[derive(Debug, Clone, Copy, PartialEq)]
pub enum EventDirection { Assertion, Deassertion }

// ──── Иерархия типов датчиков и событий ────

/// Типы датчиков из таблицы 42-3 IPMI. Non-exhaustive, потому что будущие версии IPMI
/// и диапазоны OEM добавят варианты (см. приём 3 в гл. 11).
#[non_exhaustive]
#[derive(Debug, Clone, Copy, PartialEq)]
pub enum SensorType {
    Temperature,    // 0x01
    Voltage,        // 0x02
    Current,        // 0x03
    Fan,            // 0x04
    PhysicalSecurity, // 0x05
    Processor,      // 0x07
    PowerSupply,    // 0x08
    Memory,         // 0x0C
    SystemEvent,    // 0x12
    Watchdog2,      // 0x23
}

/// Полиморфная полезная нагрузка: каждый вариант несёт собственные типизированные данные.
#[derive(Debug)]
pub enum TypedEvent {
    Threshold(ThresholdEvent),
    SensorSpecific(SensorSpecificEvent),
    Discrete { offset: u8, event_data: [u8; 3] },
}

/// События порога несут значение триггера и пороговое значение.
/// Оба — сырые значения датчика (до линеаризации), хранятся как u8.
/// После линеаризации SDR они становятся размерными типами (гл. 06).
#[derive(Debug)]
pub struct ThresholdEvent {
    pub crossing: ThresholdCrossing,
    pub trigger_reading: u8,
    pub threshold_value: u8,
}

#[derive(Debug, Clone, Copy, PartialEq)]
pub enum ThresholdCrossing {
    LowerNonCriticalLow,
    LowerNonCriticalHigh,
    LowerCriticalLow,
    LowerCriticalHigh,
    LowerNonRecoverableLow,
    LowerNonRecoverableHigh,
    UpperNonCriticalLow,
    UpperNonCriticalHigh,
    UpperCriticalLow,
    UpperCriticalHigh,
    UpperNonRecoverableLow,
    UpperNonRecoverableHigh,
}

/// Специфичные для датчика события: каждый тип датчика получает свой вариант
/// с исчерпывающим enum событий этого датчика.
#[derive(Debug)]
pub enum SensorSpecificEvent {
    Temperature(TempEvent),
    Voltage(VoltageEvent),
    Fan(FanEvent),
    Processor(ProcessorEvent),
    PowerSupply(PowerSupplyEvent),
    Memory(MemoryEvent),
    PhysicalSecurity(PhysicalSecurityEvent),
    Watchdog(WatchdogEvent),
}

// ──── Перечисления событий для каждого типа датчика (из таблицы 42-3 IPMI) ────

#[derive(Debug, Clone, Copy, PartialEq)]
pub enum MemoryEvent {
    CorrectableEcc,
    UncorrectableEcc,
    Parity,
    MemoryBoardScrubFailed,
    MemoryDeviceDisabled,
    CorrectableEccLogLimit,
    PresenceDetected,
    ConfigurationError,
    Spare,
    Throttled,
    CriticalOvertemperature,
}

#[derive(Debug, Clone, Copy, PartialEq)]
pub enum PowerSupplyEvent {
    PresenceDetected,
    Failure,
    PredictiveFailure,
    InputLost,
    InputOutOfRange,
    InputLostOrOutOfRange,
    ConfigurationError,
    InactiveStandby,
}

#[derive(Debug, Clone, Copy, PartialEq)]
pub enum TempEvent {
    UpperNonCritical,
    UpperCritical,
    UpperNonRecoverable,
    LowerNonCritical,
    LowerCritical,
    LowerNonRecoverable,
}

#[derive(Debug, Clone, Copy, PartialEq)]
pub enum VoltageEvent {
    UpperNonCritical,
    UpperCritical,
    UpperNonRecoverable,
    LowerNonCritical,
    LowerCritical,
    LowerNonRecoverable,
}

#[derive(Debug, Clone, Copy, PartialEq)]
pub enum FanEvent {
    UpperNonCritical,
    UpperCritical,
    UpperNonRecoverable,
    LowerNonCritical,
    LowerCritical,
    LowerNonRecoverable,
}

#[derive(Debug, Clone, Copy, PartialEq)]
pub enum ProcessorEvent {
    Ierr,
    ThermalTrip,
    Frb1BistFailure,
    Frb2HangInPost,
    Frb3ProcessorStartupFailure,
    ConfigurationError,
    UncorrectableMachineCheck,
    PresenceDetected,
    Disabled,
    TerminatorPresenceDetected,
    Throttled,
}

#[derive(Debug, Clone, Copy, PartialEq)]
pub enum PhysicalSecurityEvent {
    ChassisIntrusion,
    DriveIntrusion,
    IOCardAreaIntrusion,
    ProcessorAreaIntrusion,
    LanLeashedLost,
    UnauthorizedDocking,
    FanAreaIntrusion,
}

#[derive(Debug, Clone, Copy, PartialEq)]
pub enum WatchdogEvent {
    BiosReset,
    OsReset,
    OsShutdown,
    OsPowerDown,
    OsPowerCycle,
    BiosNmi,
    Timer,
}

Шаг 3 — связка парсера

fn parse_system_event(record_id: u16, d: &[u8]) -> Result<SystemEventRecord, SelParseError> {
    let timestamp = u32::from_le_bytes([d[3], d[4], d[5], d[6]]);

    let generator = if d[7] & 0x01 == 0 {
        GeneratorId::Ipmb {
            slave_addr: d[7] & 0xFE,
            channel: (d[8] >> 4) & 0x0F,
            lun: d[8] & 0x03,
        }
    } else {
        GeneratorId::Software(d[7])
    };

    let sensor_type = parse_sensor_type(d[10])?;
    let sensor_number = d[11];
    let event_direction = if d[12] & 0x80 != 0 {
        EventDirection::Deassertion
    } else {
        EventDirection::Assertion
    };

    let event_type_code = d[12] & 0x7F;
    let event_data = [d[13], d[14], d[15]];

    let event = match event_type_code {
        0x01 => {
            // Порог — байт 2 данных события это показание триггера, байт 3 — порог
            let offset = event_data[0] & 0x0F;
            TypedEvent::Threshold(ThresholdEvent {
                crossing: parse_threshold_crossing(offset)?,
                trigger_reading: event_data[1],
                threshold_value: event_data[2],
            })
        }
        0x6F => {
            // Специфичное для датчика — диспетчеризация по типу датчика
            let offset = event_data[0] & 0x0F;
            let specific = parse_sensor_specific(&sensor_type, offset)?;
            TypedEvent::SensorSpecific(specific)
        }
        0x02..=0x0C => {
            // Дискретное общего вида
            TypedEvent::Discrete { offset: event_data[0] & 0x0F, event_data }
        }
        other => return Err(SelParseError::UnknownEventType(other)),
    };

    Ok(SystemEventRecord {
        record_id,
        timestamp,
        generator,
        sensor_type,
        sensor_number,
        event_direction,
        event,
    })
}

fn parse_sensor_type(code: u8) -> Result<SensorType, SelParseError> {
    match code {
        0x01 => Ok(SensorType::Temperature),
        0x02 => Ok(SensorType::Voltage),
        0x03 => Ok(SensorType::Current),
        0x04 => Ok(SensorType::Fan),
        0x05 => Ok(SensorType::PhysicalSecurity),
        0x07 => Ok(SensorType::Processor),
        0x08 => Ok(SensorType::PowerSupply),
        0x0C => Ok(SensorType::Memory),
        0x12 => Ok(SensorType::SystemEvent),
        0x23 => Ok(SensorType::Watchdog2),
        other => Err(SelParseError::UnknownSensorType(other)),
    }
}

fn parse_threshold_crossing(offset: u8) -> Result<ThresholdCrossing, SelParseError> {
    match offset {
        0x00 => Ok(ThresholdCrossing::LowerNonCriticalLow),
        0x01 => Ok(ThresholdCrossing::LowerNonCriticalHigh),
        0x02 => Ok(ThresholdCrossing::LowerCriticalLow),
        0x03 => Ok(ThresholdCrossing::LowerCriticalHigh),
        0x04 => Ok(ThresholdCrossing::LowerNonRecoverableLow),
        0x05 => Ok(ThresholdCrossing::LowerNonRecoverableHigh),
        0x06 => Ok(ThresholdCrossing::UpperNonCriticalLow),
        0x07 => Ok(ThresholdCrossing::UpperNonCriticalHigh),
        0x08 => Ok(ThresholdCrossing::UpperCriticalLow),
        0x09 => Ok(ThresholdCrossing::UpperCriticalHigh),
        0x0A => Ok(ThresholdCrossing::UpperNonRecoverableLow),
        0x0B => Ok(ThresholdCrossing::UpperNonRecoverableHigh),
        _ => Err(SelParseError::InvalidEventData {
            reason: "смещение порога вне диапазона",
        }),
    }
}

fn parse_sensor_specific(
    sensor_type: &SensorType,
    offset: u8,
) -> Result<SensorSpecificEvent, SelParseError> {
    match sensor_type {
        SensorType::Memory => {
            let ev = match offset {
                0x00 => MemoryEvent::CorrectableEcc,
                0x01 => MemoryEvent::UncorrectableEcc,
                0x02 => MemoryEvent::Parity,
                0x03 => MemoryEvent::MemoryBoardScrubFailed,
                0x04 => MemoryEvent::MemoryDeviceDisabled,
                0x05 => MemoryEvent::CorrectableEccLogLimit,
                0x06 => MemoryEvent::PresenceDetected,
                0x07 => MemoryEvent::ConfigurationError,
                0x08 => MemoryEvent::Spare,
                0x09 => MemoryEvent::Throttled,
                0x0A => MemoryEvent::CriticalOvertemperature,
                _ => return Err(SelParseError::InvalidEventData {
                    reason: "неизвестное смещение события памяти",
                }),
            };
            Ok(SensorSpecificEvent::Memory(ev))
        }
        SensorType::PowerSupply => {
            let ev = match offset {
                0x00 => PowerSupplyEvent::PresenceDetected,
                0x01 => PowerSupplyEvent::Failure,
                0x02 => PowerSupplyEvent::PredictiveFailure,
                0x03 => PowerSupplyEvent::InputLost,
                0x04 => PowerSupplyEvent::InputOutOfRange,
                0x05 => PowerSupplyEvent::InputLostOrOutOfRange,
                0x06 => PowerSupplyEvent::ConfigurationError,
                0x07 => PowerSupplyEvent::InactiveStandby,
                _ => return Err(SelParseError::InvalidEventData {
                    reason: "неизвестное смещение события блока питания",
                }),
            };
            Ok(SensorSpecificEvent::PowerSupply(ev))
        }
        SensorType::Processor => {
            let ev = match offset {
                0x00 => ProcessorEvent::Ierr,
                0x01 => ProcessorEvent::ThermalTrip,
                0x02 => ProcessorEvent::Frb1BistFailure,
                0x03 => ProcessorEvent::Frb2HangInPost,
                0x04 => ProcessorEvent::Frb3ProcessorStartupFailure,
                0x05 => ProcessorEvent::ConfigurationError,
                0x06 => ProcessorEvent::UncorrectableMachineCheck,
                0x07 => ProcessorEvent::PresenceDetected,
                0x08 => ProcessorEvent::Disabled,
                0x09 => ProcessorEvent::TerminatorPresenceDetected,
                0x0A => ProcessorEvent::Throttled,
                _ => return Err(SelParseError::InvalidEventData {
                    reason: "неизвестное смещение события процессора",
                }),
            };
            Ok(SensorSpecificEvent::Processor(ev))
        }
        // Аналогично для Temperature, Voltage, Fan и других типов.
        // Каждый тип датчика сопоставляет свои смещения с отдельным enum.
        _ => Err(SelParseError::InvalidEventData {
            reason: "диспетчеризация специфичных событий не реализована для этого типа датчика",
        }),
    }
}

Шаг 4 — потребление типизированных записей SEL

После разбора нижележащий код выполняет сопоставление с образцом по вложенным enum. Компилятор требует исчерпывающей обработки: никаких тихих проваливаний и забытых типов датчиков:

/// Определяет, должно ли событие SEL вызывать аппаратное оповещение.
/// Компилятор гарантирует, что обрабатывается каждый вариант.
fn should_alert(record: &ValidSelRecord) -> bool {
    match record {
        ValidSelRecord::SystemEvent(sys) => match &sys.event {
            TypedEvent::Threshold(t) => {
                // Любое критическое или невосстанавливаемое пересечение порога → оповещение
                matches!(t.crossing,
                    ThresholdCrossing::UpperCriticalLow
                    | ThresholdCrossing::UpperCriticalHigh
                    | ThresholdCrossing::LowerCriticalLow
                    | ThresholdCrossing::LowerCriticalHigh
                    | ThresholdCrossing::UpperNonRecoverableLow
                    | ThresholdCrossing::UpperNonRecoverableHigh
                    | ThresholdCrossing::LowerNonRecoverableLow
                    | ThresholdCrossing::LowerNonRecoverableHigh
                )
            }
            TypedEvent::SensorSpecific(ss) => match ss {
                SensorSpecificEvent::Memory(m) => matches!(m,
                    MemoryEvent::UncorrectableEcc
                    | MemoryEvent::Parity
                    | MemoryEvent::CriticalOvertemperature
                ),
                SensorSpecificEvent::PowerSupply(p) => matches!(p,
                    PowerSupplyEvent::Failure
                    | PowerSupplyEvent::InputLost
                ),
                SensorSpecificEvent::Processor(p) => matches!(p,
                    ProcessorEvent::Ierr
                    | ProcessorEvent::ThermalTrip
                    | ProcessorEvent::UncorrectableMachineCheck
                ),
                // Без запасной ветки `_`: новый вариант датчика заставит явно решить,
                // вызывает ли он оповещение. Иначе компилятор выдаст ошибку non-exhaustive patterns
                SensorSpecificEvent::Temperature(_)
                | SensorSpecificEvent::Voltage(_)
                | SensorSpecificEvent::Fan(_)
                | SensorSpecificEvent::PhysicalSecurity(_)
                | SensorSpecificEvent::Watchdog(_) => false,
            },
            TypedEvent::Discrete { .. } => false,
        },
        // Записи OEM в этой политике не вызывают оповещений
        ValidSelRecord::OemTimestamped(_) => false,
        ValidSelRecord::OemNonTimestamped(_) => false,
    }
}

/// Формирует понятное человеку описание.
/// Каждая ветка даёт конкретное сообщение — без запасного варианта «неизвестное событие».
fn describe(record: &ValidSelRecord) -> String {
    match record {
        ValidSelRecord::SystemEvent(sys) => {
            let sensor = format!("датчик {:?} #{}", sys.sensor_type, sys.sensor_number);
            let dir = match sys.event_direction {
                EventDirection::Assertion => "установлено",
                EventDirection::Deassertion => "снято",
            };
            match &sys.event {
                TypedEvent::Threshold(t) => {
                    format!("{sensor}: {:?} {dir} (показание: 0x{:02X}, порог: 0x{:02X})",
                        t.crossing, t.trigger_reading, t.threshold_value)
                }
                TypedEvent::SensorSpecific(ss) => {
                    format!("{sensor}: {ss:?} {dir}")
                }
                TypedEvent::Discrete { offset, .. } => {
                    format!("{sensor}: дискретное смещение {offset:#x} {dir}")
                }
            }
        }
        ValidSelRecord::OemTimestamped(oem) =>
            format!("OEM-запись 0x{:04X} (производитель {:02X}{:02X}{:02X})",
                oem.record_id,
                oem.manufacturer_id[0], oem.manufacturer_id[1], oem.manufacturer_id[2]),
        ValidSelRecord::OemNonTimestamped(oem) =>
            format!("OEM-запись без метки времени 0x{:04X}", oem.record_id),
    }
}

Пошаговый разбор: сквозная обработка SEL

Вот полный поток: от сырых байтов по линии до решения о тревоге, с каждой типизированной передачей:

/// Обрабатывает все записи SEL от BMC, формируя типизированные тревоги.
fn process_sel_log(raw_entries: &[[u8; 16]]) -> Vec<String> {
    let mut alerts = Vec::new();

    for (i, raw_bytes) in raw_entries.iter().enumerate() {
        // ─── Граница: сырые байты → проверенная запись ───
        let raw = RawSelRecord(*raw_bytes);
        let record = match ValidSelRecord::try_from(raw) {
            Ok(r) => r,
            Err(e) => {
                eprintln!("Запись SEL {i}: ошибка разбора: {e}");
                continue;
            }
        };

        // ─── Отсюда всё типизировано ───

        // 1. Описываем событие (исчерпывающее сопоставление — покрыты все варианты)
        let description = describe(&record);
        println!("SEL[{i}]: {description}");

        // 2. Проверяем политику тревог (исчерпывающее сопоставление — компилятор доказывает полноту)
        if should_alert(&record) {
            alerts.push(description);
        }

        // 3. Извлекаем размерные показания из событий порога
        if let ValidSelRecord::SystemEvent(sys) = &record {
            if let TypedEvent::Threshold(t) = &sys.event {
                // Компилятор знает, что t.trigger_reading — это показание события порога,
                // а не произвольный байт. После линеаризации SDR (гл. 06) это станет:
                //   let temp: Celsius = linearize(t.trigger_reading, &sdr);
                // А затем Celsius нельзя сравнить с Rpm.
                println!(
                    "  → сырое показание: 0x{:02X}, сырой порог: 0x{:02X}",
                    t.trigger_reading, t.threshold_value
                );
            }
        }
    }

    alerts
}

fn main() {
    // Пример: две записи SEL (сфабрикованы для иллюстрации)
    let sel_data: Vec<[u8; 16]> = vec![
        // Запись 1: системное событие, датчик памяти #3, специфичное для датчика,
        //           смещение 0x00 = CorrectableEcc, установка
        [
            0x01, 0x00,       // ID записи: 1
            0x02,             // тип записи: системное событие
            0x00, 0x00, 0x00, 0x00, // метка времени (заглушка)
            0x20,             // генератор: адрес ведомого IPMB 0x20
            0x00,             // канал/LUN
            0x04,             // версия сообщения события
            0x0C,             // тип датчика: Memory (0x0C)
            0x03,             // номер датчика: 3
            0x6F,             // направление: установка, тип события: специфичное для датчика
            0x00,             // данные события 1: смещение 0x00 = CorrectableEcc
            0x00, 0x00,       // данные события 2–3
        ],
        // Запись 2: системное событие, датчик температуры #1, порог,
        //           смещение 0x09 = UpperCriticalHigh, показание=95, порог=90
        [
            0x02, 0x00,       // ID записи: 2
            0x02,             // тип записи: системное событие
            0x00, 0x00, 0x00, 0x00, // метка времени (заглушка)
            0x20,             // генератор
            0x00,             // канал/LUN
            0x04,             // версия сообщения события
            0x01,             // тип датчика: Temperature (0x01)
            0x01,             // номер датчика: 1
            0x01,             // направление: установка, тип события: порог (0x01)
            0x09,             // данные события 1: смещение 0x09 = UpperCriticalHigh
            0x5F,             // данные события 2: значение триггера (95 в сыром виде)
            0x5A,             // данные события 3: пороговое значение (90 в сыром виде)
        ],
    ];

    let alerts = process_sel_log(&sel_data);
    println!("\n=== ТРЕВОГИ ({}) ===", alerts.len());
    for alert in &alerts {
        println!("  🚨 {alert}");
    }
}

Ожидаемый вывод:

SEL[0]: датчик Memory #3: Memory(CorrectableEcc) установлено
SEL[1]: датчик Temperature #1: UpperCriticalHigh установлено (показание: 0x5F, порог: 0x5A)
  → сырое показание: 0x5F, сырой порог: 0x5A

=== ТРЕВОГИ (1) ===
  🚨 датчик Temperature #1: UpperCriticalHigh установлено (показание: 0x5F, порог: 0x5A)

Запись 0 (корректируемая ошибка ECC) регистрируется, но тревоги не вызывает. Запись 1 (верхний критический порог температуры) вызывает тревогу. Оба решения обеспечены исчерпывающим сопоставлением с образцом: компилятор доказывает, что каждый тип датчика и каждое пересечение порога обработаны.

От разобранных событий к здоровью Redfish: конвейер потребителя

Пошаговый разбор выше заканчивается тревогами, но в реальном BMC разобранные записи SEL попадают в агрегацию состояния здоровья Redfish (гл. 18). Текущая передача данных — это bool, который теряет информацию:

// ❌ С потерями: отбрасываем детали по подсистемам
pub struct SelSummary {
    pub has_critical_events: bool,
    pub total_entries: u32,
}

Это теряет всё, что дала система типов: какая подсистема затронута, уровень серьёзности и есть ли в показании размерные данные. Построим полный конвейер.

Шаг 1 — линеаризация SDR: сырые байты → размерные типы (гл. 06)

События порога SEL несут сырые показания датчика в байтах данных 2–3. SDR (Sensor Data Record) IPMI задаёт формулу линеаризации. После линеаризации сырой байт становится размерным типом:

/// Коэффициенты линеаризации SDR для одного датчика.
/// См. раздел 36.3 спецификации IPMI для полной формулы.
pub struct SdrLinearization {
    pub sensor_type: SensorType,
    pub m: i16,        // множитель
    pub b: i16,        // смещение
    pub r_exp: i8,     // экспонента результата (степень 10)
    pub b_exp: i8,     // экспонента B
}

/// Линеаризованное показание датчика с прикреплённой единицей измерения.
/// Возвращаемый тип зависит от типа датчика: компилятор гарантирует, что датчики
/// температуры возвращают Celsius, а не Rpm.
#[derive(Debug, Clone)]
pub enum LinearizedReading {
    Temperature(Celsius),
    Voltage(Volts),
    Fan(Rpm),
    Current(Amps),
    Power(Watts),
}

#[derive(Debug, Clone, Copy, PartialEq, PartialOrd)]
pub struct Amps(pub f64);

impl SdrLinearization {
    /// Применяет формулу линеаризации IPMI:
    ///   y = (M × raw + B × 10^B_exp) × 10^R_exp
    /// Возвращает размерный тип в зависимости от типа датчика.
    pub fn linearize(&self, raw: u8) -> LinearizedReading {
        let y = (self.m as f64 * raw as f64
                + self.b as f64 * 10_f64.powi(self.b_exp as i32))
                * 10_f64.powi(self.r_exp as i32);

        match self.sensor_type {
            SensorType::Temperature => LinearizedReading::Temperature(Celsius(y)),
            SensorType::Voltage     => LinearizedReading::Voltage(Volts(y)),
            SensorType::Fan         => LinearizedReading::Fan(Rpm(y as u32)),
            SensorType::Current     => LinearizedReading::Current(Amps(y)),
            SensorType::PowerSupply => LinearizedReading::Power(Watts(y)),
            // Остальные типы датчиков — расширить при необходимости
            _ => LinearizedReading::Temperature(Celsius(y)),
        }
    }
}

Теперь сырой байт 0x5F (95 в десятичной системе) из нашего пошагового разбора SEL становится Celsius(95.0), и компилятор не даст сравнить его с Rpm или Watts.

Шаг 2 — классификация здоровья по подсистемам

Вместо свёртывания всего в has_critical_events: bool классифицируем каждое разобранное событие SEL по корзине здоровья подсистемы:

/// Худшее значение здоровья — Ord даёт нам `.max()` бесплатно.
/// (Полное определение в гл. 18; здесь повторено для конвейера SEL.)
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
pub enum HealthValue { OK, Warning, Critical }

/// Вклад одного события SEL в здоровье, классифицированный по подсистемам.
#[derive(Debug, Clone)]
pub enum SubsystemHealth {
    Processor(HealthValue),
    Memory(HealthValue),
    PowerSupply(HealthValue),
    Thermal(HealthValue),
    Fan(HealthValue),
    Storage(HealthValue),
    Security(HealthValue),
}

/// Классифицирует типизированное событие SEL по подсистемам.
/// Исчерпывающее сопоставление гарантирует, что каждый тип датчика вносит вклад.
fn classify_event_health(record: &SystemEventRecord) -> SubsystemHealth {
    match &record.event {
        TypedEvent::Threshold(t) => {
            // Серьёзность порога зависит от уровня пересечения
            let health = match t.crossing {
                // Некритичный → Warning
                ThresholdCrossing::UpperNonCriticalLow
                | ThresholdCrossing::UpperNonCriticalHigh
                | ThresholdCrossing::LowerNonCriticalLow
                | ThresholdCrossing::LowerNonCriticalHigh => HealthValue::Warning,

                // Критический или невосстанавливаемый → Critical
                ThresholdCrossing::UpperCriticalLow
                | ThresholdCrossing::UpperCriticalHigh
                | ThresholdCrossing::LowerCriticalLow
                | ThresholdCrossing::LowerCriticalHigh
                | ThresholdCrossing::UpperNonRecoverableLow
                | ThresholdCrossing::UpperNonRecoverableHigh
                | ThresholdCrossing::LowerNonRecoverableLow
                | ThresholdCrossing::LowerNonRecoverableHigh => HealthValue::Critical,
            };

            // Направляем в нужную подсистему по типу датчика
            match record.sensor_type {
                SensorType::Temperature => SubsystemHealth::Thermal(health),
                SensorType::Voltage     => SubsystemHealth::PowerSupply(health),
                SensorType::Current     => SubsystemHealth::PowerSupply(health),
                SensorType::Fan         => SubsystemHealth::Fan(health),
                SensorType::Processor   => SubsystemHealth::Processor(health),
                SensorType::PowerSupply => SubsystemHealth::PowerSupply(health),
                SensorType::Memory      => SubsystemHealth::Memory(health),
                _                       => SubsystemHealth::Thermal(health),
            }
        }

        TypedEvent::SensorSpecific(ss) => match ss {
            SensorSpecificEvent::Memory(m) => {
                let health = match m {
                    MemoryEvent::UncorrectableEcc
                    | MemoryEvent::Parity
                    | MemoryEvent::CriticalOvertemperature => HealthValue::Critical,

                    MemoryEvent::CorrectableEccLogLimit
                    | MemoryEvent::MemoryBoardScrubFailed
                    | MemoryEvent::Throttled => HealthValue::Warning,

                    MemoryEvent::CorrectableEcc
                    | MemoryEvent::PresenceDetected
                    | MemoryEvent::MemoryDeviceDisabled
                    | MemoryEvent::ConfigurationError
                    | MemoryEvent::Spare => HealthValue::OK,
                };
                SubsystemHealth::Memory(health)
            }

            SensorSpecificEvent::PowerSupply(p) => {
                let health = match p {
                    PowerSupplyEvent::Failure
                    | PowerSupplyEvent::InputLost => HealthValue::Critical,

                    PowerSupplyEvent::PredictiveFailure
                    | PowerSupplyEvent::InputOutOfRange
                    | PowerSupplyEvent::InputLostOrOutOfRange
                    | PowerSupplyEvent::ConfigurationError => HealthValue::Warning,

                    PowerSupplyEvent::PresenceDetected
                    | PowerSupplyEvent::InactiveStandby => HealthValue::OK,
                };
                SubsystemHealth::PowerSupply(health)
            }

            SensorSpecificEvent::Processor(p) => {
                let health = match p {
                    ProcessorEvent::Ierr
                    | ProcessorEvent::ThermalTrip
                    | ProcessorEvent::UncorrectableMachineCheck => HealthValue::Critical,

                    ProcessorEvent::Frb1BistFailure
                    | ProcessorEvent::Frb2HangInPost
                    | ProcessorEvent::Frb3ProcessorStartupFailure
                    | ProcessorEvent::ConfigurationError
                    | ProcessorEvent::Disabled => HealthValue::Warning,

                    ProcessorEvent::PresenceDetected
                    | ProcessorEvent::TerminatorPresenceDetected
                    | ProcessorEvent::Throttled => HealthValue::OK,
                };
                SubsystemHealth::Processor(health)
            }

            SensorSpecificEvent::PhysicalSecurity(_) =>
                SubsystemHealth::Security(HealthValue::Warning),

            SensorSpecificEvent::Watchdog(_) =>
                SubsystemHealth::Processor(HealthValue::Warning),

            // Специфичные для датчиков температуры, напряжения и вентилятора события
            SensorSpecificEvent::Temperature(_) =>
                SubsystemHealth::Thermal(HealthValue::Warning),
            SensorSpecificEvent::Voltage(_) =>
                SubsystemHealth::PowerSupply(HealthValue::Warning),
            SensorSpecificEvent::Fan(_) =>
                SubsystemHealth::Fan(HealthValue::Warning),
        },

        TypedEvent::Discrete { .. } => {
            // Дискретное общего вида: классифицируем по типу датчика с Warning
            match record.sensor_type {
                SensorType::Processor => SubsystemHealth::Processor(HealthValue::Warning),
                SensorType::Memory    => SubsystemHealth::Memory(HealthValue::Warning),
                _                     => SubsystemHealth::Thermal(HealthValue::OK),
            }
        }
    }
}

Каждая ветка match исчерпывающая: добавьте новый вариант MemoryEvent, и компилятор заставит вас определить его серьёзность. Добавьте новый вариант SensorSpecificEvent, и каждый потребитель должен будет его классифицировать. Это и есть выигрыш от дерева enum из раздела о разборе.

Шаг 3 — агрегация в типизированную сводку SEL

Заменяем bool, который теряет информацию, структурированной сводкой, сохраняющей здоровье по подсистемам:

use std::collections::HashMap;

/// Расширенная сводка SEL: здоровье по подсистемам, выведенное из типизированных событий.
/// Именно её получает сервер Redfish (гл. 18) для агрегации состояния здоровья.
#[derive(Debug, Clone)]
pub struct TypedSelSummary {
    pub total_entries: u32,
    pub processor_health: HealthValue,
    pub memory_health: HealthValue,
    pub power_health: HealthValue,
    pub thermal_health: HealthValue,
    pub fan_health: HealthValue,
    pub storage_health: HealthValue,
    pub security_health: HealthValue,
    /// Размерные показания из событий порога (после линеаризации).
    pub threshold_readings: Vec<LinearizedThresholdEvent>,
}

/// Событие порога с прикреплёнными линеаризованными показаниями.
#[derive(Debug, Clone)]
pub struct LinearizedThresholdEvent {
    pub sensor_type: SensorType,
    pub sensor_number: u8,
    pub crossing: ThresholdCrossing,
    pub trigger_reading: LinearizedReading,
    pub threshold_value: LinearizedReading,
}

/// Строит TypedSelSummary из разобранных записей SEL.
/// Это конвейер потребителя: разбор (шаг 0 выше) → классификация → агрегация.
pub fn summarize_sel(
    records: &[ValidSelRecord],
    sdr_table: &HashMap<u8, SdrLinearization>,
) -> TypedSelSummary {
    let mut processor = HealthValue::OK;
    let mut memory = HealthValue::OK;
    let mut power = HealthValue::OK;
    let mut thermal = HealthValue::OK;
    let mut fan = HealthValue::OK;
    let mut storage = HealthValue::OK;
    let mut security = HealthValue::OK;
    let mut threshold_readings = Vec::new();
    let mut count = 0u32;

    for record in records {
        count += 1;

        let ValidSelRecord::SystemEvent(sys) = record else {
            continue; // записи OEM не влияют на здоровье
        };

        // ── Классифицируем событие → здоровье по подсистемам ──
        let health = classify_event_health(sys);
        match &health {
            SubsystemHealth::Processor(h) => processor = processor.max(*h),
            SubsystemHealth::Memory(h)    => memory = memory.max(*h),
            SubsystemHealth::PowerSupply(h) => power = power.max(*h),
            SubsystemHealth::Thermal(h)   => thermal = thermal.max(*h),
            SubsystemHealth::Fan(h)       => fan = fan.max(*h),
            SubsystemHealth::Storage(h)   => storage = storage.max(*h),
            SubsystemHealth::Security(h)  => security = security.max(*h),
        }

        // ── Линеаризуем показания порога, если SDR доступен ──
        if let TypedEvent::Threshold(t) = &sys.event {
            if let Some(sdr) = sdr_table.get(&sys.sensor_number) {
                threshold_readings.push(LinearizedThresholdEvent {
                    sensor_type: sys.sensor_type,
                    sensor_number: sys.sensor_number,
                    crossing: t.crossing,
                    trigger_reading: sdr.linearize(t.trigger_reading),
                    threshold_value: sdr.linearize(t.threshold_value),
                });
            }
        }
    }

    TypedSelSummary {
        total_entries: count,
        processor_health: processor,
        memory_health: memory,
        power_health: power,
        thermal_health: thermal,
        fan_health: fan,
        storage_health: storage,
        security_health: security,
        threshold_readings,
    }
}

Шаг 4 — полный конвейер: сырые байты → здоровье Redfish

Вот полный конвейер потребителя, с каждой типизированной передачей от сырых байтов SEL до значений здоровья, готовых для Redfish:

flowchart LR
    RAW["Сырые [u8; 16]<br/>записи SEL"]
    PARSE["TryFrom:<br/>ValidSelRecord<br/>(дерево enum)"]
    CLASSIFY["classify_event_health<br/>(исчерпывающее match)"]
    LINEARIZE["Линеаризация SDR<br/>сырое → Celsius/Rpm/Watts"]
    SUMMARY["TypedSelSummary<br/>(здоровье по подсистемам<br/>+ размерные показания)"]
    REDFISH["гл. 18: агрегация здоровья<br/>→ JSON Status.Health"]

    RAW -->|"гл. 07 §Разбор"| PARSE
    PARSE -->|"типизированные события"| CLASSIFY
    PARSE -->|"байты порога"| LINEARIZE
    CLASSIFY -->|"SubsystemHealth"| SUMMARY
    LINEARIZE -->|"LinearizedReading"| SUMMARY
    SUMMARY -->|"TypedSelSummary"| REDFISH

    style RAW fill:#fff3e0,color:#000
    style PARSE fill:#e1f5fe,color:#000
    style CLASSIFY fill:#f3e5f5,color:#000
    style LINEARIZE fill:#e8f5e9,color:#000
    style SUMMARY fill:#c8e6c9,color:#000
    style REDFISH fill:#bbdefb,color:#000
use std::collections::HashMap;

fn full_sel_pipeline() {
    // ── Сырые данные SEL от BMC ──
    let raw_entries: Vec<[u8; 16]> = vec![
        // Корректируемая ошибка ECC памяти на датчике #3
        [0x01,0x00, 0x02, 0x00,0x00,0x00,0x00,
         0x20,0x00, 0x04, 0x0C, 0x03, 0x6F, 0x00, 0x00,0x00],
        // Верхний критический порог температуры на датчике #1, показание=95, порог=90
        [0x02,0x00, 0x02, 0x00,0x00,0x00,0x00,
         0x20,0x00, 0x04, 0x01, 0x01, 0x01, 0x09, 0x5F,0x5A],
        // Отказ блока питания на датчике #5
        [0x03,0x00, 0x02, 0x00,0x00,0x00,0x00,
         0x20,0x00, 0x04, 0x08, 0x05, 0x6F, 0x01, 0x00,0x00],
    ];

    // ── Шаг 0: разбор на границе (TryFrom, гл. 07) ──
    let records: Vec<ValidSelRecord> = raw_entries.iter()
        .filter_map(|raw| ValidSelRecord::try_from(RawSelRecord(*raw)).ok())
        .collect();

    // ── Шаги 1–3: классификация, линеаризация, агрегация ──
    let mut sdr_table = HashMap::new();
    sdr_table.insert(1u8, SdrLinearization {
        sensor_type: SensorType::Temperature,
        m: 1, b: 0, r_exp: 0, b_exp: 0,  // отображение 1:1 для этого примера
    });

    let summary = summarize_sel(&records, &sdr_table);

    // ── Результат: структурированный, типизированный, готовый для Redfish ──
    println!("Сводка SEL:");
    println!("  Всего записей: {}", summary.total_entries);
    println!("  Процессор:    {:?}", summary.processor_health);  // OK
    println!("  Память:       {:?}", summary.memory_health);      // OK (корректируемая → OK)
    println!("  Питание:      {:?}", summary.power_health);       // Critical (отказ БП)
    println!("  Температура:  {:?}", summary.thermal_health);     // Critical (верхний критический)
    println!("  Вентилятор:   {:?}", summary.fan_health);         // OK
    println!("  Безопасность: {:?}", summary.security_health);    // OK

    // Размерные показания, сохранённые из событий порога:
    for r in &summary.threshold_readings {
        println!("  Порог: датчик {:?} #{} — {:?} пересёк {:?}",
            r.sensor_type, r.sensor_number,
            r.trigger_reading, r.crossing);
        // trigger_reading — это LinearizedReading::Temperature(Celsius(95.0)),
        // а не сырой байт и не нетипизированный f64
    }

    // ── Эта сводка напрямую поступает в агрегацию здоровья гл. 18 ──
    // compute_system_health() теперь может использовать значения по подсистемам
    // вместо одного `has_critical_events: bool`
}

Ожидаемый вывод:

Сводка SEL:
  Всего записей: 3
  Процессор:    OK
  Память:       OK
  Питание:      Critical
  Температура:  Critical
  Вентилятор:   OK
  Безопасность: OK
  Порог: датчик Temperature #1 — Temperature(Celsius(95.0)) пересёк UpperCriticalHigh

Что доказывает конвейер потребителя

ЭтапПаттернЧто обеспечивается
РазборПроверенная граница (гл. 07)Каждый потребитель работает с типизированными enum, а не с сырыми байтами
КлассификацияИсчерпывающее сопоставлениеКаждый тип датчика и вариант события сопоставлен со значением здоровья: ничего не пропустить
ЛинеаризацияАнализ размерностей (гл. 06)Сырой байт 0x5F становится Celsius(95.0), а не f64: нельзя перепутать с об/мин
АгрегацияТипизированная свёрткаЗдоровье по подсистемам использует HealthValue::max(): Ord гарантирует корректность
ПередачаСтруктурированная сводкагл. 18 получает TypedSelSummary с 7 значениями здоровья подсистем, а не bool

Сравним с нетипизированным конвейером на C:

ШагCRust
Разбор типа записиswitch с возможным проваливаниемmatch по enum: исчерпывающий
Классификация серьёзностиручная цепочка if, забыли БПисчерпывающий match: ошибка компиляции при отсутствующем варианте
Линеаризация показанияdouble: без единицCelsius / Rpm / Watts: разные типы
Агрегация здоровьяbool has_critical7 типизированных полей подсистем
Передача в Redfishнетипизированный json_object_set("Health", "OK")TypedSelSummary → типизированная агрегация здоровья (гл. 18)

Конвейер на Rust не просто предотвращает больше ошибок: он даёт более богатый результат. Конвейер на C теряет информацию на каждом этапе (полиморфное → плоское, размерное → нетипизированное, по подсистемам → один bool). Конвейер на Rust сохраняет всё, потому что система типов делает проще сохранить структуру, чем её выбросить.

Что доказывает компилятор

Ошибка в CКак Rust её предотвращает
Забыли проверить тип записиmatch по ValidSelRecord: обязан обработать все три варианта
Неверный индекс байта для показания триггераРазбирается один раз в ThresholdEvent.trigger_reading: потребители никогда не трогают сырые байты
Пропущен case для типа датчикаСопоставление SensorSpecificEvent исчерпывающее: ошибка компиляции при отсутствующем варианте
Тихо отброшенные записи OEMВариант enum существует: его нужно обработать или явно проигнорировать через _ =>
Сравнили показание порога (°C) со смещением вентилятораПосле линеаризации SDR Celsius ≠ Rpm (гл. 06)
Добавили новый тип датчика и забыли логику тревог#[non_exhaustive] + исчерпывающее сопоставление → ошибка компиляции в зависимых крейтах
Событие разбирается по-разному в двух путях кодаЕдиная граница parse_system_event(): один источник истины

Трёхэтапный паттерн

Посмотрев на три кейс-стади этой главы, заметьте постепенную дугу:

Кейс-стадиФорма входаСложность разбораКлючевая техника
FRU (байты)Плоская, фиксированная раскладкаОдин TryFrom, проверка полейТип проверенной границы
Redfish (JSON)Структурированная, известная схемаОдин TryFrom, проверка полей и вложенностиТа же техника, другой транспорт
SEL (полиморфные байты)Вложенное тегированное объединениеЦепочка диспетчеризации: тип записи → тип события → тип датчикаДерево enum + исчерпывающее сопоставление

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

Композиция проверенных типов

Проверенные типы компонуются: структура из проверенных полей сама является проверенной:

#[derive(Debug)]
pub struct ValidFru { format_version: u8 }
#[derive(Debug)]
pub struct ValidThermalResponse { }

/// Полностью проверенный снимок системы.
/// Каждое поле проверено независимо; составной объект тоже корректен.
#[derive(Debug)]
pub struct ValidSystemSnapshot {
    pub fru: ValidFru,
    pub thermal: ValidThermalResponse,
    // Каждое поле несёт собственную гарантию корректности.
    // Не нужна функция «validate_snapshot()».
}

/// Поскольку ValidSystemSnapshot составлен из проверенных частей,
/// любая функция, которая его получает, может доверять ВСЕМ данным.
fn generate_health_report(snapshot: &ValidSystemSnapshot) {
    println!("Версия FRU: {}", snapshot.fru.format_version);
    // Проверка не нужна: тип гарантирует всё
}

Ключевая идея

Проверяйте на границе. Храните доказательство в типе. Никогда не проверяйте повторно.

Это устраняет целый класс ошибок: «забыли проверить в этой одной функции». Если функция принимает &ValidFru, данные ЯВЛЯЮТСЯ корректными. Точка.

Когда использовать типы проверенных границ

Источник данныхИспользовать тип проверенной границы?
Данные IPMI FRU от BMC✅ Всегда: сложный бинарный формат
Ответы JSON Redfish✅ Всегда: много обязательных полей
Пространство конфигурации PCIe✅ Всегда: раскладка регистров строгая
Таблицы SMBIOS✅ Всегда: версионированный формат с контрольными суммами
Параметры тестов от пользователя✅ Всегда: предотвращает инъекции
Внутренние вызовы функций❌ Обычно нет: типы уже ограничивают
Сообщения журнала❌ Нет: best-effort, не критично для безопасности

Поток проверки на границе

flowchart LR
    RAW["Сырые байты / JSON"] -->|"TryFrom / serde"| V{"Корректно?"}
    V -->|Да| VT["ValidFru / ValidRedfish"]
    V -->|Нет| E["Err(ParseError)"]
    VT -->|"&ValidFru"| F1["fn process()"] & F2["fn report()"] & F3["fn store()"]
    style RAW fill:#fff3e0,color:#000
    style V fill:#e1f5fe,color:#000
    style VT fill:#c8e6c9,color:#000
    style E fill:#ffcdd2,color:#000
    style F1 fill:#e8f5e9,color:#000
    style F2 fill:#e8f5e9,color:#000
    style F3 fill:#e8f5e9,color:#000

Упражнение: проверенная таблица SMBIOS

Спроектируйте тип ValidSmbiosType17 для записей SMBIOS Type 17 (Memory Device):

  • Вход: &[u8]; минимальная длина 21 байт, байт 0 должен быть 0x11.
  • Поля: handle: u16, size_mb: u16, speed_mhz: u16.
  • Используйте TryFrom<&[u8]>, чтобы все нижележащие функции принимали &ValidSmbiosType17.
Решение
#[derive(Debug)]
pub struct ValidSmbiosType17 {
    pub handle: u16,
    pub size_mb: u16,
    pub speed_mhz: u16,
}

impl TryFrom<&[u8]> for ValidSmbiosType17 {
    type Error = String;
    fn try_from(raw: &[u8]) -> Result<Self, Self::Error> {
        if raw.len() < 21 {
            return Err(format!("слишком короткий: {} < 21", raw.len()));
        }
        if raw[0] != 0x11 {
            return Err(format!("неверный тип: 0x{:02X} != 0x11", raw[0]));
        }
        Ok(ValidSmbiosType17 {
            handle: u16::from_le_bytes([raw[1], raw[2]]),
            size_mb: u16::from_le_bytes([raw[12], raw[13]]),
            speed_mhz: u16::from_le_bytes([raw[19], raw[20]]),
        })
    }
}

// Нижележащие функции принимают проверенный тип: повторных проверок нет
pub fn report_dimm(dimm: &ValidSmbiosType17) -> String {
    format!("DIMM, handle 0x{:04X}: {} МБ @ {} МГц",
        dimm.handle, dimm.size_mb, dimm.speed_mhz)
}

Ключевые выводы

  1. Разбирайте один раз на границе: TryFrom проверяет сырые данные ровно один раз, а весь нижележащий код доверяет типу.
  2. Устраняйте разбросанную валидацию: если функция принимает &ValidFru, данные ЯВЛЯЮТСЯ корректными. Точка.
  3. Паттерн масштабируется от плоского к полиморфному: FRU (плоские байты), Redfish (структурированный JSON) и SEL (вложенное тегированное объединение) используют одну технику с возрастающей сложностью.
  4. Исчерпывающее сопоставление — это валидация: для полиморфных данных вроде SEL проверка исчерпываемости enum компилятора предотвращает класс ошибок «забыли тип датчика» без накладных расходов во время выполнения.
  5. Конвейер потребителя сохраняет структуру: разбор → классификация → линеаризация → агрегация сохраняют здоровье по подсистемам и размерные показания, тогда как C сводит всё к одному bool. Система типов делает проще сохранить информацию, чем её выбросить.
  6. serde — естественная граница: #[derive(Deserialize)] с #[serde(try_from)] проверяет JSON на этапе разбора.
  7. Компонуйте проверенные типы: ValidServerHealth может требовать ValidFru + ValidThermal + ValidPower.
  8. Дополняйте proptest (гл. 14): подавайте фаззинг на границу TryFrom, чтобы убедиться, что ни один корректный вход не отвергается и ни один некорректный не проходит.
  9. Эти паттерны складываются в полные сценарии Redfish: гл. 17 применяет проверенные границы на стороне клиента (разбор JSON-ответов в типизированные структуры), а гл. 18 переворачивает паттерн на стороне сервера (typestate билдера гарантирует наличие каждого обязательного поля до сериализации). Конвейер потребителя SEL, построенный здесь, напрямую передаёт TypedSelSummary в агрегацию здоровья гл. 18.

Capability-миксины: контракты с аппаратурой на этапе компиляции 🟡

Что вы узнаете: как трейты-ингредиенты (возможности шин) в сочетании с трейтами-миксинами и blanket impl устраняют дублирование диагностического кода и гарантируют на этапе компиляции, что каждая аппаратная зависимость выполнена.

Перекрёстные ссылки: гл. 04 (capability-токены), гл. 09 (phantom-типы), гл. 10 (интеграция)

Проблема: дублирование диагностического кода

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

// C — дублирующаяся логика в разных подсистемах
int run_fan_diag(spi_bus_t *spi, i2c_bus_t *i2c) {
    // ... 50 строк чтения датчика по SPI ...
    // ... 30 строк проверки регистров I2C ...
    // ... 20 строк сравнения с порогами (как в диагностике CPU) ...
}

int run_cpu_temp_diag(i2c_bus_t *i2c, gpio_t *gpio) {
    // ... 30 строк проверки регистров I2C (как в диагностике вентиляторов) ...
    // ... 15 строк проверки сигнала тревоги GPIO ...
    // ... 20 строк сравнения с порогами (как в диагностике вентиляторов) ...
}

Логика сравнения с порогами одинакова, но вынести её нельзя, потому что типы шин различаются. С capability-миксинами каждая аппаратная шина становится трейтом-ингредиентом, а диагностические поведения предоставляются автоматически, когда есть нужные ингредиенты.

Трейты-ингредиенты (аппаратные возможности)

Каждая шина или периферийное устройство — это ассоциированный тип трейта. Диагностический контроллер объявляет, какие шины у него есть:

/// Возможность шины SPI.
pub trait HasSpi {
    type Spi: SpiBus;
    fn spi(&self) -> &Self::Spi;
}

/// Возможность шины I2C.
pub trait HasI2c {
    type I2c: I2cBus;
    fn i2c(&self) -> &Self::I2c;
}

/// Возможность доступа к пинам GPIO.
pub trait HasGpio {
    type Gpio: GpioController;
    fn gpio(&self) -> &Self::Gpio;
}

/// Возможность доступа к IPMI.
pub trait HasIpmi {
    type Ipmi: IpmiClient;
    fn ipmi(&self) -> &Self::Ipmi;
}

// Определения трейтов шин:
pub trait SpiBus {
    fn transfer(&self, data: &[u8]) -> Vec<u8>;
}

pub trait I2cBus {
    fn read_register(&self, addr: u8, reg: u8) -> u8;
    fn write_register(&self, addr: u8, reg: u8, value: u8);
}

pub trait GpioController {
    fn read_pin(&self, pin: u32) -> bool;
    fn set_pin(&self, pin: u32, value: bool);
}

pub trait IpmiClient {
    fn send_raw(&self, netfn: u8, cmd: u8, data: &[u8]) -> Vec<u8>;
}

Трейты-миксины (диагностические поведения)

Миксин предоставляет поведение автоматически любому типу, у которого есть нужные возможности:

pub trait SpiBus { fn transfer(&self, data: &[u8]) -> Vec<u8>; }
pub trait I2cBus {
    fn read_register(&self, addr: u8, reg: u8) -> u8;
    fn write_register(&self, addr: u8, reg: u8, value: u8);
}
pub trait GpioController { fn read_pin(&self, pin: u32) -> bool; }
pub trait IpmiClient { fn send_raw(&self, netfn: u8, cmd: u8, data: &[u8]) -> Vec<u8>; }
pub trait HasSpi { type Spi: SpiBus; fn spi(&self) -> &Self::Spi; }
pub trait HasI2c { type I2c: I2cBus; fn i2c(&self) -> &Self::I2c; }
pub trait HasGpio { type Gpio: GpioController; fn gpio(&self) -> &Self::Gpio; }
pub trait HasIpmi { type Ipmi: IpmiClient; fn ipmi(&self) -> &Self::Ipmi; }

/// Миксин диагностики вентиляторов — реализуется автоматически для любого типа с SPI + I2C.
pub trait FanDiagMixin: HasSpi + HasI2c {
    fn read_fan_speed(&self, fan_id: u8) -> u32 {
        // Считываем тахометр через SPI
        let cmd = [0x80 | fan_id, 0x00];
        let response = self.spi().transfer(&cmd);
        u32::from_be_bytes([0, 0, response[0], response[1]])
    }

    fn set_fan_pwm(&self, fan_id: u8, duty_percent: u8) {
        // Устанавливаем PWM через контроллер I2C
        self.i2c().write_register(0x2E, fan_id, duty_percent);
    }

    fn run_fan_diagnostic(&self) -> bool {
        // Полная диагностика: читаем все вентиляторы, проверяем пороги
        for fan_id in 0..6 {
            let speed = self.read_fan_speed(fan_id);
            if speed < 1000 || speed > 20000 {
                println!("Вентилятор {fan_id}: ОШИБКА ({speed} об/мин)");
                return false;
            }
        }
        true
    }
}

// Blanket-реализация — ЛЮБОЙ тип с SPI + I2C получает FanDiagMixin бесплатно
impl<T: HasSpi + HasI2c> FanDiagMixin for T {}

/// Миксин мониторинга температуры — требует I2C + GPIO.
pub trait TempMonitorMixin: HasI2c + HasGpio {
    fn read_temperature(&self, sensor_addr: u8) -> f64 {
        let raw = self.i2c().read_register(sensor_addr, 0x00);
        raw as f64 * 0.5  // 0.5°C на младший разряд (LSB)
    }

    fn check_thermal_alert(&self, alert_pin: u32) -> bool {
        self.gpio().read_pin(alert_pin)
    }

    fn run_thermal_diagnostic(&self) -> bool {
        for addr in [0x48, 0x49, 0x4A] {
            let temp = self.read_temperature(addr);
            if temp > 95.0 {
                println!("Датчик 0x{addr:02X}: КРИТИЧНО ({temp}°C)");
                return false;
            }
            if self.check_thermal_alert(addr as u32) {
                println!("Датчик 0x{addr:02X}: сработал сигнал ALERT");
                return false;
            }
        }
        true
    }
}

impl<T: HasI2c + HasGpio> TempMonitorMixin for T {}

/// Миксин последовательности подачи питания — требует I2C + IPMI.
pub trait PowerSeqMixin: HasI2c + HasIpmi {
    fn read_voltage_rail(&self, rail: u8) -> f64 {
        let raw = self.i2c().read_register(0x40, rail);
        raw as f64 * 0.01  // 10 мВ на младший разряд (LSB)
    }

    fn check_power_good(&self) -> bool {
        let resp = self.ipmi().send_raw(0x04, 0x2D, &[0x01]);
        !resp.is_empty() && resp[0] == 0x00
    }
}

impl<T: HasI2c + HasIpmi> PowerSeqMixin for T {}

Конкретный контроллер: комбинирование

Конкретный диагностический контроллер объявляет свои возможности и автоматически наследует все подходящие миксины:

pub trait SpiBus { fn transfer(&self, data: &[u8]) -> Vec<u8>; }
pub trait I2cBus {
    fn read_register(&self, addr: u8, reg: u8) -> u8;
    fn write_register(&self, addr: u8, reg: u8, value: u8);
}
pub trait GpioController {
    fn read_pin(&self, pin: u32) -> bool;
    fn set_pin(&self, pin: u32, value: bool);
}
pub trait IpmiClient { fn send_raw(&self, netfn: u8, cmd: u8, data: &[u8]) -> Vec<u8>; }
pub trait HasSpi { type Spi: SpiBus; fn spi(&self) -> &Self::Spi; }
pub trait HasI2c { type I2c: I2cBus; fn i2c(&self) -> &Self::I2c; }
pub trait HasGpio { type Gpio: GpioController; fn gpio(&self) -> &Self::Gpio; }
pub trait HasIpmi { type Ipmi: IpmiClient; fn ipmi(&self) -> &Self::Ipmi; }
pub trait FanDiagMixin: HasSpi + HasI2c {}
impl<T: HasSpi + HasI2c> FanDiagMixin for T {}
pub trait TempMonitorMixin: HasI2c + HasGpio {}
impl<T: HasI2c + HasGpio> TempMonitorMixin for T {}
pub trait PowerSeqMixin: HasI2c + HasIpmi {}
impl<T: HasI2c + HasIpmi> PowerSeqMixin for T {}

// Конкретные реализации шин (заглушки для иллюстрации)
pub struct LinuxSpi { bus: u8 }
impl SpiBus for LinuxSpi {
    fn transfer(&self, data: &[u8]) -> Vec<u8> { vec![0; data.len()] }
}

pub struct LinuxI2c { bus: u8 }
impl I2cBus for LinuxI2c {
    fn read_register(&self, _addr: u8, _reg: u8) -> u8 { 42 }
    fn write_register(&self, _addr: u8, _reg: u8, _value: u8) {}
}

pub struct LinuxGpio;
impl GpioController for LinuxGpio {
    fn read_pin(&self, _pin: u32) -> bool { false }
    fn set_pin(&self, _pin: u32, _value: bool) {}
}

pub struct IpmiToolClient;
impl IpmiClient for IpmiToolClient {
    fn send_raw(&self, _netfn: u8, _cmd: u8, _data: &[u8]) -> Vec<u8> { vec![0x00] }
}

/// BaseBoardController имеет ВСЕ шины → получает ВСЕ миксины.
pub struct BaseBoardController {
    spi: LinuxSpi,
    i2c: LinuxI2c,
    gpio: LinuxGpio,
    ipmi: IpmiToolClient,
}

impl HasSpi for BaseBoardController {
    type Spi = LinuxSpi;
    fn spi(&self) -> &LinuxSpi { &self.spi }
}

impl HasI2c for BaseBoardController {
    type I2c = LinuxI2c;
    fn i2c(&self) -> &LinuxI2c { &self.i2c }
}

impl HasGpio for BaseBoardController {
    type Gpio = LinuxGpio;
    fn gpio(&self) -> &LinuxGpio { &self.gpio }
}

impl HasIpmi for BaseBoardController {
    type Ipmi = IpmiToolClient;
    fn ipmi(&self) -> &IpmiToolClient { &self.ipmi }
}

// BaseBoardController теперь автоматически имеет:
// - FanDiagMixin     (потому что у него есть HasSpi + HasI2c)
// - TempMonitorMixin (потому что у него есть HasI2c + HasGpio)
// - PowerSeqMixin    (потому что у него есть HasI2c + HasIpmi)
// Ручная реализация не нужна: blanket impl делает всё сам.

Аспект корректности по построению

Паттерн миксинов корректен по построению, потому что:

  1. Нельзя вызвать read_fan_speed() без SPI: метод существует только у типов, которые реализуют HasSpi + HasI2c
  2. Нельзя забыть шину: если убрать HasSpi у BaseBoardController, методы FanDiagMixin исчезнут на этапе компиляции
  3. Тестирование с моками автоматическое: замените LinuxSpi на MockSpi, и вся логика миксинов будет работать с моком
  4. Новые платформы просто объявляют возможности: дочерняя плата GPU только с I2C получит TempMonitorMixin (если есть ещё и GPIO), но не FanDiagMixin (нет SPI)

Когда использовать capability-миксины

СценарийИспользовать миксины?
Сквозные диагностические поведения✅ Да: устраняют copy-paste
Контроллеры аппаратуры с несколькими шинами✅ Да: объявляете возможности, получаете поведения
Специализированные тестовые стенды для платформ✅ Да: мокайте возможности для тестов
Простые периферийные устройства с одной шиной⚠️ Накладные расходы могут не окупиться
Чистая бизнес-логика (без аппаратуры)❌ Простых паттернов достаточно

Архитектура трейтов-миксинов

flowchart TD
    subgraph "Трейты-ингредиенты"
        SPI["HasSpi"]
        I2C["HasI2c"]
        GPIO["HasGpio"]
    end
    subgraph "Трейты-миксины (blanket impl)"
        FAN["FanDiagMixin"]
        TEMP["TempMonitorMixin"]
    end
    SPI & I2C -->|"нужны оба"| FAN
    I2C & GPIO -->|"нужны оба"| TEMP
    subgraph "Конкретные типы"
        BBC["BaseBoardController"]
    end
    BBC -->|"impl HasSpi + HasI2c + HasGpio"| FAN & TEMP
    style SPI fill:#e1f5fe,color:#000
    style I2C fill:#e1f5fe,color:#000
    style GPIO fill:#e1f5fe,color:#000
    style FAN fill:#c8e6c9,color:#000
    style TEMP fill:#c8e6c9,color:#000
    style BBC fill:#fff3e0,color:#000

Упражнение: сетевые диагностические миксины

Спроектируйте систему миксинов для сетевой диагностики:

  • Трейты-ингредиенты: HasEthernet, HasIpmi
  • Миксин: LinkHealthMixin (требует HasEthernet) с методом check_link_status(&self)
  • Миксин: RemoteDiagMixin (требует HasEthernet + HasIpmi) с методом remote_health_check(&self)
  • Конкретный тип: NicController, который реализует оба ингредиента.
Решение
pub trait HasEthernet {
    fn eth_link_up(&self) -> bool;
}

pub trait HasIpmi {
    fn ipmi_ping(&self) -> bool;
}

pub trait LinkHealthMixin: HasEthernet {
    fn check_link_status(&self) -> &'static str {
        if self.eth_link_up() { "канал: UP" } else { "канал: DOWN" }
    }
}
impl<T: HasEthernet> LinkHealthMixin for T {}

pub trait RemoteDiagMixin: HasEthernet + HasIpmi {
    fn remote_health_check(&self) -> &'static str {
        if self.eth_link_up() && self.ipmi_ping() {
            "удалённо: HEALTHY"
        } else {
            "удалённо: DEGRADED"
        }
    }
}
impl<T: HasEthernet + HasIpmi> RemoteDiagMixin for T {}

pub struct NicController;
impl HasEthernet for NicController {
    fn eth_link_up(&self) -> bool { true }
}
impl HasIpmi for NicController {
    fn ipmi_ping(&self) -> bool { true }
}
// NicController автоматически получает оба метода миксинов

Ключевые выводы

  1. Трейты-ингредиенты объявляют аппаратные возможности: HasSpi, HasI2c, HasGpio — это трейты с ассоциированными типами.
  2. Трейты-миксины предоставляют поведение через blanket impl: impl<T: HasSpi + HasI2c> FanDiagMixin for T {}.
  3. Новая платформа = перечисление её возможностей: компилятор предоставляет все подходящие методы миксинов.
  4. Удаление шины = ошибки компиляции везде, где она используется: нельзя забыть обновить нижележащий код.
  5. Тестирование с моками бесплатно: замените LinuxSpi на MockSpi, и вся логика миксинов работает без изменений.

Phantom-типы для отслеживания ресурсов 🟡

Что вы узнаете: как маркеры PhantomData кодируют ширину регистра, направление DMA и состояние файлового дескриптора на уровне типов, предотвращая целый класс ошибок рассогласования ресурсов без накладных расходов во время выполнения.

Перекрёстные ссылки: гл. 05 (typestate), гл. 06 (размерные типы), гл. 08 (миксины), гл. 10 (интеграция)

Проблема: путаница ресурсов

Аппаратные ресурсы в коде выглядят одинаково, но не взаимозаменяемы:

  • 32-битный регистр и 16-битный регистр — оба «регистры»
  • Буфер DMA для чтения и буфер DMA для записи оба выглядят как *mut u8
  • Открытый файловый дескриптор и закрытый — оба i32

В C:

// C — все регистры выглядят одинаково
uint32_t read_reg32(volatile void *base, uint32_t offset);
uint16_t read_reg16(volatile void *base, uint32_t offset);

// Ошибка: чтение 16-битного регистра 32-битной функцией
uint32_t status = read_reg32(pcie_bar, LINK_STATUS_REG);  // должен быть reg16!

Параметры phantom-типов

Phantom-тип — это параметр типа, который присутствует в определении структуры, но не появляется ни в одном поле. Он существует исключительно для того, чтобы нести информацию на уровне типов:

use std::marker::PhantomData;

// Маркеры ширины регистра — нулевого размера
pub struct Width8;
pub struct Width16;
pub struct Width32;
pub struct Width64;

/// Дескриптор регистра, параметризованный его шириной.
/// PhantomData<W> не занимает места: это маркер только для этапа компиляции.
pub struct Register<W> {
    base: usize,
    offset: usize,
    _width: PhantomData<W>,
}

impl Register<Width8> {
    pub fn read(&self) -> u8 {
        // ... читаем 1 байт из base + offset ...
        0 // заглушка
    }
    pub fn write(&self, _value: u8) {
        // ... пишем 1 байт ...
    }
}

impl Register<Width16> {
    pub fn read(&self) -> u16 {
        // ... читаем 2 байта из base + offset ...
        0 // заглушка
    }
    pub fn write(&self, _value: u16) {
        // ... пишем 2 байта ...
    }
}

impl Register<Width32> {
    pub fn read(&self) -> u32 {
        // ... читаем 4 байта из base + offset ...
        0 // заглушка
    }
    pub fn write(&self, _value: u32) {
        // ... пишем 4 байта ...
    }
}

/// Определения регистров конфигурационного пространства PCIe.
pub struct PcieConfig {
    base: usize,
}

impl PcieConfig {
    pub fn vendor_id(&self) -> Register<Width16> {
        Register { base: self.base, offset: 0x00, _width: PhantomData }
    }

    pub fn device_id(&self) -> Register<Width16> {
        Register { base: self.base, offset: 0x02, _width: PhantomData }
    }

    pub fn command(&self) -> Register<Width16> {
        Register { base: self.base, offset: 0x04, _width: PhantomData }
    }

    pub fn status(&self) -> Register<Width16> {
        Register { base: self.base, offset: 0x06, _width: PhantomData }
    }

    pub fn bar0(&self) -> Register<Width32> {
        Register { base: self.base, offset: 0x10, _width: PhantomData }
    }
}

fn pcie_example() {
    let cfg = PcieConfig { base: 0xFE00_0000 };

    let vid: u16 = cfg.vendor_id().read();    // возвращает u16 ✅
    let bar: u32 = cfg.bar0().read();         // возвращает u32 ✅

    // Нельзя перепутать:
    // let bad: u32 = cfg.vendor_id().read(); // ❌ ОШИБКА: expected u16
    // cfg.bar0().write(0u16);                // ❌ ОШИБКА: expected u32
}

Контроль доступа к буферам DMA

У буферов DMA есть направление: одни предназначены для передачи от устройства к хосту (чтение), другие — для передачи от хоста к устройству (запись). Использование неверного направления портит данные или вызывает ошибки шины:

use std::marker::PhantomData;

// Маркеры направления
pub struct ToDevice;     // хост пишет, устройство читает
pub struct FromDevice;   // устройство пишет, хост читает

/// Буфер DMA с контролем направления.
pub struct DmaBuffer<Dir> {
    ptr: *mut u8,
    len: usize,
    dma_addr: u64,  // физический адрес для устройства
    _dir: PhantomData<Dir>,
}

impl DmaBuffer<ToDevice> {
    /// Заполняет буфер данными для отправки устройству.
    pub fn write_data(&mut self, data: &[u8]) {
        assert!(data.len() <= self.len);
        // SAFETY: ptr действителен для self.len байт (выделен при создании),
        // а data.len() <= self.len (проверено assert выше).
        unsafe { std::ptr::copy_nonoverlapping(data.as_ptr(), self.ptr, data.len()) }
    }

    /// Получает адрес DMA, из которого устройство будет читать.
    pub fn device_addr(&self) -> u64 {
        self.dma_addr
    }
}

impl DmaBuffer<FromDevice> {
    /// Читает данные, которые устройство записало в буфер.
    pub fn read_data(&self) -> &[u8] {
        // SAFETY: ptr действителен для self.len байт, а устройство уже закончило
        // запись (вызывающий гарантирует, что DMA-передача завершена).
        unsafe { std::slice::from_raw_parts(self.ptr, self.len) }
    }

    /// Получает адрес DMA, в который устройство будет писать.
    pub fn device_addr(&self) -> u64 {
        self.dma_addr
    }
}

// Нельзя писать в буфер FromDevice:
// fn oops(buf: &mut DmaBuffer<FromDevice>) {
//     buf.write_data(&[1, 2, 3]);  // ❌ нет метода `write_data` у DmaBuffer<FromDevice>
// }

// Нельзя читать из буфера ToDevice:
// fn oops2(buf: &DmaBuffer<ToDevice>) {
//     let data = buf.read_data();  // ❌ нет метода `read_data` у DmaBuffer<ToDevice>
// }

Владение файловым дескриптором

Распространённая ошибка: использование файлового дескриптора после его закрытия. Phantom-типы могут отслеживать состояние «открыт/закрыт»:

use std::marker::PhantomData;

pub struct Open;
pub struct Closed;

/// Файловый дескриптор с отслеживанием состояния.
pub struct Fd<State> {
    raw: i32,
    _state: PhantomData<State>,
}

impl Fd<Open> {
    pub fn open(path: &str) -> Result<Self, String> {
        // ... открываем файл ...
        Ok(Fd { raw: 3, _state: PhantomData }) // заглушка
    }

    pub fn read(&self, buf: &mut [u8]) -> Result<usize, String> {
        // ... читаем из fd ...
        Ok(0) // заглушка
    }

    pub fn write(&self, data: &[u8]) -> Result<usize, String> {
        // ... пишем в fd ...
        Ok(data.len()) // заглушка
    }

    /// Закрывает fd — возвращает дескриптор в состоянии Closed.
    /// Дескриптор Open потребляется, что исключает использование после закрытия.
    pub fn close(self) -> Fd<Closed> {
        // ... закрываем fd ...
        Fd { raw: self.raw, _state: PhantomData }
    }
}

impl Fd<Closed> {
    // Нет методов read() и write(): у Fd<Closed> их просто нет.
    // Поэтому использование после закрытия — ошибка компиляции.

    pub fn raw_fd(&self) -> i32 {
        self.raw
    }
}

fn fd_example() -> Result<(), String> {
    let fd = Fd::open("/dev/ipmi0")?;
    let mut buf = [0u8; 256];
    fd.read(&mut buf)?;

    let closed = fd.close();

    // closed.read(&mut buf)?;  // ❌ нет метода `read` у Fd<Closed>
    // closed.write(&[1])?;     // ❌ нет метода `write` у Fd<Closed>

    Ok(())
}

Комбинирование phantom-типов с предыдущими паттернами

Phantom-типы сочетаются со всем, что мы уже видели:

use std::marker::PhantomData;
pub struct Width32;
pub struct Width16;
pub struct Register<W> { _w: PhantomData<W> }
impl Register<Width16> { pub fn read(&self) -> u16 { 0 } }
impl Register<Width32> { pub fn read(&self) -> u32 { 0 } }
#[derive(Debug, Clone, Copy, PartialEq, PartialOrd)]
pub struct Celsius(pub f64);

/// Комбинирует phantom-типы (ширина регистра) с размерными типами (Celsius).
fn read_temp_sensor(reg: &Register<Width16>) -> Celsius {
    let raw = reg.read();  // тип u16 гарантирован phantom-типом
    Celsius(raw as f64 * 0.0625)  // тип Celsius гарантирован возвращаемым типом
}

// Компилятор обеспечивает:
// 1. Регистр 16-битный (phantom-тип)
// 2. Результат — Celsius (newtype)
// Оба — без накладных расходов во время выполнения.

Когда использовать phantom-типы

СценарийИспользовать phantom-параметр?
Кодирование ширины регистра✅ Всегда: предотвращает несоответствие ширины
Направление буфера DMA✅ Всегда: предотвращает порчу данных
Состояние файлового дескриптора✅ Всегда: предотвращает использование после закрытия
Права доступа к области памяти (R/W/X)✅ Всегда: обеспечивает контроль доступа
Обобщённые контейнеры (Vec, HashMap)❌ Нет: используйте конкретные параметры типа
Атрибуты, меняющиеся во время выполнения❌ Нет: phantom-типы существуют только на этапе компиляции

Матрица ресурсов с phantom-типами

flowchart TD
    subgraph "Маркеры ширины"
        W8["Width8"]
        W16["Width16"]
        W32["Width32"]
    end
    subgraph "Маркеры направления"
        RD["Read"]
        WR["Write"]
    end
    subgraph "Типизированные ресурсы"
        R1["Register<Width16>"]
        R2["DmaBuffer<Read>"]
        R3["DmaBuffer<Write>"]
    end
    W16 --> R1
    RD --> R2
    WR --> R3
    R2 -.->|"попытка записи"| ERR["❌ Ошибка компиляции"]
    style W8 fill:#e1f5fe,color:#000
    style W16 fill:#e1f5fe,color:#000
    style W32 fill:#e1f5fe,color:#000
    style RD fill:#c8e6c9,color:#000
    style WR fill:#fff3e0,color:#000
    style R1 fill:#e8eaf6,color:#000
    style R2 fill:#c8e6c9,color:#000
    style R3 fill:#fff3e0,color:#000
    style ERR fill:#ffcdd2,color:#000

Упражнение: права доступа к области памяти

Спроектируйте phantom-типы для областей памяти с правами чтения, записи и выполнения:

  • MemRegion<ReadOnly> имеет fn read(&self, offset: usize) -> u8
  • MemRegion<ReadWrite> имеет и read, и write
  • MemRegion<Executable> имеет read и fn execute(&self)
  • Запись в ReadOnly или выполнение ReadWrite не должны компилироваться.
Решение
use std::marker::PhantomData;

pub struct ReadOnly;
pub struct ReadWrite;
pub struct Executable;

pub struct MemRegion<Perm> {
    base: *mut u8,
    len: usize,
    _perm: PhantomData<Perm>,
}

// Чтение доступно для всех типов прав
impl<P> MemRegion<P> {
    pub fn read(&self, offset: usize) -> u8 {
        assert!(offset < self.len);
        // SAFETY: offset < self.len (проверено assert выше), base действителен для len байт.
        unsafe { *self.base.add(offset) }
    }
}

impl MemRegion<ReadWrite> {
    pub fn write(&mut self, offset: usize, val: u8) {
        assert!(offset < self.len);
        // SAFETY: offset < self.len (проверено assert выше), base действителен для len байт,
        // а &mut self гарантирует эксклюзивный доступ.
        unsafe { *self.base.add(offset) = val; }
    }
}

impl MemRegion<Executable> {
    pub fn execute(&self) {
        // Переход по базовому адресу (концептуально)
    }
}

// ❌ region_ro.write(0, 0xFF);  // Ошибка компиляции: нет метода `write`
// ❌ region_rw.execute();       // Ошибка компиляции: нет метода `execute`

Ключевые выводы

  1. PhantomData несёт информацию о типе при нулевом размере: маркер существует только для компилятора.
  2. Несоответствие ширины регистра становится ошибкой компиляции: Register<Width16> возвращает u16, а не u32.
  3. Направление DMA обеспечивается структурно: у DmaBuffer<Read> нет метода write().
  4. Сочетайте с размерными типами (гл. 06): Register<Width16> может возвращать Celsius через шаг разбора.
  5. Phantom-типы работают только на этапе компиляции: они не подходят для атрибутов, которые меняются во время выполнения; для них используйте enum.

Const fn: доказательства корректности на этапе компиляции 🟠

Что вы узнаете: как const fn и assert! превращают компилятор в движок доказательств: проверка карт памяти SRAM, раскладки регистров, кадров протокола, масок битовых полей, деревьев тактирования и таблиц поиска на этапе компиляции, без затрат во время выполнения.

Перекрёстные ссылки: гл. 04 (capability-токены), гл. 06 (анализ размерностей), гл. 09 (phantom-типы)

Проблема: карты памяти, которые лгут

Во встраиваемом и системном программировании карты памяти лежат в основе всего: они определяют, где находятся загрузчик, прошивка, секции данных и стеки. Ошибся с границей, и две подсистемы тихо портят друг друга. В C такие карты обычно задаются #define-константами без структурной связи:

/* Раскладка SRAM STM32F4: 256 KB по адресу 0x20000000 */
#define SRAM_BASE       0x20000000
#define SRAM_SIZE       (256 * 1024)

#define BOOT_BASE       0x20000000
#define BOOT_SIZE       (16 * 1024)

#define FW_BASE         0x20004000
#define FW_SIZE         (128 * 1024)

#define DATA_BASE       0x20024000
#define DATA_SIZE       (80 * 1024)     /* Кто-то поднял это значение с 64K до 80K */

#define STACK_BASE      0x20038000
#define STACK_SIZE      (48 * 1024)     /* 0x20038000 + 48K = 0x20044000 — за концом SRAM! */

Ошибка: 16 + 128 + 80 + 48 = 272 KB, а SRAM — всего 256 KB. Стек выходит за конец физической памяти на 16 KB. Ни предупреждения компилятора, ни ошибки линкера, ни проверки во время выполнения: только тихое повреждение, когда стек растёт в незанятое адресное пространство.

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

Const fn: превращаем компилятор в движок доказательств

Функции const fn в Rust могут выполняться на этапе компиляции. Если const fn паникует во время такого вычисления, panic становится ошибкой компиляции. В сочетании с assert! это превращает компилятор в доказатель теорем для ваших инвариантов:

pub const fn checked_add(a: u32, b: u32) -> u32 {
    let sum = a as u64 + b as u64;
    assert!(sum <= u32::MAX as u64, "переполнение");
    sum as u32
}

// ✅ Компилируется: 100 + 200 помещается в u32
const X: u32 = checked_add(100, 200);

// ❌ Ошибка компиляции: "переполнение"
// const Y: u32 = checked_add(u32::MAX, 1);

fn main() {
    println!("{X}");
}

Ключевая идея: const fn + assert! = обязательство доказательства. Каждое утверждение — теорема, которую компилятор должен проверить. Если доказательство не сходится, программа не компилируется. Не нужен набор тестов, и код-ревью не нужно: сам компилятор выступает аудитором.

Построение проверенной карты памяти SRAM

Тип Region

Region представляет непрерывный блок памяти. Его конструктор — const fn, который обеспечивает базовую корректность:

#[derive(Debug, Clone, Copy)]
pub struct Region {
    pub base: u32,
    pub size: u32,
}

impl Region {
    /// Создаёт регион. Паникует на этапе компиляции, если инварианты нарушены.
    pub const fn new(base: u32, size: u32) -> Self {
        assert!(size > 0, "размер региона должен быть ненулевым");
        assert!(
            base as u64 + size as u64 <= u32::MAX as u64,
            "регион выходит за пределы 32-битного адресного пространства"
        );
        Self { base, size }
    }

    pub const fn end(&self) -> u32 {
        self.base + self.size
    }

    /// Истина, если `inner` целиком помещается внутри `self`.
    pub const fn contains(&self, inner: &Region) -> bool {
        inner.base >= self.base && inner.end() <= self.end()
    }

    /// Истина, если два региона имеют общие адреса.
    pub const fn overlaps(&self, other: &Region) -> bool {
        self.base < other.end() && other.base < self.end()
    }

    /// Истина, если `addr` попадает в этот регион.
    pub const fn contains_addr(&self, addr: u32) -> bool {
        addr >= self.base && addr < self.end()
    }
}

// Регион рождается корректным: некорректный создать нельзя
const R: Region = Region::new(0x2000_0000, 1024);

fn main() {
    println!("Регион: {:#010X}..{:#010X}", R.base, R.end());
}

Проверенная карта памяти

Теперь объединяем регионы в полную карту SRAM. Конструктор доказывает шесть инвариантов непересечения и четыре инварианта вложенности, всё на этапе компиляции:

#[derive(Debug, Clone, Copy)]
pub struct Region { pub base: u32, pub size: u32 }
impl Region {
    pub const fn new(base: u32, size: u32) -> Self {
        assert!(size > 0, "размер региона должен быть ненулевым");
        assert!(base as u64 + size as u64 <= u32::MAX as u64, "переполнение");
        Self { base, size }
    }
    pub const fn end(&self) -> u32 { self.base + self.size }
    pub const fn contains(&self, inner: &Region) -> bool {
        inner.base >= self.base && inner.end() <= self.end()
    }
    pub const fn overlaps(&self, other: &Region) -> bool {
        self.base < other.end() && other.base < self.end()
    }
}
pub struct SramMap {
    pub total:      Region,
    pub bootloader: Region,
    pub firmware:   Region,
    pub data:       Region,
    pub stack:      Region,
}

impl SramMap {
    pub const fn verified(
        total: Region,
        bootloader: Region,
        firmware: Region,
        data: Region,
        stack: Region,
    ) -> Self {
        // ── Вложенность: каждый подрегион помещается в общий объём SRAM ──
        assert!(total.contains(&bootloader), "загрузчик выходит за пределы SRAM");
        assert!(total.contains(&firmware),   "прошивка выходит за пределы SRAM");
        assert!(total.contains(&data),       "секция данных выходит за пределы SRAM");
        assert!(total.contains(&stack),      "стек выходит за пределы SRAM");

        // ── Непересечение: никакие два подрегиона не делят адрес ──
        assert!(!bootloader.overlaps(&firmware), "загрузчик/прошивка пересекаются");
        assert!(!bootloader.overlaps(&data),     "загрузчик/данные пересекаются");
        assert!(!bootloader.overlaps(&stack),    "загрузчик/стек пересекаются");
        assert!(!firmware.overlaps(&data),       "прошивка/данные пересекаются");
        assert!(!firmware.overlaps(&stack),      "прошивка/стек пересекаются");
        assert!(!data.overlaps(&stack),          "данные/стек пересекаются");

        Self { total, bootloader, firmware, data, stack }
    }
}

// ✅ Все 10 инвариантов проверены на этапе компиляции — ноль затрат во время выполнения
const SRAM: SramMap = SramMap::verified(
    Region::new(0x2000_0000, 256 * 1024),   // 256 KB всего SRAM
    Region::new(0x2000_0000,  16 * 1024),   // загрузчик: 16 KB
    Region::new(0x2000_4000, 128 * 1024),   // прошивка: 128 KB
    Region::new(0x2002_4000,  64 * 1024),   // данные: 64 KB
    Region::new(0x2003_4000,  48 * 1024),   // стек: 48 KB
);

fn main() {
    println!("SRAM:       {:#010X} — {} KB", SRAM.total.base, SRAM.total.size / 1024);
    println!("Загрузчик:  {:#010X} — {} KB", SRAM.bootloader.base, SRAM.bootloader.size / 1024);
    println!("Прошивка:   {:#010X} — {} KB", SRAM.firmware.base, SRAM.firmware.size / 1024);
    println!("Данные:     {:#010X} — {} KB", SRAM.data.base, SRAM.data.size / 1024);
    println!("Стек:       {:#010X} — {} KB", SRAM.stack.base, SRAM.stack.size / 1024);
}

Десять проверок на этапе компиляции, ноль инструкций во время выполнения. Бинарный файл содержит только проверенные константы.

Нарушаем карту

Допустим, кто-то увеличил секцию данных с 64 KB до 80 KB, ничего больше не меняя:

// ❌ Не компилируется
const BAD_SRAM: SramMap = SramMap::verified(
    Region::new(0x2000_0000, 256 * 1024),
    Region::new(0x2000_0000,  16 * 1024),
    Region::new(0x2000_4000, 128 * 1024),
    Region::new(0x2002_4000,  80 * 1024),   // 80 KB: на 16 KB больше, чем нужно
    Region::new(0x2003_8000,  48 * 1024),   // стек сдвинут за конец SRAM
);

Компилятор выдаёт:

error[E0080]: evaluation of constant value failed
  --> src/main.rs:38:9
   |
38 |         assert!(total.contains(&stack), "стек выходит за пределы SRAM");
   |         ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
   |         the evaluated program panicked at 'стек выходит за пределы SRAM'

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

Контроль доступа с phantom-типами

Объедините проверку const fn с phantom-типами прав доступа (гл. 09), чтобы обеспечивать ограничения чтения и записи на уровне типов:

use std::marker::PhantomData;

pub struct ReadOnly;
pub struct ReadWrite;

pub struct TypedRegion<Access> {
    base: u32,
    size: u32,
    _access: PhantomData<Access>,
}

impl<A> TypedRegion<A> {
    pub const fn new(base: u32, size: u32) -> Self {
        assert!(size > 0, "размер региона должен быть ненулевым");
        Self { base, size, _access: PhantomData }
    }
}

// Чтение доступно для любого уровня доступа
fn read_word<A>(region: &TypedRegion<A>, offset: u32) -> u32 {
    assert!(offset + 4 <= region.size, "чтение за пределами");
    // В реальной прошивке: unsafe { core::ptr::read_volatile((region.base + offset) as *const u32) }
    0 // заглушка
}

// Запись требует ReadWrite: сигнатура функции это обеспечивает
fn write_word(region: &TypedRegion<ReadWrite>, offset: u32, value: u32) {
    assert!(offset + 4 <= region.size, "запись за пределами");
    // В реальной прошивке: unsafe { core::ptr::write_volatile(...) }
    let _ = value; // заглушка
}

const BOOTLOADER: TypedRegion<ReadOnly>  = TypedRegion::new(0x2000_0000, 16 * 1024);
const DATA:       TypedRegion<ReadWrite> = TypedRegion::new(0x2002_4000, 64 * 1024);

fn main() {
    read_word(&BOOTLOADER, 0);      // ✅ чтение из региона только для чтения
    read_word(&DATA, 0);            // ✅ чтение из региона чтения-записи
    write_word(&DATA, 0, 42);       // ✅ запись в регион чтения-записи
    // write_word(&BOOTLOADER, 0, 42); // ❌ Ошибка компиляции: ожидается ReadWrite, найден ReadOnly
}

Регион загрузчика физически доступен для записи (это SRAM), но система типов предотвращает случайную запись. Именно это различие между аппаратной возможностью и программным разрешением и означает корректность по построению.

Происхождение указателя: доказываем, что адреса принадлежат регионам

Двигаясь дальше, можно создавать проверенные адреса: значения, которые статически доказаны как лежащие внутри конкретного региона:

#[derive(Debug, Clone, Copy)]
pub struct Region { pub base: u32, pub size: u32 }
impl Region {
    pub const fn new(base: u32, size: u32) -> Self {
        assert!(size > 0);
        assert!(base as u64 + size as u64 <= u32::MAX as u64);
        Self { base, size }
    }
    pub const fn end(&self) -> u32 { self.base + self.size }
    pub const fn contains_addr(&self, addr: u32) -> bool {
        addr >= self.base && addr < self.end()
    }
}
/// Адрес, доказанно лежащий внутри Region на этапе компиляции.
pub struct VerifiedAddr {
    addr: u32, // приватное: создаётся только через проверяющий конструктор
}

impl VerifiedAddr {
    /// Паникует на этапе компиляции, если `addr` вне `region`.
    pub const fn new(region: &Region, addr: u32) -> Self {
        assert!(region.contains_addr(addr), "адрес вне региона");
        Self { addr }
    }

    pub const fn raw(&self) -> u32 {
        self.addr
    }
}

const DATA: Region = Region::new(0x2002_4000, 64 * 1024);

// ✅ Доказано на этапе компиляции, что адрес внутри региона данных
const STATUS_WORD: VerifiedAddr = VerifiedAddr::new(&DATA, 0x2002_4000);
const CONFIG_WORD: VerifiedAddr = VerifiedAddr::new(&DATA, 0x2002_5000);

// ❌ Не скомпилируется: адрес лежит в регионе загрузчика, а не данных
// const BAD_ADDR: VerifiedAddr = VerifiedAddr::new(&DATA, 0x2000_0000);

fn main() {
    println!("Регистр статуса по адресу {:#010X}", STATUS_WORD.raw());
    println!("Регистр конфигурации по адресу {:#010X}", CONFIG_WORD.raw());
}

Происхождение доказано на этапе компиляции: при доступе к этим адресам не нужна проверка границ во время выполнения. Конструктор приватный, поэтому VerifiedAddr может существовать только тогда, когда компилятор доказал его корректность.

За пределами карт памяти

Паттерн доказательства с const fn применим везде, где есть известные на этапе компиляции значения со структурными инвариантами. Карта SRAM выше доказала свойства между регионами (вложенность, непересечение). Та же техника масштабируется на всё более мелкие области:

flowchart TD
    subgraph coarse["Крупнозернистые"]
        MEM["Карты памяти<br/>регионы не перекрываются"]
        REG["Карты регистров<br/>смещения выровнены и не пересекаются"]
    end

    subgraph fine["Мелкозернистые"]
        BIT["Раскладка битовых полей<br/>маски не пересекаются внутри регистра"]
        FRAME["Кадры протокола<br/>поля идут подряд, суммарно ≤ максимума"]
    end

    subgraph derived["Цепочки производных значений"]
        PLL["Деревья тактирования / PLL<br/>каждая промежуточная частота в диапазоне"]
        LUT["Таблицы поиска<br/>вычисляются и проверяются на этапе компиляции"]
    end

    MEM --> REG --> BIT
    MEM --> FRAME
    REG --> PLL
    PLL --> LUT

    style MEM fill:#c8e6c9,color:#000
    style REG fill:#c8e6c9,color:#000
    style BIT fill:#e1f5fe,color:#000
    style FRAME fill:#e1f5fe,color:#000
    style PLL fill:#fff3e0,color:#000
    style LUT fill:#fff3e0,color:#000

Каждый подраздел ниже следует одному шаблону: определяется тип с конструктором const fn, который кодирует инварианты, а затем используется const _: () = { ... } или const-привязка для запуска проверки.

Карты регистров

Блоки аппаратных регистров имеют фиксированные смещения и ширины. Смещённое или перекрывающееся определение регистра всегда является ошибкой:

#[derive(Debug, Clone, Copy)]
pub struct Register {
    pub offset: u32,
    pub width: u32,
}

impl Register {
    pub const fn new(offset: u32, width: u32) -> Self {
        assert!(
            width == 1 || width == 2 || width == 4,
            "ширина регистра должна быть 1, 2 или 4 байта"
        );
        assert!(offset % width == 0, "регистр должен быть естественно выровнен");
        Self { offset, width }
    }

    pub const fn end(&self) -> u32 {
        self.offset + self.width
    }
}

const fn disjoint(a: &Register, b: &Register) -> bool {
    a.end() <= b.offset || b.end() <= a.offset
}

// Регистры периферии UART
const DATA:   Register = Register::new(0x00, 4);
const STATUS: Register = Register::new(0x04, 4);
const CTRL:   Register = Register::new(0x08, 4);
const BAUD:   Register = Register::new(0x0C, 4);

// Доказательство на этапе компиляции: ни один регистр не перекрывает другой
const _: () = {
    assert!(disjoint(&DATA,   &STATUS));
    assert!(disjoint(&DATA,   &CTRL));
    assert!(disjoint(&DATA,   &BAUD));
    assert!(disjoint(&STATUS, &CTRL));
    assert!(disjoint(&STATUS, &BAUD));
    assert!(disjoint(&CTRL,   &BAUD));
};

fn main() {
    println!("UART DATA:   offset={:#04X}, width={}", DATA.offset, DATA.width);
    println!("UART STATUS: offset={:#04X}, width={}", STATUS.offset, STATUS.width);
}

Обратите внимание на идиому const _: () = { ... };: безымянная константа, единственная цель которой — выполнить утверждения на этапе компиляции. Если хотя бы одно утверждение не выполняется, константа не может быть вычислена, и компиляция останавливается.

Мини-упражнение: банк регистров SPI

Дан набор регистров SPI-контроллера. Добавьте const fn-утверждения, которые доказывают:

  1. Каждый регистр естественно выровнен (offset % width == 0)
  2. Никакие два регистра не перекрываются
  3. Все регистры помещаются в блок регистров размером 64 байта
Подсказка

Используйте повторно Register и disjoint из примера с UART выше. Определите три-четыре const Register (например, CTRL со смещением 0x00 и шириной 4, STATUS со смещением 0x04 и шириной 4, TX_DATA со смещением 0x08 и шириной 1, RX_DATA со смещением 0x0C и шириной 1) и проверьте три свойства.

Раскладка кадров протокола

Кадры сетевых или шинных протоколов содержат поля по определённым смещениям. Метод then() делает непрерывность структурной: промежутки и перекрытия невозможны по построению:

#[derive(Debug, Clone, Copy)]
pub struct Field {
    pub offset: usize,
    pub size: usize,
}

impl Field {
    pub const fn new(offset: usize, size: usize) -> Self {
        assert!(size > 0, "размер поля должен быть ненулевым");
        Self { offset, size }
    }

    pub const fn end(&self) -> usize {
        self.offset + self.size
    }

    /// Создаёт следующее поле сразу после этого.
    pub const fn then(&self, size: usize) -> Field {
        Field::new(self.end(), size)
    }
}

const MAX_FRAME: usize = 256;

const HEADER:  Field = Field::new(0, 4);
const SEQ_NUM: Field = HEADER.then(2);
const PAYLOAD: Field = SEQ_NUM.then(246);
const CRC:     Field = PAYLOAD.then(4);

// Доказательство на этапе компиляции: кадр помещается в максимальный размер
const _: () = assert!(CRC.end() <= MAX_FRAME, "кадр превышает максимальный размер");

fn main() {
    println!("Заголовок:       [{}..{})", HEADER.offset, HEADER.end());
    println!("Номер:           [{}..{})", SEQ_NUM.offset, SEQ_NUM.end());
    println!("Полезные данные: [{}..{})", PAYLOAD.offset, PAYLOAD.end());
    println!("CRC:             [{}..{})", CRC.offset, CRC.end());
    println!("Итого:           {}/{} байт", CRC.end(), MAX_FRAME);
}

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

Встроенные const-блоки для проверки обобщений

Начиная с Rust 1.79, блоки const { ... } позволяют проверять параметры const-обобщений в месте использования. Это идеально для ограничений размера DMA-буферов и требований к выравниванию:

fn dma_transfer<const N: usize>(buf: &[u8; N]) {
    const { assert!(N % 4 == 0, "размер DMA-буфера должен быть кратен 4 байтам") };
    const { assert!(N <= 65536, "DMA-передача превышает максимальный размер") };
    // ... запускаем передачу ...
}

dma_transfer(&[0u8; 1024]);   // ✅ 1024 делится на 4 и ≤ 65536
// dma_transfer(&[0u8; 1023]); // ❌ Ошибка компиляции: не выровнено по 4 байтам

Утверждения вычисляются при мономорфизации функции: каждый вызов с другим N получает собственную проверку на этапе компиляции.

Раскладка битовых полей внутри регистра

Карты регистров доказывают, что регистры не перекрываются друг с другом. Но что насчёт битов внутри одного регистра? Управляющие регистры упаковывают несколько полей в одно слово. Если два поля занимают один и тот же бит, операции чтения и записи тихо портят друг друга. В C это обычно ловят (или не ловят) вручную, проверяя константы масок.

const fn может доказать, что пара маска/сдвиг каждого поля не пересекается с парами всех остальных полей того же регистра:

#[derive(Debug, Clone, Copy)]
pub struct BitField {
    pub mask: u32,
    pub shift: u8,
}

impl BitField {
    pub const fn new(shift: u8, width: u8) -> Self {
        assert!(width > 0, "ширина битового поля должна быть ненулевой");
        assert!(shift as u32 + width as u32 <= 32, "битовое поле выходит за пределы 32-битного регистра");
        // Строим маску: `width` единиц, начиная с бита `shift`
        let mask = ((1u64 << width as u64) - 1) as u32;
        Self { mask: mask << shift as u32, shift }
    }

    pub const fn positioned_mask(&self) -> u32 {
        self.mask
    }

    pub const fn encode(&self, value: u32) -> u32 {
        assert!(value & !( self.mask >> self.shift as u32 ) == 0, "значение превышает ширину поля");
        value << self.shift as u32
    }
}

const fn fields_disjoint(a: &BitField, b: &BitField) -> bool {
    a.positioned_mask() & b.positioned_mask() == 0
}

// Поля управляющего регистра SPI: enable[0], mode[1:2], clock_div[4:7], irq_en[8]
const SPI_EN:     BitField = BitField::new(0, 1);   // бит 0
const SPI_MODE:   BitField = BitField::new(1, 2);   // биты 1–2
const SPI_CLKDIV: BitField = BitField::new(4, 4);   // биты 4–7
const SPI_IRQ:    BitField = BitField::new(8, 1);   // бит 8

// Доказательство на этапе компиляции: ни одно поле не занимает общий бит
const _: () = {
    assert!(fields_disjoint(&SPI_EN,   &SPI_MODE));
    assert!(fields_disjoint(&SPI_EN,   &SPI_CLKDIV));
    assert!(fields_disjoint(&SPI_EN,   &SPI_IRQ));
    assert!(fields_disjoint(&SPI_MODE, &SPI_CLKDIV));
    assert!(fields_disjoint(&SPI_MODE, &SPI_IRQ));
    assert!(fields_disjoint(&SPI_CLKDIV, &SPI_IRQ));
};

fn main() {
    let ctrl = SPI_EN.encode(1)
             | SPI_MODE.encode(0b10)
             | SPI_CLKDIV.encode(0b0110)
             | SPI_IRQ.encode(1);
    println!("SPI_CTRL = {:#010b} ({:#06X})", ctrl, ctrl);
}

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

Конфигурация дерева тактирования / PLL

Микроконтроллеры получают тактовые частоты периферии через цепочки умножителей и делителей. PLL выдаёт f_vco = f_in × N / M, и частота VCO должна оставаться в диапазоне, заданном аппаратурой. Ошибка в любом параметре для конкретной платы, и чип выдаёт некорректные тактовые сигналы или не может захватить частоту. Эти ограничения идеально подходят для const fn:

#[derive(Debug, Clone, Copy)]
pub struct PllConfig {
    pub input_khz: u32,     // внешний генератор
    pub m: u32,             // входной делитель
    pub n: u32,             // множитель VCO
    pub p: u32,             // делитель системной частоты
}

impl PllConfig {
    pub const fn verified(input_khz: u32, m: u32, n: u32, p: u32) -> Self {
        // Входной делитель даёт частоту на входе PLL
        let pll_input = input_khz / m;
        assert!(pll_input >= 1_000 && pll_input <= 2_000,
            "вход PLL должен быть 1–2 МГц");

        // Частота VCO должна быть в пределах аппаратных ограничений
        let vco = pll_input as u64 * n as u64;
        assert!(vco >= 192_000 && vco <= 432_000,
            "VCO должен быть в диапазоне 192–432 МГц");

        // Делитель системной частоты должен быть чётным (аппаратное ограничение)
        assert!(p == 2 || p == 4 || p == 6 || p == 8,
            "P должен быть 2, 4, 6 или 8");

        // Итоговая системная частота
        let sysclk = vco / p as u64;
        assert!(sysclk <= 168_000,
            "системная частота превышает максимум 168 МГц");

        Self { input_khz, m, n, p }
    }

    pub const fn vco_khz(&self) -> u32 {
        (self.input_khz / self.m) * self.n
    }

    pub const fn sysclk_khz(&self) -> u32 {
        self.vco_khz() / self.p
    }
}

// STM32F4 с кварцем HSE 8 МГц → системная частота 168 МГц
const PLL: PllConfig = PllConfig::verified(8_000, 8, 336, 2);

// ❌ Не скомпилируется: VCO = 480 МГц превышает предел 432 МГц
// const BAD: PllConfig = PllConfig::verified(8_000, 8, 480, 2);

fn main() {
    println!("VCO:    {} МГц", PLL.vco_khz() / 1_000);
    println!("SYSCLK: {} МГц", PLL.sysclk_khz() / 1_000);
}

Раскомментирование константы BAD даёт ошибку на этапе компиляции, которая точно указывает на нарушенное ограничение:

error[E0080]: evaluation of constant value failed
  --> src/main.rs:18:9
   |
18 |         assert!(vco >= 192_000 && vco <= 432_000,
   |         ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
   |         the evaluated program panicked at 'VCO должен быть в диапазоне 192–432 МГц'

Компилятор ловит нарушение ограничения в середине цепочки вычислений, а не в конце. Если бы вместо этого было нарушено ограничение системной частоты (sysclk > 168 МГц), сообщение об ошибке указывало бы на другое утверждение.

Цепочки ограничений производных значений превращают один const fn в многоступенчатое доказательство. У каждого промежуточного значения свой диапазон, заданный аппаратурой. Изменение одного параметра (например, переход на кварц 25 МГц) сразу выявляет любое нарушение ниже по цепочке.

Цепочки ограничений производных значений: частота VCO зависит от input / m × n, а системная частота зависит от vco / p. У каждого промежуточного значения свой диапазон, заданный аппаратурой. Один const fn проверяет всю цепочку, поэтому изменение одного параметра (например, переход на кварц 25 МГц) сразу выявляет любое нарушение ниже по цепочке.

Таблицы поиска на этапе компиляции

const fn может вычислить целые таблицы поиска на этапе компиляции и поместить их в .rodata без затрат на инициализацию при запуске. Это особенно полезно для таблиц CRC, тригонометрии, таблиц кодирования и кодов коррекции ошибок, то есть везде, где обычно использовали бы build-скрипт или генерацию кода:

const fn crc32_table() -> [u32; 256] {
    let mut table = [0u32; 256];
    let mut i: usize = 0;
    while i < 256 {
        let mut crc = i as u32;
        let mut j = 0;
        while j < 8 {
            if crc & 1 != 0 {
                crc = (crc >> 1) ^ 0xEDB8_8320; // стандартный полином CRC-32
            } else {
                crc >>= 1;
            }
            j += 1;
        }
        table[i] = crc;
        i += 1;
    }
    table
}

/// Полная таблица CRC-32: вычисляется на этапе компиляции и размещается в .rodata
const CRC32_TABLE: [u32; 256] = crc32_table();

/// Вычисляет CRC-32 для среза байтов во время выполнения, используя готовую таблицу.
fn crc32(data: &[u8]) -> u32 {
    let mut crc: u32 = !0;
    for &byte in data {
        let index = ((crc ^ byte as u32) & 0xFF) as usize;
        crc = (crc >> 8) ^ CRC32_TABLE[index];
    }
    !crc
}

// Проверка: известный CRC-32 для «123456789»
const _: () = {
    // Проверяем отдельные элементы таблицы на этапе компиляции
    assert!(CRC32_TABLE[0] == 0x0000_0000);
    assert!(CRC32_TABLE[1] == 0x7707_3096);
};

fn main() {
    let check = crc32(b"123456789");
    // Известный CRC-32 для «123456789» — 0xCBF43926
    assert_eq!(check, 0xCBF4_3926);
    println!("CRC-32 для '123456789' = {:#010X} ✓", check);
    println!("Размер таблицы: {} элементов × 4 байта = {} байт в .rodata",
        CRC32_TABLE.len(), CRC32_TABLE.len() * 4);
}

Функция crc32_table() выполняется целиком во время компиляции. Полученная таблица размером 1 KB записывается в секцию read-only данных бинарного файла: ни аллокатора, ни кода инициализации, ни затрат на запуск. Сравните с подходом C, где таблицу либо генерирует отдельный инструмент, либо вычисляют при запуске. Версия на Rust доказуемо корректна (утверждения const _ проверяют известные значения) и доказуемо полна (компилятор отвергнет программу, если функция не сможет построить корректную таблицу).

Когда использовать доказательства на основе const fn

СценарийРекомендация
Карты памяти, смещения регистров, таблицы разделов✅ Всегда
Раскладка кадров протокола с фиксированными полями✅ Всегда
Битовые маски внутри регистра✅ Всегда
Дерево тактирования / параметры PLL✅ Всегда
Таблицы поиска (CRC, тригонометрия, кодирование)✅ Всегда: ноль затрат на запуск
Константы с перекрёстными инвариантами (непересечение, сумма ≤ границы)✅ Всегда
Значения конфигурации с ограничениями предметной области✅ Когда значения известны на этапе компиляции
Значения, вычисленные из пользовательского ввода или файлов❌ Используйте проверку во время выполнения
Сильно динамические структуры (деревья, графы)❌ Используйте property-based тестирование
Проверка диапазона одного значения⚠️ Лучше newtype + From (гл. 07)

Сводка стоимости

ЧтоСтоимость во время выполнения
Утверждения const fn (assert!, panic!)Только время компиляции: 0 инструкций
Блоки проверки const _: () = { ... }Только время компиляции: не попадают в бинарник
Структуры Region, Register, FieldОбычные данные: та же раскладка, что у голых целых
Встроенная проверка обобщений const { }Мономорфизируется на этапе компиляции: 0 затрат
Таблицы поиска (crc32_table())Вычисляются на этапе компиляции: размещаются в .rodata
Маркеры доступа с phantom-типами (TypedRegion<RW>)Нулевой размер: оптимизируются

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

Упражнение: карта разделов флеш-памяти

Спроектируйте проверенную карту разделов для NOR-флеш-памяти 1 MB, начинающейся с адреса 0x0800_0000. Требования:

  1. Четыре раздела: загрузчик (64 KB), приложение (640 KB), конфигурация (64 KB), промежуточный буфер OTA (256 KB)
  2. Каждый раздел должен быть выровнен по 4 KB (гранулярность стирания флеш-памяти): и базовый адрес, и размер должны быть кратны 4096
  3. Ни один раздел не может пересекаться с другим
  4. Все разделы должны помещаться во флеш-памяти
  5. Добавьте const fn total_used(), которая возвращает сумму размеров всех разделов, и утверждение, что она равна 1 MB
Пример решения
#[derive(Debug, Clone, Copy)]
pub struct FlashRegion {
    pub base: u32,
    pub size: u32,
}

impl FlashRegion {
    pub const fn new(base: u32, size: u32) -> Self {
        assert!(size > 0, "размер раздела должен быть ненулевым");
        assert!(base % 4096 == 0, "базовый адрес раздела должен быть выровнен по 4 KB");
        assert!(size % 4096 == 0, "размер раздела должен быть выровнен по 4 KB");
        assert!(
            base as u64 + size as u64 <= u32::MAX as u64,
            "раздел выходит за пределы адресного пространства"
        );
        Self { base, size }
    }

    pub const fn end(&self) -> u32 { self.base + self.size }

    pub const fn contains(&self, inner: &FlashRegion) -> bool {
        inner.base >= self.base && inner.end() <= self.end()
    }

    pub const fn overlaps(&self, other: &FlashRegion) -> bool {
        self.base < other.end() && other.base < self.end()
    }
}

pub struct FlashMap {
    pub total:  FlashRegion,
    pub boot:   FlashRegion,
    pub app:    FlashRegion,
    pub config: FlashRegion,
    pub ota:    FlashRegion,
}

impl FlashMap {
    pub const fn verified(
        total: FlashRegion,
        boot: FlashRegion,
        app: FlashRegion,
        config: FlashRegion,
        ota: FlashRegion,
    ) -> Self {
        assert!(total.contains(&boot),   "загрузчик выходит за пределы флеш-памяти");
        assert!(total.contains(&app),    "приложение выходит за пределы флеш-памяти");
        assert!(total.contains(&config), "конфигурация выходит за пределы флеш-памяти");
        assert!(total.contains(&ota),    "промежуточный буфер OTA выходит за пределы флеш-памяти");

        assert!(!boot.overlaps(&app),    "загрузчик/приложение пересекаются");
        assert!(!boot.overlaps(&config), "загрузчик/конфигурация пересекаются");
        assert!(!boot.overlaps(&ota),    "загрузчик/OTA пересекаются");
        assert!(!app.overlaps(&config),  "приложение/конфигурация пересекаются");
        assert!(!app.overlaps(&ota),     "приложение/OTA пересекаются");
        assert!(!config.overlaps(&ota),  "конфигурация/OTA пересекаются");

        Self { total, boot, app, config, ota }
    }

    pub const fn total_used(&self) -> u32 {
        self.boot.size + self.app.size + self.config.size + self.ota.size
    }
}

const FLASH: FlashMap = FlashMap::verified(
    FlashRegion::new(0x0800_0000, 1024 * 1024),  // всего 1 MB
    FlashRegion::new(0x0800_0000,   64 * 1024),   // загрузчик: 64 KB
    FlashRegion::new(0x0801_0000,  640 * 1024),   // приложение: 640 KB
    FlashRegion::new(0x080B_0000,   64 * 1024),   // конфигурация: 64 KB
    FlashRegion::new(0x080C_0000,  256 * 1024),   // промежуточный буфер OTA: 256 KB
);

// Каждый байт флеш-памяти учтён
const _: () = assert!(
    FLASH.total_used() == 1024 * 1024,
    "разделы должны точно заполнять флеш-память"
);

fn main() {
    println!("Карта флеш-памяти: {} KB используется / {} KB всего",
        FLASH.total_used() / 1024,
        FLASH.total.size / 1024);
}
flowchart LR
    subgraph compile["Этап компиляции — ноль затрат во время выполнения"]
        direction TB
        RGN["Region::new()<br/>✅ размер &gt; 0<br/>✅ нет переполнения"]
        MAP["SramMap::verified()<br/>✅ вложенность<br/>✅ без пересечений"]
        ACC["TypedRegion&lt;RW&gt;<br/>✅ контроль доступа"]
        PROV["VerifiedAddr::new()<br/>✅ происхождение"]
    end

    subgraph runtime["Рантайм"]
        HW["Доступ к аппаратуре<br/>Без проверки границ<br/>Без проверки прав"]
    end

    RGN --> MAP --> ACC --> PROV --> HW

    style RGN fill:#c8e6c9,color:#000
    style MAP fill:#c8e6c9,color:#000
    style ACC fill:#e1f5fe,color:#000
    style PROV fill:#e1f5fe,color:#000
    style HW fill:#fff3e0,color:#000

Ключевые выводы

  1. const fn + assert! = обязательство доказательства на этапе компиляции: если утверждение не выполняется при вычислении константы, программа не компилируется. Не нужен тест и не нужен код-ревью: компилятор доказывает это сам.

  2. Карты памяти — идеальные кандидаты: вложенность подрегионов, непересечение, ограничения общего объёма и выравнивания выражаются утверждениями const fn. Подход с #define в C не даёт ни одной из этих гарантий.

  3. Phantom-типы добавляются сверху: сочетайте const fn (проверку значений) с phantom-маркерами доступа (проверку прав), чтобы получить защиту в глубину без затрат во время выполнения.

  4. Происхождение можно установить на этапе компиляции: VerifiedAddr доказывает на этапе компиляции, что адрес принадлежит конкретному региону, устраняя проверки границ во время выполнения при каждом обращении.

  5. Паттерн обобщается за пределы памяти: карты регистров, маски битовых полей, кадры протокола, деревья тактирования, параметры DMA. Всюду, где есть известные на этапе компиляции значения со структурными инвариантами.

  6. Битовые поля и деревья тактирования — идеальные кандидаты: непересечение битов внутри регистра и цепочки ограничений производных значений (диапазон VCO, пределы делителей) — ровно то, что const fn доказывает без усилий.

  7. const fn заменяет генераторы кода и build-скрипты для таблиц поиска: таблицы CRC, тригонометрия, таблицы кодирования вычисляются на этапе компиляции, размещаются в .rodata и не требуют затрат на запуск или внешних инструментов.

  8. Встроенные блоки const { } проверяют параметры обобщений: начиная с Rust 1.79, можно накладывать ограничения на const-обобщения прямо в месте вызова и ловить неправильное использование до запуска любого кода.

Send и Sync: доказательства корректности конкурентности на этапе компиляции 🟠

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

Перекрёстные ссылки: гл. 04 (capability-токены), гл. 09 (phantom-типы), гл. 15 (доказательства через const fn)

Проблема: конкурентный доступ без страховочной сетки

В системном программировании периферия, общие буферы и глобальное состояние используются из нескольких контекстов: главного цикла, обработчиков прерываний, колбэков DMA и рабочих потоков. В C компилятор никак этого не контролирует:

/* Общий буфер датчика: доступ из главного цикла и из ISR */
volatile uint32_t sensor_buf[64];
volatile uint32_t buf_index = 0;

void SENSOR_IRQHandler(void) {
    sensor_buf[buf_index++] = read_sensor();  /* Гонка: чтение и запись buf_index */
}

void process_sensors(void) {
    for (uint32_t i = 0; i < buf_index; i++) {  /* buf_index меняется посреди цикла */
        process(sensor_buf[i]);                   /* данные перезаписаны во время чтения */
    }
    buf_index = 0;                                /* ISR срабатывает между этими строками */
}

Ключевое слово volatile не даёт компилятору оптимизировать чтения, но ничего не делает с гонками данных. Два контекста могут одновременно читать и писать buf_index, давая рваные значения, потерянные обновления или переполнение буфера. Та же проблема возникает и с pthread_mutex_t: компилятор спокойно позволит забыть взять блокировку:

pthread_mutex_t lock;
int shared_counter;

void increment(void) {
    shared_counter++;  /* Упс: забыли pthread_mutex_lock(&lock) */
}

Каждая ошибка конкурентности обнаруживается во время выполнения, обычно под нагрузкой, в продакшене и с перебоями.

Что доказывают Send и Sync

Rust определяет два маркерных трейта, которые компилятор выводит автоматически:

ТрейтДоказываетНеформальный смысл
SendЗначение типа T можно безопасно переместить в другой поток«Это может пересекать границу потока»
SyncРазделяемую ссылку &T можно безопасно использовать из нескольких потоков«Это можно читать из нескольких потоков»

Это авто-трейты: компилятор выводит их, проверяя каждое поле. Структура является Send, если все её поля Send. Структура является Sync, если все её поля Sync. Если хотя бы одно поле отказывается от трейта, отказывается вся структура. Аннотация не нужна, накладных расходов во время выполнения нет: доказательство структурное.

flowchart TD
    STRUCT["Ваша структура"]
    INSPECT["Компилятор проверяет<br/>каждое поле"]
    ALL_SEND{"Все поля<br/>Send?"}
    ALL_SYNC{"Все поля<br/>Sync?"}
    SEND_YES["Send ✅<br/><i>может пересекать границы потоков</i>"]
    SEND_NO["!Send ❌<br/><i>ограничена одним потоком</i>"]
    SYNC_YES["Sync ✅<br/><i>может разделяться между потоками</i>"]
    SYNC_NO["!Sync ❌<br/><i>нет конкурентных ссылок</i>"]

    STRUCT --> INSPECT
    INSPECT --> ALL_SEND
    INSPECT --> ALL_SYNC
    ALL_SEND -->|Да| SEND_YES
    ALL_SEND -->|"Любое поле !Send<br/>(например, Rc, *const T)"| SEND_NO
    ALL_SYNC -->|Да| SYNC_YES
    ALL_SYNC -->|"Любое поле !Sync<br/>(например, Cell, RefCell)"| SYNC_NO

    style SEND_YES fill:#c8e6c9,color:#000
    style SYNC_YES fill:#c8e6c9,color:#000
    style SEND_NO fill:#ffcdd2,color:#000
    style SYNC_NO fill:#ffcdd2,color:#000

Компилятор — это аудитор. В C аннотации потокобезопасности живут в комментариях и документации к заголовочным файлам: это рекомендации, которые никто не проверяет. В Rust Send и Sync выводятся из структуры самого типа. Добавление одного поля Cell<f32> автоматически делает содержащую структуру !Sync. Никаких действий программиста не требуется, и забыть невозможно.

Два трейта связаны ключевым тождеством:

T является Sync тогда и только тогда, когда &T является Send.

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

Типы, которые отказываются от трейтов

Некоторые типы намеренно являются !Send или !Sync:

ТипSendSyncПочему
u32, String, Vec<T>✅✅Нет внутренней изменяемости и сырых указателей
Cell<T>, RefCell<T>✅❌Внутренняя изменяемость без синхронизации
Rc<T>❌❌Счётчик ссылок не атомарный
*const T, *mut T❌❌У сырых указателей нет гарантий безопасности
Arc<T> (где T: Send + Sync)✅✅Атомарный счётчик ссылок
Mutex<T> (где T: Send)✅✅Блокировка сериализует весь доступ

Каждая ❌ в этой таблице — инвариант времени компиляции. Случайно отправить Rc в другой поток нельзя: компилятор это отвергнет.

Дескрипторы периферии, которые не являются Send

Во встраиваемых системах блок регистров периферии находится по фиксированному адресу памяти и должен использоваться только из одного контекста выполнения. Сырые указатели по своей природе !Send и !Sync, поэтому обёртка над ними автоматически лишает содержащий тип обоих трейтов:

/// Дескриптор периферийного UART, отображённого в память.
/// Сырой указатель делает его автоматически !Send и !Sync.
pub struct Uart {
    regs: *const u32,
}

impl Uart {
    pub fn new(base: usize) -> Self {
        Self { regs: base as *const u32 }
    }

    pub fn write_byte(&self, byte: u8) {
        // В реальной прошивке: unsafe { write_volatile(self.regs.add(DATA_OFFSET), byte as u32) }
        println!("UART TX: {:#04X}", byte);
    }
}

fn main() {
    let uart = Uart::new(0x4000_1000);
    uart.write_byte(b'A');  // ✅ Использование в потоке, который его создал

    // ❌ Не скомпилируется: Uart — !Send
    // std::thread::spawn(move || {
    //     uart.write_byte(b'B');
    // });
}

Закомментированный thread::spawn дал бы:

error[E0277]: `*const u32` cannot be sent between threads safely
   |
   |     std::thread::spawn(move || {
   |     ^^^^^^^^^^^^^^^^^^ within `Uart`, the trait `Send` is not
   |                        implemented for `*const u32`

Нет сырого указателя? Используйте PhantomData. Иногда у типа нет сырого указателя, но его всё равно нужно ограничить одним потоком, например индекс файлового дескриптора или дескриптор, полученный из библиотеки на C:

use std::marker::PhantomData;

/// Непрозрачный дескриптор из библиотеки на C. PhantomData<*const ()> делает его
/// !Send + !Sync, хотя внутренний fd — это просто целое число.
pub struct LibHandle {
    fd: i32,
    _not_send: PhantomData<*const ()>,
}

impl LibHandle {
    pub fn open(path: &str) -> Self {
        let _ = path;
        Self { fd: 42, _not_send: PhantomData }
    }

    pub fn fd(&self) -> i32 { self.fd }
}

fn main() {
    let handle = LibHandle::open("/dev/sensor0");
    println!("fd = {}", handle.fd());

    // ❌ Не скомпилируется: LibHandle — !Send
    // std::thread::spawn(move || { let _ = handle.fd(); });
}

Это аналог на этапе компиляции той строки из документации C: «прочитайте, что этот дескриптор не потокобезопасен». В Rust это проверяет компилятор.

Mutex превращает !Sync в Sync

Cell<T> и RefCell<T> дают внутреннюю изменяемость без какой-либо синхронизации, поэтому они !Sync. Но иногда действительно нужно разделять изменяемое состояние между потоками. Mutex<T> добавляет недостающую синхронизацию, и компилятор это учитывает:

Если T: Send, то Mutex<T>: Send + Sync.

Блокировка сериализует весь доступ, поэтому внутренний !Sync-тип становится безопасным для разделения. Компилятор доказывает это структурно: никакой проверки во время выполнения «не забыл ли программист взять блокировку» не нужно:

use std::sync::{Arc, Mutex};
use std::cell::Cell;

/// Кэш показаний датчика с Cell для внутренней изменяемости.
/// Cell<u32> — !Sync: напрямую разделять между потоками его нельзя.
struct SensorCache {
    last_reading: Cell<u32>,
    reading_count: Cell<u32>,
}

fn main() {
    // Mutex делает SensorCache безопасным для разделения: компилятор это доказывает
    let cache = Arc::new(Mutex::new(SensorCache {
        last_reading: Cell::new(0),
        reading_count: Cell::new(0),
    }));

    let handles: Vec<_> = (0..4).map(|i| {
        let c = Arc::clone(&cache);
        std::thread::spawn(move || {
            let guard = c.lock().unwrap();  // Перед доступом обязательно блокируем
            guard.last_reading.set(i * 10);
            guard.reading_count.set(guard.reading_count.get() + 1);
        })
    }).collect();

    for h in handles { h.join().unwrap(); }

    let guard = cache.lock().unwrap();
    println!("Последнее показание: {}", guard.last_reading.get());
    println!("Всего чтений:        {}", guard.reading_count.get());
}

Сравните с версией на C: pthread_mutex_lock — это вызов во время выполнения, который программист может забыть. Здесь система типов делает невозможным доступ к SensorCache в обход Mutex. Доказательство структурное, а единственная затрата во время выполнения — сама блокировка.

Mutex не просто синхронизирует, а доказывает синхронизацию. Mutex::lock() возвращает MutexGuard, который разыменовывается в &T. Получить ссылку на внутренние данные в обход блокировки невозможно. API делает «забыл взять блокировку» структурно невыразимым.

Ограничения функций как теоремы

std::thread::spawn имеет такую сигнатуру:

pub fn spawn<F, T>(f: F) -> JoinHandle<T>
where
    F: FnOnce() -> T + Send + 'static,
    T: Send + 'static,

Ограничение Send + 'static — не деталь реализации, а теорема:

«Любое замыкание и возвращаемое значение, переданные в spawn, доказано на этапе компиляции безопасными для запуска в другом потоке, без висячих ссылок.»

Тот же паттерн можно применить к собственным API:

use std::sync::mpsc;

/// Выполняет задачу в фоновом потоке и возвращает её результат.
/// Ограничения доказывают: замыкание и его результат потокобезопасны.
fn run_on_background<F, T>(task: F) -> T
where
    F: FnOnce() -> T + Send + 'static,
    T: Send + 'static,
{
    let (tx, rx) = mpsc::channel();
    std::thread::spawn(move || {
        let _ = tx.send(task());
    });
    rx.recv().expect("фоновая задача завершилась паникой")
}

fn main() {
    // ✅ u32 — Send, замыкание не захватывает ничего не-Send
    let result = run_on_background(|| 6 * 7);
    println!("Результат: {result}");

    // ✅ String — Send
    let greeting = run_on_background(|| String::from("привет из фонового потока"));
    println!("{greeting}");

    // ❌ Не скомпилируется: Rc — !Send
    // use std::rc::Rc;
    // let data = Rc::new(42);
    // run_on_background(move || *data);
}

Раскомментирование примера с Rc даёт точную диагностику:

error[E0277]: `Rc<i32>` cannot be sent between threads safely
   --> src/main.rs
    |
    |     run_on_background(move || *data);
    |     ^^^^^^^^^^^^^^^^^^ `Rc<i32>` cannot be sent between threads safely
    |
note: required by a bound in `run_on_background`
    |
    |     F: FnOnce() -> T + Send + 'static,
    |                        ^^^^ required by this bound

Компилятор возвращает нарушение к точному ограничению и сообщает программисту, почему это нарушение. Сравните с pthread_create из C:

int pthread_create(pthread_t *thread, const pthread_attr_t *attr,
                   void *(*start_routine)(void *), void *arg);

void *arg принимает что угодно: потокобезопасное или нет. Компилятор C не может отличить неатомарный счётчик ссылок от обычного целого. Ограничения трейтов в Rust проводят это различие на уровне типов.

Когда использовать доказательства Send/Sync

СценарийПодход
Дескриптор периферии с сырым указателемАвтоматически !Send + !Sync: ничего делать не нужно
Дескриптор из библиотеки на C (целочисленный fd/handle)Добавить PhantomData<*const ()> для !Send + !Sync
Общая конфигурация за блокировкойArc<Mutex<T>>: компилятор доказывает безопасность доступа
Передача сообщений между потокамиmpsc::channel: ограничение Send применяется автоматически
API планировщика задач или пула потоковТребовать F: Send + 'static в сигнатуре
Однопоточный ресурс (например, контекст GPU)PhantomData<*const ()>, чтобы предотвратить разделение
Тип должен быть Send, но содержит сырой указательunsafe impl Send с задокументированным обоснованием безопасности

Сводка стоимости

ЧтоСтоимость во время выполнения
Автоматический вывод Send / SyncТолько время компиляции: 0 байт
Поле PhantomData<*const ()>Нулевой размер: оптимизируется
Проверка !Send / !SyncТолько время компиляции: без проверок во время выполнения
Ограничения функций F: Send + 'staticМономорфизируются: статическая диспетчеризация, без боксинга
Блокировка Mutex<T>Блокировка во время выполнения (неизбежна для разделяемых изменений)
Подсчёт ссылок Arc<T>Атомарные инкременты и декременты (неизбежны для разделяемого владения)

Первые четыре строки имеют нулевую стоимость: они существуют только в системе типов и исчезают после компиляции. Mutex и Arc несут неизбежные затраты во время выполнения, но эти затраты — минимум, который должна платить любая корректная конкурентная программа. Rust лишь следит за тем, чтобы вы их заплатили.

Упражнение: защита DMA-передачи

Спроектируйте DmaTransfer<T>, который удерживает буфер, пока выполняется DMA-передача. Требования:

  1. DmaTransfer должен быть !Send: контроллер DMA использует физические адреса, привязанные к шине памяти этого ядра
  2. DmaTransfer должен быть !Sync: одновременное чтение во время записи DMA увидит рваные данные
  3. Предоставьте метод wait(), который потребляет защиту и возвращает буфер: владение доказывает, что передача завершена
  4. Тип буфера T должен реализовывать маркерный трейт DmaSafe
Пример решения
use std::marker::PhantomData;

/// Маркерный трейт для типов, которые можно использовать как DMA-буферы.
/// В реальной прошивке: тип должен быть repr(C) без паддинга.
trait DmaSafe {}

impl DmaSafe for [u8; 64] {}
impl DmaSafe for [u8; 256] {}

/// Защита, представляющая DMA-передачу в процессе выполнения.
/// !Send + !Sync: её нельзя передать в другой поток или разделить.
pub struct DmaTransfer<T: DmaSafe> {
    buffer: T,
    channel: u8,
    _no_send_sync: PhantomData<*const ()>,
}

impl<T: DmaSafe> DmaTransfer<T> {
    /// Запускает DMA-передачу. Буфер потребляется: никто другой до него не дотронется.
    pub fn start(buffer: T, channel: u8) -> Self {
        // В реальной прошивке: настроить канал DMA, задать источник/приёмник, запустить передачу
        println!("Канал DMA {} запущен", channel);
        Self {
            buffer,
            channel,
            _no_send_sync: PhantomData,
        }
    }

    /// Ждёт завершения передачи и возвращает буфер.
    /// Потребляет self: после этого защиты больше не существует.
    pub fn wait(self) -> T {
        // В реальной прошивке: опрашивать регистр статуса DMA до завершения
        println!("Канал DMA {} завершён", self.channel);
        self.buffer
    }
}

fn main() {
    let buf = [0u8; 64];

    // Запуск передачи: buf перемещается в защиту
    let transfer = DmaTransfer::start(buf, 2);

    // ❌ buf больше недоступен: владение не даёт использовать его во время DMA
    // println!("{:?}", buf);

    // ❌ Не скомпилируется: DmaTransfer — !Send
    // std::thread::spawn(move || { transfer.wait(); });

    // ✅ Ждём в исходном потоке и получаем буфер обратно
    let buf = transfer.wait();
    println!("Буфер возвращён: {} байт", buf.len());
}
flowchart TB
    subgraph compiler["Этап компиляции — автоматически выводимые доказательства"]
        direction TB
        SEND["Send<br/>✅ безопасно перемещать между потоками"]
        SYNC["Sync<br/>✅ безопасно разделять ссылки"]
        NOTSEND["!Send<br/>❌ ограничен одним потоком"]
        NOTSYNC["!Sync<br/>❌ нет конкурентного разделения"]
    end

    subgraph types["Таксономия типов"]
        direction TB
        PLAIN["Примитивы, String, Vec<br/>Send + Sync"]
        CELL["Cell, RefCell<br/>Send + !Sync"]
        RC["Rc, сырые указатели<br/>!Send + !Sync"]
        MUTEX["Mutex&lt;T&gt;<br/>восстанавливает Sync"]
        ARC["Arc&lt;T&gt;<br/>разделяемое владение + Send"]
    end

    subgraph runtime["Рантайм"]
        SAFE["Потокобезопасный доступ<br/>Нет гонок данных<br/>Нет забытых блокировок"]
    end

    SEND --> PLAIN
    NOTSYNC --> CELL
    NOTSEND --> RC
    CELL --> MUTEX --> SAFE
    RC --> ARC --> SAFE
    PLAIN --> SAFE

    style SEND fill:#c8e6c9,color:#000
    style SYNC fill:#c8e6c9,color:#000
    style NOTSEND fill:#ffcdd2,color:#000
    style NOTSYNC fill:#ffcdd2,color:#000
    style PLAIN fill:#c8e6c9,color:#000
    style CELL fill:#fff3e0,color:#000
    style RC fill:#ffcdd2,color:#000
    style MUTEX fill:#e1f5fe,color:#000
    style ARC fill:#e1f5fe,color:#000
    style SAFE fill:#c8e6c9,color:#000

Ключевые выводы

  1. Send и Sync — это доказательства времени компиляции о безопасности конкурентности: компилятор выводит их структурно, проверяя каждое поле. Ни аннотаций, ни накладных расходов во время выполнения, ни явного согласия не нужно.

  2. Сырые указатели автоматически отказываются от трейтов: любой тип, содержащий *const T или *mut T, становится !Send + !Sync. Поэтому дескрипторы периферии естественно ограничены одним потоком.

  3. PhantomData<*const ()> — явный отказ: когда у типа нет сырого указателя, но его всё равно нужно ограничить одним потоком (дескрипторы библиотек на C, контексты GPU), задачу решает phantom-поле.

  4. Mutex<T> восстанавливает Sync с доказательством: компилятор структурно доказывает, что весь доступ проходит через блокировку. В отличие от pthread_mutex_t из C, забыть взять блокировку невозможно.

  5. Ограничения функций — это теоремы: F: Send + 'static в сигнатуре планировщика — обязательство доказательства на этапе компиляции: каждый вызов должен доказать, что его замыкание потокобезопасно. Сравните с void *arg в C, который принимает что угодно.

  6. Паттерн дополняет все остальные техники корректности: typestate доказывает порядок протокола, phantom-типы доказывают права доступа, const fn доказывает инварианты значений, а Send/Sync доказывают безопасность конкурентности. Вместе они покрывают всю поверхность корректности.

Собираем всё вместе: полная платформа диагностики 🟡

Что вы узнаете: как все семь основных паттернов (гл. 02–09) объединяются в единый диагностический сценарий: аутентификация, сессии, типизированные команды, токены аудита, размерные результаты, проверенные данные и регистры с phantom-типами. Суммарные накладные расходы во время выполнения при этом равны нулю.

Перекрёстные ссылки: каждая глава с основными паттернами (гл. 02–09), гл. 14 (тестирование этих гарантий)

Цель

Эта глава объединяет семь паттернов из глав 2–9 в единый реалистичный диагностический сценарий. Мы построим проверку состояния сервера, которая:

  1. Проходит аутентификацию (capability-токен, гл. 04)
  2. Открывает сессию IPMI (typestate, гл. 05)
  3. Отправляет типизированные команды (типизированные команды, гл. 02)
  4. Использует одноразовые токены для журнала аудита (одноразовые типы, гл. 03)
  5. Возвращает размерные результаты (анализ размерностей, гл. 06)
  6. Проверяет данные FRU (проверенные границы, гл. 07)
  7. Читает типизированные регистры (phantom-типы, гл. 09)
use std::marker::PhantomData;
use std::io;
// ──── Паттерн 1: размерные типы (гл. 06) ────

#[derive(Debug, Clone, Copy, PartialEq, PartialOrd)]
pub struct Celsius(pub f64);

#[derive(Debug, Clone, Copy, PartialEq, PartialOrd)]
pub struct Rpm(pub f64);

#[derive(Debug, Clone, Copy, PartialEq, PartialOrd)]
pub struct Volts(pub f64);

// ──── Паттерн 2: типизированные команды (гл. 02) ────

/// Та же форма трейта, что в гл. 02; здесь используются методы (а не ассоциированные
/// константы) для единообразия. Ассоциированные константы (`const NETFN: u8`) — равноценная
/// альтернатива, если значение действительно фиксировано для типа.
pub trait IpmiCmd {
    type Response;
    fn net_fn(&self) -> u8;
    fn cmd_byte(&self) -> u8;
    fn payload(&self) -> Vec<u8>;
    fn parse_response(&self, raw: &[u8]) -> io::Result<Self::Response>;
}

pub struct ReadTemp { pub sensor_id: u8 }
impl IpmiCmd for ReadTemp {
    type Response = Celsius;   // ← размерный тип!
    fn net_fn(&self) -> u8 { 0x04 }
    fn cmd_byte(&self) -> u8 { 0x2D }
    fn payload(&self) -> Vec<u8> { vec![self.sensor_id] }
    fn parse_response(&self, raw: &[u8]) -> io::Result<Celsius> {
        if raw.is_empty() {
            return Err(io::Error::new(io::ErrorKind::InvalidData, "пустой ответ"));
        }
        Ok(Celsius(raw[0] as f64))
    }
}

pub struct ReadFanSpeed { pub fan_id: u8 }
impl IpmiCmd for ReadFanSpeed {
    type Response = Rpm;
    fn net_fn(&self) -> u8 { 0x04 }
    fn cmd_byte(&self) -> u8 { 0x2D }
    fn payload(&self) -> Vec<u8> { vec![self.fan_id] }
    fn parse_response(&self, raw: &[u8]) -> io::Result<Rpm> {
        if raw.len() < 2 {
            return Err(io::Error::new(io::ErrorKind::InvalidData, "нужно 2 байта"));
        }
        Ok(Rpm(u16::from_le_bytes([raw[0], raw[1]]) as f64))
    }
}

// ──── Паттерн 3: capability-токен (гл. 04) ────

pub struct AdminToken { _private: () }

pub fn authenticate(user: &str, pass: &str) -> Result<AdminToken, &'static str> {
    if user == "admin" && pass == "secret" {
        Ok(AdminToken { _private: () })
    } else {
        Err("аутентификация не пройдена")
    }
}

// ──── Паттерн 4: сессия с typestate (гл. 05) ────

pub struct Idle;
pub struct Active;

pub struct Session<State> {
    host: String,
    _state: PhantomData<State>,
}

impl Session<Idle> {
    pub fn connect(host: &str) -> Self {
        Session { host: host.to_string(), _state: PhantomData }
    }

    pub fn activate(
        self,
        _admin: &AdminToken,  // ← требует capability-токен
    ) -> Result<Session<Active>, String> {
        println!("Сессия активирована на {}", self.host);
        Ok(Session { host: self.host, _state: PhantomData })
    }
}

impl Session<Active> {
    /// Выполняет типизированную команду — доступно только для активных сессий.
    /// Возвращает io::Result, чтобы пробрасывать ошибки транспорта (согласованно с гл. 02).
    pub fn execute<C: IpmiCmd>(&mut self, cmd: &C) -> io::Result<C::Response> {
        let raw_response = self.raw_send(cmd.net_fn(), cmd.cmd_byte(), &cmd.payload())?;
        cmd.parse_response(&raw_response)
    }

    fn raw_send(&self, _nf: u8, _cmd: u8, _data: &[u8]) -> io::Result<Vec<u8>> {
        Ok(vec![42, 0x1E]) // заглушка: сырой ответ IPMI
    }

    pub fn close(self) { println!("Сессия закрыта"); }
}

// ──── Паттерн 5: одноразовый токен аудита (гл. 03) ────

/// Каждый запуск диагностики получает уникальный токен аудита.
/// Не Clone, не Copy: гарантирует, что каждая запись аудита уникальна.
pub struct AuditToken {
    run_id: u64,
}

impl AuditToken {
    pub fn issue(run_id: u64) -> Self {
        AuditToken { run_id }
    }

    /// Потребляет токен для записи в журнал аудита.
    pub fn log(self, message: &str) {
        println!("[АУДИТ run_id={}] {}", self.run_id, message);
        // токен потреблён — нельзя записать один и тот же run_id дважды
    }
}

// ──── Паттерн 6: проверенная граница (гл. 07) ────
// Упрощено по сравнению с полным ValidFru из гл. 07: только поля, нужные для этого
// составного примера. Полную версию TryFrom<RawFruData> см. в гл. 07.

pub struct ValidFru {
    pub board_serial: String,
    pub product_name: String,
}

impl ValidFru {
    pub fn parse(raw: &[u8]) -> Result<Self, &'static str> {
        if raw.len() < 8 { return Err("FRU слишком короткий"); }
        if raw[0] != 0x01 { return Err("неверная версия FRU"); }
        Ok(ValidFru {
            board_serial: "SN12345".to_string(),  // заглушка
            product_name: "ServerX".to_string(),
        })
    }
}

// ──── Паттерн 7: регистры с phantom-типом (гл. 09) ────

pub struct Width16;
pub struct Reg<W> { offset: u16, _w: PhantomData<W> }

impl Reg<Width16> {
    pub fn read(&self) -> u16 { 0x8086 } // заглушка
}

pub struct PcieDev {
    pub vendor_id: Reg<Width16>,
    pub device_id: Reg<Width16>,
}

impl PcieDev {
    pub fn new() -> Self {
        PcieDev {
            vendor_id: Reg { offset: 0x00, _w: PhantomData },
            device_id: Reg { offset: 0x02, _w: PhantomData },
        }
    }
}

// ──── Составной сценарий ────

fn full_diagnostic() -> Result<(), String> {
    // 1. Аутентификация → получаем capability-токен
    let admin = authenticate("admin", "secret")
        .map_err(|e| e.to_string())?;

    // 2. Подключение и активация сессии (typestate: Idle → Active)
    let session = Session::connect("192.168.1.100");
    let mut session = session.activate(&admin)?;  // требует AdminToken

    // 3. Отправляем типизированные команды (тип ответа соответствует команде)
    let temp: Celsius = session.execute(&ReadTemp { sensor_id: 0 })
        .map_err(|e| e.to_string())?;
    let fan: Rpm = session.execute(&ReadFanSpeed { fan_id: 1 })
        .map_err(|e| e.to_string())?;

    // Несоответствие типов будет поймано:
    // let wrong: Volts = session.execute(&ReadTemp { sensor_id: 0 })?;
    //  ❌ ОШИБКА: expected Celsius, found Volts

    // 4. Читаем регистры PCIe с phantom-типом
    let pcie = PcieDev::new();
    let vid: u16 = pcie.vendor_id.read();  // гарантированно u16

    // 5. Проверяем данные FRU на границе
    let raw_fru = vec![0x01, 0x00, 0x00, 0x01, 0x01, 0x00, 0x00, 0xFD];
    let fru = ValidFru::parse(&raw_fru)
        .map_err(|e| e.to_string())?;

    // 6. Выдаём одноразовый токен аудита
    let audit = AuditToken::issue(1001);

    // 7. Формируем отчёт (все данные типизированы и проверены)
    let report = format!(
        "Сервер: {} (SN: {}), VID: 0x{:04X}, CPU: {:?}, Вентилятор: {:?}",
        fru.product_name, fru.board_serial, vid, temp, fan,
    );

    // 8. Потребляем токен аудита — записать дважды нельзя
    audit.log(&report);
    // audit.log("повтор");  // ❌ use of moved value

    // 9. Закрываем сессию (typestate: Active → уничтожена)
    session.close();

    Ok(())
}

Что доказывает компилятор

Класс ошибкиКак это предотвращаетсяПаттерн
Доступ без аутентификацииactivate() требует &AdminTokenCapability-токен
Команда в неверном состоянии сессииexecute() существует только у Session<Active>Typestate
Неверный тип ответаReadTemp::Response = Celsius, фиксируется трейтомТипизированные команды
Путаница единиц (°C и об/мин)Celsius ≠ Rpm ≠ VoltsРазмерные типы
Несоответствие ширины регистраReg<Width16> возвращает u16Phantom-типы
Обработка непроверенных данныхСначала нужно вызвать ValidFru::parse()Проверенная граница
Дублирование записей аудитаAuditToken потребляется при записи в журналОдноразовый тип
Нарушение порядка подачи питанияКаждый шаг требует токен предыдущегоCapability-токены (гл. 04)

Суммарные накладные расходы во время выполнения от ВСЕХ этих гарантий: ноль.

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

Ключевые выводы

  1. Семь паттернов сочетаются без швов: capability-токены, typestate, типизированные команды, одноразовые типы, размерные типы, проверенные границы и phantom-типы работают вместе.
  2. Компилятор доказывает невозможность восьми классов ошибок: см. таблицу «Что доказывает компилятор» выше.
  3. Нулевые суммарные накладные расходы во время выполнения: сгенерированный ассемблер идентичен коду на C без проверок.
  4. Каждый паттерн полезен сам по себе: не обязательно использовать все семь; внедряйте их постепенно.
  5. Глава с интеграцией — это шаблон проектирования: используйте её как отправную точку для собственных типизированных диагностических сценариев.
  6. От IPMI к Redfish в масштабе: гл. 17 и 18 применяют эти же семь паттернов (плюс capability-миксины из гл. 08) к полному клиенту и серверу Redfish. Сценарий IPMI здесь — основа; пошаговые разборы Redfish показывают, как композиция масштабируется до производственных систем с несколькими источниками данных и ограничениями версий схем.

Пошаговый разбор: типобезопасный клиент Redfish 🟡

Что вы узнаете: как скомпоновать typestate сессий, capability-токены, навигацию по ресурсам с phantom-типами, анализ размерностей, проверенные границы, typestate билдера и одноразовые типы в полноценный клиент Redfish с нулевыми накладными расходами, где каждое нарушение протокола становится ошибкой компиляции.

Перекрёстные ссылки: гл. 02 (типизированные команды), гл. 03 (одноразовые типы), гл. 04 (capability-токены), гл. 05 (typestate), гл. 06 (размерные типы), гл. 07 (проверенные границы), гл. 09 (phantom-типы), гл. 10 (интеграция с IPMI), гл. 11 (приём 4: билдер с typestate)

Почему Redfish заслуживает отдельной главы

Глава 10 строит основные паттерны вокруг IPMI, протокола на уровне байтов. Но большинство платформ BMC сейчас предоставляют REST API Redfish наряду с IPMI (или вместо него), и Redfish приносит собственную категорию опасностей для корректности:

ОпасностьПримерПоследствие
Некорректный URIGET /redfish/v1/Chassis/1/Processors (неверный родитель)Тихо возвращается 404 или неверные данные
Действие в неверном состоянии питанияReset(ForceOff) для уже выключенной системыBMC возвращает ошибку или, хуже, гонка с другой операцией
Отсутствие привилегииКод уровня оператора вызывает Manager.ResetToDefaults403 в продакшене, замечание службы безопасности
Неполный PATCHВ теле PATCH пропущен обязательный атрибут BIOSТихий no-op или частичное повреждение конфигурации
Непроверенное применение прошивкиSimpleUpdate вызван до проверки целостности образаПревращённый в кирпич BMC
Несоответствие версии схемыОбращение к LastResetTime на BMC v1.5 (добавлено в v1.13)Поле null → паника во время выполнения
Путаница единиц в телеметрииСравнение температуры на входе (°C) с потреблением мощности (Вт)Бессмысленные пороговые решения

В C, Python или нетипизированном Rust каждую из этих опасностей предотвращают только дисциплина и тестирование. Эта глава делает их ошибками компиляции.

Нетипизированный клиент Redfish

Типичный клиент Redfish выглядит так:

use std::collections::HashMap;

struct RedfishClient {
    base_url: String,
    token: Option<String>,
}

impl RedfishClient {
    fn get(&self, path: &str) -> Result<serde_json::Value, String> {
        // ... HTTP GET ...
        Ok(serde_json::json!({})) // заглушка
    }

    fn patch(&self, path: &str, body: &serde_json::Value) -> Result<(), String> {
        // ... HTTP PATCH ...
        Ok(()) // заглушка
    }

    fn post_action(&self, path: &str, body: &serde_json::Value) -> Result<(), String> {
        // ... HTTP POST ...
        Ok(()) // заглушка
    }
}

fn check_thermal(client: &RedfishClient) -> Result<(), String> {
    let resp = client.get("/redfish/v1/Chassis/1/Thermal")?;

    // 🐛 Это поле всегда присутствует? Что, если BMC вернёт null?
    let cpu_temp = resp["Temperatures"][0]["ReadingCelsius"]
        .as_f64().unwrap();

    let fan_rpm = resp["Fans"][0]["Reading"]
        .as_f64().unwrap();

    // 🐛 Сравнение °C с об/мин: оба — f64
    if cpu_temp > fan_rpm {
        println!("проблема с температурой");
    }

    // 🐛 Это правильный путь? Проверки на этапе компиляции нет.
    client.post_action(
        "/redfish/v1/Systems/1/Actions/ComputerSystem.Reset",
        &serde_json::json!({"ResetType": "ForceOff"})
    )?;

    Ok(())
}

Это «работает», пока не перестаёт работать. Каждый unwrap() — потенциальная паника, каждый строковый путь — непроверенное допущение, а путаница единиц не видна.


Раздел 1 — Жизненный цикл сессии (typestate, гл. 05)

У сессии Redfish строгий жизненный цикл: подключение → аутентификация → использование → закрытие. Каждое состояние кодируем отдельным типом.

stateDiagram-v2
    [*] --> Disconnected
    Disconnected --> Connected : connect(host)
    Connected --> Authenticated : login(user, pass)
    Authenticated --> Authenticated : get() / patch() / post_action()
    Authenticated --> Closed : logout()
    Closed --> [*]

    note right of Authenticated : вызовы API существуют только здесь
    note right of Connected : get() → ошибка компиляции
use std::marker::PhantomData;

// ──── Состояния сессии ────

pub struct Disconnected;
pub struct Connected;
pub struct Authenticated;

pub struct RedfishSession<S> {
    base_url: String,
    auth_token: Option<String>,
    _state: PhantomData<S>,
}

impl RedfishSession<Disconnected> {
    pub fn new(host: &str) -> Self {
        RedfishSession {
            base_url: format!("https://{}", host),
            auth_token: None,
            _state: PhantomData,
        }
    }

    /// Переход: Disconnected → Connected.
    /// Проверяет, что корневой сервис доступен.
    pub fn connect(self) -> Result<RedfishSession<Connected>, RedfishError> {
        // GET /redfish/v1 — проверяем корневой сервис
        println!("Подключение к {}/redfish/v1", self.base_url);
        Ok(RedfishSession {
            base_url: self.base_url,
            auth_token: None,
            _state: PhantomData,
        })
    }
}

impl RedfishSession<Connected> {
    /// Переход: Connected → Authenticated.
    /// Создаёт сессию через POST /redfish/v1/SessionService/Sessions.
    pub fn login(
        self,
        user: &str,
        _pass: &str,
    ) -> Result<(RedfishSession<Authenticated>, LoginToken), RedfishError> {
        // POST /redfish/v1/SessionService/Sessions
        println!("Аутентификация как {}", user);
        let token = "X-Auth-Token-abc123".to_string();
        Ok((
            RedfishSession {
                base_url: self.base_url,
                auth_token: Some(token),
                _state: PhantomData,
            },
            LoginToken { _private: () },
        ))
    }
}

impl RedfishSession<Authenticated> {
    /// Доступен только у аутентифицированных сессий.
    fn http_get(&self, path: &str) -> Result<serde_json::Value, RedfishError> {
        let _url = format!("{}{}", self.base_url, path);
        // ... HTTP GET с заголовком auth_token ...
        Ok(serde_json::json!({})) // заглушка
    }

    fn http_patch(
        &self,
        path: &str,
        body: &serde_json::Value,
    ) -> Result<serde_json::Value, RedfishError> {
        let _url = format!("{}{}", self.base_url, path);
        let _ = body;
        Ok(serde_json::json!({})) // заглушка
    }

    fn http_post(
        &self,
        path: &str,
        body: &serde_json::Value,
    ) -> Result<serde_json::Value, RedfishError> {
        let _url = format!("{}{}", self.base_url, path);
        let _ = body;
        Ok(serde_json::json!({})) // заглушка
    }

    /// Переход: Authenticated → Closed (сессия потребляется).
    pub fn logout(self) {
        // DELETE /redfish/v1/SessionService/Sessions/{id}
        println!("Сессия закрыта");
        // self потреблён: после logout сессию использовать нельзя
    }
}

// Попытка вызвать http_get у неаутентифицированной сессии:
//
//   let session = RedfishSession::new("bmc01").connect()?;
//   session.http_get("/redfish/v1/Systems");
//   ❌ ОШИБКА: метод `http_get` не найден для `RedfishSession<Connected>`

#[derive(Debug)]
pub enum RedfishError {
    ConnectionFailed(String),
    AuthenticationFailed(String),
    HttpError { status: u16, message: String },
    ValidationError(String),
}

impl std::fmt::Display for RedfishError {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        match self {
            Self::ConnectionFailed(msg) => write!(f, "ошибка подключения: {msg}"),
            Self::AuthenticationFailed(msg) => write!(f, "ошибка аутентификации: {msg}"),
            Self::HttpError { status, message } =>
                write!(f, "HTTP {status}: {message}"),
            Self::ValidationError(msg) => write!(f, "валидация: {msg}"),
        }
    }
}

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


Раздел 2 — Токены привилегий (capability-токены, гл. 04)

Redfish определяет четыре уровня привилегий: Login, ConfigureComponents, ConfigureManager, ConfigureSelf. Вместо проверки прав во время выполнения кодируем их как токены-доказательства нулевого размера.

// ──── Токены привилегий (нулевого размера) ────

/// Доказательство, что у вызывающего есть привилегия Login.
/// Возвращается при успешном входе: это единственный способ его получить.
pub struct LoginToken { _private: () }

/// Доказательство, что у вызывающего есть привилегия ConfigureComponents.
/// Получить его может только аутентификация на уровне администратора.
pub struct ConfigureComponentsToken { _private: () }

/// Доказательство, что у вызывающего есть привилегия ConfigureManager (обновления прошивки и т. д.).
pub struct ConfigureManagerToken { _private: () }

// Расширяем вход так, чтобы он возвращал токены привилегий в зависимости от роли:

impl RedfishSession<Connected> {
    /// Вход администратора: возвращает все токены привилегий.
    pub fn login_admin(
        self,
        user: &str,
        pass: &str,
    ) -> Result<(
        RedfishSession<Authenticated>,
        LoginToken,
        ConfigureComponentsToken,
        ConfigureManagerToken,
    ), RedfishError> {
        let (session, login_tok) = self.login(user, pass)?;
        Ok((
            session,
            login_tok,
            ConfigureComponentsToken { _private: () },
            ConfigureManagerToken { _private: () },
        ))
    }

    /// Вход оператора: возвращает только Login и ConfigureComponents.
    pub fn login_operator(
        self,
        user: &str,
        pass: &str,
    ) -> Result<(
        RedfishSession<Authenticated>,
        LoginToken,
        ConfigureComponentsToken,
    ), RedfishError> {
        let (session, login_tok) = self.login(user, pass)?;
        Ok((
            session,
            login_tok,
            ConfigureComponentsToken { _private: () },
        ))
    }

    /// Вход только для чтения: возвращает только токен Login.
    pub fn login_readonly(
        self,
        user: &str,
        pass: &str,
    ) -> Result<(RedfishSession<Authenticated>, LoginToken), RedfishError> {
        self.login(user, pass)
    }
}

Теперь требования к привилегиям — часть сигнатуры функции:

use std::marker::PhantomData;
pub struct Authenticated;
pub struct RedfishSession<S> { base_url: String, auth_token: Option<String>, _state: PhantomData<S> }
pub struct LoginToken { _private: () }
pub struct ConfigureComponentsToken { _private: () }
pub struct ConfigureManagerToken { _private: () }
#[derive(Debug)] pub enum RedfishError { HttpError { status: u16, message: String } }

/// Любой с Login может читать данные о температуре.
fn get_thermal(
    session: &RedfishSession<Authenticated>,
    _proof: &LoginToken,
) -> Result<serde_json::Value, RedfishError> {
    // GET /redfish/v1/Chassis/1/Thermal
    Ok(serde_json::json!({})) // заглушка
}

/// Изменение порядка загрузки требует ConfigureComponents.
fn set_boot_order(
    session: &RedfishSession<Authenticated>,
    _proof: &ConfigureComponentsToken,
    order: &[&str],
) -> Result<(), RedfishError> {
    let _ = order;
    // PATCH /redfish/v1/Systems/1
    Ok(())
}

/// Сброс к заводским настройкам требует ConfigureManager.
fn reset_to_defaults(
    session: &RedfishSession<Authenticated>,
    _proof: &ConfigureManagerToken,
) -> Result<(), RedfishError> {
    // POST .../Actions/Manager.ResetToDefaults
    Ok(())
}

// Код оператора, вызывающий reset_to_defaults:
//
//   let (session, login, configure) = session.login_operator("op", "pass")?;
//   reset_to_defaults(&session, &???);
//   ❌ ОШИБКА: нет доступного ConfigureManagerToken, оператор не может этого сделать

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


Раздел 3 — Типизированная навигация по ресурсам (phantom-типы, гл. 09)

Ресурсы Redfish образуют дерево. Кодирование иерархии в типах не даёт построить недопустимые URI:

graph TD
    SR["Корень службы (ServiceRoot)"] --> Systems["Системы"]
    SR --> Chassis["Шасси"]
    SR --> Managers["Менеджеры"]
    SR --> UpdateService["Служба обновления"]
    Systems --> CS["ComputerSystem"]
    CS --> Processors["Процессоры"]
    CS --> Memory["Память"]
    CS --> Bios["BIOS"]
    Chassis --> Ch1["Экземпляр шасси"]
    Ch1 --> Thermal["Термо (Thermal)"]
    Ch1 --> Power["Питание (Power)"]
    Managers --> Mgr["Экземпляр менеджера"]
use std::marker::PhantomData;

// ──── Маркеры типов ресурсов ────

pub struct ServiceRoot;
pub struct SystemsCollection;
pub struct ComputerSystem;
pub struct ChassisCollection;
pub struct ChassisInstance;
pub struct ThermalResource;
pub struct PowerResource;
pub struct BiosResource;
pub struct ManagersCollection;
pub struct ManagerInstance;
pub struct UpdateServiceResource;

// ──── Типизированный путь к ресурсу ────

pub struct RedfishPath<R> {
    uri: String,
    _resource: PhantomData<R>,
}

impl RedfishPath<ServiceRoot> {
    pub fn root() -> Self {
        RedfishPath {
            uri: "/redfish/v1".to_string(),
            _resource: PhantomData,
        }
    }

    pub fn systems(&self) -> RedfishPath<SystemsCollection> {
        RedfishPath {
            uri: format!("{}/Systems", self.uri),
            _resource: PhantomData,
        }
    }

    pub fn chassis(&self) -> RedfishPath<ChassisCollection> {
        RedfishPath {
            uri: format!("{}/Chassis", self.uri),
            _resource: PhantomData,
        }
    }

    pub fn managers(&self) -> RedfishPath<ManagersCollection> {
        RedfishPath {
            uri: format!("{}/Managers", self.uri),
            _resource: PhantomData,
        }
    }

    pub fn update_service(&self) -> RedfishPath<UpdateServiceResource> {
        RedfishPath {
            uri: format!("{}/UpdateService", self.uri),
            _resource: PhantomData,
        }
    }
}

impl RedfishPath<SystemsCollection> {
    pub fn system(&self, id: &str) -> RedfishPath<ComputerSystem> {
        RedfishPath {
            uri: format!("{}/{}", self.uri, id),
            _resource: PhantomData,
        }
    }
}

impl RedfishPath<ComputerSystem> {
    pub fn bios(&self) -> RedfishPath<BiosResource> {
        RedfishPath {
            uri: format!("{}/Bios", self.uri),
            _resource: PhantomData,
        }
    }
}

impl RedfishPath<ChassisCollection> {
    pub fn instance(&self, id: &str) -> RedfishPath<ChassisInstance> {
        RedfishPath {
            uri: format!("{}/{}", self.uri, id),
            _resource: PhantomData,
        }
    }
}

impl RedfishPath<ChassisInstance> {
    pub fn thermal(&self) -> RedfishPath<ThermalResource> {
        RedfishPath {
            uri: format!("{}/Thermal", self.uri),
            _resource: PhantomData,
        }
    }

    pub fn power(&self) -> RedfishPath<PowerResource> {
        RedfishPath {
            uri: format!("{}/Power", self.uri),
            _resource: PhantomData,
        }
    }
}

impl RedfishPath<ManagersCollection> {
    pub fn manager(&self, id: &str) -> RedfishPath<ManagerInstance> {
        RedfishPath {
            uri: format!("{}/{}", self.uri, id),
            _resource: PhantomData,
        }
    }
}

impl<R> RedfishPath<R> {
    pub fn uri(&self) -> &str {
        &self.uri
    }
}

// ── Использование ──

fn build_paths() {
    let root = RedfishPath::root();

    // ✅ Корректная навигация
    let thermal = root.chassis().instance("1").thermal();
    assert_eq!(thermal.uri(), "/redfish/v1/Chassis/1/Thermal");

    let bios = root.systems().system("1").bios();
    assert_eq!(bios.uri(), "/redfish/v1/Systems/1/Bios");

    // ❌ Ошибка компиляции: у ServiceRoot нет метода .thermal()
    // root.thermal();

    // ❌ Ошибка компиляции: у SystemsCollection нет метода .bios()
    // root.systems().bios();

    // ❌ Ошибка компиляции: у ChassisInstance нет метода .bios()
    // root.chassis().instance("1").bios();
}

Устранённый класс ошибок: некорректные URI, переход к дочернему ресурсу, которого нет под данным родителем. Иерархия обеспечивается структурно: до Thermal можно добраться только через Chassis → Instance → Thermal.


Раздел 4 — Типизированное чтение телеметрии (типизированные команды + анализ размерностей, гл. 02 + гл. 06)

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

use std::marker::PhantomData;

// ──── Размерные типы (гл. 06) ────

#[derive(Debug, Clone, Copy, PartialEq, PartialOrd)]
pub struct Celsius(pub f64);

#[derive(Debug, Clone, Copy, PartialEq, PartialOrd)]
pub struct Rpm(pub u32);

#[derive(Debug, Clone, Copy, PartialEq, PartialOrd)]
pub struct Watts(pub f64);

#[derive(Debug, Clone, Copy, PartialEq, PartialOrd)]
pub struct Volts(pub f64);

// ──── Типизированный GET Redfish (паттерн гл. 02, применённый к REST) ────

/// Тип ресурса Redfish определяет его разобранный ответ.
pub trait RedfishResource {
    type Response;
    fn parse(json: &serde_json::Value) -> Result<Self::Response, RedfishError>;
}

// ──── Проверенный ответ Thermal (гл. 07) ────

#[derive(Debug)]
pub struct ValidThermalResponse {
    pub temperatures: Vec<TemperatureReading>,
    pub fans: Vec<FanReading>,
}

#[derive(Debug)]
pub struct TemperatureReading {
    pub name: String,
    pub reading: Celsius,           // ← размерный тип, а не f64
    pub upper_critical: Celsius,
    pub status: HealthStatus,
}

#[derive(Debug)]
pub struct FanReading {
    pub name: String,
    pub reading: Rpm,               // ← размерный тип, а не u32
    pub status: HealthStatus,
}

#[derive(Debug, Clone, Copy, PartialEq)]
pub enum HealthStatus { Ok, Warning, Critical }

impl RedfishResource for ThermalResource {
    type Response = ValidThermalResponse;

    fn parse(json: &serde_json::Value) -> Result<ValidThermalResponse, RedfishError> {
        // Разбор и валидация за один проход: валидация на границе (гл. 07)
        let temps = json["Temperatures"]
            .as_array()
            .ok_or_else(|| RedfishError::ValidationError(
                "отсутствует массив Temperatures".into(),
            ))?
            .iter()
            .map(|t| {
                Ok(TemperatureReading {
                    name: t["Name"]
                        .as_str()
                        .ok_or_else(|| RedfishError::ValidationError(
                            "отсутствует Name".into(),
                        ))?
                        .to_string(),
                    reading: Celsius(
                        t["ReadingCelsius"]
                            .as_f64()
                            .ok_or_else(|| RedfishError::ValidationError(
                                "отсутствует ReadingCelsius".into(),
                            ))?,
                    ),
                    upper_critical: Celsius(
                        t["UpperThresholdCritical"]
                            .as_f64()
                            .unwrap_or(105.0), // безопасное значение по умолчанию для отсутствующего порога
                    ),
                    status: parse_health(
                        t["Status"]["Health"]
                            .as_str()
                            .unwrap_or("OK"),
                    ),
                })
            })
            .collect::<Result<Vec<_>, _>>()?;

        let fans = json["Fans"]
            .as_array()
            .ok_or_else(|| RedfishError::ValidationError(
                "отсутствует массив Fans".into(),
            ))?
            .iter()
            .map(|f| {
                Ok(FanReading {
                    name: f["Name"]
                        .as_str()
                        .ok_or_else(|| RedfishError::ValidationError(
                            "отсутствует Name".into(),
                        ))?
                        .to_string(),
                    reading: Rpm(
                        f["Reading"]
                            .as_u64()
                            .ok_or_else(|| RedfishError::ValidationError(
                                "отсутствует Reading".into(),
                            ))? as u32,
                    ),
                    status: parse_health(
                        f["Status"]["Health"]
                            .as_str()
                            .unwrap_or("OK"),
                    ),
                })
            })
            .collect::<Result<Vec<_>, _>>()?;

        Ok(ValidThermalResponse { temperatures: temps, fans })
    }
}

fn parse_health(s: &str) -> HealthStatus {
    match s {
        "OK" => HealthStatus::Ok,
        "Warning" => HealthStatus::Warning,
        _ => HealthStatus::Critical,
    }
}

// ──── Типизированный GET в аутентифицированной сессии ────

impl RedfishSession<Authenticated> {
    pub fn get_resource<R: RedfishResource>(
        &self,
        path: &RedfishPath<R>,
    ) -> Result<R::Response, RedfishError> {
        let json = self.http_get(path.uri())?;
        R::parse(&json)
    }
}

// ── Использование ──

fn read_thermal(
    session: &RedfishSession<Authenticated>,
    _proof: &LoginToken,
) -> Result<(), RedfishError> {
    let path = RedfishPath::root().chassis().instance("1").thermal();

    // Тип ответа выводится: ValidThermalResponse
    let thermal = session.get_resource(&path)?;

    for t in &thermal.temperatures {
        // t.reading — Celsius: сравнивать можно только с Celsius
        if t.reading > t.upper_critical {
            println!("КРИТИЧНО: {} при {:?}", t.name, t.reading);
        }

        // ❌ Ошибка компиляции: нельзя сравнить Celsius с Rpm
        // if t.reading > thermal.fans[0].reading { }

        // ❌ Ошибка компиляции: нельзя сравнить Celsius с Watts
        // if t.reading > Watts(350.0) { }
    }

    Ok(())
}

Устранённые классы ошибок:

  • Путаница единиц: Celsius ≠ Rpm ≠ Watts, компилятор отвергает сравнения.
  • Паника из-за отсутствующего поля: parse() валидирует на границе; ValidThermalResponse гарантирует наличие всех полей.
  • Неверный тип ответа: get_resource(&thermal_path) возвращает ValidThermalResponse, а не сырой JSON. Тип ресурса определяет тип ответа на этапе компиляции.

Раздел 5 — PATCH с билдером typestate (гл. 11, приём 4)

Полезные нагрузки PATCH в Redfish должны содержать определённые поля. Билдер, который открывает .apply() только при установке обязательных полей, не даёт отправить неполные или пустые патчи:

use std::marker::PhantomData;

// ──── Типовые булевы значения для обязательных полей ────

pub struct FieldUnset;
pub struct FieldSet;

// ──── Билдер PATCH для настроек BIOS ────

pub struct BiosPatchBuilder<BootOrder, TpmState> {
    boot_order: Option<Vec<String>>,
    tpm_enabled: Option<bool>,
    _markers: PhantomData<(BootOrder, TpmState)>,
}

impl BiosPatchBuilder<FieldUnset, FieldUnset> {
    pub fn new() -> Self {
        BiosPatchBuilder {
            boot_order: None,
            tpm_enabled: None,
            _markers: PhantomData,
        }
    }
}

impl<T> BiosPatchBuilder<FieldUnset, T> {
    /// Задаёт порядок загрузки: переводит маркер BootOrder в FieldSet.
    pub fn boot_order(self, order: Vec<String>) -> BiosPatchBuilder<FieldSet, T> {
        BiosPatchBuilder {
            boot_order: Some(order),
            tpm_enabled: self.tpm_enabled,
            _markers: PhantomData,
        }
    }
}

impl<B> BiosPatchBuilder<B, FieldUnset> {
    /// Задаёт состояние TPM: переводит маркер TpmState в FieldSet.
    pub fn tpm_enabled(self, enabled: bool) -> BiosPatchBuilder<B, FieldSet> {
        BiosPatchBuilder {
            boot_order: self.boot_order,
            tpm_enabled: Some(enabled),
            _markers: PhantomData,
        }
    }
}

impl BiosPatchBuilder<FieldSet, FieldSet> {
    /// .apply() существует только тогда, когда ВСЕ обязательные поля заданы.
    pub fn apply(
        self,
        session: &RedfishSession<Authenticated>,
        _proof: &ConfigureComponentsToken,
        system: &RedfishPath<ComputerSystem>,
    ) -> Result<(), RedfishError> {
        let body = serde_json::json!({
            "Boot": {
                "BootOrder": self.boot_order.unwrap(),
            },
            "Oem": {
                "TpmEnabled": self.tpm_enabled.unwrap(),
            }
        });
        session.http_patch(
            &format!("{}/Bios/Settings", system.uri()),
            &body,
        )?;
        Ok(())
    }
}

// ── Использование ──

fn configure_bios(
    session: &RedfishSession<Authenticated>,
    configure: &ConfigureComponentsToken,
) -> Result<(), RedfishError> {
    let system = RedfishPath::root().systems().system("1");

    // ✅ Оба обязательных поля заданы: .apply() доступен
    BiosPatchBuilder::new()
        .boot_order(vec!["Pxe".into(), "Hdd".into()])
        .tpm_enabled(true)
        .apply(session, configure, &system)?;

    // ❌ Ошибка компиляции: .apply() не найден у BiosPatchBuilder<FieldSet, FieldUnset>
    // BiosPatchBuilder::new()
    //     .boot_order(vec!["Pxe".into()])
    //     .apply(session, configure, &system)?;

    // ❌ Ошибка компиляции: .apply() не найден у BiosPatchBuilder<FieldUnset, FieldUnset>
    // BiosPatchBuilder::new()
    //     .apply(session, configure, &system)?;

    Ok(())
}

Устранённые классы ошибок:

  • Пустой PATCH: нельзя вызвать .apply(), не задав каждое обязательное поле.
  • Отсутствие привилегии: .apply() требует &ConfigureComponentsToken.
  • Неверный ресурс: принимает &RedfishPath<ComputerSystem>, а не строку.

Раздел 6 — Жизненный цикл обновления прошивки (одноразовые типы + typestate, гл. 03 + гл. 05)

У UpdateService в Redfish строгая последовательность: загрузить образ → проверить → применить → перезагрузить. Каждая фаза должна произойти ровно один раз и в порядке очерёдности.

stateDiagram-v2
    [*] --> Idle
    Idle --> Uploading : push_image()
    Uploading --> Uploaded : загрузка завершена
    Uploaded --> Verified : verify() ✓
    Uploaded --> Failed : verify() ✗
    Verified --> Applying : apply() — потребляет Verified
    Applying --> NeedsReboot : применение завершено
    NeedsReboot --> [*] : reboot()
    Failed --> [*]

    note right of Verified : apply() потребляет это состояние —
    note right of Verified : применить дважды нельзя
use std::marker::PhantomData;

// ──── Состояния обновления прошивки ────

pub struct FwIdle;
pub struct FwUploaded;
pub struct FwVerified;
pub struct FwApplying;
pub struct FwNeedsReboot;

pub struct FirmwareUpdate<S> {
    task_uri: String,
    image_hash: String,
    _phase: PhantomData<S>,
}

impl FirmwareUpdate<FwIdle> {
    pub fn push_image(
        session: &RedfishSession<Authenticated>,
        _proof: &ConfigureManagerToken,
        image: &[u8],
    ) -> Result<FirmwareUpdate<FwUploaded>, RedfishError> {
        // POST /redfish/v1/UpdateService/Actions/UpdateService.SimpleUpdate
        // или multipart-загрузка в /redfish/v1/UpdateService/upload
        let _ = image;
        println!("Образ загружен ({} байт)", image.len());
        Ok(FirmwareUpdate {
            task_uri: "/redfish/v1/TaskService/Tasks/1".to_string(),
            image_hash: "sha256:abc123".to_string(),
            _phase: PhantomData,
        })
    }
}

impl FirmwareUpdate<FwUploaded> {
    /// Проверяет целостность образа. При успехе возвращает FwVerified.
    pub fn verify(self) -> Result<FirmwareUpdate<FwVerified>, RedfishError> {
        // Опрашиваем задачу до завершения проверки
        println!("Образ проверен: {}", self.image_hash);
        Ok(FirmwareUpdate {
            task_uri: self.task_uri,
            image_hash: self.image_hash,
            _phase: PhantomData,
        })
    }
}

impl FirmwareUpdate<FwVerified> {
    /// Применяет обновление. Потребляет self: применить дважды нельзя.
    /// Это одноразовый паттерн из гл. 03.
    pub fn apply(self) -> Result<FirmwareUpdate<FwNeedsReboot>, RedfishError> {
        // PATCH /redfish/v1/UpdateService: задаём ApplyTime
        println!("Прошивка применена из {}", self.task_uri);
        // self перемещён: повторный вызов apply() — ошибка компиляции
        Ok(FirmwareUpdate {
            task_uri: self.task_uri,
            image_hash: self.image_hash,
            _phase: PhantomData,
        })
    }
}

impl FirmwareUpdate<FwNeedsReboot> {
    /// Перезагрузка, чтобы активировать новую прошивку.
    pub fn reboot(
        self,
        session: &RedfishSession<Authenticated>,
        _proof: &ConfigureManagerToken,
    ) -> Result<(), RedfishError> {
        // POST .../Actions/Manager.Reset {"ResetType": "GracefulRestart"}
        let _ = session;
        println!("BMC перезагружается для активации прошивки");
        Ok(())
    }
}

// ── Использование ──

fn update_bmc_firmware(
    session: &RedfishSession<Authenticated>,
    manager_proof: &ConfigureManagerToken,
    image: &[u8],
) -> Result<(), RedfishError> {
    // Каждый шаг возвращает следующее состояние, предыдущее потребляется
    let uploaded = FirmwareUpdate::push_image(session, manager_proof, image)?;
    let verified = uploaded.verify()?;
    let needs_reboot = verified.apply()?;
    needs_reboot.reboot(session, manager_proof)?;

    // ❌ Ошибка компиляции: использование перемещённого значения `verified`
    // verified.apply()?;

    // ❌ Ошибка компиляции: у FirmwareUpdate<FwUploaded> нет метода .apply()
    // uploaded.apply()?;      // сначала нужно проверить!

    // ❌ Ошибка компиляции: push_image требует &ConfigureManagerToken
    // FirmwareUpdate::push_image(session, &login_token, image)?;

    Ok(())
}

Устранённые классы ошибок:

  • Применение непроверенной прошивки: .apply() существует только у FwVerified.
  • Двойное применение: apply() потребляет self, перемещённое значение нельзя использовать повторно.
  • Пропуск перезагрузки: FwNeedsReboot — отдельный тип; нельзя по ошибке продолжить обычную работу, пока прошивка подготовлена.
  • Несанкционированное обновление: push_image() требует &ConfigureManagerToken.

Раздел 7 — Собираем всё вместе

Вот полный диагностический сценарий, который объединяет все шесть разделов:

fn full_redfish_diagnostic() -> Result<(), RedfishError> {
    // ── 1. Жизненный цикл сессии (раздел 1) ──
    let session = RedfishSession::new("bmc01.lab.local");
    let session = session.connect()?;

    // ── 2. Токены привилегий (раздел 2) ──
    // Вход администратора: получаем все токены возможностей
    let (session, _login, configure, manager) =
        session.login_admin("admin", "p@ssw0rd")?;

    // ── 3. Типизированная навигация (раздел 3) ──
    let thermal_path = RedfishPath::root()
        .chassis()
        .instance("1")
        .thermal();

    // ── 4. Типизированное чтение телеметрии (раздел 4) ──
    let thermal: ValidThermalResponse = session.get_resource(&thermal_path)?;

    for t in &thermal.temperatures {
        // Celsius сравнивается только с Celsius: размерная безопасность
        if t.reading > t.upper_critical {
            println!("🔥 {} в критическом состоянии: {:?}", t.name, t.reading);
        }
    }

    for f in &thermal.fans {
        if f.reading < Rpm(1000) {
            println!("⚠ {} ниже порога: {:?}", f.name, f.reading);
        }
    }

    // ── 5. Типобезопасный PATCH (раздел 5) ──
    let system_path = RedfishPath::root().systems().system("1");

    BiosPatchBuilder::new()
        .boot_order(vec!["Pxe".into(), "Hdd".into()])
        .tpm_enabled(true)
        .apply(&session, &configure, &system_path)?;

    // ── 6. Жизненный цикл обновления прошивки (раздел 6) ──
    let firmware_image = include_bytes!("bmc_firmware.bin");
    let uploaded = FirmwareUpdate::push_image(&session, &manager, firmware_image)?;
    let verified = uploaded.verify()?;
    let needs_reboot = verified.apply()?;

    // ── 7. Чистое завершение ──
    needs_reboot.reboot(&session, &manager)?;
    session.logout();

    Ok(())
}

Что доказывает компилятор

№Класс ошибкиКак это предотвращаетсяПаттерн (раздел)
1Запрос в неаутентифицированной сессииhttp_get() существует только у Session<Authenticated>Typestate (§1)
2Эскалация привилегийConfigureManagerToken не выдаётся при входе оператораCapability-токены (§2)
3Некорректный URI RedfishМетоды навигации обеспечивают иерархию родитель → потомокPhantom-типы (§3)
4Путаница единиц (°C, об/мин, Вт)Celsius, Rpm, Watts — разные типыАнализ размерностей (§4)
5Отсутствующее поле JSON → паникаValidThermalResponse валидирует на границе разбораПроверенные границы (§4)
6Неверный тип ответаRedfishResource::Response фиксирован для каждого ресурсаТипизированные команды (§4)
7Неполная нагрузка PATCH.apply() существует только при FieldSet у всех полейTypestate билдера (§5)
8Отсутствие привилегии для PATCH.apply() требует &ConfigureComponentsTokenCapability-токены (§5)
9Применение непроверенной прошивки.apply() существует только у FwVerifiedTypestate (§6)
10Двойное применение прошивкиapply() потребляет self: значение перемещаетсяОдноразовые типы (§6)
11Обновление прошивки без полномочийpush_image() требует &ConfigureManagerTokenCapability-токены (§6)
12Использование после выходаlogout() потребляет сессиюВладение (§1)

Суммарные накладные расходы во время выполнения от ВСЕХ двенадцати гарантий: ноль.

Скомпилированный бинарник делает те же HTTP-вызовы, что и нетипизированная версия, но нетипизированная версия может содержать 12 классов ошибок. Эта версия не может.


Сравнение: интеграция с IPMI (гл. 10) и с Redfish

Аспектгл. 10 (IPMI)Эта глава (Redfish)
ТранспортСырые байты через KCS/LANJSON через HTTPS
НавигацияПлоские коды команд (NetFn/Cmd)Иерархическое дерево URI
Привязка ответаIpmiCmd::ResponseRedfishResource::Response
Модель привилегийОдин AdminTokenМноготокенная модель на основе ролей
Построение полезной нагрузкиМассивы байтовБилдер с typestate для JSON
Жизненный цикл обновленияНе рассматриваетсяПолная цепочка typestate
Задействованные паттерны78 (добавляется typestate билдера)

Две главы дополняют друг друга: гл. 10 показывает, что паттерны работают на уровне байтов, а эта глава показывает, что они работают одинаково на уровне REST/JSON. Система типов не зависит от транспорта и доказывает корректность в обоих случаях.

Ключевые выводы

  1. Восемь паттернов складываются в один клиент Redfish: typestate сессии, capability-токены, URI с phantom-типами, типизированные команды, анализ размерностей, проверенные границы, typestate билдера и одноразовое применение прошивки.
  2. Двенадцать классов ошибок становятся ошибками компиляции: см. таблицу выше.
  3. Нулевые накладные расходы во время выполнения: каждый токен-доказательство, phantom-тип и маркер typestate исчезает при компиляции. Бинарный файл идентичен написанному вручную нетипизированному коду.
  4. REST API выигрывают не меньше, чем протоколы на уровне байтов: паттерны из гл. 02–09 одинаково применимы к JSON-over-HTTPS (Redfish) и байтам-over-KCS (IPMI).
  5. Соблюдение привилегий структурно, а не процедурно: сигнатура функции объявляет требования, а компилятор их обеспечивает.
  6. Это шаблон проектирования: адаптируйте маркеры типов ресурсов, capability-токены и билдер под вашу конкретную схему Redfish и иерархию ролей в организации.

Пошаговый разбор: типобезопасный сервер Redfish 🟡

Что вы узнаете: как скомпоновать typestate билдера ответа, токены доступности источников, размерную сериализацию, агрегацию здоровья, версионирование схем и типизированную диспетчеризацию действий в сервер Redfish, который не может выдать ответ, не соответствующий схеме. Это зеркало пошагового разбора клиента из гл. 17.

Перекрёстные ссылки: гл. 02 (типизированные команды, перевёрнутые для диспетчеризации действий), гл. 04 (capability-токены для доступности источников), гл. 06 (размерные типы, сторона сериализации), гл. 07 (проверенные границы, перевёрнуто: «строй, а не сериализуй»), гл. 09 (phantom-типы, версионирование схем), гл. 11 (приём 3: #[non_exhaustive], приём 4: билдер с typestate), гл. 17 (клиентская часть)

Проблема зеркала

Глава 17 спрашивает: «Как правильно потреблять Redfish?» Эта глава задаёт зеркальный вопрос: «Как правильно отдавать Redfish?»

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

Один ответ GET /redfish/v1/Systems/1 должен объединять данные из многих источников:

flowchart LR
    subgraph Sources["Источники"]
        SMBIOS["SMBIOS<br/>Тип 1, Тип 17"]
        SDR["Датчики IPMI<br/>(SDR + показания)"]
        SEL["SEL IPMI<br/>(критические события)"]
        PCIe["Конфигурационное<br/>пространство PCIe"]
        FW["Таблица версий<br/>прошивки"]
        PWR["Регистр состояния<br/>питания"]
    end

    subgraph Server["Сервер Redfish"]
        Handler["Обработчик GET"]
        Builder["Билдер<br/>ComputerSystem"]
    end

    SMBIOS -->|"Имя, UUID, серийный номер"| Handler
    SDR -->|"Temperatures, Fans"| Handler
    SEL -->|"Эскалация здоровья"| Handler
    PCIe -->|"Связи устройств"| Handler
    FW -->|"Версия BIOS"| Handler
    PWR -->|"PowerState"| Handler
    Handler --> Builder
    Builder -->|".build()"| JSON["JSON, соответствующий<br/>схеме"]

    style JSON fill:#c8e6c9,color:#000
    style Builder fill:#e1f5fe,color:#000

В C это обработчик на 500 строк, который обращается к шести подсистемам, вручную строит дерево JSON через json_object_set() и надеется, что каждое обязательное поле заполнено. Забыли одно? Ответ нарушает схему Redfish. Перепутали единицу? Каждый клиент получает искажённую телеметрию.

// C — проблема сборки
json_t *get_computer_system(const char *id) {
    json_t *obj = json_object();
    json_object_set_new(obj, "@odata.type",
        json_string("#ComputerSystem.v1_13_0.ComputerSystem"));

    // 🐛 Забыли задать "Name": схема его требует
    // 🐛 Забыли задать "UUID": схема его требует

    smbios_type1_t *t1 = smbios_get_type1();
    if (t1) {
        json_object_set_new(obj, "Manufacturer",
            json_string(t1->manufacturer));
    }

    json_object_set_new(obj, "PowerState",
        json_string(get_power_state()));  // хотя бы это всегда доступно

    // 🐛 Показание в сырых отсчётах АЦП, а не в Цельсиях: нет типа, который это поймает
    double cpu_temp = read_sensor(SENSOR_CPU_TEMP);
    // Это число где-то попадает в ответ Thermal...
    // но ничто не связывает его с «Цельсием» на уровне типов

    // 🐛 Здоровье вычисляется вручную: забыли учесть статус БП
    json_object_set_new(obj, "Status",
        build_status("Enabled", "OK")); // должно быть "Critical": БП выходит из строя

    return obj; // не хватает 2 обязательных полей, неверное здоровье, сырые единицы
}

Четыре ошибки в одном обработчике. На стороне клиента каждая ошибка затрагивает одного клиента. На стороне сервера каждая ошибка затрагивает каждого клиента, который запрашивает этот BMC.


Раздел 1 — Typestate билдера ответа: «строй, а не сериализуй» (гл. 07, перевёрнуто)

Глава 7 учит «parse, don’t validate»: проверить входящие данные один раз и хранить доказательство в типе. Зеркальный приём на стороне сервера — «construct, don’t serialize»: строить исходящий ответ через билдер, который открывает .build() только при наличии всех обязательных полей.

use std::marker::PhantomData;

// ──── Отслеживание полей на уровне типов ────

pub struct HasField;
pub struct MissingField;

// ──── Билдер ответа ────

/// Билдер ресурса ComputerSystem Redfish.
/// Параметры типа отслеживают, какие ОБЯЗАТЕЛЬНЫЕ поля уже заданы.
/// Необязательные поля не нужно отслеживать на уровне типов.
pub struct ComputerSystemBuilder<Name, Uuid, PowerState, Status> {
    // Обязательные поля — отслеживаются на уровне типов
    name: Option<String>,
    uuid: Option<String>,
    power_state: Option<PowerStateValue>,
    status: Option<ResourceStatus>,
    // Необязательные поля — не отслеживаются (можно задать всегда)
    manufacturer: Option<String>,
    model: Option<String>,
    serial_number: Option<String>,
    bios_version: Option<String>,
    processor_summary: Option<ProcessorSummary>,
    memory_summary: Option<MemorySummary>,
    _markers: PhantomData<(Name, Uuid, PowerState, Status)>,
}

#[derive(Debug, Clone, serde::Serialize)]
pub enum PowerStateValue { On, Off, PoweringOn, PoweringOff }

#[derive(Debug, Clone, serde::Serialize)]
pub struct ResourceStatus {
    #[serde(rename = "State")]
    pub state: StatusState,
    #[serde(rename = "Health")]
    pub health: HealthValue,
    #[serde(rename = "HealthRollup", skip_serializing_if = "Option::is_none")]
    pub health_rollup: Option<HealthValue>,
}

#[derive(Debug, Clone, Copy, serde::Serialize)]
pub enum StatusState { Enabled, Disabled, Absent, StandbyOffline, Starting }

#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, serde::Serialize)]
pub enum HealthValue { OK, Warning, Critical }

#[derive(Debug, Clone, serde::Serialize)]
pub struct ProcessorSummary {
    #[serde(rename = "Count")]
    pub count: u32,
    #[serde(rename = "Status")]
    pub status: ResourceStatus,
}

#[derive(Debug, Clone, serde::Serialize)]
pub struct MemorySummary {
    #[serde(rename = "TotalSystemMemoryGiB")]
    pub total_gib: f64,
    #[serde(rename = "Status")]
    pub status: ResourceStatus,
}

// ──── Конструктор: все поля начинаются как MissingField ────

impl ComputerSystemBuilder<MissingField, MissingField, MissingField, MissingField> {
    pub fn new() -> Self {
        ComputerSystemBuilder {
            name: None, uuid: None, power_state: None, status: None,
            manufacturer: None, model: None, serial_number: None,
            bios_version: None, processor_summary: None, memory_summary: None,
            _markers: PhantomData,
        }
    }
}

// ──── Сеттеры обязательных полей — каждый переводит один параметр типа ────

impl<U, P, S> ComputerSystemBuilder<MissingField, U, P, S> {
    pub fn name(self, name: String) -> ComputerSystemBuilder<HasField, U, P, S> {
        ComputerSystemBuilder {
            name: Some(name), uuid: self.uuid,
            power_state: self.power_state, status: self.status,
            manufacturer: self.manufacturer, model: self.model,
            serial_number: self.serial_number, bios_version: self.bios_version,
            processor_summary: self.processor_summary,
            memory_summary: self.memory_summary, _markers: PhantomData,
        }
    }
}

impl<N, P, S> ComputerSystemBuilder<N, MissingField, P, S> {
    pub fn uuid(self, uuid: String) -> ComputerSystemBuilder<N, HasField, P, S> {
        ComputerSystemBuilder {
            name: self.name, uuid: Some(uuid),
            power_state: self.power_state, status: self.status,
            manufacturer: self.manufacturer, model: self.model,
            serial_number: self.serial_number, bios_version: self.bios_version,
            processor_summary: self.processor_summary,
            memory_summary: self.memory_summary, _markers: PhantomData,
        }
    }
}

impl<N, U, S> ComputerSystemBuilder<N, U, MissingField, S> {
    pub fn power_state(self, ps: PowerStateValue)
        -> ComputerSystemBuilder<N, U, HasField, S>
    {
        ComputerSystemBuilder {
            name: self.name, uuid: self.uuid,
            power_state: Some(ps), status: self.status,
            manufacturer: self.manufacturer, model: self.model,
            serial_number: self.serial_number, bios_version: self.bios_version,
            processor_summary: self.processor_summary,
            memory_summary: self.memory_summary, _markers: PhantomData,
        }
    }
}

impl<N, U, P> ComputerSystemBuilder<N, U, P, MissingField> {
    pub fn status(self, status: ResourceStatus)
        -> ComputerSystemBuilder<N, U, P, HasField>
    {
        ComputerSystemBuilder {
            name: self.name, uuid: self.uuid,
            power_state: self.power_state, status: Some(status),
            manufacturer: self.manufacturer, model: self.model,
            serial_number: self.serial_number, bios_version: self.bios_version,
            processor_summary: self.processor_summary,
            memory_summary: self.memory_summary, _markers: PhantomData,
        }
    }
}

// ──── Сеттеры необязательных полей — доступны в любом состоянии ────

impl<N, U, P, S> ComputerSystemBuilder<N, U, P, S> {
    pub fn manufacturer(mut self, m: String) -> Self {
        self.manufacturer = Some(m); self
    }
    pub fn model(mut self, m: String) -> Self {
        self.model = Some(m); self
    }
    pub fn serial_number(mut self, s: String) -> Self {
        self.serial_number = Some(s); self
    }
    pub fn bios_version(mut self, v: String) -> Self {
        self.bios_version = Some(v); self
    }
    pub fn processor_summary(mut self, ps: ProcessorSummary) -> Self {
        self.processor_summary = Some(ps); self
    }
    pub fn memory_summary(mut self, ms: MemorySummary) -> Self {
        self.memory_summary = Some(ms); self
    }
}

// ──── .build() существует ТОЛЬКО когда все обязательные поля — HasField ────

impl ComputerSystemBuilder<HasField, HasField, HasField, HasField> {
    pub fn build(self, id: &str) -> serde_json::Value {
        let mut obj = serde_json::json!({
            "@odata.id": format!("/redfish/v1/Systems/{id}"),
            "@odata.type": "#ComputerSystem.v1_13_0.ComputerSystem",
            "Id": id,
            // Type-state гарантирует, что эти значения Some — .unwrap() здесь безопасен.
            // В продакшене предпочтительнее .expect("guaranteed by type state").
            "Name": self.name.unwrap(),
            "UUID": self.uuid.unwrap(),
            "PowerState": self.power_state.unwrap(),
            "Status": self.status.unwrap(),
        });

        // Необязательные поля — включаются, только если заданы
        if let Some(m) = self.manufacturer {
            obj["Manufacturer"] = serde_json::json!(m);
        }
        if let Some(m) = self.model {
            obj["Model"] = serde_json::json!(m);
        }
        if let Some(s) = self.serial_number {
            obj["SerialNumber"] = serde_json::json!(s);
        }
        if let Some(v) = self.bios_version {
            obj["BiosVersion"] = serde_json::json!(v);
        }
        // ПРИМЕЧАНИЕ: .unwrap() при to_value() используется для краткости.
        // В продакшене ошибки сериализации следует передавать через `?`.
        if let Some(ps) = self.processor_summary {
            obj["ProcessorSummary"] = serde_json::to_value(ps).unwrap();
        }
        if let Some(ms) = self.memory_summary {
            obj["MemorySummary"] = serde_json::to_value(ms).unwrap();
        }

        obj
    }
}

//
// ── Компилятор обеспечивает полноту ──
//
// ✅ Все обязательные поля заданы — .build() доступен:
// ComputerSystemBuilder::new()
//     .name("PowerEdge R750".into())
//     .uuid("4c4c4544-...".into())
//     .power_state(PowerStateValue::On)
//     .status(ResourceStatus { ... })
//     .manufacturer("Dell".into())        // необязательное — можно включать
//     .build("1")
//
// ❌ Нет «Name» — ошибка компиляции:
// ComputerSystemBuilder::new()
//     .uuid("4c4c4544-...".into())
//     .power_state(PowerStateValue::On)
//     .status(ResourceStatus { ... })
//     .build("1")
//   ОШИБКА: метод `build` не найден для
//   `ComputerSystemBuilder<MissingField, HasField, HasField, HasField>`

Устранённый класс ошибок: ответы, не соответствующие схеме. Обработчик физически не может сериализовать ComputerSystem, не задав каждое обязательное поле. Сообщение компилятора даже подсказывает, какого поля не хватает: это видно в параметре типа, где стоит MissingField на месте Name.


Раздел 2 — Токены доступности источников (capability-токены, гл. 04, новый поворот)

В гл. 04 и гл. 17 capability-токены доказывают авторизацию: «вызывающему разрешено это делать». На стороне сервера тот же паттерн доказывает доступность: «этот источник данных успешно инициализирован».

Каждая подсистема, которую опрашивает BMC, может отказать независимо. Таблицы SMBIOS могут быть повреждены. Подсистема датчиков может ещё инициализироваться. Сканирование шины PCIe может завершиться по тайм-ауту. Кодируем каждую как токен-доказательство:

/// Доказательство, что таблицы SMBIOS успешно разобраны.
/// Создаётся только функцией инициализации SMBIOS.
pub struct SmbiosReady {
    _private: (),
}

/// Доказательство, что подсистема датчиков IPMI отвечает.
pub struct SensorsReady {
    _private: (),
}

/// Доказательство, что сканирование шины PCIe завершено.
pub struct PcieReady {
    _private: (),
}

/// Доказательство, что SEL успешно прочитан.
pub struct SelReady {
    _private: (),
}

// ──── Инициализация источников данных ────

pub struct SmbiosTables {
    pub product_name: String,
    pub manufacturer: String,
    pub serial_number: String,
    pub uuid: String,
}

pub struct SensorCache {
    pub cpu_temp: Celsius,
    pub inlet_temp: Celsius,
    pub fan_readings: Vec<(String, Rpm)>,
    pub psu_power: Vec<(String, Watts)>,
}

/// Расширенная сводка SEL — здоровье по подсистемам, выведенное из типизированных событий.
/// Строится конвейером потребителя из раздела SEL гл. 07.
/// Заменяет теряющий информацию `has_critical_events: bool` типизированной детализацией.
pub struct TypedSelSummary {
    pub total_entries: u32,
    pub processor_health: HealthValue,
    pub memory_health: HealthValue,
    pub power_health: HealthValue,
    pub thermal_health: HealthValue,
    pub fan_health: HealthValue,
    pub storage_health: HealthValue,
    pub security_health: HealthValue,
}

pub fn init_smbios() -> Option<(SmbiosReady, SmbiosTables)> {
    // Читаем точку входа SMBIOS, разбираем таблицы...
    // Возвращает None, если таблицы отсутствуют или повреждены
    Some((
        SmbiosReady { _private: () },
        SmbiosTables {
            product_name: "PowerEdge R750".into(),
            manufacturer: "Dell Inc.".into(),
            serial_number: "SVC1234567".into(),
            uuid: "4c4c4544-004d-5610-804c-b2c04f435031".into(),
        },
    ))
}

pub fn init_sensors() -> Option<(SensorsReady, SensorCache)> {
    // Инициализируем репозиторий SDR, читаем все датчики...
    // Возвращает None, если подсистема IPMI не отвечает
    Some((
        SensorsReady { _private: () },
        SensorCache {
            cpu_temp: Celsius(68.0),
            inlet_temp: Celsius(24.0),
            fan_readings: vec![
                ("Fan1".into(), Rpm(8400)),
                ("Fan2".into(), Rpm(8200)),
            ],
            psu_power: vec![
                ("PSU1".into(), Watts(285.0)),
                ("PSU2".into(), Watts(290.0)),
            ],
        },
    ))
}

pub fn init_sel() -> Option<(SelReady, TypedSelSummary)> {
    // В продакшене: читаем записи SEL, разбираем через TryFrom из гл. 07,
    // классифицируем через classify_event_health(), агрегируем через summarize_sel().
    Some((
        SelReady { _private: () },
        TypedSelSummary {
            total_entries: 42,
            processor_health: HealthValue::OK,
            memory_health: HealthValue::OK,
            power_health: HealthValue::OK,
            thermal_health: HealthValue::OK,
            fan_health: HealthValue::OK,
            storage_health: HealthValue::OK,
            security_health: HealthValue::OK,
        },
    ))
}

Теперь функции, которые заполняют поля билдера из источника данных, требуют соответствующий токен-доказательство:

/// Заполняет поля из SMBIOS. Требует доказательство, что SMBIOS доступен.
fn populate_from_smbios<P, S>(
    builder: ComputerSystemBuilder<MissingField, MissingField, P, S>,
    _proof: &SmbiosReady,
    tables: &SmbiosTables,
) -> ComputerSystemBuilder<HasField, HasField, P, S> {
    builder
        .name(tables.product_name.clone())
        .uuid(tables.uuid.clone())
        .manufacturer(tables.manufacturer.clone())
        .serial_number(tables.serial_number.clone())
}

/// Запасной вариант, когда SMBIOS недоступен: обязательные поля заполняются
/// безопасными значениями по умолчанию.
fn populate_smbios_fallback<P, S>(
    builder: ComputerSystemBuilder<MissingField, MissingField, P, S>,
) -> ComputerSystemBuilder<HasField, HasField, P, S> {
    builder
        .name("Неизвестная система".into())
        .uuid("00000000-0000-0000-0000-000000000000".into())
}

Обработчик выбирает путь в зависимости от того, какие токены доступны:

fn build_computer_system(
    smbios: &Option<(SmbiosReady, SmbiosTables)>,
    power_state: PowerStateValue,
    health: ResourceStatus,
) -> serde_json::Value {
    let builder = ComputerSystemBuilder::new()
        .power_state(power_state)
        .status(health);

    let builder = match smbios {
        Some((proof, tables)) => populate_from_smbios(builder, proof, tables),
        None => populate_smbios_fallback(builder),
    };

    // Оба пути дают HasField для Name и UUID.
    // .build() доступен в любом случае.
    builder.build("1")
}

Устранённый класс ошибок: обращение к подсистеме, которая не смогла инициализироваться. Если SMBIOS не разобрался, токена SmbiosReady у вас нет: компилятор заставляет пройти через запасной путь. Никакой проверки if (smbios != NULL) во время выполнения, которую можно забыть.

Комбинирование токенов источников с capability-миксинами (гл. 08)

Когда нужно обслуживать несколько типов ресурсов Redfish (ComputerSystem, Chassis, Manager, Thermal, Power), логика заполнения из источников повторяется в обработчиках. Паттерн миксинов из гл. 08 устраняет это дублирование. Объявляете, какие источники есть у обработчика, и blanket impl сами предоставляют методы заполнения:

/// ── Трейты-ингредиенты (гл. 08) для источников данных ──

pub trait HasSmbios {
    fn smbios(&self) -> &(SmbiosReady, SmbiosTables);
}

pub trait HasSensors {
    fn sensors(&self) -> &(SensorsReady, SensorCache);
}

pub trait HasSel {
    fn sel(&self) -> &(SelReady, TypedSelSummary);
}

/// ── Миксин: любой обработчик с SMBIOS и датчиками получает заполнение идентификации ──

pub trait IdentityMixin: HasSmbios {
    fn populate_identity<P, S>(
        &self,
        builder: ComputerSystemBuilder<MissingField, MissingField, P, S>,
    ) -> ComputerSystemBuilder<HasField, HasField, P, S> {
        let (_, tables) = self.smbios();
        builder
            .name(tables.product_name.clone())
            .uuid(tables.uuid.clone())
            .manufacturer(tables.manufacturer.clone())
            .serial_number(tables.serial_number.clone())
    }
}

/// Автоматическая реализация для любого типа с возможностью SMBIOS.
impl<T: HasSmbios> IdentityMixin for T {}

/// ── Миксин: любой обработчик с датчиками и SEL получает агрегацию здоровья ──

pub trait HealthMixin: HasSensors + HasSel {
    fn compute_health(&self) -> ResourceStatus {
        let (_, cache) = self.sensors();
        let (_, sel_summary) = self.sel();
        compute_system_health(
            Some(&(SensorsReady { _private: () }, cache.clone())).as_ref(),
            Some(&(SelReady { _private: () }, sel_summary.clone())).as_ref(),
        )
    }
}

impl<T: HasSensors + HasSel> HealthMixin for T {}

/// ── Конкретный обработчик владеет доступными источниками ──

struct FullPlatformHandler {
    smbios: (SmbiosReady, SmbiosTables),
    sensors: (SensorsReady, SensorCache),
    sel: (SelReady, TypedSelSummary),
}

impl HasSmbios  for FullPlatformHandler {
    fn smbios(&self) -> &(SmbiosReady, SmbiosTables) { &self.smbios }
}
impl HasSensors for FullPlatformHandler {
    fn sensors(&self) -> &(SensorsReady, SensorCache) { &self.sensors }
}
impl HasSel     for FullPlatformHandler {
    fn sel(&self) -> &(SelReady, TypedSelSummary) { &self.sel }
}

// FullPlatformHandler автоматически получает:
//   IdentityMixin::populate_identity()   (через HasSmbios)
//   HealthMixin::compute_health()        (через HasSensors + HasSel)
//
// SensorsOnlyHandler, который реализует HasSensors, но НЕ HasSel,
// получит IdentityMixin (если есть SMBIOS), но НЕ HealthMixin.
// Вызов .compute_health() у него → ошибка компиляции.

Это напрямую повторяет паттерн BaseBoardController из гл. 08: трейты-ингредиенты объявляют, что у вас есть, трейты-миксины предоставляют поведение через blanket impl, а компилятор открывает каждый миксин только при выполнении его предусловий. Добавление нового источника данных (например, HasNvme) и миксина (например, StorageMixin: HasNvme + HasSel) даёт агрегацию здоровья для хранилища каждому обработчику, у которого есть оба, автоматически.


Раздел 3 — Размерные типы на границе сериализации (гл. 06)

На стороне клиента (гл. 17, §4) размерные типы не дают прочитать °C как об/мин. На стороне сервера они не дают записать об/мин в JSON-поле в градусах Цельсия. Это, пожалуй, опаснее: неверное значение на сервере доходит до каждого клиента.

use serde::Serialize;

// ──── Размерные типы из гл. 06, с Serialize ────

#[derive(Debug, Clone, Copy, PartialEq, PartialOrd, Serialize)]
pub struct Celsius(pub f64);

#[derive(Debug, Clone, Copy, PartialEq, PartialOrd, Serialize)]
pub struct Rpm(pub u32);

#[derive(Debug, Clone, Copy, PartialEq, PartialOrd, Serialize)]
pub struct Watts(pub f64);

// ──── Элементы ответа Redfish Thermal ────
// Типы полей задают, какая единица относится к какому JSON-свойству.

#[derive(Serialize)]
#[serde(rename_all = "PascalCase")]
pub struct TemperatureMember {
    pub member_id: String,
    pub name: String,
    pub reading_celsius: Celsius,           // ← обязан быть Celsius
    #[serde(skip_serializing_if = "Option::is_none")]
    pub upper_threshold_critical: Option<Celsius>,
    #[serde(skip_serializing_if = "Option::is_none")]
    pub upper_threshold_fatal: Option<Celsius>,
    pub status: ResourceStatus,
}

#[derive(Serialize)]
#[serde(rename_all = "PascalCase")]
pub struct FanMember {
    pub member_id: String,
    pub name: String,
    pub reading: Rpm,                       // ← обязан быть Rpm
    pub reading_units: &'static str,        // всегда "RPM"
    pub status: ResourceStatus,
}

#[derive(Serialize)]
#[serde(rename_all = "PascalCase")]
pub struct PowerControlMember {
    pub member_id: String,
    pub name: String,
    pub power_consumed_watts: Watts,        // ← обязан быть Watts
    #[serde(skip_serializing_if = "Option::is_none")]
    pub power_capacity_watts: Option<Watts>,
    pub status: ResourceStatus,
}

// ──── Построение ответа Thermal из кэша датчиков ────

fn build_thermal_response(
    _proof: &SensorsReady,
    cache: &SensorCache,
) -> serde_json::Value {
    let temps = vec![
        TemperatureMember {
            member_id: "0".into(),
            name: "Температура ЦП".into(),
            reading_celsius: cache.cpu_temp,     // Celsius → Celsius ✅
            upper_threshold_critical: Some(Celsius(95.0)),
            upper_threshold_fatal: Some(Celsius(105.0)),
            status: ResourceStatus {
                state: StatusState::Enabled,
                health: if cache.cpu_temp < Celsius(95.0) {
                    HealthValue::OK
                } else {
                    HealthValue::Critical
                },
                health_rollup: None,
            },
        },
        TemperatureMember {
            member_id: "1".into(),
            name: "Температура на входе".into(),
            reading_celsius: cache.inlet_temp,   // Celsius → Celsius ✅
            upper_threshold_critical: Some(Celsius(42.0)),
            upper_threshold_fatal: None,
            status: ResourceStatus {
                state: StatusState::Enabled,
                health: HealthValue::OK,
                health_rollup: None,
            },
        },

        // ❌ Ошибка компиляции — нельзя положить Rpm в поле Celsius:
        // TemperatureMember {
        //     reading_celsius: cache.fan_readings[0].1,  // Rpm ≠ Celsius
        //     ...
        // }
    ];

    let fans: Vec<FanMember> = cache.fan_readings.iter().enumerate().map(|(i, (name, rpm))| {
        FanMember {
            member_id: i.to_string(),
            name: name.clone(),
            reading: *rpm,                       // Rpm → Rpm ✅
            reading_units: "RPM",
            status: ResourceStatus {
                state: StatusState::Enabled,
                health: if *rpm > Rpm(1000) { HealthValue::OK } else { HealthValue::Critical },
                health_rollup: None,
            },
        }
    }).collect();

    serde_json::json!({
        "@odata.type": "#Thermal.v1_7_0.Thermal",
        "Temperatures": temps,
        "Fans": fans,
    })
}

Устранённый класс ошибок: путаница единиц при сериализации. Схема Redfish говорит, что ReadingCelsius — в °C. Система типов Rust говорит, что reading_celsius обязан быть Celsius. Если разработчик по ошибке передаст Rpm(8400) или Watts(285.0), компилятор поймает это до того, как значение попадёт в JSON.


Раздел 4 — Агрегация здоровья как типизированная свёртка

Status.Health в Redfish — это агрегат: худшее состояние среди всех подкомпонентов. В C это обычно серия проверок if, в которой неизбежно пропускается какой-нибудь источник. С типизированными enum и Ord агрегация становится однострочной свёрткой, а компилятор гарантирует, что каждый источник участвует:

/// Агрегирует здоровье из нескольких источников.
/// Ord для HealthValue: OK < Warning < Critical.
/// Возвращает худшее (максимальное) значение.
fn rollup(sources: &[HealthValue]) -> HealthValue {
    sources.iter().copied().max().unwrap_or(HealthValue::OK)
}

/// Вычисляет здоровье системы по всем подкомпонентам.
/// Принимает явные ссылки на каждый источник: вызывающий обязан передать ВСЕ.
fn compute_system_health(
    sensors: Option<&(SensorsReady, SensorCache)>,
    sel: Option<&(SelReady, TypedSelSummary)>,
) -> ResourceStatus {
    let mut inputs = Vec::new();

    // ── Текущие показания датчиков ──
    if let Some((_proof, cache)) = sensors {
        // Здоровье по температуре (размерное: сравнение Celsius)
        if cache.cpu_temp > Celsius(95.0) {
            inputs.push(HealthValue::Critical);
        } else if cache.cpu_temp > Celsius(85.0) {
            inputs.push(HealthValue::Warning);
        } else {
            inputs.push(HealthValue::OK);
        }

        // Здоровье вентиляторов (размерное: сравнение Rpm)
        for (_name, rpm) in &cache.fan_readings {
            if *rpm < Rpm(500) {
                inputs.push(HealthValue::Critical);
            } else if *rpm < Rpm(1000) {
                inputs.push(HealthValue::Warning);
            } else {
                inputs.push(HealthValue::OK);
            }
        }

        // Здоровье БП (размерное: сравнение Watts)
        for (_name, watts) in &cache.psu_power {
            if *watts > Watts(800.0) {
                inputs.push(HealthValue::Critical);
            } else {
                inputs.push(HealthValue::OK);
            }
        }
    }

    // ── Здоровье по подсистемам из SEL (из TypedSelSummary гл. 07) ──
    // Здоровье каждой подсистемы выведено исчерпывающим сопоставлением по
    // каждому типу датчика и варианту события. Информация не потеряна.
    if let Some((_proof, sel_summary)) = sel {
        inputs.push(sel_summary.processor_health);
        inputs.push(sel_summary.memory_health);
        inputs.push(sel_summary.power_health);
        inputs.push(sel_summary.thermal_health);
        inputs.push(sel_summary.fan_health);
        inputs.push(sel_summary.storage_health);
        inputs.push(sel_summary.security_health);
    }

    let health = rollup(&inputs);

    ResourceStatus {
        state: StatusState::Enabled,
        health,
        health_rollup: Some(health),
    }
}

Устранённый класс ошибок: неполная агрегация здоровья. В C забытый учёт статуса БП в расчёте здоровья — тихая ошибка: система сообщает «OK», пока БП выходит из строя. Здесь compute_system_health принимает явные ссылки на каждый источник данных. Вклад SEL больше не bool, который теряет информацию: это семь полей HealthValue по подсистемам, выведенных исчерпывающим сопоставлением в конвейере потребителя из гл. 07. Добавление нового типа датчика SEL вынуждает классификатор его обработать; добавление нового поля подсистемы вынуждает агрегацию его учесть.


Раздел 5 — Версионирование схем с phantom-типами (гл. 09)

Если BMC объявляет ComputerSystem.v1_13_0, ответ обязан содержать свойства, введённые в этой версии схемы (LastResetTime, BootProgress). Объявить v1.13 без этих полей — провал Redfish Interop Validator. Phantom-маркеры версий превращают это в контракт на этапе компиляции:

use std::marker::PhantomData;

// ──── Маркеры версий схемы ────

pub struct V1_5;
pub struct V1_13;

// ──── Ответ с учётом версии ────

pub struct ComputerSystemResponse<V> {
    pub base: ComputerSystemBase,
    _version: PhantomData<V>,
}

pub struct ComputerSystemBase {
    pub id: String,
    pub name: String,
    pub uuid: String,
    pub power_state: PowerStateValue,
    pub status: ResourceStatus,
    pub manufacturer: Option<String>,
    pub serial_number: Option<String>,
    pub bios_version: Option<String>,
}

// Методы, доступные во ВСЕХ версиях:
impl<V> ComputerSystemResponse<V> {
    pub fn base_json(&self) -> serde_json::Value {
        serde_json::json!({
            "Id": self.base.id,
            "Name": self.base.name,
            "UUID": self.base.uuid,
            "PowerState": self.base.power_state,
            "Status": self.base.status,
        })
    }
}

// ──── Поля, специфичные для v1.13 ────

/// Дата и время последнего сброса системы.
pub struct LastResetTime(pub String);

/// Информация о ходе загрузки.
pub struct BootProgress {
    pub last_state: String,
    pub last_state_time: String,
}

impl ComputerSystemResponse<V1_13> {
    /// LastResetTime — ОБЯЗАТЕЛЕН в v1.13 и выше.
    /// Этот метод есть только у V1_13. Если BMC объявляет v1.13,
    /// а обработчик не вызывает его, поле будет отсутствовать.
    pub fn last_reset_time(&self) -> LastResetTime {
        // Читаем из RTC или регистра метки времени загрузки
        LastResetTime("2026-03-16T08:30:00Z".to_string())
    }

    /// BootProgress — ОБЯЗАТЕЛЕН в v1.13 и выше.
    pub fn boot_progress(&self) -> BootProgress {
        BootProgress {
            last_state: "OSRunning".to_string(),
            last_state_time: "2026-03-16T08:32:00Z".to_string(),
        }
    }

    /// Формирует полный JSON-ответ v1.13, включая поля, специфичные для версии.
    pub fn to_json(&self) -> serde_json::Value {
        let mut obj = self.base_json();
        obj["@odata.type"] =
            serde_json::json!("#ComputerSystem.v1_13_0.ComputerSystem");

        let reset_time = self.last_reset_time();
        obj["LastResetTime"] = serde_json::json!(reset_time.0);

        let boot = self.boot_progress();
        obj["BootProgress"] = serde_json::json!({
            "LastState": boot.last_state,
            "LastStateTime": boot.last_state_time,
        });

        obj
    }
}

impl ComputerSystemResponse<V1_5> {
    /// JSON версии v1.5: без LastResetTime и BootProgress.
    pub fn to_json(&self) -> serde_json::Value {
        let mut obj = self.base_json();
        obj["@odata.type"] =
            serde_json::json!("#ComputerSystem.v1_5_0.ComputerSystem");
        obj
    }

    // last_reset_time() здесь не существует.
    // Вызов → ошибка компиляции:
    //   let resp: ComputerSystemResponse<V1_5> = ...;
    //   resp.last_reset_time();
    //   ❌ ОШИБКА: метод `last_reset_time` не найден для
    //            `ComputerSystemResponse<V1_5>`
}

Устранённый класс ошибок: несоответствие версии схемы. Если BMC настроен объявлять v1.13, используйте ComputerSystemResponse<V1_13>, и компилятор гарантирует, что каждое поле, обязательное для v1.13, будет сформировано. Понижаете до v1.5? Смените параметр типа: методы v1.13 исчезнут, и в ответ не просочатся лишние поля.


Раздел 6 — Типизированная диспетчеризация действий (гл. 02, перевёрнуто)

В гл. 02 паттерн типизированной команды связывает Request → Response на стороне клиента. На стороне сервера тот же паттерн проверяет входящие полезные нагрузки действий и диспетчеризует их безопасно по типам: обратное направление.

use serde::Deserialize;

// ──── Трейт действия (зеркало трейта IpmiCmd из гл. 02) ────

/// Действие Redfish: фреймворк десериализует Params из тела POST,
/// затем вызывает execute(). Если JSON не соответствует Params,
/// десериализация завершается ошибкой: execute() никогда не вызывается с некорректными данными.
pub trait RedfishAction {
    /// Ожидаемая структура тела JSON.
    type Params: serde::de::DeserializeOwned;
    /// Результат выполнения действия.
    type Result: serde::Serialize;

    fn execute(&self, params: Self::Params) -> Result<Self::Result, RedfishError>;
}

#[derive(Debug)]
pub enum RedfishError {
    InvalidPayload(String),
    ActionFailed(String),
}

// ──── ComputerSystem.Reset ────

pub struct ComputerSystemReset;

#[derive(Debug, Deserialize)]
pub enum ResetType {
    On,
    ForceOff,
    GracefulShutdown,
    GracefulRestart,
    ForceRestart,
    ForceOn,
    PushPowerButton,
}

#[derive(Debug, Deserialize)]
#[serde(rename_all = "PascalCase")]
pub struct ResetParams {
    pub reset_type: ResetType,
}

impl RedfishAction for ComputerSystemReset {
    type Params = ResetParams;
    type Result = ();

    fn execute(&self, params: ResetParams) -> Result<(), RedfishError> {
        match params.reset_type {
            ResetType::GracefulShutdown => {
                // Отправляем хосту ACPI-команду выключения
                println!("Инициирован ACPI-выключение");
                Ok(())
            }
            ResetType::ForceOff => {
                // Выставляем сигнал отключения питания хоста
                println!("Принудительное выключение питания");
                Ok(())
            }
            ResetType::On | ResetType::ForceOn => {
                println!("Включение питания");
                Ok(())
            }
            ResetType::GracefulRestart => {
                println!("Перезапуск ACPI");
                Ok(())
            }
            ResetType::ForceRestart => {
                println!("Принудительный перезапуск");
                Ok(())
            }
            ResetType::PushPowerButton => {
                println!("Имитация нажатия кнопки питания");
                Ok(())
            }
            // Исчерпывающе — компилятор поймает пропущенные варианты
        }
    }
}

// ──── Manager.ResetToDefaults ────

pub struct ManagerResetToDefaults;

#[derive(Debug, Deserialize)]
pub enum ResetToDefaultsType {
    ResetAll,
    PreserveNetworkAndUsers,
    PreserveNetwork,
}

#[derive(Debug, Deserialize)]
#[serde(rename_all = "PascalCase")]
pub struct ResetToDefaultsParams {
    pub reset_to_defaults_type: ResetToDefaultsType,
}

impl RedfishAction for ManagerResetToDefaults {
    type Params = ResetToDefaultsParams;
    type Result = ();

    fn execute(&self, params: ResetToDefaultsParams) -> Result<(), RedfishError> {
        match params.reset_to_defaults_type {
            ResetToDefaultsType::ResetAll => {
                println!("Полный сброс к заводским настройкам");
                Ok(())
            }
            ResetToDefaultsType::PreserveNetworkAndUsers => {
                println!("Сброс с сохранением сети и пользователей");
                Ok(())
            }
            ResetToDefaultsType::PreserveNetwork => {
                println!("Сброс с сохранением сетевой конфигурации");
                Ok(())
            }
        }
    }
}

// ──── Общий диспетчер действий ────

fn dispatch_action<A: RedfishAction>(
    action: &A,
    raw_body: &str,
) -> Result<A::Result, RedfishError> {
    // Десериализация проверяет структуру полезной нагрузки.
    // Если JSON не соответствует A::Params, это завершается ошибкой,
    // и execute() не вызывается.
    let params: A::Params = serde_json::from_str(raw_body)
        .map_err(|e| RedfishError::InvalidPayload(e.to_string()))?;

    action.execute(params)
}

// ── Использование ──

fn handle_reset_action(body: &str) -> Result<(), RedfishError> {
    // Типобезопасно: ResetParams проверяется serde до вызова execute()
    dispatch_action(&ComputerSystemReset, body)?;
    Ok(())

    // Некорректный JSON: {"ResetType": "Explode"}
    // → ошибка serde: "unknown variant `Explode`"
    // → execute() не вызывается

    // Отсутствует поле: {}
    // → ошибка serde: "missing field `ResetType`"
    // → execute() не вызывается
}

Устранённые классы ошибок:

  • Некорректная полезная нагрузка действия: serde отвергает неизвестные варианты enum и отсутствующие поля до вызова execute(). Никаких ручных цепочек if (body["ResetType"] == ...).
  • Необработанный вариант: match params.reset_type исчерпывающий: добавление нового ResetType вынуждает обновить каждый обработчик действия.
  • Путаница типов: ComputerSystemReset ожидает ResetParams, а ManagerResetToDefaults ожидает ResetToDefaultsParams. Система трейтов не даёт передать параметры одного действия обработчику другого.

Раздел 7 — Собираем всё вместе: обработчик GET

Вот полный обработчик, который соединяет все шесть разделов в один ответ, соответствующий схеме:

/// Полный обработчик GET /redfish/v1/Systems/1.
///
/// Каждое обязательное поле обеспечивается typestate билдера.
/// Каждый источник данных открывается токенами доступности.
/// Каждая единица измерения закреплена за своим размерным типом.
/// Каждый входной сигнал здоровья попадает в типизированную агрегацию.
fn handle_get_computer_system(
    smbios: &Option<(SmbiosReady, SmbiosTables)>,
    sensors: &Option<(SensorsReady, SensorCache)>,
    sel: &Option<(SelReady, TypedSelSummary)>,
    power_state: PowerStateValue,
    bios_version: Option<String>,
) -> serde_json::Value {
    // ── 1. Агрегация здоровья (раздел 4) ──
    // Сворачиваем здоровье датчиков и SEL в один типизированный статус
    let health = compute_system_health(
        sensors.as_ref(),
        sel.as_ref(),
    );

    // ── 2. Typestate билдера (раздел 1) ──
    let builder = ComputerSystemBuilder::new()
        .power_state(power_state)
        .status(health);

    // ── 3. Токены доступности источников (раздел 2) ──
    let builder = match smbios {
        Some((proof, tables)) => {
            // SMBIOS доступен — заполняем из аппаратуры
            populate_from_smbios(builder, proof, tables)
        }
        None => {
            // SMBIOS недоступен — безопасные значения по умолчанию
            populate_smbios_fallback(builder)
        }
    };

    // ── 4. Необязательное дополнение из датчиков (раздел 3) ──
    let builder = if let Some((_proof, cache)) = sensors {
        builder
            .processor_summary(ProcessorSummary {
                count: 2,
                status: ResourceStatus {
                    state: StatusState::Enabled,
                    health: if cache.cpu_temp < Celsius(95.0) {
                        HealthValue::OK
                    } else {
                        HealthValue::Critical
                    },
                    health_rollup: None,
                },
            })
    } else {
        builder
    };

    let builder = match bios_version {
        Some(v) => builder.bios_version(v),
        None => builder,
    };

    // ── 5. Сборка (раздел 1) ──
    // .build() доступен, потому что оба пути (SMBIOS есть / нет)
    // дают HasField для Name и UUID. Компилятор это проверил.
    builder.build("1")
}

// ──── Запуск сервера ────

fn main() {
    // Инициализируем все источники данных: каждый возвращает токен доступности
    let smbios = init_smbios();
    let sensors = init_sensors();
    let sel = init_sel();

    // Имитируем вызов обработчика
    let response = handle_get_computer_system(
        &smbios,
        &sensors,
        &sel,
        PowerStateValue::On,
        Some("2.10.1".into()),
    );

    // ПРИМЕЧАНИЕ: .unwrap() используется для краткости — в продакшене обрабатывайте ошибки.
    println!("{}", serde_json::to_string_pretty(&response).unwrap());
}

Ожидаемый вывод:

{
  "@odata.id": "/redfish/v1/Systems/1",
  "@odata.type": "#ComputerSystem.v1_13_0.ComputerSystem",
  "Id": "1",
  "Name": "PowerEdge R750",
  "UUID": "4c4c4544-004d-5610-804c-b2c04f435031",
  "PowerState": "On",
  "Status": {
    "State": "Enabled",
    "Health": "OK",
    "HealthRollup": "OK"
  },
  "Manufacturer": "Dell Inc.",
  "SerialNumber": "SVC1234567",
  "BiosVersion": "2.10.1",
  "ProcessorSummary": {
    "Count": 2,
    "Status": {
      "State": "Enabled",
      "Health": "OK"
    }
  }
}

Что доказывает компилятор (сторона сервера)

№Класс ошибкиКак это предотвращаетсяПаттерн (раздел)
1Отсутствует обязательное поле в ответе.build() требует, чтобы все маркеры типа были HasFieldTypestate билдера (§1)
2Обращение к неисправной подсистемеТокены доступности источников открывают доступ к даннымCapability-токены (§2)
3Нет запасного пути для недоступного источникаОбе ветки match (есть / нет) обязаны давать HasFieldTypestate + исчерпывающее match (§2)
4Неверная единица в поле JSONreading_celsius: Celsius ≠ Rpm ≠ WattsРазмерные типы (§3)
5Неполная агрегация здоровьяcompute_system_health принимает явные ссылки на источники; SEL даёт HealthValue по подсистемам через TypedSelSummary гл. 07Типизированная сигнатура функции + исчерпывающее сопоставление (§4)
6Несоответствие версии схемыComputerSystemResponse<V1_13> имеет last_reset_time(), у V1_5 его нетPhantom-типы (§5)
7Принимается некорректная полезная нагрузка действияserde отвергает неизвестные и отсутствующие поля до execute()Типизированная диспетчеризация действий (§6)
8Отсутствует обработка варианта действияmatch params.reset_type исчерпывающийИсчерпываемость enum (§6)
9Параметры не того действия переданы обработчикуRedfishAction::Params — ассоциированный типТипизированные команды, перевёрнутые (§6)

Суммарные накладные расходы во время выполнения: ноль. Маркеры билдера, токены доступности, phantom-типы версий и размерные newtype исчезают при компиляции. Получаемый JSON идентичен версии на C, написанной вручную, за вычетом девяти классов ошибок.


Зеркало: карта паттернов клиента и сервера

АспектКлиент (гл. 17)Сервер (эта глава)
Направление границыВходящая: JSON → типизированные значенияИсходящая: типизированные значения → JSON
Основной принцип«Parse, don’t validate»«Construct, don’t serialize»
Полнота полейTryFrom проверяет наличие обязательных полейTypestate билдера открывает .build() при обязательных полях
Безопасность единицCelsius ≠ Rpm при чтенииCelsius ≠ Rpm при записи
Привилегии / доступностьCapability-токены открывают запросыТокены доступности открывают доступ к источникам данных
Источники данныхОдин источник (BMC)Несколько источников (SMBIOS, датчики, SEL, PCIe, …)
Версия схемыPhantom-типы не дают обращаться к неподдерживаемым полямPhantom-типы требуют заполнения полей, обязательных для версии
ДействияКлиент отправляет типизированный POST действияСервер проверяет и диспетчеризует через трейт RedfishAction
ЗдоровьеЧитает и доверяет Status.HealthВычисляет Status.Health через типизированную агрегацию
Распространение сбояОдин плохой разбор → одна ошибка клиентаОдна плохая сериализация → каждый клиент видит неверные данные

Две главы образуют полную картину. Гл. 17: «Каждый ответ, который я потребляю, проверен типами». Эта глава: «Каждый ответ, который я отдаю, проверен типами». Одни и те же паттерны работают в обоих направлениях: система типов не знает и не заботится о том, с какой стороны провода вы находитесь.

Ключевые выводы

  1. «Construct, don’t serialize» — серверное зеркало «parse, don’t validate»: используйте typestate билдера, чтобы .build() существовал только тогда, когда заданы все обязательные поля.
  2. Токены доступности источников доказывают инициализацию: тот же паттерн capability-токена из гл. 04, переиспользованный, чтобы доказать готовность источника данных.
  3. Размерные типы защищают и производителей, и потребителей: положить Rpm в поле ReadingCelsius — это ошибка компиляции, а не баг, о котором сообщит заказчик.
  4. Агрегация здоровья — это типизированная свёртка: Ord для HealthValue плюс явные ссылки на источники означают, что компилятор поймает «забыли учесть статус БП».
  5. Версионирование схемы на уровне типов: параметры phantom-типов заставляют поля, специфичные для версии, появляться и исчезать на этапе компиляции.
  6. Диспетчеризация действий — обратная сторона гл. 02: serde десериализует полезную нагрузку в типизированную структуру Params, а исчерпывающее сопоставление по вариантам enum означает, что добавление нового ResetType вынуждает обновить каждый обработчик.
  7. Ошибки на стороне сервера распространяются на каждого клиента: поэтому корректность на этапе компиляции со стороны производителя ещё важнее, чем со стороны потребителя.

Четырнадцать проверенных на практике приёмов 🟡

Что вы узнаете: четырнадцать более мелких техник корректности по построению, от устранения sentinel-значений и sealed-трейтов до сессионных типов, Pin, RAII и #[must_use]. Каждая из них устраняет конкретный класс ошибок почти без затрат.

Перекрёстные ссылки: гл. 02 (sealed-трейты расширяют гл. 02), гл. 05 (билдер с typestate расширяет гл. 05), гл. 07 (FromStr расширяет гл. 07)

Четырнадцать проверенных на практике приёмов

Восемь основных паттернов (гл. 02–09) покрывают основные техники корректности по построению. Эта глава собирает четырнадцать более мелких, но ценных приёмов, которые регулярно встречаются в продакшн-коде на Rust. Каждый из них устраняет конкретный класс ошибок почти без затрат.

Приём 1 — Sentinel → Option на границе

Аппаратные протоколы полны sentinel-значений: IPMI использует 0xFF для «датчик отсутствует», PCI использует 0xFFFF для «нет устройства», а SMBIOS использует 0x00 для «неизвестно». Если протаскивать такие значения через код как обычные целые числа, каждый потребитель должен помнить о проверке магического значения. Если хотя бы одно сравнение забыто, получаем призрачное показание 255 °C или ложное совпадение ID производителя.

Правило: преобразуйте sentinel-значения в Option на самой первой границе разбора и преобразуйте их обратно в sentinel только на границе сериализации.

Антипаттерн (из pcie_tree/src/lspci.rs)

// Sentinel хранится внутри — каждое сравнение должно помнить о нём
let mut current_vendor_id: u16 = 0xFFFF;
let mut current_device_id: u16 = 0xFFFF;

// ... позже разбор молча завершается ошибкой ...
current_vendor_id = u16::from_str_radix(hex, 16)
    .unwrap_or(0xFFFF);  // sentinel скрывает ошибку

Каждая функция, которая получает current_vendor_id, должна знать, что 0xFFFF — особое значение. Если кто-то напишет if vendor_id == target_id и не проверит сначала 0xFFFF, отсутствующее устройство тихо совпадёт, если и цель случайно разобрана из некорректных входных данных как 0xFFFF.

Правильный паттерн (из nic_sel/src/events.rs)

pub struct ThermalEvent {
    pub record_id: u16,
    pub temperature: Option<u8>,  // None, если датчик сообщает 0xFF
}

impl ThermalEvent {
    pub fn from_raw(record_id: u16, raw_temp: u8) -> Self {
        ThermalEvent {
            record_id,
            temperature: if raw_temp != 0xFF {
                Some(raw_temp)
            } else {
                None
            },
        }
    }
}

Теперь каждый потребитель обязан обработать случай None, и компилятор это обеспечивает:

// Безопасно: компилятор гарантирует обработку отсутствующих температур
fn is_overtemp(temp: Option<u8>, threshold: u8) -> bool {
    temp.map_or(false, |t| t > threshold)
}

// Забыть обработать None — ошибка компиляции:
// fn bad_check(temp: Option<u8>, threshold: u8) -> bool {
//     temp > threshold  // ОШИБКА: нельзя сравнить Option<u8> с u8
// }

Практический эффект

inventory/src/events.rs использует тот же паттерн для тревог о температуре GPU:

temperature: if data[1] != 0xFF {
    Some(data[1] as i8)
} else {
    None
},

Рефакторинг для pcie_tree/src/lspci.rs несложен: замените current_vendor_id: u16 на current_vendor_id: Option<u16>, замените 0xFFFF на None и позвольте компилятору найти каждое место, которое нужно обновить.

ДоПосле
let mut vendor_id: u16 = 0xFFFFlet mut vendor_id: Option<u16> = None
.unwrap_or(0xFFFF).ok() (уже возвращает Option)
if vendor_id != 0xFFFF { ... }if let Some(vid) = vendor_id { ... }
Сериализация: vendor_idvendor_id.unwrap_or(0xFFFF)

Приём 2 — Sealed-трейты

Глава 2 ввела IpmiCmd с ассоциированным типом, который связывает каждую команду с её ответом. Но есть лазейка: если любой код может реализовать IpmiCmd, кто-то может написать MaliciousCmd, у которого parse_response возвращает неверный тип или паникует. Безопасность типов всей системы держится на том, что каждая реализация корректна.

Sealed-трейт закрывает эту лазейку. Идея проста: сделать так, чтобы трейт требовал приватный супертрейт, который может реализовать только ваш крейт.

// — Приватный модуль: не экспортируется из крейта —
mod private {
    pub trait Sealed {}
}

// — Публичный трейт: требует Sealed, который внешний код не может реализовать —
pub trait IpmiCmd: private::Sealed {
    type Response;
    fn net_fn(&self) -> u8;
    fn cmd_byte(&self) -> u8;
    fn payload(&self) -> Vec<u8>;
    fn parse_response(&self, raw: &[u8]) -> io::Result<Self::Response>;
}

Внутри крейта вы реализуете Sealed для каждого одобренного типа команды:

pub struct ReadTemp { pub sensor_id: u8 }
impl private::Sealed for ReadTemp {}

impl IpmiCmd for ReadTemp {
    type Response = Celsius;
    fn net_fn(&self) -> u8 { 0x04 }
    fn cmd_byte(&self) -> u8 { 0x2D }
    fn payload(&self) -> Vec<u8> { vec![self.sensor_id] }
    fn parse_response(&self, raw: &[u8]) -> io::Result<Celsius> {
        if raw.is_empty() { return Err(io::Error::new(io::ErrorKind::InvalidData, "пустой ответ")); }
        Ok(Celsius(raw[0] as f64))
    }
}

Внешний код видит IpmiCmd и может вызывать execute(), но не может его реализовать:

// В другом крейте:
struct EvilCmd;
// impl private::Sealed for EvilCmd {}  // ОШИБКА: модуль `private` приватный
// impl IpmiCmd for EvilCmd { ... }     // ОШИБКА: требование `Sealed` не выполнено

Когда применять sealing

Применяйте sealing, когда…Не применяйте, когда…
Безопасность зависит от корректности реализации (IpmiCmd, DiagModule)Пользователи должны расширять систему (свои форматировщики отчётов)
Ассоциированные типы должны соблюдать инвариантыТрейт — простой маркер возможности (HasIpmi)
У вас есть эталонный набор реализацийСторонние плагины — цель дизайна

Кандидаты в реальном коде

  • IpmiCmd: некорректный разбор может повредить типизированные ответы
  • DiagModule: фреймворк предполагает, что run() возвращает корректные записи DER
  • SelEventFilter: сломанный фильтр может поглотить критические события SEL

Приём 3 — #[non_exhaustive] для развивающихся enum

SkuVariant в inventory/src/types.rs сейчас имеет пять вариантов:

pub enum SkuVariant {
    S1001, S2001, S2002, S2003, S3001,
}

Когда выйдет следующее поколение и вы добавите S4001, любой внешний код, который сопоставляет с SkuVariant без ветки-заглушки _, тихо перестанет компилироваться. В этом и смысл. Но что с внутренним кодом? Без #[non_exhaustive] ваш match в том же крейте компилируется без _, и добавление нового варианта ломает вашу собственную сборку.

Пометка enum как #[non_exhaustive] заставляет внешние крейты, которые сопоставляют с ним, включать ветку-заглушку. Внутри определяющего крейта #[non_exhaustive] не действует: исчерпывающие сопоставления по-прежнему возможны.

Почему это полезно: когда вы публикуете SkuVariant из библиотечного крейта (или общего подкрейта в рабочей области), нижележащий код вынужден обрабатывать неизвестные будущие варианты. Когда вы добавите S4001 в следующем поколении, нижележащий код уже компилируется: у него есть ветка _.

// В крейте gpu_sel (определяющем крейте):
#[non_exhaustive]
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub enum SkuVariant {
    S1001,
    S2001,
    S2002,
    S2003,
    S3001,
    // Когда выйдет следующий SKU, добавьте его сюда.
    // У внешних потребителей уже есть подстановка: для них ноль поломок.
}

// Внутри gpu_sel — исчерпывающее сопоставление разрешено (подстановка не нужна):
fn diag_path_internal(sku: SkuVariant) -> &'static str {
    match sku {
        SkuVariant::S1001 => "legacy_gen1",
        SkuVariant::S2001 => "gen2_accel_diag",
        SkuVariant::S2002 => "gen2_alt_diag",
        SkuVariant::S2003 => "gen2_alt_hf_diag",
        SkuVariant::S3001 => "gen3_accel_diag",
        // Подстановка внутри определяющего крейта не нужна.
        // Добавление S4001 сюда вызовет ошибку компиляции в этом match,
        // и это именно то, что нужно: это заставит обновить код.
    }
}
// В бинарном крейте (нижележащем крейте, который зависит от inventory):
fn diag_path_external(sku: inventory::SkuVariant) -> &'static str {
    match sku {
        inventory::SkuVariant::S1001 => "legacy_gen1",
        inventory::SkuVariant::S2001 => "gen2_accel_diag",
        inventory::SkuVariant::S2002 => "gen2_alt_diag",
        inventory::SkuVariant::S2003 => "gen2_alt_hf_diag",
        inventory::SkuVariant::S3001 => "gen3_accel_diag",
        _ => "generic_diag",  // ОБЯЗАТЕЛЬНО для внешних крейтов из-за #[non_exhaustive]
    }
}

Совет по рабочей области: если весь код находится в одном крейте, #[non_exhaustive] не поможет: он действует только на границах между крейтами. Для большой рабочей области проекта размещайте развивающиеся enum в общем крейте (core_lib или inventory), чтобы атрибут защищал потребителей в других крейтах рабочей области.

Кандидаты

EnumМодульПочему
SkuVariantinventory, net_inventoryНовые SKU в каждом поколении
SensorTypeprotocol_libСпецификация IPMI резервирует 0xC0–0xFF для OEM
CompletionCodeprotocol_libПроизводители BMC добавляют собственные коды
Componentevent_handlerНовые категории оборудования (NewSoC был добавлен недавно)

Приём 4 — Билдер с typestate

Глава 5 показала typestate для протоколов (жизненные циклы сессий, обучение линка). Та же идея применима к билдерам: структурам, у которых build() или finish() можно вызвать только тогда, когда установлены все обязательные поля.

Проблема текучих (fluent) билдеров

DerBuilder в diag_framework/src/der.rs сейчас выглядит так (упрощённо):

// Текущий текучий билдер: finish() доступен всегда
pub struct DerBuilder {
    der: Der,
}

impl DerBuilder {
    pub fn new(marker: &str, fault_code: u32) -> Self { ... }
    pub fn mnemonic(mut self, m: &str) -> Self { ... }
    pub fn fault_class(mut self, fc: &str) -> Self { ... }
    pub fn finish(self) -> Der { self.der }  // ← доступен всегда!
}

Это компилируется без ошибок, но создаёт неполную запись DER:

let bad = DerBuilder::new("CSI_ERR", 62691)
    .finish();  // упс: нет mnemonic и fault_class

Билдер с typestate: finish() требует оба поля

pub struct Missing;
pub struct Set<T>(T);

pub struct DerBuilder<Mnemonic, FaultClass> {
    marker: String,
    fault_code: u32,
    mnemonic: Mnemonic,
    fault_class: FaultClass,
    description: Option<String>,
}

// Конструктор: начинаем с обоими обязательными полями в состоянии Missing
impl DerBuilder<Missing, Missing> {
    pub fn new(marker: &str, fault_code: u32) -> Self {
        DerBuilder {
            marker: marker.to_string(),
            fault_code,
            mnemonic: Missing,
            fault_class: Missing,
            description: None,
        }
    }
}

// Устанавливаем mnemonic (работает независимо от состояния fault_class)
impl<FC> DerBuilder<Missing, FC> {
    pub fn mnemonic(self, m: &str) -> DerBuilder<Set<String>, FC> {
        DerBuilder {
            marker: self.marker, fault_code: self.fault_code,
            mnemonic: Set(m.to_string()),
            fault_class: self.fault_class,
            description: self.description,
        }
    }
}

// Устанавливаем fault_class (работает независимо от состояния mnemonic)
impl<MN> DerBuilder<MN, Missing> {
    pub fn fault_class(self, fc: &str) -> DerBuilder<MN, Set<String>> {
        DerBuilder {
            marker: self.marker, fault_code: self.fault_code,
            mnemonic: self.mnemonic,
            fault_class: Set(fc.to_string()),
            description: self.description,
        }
    }
}

// Необязательные поля доступны в ЛЮБОМ состоянии
impl<MN, FC> DerBuilder<MN, FC> {
    pub fn description(mut self, desc: &str) -> Self {
        self.description = Some(desc.to_string());
        self
    }
}

/// Полностью собранная запись DER.
pub struct Der {
    pub marker: String,
    pub fault_code: u32,
    pub mnemonic: String,
    pub fault_class: String,
    pub description: Option<String>,
}

// finish() доступен ТОЛЬКО тогда, когда оба обязательных поля в состоянии Set
impl DerBuilder<Set<String>, Set<String>> {
    pub fn finish(self) -> Der {
        Der {
            marker: self.marker,
            fault_code: self.fault_code,
            mnemonic: self.mnemonic.0,
            fault_class: self.fault_class.0,
            description: self.description,
        }
    }
}

Теперь ошибочный вызов становится ошибкой компиляции:

// ✅ Компилируется: оба обязательных поля установлены (порядок не важен)
let der = DerBuilder::new("CSI_ERR", 62691)
    .fault_class("GPU Module")   // порядок не важен
    .mnemonic("ACCEL_CARD_ER691")
    .description("Thermal throttle")
    .finish();

// ❌ Ошибка компиляции: finish() не существует у DerBuilder<Set<String>, Missing>
let bad = DerBuilder::new("CSI_ERR", 62691)
    .mnemonic("ACCEL_CARD_ER691")
    .finish();  // ОШИБКА: метод `finish` не найден

Когда использовать билдеры с typestate

Используйте, когда…Не стоит, когда…
Пропуск поля приводит к тихим ошибкам (в DER пропущен mnemonic)Все поля имеют разумные значения по умолчанию
Билдер — часть публичного APIБилдер — только тестовая обвязка
Более 2–3 обязательных полейОдно обязательное поле (просто передайте его в new())

Приём 5 — FromStr как граница валидации

Глава 7 показала TryFrom<&[u8]> для бинарных данных (записи FRU, записи SEL). Для строковых входных данных (файлов конфигурации, аргументов CLI, полей JSON) аналогичной границей служит FromStr.

Проблема

// C++ / Rust без валидации: тихо падает в значение по умолчанию
fn route_diag(level: &str) -> DiagMode {
    if level == "quick" { ... }
    else if level == "standard" { ... }
    else { QuickMode }  // опечатка в конфиге?  ¯\_(ツ)_/¯
}

Файл конфигурации с "diag_level": "extendedd" (опечатка) тихо получит QuickMode.

Паттерн (из config_loader/src/diag.rs)

use std::str::FromStr;

#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum DiagLevel {
    Quick,
    Standard,
    Extended,
    Stress,
}

impl FromStr for DiagLevel {
    type Err = String;
    fn from_str(s: &str) -> Result<Self, Self::Err> {
        match s.to_lowercase().as_str() {
            "quick"    | "1" => Ok(DiagLevel::Quick),
            "standard" | "2" => Ok(DiagLevel::Standard),
            "extended" | "3" => Ok(DiagLevel::Extended),
            "stress"   | "4" => Ok(DiagLevel::Stress),
            other => Err(format!("неизвестный уровень диагностики: '{other}'")),
        }
    }
}

Теперь опечатка ловится сразу:

let level: DiagLevel = "extendedd".parse()?;
// Err("неизвестный уровень диагностики: 'extendedd'")

Три преимущества

  1. Быстрый отказ: некорректные входные данные ловятся на границе разбора, а не на три уровня глубже в логике диагностики.
  2. Синонимы явные: "MEM", "DIMM" и "MEMORY" все отображаются в Component::Memory: ветки match документируют соответствие.
  3. .parse() удобен: поскольку FromStr интегрируется с str::parse(), получаются лаконичные однострочники: let level: DiagLevel = config["level"].parse()?;

Использование в реальном коде

В проекте уже есть 8 реализаций FromStr:

ТипМодульЗаметные синонимы
DiagLevelconfig_loader"1" = быстрый, "4" = стресс
Componentevent_handler"MEM" / "DIMM" = память, "SSD" / "NVME" = диск
SkuVariantnet_inventory"Accel-X1" = S2001, "Accel-M1" = S2002, "Accel-Z1" = S3001
SkuVariantinventoryТе же синонимы (отдельный модуль, тот же паттерн)
FaultStatusconfig_loaderСостояния жизненного цикла неисправности
DiagActionconfig_loaderТипы действий по устранению
ActionTypeconfig_loaderКатегории действий
DiagModecluster_diagРежимы тестирования нескольких узлов

Сравнение с TryFrom:

TryFrom<&[u8]>FromStr
ВходСырые байты (бинарные протоколы)Строки (конфиги, CLI, JSON)
Типичный источникIPMI, пространство конфигурации PCIe, FRUПоля JSON, переменные окружения, ввод пользователя
Главагл. 07гл. 11
Оба используютResult: заставляет вызывающего обрабатывать некорректный ввод

Приём 6 — const-обобщения для проверки размера на этапе компиляции

Когда аппаратные буферы, банки регистров или кадры протокола имеют фиксированный размер, const-обобщения позволяют компилятору его проверить:

/// Банк регистров фиксированного размера. Размер — часть типа.
/// `RegisterBank<256>` и `RegisterBank<4096>` — разные типы.
pub struct RegisterBank<const N: usize> {
    data: [u8; N],
}

impl<const N: usize> RegisterBank<N> {
    /// Читает регистр по заданному смещению.
    /// Время компиляции: N известно, поэтому размер массива фиксирован.
    /// Время выполнения: проверяется только смещение.
    pub fn read(&self, offset: usize) -> Option<u8> {
        self.data.get(offset).copied()
    }
}

// Конфигурационное пространство PCIe (обычное): 256 байт
type PciConfigSpace = RegisterBank<256>;

// Расширенное конфигурационное пространство PCIe: 4096 байт
type PcieExtConfigSpace = RegisterBank<4096>;

// Это разные типы: нельзя случайно передать один вместо другого:
fn read_extended_cap(config: &PcieExtConfigSpace, offset: usize) -> Option<u8> {
    config.read(offset)
}
// read_extended_cap(&pci_config, 0x100);
//                   ^^^^^^^^^^^ expected RegisterBank<4096>, found RegisterBank<256> ❌

Утверждения на этапе компиляции с const-обобщениями:

/// Административные команды NVMe используют буферы по 4096 байт. Проверяем на этапе компиляции.
pub struct NvmeBuffer<const N: usize> {
    data: Box<[u8; N]>,
}

impl<const N: usize> NvmeBuffer<N> {
    pub fn new() -> Self {
        // Проверка во время выполнения: разрешены только 512 или 4096
        assert!(N == 4096 || N == 512, "Буферы NVMe должны быть 512 или 4096 байт");
        NvmeBuffer { data: Box::new([0u8; N]) }
    }
}
// NvmeBuffer::<1024>::new();  // в такой форме паникует во время выполнения
// Для настоящей проверки на этапе компиляции см. приём 9 (const-утверждения).

Когда использовать: фиксированные протокольные буферы (NVMe, конфигурационное пространство PCIe), дескрипторы DMA, глубины аппаратных FIFO. Везде, где размер — аппаратная константа, которая никогда не должна меняться во время выполнения.


Приём 7 — Безопасные обёртки вокруг unsafe

В проекте сейчас нет ни одного блока unsafe. Но когда вы добавите доступ к регистрам MMIO, DMA или FFI в accel-mgmt или accel-query, unsafe понадобится. Корректный по построению подход: обернуть каждый блок unsafe в безопасную абстракцию, чтобы небезопасность была локализована и поддавалась аудиту.

/// Регистр, отображённый в MMIO. Указатель действителен, пока существует отображение.
/// Весь unsafe заключён в этом модуле: вызывающий код использует безопасные методы.
pub struct MmioRegion {
    base: *mut u8,
    len: usize,
}

impl MmioRegion {
    /// # Safety
    /// - `base` должен быть корректным указателем на область, отображённую в MMIO
    /// - Область должна оставаться отображённой, пока существует эта структура
    /// - Никакой другой код не должен алиасить эту область
    pub unsafe fn new(base: *mut u8, len: usize) -> Self {
        MmioRegion { base, len }
    }

    /// Безопасное чтение: проверка границ предотвращает выход за пределы MMIO.
    pub fn read_u32(&self, offset: usize) -> Option<u32> {
        if offset + 4 > self.len { return None; }
        // SAFETY: offset проверен выше, base корректен согласно контракту new()
        Some(unsafe {
            core::ptr::read_volatile(self.base.add(offset) as *const u32)
        })
    }

    /// Безопасная запись: проверка границ предотвращает выход за пределы MMIO.
    pub fn write_u32(&self, offset: usize, value: u32) -> bool {
        if offset + 4 > self.len { return false; }
        // SAFETY: offset проверен выше, base корректен согласно контракту new()
        unsafe {
            core::ptr::write_volatile(self.base.add(offset) as *mut u32, value);
        }
        true
    }
}

Комбинируем с phantom-типами (гл. 09) для типизированного MMIO:

use std::marker::PhantomData;

pub struct ReadOnly;
pub struct ReadWrite;

pub struct TypedMmio<Perm> {
    region: MmioRegion,
    _perm: PhantomData<Perm>,
}

impl TypedMmio<ReadOnly> {
    pub fn read_u32(&self, offset: usize) -> Option<u32> {
        self.region.read_u32(offset)
    }
    // Нет метода записи: ошибка компиляции при попытке записи в область ReadOnly
}

impl TypedMmio<ReadWrite> {
    pub fn read_u32(&self, offset: usize) -> Option<u32> {
        self.region.read_u32(offset)
    }
    pub fn write_u32(&self, offset: usize, value: u32) -> bool {
        self.region.write_u32(offset, value)
    }
}

Правила для обёрток над unsafe:

ПравилоЗачем
Один unsafe fn new() с задокументированными инвариантами # SafetyВызывающий берёт ответственность один раз
Все остальные методы безопасныВызывающие не могут вызвать UB
Комментарий # SAFETY: у каждого блока unsafeАудиторы могут проверить локально
Обернуть в модуль с #[deny(unsafe_op_in_unsafe_fn)]Даже внутри unsafe fn отдельные операции требуют unsafe
Запустить cargo +nightly miri test на обёрткеПроверка соответствия модели памяти

✅ Контрольная точка: приёмы 1–7

Теперь у вас есть семь повседневных приёмов. Вот краткая сводка:

ПриёмУстраняемый класс ошибокУсилия на внедрение
1Путаница sentinel-значений (0xFF)Низкие: один match на границе
2Несанкционированные реализации трейтовНизкие: добавить супертрейт Sealed
3Поломка потребителей при росте enumНизкие: одна строка-атрибут
4Пропущенные поля билдераСредние: дополнительные параметры типа
5Опечатки в строковых конфигурацияхНизкие: impl FromStr
6Неверные размеры буферовНизкие: параметр const-обобщения
7Небезопасный код, разбросанный по кодовой базеСредние: модуль-обёртка

Приёмы 8–14 — более продвинутые: они затрагивают async, вычисления на этапе компиляции, сессионные типы, Pin и Drop. Сделайте паузу, если нужно: приёмы выше уже дают высокую отдачу при низких затратах, и их можно внедрить хоть завтра.


Приём 8 — Автоматы состояний в async-коде

Когда драйверы аппаратуры используют async (например, асинхронная связь с BMC или асинхронный ввод-вывод NVMe), typestate по-прежнему работает, но владение через точки .await требует внимания:

use std::marker::PhantomData;

pub struct Idle;
pub struct Authenticating;
pub struct Active;

pub struct AsyncSession<S> {
    host: String,
    _state: PhantomData<S>,
}

impl AsyncSession<Idle> {
    pub fn new(host: &str) -> Self {
        AsyncSession { host: host.to_string(), _state: PhantomData }
    }

    /// Переход Idle → Authenticating → Active.
    /// Сессия потребляется (перемещается в future) через .await.
    pub async fn authenticate(self, user: &str, pass: &str)
        -> Result<AsyncSession<Active>, String>
    {
        // Фаза 1: отправляем учётные данные (потребляет сессию Idle)
        let pending: AsyncSession<Authenticating> = AsyncSession {
            host: self.host,
            _state: PhantomData,
        };

        // Имитация асинхронной аутентификации в BMC
        // tokio::time::sleep(Duration::from_secs(1)).await;

        // Фаза 2: возвращаем активную сессию
        Ok(AsyncSession {
            host: pending.host,
            _state: PhantomData,
        })
    }
}

impl AsyncSession<Active> {
    pub async fn send_command(&mut self, cmd: &[u8]) -> Vec<u8> {
        // асинхронный ввод-вывод здесь...
        vec![0x00]
    }
}

// Использование:
// let session = AsyncSession::new("192.168.1.100");
// let mut session = session.authenticate("admin", "pass").await?;
// let resp = session.send_command(&[0x04, 0x2D]).await;

Ключевые правила для async typestate:

ПравилоЗачем
Методы перехода принимают self (по значению), а не &mut selfПередача владения работает через .await
Возвращать Result<NextState, (Error, PrevState)> для восстанимых ошибокВызывающий может повторить попытку с предыдущего состояния
Не разделять состояние между несколькими futureОдин future владеет одной сессией
Использовать ограничения Send + 'static, если применяется tokio::spawnСессия должна перемещаться между потоками

Оговорка: если при ошибке нужно вернуть предыдущее состояние (чтобы повторить попытку), верните Result<AsyncSession<Active>, (Error, AsyncSession<Idle>)>, чтобы вызывающий получил владение обратно. Без этого неудачный .await навсегда уничтожает сессию.


Приём 9 — Уточняющие типы через const-утверждения

Когда числовое ограничение является инвариантом времени компиляции (а не данными времени выполнения), используйте вычисления на этапе компиляции (const), чтобы его обеспечить. Это отличается от приёма 6, который задаёт различия размеров на уровне типов: здесь мы отвергаем недопустимые значения на этапе компиляции:

/// ID датчика, который должен находиться в диапазоне SDR IPMI (0x01..=0xFE).
/// Ограничение проверяется на этапе компиляции, когда `N` — константа.
pub struct SdrSensorId<const N: u8>;

impl<const N: u8> SdrSensorId<N> {
    /// Проверка на этапе компиляции: паникует во время компиляции, если N вне диапазона.
    pub const fn validate() {
        assert!(N >= 0x01, "ID датчика должен быть >= 0x01");
        assert!(N <= 0xFE, "ID датчика должен быть <= 0xFE (0xFF зарезервирован)");
    }

    pub const VALIDATED: () = Self::validate();

    pub const fn value() -> u8 { N }
}

// Использование:
fn read_sensor_const<const N: u8>() -> f64 {
    let _ = SdrSensorId::<N>::VALIDATED;  // проверка на этапе компиляции
    // читаем датчик N...
    42.0
}

// read_sensor_const::<0x20>();   // ✅ компилируется: 0x20 допустим
// read_sensor_const::<0x00>();   // ❌ ошибка компиляции: "ID датчика должен быть >= 0x01"
// read_sensor_const::<0xFF>();   // ❌ ошибка компиляции: 0xFF зарезервирован

Упрощённый вариант: ограниченные ID вентиляторов:

pub struct BoundedFanId<const N: u8>;

impl<const N: u8> BoundedFanId<N> {
    pub const VALIDATED: () = assert!(N < 8, "Сервер поддерживает не более 8 вентиляторов (0..7)");

    pub const fn id() -> u8 {
        let _ = Self::VALIDATED;
        N
    }
}

// BoundedFanId::<3>::id();   // ✅
// BoundedFanId::<10>::id();  // ❌ ошибка компиляции

Когда использовать: аппаратно заданные фиксированные ID (ID датчиков, слоты вентиляторов, номера слотов PCIe), известные на этапе компиляции. Если значение приходит из данных времени выполнения (файл конфигурации, ввод пользователя), используйте TryFrom / FromStr (гл. 07, приём 5).


Приём 10 — Сессионные типы для обмена по каналам

Когда два компонента обмениваются сообщениями по каналу (например, оркестратор диагностики и рабочий поток), сессионные типы кодируют протокол в системе типов:

use std::marker::PhantomData;

// Протокол: клиент отправляет Request, сервер отправляет Response, затем завершение.
pub struct SendRequest;
pub struct RecvResponse;
pub struct Done;

/// Типизированный конец канала. `S` — текущее состояние протокола.
pub struct Chan<S> {
    // В реальном коде: оборачивает пару mpsc::Sender/Receiver
    _state: PhantomData<S>,
}

impl Chan<SendRequest> {
    /// Отправить запрос: переход в состояние RecvResponse.
    pub fn send(self, request: DiagRequest) -> Chan<RecvResponse> {
        // ... отправка в канал ...
        Chan { _state: PhantomData }
    }
}

impl Chan<RecvResponse> {
    /// Получить ответ: переход в состояние Done.
    pub fn recv(self) -> (DiagResponse, Chan<Done>) {
        // ... получение из канала ...
        (DiagResponse { passed: true }, Chan { _state: PhantomData })
    }
}

impl Chan<Done> {
    /// Закрытие канала: возможно только когда протокол завершён.
    pub fn close(self) { /* drop */ }
}

pub struct DiagRequest { pub test_name: String }
pub struct DiagResponse { pub passed: bool }

// Протокол ОБЯЗАН соблюдаться по порядку:
fn orchestrator(chan: Chan<SendRequest>) {
    let chan = chan.send(DiagRequest { test_name: "gpu_stress".into() });
    let (response, chan) = chan.recv();
    chan.close();
    println!("Результат: {}", if response.passed { "PASS" } else { "FAIL" });
}

// Нельзя получить ответ до отправки:
// fn wrong_order(chan: Chan<SendRequest>) {
//     chan.recv();  // ❌ нет метода `recv` у Chan<SendRequest>
// }

Когда использовать: межпоточные диагностические протоколы, последовательности команд BMC, любой паттерн «запрос — ответ», где важен порядок. Для сложных многосообщенческих протоколов рассмотрите крейты session-types или rumpsteak.


Приём 11 — Pin для самоссылочных автоматов состояний

Некоторым автоматам состояний нужно хранить ссылки на собственные данные (например, парсеру, который отслеживает позицию внутри собственного буфера). Rust обычно запрещает это, потому что перемещение структуры сделает внутренний указатель недействительным. Pin<T> решает эту проблему, гарантируя, что значение не будет перемещено:

use std::pin::Pin;
use std::marker::PhantomPinned;

/// Потоковый парсер, который хранит ссылку на собственный буфер.
/// После закрепления его нельзя переместить: внутренняя ссылка остаётся действительной.
pub struct StreamParser {
    buffer: Vec<u8>,
    /// Указывает внутрь `buffer`. Действительна только пока парсер закреплён.
    cursor: *const u8,
    _pin: PhantomPinned,  // отказываемся от Unpin: предотвращает случайное открепление
}

impl StreamParser {
    pub fn new(data: Vec<u8>) -> Pin<Box<Self>> {
        let parser = StreamParser {
            buffer: data,
            cursor: std::ptr::null(),
            _pin: PhantomPinned,
        };
        let mut boxed = Box::pin(parser);

        // Устанавливаем cursor на закреплённый буфер
        let cursor = boxed.buffer.as_ptr();
        // SAFETY: у нас эксклюзивный доступ, и парсер закреплён
        unsafe {
            let mut_ref = Pin::as_mut(&mut boxed);
            Pin::get_unchecked_mut(mut_ref).cursor = cursor;
        }

        boxed
    }

    /// Читает следующий байт: вызывается только через Pin<&mut Self>.
    pub fn next_byte(self: Pin<&mut Self>) -> Option<u8> {
        // Парсер нельзя переместить, поэтому cursor остаётся действительным
        if self.cursor.is_null() { return None; }
        // ... продвигаем cursor по буферу ...
        Some(42) // заглушка
    }
}

// Использование:
// let mut parser = StreamParser::new(vec![0x01, 0x02, 0x03]);
// let byte = parser.as_mut().next_byte();

Ключевая идея: Pin — это корректное по построению решение проблемы самоссылочных структур. Без него понадобился бы unsafe и ручное отслеживание времён жизни. С ним компилятор запрещает перемещения, и инвариант внутреннего указателя сохраняется.

Используйте Pin, когда…Не используйте Pin, когда…
Автомат состояний хранит ссылки внутри структурыВсе поля независимо принадлежат структуре
Async-future заимствуют данные через .awaitСамоссылки не нужны
Дескрипторы DMA, которые не должны перемещаться в памятиДанные можно свободно перемещать
Аппаратные кольцевые буферы с внутренним курсоромПростой итерации по индексу достаточно

Приём 12 — RAII / Drop как гарантия корректности

Трейт Drop в Rust — это механизм корректности по построению: код очистки нельзя забыть, потому что компилятор вставляет его автоматически. Это особенно ценно для аппаратных ресурсов, которые нужно освободить ровно один раз.

use std::io;

/// Сессия IPMI, которую ОБЯЗАТЕЛЬНО нужно закрыть по завершении.
/// Реализация `Drop` гарантирует очистку даже при панике или раннем возврате через `?`.
pub struct IpmiSession {
    handle: u32,
}

impl IpmiSession {
    pub fn open(host: &str) -> io::Result<Self> {
        // ... согласовываем сессию IPMI ...
        Ok(IpmiSession { handle: 42 })
    }

    pub fn send_raw(&self, _data: &[u8]) -> io::Result<Vec<u8>> {
        Ok(vec![0x00])
    }
}

impl Drop for IpmiSession {
    fn drop(&mut self) {
        // Команда Close Session: выполняется всегда, даже при панике или раннем возврате.
        // В C забытый CloseSession() оставляет слот сессии BMC занятым.
        let _ = self.send_raw(&[0x06, 0x3C]);
        eprintln!("[RAII] сессия {} закрыта", self.handle);
    }
}
// Использование:
fn diagnose(host: &str) -> io::Result<()> {
    let session = IpmiSession::open(host)?;
    session.send_raw(&[0x04, 0x2D, 0x20])?;
    // Явное закрытие не нужно: Drop выполнится здесь автоматически
    Ok(())
    // Даже если send_raw вернёт Err(...), сессия всё равно закроется.
}

Сбой C/C++, который устраняет RAII:

C:     session = ipmi_open(host);
       ipmi_send(session, data);
       if (error) return -1;        // 🐛 утечка сессии: забыли close()
       ipmi_close(session);

Rust:  let session = IpmiSession::open(host)?;
       session.send_raw(data)?;     // ✅ Drop выполняется при раннем возврате через ?
       // Drop всегда выполняется: утечка невозможна

Комбинируем RAII с typestate (гл. 05) для упорядоченной очистки:

Нельзя специализировать Drop по параметру обобщённого типа (ошибка Rust E0366). Вместо этого используйте отдельные типы-обёртки для каждого состояния:

use std::marker::PhantomData;

pub struct Open;
pub struct Locked;

pub struct GpuContext<S> {
    device_id: u32,
    _state: PhantomData<S>,
}

impl GpuContext<Open> {
    pub fn lock_clocks(self) -> LockedGpu {
        // ... блокируем частоты GPU для стабильных замеров ...
        LockedGpu { device_id: self.device_id }
    }
}

/// Отдельный тип для заблокированного состояния: имеет собственный Drop.
/// Нельзя написать `impl Drop for GpuContext<Locked>` (E0366),
/// поэтому используем отдельную обёртку, которая владеет заблокированным ресурсом.
pub struct LockedGpu {
    device_id: u32,
}

impl LockedGpu {
    pub fn run_benchmark(&self) -> f64 {
        // ... бенчмарк с заблокированными частотами ...
        42.0
    }
}

impl Drop for LockedGpu {
    fn drop(&mut self) {
        // Разблокируем частоты при Drop: срабатывает только для заблокированной обёртки.
        eprintln!("[RAII] частоты GPU {} разблокированы", self.device_id);
    }
}

// GpuContext<Open> не имеет собственного Drop: разблокировать нечего.
// LockedGpu всегда разблокирует частоты при Drop, даже при панике или раннем возврате.

Почему не impl Drop for GpuContext<Locked>? Rust требует, чтобы реализации Drop применялись ко всем инстанциям обобщённого типа. Чтобы получить очистку, зависящую от состояния, используйте один из вариантов:

ПодходПлюсыМинусы
Отдельный тип-обёртка (выше)Чисто, без накладных расходовДополнительное имя типа
Обобщённый Drop и проверка TypeId во время выполненияОдин типТребует 'static, накладные расходы во время выполнения
Состояние в виде enum с исчерпывающим match в DropОдин обобщённый типДиспетчеризация во время выполнения, меньше безопасности типов

Когда использовать: сессии BMC, блокировки частот GPU, отображения буферов DMA, файловые дескрипторы, охранники мьютексов (mutex guards), любой ресурс с обязательным шагом освобождения. Если вы пишете fn close(&mut self) или fn cleanup(), почти наверняка это должен быть Drop.


Приём 13 — Иерархии типов ошибок как инструмент корректности

Хорошо спроектированные типы ошибок предотвращают тихое проглатывание ошибок и заставляют вызывающий код корректно обрабатывать каждый режим отказа. Использование thiserror для структурированных ошибок — паттерн корректности по построению: компилятор требует исчерпывающего сопоставления.

# Cargo.toml
[dependencies]
thiserror = "1"
# Для обработки ошибок на уровне приложения (необязательно):
# anyhow = "1"
use thiserror::Error;

#[derive(Debug, Error)]
pub enum DiagError {
    #[error("сбой связи по IPMI: {0}")]
    Ipmi(#[from] IpmiError),

    #[error("показание датчика {sensor_id:#04x} вне диапазона: {value}")]
    SensorRange { sensor_id: u8, value: f64 },

    #[error("GPU {gpu_id} не отвечает")]
    GpuTimeout { gpu_id: u32 },

    #[error("некорректная конфигурация: {0}")]
    Config(String),
}

#[derive(Debug, Error)]
pub enum IpmiError {
    #[error("аутентификация сессии не пройдена")]
    AuthFailed,

    #[error("команда {net_fn:#04x}/{cmd:#04x}: истекло время ожидания")]
    Timeout { net_fn: u8, cmd: u8 },

    #[error("код завершения {0:#04x}")]
    CompletionCode(u8),
}

// Вызывающий ОБЯЗАН обработать каждый вариант: никакого тихого проглатывания
fn run_thermal_check() -> Result<(), DiagError> {
    // Если здесь возвращается IpmiError, он автоматически преобразуется в DiagError::Ipmi
    // через атрибут #[from].
    let temp = read_cpu_temp()?;
    if temp > 105.0 {
        return Err(DiagError::SensorRange {
            sensor_id: 0x20,
            value: temp,
        });
    }
    Ok(())
}

fn read_cpu_temp() -> Result<f64, DiagError> { Ok(42.0) }

Почему это корректно по построению:

Без структурированных ошибокС enum из thiserror
fn op() -> Result<T, String>fn op() -> Result<T, DiagError>
Вызывающий получает непрозрачную строкуВызывающий сопоставляет конкретные варианты
Нельзя отличить сбой аутентификации от тайм-аутаDiagError::Ipmi(IpmiError::AuthFailed) и Timeout
Логирование проглатывает ошибкуmatch заставляет обработать каждый случай
Новый вариант ошибки: никто не замечаетНовый вариант: компилятор предупреждает о непокрытых ветках

Выбор между anyhow и thiserror:

Используйте thiserror, когда…Используйте anyhow, когда…
Пишете библиотеку или крейтПишете бинарник или CLI
Вызывающим нужно сопоставлять варианты ошибокВызывающие просто логируют и завершаются
Типы ошибок — часть публичного APIВнутренняя обвязка ошибок
protocol_lib, accel_diag, thermal_diagГлавный бинарник diag_tool

Когда использовать: каждый крейт в рабочей области должен определять собственный enum ошибок с thiserror. Верхнеуровневый бинарный крейт может использовать anyhow, чтобы объединить их. Это даёт вызывающим библиотеки гарантии обработки ошибок на этапе компиляции, сохраняя при этом удобство бинарника.


Приём 14 — #[must_use] для обязательного использования

Атрибут #[must_use] превращает проигнорированные возвращаемые значения в предупреждения компилятора. Это лёгкий инструмент корректности по построению, который сочетается с каждым паттерном этого руководства:

/// Токен калибровки, который ОБЯЗАН быть использован: тихое отбрасывание — ошибка.
#[must_use = "токен калибровки нужно передать в calibrate(), а не отбрасывать"]
pub struct CalibrationToken {
    _private: (),
}

/// Результат диагностики, который ОБЯЗАН быть проверен: игнорирование сбоев — ошибка.
#[must_use = "результат диагностики нужно проверить на наличие сбоев"]
pub struct DiagResult {
    pub passed: bool,
    pub details: String,
}

/// Функции, возвращающие важные значения, тоже стоит пометить:
#[must_use = "аутентифицированную сессию нужно использовать или явно закрыть"]
pub fn authenticate(user: &str, pass: &str) -> Result<Session, AuthError> {
    // ...
  unimplemented!()
}

pub struct Session;
pub struct AuthError;

Что сообщает компилятор:

warning: unused `CalibrationToken` that must be used
  --> src/main.rs:5:5
   |
5  |     CalibrationToken { _private: () };
   |     ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
   |
   = note: токен калибровки нужно передать в calibrate(), а не отбрасывать

Применяйте #[must_use] к этим паттернам:

ПаттернЧто аннотироватьПочему
Одноразовые токены (гл. 03)CalibrationToken, FusePayloadОтбрасывание без использования: логическая ошибка
Capability-токены (гл. 04)AdminTokenАутентификация, но токен игнорируется
Переходы typestateВозвращаемый тип authenticate(), activate()Сессия создана, но не используется
РезультатыDiagResult, SensorReadingТихое проглатывание сбоев
Обработчики RAII (приём 12)IpmiSession, LockedGpuРесурс открыт, но не используется

Эмпирическое правило: если отбрасывание значения без использования всегда является ошибкой, добавьте #[must_use]. Если это иногда намеренно (например, для Vec), не добавляйте. Префикс _ (let _ = foo()) явно подтверждает отбрасывание и отключает предупреждение: это нормально, когда отбрасывание намеренное.

Ключевые выводы

  1. Sentinel → Option на границе: преобразуйте магические значения в Option при разборе; компилятор заставит вызывающих обрабатывать None.
  2. Sealed-трейты закрывают лазейку в реализациях: приватный супертрейт означает, что реализовать трейт может только ваш крейт.
  3. #[non_exhaustive] и #[must_use] — однострочные аннотации с высокой отдачей: добавляйте их к развивающимся enum и потребляемым токенам.
  4. Билдеры с typestate обеспечивают обязательные поля: finish() существует только тогда, когда все обязательные параметры типа в состоянии Set.
  5. Каждый приём нацелен на конкретный класс ошибок: внедряйте их постепенно; ни один приём не требует переписывать архитектуру.

Упражнения 🟡

Что вы узнаете: практику применения паттернов корректности по построению к реалистичным сценариям аппаратного обеспечения: административные команды NVMe, автоматы состояний обновления прошивки, конвейеры обработки показаний датчиков, phantom-типы для PCIe, проверки здоровья по нескольким протоколам и диагностические протоколы с сессионными типами.

Перекрёстные ссылки: гл. 02 (упражнение 1), гл. 05 (упражнение 2), гл. 06 (упражнение 3), гл. 09 (упражнение 4), гл. 10 (упражнение 5)

Практические задачи

Упражнение 1: команда администрирования NVMe (типизированные команды)

Спроектируйте типизированный командный интерфейс для административных команд NVMe:

  • Identify → IdentifyResponse (номер модели, серийный номер, версия прошивки)
  • GetLogPage → SmartLog (температура, доступный резерв, количество прочитанных единиц данных)
  • GetFeature → ответ, специфичный для функции

Требования:

  1. Тип команды определяет тип ответа
  2. Никакой диспетчеризации во время выполнения, только статическая
  3. Добавьте newtype NamespaceId, который не даёт перепутать идентификаторы пространств имён с другими значениями u32

Подсказка: следуйте паттерну трейта IpmiCmd из гл. 02, но используйте константы, специфичные для NVMe.

Пример решения (упражнение 1)
use std::io;

#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
pub struct NamespaceId(pub u32);

#[derive(Debug, Clone, PartialEq)]
pub struct IdentifyResponse {
    pub model: String,
    pub serial: String,
    pub firmware_rev: String,
}

#[derive(Debug, Clone, PartialEq)]
pub struct SmartLog {
    pub temperature_kelvin: u16,
    pub available_spare_pct: u8,
    pub data_units_read: u64,
}

#[derive(Debug, Clone, PartialEq)]
pub struct ArbitrationFeature {
    pub high_priority_weight: u8,
    pub medium_priority_weight: u8,
    pub low_priority_weight: u8,
}

/// Ключевой паттерн: ассоциированный тип фиксирует ответ каждой команды.
pub trait NvmeAdminCmd {
    type Response;
    fn opcode(&self) -> u8;
    fn nsid(&self) -> Option<NamespaceId>;
    fn parse_response(&self, raw: &[u8]) -> io::Result<Self::Response>;
}

pub struct Identify { pub nsid: NamespaceId }

impl NvmeAdminCmd for Identify {
    type Response = IdentifyResponse;
    fn opcode(&self) -> u8 { 0x06 }
    fn nsid(&self) -> Option<NamespaceId> { Some(self.nsid) }
    fn parse_response(&self, raw: &[u8]) -> io::Result<IdentifyResponse> {
        if raw.len() < 12 {
            return Err(io::Error::new(io::ErrorKind::InvalidData, "слишком коротко"));
        }
        Ok(IdentifyResponse {
            model: String::from_utf8_lossy(&raw[0..4]).trim().to_string(),
            serial: String::from_utf8_lossy(&raw[4..8]).trim().to_string(),
            firmware_rev: String::from_utf8_lossy(&raw[8..12]).trim().to_string(),
        })
    }
}

pub struct GetLogPage { pub log_id: u8 }

impl NvmeAdminCmd for GetLogPage {
    type Response = SmartLog;
    fn opcode(&self) -> u8 { 0x02 }
    fn nsid(&self) -> Option<NamespaceId> { None }
    fn parse_response(&self, raw: &[u8]) -> io::Result<SmartLog> {
        if raw.len() < 11 {
            return Err(io::Error::new(io::ErrorKind::InvalidData, "слишком коротко"));
        }
        Ok(SmartLog {
            temperature_kelvin: u16::from_le_bytes([raw[0], raw[1]]),
            available_spare_pct: raw[2],
            data_units_read: u64::from_le_bytes(raw[3..11].try_into().unwrap()),
        })
    }
}

pub struct GetFeature { pub feature_id: u8 }

impl NvmeAdminCmd for GetFeature {
    type Response = ArbitrationFeature;
    fn opcode(&self) -> u8 { 0x0A }
    fn nsid(&self) -> Option<NamespaceId> { None }
    fn parse_response(&self, raw: &[u8]) -> io::Result<ArbitrationFeature> {
        if raw.len() < 3 {
            return Err(io::Error::new(io::ErrorKind::InvalidData, "слишком коротко"));
        }
        Ok(ArbitrationFeature {
            high_priority_weight: raw[0],
            medium_priority_weight: raw[1],
            low_priority_weight: raw[2],
        })
    }
}

/// Статическая диспетчеризация: компилятор мономорфизирует код для каждого типа команды.
pub struct NvmeController;

impl NvmeController {
    pub fn execute<C: NvmeAdminCmd>(&self, cmd: &C) -> io::Result<C::Response> {
        // Собираем SQE из cmd.opcode()/cmd.nsid(),
        // отправляем в SQ, ждём CQ, затем:
        let raw = self.submit_and_read(cmd.opcode())?;
        cmd.parse_response(&raw)
    }

    fn submit_and_read(&self, _opcode: u8) -> io::Result<Vec<u8>> {
        // Реальная реализация обращается к /dev/nvme0
        Ok(vec![0; 512])
    }
}

Ключевые моменты:

  • NamespaceId(u32) не даёт смешивать идентификаторы пространств имён с произвольными значениями u32.
  • NvmeAdminCmd::Response — это «индекс типа»: execute() возвращает ровно C::Response.
  • Полностью статическая диспетчеризация: никаких Box<dyn …> и никакого динамического приведения типов во время выполнения.

Упражнение 2: автомат состояний обновления прошивки (typestate)

Смоделируйте жизненный цикл обновления прошивки BMC:

stateDiagram-v2
    [*] --> Idle
    Idle --> Uploading : begin_upload()
    Uploading --> Uploading : send_chunk(data)
    Uploading --> Verifying : finish_upload()
    Uploading --> Idle : abort()
    Verifying --> Applying : verify() ✅ + токен VerifiedImage
    Verifying --> Idle : verify() ❌ или abort()
    Applying --> Rebooting : apply(token)
    Rebooting --> Complete : reboot_complete()
    Complete --> [*]

    note right of Applying : нет abort(): необратимо
    note right of Verifying : VerifiedImage — токен-доказательство

Требования:

  1. Каждое состояние — отдельный тип
  2. Загрузка может начинаться только из Idle
  3. Проверка требует завершённой загрузки
  4. Применение возможно только после успешной проверки: примите токен-доказательство VerifiedImage
  5. Перезагрузка — единственный вариант после применения
  6. Добавьте метод abort(), доступный в Uploading и Verifying (но не в Applying: уже поздно)

Подсказка: совместите typestate (гл. 05) с capability-токенами (гл. 04).

Пример решения (упражнение 2)
// --- Типы состояний ---
// Проектное решение: здесь состояние хранится непосредственно (`_state: S`), а не через
// `PhantomData<S>` (подход гл. 05). Это позволяет состояниям нести данные: например,
// `Uploading { bytes_sent: usize }` отслеживает прогресс. Используйте `PhantomData`,
// когда состояния — чистые маркеры (нулевого размера); храните состояние в структуре,
// когда состояния несут значимые данные времени выполнения.
pub struct Idle;
pub struct Uploading { bytes_sent: usize }  // не ZST: несёт данные о прогрессе
pub struct Verifying;
pub struct Applying;
pub struct Rebooting;
pub struct Complete;

/// Токен-доказательство: создаётся только внутри verify().
pub struct VerifiedImage { _private: () }

pub struct FwUpdate<S> {
    bmc_addr: String,
    _state: S,
}

impl FwUpdate<Idle> {
    pub fn new(bmc_addr: &str) -> Self {
        FwUpdate { bmc_addr: bmc_addr.to_string(), _state: Idle }
    }
    pub fn begin_upload(self) -> FwUpdate<Uploading> {
        FwUpdate { bmc_addr: self.bmc_addr, _state: Uploading { bytes_sent: 0 } }
    }
}

impl FwUpdate<Uploading> {
    pub fn send_chunk(mut self, chunk: &[u8]) -> Self {
        self._state.bytes_sent += chunk.len();
        self
    }
    pub fn finish_upload(self) -> FwUpdate<Verifying> {
        FwUpdate { bmc_addr: self.bmc_addr, _state: Verifying }
    }
    /// Abort доступен во время загрузки: возврат в Idle.
    pub fn abort(self) -> FwUpdate<Idle> {
        FwUpdate { bmc_addr: self.bmc_addr, _state: Idle }
    }
}

impl FwUpdate<Verifying> {
    /// При успехе возвращает следующее состояние И токен-доказательство VerifiedImage.
    pub fn verify(self) -> Result<(FwUpdate<Applying>, VerifiedImage), FwUpdate<Idle>> {
        // Реально: проверяем CRC, подпись, совместимость
        let token = VerifiedImage { _private: () };
        Ok((
            FwUpdate { bmc_addr: self.bmc_addr, _state: Applying },
            token,
        ))
    }
    /// Abort доступен во время проверки.
    pub fn abort(self) -> FwUpdate<Idle> {
        FwUpdate { bmc_addr: self.bmc_addr, _state: Idle }
    }
}

impl FwUpdate<Applying> {
    /// Потребляет токен-доказательство VerifiedImage: без проверки применить нельзя.
    /// Примечание: метода abort() здесь НЕТ, потому что после начала записи это слишком опасно.
    pub fn apply(self, _proof: VerifiedImage) -> FwUpdate<Rebooting> {
        FwUpdate { bmc_addr: self.bmc_addr, _state: Rebooting }
    }
}

impl FwUpdate<Rebooting> {
    pub fn wait_for_reboot(self) -> FwUpdate<Complete> {
        FwUpdate { bmc_addr: self.bmc_addr, _state: Complete }
    }
}

impl FwUpdate<Complete> {
    pub fn version(&self) -> &str { "2.1.0" }
}

// Использование:
// let fw = FwUpdate::new("192.168.1.100")
//     .begin_upload()
//     .send_chunk(b"image_data")
//     .finish_upload();
// let (fw, proof) = fw.verify().map_err(|_| "проверка не пройдена")?;
// let fw = fw.apply(proof).wait_for_reboot();
// println!("Новая версия: {}", fw.version());

Ключевые моменты:

  • abort() существует только у FwUpdate<Uploading> и FwUpdate<Verifying>: вызов его у FwUpdate<Applying> — ошибка компиляции, а не проверка во время выполнения.
  • VerifiedImage имеет приватное поле, поэтому создать его может только verify().
  • apply() потребляет токен-доказательство: пропустить проверку нельзя.

Упражнение 3: конвейер показаний датчиков (анализ размерностей)

Постройте полный конвейер обработки показаний датчиков:

  1. Определите newtype: RawAdc, Celsius, Fahrenheit, Volts, Millivolts, Watts
  2. Реализуйте From<Celsius> for Fahrenheit и наоборот
  3. Создайте impl Mul<Volts, Output=Watts> for Amperes (P = V × I)
  4. Постройте обобщённый проверщик порогов Threshold<T>
  5. Напишите конвейер: АЦП → калибровка → проверка порога → результат

Компилятор должен отвергать: сравнение Celsius с Volts, сложение Watts с Rpm, передачу Millivolts там, где ожидаются Volts.

Пример решения (упражнение 3)
use std::ops::{Add, Sub, Mul};

#[derive(Debug, Clone, Copy, PartialEq, PartialOrd)]
pub struct RawAdc(pub u16);

#[derive(Debug, Clone, Copy, PartialEq, PartialOrd)]
pub struct Celsius(pub f64);

#[derive(Debug, Clone, Copy, PartialEq, PartialOrd)]
pub struct Fahrenheit(pub f64);

#[derive(Debug, Clone, Copy, PartialEq, PartialOrd)]
pub struct Volts(pub f64);

#[derive(Debug, Clone, Copy, PartialEq, PartialOrd)]
pub struct Millivolts(pub f64);

#[derive(Debug, Clone, Copy, PartialEq, PartialOrd)]
pub struct Amperes(pub f64);

#[derive(Debug, Clone, Copy, PartialEq, PartialOrd)]
pub struct Watts(pub f64);

// --- Безопасные преобразования ---
impl From<Celsius> for Fahrenheit {
    fn from(c: Celsius) -> Self { Fahrenheit(c.0 * 9.0 / 5.0 + 32.0) }
}
impl From<Fahrenheit> for Celsius {
    fn from(f: Fahrenheit) -> Self { Celsius((f.0 - 32.0) * 5.0 / 9.0) }
}
impl From<Millivolts> for Volts {
    fn from(mv: Millivolts) -> Self { Volts(mv.0 / 1000.0) }
}
impl From<Volts> for Millivolts {
    fn from(v: Volts) -> Self { Millivolts(v.0 * 1000.0) }
}

// --- Арифметика для величин одной единицы ---
// ПРИМЕЧАНИЕ: сложение абсолютных температур (25°C + 30°C) физически сомнительно.
// См. в гл. 06 обсуждение newtype для ΔT: там подход строже. Здесь для упражнения оставим всё просто.
impl Add for Celsius {
    type Output = Celsius;
    fn add(self, rhs: Self) -> Celsius { Celsius(self.0 + rhs.0) }
}
impl Sub for Celsius {
    type Output = Celsius;
    fn sub(self, rhs: Self) -> Celsius { Celsius(self.0 - rhs.0) }
}

// P = V × I  (умножение разных единиц)
impl Mul<Amperes> for Volts {
    type Output = Watts;
    fn mul(self, rhs: Amperes) -> Watts { Watts(self.0 * rhs.0) }
}

// --- Обобщённый проверщик порогов ---
// Упражнение 3 расширяет Threshold из гл. 06 обобщённым ThresholdResult<T>,
// который несёт показание, вызвавшее срабатывание. Это развитие более простого
// enum ThresholdResult { Normal, Warning, Critical } из гл. 06.
pub enum ThresholdResult<T> {
    Normal(T),
    Warning(T),
    Critical(T),
}

pub struct Threshold<T> {
    pub warning: T,
    pub critical: T,
}

// Обобщённая реализация: работает для любого типа единиц, поддерживающего PartialOrd.
impl<T: PartialOrd + Copy> Threshold<T> {
    pub fn check(&self, reading: T) -> ThresholdResult<T> {
        if reading >= self.critical {
            ThresholdResult::Critical(reading)
        } else if reading >= self.warning {
            ThresholdResult::Warning(reading)
        } else {
            ThresholdResult::Normal(reading)
        }
    }
}
// Теперь `Threshold<Rpm>`, `Threshold<Volts>` и т. д. работают автоматически.

// --- Конвейер: АЦП → калибровка → порог → результат ---
pub struct CalibrationParams {
    pub scale: f64,  // отсчётов АЦП на °C
    pub offset: f64, // °C при АЦП 0
}

pub fn calibrate(raw: RawAdc, params: &CalibrationParams) -> Celsius {
    Celsius(raw.0 as f64 / params.scale + params.offset)
}

pub fn sensor_pipeline(
    raw: RawAdc,
    params: &CalibrationParams,
    threshold: &Threshold<Celsius>,
) -> ThresholdResult<Celsius> {
    let temp = calibrate(raw, params);
    threshold.check(temp)
}

// Проверка на этапе компиляции: эти строки НЕ скомпилируются:
// let _ = Celsius(25.0) + Volts(12.0);   // ОШИБКА: mismatched types
// let _: Millivolts = Volts(1.0);         // ОШИБКА: no implicit coercion
// let _ = Watts(100.0) + Rpm(3000);       // ОШИБКА: mismatched types

Ключевые моменты:

  • Каждая физическая величина — отдельный тип: случайного смешивания не происходит.
  • Mul<Amperes> for Volts даёт Watts, кодируя P = V × I в системе типов.
  • Явные преобразования From для связанных единиц (мВ ↔ В, °C ↔ °F).
  • Threshold<Celsius> принимает только Celsius: нельзя случайно проверить порог для об/мин.

Упражнение 4: обход возможностей PCIe (phantom-типы + проверенная граница)

Смоделируйте связный список возможностей PCIe:

  1. RawCapability: непроверенные байты из конфигурационного пространства
  2. ValidCapability: разобранная и проверенная (через TryFrom)
  3. Каждый тип возможности (MSI, MSI-X, PCI Express, Power Management) имеет собственную раскладку регистров с phantom-типом
  4. Обход списка возвращает итератор значений ValidCapability

Подсказка: совместите проверенные границы (гл. 07) с phantom-типами (гл. 09).

Пример решения (упражнение 4)
use std::marker::PhantomData;

// --- Phantom-маркеры для типов возможностей ---
pub struct Msi;
pub struct MsiX;
pub struct PciExpress;
pub struct PowerMgmt;

// Идентификаторы возможностей PCI из спецификации
const CAP_ID_PM:   u8 = 0x01;
const CAP_ID_MSI:  u8 = 0x05;
const CAP_ID_PCIE: u8 = 0x10;
const CAP_ID_MSIX: u8 = 0x11;

/// Непроверенные байты: могут быть мусором.
#[derive(Debug)]
pub struct RawCapability {
    pub id: u8,
    pub next_ptr: u8,
    pub data: Vec<u8>,
}

/// Проверенная и помеченная типом возможность.
#[derive(Debug)]
pub struct ValidCapability<Kind> {
    id: u8,
    next_ptr: u8,
    data: Vec<u8>,
    _kind: PhantomData<Kind>,
}

// --- TryFrom: граница parse-don't-validate ---
impl TryFrom<RawCapability> for ValidCapability<PowerMgmt> {
    type Error = &'static str;
    fn try_from(raw: RawCapability) -> Result<Self, Self::Error> {
        if raw.id != CAP_ID_PM { return Err("не возможность PM"); }
        if raw.data.len() < 2 { return Err("данные PM слишком короткие"); }
        Ok(ValidCapability {
            id: raw.id, next_ptr: raw.next_ptr,
            data: raw.data, _kind: PhantomData,
        })
    }
}

impl TryFrom<RawCapability> for ValidCapability<Msi> {
    type Error = &'static str;
    fn try_from(raw: RawCapability) -> Result<Self, Self::Error> {
        if raw.id != CAP_ID_MSI { return Err("не возможность MSI"); }
        if raw.data.len() < 6 { return Err("данные MSI слишком короткие"); }
        Ok(ValidCapability {
            id: raw.id, next_ptr: raw.next_ptr,
            data: raw.data, _kind: PhantomData,
        })
    }
}

// (Аналогичные реализации TryFrom для MsiX и PciExpress опущены для краткости)

// --- Типобезопасные методы доступа: доступны только для правильной возможности ---
impl ValidCapability<PowerMgmt> {
    pub fn pm_control(&self) -> u16 {
        u16::from_le_bytes([self.data[0], self.data[1]])
    }
}

impl ValidCapability<Msi> {
    pub fn message_control(&self) -> u16 {
        u16::from_le_bytes([self.data[0], self.data[1]])
    }
    pub fn vectors_requested(&self) -> u32 {
        1 << ((self.message_control() >> 1) & 0x07)
    }
}

impl ValidCapability<MsiX> {
    pub fn table_size(&self) -> u16 {
        (u16::from_le_bytes([self.data[0], self.data[1]]) & 0x07FF) + 1
    }
}

// --- Обходчик возможностей: проходит по связному списку ---
pub struct CapabilityWalker<'a> {
    config_space: &'a [u8],
    next_ptr: u8,
}

impl<'a> CapabilityWalker<'a> {
    pub fn new(config_space: &'a [u8]) -> Self {
        // Указатель на первую возможность находится по смещению 0x34 в конфигурационном пространстве PCI
        let first_ptr = if config_space.len() > 0x34 {
            config_space[0x34]
        } else { 0 };
        CapabilityWalker { config_space, next_ptr: first_ptr }
    }
}

impl<'a> Iterator for CapabilityWalker<'a> {
    type Item = RawCapability;
    fn next(&mut self) -> Option<RawCapability> {
        if self.next_ptr == 0 { return None; }
        let off = self.next_ptr as usize;
        if off + 2 > self.config_space.len() { return None; }
        let id = self.config_space[off];
        let next = self.config_space[off + 1];
        let end = if next > 0 { next as usize } else {
            (off + 16).min(self.config_space.len())
        };
        let data = self.config_space[off + 2..end].to_vec();
        self.next_ptr = next;
        Some(RawCapability { id, next_ptr: next, data })
    }
}

// Использование:
// for raw_cap in CapabilityWalker::new(&config_space) {
//     if let Ok(pm) = ValidCapability::<PowerMgmt>::try_from(raw_cap) {
//         println!("Управление PM: 0x{:04X}", pm.pm_control());
//     }
// }

Ключевые моменты:

  • RawCapability → ValidCapability<Kind> — граница parse-don’t-validate.
  • pm_control() существует только у ValidCapability<PowerMgmt>: вызов его у возможности MSI — ошибка компиляции.
  • Итератор CapabilityWalker выдаёт сырые возможности; вызывающий код проверяет нужные ему через TryFrom.

Упражнение 5: проверка здоровья по нескольким протоколам (capability-миксины)

Создайте фреймворк проверки здоровья:

  1. Определите трейты-ингредиенты: HasIpmi, HasRedfish, HasNvmeCli, HasGpio
  2. Создайте трейты-миксины:
    • ThermalHealthMixin (требует HasIpmi + HasGpio): читает температуры, проверяет сигналы тревоги
    • StorageHealthMixin (требует HasNvmeCli): проверки данных SMART
    • BmcHealthMixin (требует HasIpmi + HasRedfish): перекрёстная проверка данных BMC
  3. Постройте FullPlatformController, который реализует все трейты-ингредиенты
  4. Постройте StorageOnlyController, который реализует только HasNvmeCli
  5. Убедитесь, что StorageOnlyController получает StorageHealthMixin, но НЕ остальные
Пример решения (упражнение 5)
// --- Трейты-ингредиенты ---
pub trait HasIpmi {
    fn ipmi_read_sensor(&self, id: u8) -> f64;
}
pub trait HasRedfish {
    fn redfish_get(&self, path: &str) -> String;
}
pub trait HasNvmeCli {
    fn nvme_smart_log(&self, dev: &str) -> SmartData;
}
pub trait HasGpio {
    fn gpio_read_alert(&self, pin: u8) -> bool;
}

pub struct SmartData {
    pub temperature_kelvin: u16,
    pub spare_pct: u8,
}

// --- Трейты-миксины с blanket impl ---
pub trait ThermalHealthMixin: HasIpmi + HasGpio {
    fn thermal_check(&self) -> ThermalStatus {
        let temp = self.ipmi_read_sensor(0x01);
        let alert = self.gpio_read_alert(12);
        ThermalStatus { temperature: temp, alert_active: alert }
    }
}
impl<T: HasIpmi + HasGpio> ThermalHealthMixin for T {}

pub trait StorageHealthMixin: HasNvmeCli {
    fn storage_check(&self) -> StorageStatus {
        let smart = self.nvme_smart_log("/dev/nvme0");
        StorageStatus {
            temperature_ok: smart.temperature_kelvin < 343, // 70 °C
            spare_ok: smart.spare_pct > 10,
        }
    }
}
impl<T: HasNvmeCli> StorageHealthMixin for T {}

pub trait BmcHealthMixin: HasIpmi + HasRedfish {
    fn bmc_health(&self) -> BmcStatus {
        let ipmi_temp = self.ipmi_read_sensor(0x01);
        let rf_temp = self.redfish_get("/Thermal/Temperatures/0");
        BmcStatus { ipmi_temp, redfish_temp: rf_temp, consistent: true }
    }
}
impl<T: HasIpmi + HasRedfish> BmcHealthMixin for T {}

pub struct ThermalStatus { pub temperature: f64, pub alert_active: bool }
pub struct StorageStatus { pub temperature_ok: bool, pub spare_ok: bool }
pub struct BmcStatus { pub ipmi_temp: f64, pub redfish_temp: String, pub consistent: bool }

// --- Полная платформа: все ингредиенты → все три миксина бесплатно ---
pub struct FullPlatformController;

impl HasIpmi for FullPlatformController {
    fn ipmi_read_sensor(&self, _id: u8) -> f64 { 42.0 }
}
impl HasRedfish for FullPlatformController {
    fn redfish_get(&self, _path: &str) -> String { "42.0".into() }
}
impl HasNvmeCli for FullPlatformController {
    fn nvme_smart_log(&self, _dev: &str) -> SmartData {
        SmartData { temperature_kelvin: 310, spare_pct: 95 }
    }
}
impl HasGpio for FullPlatformController {
    fn gpio_read_alert(&self, _pin: u8) -> bool { false }
}

// --- Только хранилище: только HasNvmeCli → только StorageHealthMixin ---
pub struct StorageOnlyController;

impl HasNvmeCli for StorageOnlyController {
    fn nvme_smart_log(&self, _dev: &str) -> SmartData {
        SmartData { temperature_kelvin: 315, spare_pct: 80 }
    }
}

// StorageOnlyController автоматически получает storage_check().
// Вызов thermal_check() или bmc_health() у него — ОШИБКА КОМПИЛЯЦИИ.

Ключевые моменты:

  • Blanket impl<T: HasIpmi + HasGpio> ThermalHealthMixin for T {}: любой тип, реализующий оба ингредиента, автоматически получает миксин.
  • StorageOnlyController реализует только HasNvmeCli, поэтому компилятор даёт ему StorageHealthMixin, но отвергает thermal_check() и bmc_health(): никаких проверок во время выполнения не нужно.
  • Добавление нового миксина (например, NetworkHealthMixin: HasRedfish + HasGpio) — это один трейт и одна blanket impl: существующие контроллеры получат его автоматически, если подходят.

Упражнение 6: диагностический протокол с сессионными типами (одноразовые типы + typestate)

Спроектируйте диагностическую сессию с одноразовыми токенами выполнения тестов:

  1. DiagSession начинает в состоянии Setup
  2. Переход в состояние Running выдаёт N токенов выполнения (по одному на каждый тестовый случай)
  3. Каждый TestToken потребляется при запуске теста, что исключает повторный запуск одного и того же теста
  4. Когда все токены потреблены, переход в состояние Complete
  5. Формирование отчёта (только в состоянии Complete)

Продвинутый вариант: используйте const-обобщение N, чтобы отслеживать на уровне типов, сколько тестов осталось.

Пример решения (упражнение 6)
// --- Типы состояний ---
pub struct Setup;
pub struct Running;
pub struct Complete;

/// Одноразовый токен теста. НЕ Clone, НЕ Copy: потребляется при использовании.
pub struct TestToken {
    test_name: String,
}

#[derive(Debug)]
pub struct TestResult {
    pub test_name: String,
    pub passed: bool,
}

pub struct DiagSession<S> {
    name: String,
    results: Vec<TestResult>,
    _state: S,
}

impl DiagSession<Setup> {
    pub fn new(name: &str) -> Self {
        DiagSession {
            name: name.to_string(),
            results: Vec::new(),
            _state: Setup,
        }
    }

    /// Переход в Running: выдаёт по одному токену на каждый тестовый случай.
    pub fn start(self, test_names: &[&str]) -> (DiagSession<Running>, Vec<TestToken>) {
        let tokens = test_names.iter()
            .map(|n| TestToken { test_name: n.to_string() })
            .collect();
        (
            DiagSession {
                name: self.name,
                results: Vec::new(),
                _state: Running,
            },
            tokens,
        )
    }
}

impl DiagSession<Running> {
    /// Потребляет токен для запуска одного теста. Перемещение исключает двойной запуск.
    pub fn run_test(mut self, token: TestToken) -> Self {
        let passed = true; // реальный код здесь запускает настоящую диагностику
        self.results.push(TestResult {
            test_name: token.test_name,
            passed,
        });
        self
    }

    /// Переход в Complete.
    ///
    /// **Примечание:** это решение НЕ обеспечивает, что все токены потреблены:
    /// `finish()` можно вызвать, когда токены ещё остались. Такие токены просто
    /// уничтожатся (они не помечены `#[must_use]`). Для полной проверки на этапе компиляции
    /// используйте вариант с const-обобщением из примечания «Продвинутый» ниже, где
    /// `finish()` доступен только у `DiagSession<Running, 0>`.
    pub fn finish(self) -> DiagSession<Complete> {
        DiagSession {
            name: self.name,
            results: self.results,
            _state: Complete,
        }
    }
}

impl DiagSession<Complete> {
    /// Отчёт доступен ТОЛЬКО в состоянии Complete.
    pub fn report(&self) -> String {
        let total = self.results.len();
        let passed = self.results.iter().filter(|r| r.passed).count();
        format!("{}: пройдено {}/{}", self.name, passed, total)
    }
}

// Использование:
// let session = DiagSession::new("стресс-тест GPU");
// let (mut session, tokens) = session.start(&["vram", "compute", "thermal"]);
// for token in tokens {
//     session = session.run_test(token);
// }
// let session = session.finish();
// println!("{}", session.report());  // "стресс-тест GPU: пройдено 3/3"
//
// // Это НЕ скомпилируется:
// // session.run_test(used_token);  →  ОШИБКА: use of moved value
// // running_session.report();      →  ОШИБКА: no method `report` on DiagSession<Running>

Ключевые моменты:

  • TestToken не реализует Clone и Copy: потребление через run_test(token) перемещает его, поэтому повторный запуск того же теста — ошибка компиляции.
  • report() существует только у DiagSession<Complete>: вызвать его в процессе выполнения невозможно.
  • Продвинутый вариант использовал бы DiagSession<Running, N> с const-обобщениями, где run_test возвращает DiagSession<Running, {N-1}>, а finish доступен только у DiagSession<Running, 0>. Это гарантирует, что все токены потреблены до завершения.

Ключевые выводы

  1. Практикуйтесь на реалистичных протоколах: NVMe, обновление прошивки, конвейеры датчиков и PCIe — реальные цели для этих паттернов.
  2. Каждое упражнение соответствует основной главе: используйте перекрёстные ссылки, чтобы повторить паттерн перед попыткой.
  3. Решения спрятаны в раскрывающихся блоках: попробуйте каждое упражнение, прежде чем открывать решение.
  4. Сочетайте паттерны в упражнении 5: проверки здоровья по нескольким протоколам объединяют типизированные команды, размерные типы и проверенные границы.
  5. Сессионные типы (упражнение 6) — передний край: они обеспечивают порядок сообщений в каналах, распространяя typestate на распределённые системы.

Краткая шпаргалка

Краткая справка по всем 14+ паттернам корректности по построению с блок-схемой выбора, каталогом паттернов, правилами композиции, сопоставлением с крейтами и шпаргалкой по гарантиям на уровне типов.

Перекрёстные ссылки: все главы: это таблица для поиска по всей книге.

Краткая справка: паттерны корректности по построению

Руководство по выбору паттерна

Ошибка катастрофична, если её пропустить?
├── Да → Её можно закодировать в типах?
│         ├── Да → ИСПОЛЬЗУЙТЕ КОРРЕКТНОСТЬ ПО ПОСТРОЕНИЮ
│         └── Нет → Проверка во время выполнения + обширное тестирование
└── Нет → Проверка во время выполнения подойдёт

Каталог паттернов

№ПаттернКлючевой трейт/типПредотвращаетСтоимость во время выполненияГлава
1Типизированные командыtrait IpmiCmd { type Response; }Неверный тип ответаНулеваягл. 02
2Одноразовые типыstruct Nonce (не Clone/Copy)Повторное использование nonce/ключаНулеваягл. 03
3Capability-токеныstruct AdminToken { _private: () }Несанкционированный доступНулеваягл. 04
4TypestateSession<Active>Нарушения протоколаНулеваягл. 05
5Размерные типыstruct Celsius(f64)Путаница единицНулеваягл. 06
6Проверенные границыstruct ValidFru (через TryFrom)Использование непроверенных данныхРазбор один разгл. 07
7Capability-миксиныtrait FanDiagMixin: HasSpi + HasI2cОтсутствие доступа к шинеНулеваягл. 08
8Phantom-типыRegister<Width16>Несоответствие ширины/направленияНулеваягл. 09
9Sentinel → OptionOption<u8> (не 0xFF)Ошибки «sentinel как значение»Нулеваягл. 11
10Sealed-трейтыtrait Cmd: private::SealedНебезопасные внешние реализацииНулеваягл. 11
11Non-exhaustive enum#[non_exhaustive] enum SkuТихое проваливание в matchНулеваягл. 11
12Билдер с typestateDerBuilder<Set, Missing>Неполное создание объектаНулеваягл. 11
13Валидация через FromStrimpl FromStr for DiagLevelНепроверенный строковый вводРазбор один разгл. 11
14Размер через const-обобщенияRegisterBank<const N: usize>Несоответствие размера буфераНулеваягл. 11
15Безопасная обёртка unsafeMmioRegion::read_u32()Непроверенный MMIO/FFIНулеваягл. 11
16Async typestateAsyncSession<Active>Нарушения async-протоколаНулеваягл. 11
17Const-утвержденияSdrSensorId<const N: u8>Недопустимые ID на этапе компиляцииНулеваягл. 11
18Сессионные типыChan<SendRequest>Операции с каналом не по порядкуНулеваягл. 11
19Самоссылочный PinPin<Box<StreamParser>>Висячий внутренний указательНулеваягл. 11
20RAII / Dropimpl Drop for SessionУтечка ресурса при любом пути выходаНулеваягл. 11
21Иерархия типов ошибок#[derive(Error)] enum DiagErrorТихое проглатывание ошибокНулеваягл. 11
22#[must_use]#[must_use] struct TokenТихо отброшенные значенияНулеваягл. 11

Правила композиции

Capability-токен + Typestate = Авторизованные переходы состояний
Типизированная команда + Размерный тип = Физически типизированные ответы
Проверенная граница + Phantom-тип = Типизированный доступ к регистрам по проверенной конфигурации
Capability-миксин + Типизированная команда = Типизированные операции с учётом шин
Одноразовый тип + Typestate = Протоколы «потребляется при переходе»
Sealed-трейт + Типизированная команда = Закрытый, корректный набор команд
Sentinel → Option + Проверенная граница = Чистый конвейер «разбор один раз»
Билдер с typestate + Capability-токен = Доказательство полного создания
FromStr + #[non_exhaustive] = Развиваемый разбор enum с быстрым отказом
Размер через const-обобщения + Проверенная граница = Размерные, проверенные протокольные буферы
Безопасная обёртка unsafe + Phantom-тип = Типизированный и безопасный доступ к MMIO
Async typestate + Capability-токен = Авторизованные async-переходы
Сессионные типы + Типизированная команда = Полностью типизированные каналы «запрос — ответ»
Pin + Typestate = Самоссылочные автоматы состояний, которые не могут переместиться
RAII (Drop) + Typestate = Гарантии очистки, зависящей от состояния
Иерархия ошибок + Проверенная граница = Типизированные ошибки разбора с исчерпывающей обработкой
#[must_use] + Одноразовый тип = Токены, которые трудно игнорировать и трудно переиспользовать

Антипаттерны, которых следует избегать

АнтипаттернПочему это неверноПравильная альтернатива
fn read_sensor() -> f64Без единиц: может быть °C, °F или об/минfn read_sensor() -> Celsius
fn encrypt(nonce: &[u8; 12])Nonce можно переиспользовать (заимствование)fn encrypt(nonce: Nonce) (перемещение)
fn admin_op(is_admin: bool)Вызывающий может соврать (true)fn admin_op(_: &AdminToken)
fn send(session: &Session)Нет гарантии состоянияfn send(session: &Session<Active>)
fn process(data: &[u8])Не провереноfn process(data: &ValidFru)
Clone для эфемерных ключейСводит на нет гарантию одноразовостиНе выводите Clone
let vendor_id: u16 = 0xFFFFSentinel хранится внутриlet vendor_id: Option<u16> = None
fn route(level: &str) с запасным вариантомОпечатки тихо заменяются значением по умолчаниюlet level: DiagLevel = s.parse()?
Builder::new().finish() без полейСоздаётся неполный объектБилдер с typestate: finish() открывается при Set
let buf: Vec<u8> для аппаратного буфера фиксированного размераРазмер проверяется только во время выполненияRegisterBank<4096> (const-обобщение)
Разбросанный сырой unsafe { ptr::read(...) }Риск UB, не поддаётся аудитуMmioRegion::read_u32(): безопасная обёртка
async fn transition(&mut self)Изменяемые заимствования не обеспечивают состояниеasync fn transition(self) -> NextState
fn cleanup(), вызываемая вручнуюЗабывается при раннем возврате или паникеimpl Drop: компилятор вставляет вызов
fn op() -> Result<T, String>Непрозрачная ошибка, нельзя сопоставить вариантыfn op() -> Result<T, DiagError>: enum

Сопоставление с кодовой базой диагностики

МодульПрименимые паттерны
protocol_libТипизированные команды, сессии с typestate
thermal_diagCapability-миксины, размерные типы
accel_diagПроверенные границы, phantom-регистры
network_diagTypestate (обучение линка), capability-токены
pci_topologyPhantom-типы (ширина регистра), проверенная конфигурация, sentinel → Option
event_handlerОдноразовые токены аудита, capability-токены, FromStr (Component)
event_logПроверенные границы (разбор записей SEL)
compute_diagРазмерные типы (температура, частота)
memory_diagПроверенные границы (данные SPD), размерные типы
switch_diagTypestate (перечисление портов), phantom-типы
config_loaderFromStr (DiagLevel, FaultStatus, DiagAction)
log_analyzerПроверенные границы (CompiledPatterns)
diag_frameworkБилдер с typestate (DerBuilder), сессионные типы (оркестратор ↔ воркер)
topology_libConst-обобщённые банки регистров, безопасные обёртки MMIO

Типы как гарантии — краткое соответствие

ГарантияЭквивалент в RustПример
«Это доказательство существует»ТипAdminToken
«У меня есть доказательство»Значение этого типаlet tok = authenticate()?;
«A влечёт B»Функция fn(A) -> Bfn activate(AdminToken) -> Session<Active>
«И A, и B»Кортеж (A, B) или несколько параметровfn op(a: &AdminToken, b: &LinkTrained)
«Либо A, либо B»enum { A(A), B(B) } или Result<A, B>Result<Session<Active>, Error>
«Всегда истинно»() (тип unit)Всегда конструируется
«Невозможно»! (тип never) или enum Void {}Никогда не может быть сконструирована

Тестирование гарантий на уровне типов 🟡

Что вы узнаете: как проверить, что некорректный код не компилируется (trybuild), как подвергнуть фаззингу проверенные границы (proptest), как проверить инварианты RAII и как доказать бесплатность абстракций с помощью cargo-show-asm.

Перекрёстные ссылки: гл. 03 (compile-fail для nonce), гл. 07 (proptest для границ), гл. 05 (RAII для сессий)

Тестирование гарантий на уровне типов

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

Compile-fail тесты с trybuild

Крейт trybuild позволяет утверждать, что определённый код не должен компилироваться. Это важно для сохранения инвариантов уровня типов при рефакторинге: если кто-то случайно добавит Clone к вашему одноразовому Nonce, compile-fail тест это поймает.

Настройка:

# Cargo.toml
[dev-dependencies]
trybuild = "1"

Файл теста (tests/compile_fail.rs):

#[test]
fn type_safety_tests() {
    let t = trybuild::TestCases::new();
    t.compile_fail("tests/ui/*.rs");
}

Тест: повторное использование nonce не должно компилироваться (tests/ui/nonce_reuse.rs):

// tests/ui/nonce_reuse.rs
use my_crate::Nonce;

fn main() {
    let nonce = Nonce::new();
    encrypt(nonce);
    encrypt(nonce); // должно быть ошибкой: использование перемещённого значения
}

fn encrypt(_n: Nonce) {}

Ожидаемая ошибка (tests/ui/nonce_reuse.stderr):

error[E0382]: use of moved value: `nonce`
 --> tests/ui/nonce_reuse.rs:6:13
  |
4 |     let nonce = Nonce::new();
  |         ----- move occurs because `nonce` has type `Nonce`, which does not implement the `Copy` trait
5 |     encrypt(nonce);
  |             ----- value moved here
6 |     encrypt(nonce); // должно быть ошибкой: использование перемещённого значения
  |             ^^^^^ value used here after move

Другие compile-fail тесты по главам:

Паттерн (глава)Проверяемое утверждениеФайл
Одноразовый nonce (гл. 03)Нельзя использовать nonce дваждыnonce_reuse.rs
Capability-токен (гл. 04)Нельзя вызвать admin_op() без токенаmissing_token.rs
Typestate (гл. 05)Нельзя вызвать send_command() у Session<Idle>wrong_state.rs
Размерные типы (гл. 06)Нельзя сложить Celsius + Rpmunit_mismatch.rs
Sealed-трейт (приём 2)Внешний крейт не может реализовать sealed-трейтunseal_attempt.rs
Non-exhaustive (приём 3)Внешний match без подстановки не компилируетсяmissing_wildcard.rs

Интеграция с CI:

# .github/workflows/ci.yml
- name: Запуск compile-fail тестов
  run: cargo test --test compile_fail

Property-based тестирование проверенных границ

Проверенные границы (гл. 07) разбирают данные один раз и отвергают некорректный ввод. Но откуда знать, что проверка ловит все некорректные входные данные? Property-based тестирование с proptest генерирует тысячи случайных входных данных, чтобы нагрузить границу:

# Cargo.toml
[dev-dependencies]
proptest = "1"
use proptest::prelude::*;

/// Из гл. 07: ValidFru оборачивает полезную нагрузку FRU, соответствующую спецификации.
/// В этих тестах используется полный ValidFru из гл. 07 с методами board_area(),
/// product_area() и format_version().
/// Примечание: в гл. 07 определён TryFrom<RawFruData>, поэтому сначала оборачиваем сырые байты.

proptest! {
    /// Любая последовательность байтов, прошедшая проверку, должна использоваться без паники.
    #[test]
    fn valid_fru_never_panics(data in proptest::collection::vec(any::<u8>(), 0..1024)) {
        if let Ok(fru) = ValidFru::try_from(RawFruData(data)) {
            // Эти методы не должны паниковать для проверенного FRU
            // (методы из реализации ValidFru в гл. 07):
            let _ = fru.format_version();
            let _ = fru.board_area();
            let _ = fru.product_area();
        }
    }

    /// Круговой тест: format_version сохраняется при повторном разборе.
    #[test]
    fn fru_round_trip(data in valid_fru_strategy()) {
        let raw = RawFruData(data.clone());
        let fru = ValidFru::try_from(raw).unwrap();
        let version = fru.format_version();
        // Повторно разбираем те же байты: версия должна совпасть
        let reparsed = ValidFru::try_from(RawFruData(data)).unwrap();
        prop_assert_eq!(version, reparsed.format_version());
    }
}

/// Произвольная стратегия: генерирует векторы байтов, удовлетворяющие заголовку FRU.
/// Формат заголовка соответствует проверке `TryFrom<RawFruData>` из гл. 07:
///   - Байт 0: версия = 0x01
///   - Байты 1–6: смещения областей (×8 = фактическое смещение в байтах)
///   - Байт 7: контрольная сумма (сумма байтов 0–7 = 0 по модулю 256)
/// Тело случайное, но достаточно большое, чтобы смещения оставались в пределах.
fn valid_fru_strategy() -> impl Strategy<Value = Vec<u8>> {
    let header = vec![0x01, 0x00, 0x01, 0x02, 0x00, 0x00, 0x00];
    proptest::collection::vec(any::<u8>(), 64..256)
        .prop_map(move |body| {
            let mut fru = header.clone();
            let sum: u8 = fru.iter().fold(0u8, |a, &b| a.wrapping_add(b));
            fru.push(0u8.wrapping_sub(sum));
            fru.extend_from_slice(&body);
            fru
        })
}

Пирамида тестирования для кода с корректностью по построению:

┌───────────────────────────────────┐
│    Compile-fail тесты (trybuild)  │ ← «Некорректный код не должен компилироваться»
├───────────────────────────────────┤
│  Property-тесты (proptest/quickcheck) │ ← «Корректные входные данные никогда не вызывают панику»
├───────────────────────────────────┤
│    Модульные тесты (#[test])       │ ← «Конкретные входные данные дают ожидаемый результат»
├───────────────────────────────────┤
│    Система типов (гл. 02–13)      │ ← «Целые классы ошибок не могут существовать»
└───────────────────────────────────┘

Проверка RAII

RAII (приём 12) гарантирует очистку. Чтобы это проверить, убедимся, что реализация Drop действительно срабатывает:

use std::sync::atomic::{AtomicBool, Ordering};

// ПРИМЕЧАНИЕ: эти тесты используют глобальный AtomicBool, поэтому их нельзя запускать
// параллельно друг с другом. Используйте `#[serial_test::serial]` или запускайте с
// `cargo test -- --test-threads=1`. Либо используйте `Arc<AtomicBool>` для каждого теста,
// передаваемый через замыкание, чтобы полностью обойтись без глобального состояния.
static DROPPED: AtomicBool = AtomicBool::new(false);

struct TestSession;
impl Drop for TestSession {
    fn drop(&mut self) {
        DROPPED.store(true, Ordering::SeqCst);
    }
}

#[test]
fn session_drops_on_early_return() {
    DROPPED.store(false, Ordering::SeqCst);
    let result: Result<(), &str> = (|| {
        let _session = TestSession;
        Err("имитация сбоя")?;
        Ok(())
    })();
    assert!(result.is_err());
    assert!(DROPPED.load(Ordering::SeqCst), "Drop должен сработать при раннем возврате");
}

#[test]
fn session_drops_on_panic() {
    DROPPED.store(false, Ordering::SeqCst);
    let result = std::panic::catch_unwind(|| {
        let _session = TestSession;
        panic!("имитация паники");
    });
    assert!(result.is_err());
    assert!(DROPPED.load(Ordering::SeqCst), "Drop должен сработать при панике");
}

Применение к вашей кодовой базе

Вот приоритизированный план добавления тестов на уровне типов в рабочую область:

КрейтТип тестаЧто тестировать
protocol_libCompile-failSession<Idle> не может вызвать send_command()
protocol_libPropertyЛюбая последовательность байтов → TryFrom либо успешно завершается, либо возвращает Err (без паники)
thermal_diagCompile-failНельзя создать FanReading без миксина HasSpi
accel_diagPropertyРазбор показаний GPU: случайные байты → либо проверены, либо отвергнуты
config_loaderPropertyСлучайные строки → FromStr для DiagLevel никогда не паникует
pci_topologyCompile-failRegister<Width16> нельзя передать туда, где ожидается Width32
event_handlerCompile-failТокен аудита нельзя клонировать
diag_frameworkCompile-failDerBuilder<Missing, _> не может вызвать finish()

Бесплатные абстракции: доказательство через ассемблер

Распространённый вопрос: «Добавляют ли newtype и phantom-типы накладные расходы во время выполнения?» Ответ: нет. Они компилируются в такой же ассемблер, как и голые примитивы. Вот как это проверить.

Настройка:

cargo install cargo-show-asm

Пример: newtype против голого u32:

// src/lib.rs
#[derive(Clone, Copy)]
pub struct Rpm(pub u32);

#[derive(Clone, Copy)]
pub struct Celsius(pub f64);

// Арифметика через newtype
#[inline(never)]
pub fn add_rpm(a: Rpm, b: Rpm) -> Rpm {
    Rpm(a.0 + b.0)
}

// Голая арифметика (для сравнения)
#[inline(never)]
pub fn add_raw(a: u32, b: u32) -> u32 {
    a + b
}

Запуск:

cargo asm my_crate::add_rpm
cargo asm my_crate::add_raw

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

; add_rpm (newtype)           ; add_raw (голый u32)
my_crate::add_rpm:            my_crate::add_raw:
  lea eax, [rdi + rsi]         lea eax, [rdi + rsi]
  ret                          ret

Обёртка Rpm полностью стирается на этапе компиляции. То же верно для PhantomData<S> (ноль байт), токенов ZST (ноль байт) и всех остальных маркеров уровня типов, которые используются в этом руководстве.

Проверка своих типов:

# Показать ассемблер для конкретной функции
cargo asm --lib ipmi_lib::session::execute

# Показать, что PhantomData не добавляет байтов
cargo asm --lib --rust ipmi_lib::session::IpmiSession

Ключевой вывод: каждый паттерн в этом руководстве имеет нулевую стоимость во время выполнения. Система типов делает всю работу и полностью стирается при компиляции. Вы получаете безопасность Haskell с производительностью C.

Ключевые выводы

  1. trybuild проверяет, что некорректный код не компилируется: это важно для сохранения инвариантов уровня типов при рефакторинге.
  2. proptest подвергает фаззингу границы валидации: генерирует тысячи случайных входных данных, чтобы нагрузить реализации TryFrom.
  3. Проверка RAII подтверждает, что Drop выполняется: счётчики Arc или флаги-заглушки доказывают, что очистка произошла.
  4. cargo-show-asm доказывает нулевую стоимость: phantom-типы, ZST и newtype дают тот же ассемблер, что и голый C.
  5. Добавляйте compile-fail тесты для каждого «невозможного» состояния: если кто-то случайно выведет Clone для одноразового типа, тест это поймает.

Конец книги «Корректность на уровне типов в Rust»