One thing that I had to dig up from previous #Enarx work was core::marker::PhantomData
. It is not so well known but pretty important concept inside Rust.
PhantomData
is a zero-sized struct that merely acts as a life-time indicator for the other parameter, which are usually pointers on where it is applied. It is used to implement many of the core structs such as Rc
to name one instance.
It is pretty good lesson on how lifetime parameters interact with the Rust compiler.
I’d even say that if you understand PhantomData
, then you have the basic understanding of Rust, and if not, you still have to learn a bit. It is the block that the whole core library is based on after all.
All the crates that #Google has done for #Rust seem to be like stuff I’ve been looking for to get better control of the memory.
Especially zerocopy
is a time saver as it has all the thinkable stuff that I have used previously core::slice::from_raw_parts
and spent a lot of time thinking of all the possible safety scenarios, such as this recent one:
impl<'a> From<&'a Header> for &'a [u8] {
fn from(value: &Header) -> Self {
// SAFETY: out-of-boundary is not possible, given that the size constraint
// exists in the struct definition. The lifetime parameter links the lifetime
// of the header reference to the slice.
unsafe { from_raw_parts((value as *const Header) as *const u8, size_of::<Header>()) }
}
}
Previously I’ve had to do similar consideration in the #Enarx project. You can do these by hand but it is nice to have a common crate, which is tested by many for these risky scenarios.
Other mentionable crate from Google is tinyvec
, which I’m going to use in zmodem2
to remove internal heap usage.
#zmodem2 is a nice history lesson to develop:
style: cleanup and fix cosmetic stuff
1. This inherits from original `zmodem` crate: "ZLDE" is in-fact ZDLE,
an acronym of "ZMODEM Data Link Escape" character.
2. Fine-tune use statements.
Link: https://wiki.synchro.net/ref:zmodem
Signed-off-by: Jarkko Sakkinen <jarkko.sakkinen@iki.fi>
That link in the commit message is a great source of information on #zmodem.
converted legacy hard coded test cases for frame to rstest
in the #zmodem 2 crate:
#[cfg(test)]
mod tests {
use crate::frame::*;
#[rstest::rstest]
#[case(Encoding::ZBIN, Type::ZRQINIT, &[ZPAD, ZLDE, Encoding::ZBIN as u8, 0, 0, 0, 0, 0, 0, 0])]
#[case(Encoding::ZBIN32, Type::ZRQINIT, &[ZPAD, ZLDE, Encoding::ZBIN32 as u8, 0, 0, 0, 0, 0, 29, 247, 34, 198])]
fn test_header(
#[case] encoding: Encoding,
#[case] frame_type: Type,
#[case] expected: &[u8]
) {
let header = Header::new(encoding, frame_type);
let mut packet = vec![];
new_frame(&header, &mut packet);
assert_eq!(packet, expected);
}
#[rstest::rstest]
#[case(Encoding::ZBIN, Type::ZRQINIT, &[1, 1, 1, 1], &[ZPAD, ZLDE, Encoding::ZBIN as u8, 0, 1, 1, 1, 1, 98, 148])]
#[case(Encoding::ZHEX, Type::ZRQINIT, &[1, 1, 1, 1], &[ZPAD, ZPAD, ZLDE, Encoding::ZHEX as u8, b'0', b'0', b'0', b'1', b'0', b'1', b'0', b'1', b'0', b'1', 54, 50, 57, 52, b'\r', b'\n', XON])]
fn test_header_with_flags(
#[case] encoding: Encoding,
#[case] frame_type: Type,
#[case] flags: &[u8; 4],
#[case] expected: &[u8]
) {
let header = Header::new(encoding, frame_type).flags(flags);
let mut packet = vec![];
new_frame(&header, &mut packet);
assert_eq!(packet, expected);
}
}
Should be easier to refactor the legacy code now as there is less raw code that might be affected in tests.
I hope I got this right (safety-proprty), i.e. so that references are enforced to have equal life-time:
impl<'a> From<&'a Header> for &'a [u8] {
fn from(value: &Header) -> Self {
// SAFETY: out-of-boundary is not possible, given that the size constraint
// exists in the struct definition. The lifetime parameter links the lifetime
// of the header reference to the slice.
unsafe { from_raw_parts((value as *const Header) as *const u8, size_of::<Header>()) }
}
}
great implemented transmutable (with u8
) enum
for the frame type of the #ZMODEM transfer protocol header: https://github.com/jarkkojs/zmodem2/commit/c76316c2ecae097be03506e8ce19d61287e6468a
I can use this as a reference model for refactoring rest of the code base.
Next, I’ll replace encoding: u8
with a similar enum Encoding
.
After trial and error, i.e. brute force going through crc
crate algorithms, I can say that CRC32_32_ISO_HDLC
is the correct variant for ZMODEM 32-bit transfers, i.e. cipher can be acquired by:
const CRC32: Crc<u32> = Crc::<u32>::new(&CRC_32_ISO_HDLC);
It is better to declare like this so that it gets compiled into .rodata
and not initialized at run-time.
I really like this heapless
. It sort of helps to implement my strategy for developing Rust programs:
1.Maximize no_std
surface. 2 Minimize heap allocations.
It is easier to see then the hot zones where the program actually dynamically grows for a good reason, similarly as with unsafe
blocks it is easy to the red zones for memory errors. This helps a lot with availability and protection against denial-of-service (DoS) attacks.
So to summarize I don’t split Rust program in my mind just to “unsafe” and “safe” but instead I split it “unsafe”, “static” and “dynamic”, or along the lines.
really like this fsmetry
crate (also no_std
):
fsmentry::dsl! {
#[derive(Debug)]
pub Mode {
WaitingInput -> WaitingCommand -> WaitingInput;
WaitingCommand -> SendingFile -> WaitingInput;
WaitingCommand -> ReceivingFile -> WaitingInput;
WaitingCommand -> Exit;
}
}
I use it manage life-cycle in my small serial port tool tior
. I also have some #zmodem code together but it is apparently much bigger leap to implement the #cli interface than it is to implement the protocol. I had to take some time to refactor existing code (e.g. to put FSM in place) and now I’m doing file path auto-completing interface for sending and receiving files with zmodem.
For the text input I’m going to use inquire.
I guess the definition of feature complete for 0.1 version is fully working zmodem transfers and known bugs have been fixed. Right now there is a single known bug: https://github.com/jarkkojs/tior/issues/1.