chore(upload): document implementation steps and remove unused nom

import
This commit is contained in:
Stanislav N Mikhailov
2026-08-07 15:45:20 +03:00
parent 94030c34ca
commit dd6ef1d96d
2 changed files with 255 additions and 50 deletions
+251 -49
View File
@@ -1,63 +1,265 @@
# Статус разработки проекта # Статус разработки проекта
## 📌 Текущее состояние ## 📌 Текущее состояние
* **Где остановился:** Сервер успешно запускается и слушает сетевой сокет. * **Где остановился:** Окультурил контракт между protocol connection, работает точно также, но
Проверочная строка: smikhai@DebHOME:~$ printf '\x4E\x43\x4C\x44\x01\x01\x00\x00\x00\x00\x00\x11\x00\x00\x00\x00\x00\x00\x03\xFB' \ * архитектурно красивей.
> | nc 192.168.1.213 32768 * Далее, нужно реализовать UPLOAD с проверкой валидностей имени файла, размеров. И последующей передачей в storage/
выдаёт ошибку парсера: * Ниже - чеклист для реализации.
Сервер слушает 192.168.1.213:32768
Обработчик 1 создан для 192.168.1.170:50978
Обработчик 1: Критическая ошибка! Неверный формат заголовка.
Обработчик 1: ошибка: Invalid header format
что радует.
* **Проблема:** * **Проблема:**
* **Следующий шаг:** * - нет проблем 😇
## Чек-лист: исправление парсера общего заголовка ## Чек-лист реализации `UPLOAD`
* [✔] Исправить имя функции `parce_header``parse_header`. ### 1. Подготовить диспетчеризацию команд
* [✔] Убрать `async` у `parse_header`: функция не выполняет ввод-вывод.
* [✔] Убрать `.await` при вызове `parse_header`.
* [ ] Исправить порядок полей согласно протоколу:
* `bytes[0..4]``magic`; * [ ] Оставить `decode_header()` единственной публичной функцией получения проверенного `Header`.
* `bytes[4]``version`; * [ ] Убедиться, что `connection.rs` больше не вызывает `header.validate()`.
* `bytes[5]``command`. * [ ] После получения `Header` выполнить `match` по `header.command()`.
* [✔] Проверять `magic` как четыре байта `NCLD`. * [ ] Для `0x01` вызвать обработчик `UPLOAD`.
* [✔] Проверять версию как число `1`, а не ASCII-символ `b'1'`. * [ ] Для остальных команд пока возвращать ошибку `UnsupportedCommand`.
* [✔] Пока разрешать только версию протокола `1`. * [ ] Проверить: тестовый заголовок `command = 0x01` попадает в обработчик `UPLOAD`.
* [✔] Исправить команды `80 | 81` на `0x80 | 0x81`.
* [✔] Записывать в `Header.version` значение из `bytes[4]`.
* [✔] Для первого теста отправлять заголовок с `body_size = 0`.
## Проверка исправлений Ожидаемый промежуточный результат:
* [✔] Отправить корректный заголовок. ```text
* [✔] Убедиться, что сервер вывел: Получена команда UPLOAD
```
```text ### 2. Создать постоянный модуль загрузки
Header {
version: 1, * [ ] Создать `upload.rs`.
command: 1, * [ ] Подключить его в `main.rs`:
flags: 0,
request_id: 17, ```rust
body_size: 0, mod upload;
} ```
```
* [ ] Создать в нём асинхронную функцию `handle()`.
* [ ] Передать в неё:
* `&mut` сетевой поток;
* проверенный `Header`.
* [ ] Пока только вывести заголовок и вернуть `Ok(())`.
* [ ] Убедиться, что после вызова управление возвращается в цикл `Connection`.
Первая форма может иметь такой смысл:
```rust
pub async fn handle(
stream: &mut TcpStream,
header: Header,
) -> io::Result<()> {
println!("Обработчик UPLOAD получил заголовок: {header:?}");
Ok(())
}
```
### 3. Убрать чтение всего тела в `Vec`
* [ ] Не вызывать текущий `read_body()` для команды `UPLOAD`.
* [ ] Не создавать:
```rust
vec![0_u8; header.body_size() as usize]
```
* [ ] Удалить или временно оставить `read_body()` только для будущих небольших команд.
* [ ] Убедиться, что `UPLOAD` получает поток, стоящий точно перед первым байтом тела.
### 4. Описать метаданные загрузки
* [ ] В `protocol.rs` создать структуру:
```rust
pub struct UploadMetadata {
file_size: u64,
file_name: String,
}
```
* [ ] Добавить геттеры для `file_size` и `file_name`.
* [ ] Добавить константу максимальной длины имени:
```rust
MAX_FILENAME_SIZE
```
* [ ] Пока не добавлять в структуру содержимое файла.
### 5. Прочитать фиксированную часть метаданных
Формат:
```text
file_size: 8 байт
name_size: 2 байта
```
* [ ] В `upload::handle()` создать буфер размером 10 байт.
* [ ] Прочитать в него ровно 10 байт через `read_exact()`.
* [ ] Пока вывести полученные байты.
* [ ] Проверить обрыв соединения внутри этих десяти байт.
Ожидаемый результат:
```text
Получены 10 байт метаданных UPLOAD
```
### 6. Разобрать фиксированную часть в `protocol.rs`
* [ ] Создать приватный парсер `parse_upload_prefix()`.
* [ ] Разобрать:
* `file_size`;
* `name_size`.
* [ ] Создать промежуточную структуру:
```rust
UploadPrefix {
file_size: u64,
name_size: u16,
}
```
* [ ] Создать публичную функцию `decode_upload_prefix()`.
* [ ] Скрыть ошибки `nom` внутри `protocol.rs`.
* [ ] Проверить, что парсер использовал все 10 байт.
* [ ] Вернуть проверенный `UploadPrefix`.
### 7. Проверить длину имени
* [ ] Отклонить `name_size = 0`.
* [ ] Отклонить `name_size > MAX_FILENAME_SIZE`.
* [ ] Не выделять память для имени до проверки размера.
* [ ] Добавить отдельные ошибки:
* пустое имя;
* слишком длинное имя.
### 8. Прочитать имя файла
* [ ] После проверки `name_size` выделить `Vec<u8>` только под имя.
* [ ] Прочитать ровно `name_size` байт.
* [ ] Передать эти байты в `protocol.rs`.
* [ ] Проверить UTF-8.
* [ ] Получить `String`.
* [ ] Собрать `UploadMetadata`.
Ожидаемый результат:
```text
UploadMetadata {
file_size: 5,
file_name: "a.txt",
}
```
### 9. Проверить согласованность размеров
* [ ] Вычислить ожидаемый размер тела:
```text
10 + name_size + file_size
```
* [ ] Использовать `checked_add()`.
* [ ] Сравнить результат с `header.body_size()`.
* [ ] Отклонить пакет, если размеры не совпадают.
* [ ] Добавить ошибку переполнения размера.
* [ ] Добавить ошибку несовпадения `body_size`.
### 10. Добавить лимит размера файла
* [ ] Добавить временную настройку `MAX_FILE_SIZE`.
* [ ] Проверять `file_size` до создания файла.
* [ ] Не связывать `MAX_FILE_SIZE` с размером сетевого буфера.
* [ ] Позднее вынести лимит в конфигурацию сервера.
### 11. Создать модуль хранилища
* [ ] Создать `storage.rs`.
* [ ] Подключить его в `main.rs`.
* [ ] Создать тип `Storage`.
* [ ] Передавать `Storage` в `Connection`.
* [ ] Передавать ссылку на `Storage` в `upload::handle()`.
* [ ] Пока реализовать только выбор каталога хранения.
### 12. Проверить имя на уровне хранилища
* [ ] Запретить пустое имя.
* [ ] Запретить абсолютный путь.
* [ ] Запретить компоненты `..`.
* [ ] Запретить нулевой байт.
* [ ] Убедиться, что итоговый путь остаётся внутри хранилища.
* [ ] Определить политику перезаписи существующего файла.
### 13. Создать временный файл
* [ ] Формировать имя с `request_id`, например:
```text
a.txt.nocloud-part-17
```
* [ ] Создавать временный файл только после всех проверок метаданных.
* [ ] Не создавать сразу итоговый файл.
* [ ] Убедиться, что ошибка создания корректно возвращается обработчику.
### 14. Принять содержимое файла порциями
* [ ] Создать фиксированный буфер, например 64 КиБ.
* [ ] Завести счётчик `remaining_file_bytes = file_size`.
* [ ] На каждой итерации читать не больше:
```text
min(remaining_file_bytes, buffer.len())
```
* [ ] Записывать прочитанную порцию во временный файл.
* [ ] Уменьшать `remaining_file_bytes`.
* [ ] Завершить цикл при достижении нуля.
* [ ] Не читать байты следующего сообщения.
### 15. Обработать оборванную передачу
* [ ] Если клиент отключился раньше `file_size`, считать загрузку незавершённой.
* [ ] Закрыть временный файл.
* [ ] Удалить временный файл либо сохранить для будущей докачки.
* [ ] Пока выбрать простую политику: удалять.
* [ ] Не создавать итоговый файл при ошибке.
### 16. Завершить загрузку
* [ ] Сбросить буферы файла на диск.
* [ ] Закрыть временный файл.
* [ ] Переименовать временный файл в итоговый.
* [ ] Считать переименование точкой успешного завершения операции.
### 17. Отправить ответ
* [ ] Сформировать `RESPONSE` с тем же `request_id`.
* [ ] При успехе вернуть статус `0`.
* [ ] При ошибке вернуть соответствующий код.
* [ ] Не отправлять текстовую строку вместо сообщения протокола.
* [ ] После ответа вернуться в цикл `Connection`.
### 18. Провести испытания
* [ ] Корректный маленький файл.
* [ ] Пустой файл размером `0`.
* [ ] Имя длиной `0`.
* [ ] Имя длиннее лимита.
* [ ] Некорректный UTF-8.
* [ ] `body_size` меньше вычисленного.
* [ ] `body_size` больше вычисленного.
* [ ] `file_size` превышает лимит.
* [ ] Обрыв во время метаданных.
* [ ] Обрыв посередине файла.
* [ ] Попытка передать `../test.txt`.
* [ ] Повторная загрузка существующего файла.
* [ ] Два последовательных `UPLOAD` в одном соединении.
* [✔] Отправить заголовок с неправильным `magic`.
* [✔] Убедиться, что сервер вернул `InvalidFormat`.
* [✔] Отправить заголовок с неподдерживаемой версией.
* [✔] Убедиться, что сервер вернул `InvalidFormat`.
* [✔] Отправить заголовок с неизвестной командой.
* [✔] Убедиться, что сервер вернул `InvalidFormat`.
* [✔] Отправить заголовок с запрещённым флагом, например `0x0008`.
* [✔] Убедиться, что сервер вернул `InvalidFormat`.
## Следующая итерация — пока не выполнять
* [ ] Ограничить допустимый `body_size`.
* [ ] Не выделять буфер размером со всё тело сообщения.
* [ ] Начать отдельный разбор тела команды `UPLOAD`.
* [ ] Читать данные файла порциями, а не целиком в память.
+4 -1
View File
@@ -4,7 +4,10 @@ pub const MAX_BODY_SIZE: u64 = 1024 * 1024;
use nom::{ use nom::{
bytes::complete::tag, bytes::complete::tag,
combinator::verify, // verify — комбинатор nom: проверяет результат парсера по условию.
// Пока не использую, т.к. собственная валидация через thiserror
// позволяет возвращать более конкретные ошибки.
// use nom::combinator::verify;
number::complete::{ number::complete::{
be_u8, be_u8,
be_u16, be_u16,