Проекты Блог Музыка Контакты
← Все посты
Технологии 17 марта 2026 г.

Парсинг вместо валидации: Типобезопасный дизайн в Rust для неуязвимого кода

Автор: Евгений Падежнов

Illustration for: Парсинг вместо валидации: Типобезопасный дизайн в Rust для неуязвимого кода

Типобезопасный дизайн преобразует проверки во время выполнения в гарантии во время компиляции. Система типов Rust делает этот подход особенно мощным.

Основной принцип: распарсить входные данные один раз в тип, который представляет валидные данные. После парсинга система типов гарантирует корректность данных во всей кодовой базе. Никаких повторных проверок. Никакого защитного программирования.

Проблема традиционной валидации

Большинство подходов к валидации следуют этой схеме:

fn process_email(email: &str) -> Result<(), Error> {
    validate_email(email)?;
    // ... используем email
    send_welcome_email(email)?;
    // ... больше кода
    update_user_email(email)?;
    Ok(())
}

fn send_welcome_email(email: &str) -> Result<(), Error> {
    // Проверяем снова? Доверяем вызывающей стороне?
    validate_email(email)?;
    // ... отправляем email
}

Распространённая ошибка: Функции либо проверяют данные повторно (потеря производительности), либо доверяют вызывающей стороне (ошибки времени выполнения).

Паттерн "Парсинг вместо валидации"

Вместо этого, парсим один раз в новый тип:

#[derive(Debug, Clone)]
pub struct Email(String);

impl Email {
    pub fn parse(s: &str) -> Result<Self, ValidationError> {
        if s.contains('@') && s.len() > 3 {
            Ok(Email(s.to_string()))
        } else {
            Err(ValidationError::InvalidEmail)
        }
    }
    
    pub fn as_str(&self) -> &str {
        &self.0
    }
}

fn process_email(email: Email) -> Result<(), Error> {
    // Валидация не нужна - тип гарантирует корректность
    send_welcome_email(&email)?;
    update_user_email(&email)?;
    Ok(())
}

Ключевой момент: После парсинга тип Email гарантирует валидность email во всём приложении.

Продвинутые паттерны с фантомными типами

Для более сложной валидации фантомные типы кодируют состояние в системе типов:

use std::marker::PhantomData;

struct Validated;
struct Unvalidated;

struct User<State> {
    name: String,
    email: String,
    _state: PhantomData<State>,
}

impl User<Unvalidated> {
    fn new(name: String, email: String) -> Self {
        User {
            name,
            email,
            _state: PhantomData,
        }
    }
    
    fn validate(self) -> Result<User<Validated>, ValidationError> {
        // Логика валидации
        if self.email.contains('@') {
            Ok(User {
                name: self.name,
                email: self.email,
                _state: PhantomData,
            })
        } else {
            Err(ValidationError::InvalidEmail)
        }
    }
}

impl User<Validated> {
    fn save_to_database(&self) -> Result<(), DbError> {
        // Только валидированные пользователи могут быть сохранены
        // ...
    }
}

На практике: Ошибка компиляции при попытке сохранить невалидированного пользователя. Проверки времени выполнения не нужны.

Пример из реального мира: Парсинг конфигурации

#[derive(Debug)]
pub struct Config {
    port: Port,
    database_url: DatabaseUrl,
    api_key: ApiKey,
}

#[derive(Debug, Clone)]
pub struct Port(u16);

impl Port {
    pub fn parse(s: &str) -> Result<Self, ParseError> {
        let port: u16 = s.parse()?;
        if port > 1024 {
            Ok(Port(port))
        } else {
            Err(ParseError::PrivilegedPort)
        }
    }
}

#[derive(Debug, Clone)]
pub struct DatabaseUrl(String);

impl DatabaseUrl {
    pub fn parse(s: &str) -> Result<Self, ParseError> {
        if s.starts_with("postgres://") || s.starts_with("mysql://") {
            Ok(DatabaseUrl(s.to_string()))
        } else {
            Err(ParseError::InvalidDatabaseUrl)
        }
    }
}

// Парсим конфигурацию один раз при запуске
fn load_config() -> Result<Config, ParseError> {
    Ok(Config {
        port: Port::parse(&env::var("PORT")?)?,
        database_url: DatabaseUrl::parse(&env::var("DATABASE_URL")?)?,
        api_key: ApiKey::parse(&env::var("API_KEY")?)?,
    })
}

Протестировано в продакшене. Парсинг конфигурации быстро завершается с ошибкой при запуске. Гарантии валидности конфигурации во время выполнения.

Преимущества на практике

  1. Нет защитного программирования - Типы гарантируют валидность
  2. Чёткие границы ошибок - Парсинг происходит на границах системы
  3. Лучшая производительность - Валидируем один раз, а не повторно
  4. Безопасность во время компиляции - Некорректные операции не компилируются

Попробуйте: Замените строковые параметры на новые типы в одном модуле. Наблюдайте, как исчезают ошибки времени выполнения.

Распространённые паттерны и антипаттерны

Делайте так:

pub struct UserId(u64);
pub struct Temperature(f64);
pub struct Percentage(u8); // 0-100 обеспечивается при парсинге

Избегайте этого:

fn process_data(user_id: u64, temp: f64, percent: u8) {
    if percent > 100 { panic!("Invalid percentage"); }
    // ...
}

Простыми словами: Сделайте недопустимые состояния непредставимыми. Компилятор становится вашим механизмом валидации.

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

Этот паттерн выходит за рамки простой валидации. Используйте его для конечных автоматов, реализации протоколов, любых областей, где определённые состояния должны быть невозможны. Нулевая стоимость абстракций в Rust означает отсутствие накладных расходов во время выполнения для этой безопасности.

Информация актуальна на момент публикации. Условия, цены и правила могут измениться — уточняйте у профильных специалистов.

Выжимка AI
  1. Традиционная валидация либо повторяет проверки (потеря производительности), либо доверяет вызывающей стороне (ошибки в runtime). Парсинг один раз в специализированный тип решает обе проблемы — система типов Rust гарантирует корректность данных во всей кодовой базе без повторных проверок.
  2. После парсинга входных данных в новый тип (например, Email) система типов гарантирует, что все остальные функции работают только с валидными данными, исключая ошибки валидации на уровне компиляции, а не runtime.
  3. Фантомные типы кодируют состояние валидации прямо в типе (Validated/Unvalidated), заставляя вызвать validate() перед использованием — компилятор предотвращает сохранение невалидированных данных, делая проверки времени выполнения ненужными.

Powered by B1KEY