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_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<Vec<u8>> {
@@ -146,7 +174,7 @@ impl Connection {
);
let response = format!(
"Соединение {}: запрос {} принят, cmd={}\n",
"Соединение {}: запрос {} принят, cmd={:?}\n",
self.id,
message.header.request_id(),
message.header.command(),
+118 -118
View File
@@ -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<u8>` только под имя.
* [ ] Прочитать ровно `name_size` байт.
* [ ] Передать эти байты в `protocol.rs`.
* [ ] Проверить UTF-8.
* [ ] Получить `String`.
* [ ] Собрать `UploadMetadata`.
- [ ] После проверки `name_size` выделить `Vec<u8>` только под имя.
- [ ] Прочитать ровно `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` в одном соединении.
+7 -6
View File
@@ -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 — передаётся контрольная сумма
+41 -7
View File
@@ -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<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(
input: &[u8; HEADER_SIZE],
) -> Result<Header, HeaderError> {
@@ -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){