diff --git a/src/main.MD b/src/main.MD index 0a99fa5..84d5ce1 100644 --- a/src/main.MD +++ b/src/main.MD @@ -1,63 +1,265 @@ # Статус разработки проекта ## 📌 Текущее состояние -* **Где остановился:** Сервер успешно запускается и слушает сетевой сокет. - Проверочная строка: 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 - выдаёт ошибку парсера: - Сервер слушает 192.168.1.213:32768 - Обработчик 1 создан для 192.168.1.170:50978 - Обработчик 1: Критическая ошибка! Неверный формат заголовка. - Обработчик 1: ошибка: Invalid header format - что радует. +* **Где остановился:** Окультурил контракт между protocol connection, работает точно также, но +* архитектурно красивей. +* Далее, нужно реализовать UPLOAD с проверкой валидностей имени файла, размеров. И последующей передачей в storage/ +* Ниже - чеклист для реализации. * **Проблема:** -* **Следующий шаг:** - ## Чек-лист: исправление парсера общего заголовка +* - нет проблем 😇 +## Чек-лист реализации `UPLOAD` - * [✔] Исправить имя функции `parce_header` → `parse_header`. - * [✔] Убрать `async` у `parse_header`: функция не выполняет ввод-вывод. - * [✔] Убрать `.await` при вызове `parse_header`. - * [ ] Исправить порядок полей согласно протоколу: +### 1. Подготовить диспетчеризацию команд - * `bytes[0..4]` — `magic`; - * `bytes[4]` — `version`; - * `bytes[5]` — `command`. - * [✔] Проверять `magic` как четыре байта `NCLD`. - * [✔] Проверять версию как число `1`, а не ASCII-символ `b'1'`. - * [✔] Пока разрешать только версию протокола `1`. - * [✔] Исправить команды `80 | 81` на `0x80 | 0x81`. - * [✔] Записывать в `Header.version` значение из `bytes[4]`. - * [✔] Для первого теста отправлять заголовок с `body_size = 0`. +* [ ] Оставить `decode_header()` единственной публичной функцией получения проверенного `Header`. +* [ ] Убедиться, что `connection.rs` больше не вызывает `header.validate()`. +* [ ] После получения `Header` выполнить `match` по `header.command()`. +* [ ] Для `0x01` вызвать обработчик `UPLOAD`. +* [ ] Для остальных команд пока возвращать ошибку `UnsupportedCommand`. +* [ ] Проверить: тестовый заголовок `command = 0x01` попадает в обработчик `UPLOAD`. - ## Проверка исправлений +Ожидаемый промежуточный результат: - * [✔] Отправить корректный заголовок. - * [✔] Убедиться, что сервер вывел: +```text +Получена команда UPLOAD +``` - ```text - Header { - version: 1, - command: 1, - flags: 0, - request_id: 17, - body_size: 0, - } - ``` +### 2. Создать постоянный модуль загрузки + +* [ ] Создать `upload.rs`. +* [ ] Подключить его в `main.rs`: + +```rust +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` только под имя. +* [ ] Прочитать ровно `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`. - * [ ] Читать данные файла порциями, а не целиком в память. diff --git a/src/protocol.rs b/src/protocol.rs index 51ed7b8..e7ba72f 100644 --- a/src/protocol.rs +++ b/src/protocol.rs @@ -4,7 +4,10 @@ pub const MAX_BODY_SIZE: u64 = 1024 * 1024; use nom::{ bytes::complete::tag, - combinator::verify, + // verify — комбинатор nom: проверяет результат парсера по условию. + // Пока не использую, т.к. собственная валидация через thiserror + // позволяет возвращать более конкретные ошибки. + // use nom::combinator::verify; number::complete::{ be_u8, be_u16,