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
This commit is contained in:
Stanislav N Mikhailov
2026-08-07 17:46:07 +03:00
parent dd6ef1d96d
commit 3c3487b554
4 changed files with 196 additions and 133 deletions
+30 -2
View File
@@ -13,6 +13,7 @@ use crate::protocol::{
Header, Header,
HEADER_SIZE, HEADER_SIZE,
MAX_BODY_SIZE, MAX_BODY_SIZE,
Command,
}; };
struct IncomingMessage { 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)) Ok(Some(header))
} }
async fn read_body( async fn read_body( //todo: анахронизм, убрать после реализации storage
&mut self, &mut self,
body_size: u64, body_size: u64,
) -> io::Result<Vec<u8>> { ) -> io::Result<Vec<u8>> {
@@ -146,7 +174,7 @@ impl Connection {
); );
let response = format!( let response = format!(
"Соединение {}: запрос {} принят, cmd={}\n", "Соединение {}: запрос {} принят, cmd={:?}\n",
self.id, self.id,
message.header.request_id(), message.header.request_id(),
message.header.command(), message.header.command(),
+118 -118
View File
@@ -1,22 +1,25 @@
# Статус разработки проекта # Статус разработки проекта
## 📌 Текущее состояние ## 📌 Текущее состояние
* **Где остановился:** Окультурил контракт между protocol connection, работает точно также, но
* архитектурно красивей. - **Где остановился:** Окультурил контракт между protocol connection, работает точно также, но
* Далее, нужно реализовать UPLOAD с проверкой валидностей имени файла, размеров. И последующей передачей в storage/ - архитектурно красивей.
* Ниже - чеклист для реализации. - Далее, нужно реализовать UPLOAD с проверкой валидностей имени файла, размеров. И последующей передачей в storage/
* **Проблема:** - Ниже - чеклист для реализации.
* - нет проблем 😇 - **Проблема:**
- - нет проблем 😇
## Чек-лист реализации `UPLOAD` ## Чек-лист реализации `UPLOAD`
### 1. Подготовить диспетчеризацию команд ### 1. Подготовить диспетчеризацию команд
* [ ] Оставить `decode_header()` единственной публичной функцией получения проверенного `Header`. - [✔] Оставить `decode_header()` единственной публичной функцией получения проверенного `Header`.
* [ ] Убедиться, что `connection.rs` больше не вызывает `header.validate()`. - [✔] Убедиться, что `connection.rs` больше не вызывает `header.validate()`.
* [ ] После получения `Header` выполнить `match` по `header.command()`. - [✔] После получения `Header` выполнить `match` по `header.command()`.
* [ ] Для `0x01` вызвать обработчик `UPLOAD`. - [✔] Для `0x01` вызвать обработчик `UPLOAD`.
* [ ] Для остальных команд пока возвращать ошибку `UnsupportedCommand`. ~~[✔] Для остальных команд пока возвращать ошибку `UnsupportedCommand`.~~
* [ ] Проверить: тестовый заголовок `command = 0x01` попадает в обработчик `UPLOAD`. - [✔] Для остальных вызвать свои обработчики
- [✔] Проверить: тестовый заголовок `command = 0x01` попадает в обработчик `UPLOAD`.
Ожидаемый промежуточный результат: Ожидаемый промежуточный результат:
@@ -26,20 +29,21 @@
### 2. Создать постоянный модуль загрузки ### 2. Создать постоянный модуль загрузки
* [ ] Создать `upload.rs`. - [ ] Создать `upload.rs`.
* [ ] Подключить его в `main.rs`: - [ ] Подключить его в `main.rs`:
```rust ```rust
mod upload; mod upload;
``` ```
* [ ] Создать в нём асинхронную функцию `handle()`. - [ ] Создать в нём асинхронную функцию `handle()`.
* [ ] Передать в неё: - [ ] Передать в неё:
* `&mut` сетевой поток; - `&mut` сетевой поток;
* проверенный `Header`. - проверенный `Header`.
* [ ] Пока только вывести заголовок и вернуть `Ok(())`.
* [ ] Убедиться, что после вызова управление возвращается в цикл `Connection`. - [ ] Пока только вывести заголовок и вернуть `Ok(())`.
- [ ] Убедиться, что после вызова управление возвращается в цикл `Connection`.
Первая форма может иметь такой смысл: Первая форма может иметь такой смысл:
@@ -55,19 +59,19 @@ pub async fn handle(
### 3. Убрать чтение всего тела в `Vec` ### 3. Убрать чтение всего тела в `Vec`
* [ ] Не вызывать текущий `read_body()` для команды `UPLOAD`. - [ ] Не вызывать текущий `read_body()` для команды `UPLOAD`.
* [ ] Не создавать: - [ ] Не создавать:
```rust ```rust
vec![0_u8; header.body_size() as usize] vec![0_u8; header.body_size() as usize]
``` ```
* [ ] Удалить или временно оставить `read_body()` только для будущих небольших команд. - [ ] Удалить или временно оставить `read_body()` только для будущих небольших команд.
* [ ] Убедиться, что `UPLOAD` получает поток, стоящий точно перед первым байтом тела. - [ ] Убедиться, что `UPLOAD` получает поток, стоящий точно перед первым байтом тела.
### 4. Описать метаданные загрузки ### 4. Описать метаданные загрузки
* [ ] В `protocol.rs` создать структуру: - [ ] В `protocol.rs` создать структуру:
```rust ```rust
pub struct UploadMetadata { pub struct UploadMetadata {
@@ -76,14 +80,14 @@ pub struct UploadMetadata {
} }
``` ```
* [ ] Добавить геттеры для `file_size` и `file_name`. - [ ] Добавить геттеры для `file_size` и `file_name`.
* [ ] Добавить константу максимальной длины имени: - [ ] Добавить константу максимальной длины имени:
```rust ```rust
MAX_FILENAME_SIZE MAX_FILENAME_SIZE
``` ```
* [ ] Пока не добавлять в структуру содержимое файла. - [ ] Пока не добавлять в структуру содержимое файла.
### 5. Прочитать фиксированную часть метаданных ### 5. Прочитать фиксированную часть метаданных
@@ -94,10 +98,10 @@ file_size: 8 байт
name_size: 2 байта name_size: 2 байта
``` ```
* [ ] В `upload::handle()` создать буфер размером 10 байт. - [ ] В `upload::handle()` создать буфер размером 10 байт.
* [ ] Прочитать в него ровно 10 байт через `read_exact()`. - [ ] Прочитать в него ровно 10 байт через `read_exact()`.
* [ ] Пока вывести полученные байты. - [ ] Пока вывести полученные байты.
* [ ] Проверить обрыв соединения внутри этих десяти байт. - [ ] Проверить обрыв соединения внутри этих десяти байт.
Ожидаемый результат: Ожидаемый результат:
@@ -107,12 +111,13 @@ name_size: 2 байта
### 6. Разобрать фиксированную часть в `protocol.rs` ### 6. Разобрать фиксированную часть в `protocol.rs`
* [ ] Создать приватный парсер `parse_upload_prefix()`. - [ ] Создать приватный парсер `parse_upload_prefix()`.
* [ ] Разобрать: - [ ] Разобрать:
* `file_size`; - `file_size`;
* `name_size`. - `name_size`.
* [ ] Создать промежуточную структуру:
- [ ] Создать промежуточную структуру:
```rust ```rust
UploadPrefix { UploadPrefix {
@@ -121,29 +126,29 @@ UploadPrefix {
} }
``` ```
* [ ] Создать публичную функцию `decode_upload_prefix()`. - [ ] Создать публичную функцию `decode_upload_prefix()`.
* [ ] Скрыть ошибки `nom` внутри `protocol.rs`. - [ ] Скрыть ошибки `nom` внутри `protocol.rs`.
* [ ] Проверить, что парсер использовал все 10 байт. - [ ] Проверить, что парсер использовал все 10 байт.
* [ ] Вернуть проверенный `UploadPrefix`. - [ ] Вернуть проверенный `UploadPrefix`.
### 7. Проверить длину имени ### 7. Проверить длину имени
* [ ] Отклонить `name_size = 0`. - [ ] Отклонить `name_size = 0`.
* [ ] Отклонить `name_size > MAX_FILENAME_SIZE`. - [ ] Отклонить `name_size > MAX_FILENAME_SIZE`.
* [ ] Не выделять память для имени до проверки размера. - [ ] Не выделять память для имени до проверки размера.
* [ ] Добавить отдельные ошибки: - [ ] Добавить отдельные ошибки:
* пустое имя; - пустое имя;
* слишком длинное имя. - слишком длинное имя.
### 8. Прочитать имя файла ### 8. Прочитать имя файла
* [ ] После проверки `name_size` выделить `Vec<u8>` только под имя. - [ ] После проверки `name_size` выделить `Vec<u8>` только под имя.
* [ ] Прочитать ровно `name_size` байт. - [ ] Прочитать ровно `name_size` байт.
* [ ] Передать эти байты в `protocol.rs`. - [ ] Передать эти байты в `protocol.rs`.
* [ ] Проверить UTF-8. - [ ] Проверить UTF-8.
* [ ] Получить `String`. - [ ] Получить `String`.
* [ ] Собрать `UploadMetadata`. - [ ] Собрать `UploadMetadata`.
Ожидаемый результат: Ожидаемый результат:
@@ -156,110 +161,105 @@ UploadMetadata {
### 9. Проверить согласованность размеров ### 9. Проверить согласованность размеров
* [ ] Вычислить ожидаемый размер тела: - [ ] Вычислить ожидаемый размер тела:
```text ```text
10 + name_size + file_size 10 + name_size + file_size
``` ```
* [ ] Использовать `checked_add()`. - [ ] Использовать `checked_add()`.
* [ ] Сравнить результат с `header.body_size()`. - [ ] Сравнить результат с `header.body_size()`.
* [ ] Отклонить пакет, если размеры не совпадают. - [ ] Отклонить пакет, если размеры не совпадают.
* [ ] Добавить ошибку переполнения размера. - [ ] Добавить ошибку переполнения размера.
* [ ] Добавить ошибку несовпадения `body_size`. - [ ] Добавить ошибку несовпадения `body_size`.
### 10. Добавить лимит размера файла ### 10. Добавить лимит размера файла
* [ ] Добавить временную настройку `MAX_FILE_SIZE`. - [ ] Добавить временную настройку `MAX_FILE_SIZE`.
* [ ] Проверять `file_size` до создания файла. - [ ] Проверять `file_size` до создания файла.
* [ ] Не связывать `MAX_FILE_SIZE` с размером сетевого буфера. - [ ] Не связывать `MAX_FILE_SIZE` с размером сетевого буфера.
* [ ] Позднее вынести лимит в конфигурацию сервера. - [ ] Позднее вынести лимит в конфигурацию сервера.
### 11. Создать модуль хранилища ### 11. Создать модуль хранилища
* [ ] Создать `storage.rs`. - [ ] Создать `storage.rs`.
* [ ] Подключить его в `main.rs`. - [ ] Подключить его в `main.rs`.
* [ ] Создать тип `Storage`. - [ ] Создать тип `Storage`.
* [ ] Передавать `Storage` в `Connection`. - [ ] Передавать `Storage` в `Connection`.
* [ ] Передавать ссылку на `Storage` в `upload::handle()`. - [ ] Передавать ссылку на `Storage` в `upload::handle()`.
* [ ] Пока реализовать только выбор каталога хранения. - [ ] Пока реализовать только выбор каталога хранения.
### 12. Проверить имя на уровне хранилища ### 12. Проверить имя на уровне хранилища
* [ ] Запретить пустое имя. - [ ] Запретить пустое имя.
* [ ] Запретить абсолютный путь. - [ ] Запретить абсолютный путь.
* [ ] Запретить компоненты `..`. - [ ] Запретить компоненты `..`.
* [ ] Запретить нулевой байт. - [ ] Запретить нулевой байт.
* [ ] Убедиться, что итоговый путь остаётся внутри хранилища. - [ ] Убедиться, что итоговый путь остаётся внутри хранилища.
* [ ] Определить политику перезаписи существующего файла. - [ ] Определить политику перезаписи существующего файла.
### 13. Создать временный файл ### 13. Создать временный файл
* [ ] Формировать имя с `request_id`, например: - [ ] Формировать имя с `request_id`, например:
```text ```text
a.txt.nocloud-part-17 a.txt.nocloud-part-17
``` ```
* [ ] Создавать временный файл только после всех проверок метаданных. - [ ] Создавать временный файл только после всех проверок метаданных.
* [ ] Не создавать сразу итоговый файл. - [ ] Не создавать сразу итоговый файл.
* [ ] Убедиться, что ошибка создания корректно возвращается обработчику. - [ ] Убедиться, что ошибка создания корректно возвращается обработчику.
### 14. Принять содержимое файла порциями ### 14. Принять содержимое файла порциями
* [ ] Создать фиксированный буфер, например 64 КиБ. - [ ] Создать фиксированный буфер, например 64 КиБ.
* [ ] Завести счётчик `remaining_file_bytes = file_size`. - [ ] Завести счётчик `remaining_file_bytes = file_size`.
* [ ] На каждой итерации читать не больше: - [ ] На каждой итерации читать не больше:
```text ```text
min(remaining_file_bytes, buffer.len()) min(remaining_file_bytes, buffer.len())
``` ```
* [ ] Записывать прочитанную порцию во временный файл. - [ ] Записывать прочитанную порцию во временный файл.
* [ ] Уменьшать `remaining_file_bytes`. - [ ] Уменьшать `remaining_file_bytes`.
* [ ] Завершить цикл при достижении нуля. - [ ] Завершить цикл при достижении нуля.
* [ ] Не читать байты следующего сообщения. - [ ] Не читать байты следующего сообщения.
### 15. Обработать оборванную передачу ### 15. Обработать оборванную передачу
* [ ] Если клиент отключился раньше `file_size`, считать загрузку незавершённой. - [ ] Если клиент отключился раньше `file_size`, считать загрузку незавершённой.
* [ ] Закрыть временный файл. - [ ] Закрыть временный файл.
* [ ] Удалить временный файл либо сохранить для будущей докачки. - [ ] Удалить временный файл либо сохранить для будущей докачки.
* [ ] Пока выбрать простую политику: удалять. - [ ] Пока выбрать простую политику: удалять.
* [ ] Не создавать итоговый файл при ошибке. - [ ] Не создавать итоговый файл при ошибке.
### 16. Завершить загрузку ### 16. Завершить загрузку
* [ ] Сбросить буферы файла на диск. - [ ] Сбросить буферы файла на диск.
* [ ] Закрыть временный файл. - [ ] Закрыть временный файл.
* [ ] Переименовать временный файл в итоговый. - [ ] Переименовать временный файл в итоговый.
* [ ] Считать переименование точкой успешного завершения операции. - [ ] Считать переименование точкой успешного завершения операции.
### 17. Отправить ответ ### 17. Отправить ответ
* [ ] Сформировать `RESPONSE` с тем же `request_id`. - [ ] Сформировать `RESPONSE` с тем же `request_id`.
* [ ] При успехе вернуть статус `0`. - [ ] При успехе вернуть статус `0`.
* [ ] При ошибке вернуть соответствующий код. - [ ] При ошибке вернуть соответствующий код.
* [ ] Не отправлять текстовую строку вместо сообщения протокола. - [ ] Не отправлять текстовую строку вместо сообщения протокола.
* [ ] После ответа вернуться в цикл `Connection`. - [ ] После ответа вернуться в цикл `Connection`.
### 18. Провести испытания ### 18. Провести испытания
* [ ] Корректный маленький файл. - [ ] Корректный маленький файл.
* [ ] Пустой файл размером `0`. - [ ] Пустой файл размером `0`.
* [ ] Имя длиной `0`. - [ ] Имя длиной `0`.
* [ ] Имя длиннее лимита. - [ ] Имя длиннее лимита.
* [ ] Некорректный UTF-8. - [ ] Некорректный UTF-8.
* [ ] `body_size` меньше вычисленного. - [ ] `body_size` меньше вычисленного.
* [ ] `body_size` больше вычисленного. - [ ] `body_size` больше вычисленного.
* [ ] `file_size` превышает лимит. - [ ] `file_size` превышает лимит.
* [ ] Обрыв во время метаданных. - [ ] Обрыв во время метаданных.
* [ ] Обрыв посередине файла. - [ ] Обрыв посередине файла.
* [ ] Попытка передать `../test.txt`. - [ ] Попытка передать `../test.txt`.
* [ ] Повторная загрузка существующего файла. - [ ] Повторная загрузка существующего файла.
* [ ] Два последовательных `UPLOAD` в одном соединении. - [ ] Два последовательных `UPLOAD` в одном соединении.
+7 -6
View File
@@ -5,12 +5,13 @@
2. Вернуть структуру Header c понятными полями заголовка: 2. Вернуть структуру Header c понятными полями заголовка:
a. version: u8 //Версия протокола a. version: u8 //Версия протокола
b. command: u8 //Тип сообщения b. command: u8 //Тип сообщения
0x01 UPLOAD клиент → сервер Код Команда Кто отправляет
0x02 DOWNLOAD клиент → сервер 0x01 UPLOAD клиент → сервер
0x03 DELETE клиент → сервер 0x02 DOWNLOAD клиент → сервер
0x04 STAT клиент → сервер 0x03 DELETE клиент → сервер
0x80 RESPONSE в обе стороны 0x04 STAT клиент → сервер
0x81 FILE_DATA сервер → клиент 0x80 RESPONSE клиент ↔ сервер
0x81 FILE_DATA сервер → клиент
c. flags: u16 //Дополнительные признаки c. flags: u16 //Дополнительные признаки
0x0001 — разрешено перезаписать существующий файл 0x0001 — разрешено перезаписать существующий файл
0x0002 — передаётся контрольная сумма 0x0002 — передаётся контрольная сумма
+41 -7
View File
@@ -44,6 +44,43 @@ pub struct Header {
body_size: u64, body_size: u64,
} }
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum Command {
/// Клиент загружает файл на сервер.
Upload,
/// Клиент запрашивает скачивание файла с сервера.
Download,
/// Клиент запрашивает удаление файла на сервере.
Delete,
/// Клиент запрашивает информацию о файле.
Stat,
/// Ответ на ранее отправленный запрос.
Response,
/// Сервер передаёт клиенту запрошенный файл.
FileData,
}
impl TryFrom<u8> for Command {
type Error = HeaderError;
fn try_from(value: u8) -> Result<Self, Self::Error> {
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( pub fn decode_header(
input: &[u8; HEADER_SIZE], input: &[u8; HEADER_SIZE],
) -> Result<Header, HeaderError> { ) -> Result<Header, HeaderError> {
@@ -97,8 +134,9 @@ impl Header {
} }
/// Возвращает код команды (например, 1, 2, 80 или 81) /// Возвращает код команды (например, 1, 2, 80 или 81)
pub fn command(&self) -> u8 { pub fn command(&self) -> Command {
self.command Command::try_from(self.command)
.expect("Header уже прошёл валидацию команды")
} }
/// Возвращает битовые флаги пакета (уже прошедшие валидацию) /// Возвращает битовые флаги пакета (уже прошедшие валидацию)
@@ -128,11 +166,7 @@ impl Header {
} }
// Валидация command // Валидация command
if self.command != 1 { Command::try_from(self.command)?;
return Err(
HeaderError::UnsupportedCommand(self.command)
);
}
// Валидация flags // Валидация flags
if !(0..=7).contains(&self.flags){ if !(0..=7).contains(&self.flags){