From 3c3487b5546d735923a7eb400b134b5ff5bdb4a3 Mon Sep 17 00:00:00 2001 From: Stanislav N Mikhailov Date: Fri, 7 Aug 2026 17:46:07 +0300 Subject: [PATCH] feat(protocol): add typed command decoding and dispatch - add Command enum for protocol command codes - convert raw u8 command values via TryFrom - validate commands through Command conversion - dispatch decoded commands in Connection - reject unknown command codes --- src/connection.rs | 32 ++++++- src/main.MD | 236 +++++++++++++++++++++++----------------------- src/protocol.MD | 13 +-- src/protocol.rs | 48 ++++++++-- 4 files changed, 196 insertions(+), 133 deletions(-) diff --git a/src/connection.rs b/src/connection.rs index d90b33e..50dbc58 100644 --- a/src/connection.rs +++ b/src/connection.rs @@ -13,6 +13,7 @@ use crate::protocol::{ Header, HEADER_SIZE, MAX_BODY_SIZE, + Command, }; struct IncomingMessage { @@ -100,10 +101,37 @@ impl Connection { ) })?; + //Заголовок скачен и валиден. Получаем команду из заголовка + match header.command() { + Command::Upload => { + println!("Команда UPLOAD"); + } + + Command::Download => { + println!("Команда DOWNLOAD"); + } + + Command::Delete => { + println!("Команда DELETE"); + } + + Command::Stat => { + println!("Команда STAT"); + } + + Command::FileData => { + println!("Команда FILEDATA"); + } + + Command::Response => { + println!("Команда RESPONSE"); + } + } + Ok(Some(header)) } - async fn read_body( + async fn read_body( //todo: анахронизм, убрать после реализации storage &mut self, body_size: u64, ) -> io::Result> { @@ -146,7 +174,7 @@ impl Connection { ); let response = format!( - "Соединение {}: запрос {} принят, cmd={}\n", + "Соединение {}: запрос {} принят, cmd={:?}\n", self.id, message.header.request_id(), message.header.command(), diff --git a/src/main.MD b/src/main.MD index 84d5ce1..55b1a6a 100644 --- a/src/main.MD +++ b/src/main.MD @@ -1,22 +1,25 @@ # Статус разработки проекта ## 📌 Текущее состояние -* **Где остановился:** Окультурил контракт между protocol connection, работает точно также, но -* архитектурно красивей. -* Далее, нужно реализовать UPLOAD с проверкой валидностей имени файла, размеров. И последующей передачей в storage/ -* Ниже - чеклист для реализации. -* **Проблема:** -* - нет проблем 😇 + +- **Где остановился:** Окультурил контракт между protocol connection, работает точно также, но +- архитектурно красивей. +- Далее, нужно реализовать UPLOAD с проверкой валидностей имени файла, размеров. И последующей передачей в storage/ +- Ниже - чеклист для реализации. +- **Проблема:** +- - нет проблем 😇 + ## Чек-лист реализации `UPLOAD` ### 1. Подготовить диспетчеризацию команд -* [ ] Оставить `decode_header()` единственной публичной функцией получения проверенного `Header`. -* [ ] Убедиться, что `connection.rs` больше не вызывает `header.validate()`. -* [ ] После получения `Header` выполнить `match` по `header.command()`. -* [ ] Для `0x01` вызвать обработчик `UPLOAD`. -* [ ] Для остальных команд пока возвращать ошибку `UnsupportedCommand`. -* [ ] Проверить: тестовый заголовок `command = 0x01` попадает в обработчик `UPLOAD`. +- [✔] Оставить `decode_header()` единственной публичной функцией получения проверенного `Header`. +- [✔] Убедиться, что `connection.rs` больше не вызывает `header.validate()`. +- [✔] После получения `Header` выполнить `match` по `header.command()`. +- [✔] Для `0x01` вызвать обработчик `UPLOAD`. + ~~[✔] Для остальных команд пока возвращать ошибку `UnsupportedCommand`.~~ +- [✔] Для остальных вызвать свои обработчики +- [✔] Проверить: тестовый заголовок `command = 0x01` попадает в обработчик `UPLOAD`. Ожидаемый промежуточный результат: @@ -26,20 +29,21 @@ ### 2. Создать постоянный модуль загрузки -* [ ] Создать `upload.rs`. -* [ ] Подключить его в `main.rs`: +- [ ] Создать `upload.rs`. +- [ ] Подключить его в `main.rs`: ```rust mod upload; ``` -* [ ] Создать в нём асинхронную функцию `handle()`. -* [ ] Передать в неё: +- [ ] Создать в нём асинхронную функцию `handle()`. +- [ ] Передать в неё: - * `&mut` сетевой поток; - * проверенный `Header`. -* [ ] Пока только вывести заголовок и вернуть `Ok(())`. -* [ ] Убедиться, что после вызова управление возвращается в цикл `Connection`. + - `&mut` сетевой поток; + - проверенный `Header`. + +- [ ] Пока только вывести заголовок и вернуть `Ok(())`. +- [ ] Убедиться, что после вызова управление возвращается в цикл `Connection`. Первая форма может иметь такой смысл: @@ -55,19 +59,19 @@ pub async fn handle( ### 3. Убрать чтение всего тела в `Vec` -* [ ] Не вызывать текущий `read_body()` для команды `UPLOAD`. -* [ ] Не создавать: +- [ ] Не вызывать текущий `read_body()` для команды `UPLOAD`. +- [ ] Не создавать: ```rust vec![0_u8; header.body_size() as usize] ``` -* [ ] Удалить или временно оставить `read_body()` только для будущих небольших команд. -* [ ] Убедиться, что `UPLOAD` получает поток, стоящий точно перед первым байтом тела. +- [ ] Удалить или временно оставить `read_body()` только для будущих небольших команд. +- [ ] Убедиться, что `UPLOAD` получает поток, стоящий точно перед первым байтом тела. ### 4. Описать метаданные загрузки -* [ ] В `protocol.rs` создать структуру: +- [ ] В `protocol.rs` создать структуру: ```rust pub struct UploadMetadata { @@ -76,14 +80,14 @@ pub struct UploadMetadata { } ``` -* [ ] Добавить геттеры для `file_size` и `file_name`. -* [ ] Добавить константу максимальной длины имени: +- [ ] Добавить геттеры для `file_size` и `file_name`. +- [ ] Добавить константу максимальной длины имени: ```rust MAX_FILENAME_SIZE ``` -* [ ] Пока не добавлять в структуру содержимое файла. +- [ ] Пока не добавлять в структуру содержимое файла. ### 5. Прочитать фиксированную часть метаданных @@ -94,10 +98,10 @@ file_size: 8 байт name_size: 2 байта ``` -* [ ] В `upload::handle()` создать буфер размером 10 байт. -* [ ] Прочитать в него ровно 10 байт через `read_exact()`. -* [ ] Пока вывести полученные байты. -* [ ] Проверить обрыв соединения внутри этих десяти байт. +- [ ] В `upload::handle()` создать буфер размером 10 байт. +- [ ] Прочитать в него ровно 10 байт через `read_exact()`. +- [ ] Пока вывести полученные байты. +- [ ] Проверить обрыв соединения внутри этих десяти байт. Ожидаемый результат: @@ -107,12 +111,13 @@ name_size: 2 байта ### 6. Разобрать фиксированную часть в `protocol.rs` -* [ ] Создать приватный парсер `parse_upload_prefix()`. -* [ ] Разобрать: +- [ ] Создать приватный парсер `parse_upload_prefix()`. +- [ ] Разобрать: - * `file_size`; - * `name_size`. -* [ ] Создать промежуточную структуру: + - `file_size`; + - `name_size`. + +- [ ] Создать промежуточную структуру: ```rust UploadPrefix { @@ -121,29 +126,29 @@ UploadPrefix { } ``` -* [ ] Создать публичную функцию `decode_upload_prefix()`. -* [ ] Скрыть ошибки `nom` внутри `protocol.rs`. -* [ ] Проверить, что парсер использовал все 10 байт. -* [ ] Вернуть проверенный `UploadPrefix`. +- [ ] Создать публичную функцию `decode_upload_prefix()`. +- [ ] Скрыть ошибки `nom` внутри `protocol.rs`. +- [ ] Проверить, что парсер использовал все 10 байт. +- [ ] Вернуть проверенный `UploadPrefix`. ### 7. Проверить длину имени -* [ ] Отклонить `name_size = 0`. -* [ ] Отклонить `name_size > MAX_FILENAME_SIZE`. -* [ ] Не выделять память для имени до проверки размера. -* [ ] Добавить отдельные ошибки: +- [ ] Отклонить `name_size = 0`. +- [ ] Отклонить `name_size > MAX_FILENAME_SIZE`. +- [ ] Не выделять память для имени до проверки размера. +- [ ] Добавить отдельные ошибки: - * пустое имя; - * слишком длинное имя. + - пустое имя; + - слишком длинное имя. ### 8. Прочитать имя файла -* [ ] После проверки `name_size` выделить `Vec` только под имя. -* [ ] Прочитать ровно `name_size` байт. -* [ ] Передать эти байты в `protocol.rs`. -* [ ] Проверить UTF-8. -* [ ] Получить `String`. -* [ ] Собрать `UploadMetadata`. +- [ ] После проверки `name_size` выделить `Vec` только под имя. +- [ ] Прочитать ровно `name_size` байт. +- [ ] Передать эти байты в `protocol.rs`. +- [ ] Проверить UTF-8. +- [ ] Получить `String`. +- [ ] Собрать `UploadMetadata`. Ожидаемый результат: @@ -156,110 +161,105 @@ UploadMetadata { ### 9. Проверить согласованность размеров -* [ ] Вычислить ожидаемый размер тела: +- [ ] Вычислить ожидаемый размер тела: ```text 10 + name_size + file_size ``` -* [ ] Использовать `checked_add()`. -* [ ] Сравнить результат с `header.body_size()`. -* [ ] Отклонить пакет, если размеры не совпадают. -* [ ] Добавить ошибку переполнения размера. -* [ ] Добавить ошибку несовпадения `body_size`. +- [ ] Использовать `checked_add()`. +- [ ] Сравнить результат с `header.body_size()`. +- [ ] Отклонить пакет, если размеры не совпадают. +- [ ] Добавить ошибку переполнения размера. +- [ ] Добавить ошибку несовпадения `body_size`. ### 10. Добавить лимит размера файла -* [ ] Добавить временную настройку `MAX_FILE_SIZE`. -* [ ] Проверять `file_size` до создания файла. -* [ ] Не связывать `MAX_FILE_SIZE` с размером сетевого буфера. -* [ ] Позднее вынести лимит в конфигурацию сервера. +- [ ] Добавить временную настройку `MAX_FILE_SIZE`. +- [ ] Проверять `file_size` до создания файла. +- [ ] Не связывать `MAX_FILE_SIZE` с размером сетевого буфера. +- [ ] Позднее вынести лимит в конфигурацию сервера. ### 11. Создать модуль хранилища -* [ ] Создать `storage.rs`. -* [ ] Подключить его в `main.rs`. -* [ ] Создать тип `Storage`. -* [ ] Передавать `Storage` в `Connection`. -* [ ] Передавать ссылку на `Storage` в `upload::handle()`. -* [ ] Пока реализовать только выбор каталога хранения. +- [ ] Создать `storage.rs`. +- [ ] Подключить его в `main.rs`. +- [ ] Создать тип `Storage`. +- [ ] Передавать `Storage` в `Connection`. +- [ ] Передавать ссылку на `Storage` в `upload::handle()`. +- [ ] Пока реализовать только выбор каталога хранения. ### 12. Проверить имя на уровне хранилища -* [ ] Запретить пустое имя. -* [ ] Запретить абсолютный путь. -* [ ] Запретить компоненты `..`. -* [ ] Запретить нулевой байт. -* [ ] Убедиться, что итоговый путь остаётся внутри хранилища. -* [ ] Определить политику перезаписи существующего файла. +- [ ] Запретить пустое имя. +- [ ] Запретить абсолютный путь. +- [ ] Запретить компоненты `..`. +- [ ] Запретить нулевой байт. +- [ ] Убедиться, что итоговый путь остаётся внутри хранилища. +- [ ] Определить политику перезаписи существующего файла. ### 13. Создать временный файл -* [ ] Формировать имя с `request_id`, например: +- [ ] Формировать имя с `request_id`, например: ```text a.txt.nocloud-part-17 ``` -* [ ] Создавать временный файл только после всех проверок метаданных. -* [ ] Не создавать сразу итоговый файл. -* [ ] Убедиться, что ошибка создания корректно возвращается обработчику. +- [ ] Создавать временный файл только после всех проверок метаданных. +- [ ] Не создавать сразу итоговый файл. +- [ ] Убедиться, что ошибка создания корректно возвращается обработчику. ### 14. Принять содержимое файла порциями -* [ ] Создать фиксированный буфер, например 64 КиБ. -* [ ] Завести счётчик `remaining_file_bytes = file_size`. -* [ ] На каждой итерации читать не больше: +- [ ] Создать фиксированный буфер, например 64 КиБ. +- [ ] Завести счётчик `remaining_file_bytes = file_size`. +- [ ] На каждой итерации читать не больше: ```text min(remaining_file_bytes, buffer.len()) ``` -* [ ] Записывать прочитанную порцию во временный файл. -* [ ] Уменьшать `remaining_file_bytes`. -* [ ] Завершить цикл при достижении нуля. -* [ ] Не читать байты следующего сообщения. +- [ ] Записывать прочитанную порцию во временный файл. +- [ ] Уменьшать `remaining_file_bytes`. +- [ ] Завершить цикл при достижении нуля. +- [ ] Не читать байты следующего сообщения. ### 15. Обработать оборванную передачу -* [ ] Если клиент отключился раньше `file_size`, считать загрузку незавершённой. -* [ ] Закрыть временный файл. -* [ ] Удалить временный файл либо сохранить для будущей докачки. -* [ ] Пока выбрать простую политику: удалять. -* [ ] Не создавать итоговый файл при ошибке. +- [ ] Если клиент отключился раньше `file_size`, считать загрузку незавершённой. +- [ ] Закрыть временный файл. +- [ ] Удалить временный файл либо сохранить для будущей докачки. +- [ ] Пока выбрать простую политику: удалять. +- [ ] Не создавать итоговый файл при ошибке. ### 16. Завершить загрузку -* [ ] Сбросить буферы файла на диск. -* [ ] Закрыть временный файл. -* [ ] Переименовать временный файл в итоговый. -* [ ] Считать переименование точкой успешного завершения операции. +- [ ] Сбросить буферы файла на диск. +- [ ] Закрыть временный файл. +- [ ] Переименовать временный файл в итоговый. +- [ ] Считать переименование точкой успешного завершения операции. ### 17. Отправить ответ -* [ ] Сформировать `RESPONSE` с тем же `request_id`. -* [ ] При успехе вернуть статус `0`. -* [ ] При ошибке вернуть соответствующий код. -* [ ] Не отправлять текстовую строку вместо сообщения протокола. -* [ ] После ответа вернуться в цикл `Connection`. +- [ ] Сформировать `RESPONSE` с тем же `request_id`. +- [ ] При успехе вернуть статус `0`. +- [ ] При ошибке вернуть соответствующий код. +- [ ] Не отправлять текстовую строку вместо сообщения протокола. +- [ ] После ответа вернуться в цикл `Connection`. ### 18. Провести испытания -* [ ] Корректный маленький файл. -* [ ] Пустой файл размером `0`. -* [ ] Имя длиной `0`. -* [ ] Имя длиннее лимита. -* [ ] Некорректный UTF-8. -* [ ] `body_size` меньше вычисленного. -* [ ] `body_size` больше вычисленного. -* [ ] `file_size` превышает лимит. -* [ ] Обрыв во время метаданных. -* [ ] Обрыв посередине файла. -* [ ] Попытка передать `../test.txt`. -* [ ] Повторная загрузка существующего файла. -* [ ] Два последовательных `UPLOAD` в одном соединении. - - - - - +- [ ] Корректный маленький файл. +- [ ] Пустой файл размером `0`. +- [ ] Имя длиной `0`. +- [ ] Имя длиннее лимита. +- [ ] Некорректный UTF-8. +- [ ] `body_size` меньше вычисленного. +- [ ] `body_size` больше вычисленного. +- [ ] `file_size` превышает лимит. +- [ ] Обрыв во время метаданных. +- [ ] Обрыв посередине файла. +- [ ] Попытка передать `../test.txt`. +- [ ] Повторная загрузка существующего файла. +- [ ] Два последовательных `UPLOAD` в одном соединении. diff --git a/src/protocol.MD b/src/protocol.MD index d48313f..4c6f1a1 100644 --- a/src/protocol.MD +++ b/src/protocol.MD @@ -5,12 +5,13 @@ 2. Вернуть структуру Header c понятными полями заголовка: a. version: u8 //Версия протокола b. command: u8 //Тип сообщения - 0x01 UPLOAD клиент → сервер - 0x02 DOWNLOAD клиент → сервер - 0x03 DELETE клиент → сервер - 0x04 STAT клиент → сервер - 0x80 RESPONSE в обе стороны - 0x81 FILE_DATA сервер → клиент + Код Команда Кто отправляет + 0x01 UPLOAD клиент → сервер + 0x02 DOWNLOAD клиент → сервер + 0x03 DELETE клиент → сервер + 0x04 STAT клиент → сервер + 0x80 RESPONSE клиент ↔ сервер + 0x81 FILE_DATA сервер → клиент c. flags: u16 //Дополнительные признаки 0x0001 — разрешено перезаписать существующий файл 0x0002 — передаётся контрольная сумма diff --git a/src/protocol.rs b/src/protocol.rs index e7ba72f..2548f82 100644 --- a/src/protocol.rs +++ b/src/protocol.rs @@ -44,6 +44,43 @@ pub struct Header { body_size: u64, } +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum Command { + /// Клиент загружает файл на сервер. + Upload, + + /// Клиент запрашивает скачивание файла с сервера. + Download, + + /// Клиент запрашивает удаление файла на сервере. + Delete, + + /// Клиент запрашивает информацию о файле. + Stat, + + /// Ответ на ранее отправленный запрос. + Response, + + /// Сервер передаёт клиенту запрошенный файл. + FileData, +} + +impl TryFrom for Command { + type Error = HeaderError; + + fn try_from(value: u8) -> Result { + match value { + 0x01 => Ok(Command::Upload), //клиент → сервер + 0x02 => Ok(Command::Download), + 0x03 => Ok(Command::Delete), + 0x04 => Ok(Command::Stat), + 0x80 => Ok(Command::Response), + 0x81 => Ok(Command::FileData), + other => Err(HeaderError::UnsupportedCommand(other)), + } + } +} + pub fn decode_header( input: &[u8; HEADER_SIZE], ) -> Result { @@ -97,8 +134,9 @@ impl Header { } /// Возвращает код команды (например, 1, 2, 80 или 81) - pub fn command(&self) -> u8 { - self.command + pub fn command(&self) -> Command { + Command::try_from(self.command) + .expect("Header уже прошёл валидацию команды") } /// Возвращает битовые флаги пакета (уже прошедшие валидацию) @@ -128,11 +166,7 @@ impl Header { } // Валидация command - if self.command != 1 { - return Err( - HeaderError::UnsupportedCommand(self.command) - ); - } + Command::try_from(self.command)?; // Валидация flags if !(0..=7).contains(&self.flags){