Compare commits
13 Commits
359337bc10
..
master
| Author | SHA1 | Date | |
|---|---|---|---|
| 9bc62acbc3 | |||
| 46f2721570 | |||
| e45b86cd20 | |||
| 3c3487b554 | |||
| dd6ef1d96d | |||
| 94030c34ca | |||
| b80b8e9e04 | |||
| 80b8c125ad | |||
| 6a269ca087 | |||
| d59e9d83a4 | |||
| 7bacf7ac73 | |||
| 0f2cb0384c | |||
| 9f2d7f635e |
Generated
+37
@@ -163,6 +163,12 @@ version = "0.4.33"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "0ceec5bc11778974d1bcb055b18002eba7f4b3518b6a0081b3af5f21666da9ad"
|
||||
|
||||
[[package]]
|
||||
name = "memchr"
|
||||
version = "2.8.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "cf8baf1c55e62ffcace7a9f06f4bd9cd3f0c4beb022d3b367256b91b87513d98"
|
||||
|
||||
[[package]]
|
||||
name = "mio"
|
||||
version = "1.2.1"
|
||||
@@ -208,9 +214,20 @@ name = "nocloud-core"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"local-ip-address",
|
||||
"nom",
|
||||
"thiserror",
|
||||
"tokio",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "nom"
|
||||
version = "8.0.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "df9761775871bdef83bee530e60050f7e54b1105350d6884eb0fb4f46c2f9405"
|
||||
dependencies = [
|
||||
"memchr",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "parking_lot"
|
||||
version = "0.12.5"
|
||||
@@ -346,6 +363,26 @@ dependencies = [
|
||||
"unicode-ident",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "thiserror"
|
||||
version = "2.0.18"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "4288b5bcbc7920c07a1149a35cf9590a2aa808e0bc1eafaade0b80947865fbc4"
|
||||
dependencies = [
|
||||
"thiserror-impl",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "thiserror-impl"
|
||||
version = "2.0.18"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ebc4ee7f67670e9b64d05fa4253e753e016c6c95ff35b89b7941d6b856dec1d5"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "tokio"
|
||||
version = "1.52.3"
|
||||
|
||||
@@ -13,3 +13,5 @@ panic = "abort"
|
||||
[dependencies]
|
||||
tokio = { version = "1", features = ["full"] }
|
||||
local-ip-address = "0.6"
|
||||
thiserror = "2.0"
|
||||
nom = "8.0.0"
|
||||
|
||||
+107
-34
@@ -1,8 +1,20 @@
|
||||
use std::net::SocketAddr;
|
||||
use std::{
|
||||
io,
|
||||
net::SocketAddr,
|
||||
};
|
||||
|
||||
use tokio::io::{AsyncReadExt, AsyncWriteExt};
|
||||
use tokio::net::{TcpStream};
|
||||
use std::io;
|
||||
use tokio::{
|
||||
io::{AsyncReadExt, AsyncWriteExt},
|
||||
net::TcpStream,
|
||||
};
|
||||
|
||||
use crate::{protocol::{
|
||||
Command, HEADER_SIZE, Header, decode_header,
|
||||
}, upload};
|
||||
|
||||
struct IncomingMessage {
|
||||
header: Header,
|
||||
}
|
||||
|
||||
pub struct Connection {
|
||||
id: u64,
|
||||
@@ -27,46 +39,107 @@ impl Connection {
|
||||
|
||||
pub async fn run(mut self) -> io::Result<()> {
|
||||
println!(
|
||||
"Обработчик {} создан для {}",
|
||||
"Соединение {} установлено с {}",
|
||||
self.id,
|
||||
self.client_addr
|
||||
);
|
||||
|
||||
let mut buffer = [0_u8; 1024];
|
||||
while let Some(message) = self.read_message().await? {
|
||||
self.message_count += 1;
|
||||
self.print_message(message).await?;
|
||||
}
|
||||
|
||||
loop {
|
||||
let bytes_read = self.stream.read(&mut buffer).await?;
|
||||
println!(
|
||||
"Соединение {} с {} закрыто клиентом",
|
||||
self.id,
|
||||
self.client_addr
|
||||
);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn read_message(
|
||||
&mut self,
|
||||
) -> io::Result<Option<IncomingMessage>> {
|
||||
let Some(header) = self.read_header().await? else {
|
||||
return Ok(None);
|
||||
};
|
||||
//Заголовок скачен и валиден. Получаем команду из заголовка
|
||||
match header.command() {
|
||||
Command::Upload => {
|
||||
println!("Команда UPLOAD");
|
||||
let upl_result=
|
||||
upload::handle(&mut self.stream, header).await.map_err(|error| {
|
||||
io::Error::new(
|
||||
io::ErrorKind::InvalidData,
|
||||
format!("Ошибка UPLOAD:{error:}")
|
||||
)
|
||||
}
|
||||
)?;
|
||||
}
|
||||
|
||||
Command::Download => {
|
||||
println!("Команда DOWNLOAD");
|
||||
}
|
||||
|
||||
Command::Delete => {
|
||||
println!("Команда DELETE");
|
||||
}
|
||||
|
||||
Command::Stat => {
|
||||
println!("Команда STAT");
|
||||
}
|
||||
|
||||
Command::FileData => {
|
||||
println!("Команда FILEDATA");
|
||||
}
|
||||
|
||||
Command::Response => {
|
||||
println!("Команда RESPONSE");
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
//let body = self.read_body(header.body_size()).await?;
|
||||
|
||||
Ok(None)
|
||||
}
|
||||
|
||||
async fn read_header(&mut self) -> io::Result<Option<Header>> {
|
||||
let mut buffer = [0_u8; HEADER_SIZE];
|
||||
|
||||
// Первый байт читаем отдельно, чтобы отличить нормальное
|
||||
// закрытие соединения от оборванного заголовка.
|
||||
let bytes_read = self.stream.read(&mut buffer[..1]).await?;
|
||||
|
||||
if bytes_read == 0 {
|
||||
println!(
|
||||
"Обработчик {}: клиент {} отключился",
|
||||
self.id,
|
||||
self.client_addr
|
||||
);
|
||||
|
||||
return Ok(());
|
||||
return Ok(None);
|
||||
}
|
||||
|
||||
self.message_count += 1;
|
||||
// Читаем остальную часть буфера от 1 до конца общего заголовка
|
||||
self.stream.read_exact(&mut buffer[1..HEADER_SIZE]).await?;
|
||||
|
||||
let received =
|
||||
String::from_utf8_lossy(&buffer[..bytes_read]);
|
||||
|
||||
println!(
|
||||
"Обработчик {}: сообщение №{}: {:?}",
|
||||
self.id,
|
||||
self.message_count,
|
||||
received
|
||||
);
|
||||
|
||||
let response = format!(
|
||||
"Обработчик {}: сообщение №{}: {}\n",
|
||||
self.id,
|
||||
self.message_count,
|
||||
received
|
||||
);
|
||||
|
||||
self.stream.write_all(response.as_bytes()).await?;
|
||||
//Парсим скачанный заголовок через nocloud_core::protocol parse_header
|
||||
let header =
|
||||
decode_header(&buffer).map_err(|error| {
|
||||
io::Error::new(
|
||||
io::ErrorKind::InvalidData,
|
||||
format!("Ошибка разбора заголовка: {error:?}"),
|
||||
)
|
||||
})?;
|
||||
Ok(Some(header))
|
||||
}
|
||||
|
||||
async fn print_message(
|
||||
&mut self,
|
||||
message: IncomingMessage,
|
||||
) -> io::Result<()> {
|
||||
println!(
|
||||
"Соединение {}: сообщение №{}, заголовок: {:?}",
|
||||
self.id,
|
||||
self.message_count,
|
||||
message.header
|
||||
);
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
+262
-3
@@ -1,6 +1,265 @@
|
||||
# Статус разработки проекта
|
||||
|
||||
## 📌 Текущее состояние
|
||||
* **Где остановился:** Сервер успешно запускается и слушает сетевой сокет.
|
||||
* **Проблема:** Не удалось проверить эхо-ответ через PowerShell.
|
||||
* **Следующий шаг:** Найти рабочий способ отправки тестового запроса в PowerShell или использовать альтернативный инструмент.
|
||||
|
||||
- **Где остановился:** Окультурил контракт между protocol connection, работает точно также, но
|
||||
- архитектурно красивей.
|
||||
- Далее, нужно реализовать UPLOAD с проверкой валидностей имени файла, размеров. И последующей передачей в storage/
|
||||
- Ниже - чеклист для реализации.
|
||||
- **Проблема:**
|
||||
- - нет проблем 😇
|
||||
|
||||
## Чек-лист реализации `UPLOAD`
|
||||
|
||||
### 1. Подготовить диспетчеризацию команд
|
||||
|
||||
- [✔] Оставить `decode_header()` единственной публичной функцией получения проверенного `Header`.
|
||||
- [✔] Убедиться, что `connection.rs` больше не вызывает `header.validate()`.
|
||||
- [✔] После получения `Header` выполнить `match` по `header.command()`.
|
||||
- [✔] Для `0x01` вызвать обработчик `UPLOAD`.
|
||||
~~[✔] Для остальных команд пока возвращать ошибку `UnsupportedCommand`.~~
|
||||
- [✔] Для остальных вызвать свои обработчики
|
||||
- [✔] Проверить: тестовый заголовок `command = 0x01` попадает в обработчик `UPLOAD`.
|
||||
|
||||
Ожидаемый промежуточный результат:
|
||||
|
||||
```text
|
||||
Получена команда UPLOAD
|
||||
```
|
||||
|
||||
### 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_RELATIVE_PATH_BYTES
|
||||
```
|
||||
|
||||
- [✔] Пока не добавлять в структуру содержимое файла.
|
||||
|
||||
### 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` в одном соединении.
|
||||
|
||||
+2
-2
@@ -1,5 +1,6 @@
|
||||
mod connection; //модуль отвечающий за соединения
|
||||
mod protocol; //модуль отвечающий за протокол
|
||||
mod protocol; //модуль отвечающий за протокол ...
|
||||
mod upload; //Модуль отвечающий за загрузку файла
|
||||
|
||||
use tokio::net::{TcpListener};
|
||||
use tokio::io;
|
||||
@@ -30,7 +31,6 @@ async fn main() -> io::Result<()> {
|
||||
client_addr,
|
||||
stream,
|
||||
);
|
||||
|
||||
tokio::spawn(async move {
|
||||
if let Err(error) = connection.run().await {
|
||||
eprintln!(
|
||||
|
||||
+2
-1
@@ -5,11 +5,12 @@
|
||||
2. Вернуть структуру Header c понятными полями заголовка:
|
||||
a. version: u8 //Версия протокола
|
||||
b. command: u8 //Тип сообщения
|
||||
Код Команда Кто отправляет
|
||||
0x01 UPLOAD клиент → сервер
|
||||
0x02 DOWNLOAD клиент → сервер
|
||||
0x03 DELETE клиент → сервер
|
||||
0x04 STAT клиент → сервер
|
||||
0x80 RESPONSE в обе стороны
|
||||
0x80 RESPONSE клиент ↔ сервер
|
||||
0x81 FILE_DATA сервер → клиент
|
||||
c. flags: u16 //Дополнительные признаки
|
||||
0x0001 — разрешено перезаписать существующий файл
|
||||
|
||||
+177
@@ -0,0 +1,177 @@
|
||||
// Парсим заголовок на базе Nom
|
||||
pub const HEADER_SIZE: usize = 20;
|
||||
|
||||
use nom::{
|
||||
bytes::complete::tag,
|
||||
// verify — комбинатор nom: проверяет результат парсера по условию.
|
||||
// Пока не использую, т.к. собственная валидация через thiserror
|
||||
// позволяет возвращать более конкретные ошибки.
|
||||
// use nom::combinator::verify;
|
||||
number::complete::{
|
||||
be_u8,
|
||||
be_u16,
|
||||
be_u32,
|
||||
be_u64,
|
||||
},
|
||||
IResult,
|
||||
Parser,
|
||||
};
|
||||
|
||||
use thiserror::Error;
|
||||
|
||||
#[derive(Debug, Error)]
|
||||
pub enum HeaderError {
|
||||
#[error("Некорректный формат заголовка")]
|
||||
InvalidFormat,
|
||||
|
||||
#[error("Неподдерживаемая версия протокола: {0}")]
|
||||
UnsupportedVersion(u8),
|
||||
|
||||
#[error("Неизвестная команда протокола: {0}")]
|
||||
UnsupportedCommand(u8),
|
||||
|
||||
#[error("Неизвестный набор флагов: {0}")]
|
||||
UnsupportedFlagSet(u16),
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct Header {
|
||||
version: u8,
|
||||
command: u8,
|
||||
flags: u16,
|
||||
request_id: u32,
|
||||
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> {
|
||||
// Парсим заголовок:
|
||||
let (remaining, header) = parse_header(input)
|
||||
.map_err(|_| HeaderError::InvalidFormat)?;
|
||||
|
||||
// Выходим с ошибкой, если заголовок был недочитан парсером:
|
||||
if !remaining.is_empty() {
|
||||
return Err(HeaderError::InvalidFormat);
|
||||
}
|
||||
|
||||
//Валидация заголовка
|
||||
header.validate()?;
|
||||
|
||||
Ok(header)
|
||||
}
|
||||
|
||||
fn parse_header(
|
||||
input: &[u8],
|
||||
) -> IResult<&[u8],Header> {
|
||||
//1. Парсим магические байты "NCLD"
|
||||
let (input, _) = tag(&b"NCLD"[..]).parse(input)?;
|
||||
//2. Парсим и валидируем версию
|
||||
let (input, version) = be_u8(input)?;
|
||||
//3. Парсим и валидируем Header
|
||||
let (input, command) = be_u8(input)?;
|
||||
//4. Парсим и валидируем flags
|
||||
let (input, flags) = be_u16(input)?;
|
||||
//5. Парсим Request без валидации
|
||||
let (input, request_id) = be_u32(input)?;
|
||||
//6. Парсим и валидируем величину заголовка
|
||||
let (input, body_size) = be_u64(input)?;
|
||||
|
||||
let local_header = Header{
|
||||
version,
|
||||
command,
|
||||
flags,
|
||||
request_id,
|
||||
body_size,
|
||||
};
|
||||
Ok((input,local_header))
|
||||
}
|
||||
|
||||
// Реализация публичных методов-геттеров для безопасного доступа к приватным полям
|
||||
impl Header {
|
||||
/// Возвращает версию протокола пакета
|
||||
pub fn version(&self) -> u8 {
|
||||
// Так как тип u8 реализует трейт Copy, значение просто копируется наружу
|
||||
self.version
|
||||
}
|
||||
|
||||
/// Возвращает код команды (например, 1, 2, 80 или 81)
|
||||
pub fn command(&self) -> Command {
|
||||
Command::try_from(self.command)
|
||||
.expect("Header уже прошёл валидацию команды")
|
||||
}
|
||||
|
||||
/// Возвращает битовые флаги пакета (уже прошедшие валидацию)
|
||||
pub fn flags(&self) -> u16 {
|
||||
self.flags
|
||||
}
|
||||
|
||||
/// Возвращает уникальный идентификатор запроса клиента
|
||||
pub fn request_id(&self) -> u32 {
|
||||
self.request_id
|
||||
}
|
||||
|
||||
/// Возвращает размер тела сообщения в байтах
|
||||
pub fn body_size(&self) -> u64 {
|
||||
self.body_size
|
||||
}
|
||||
|
||||
///Валидаторы для данных из заголовка:
|
||||
///
|
||||
// Валидация Header
|
||||
pub fn validate(&self) -> Result<(), HeaderError> {
|
||||
//Валидация version
|
||||
if self.version != 1 {
|
||||
return Err(
|
||||
HeaderError::UnsupportedVersion(self.version)
|
||||
);
|
||||
}
|
||||
|
||||
// Валидация command
|
||||
Command::try_from(self.command)?;
|
||||
|
||||
// Валидация flags
|
||||
if !(0..=7).contains(&self.flags){
|
||||
return Err(HeaderError::UnsupportedFlagSet (self.flags));
|
||||
};
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,44 @@
|
||||
use tokio::{io::{self, AsyncReadExt}, net::TcpStream, stream};
|
||||
|
||||
use crate::protocol::Header;
|
||||
|
||||
const UPLOAD_PREFIX_SIZE: usize = 10; //Размер префикса в UPLOAD
|
||||
pub const MAX_RELATIVE_PATH_BYTES: usize = 128 * 1024; //Размер пути с именем
|
||||
|
||||
struct UploadMetadata {
|
||||
path_size: u64,
|
||||
file_name: String,
|
||||
}
|
||||
|
||||
|
||||
pub async fn handle(stream: &mut TcpStream, header: Header) -> io::Result<()> {
|
||||
println!("Обработчик UPLOAD получил заголовок: {header:?}");
|
||||
|
||||
let mut prefix_buffer = [0_u8; UPLOAD_PREFIX_SIZE];
|
||||
stream.read_exact(&mut buffer).await?;
|
||||
|
||||
println!(
|
||||
"Получены {UPLOAD_PREFIX_SIZE} байт метаданных UPLOAD: {buffer:?}"
|
||||
);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
impl UploadMetadata {
|
||||
pub(crate) fn new(
|
||||
file_size: u64,
|
||||
relative_path: String,
|
||||
) -> Self {
|
||||
Self {
|
||||
file_size,
|
||||
relative_path,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn file_size(&self) -> u64 {
|
||||
self.file_size
|
||||
}
|
||||
|
||||
pub fn relative_path(&self) -> &str {
|
||||
&self.relative_path
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user