Решение на Network Packets от Митко Чакъров

Обратно към всички решения

Към профила на Митко Чакъров

Резултати

  • 20 точки от тестове
  • 0 бонус точки
  • 20 точки общо
  • 15 успешни тест(а)
  • 0 неуспешни тест(а)

Код

#[cfg(test)]
mod tests {
#[test]
fn it_works() {
assert_eq!(2 + 2, 4);
}
}
use std::fmt;
/// Грешките, които ще очакваме да върнете. По-долу ще е описано кои от тези грешки очакваме да се
/// върнат в каква ситуация.
///
#[derive(Debug)]
#[derive(PartialEq)]
pub enum PacketError {
InvalidPacket,
InvalidChecksum,
UnknownProtocolVersion,
CorruptedMessage,
}
/// Нужна е имплементация на Display за грешките, за да може да имплементират `std::error::Error`.
/// Свободни сте да напишете каквито искате съобщения, ще тестваме само типовете, не низовия им
/// вид.
///
/// Ако са във формат на хайку, няма да получите бонус точки, но може да получите чувство на
/// вътрешно удовлетворение.
///
impl fmt::Display for PacketError {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
match self {
PacketError::InvalidPacket => write!(f, "Invalid Packet"),
PacketError::InvalidChecksum => write!(f, "Invalid Checksum"),
PacketError::UnknownProtocolVersion => write!(f, "Unknown Protocol Version"),
PacketError::CorruptedMessage => write!(f, "Corrupted Message")
}
}
}
/// Тази имплементация би трябвало да сработи директно благодарение на горните. При желание, можете
/// да си имплементирате ръчно някои от методите, само внимавайте.
///
impl std::error::Error for PacketError {}
/// Един пакет, съдържащ част от съобщението. Изберете сами какви полета да използвате за
/// съхранение.
///
/// Може да е нужно да добавите lifetimes на дефиницията тук и/или на методите в impl блока.
///
#[derive(PartialEq, Debug)]
pub struct Packet {
protocol_version : u8,
payload : Vec<u8>,
checksum : u32
}
impl Packet {
/// Конструира пакет от дадения slice от байтове. Приема параметър `size`, който е размера на
/// payload-а на новия пакет. Връща двойка от пакет + оставащите байтове. Тоест, ако имате низа
/// "abcd" и викнете метода върху байтовата му репрезентация с параметър `size` равен на 3, ще
/// върнете двойката `(<пакет с payload "abc">, <байтовия низ "d">)`.
///
/// Байтове от низ можете да извадите чрез `.as_bytes()`, можете и да си конструирате байтов
/// литерал като b"abcd".
///
/// Ако подадения `size` е по-голям от дължината на `source`, приемаме, че размера ще е точно
/// дължината на `source` (и остатъка ще е празен slice).
///
/// Ако параметъра `size` е 0, очакваме тази функция да panic-не (приемаме, че това извикване
/// просто е невалидно, програмистка грешка).
///
pub fn from_source(source: &[u8], size: u8) -> (Self, &[u8]) {
if size < 1 {
panic!()
}
let mut sum : u32 = 0;
let min = std::cmp::min(size as usize, source.len());
for i in 0..min {
sum += source[i] as u32;
};
(
Packet { protocol_version : 1, payload : source[0..min].to_vec(), checksum : sum },
if min < source.len() as usize { &source[min..]} else { &[] }

На практика, тук не е нужно да имаш if-клауза. Стига останалата логика да ти е вярна, тук директно можеш да върнеш &source[min..] -- тъй като min винаги ще е по-малко или равно на размера на slice-а.

)
}
/// Връща само slice-а който пакета опакова. Тоест, ако сме конструирали пакета със
/// `Packet::from_source(b"abc", 3)`, очакваме `.payload()` да ни върне `b"abc"`.
///
/// Защо това просто не е публично property? За да не позволяваме мутация, а само конструиране
/// и четене.
///
pub fn payload(&self) -> &[u8] {
&self.payload
}
pub fn checksum_to_bytes(&self) -> Vec<u8> {
let mut v = self.checksum;
let mut vec = Vec::new();
while vec.len() != 4 {
vec.push((v % 256) as u8);
v /= 256;
}
vec.reverse();
vec
}

Разделянето на байтове е добре имплементирано, но можеше да използваш вградената функция .to_be_bytes(). Така съдържанието на тази функция може да е просто self.checksum.to_be_bytes().to_vec(). (И всъщност, предвид че по-долу използваш extend_from_slice, вероятно можеше просто да сложиш долу self.checksum.to_be_bytes().)

/// Сериализира пакета, тоест превръща го в байтове, готови за трансфер. Версия, дължина,
/// съобщение (payload), checksum. Вижте по-горе за детайлно обяснение.
///
pub fn serialize(&self) -> Vec<u8> {
let mut result = Vec::new();
result.push(self.protocol_version);
result.push(self.payload.len() as u8);
result.extend_from_slice(&self.payload);
result.extend_from_slice(&self.checksum_to_bytes());
result
}
/// Имайки slice от байтове, искаме да извадим един пакет от началото и да върнем остатъка,
/// пакетиран в `Result`.
///
/// Байтовете са репрезентация на пакет -- версия, размер, и т.н. както е описано по-горе.
///
/// Ако липсват версия, размер, чексума, или размера е твърде малък, за да може да се изпарси
/// валиден пакет от байтовете, връщаме грешка `PacketError::InvalidPacket`.
///
/// Ако версията е различна от 1, връщаме `PacketError::UnknownProtocolVersion`.
///
/// Ако checksum-а, който прочитаме от последните 4 байта на пакета е различен от изчисления
/// checksum на payload-а (сумата от байтовете му), връщаме `PacketError::InvalidChecksum`.
///
/// Забележете, че ако размера е по-голям от истинския размер на payload-а, се очаква
/// `PacketError::InvalidPacket`. Ако размера е по-малък от истинския размер на payload-а,
/// въпросния ще се изпарси, но чексумата ще е грешна, така че ще очакваме
/// `PacketError::InvalidChecksum`. Малко тъпо! Но уви, протоколите имат подобни тъпи ръбове,
/// особено като са написани за един уикенд. Авторите обещават по-добър протокол за версия 2.
///
pub fn deserialize(bytes: &[u8]) -> Result<(Packet, &[u8]), PacketError> {
let len = bytes.len();
if len > 2 && len >= bytes[1] as usize + 6 {
let packet_len = bytes[1] as usize + 6;
if bytes[0] == 1 {
let mut checksum = 0;
let mut op = 24;
let checksum_start = bytes[1] as usize + 2;
for i in checksum_start..checksum_start + 4 {
checksum += (bytes[i] as u32) << op;
op -= 8;
}
let packet = Packet::from_source(&bytes[2..(bytes[1] as usize + 2)], bytes[1]).0;
if checksum == packet.checksum {
Ok( (packet, &bytes[packet_len..bytes.len()]))
} else { Err(PacketError::InvalidChecksum)}
} else { Err(PacketError::UnknownProtocolVersion) }
} else { Err(PacketError::InvalidPacket) }

Досадното на вложените if-клаузи е, че else-овете са далеч от логиката, която ги причинява. Според мен това би било по-четимо:

if bytes[0] != 1 {
    return Err(PacketError::UnknownProtocolVersion);
}
// ... останалата част от кода

По този начин намаляваш nesting-а и нямаш 13 реда между проверката и грешката. Същото се отнася и за останалите проверки -- често е по-удачно да провериш за проблем и веднага да излезеш, вместо да влагаш if-клаузи.

}
}
/// Структура, която ще служи за итериране по пакети. Ще я конструираме от някакво съобщение, и
/// итерацията ще връща всеки следващ пакет, докато съобщението не бъде напълно "изпратено".
/// Изберете каквито полета ви трябват.
///
/// Може да е нужно да добавите lifetimes на дефиницията тук и/или на методите в impl блока.
///
pub struct PacketSerializer {
message : Vec<u8>,
packet_size : u8
}
impl Iterator for PacketSerializer {
type Item = Packet;
fn next(&mut self) -> Option<Self::Item> {
if self.message.len() > 0 {
let (packet, remaining) = Packet::from_source(&self.message, self.packet_size);
self.message = remaining.to_vec();
Some(packet)
} else { None }
}
}
/// Този trait ще ни позволи да конвертираме един `String` (а ако искаме, и други неща) от и до
/// комплект от байтове за прехвърляне по мрежата.
///
/// Детайли за методите вижте по-долу в имплементацията на този trait за `String`.
///
pub trait Packetable: Sized {
fn to_packets(&self, packet_size: u8) -> PacketSerializer;
fn to_packet_data(&self, packet_size: u8) -> Vec<u8>;
fn from_packet_data(packet_data: &[u8]) -> Result<Self, PacketError>;
}
impl Packetable for String {
/// Този метод приема размер, който да използваме за размера на payload-а на всеки пакет. Връща
/// итератор върху въпросните пакети. Низа трябва да се използва под формата на байтове.
///
/// Както при `.from_source`, ако подадения `packet_size` е по-голям от дължината на оставащите
/// байтове, приемаме, че размера на съответния пакет ще е колкото остава.
///
fn to_packets(&self, packet_size: u8) -> PacketSerializer {
PacketSerializer { message: self.as_bytes().to_vec(), packet_size : packet_size }
}
/// Имайки итератор по пакети, лесно можем да сериализираме всеки индивидуален пакет в поредица
/// от байтове със `.serialize()` и да го натъпчем във вектора.
///
/// Както при `.from_source`, ако подадения `packet_size` е по-голям от дължината на оставащите
/// байтове, приемаме, че размера на съответния пакет ще е колкото остава.
///
fn to_packet_data(&self, packet_size: u8) -> Vec<u8> {
let mut data = Vec::new();
for packet in self.to_packets(packet_size) {
data.append(&mut packet.serialize());
}
data
}
/// Обратното на горния метод е тази асоциирана функция -- имайки slice от байтове които са
/// сериализирана репрезентация на пакети, искаме да десериализираме пакети от този slice, да
/// им извадим payload-ите, и да ги сглобим в оригиналното съобщение.
///
/// Грешките, които могат да се върнат, са същите, които идват от `.deserialize()`.
///
/// Една допълнителна грешка, която може да се случи е при сглобяване на съобщението -- ако е
/// имало липсващ пакет, може съчетанието на байтовете да не генерира правилно UTF8 съобщение.
/// Тогава връщаме `PacketError::CorruptedMessage`.
///
fn from_packet_data(packet_data: &[u8]) -> Result<Self, PacketError> {
let mut msg = String::new();
let mut data = packet_data;
while data.len() > 0 {
match Packet::deserialize(data) {
Ok((packet, remaining_msg)) => {
data = remaining_msg;
if let Ok(slice) = std::str::from_utf8(packet.payload()) {
msg.push_str(slice);
} else { return Err(PacketError::CorruptedMessage) }
},
Err(packet_errror) => return Err(packet_errror)
}
}
Ok(msg)
}
}

Лог от изпълнението

Compiling solution v0.1.0 (/tmp/d20200111-2173579-1kdz4ae/solution)
    Finished test [unoptimized + debuginfo] target(s) in 3.81s
     Running target/debug/deps/solution-a73e64ec87929bd0

running 1 test
test tests::it_works ... ok

test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out

     Running target/debug/deps/solution_test-38971695424b36d5

running 15 tests
test solution_test::test_construct_packet_from_unicode ... ok
test solution_test::test_construct_packet_no_remainder ... ok
test solution_test::test_construct_packet_with_remainder ... ok
test solution_test::test_construct_packet_with_remainder_cyrillic ... ok
test solution_test::test_consuming_packets ... ok
test solution_test::test_deserialize_invalid_packet ... ok
test solution_test::test_deserialize_packet ... ok
test solution_test::test_deserialize_unicode_packet ... ok
test solution_test::test_full_roundtrip ... ok
test solution_test::test_full_roundtrip_for_zero_size_string ... ok
test solution_test::test_invalid_packet_combination ... ok
test solution_test::test_iterating_packets ... ok
test solution_test::test_iterating_packets_for_zero_size_string ... ok
test solution_test::test_serialize_packet ... ok
test solution_test::test_zero_size ... ok

test result: ok. 15 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out

   Doc-tests solution

running 0 tests

test result: ok. 0 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out

История (1 версия и 4 коментара)