-
Notifications
You must be signed in to change notification settings - Fork 53
Protocol tests changes #76
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Closed
Closed
Changes from all commits
Commits
Show all changes
2 commits
Select commit
Hold shift + click to select a range
File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,92 +1,77 @@ | ||
| use super::{client_id::ClientId, timestamp_32::Timestamp32}; | ||
| use crate::udp::connection::client_id::ClientId; | ||
| use crate::udp::connection::timestamp_32::Timestamp32; | ||
|
|
||
| /// The data stored inside the connection id | ||
| #[derive(PartialEq, Debug, Copy, Clone)] | ||
| pub struct ConnectionIdData { | ||
| pub client_id: ClientId, | ||
| pub expiration_timestamp: Timestamp32 | ||
| } | ||
| pub struct ConnectionIdData(pub [u8; 8]); | ||
|
|
||
| impl ConnectionIdData { | ||
| pub fn from_bytes(bytes: &[u8; 8]) -> Self { | ||
| let client_id = Self::extract_client_id(bytes); | ||
| let expiration_timestamp = Self::extract_timestamp(bytes); | ||
| Self { | ||
| client_id, | ||
| expiration_timestamp | ||
| } | ||
| pub fn from_bytes(bytes: &[u8]) -> Self { | ||
| let mut sized_bytes_arr = [0u8; 8]; | ||
|
|
||
| sized_bytes_arr.copy_from_slice(&bytes[..8]); | ||
|
|
||
| Self(sized_bytes_arr) | ||
| } | ||
|
|
||
| pub fn from_client_id_and_timestamp(client_id: ClientId, timestamp: Timestamp32) -> Self { | ||
| let bytes_vec = [client_id.value, timestamp.0.to_le_bytes()].concat(); | ||
|
|
||
| Self::from_bytes(&bytes_vec) | ||
| } | ||
|
|
||
| pub fn as_bytes(&self) -> &[u8; 8] { | ||
| &self.0 | ||
| } | ||
|
|
||
| pub fn client_id(&self) -> &[u8] { | ||
| &self.0[..4] | ||
| } | ||
|
|
||
| pub fn to_bytes(&self) -> [u8; 8] { | ||
| let connection_id: Vec<u8> = [ | ||
| self.client_id.to_bytes().as_slice(), | ||
| self.expiration_timestamp.to_le_bytes().as_slice(), | ||
| ].concat(); | ||
|
|
||
| let connection_as_array: [u8; 8] = connection_id.try_into().unwrap(); | ||
|
|
||
| connection_as_array | ||
| pub fn timestamp(&self) -> u32 { | ||
| u32::from_le_bytes([self.0[4], self.0[5], self.0[6], self.0[7]]) | ||
| } | ||
|
|
||
| fn extract_timestamp(decrypted_connection_id: &[u8; 8]) -> Timestamp32 { | ||
| let timestamp_bytes = &decrypted_connection_id[4..]; | ||
| let timestamp = Timestamp32::from_le_bytes(timestamp_bytes); | ||
| timestamp | ||
| pub fn timestamp_as_bytes(&self) -> &[u8] { | ||
| &self.0[4..] | ||
| } | ||
|
|
||
| fn extract_client_id(decrypted_connection_id: &[u8; 8]) -> ClientId { | ||
| ClientId::from_slice(&decrypted_connection_id[..4]) | ||
| } | ||
| } | ||
|
|
||
| #[cfg(test)] | ||
| mod tests { | ||
| use crate::udp::connection::{connection_id_data::ConnectionIdData, client_id::ClientId}; | ||
|
|
||
| use std::net::{IpAddr, Ipv4Addr, SocketAddr}; | ||
| use crate::protocol::clock::current_timestamp; | ||
| use crate::udp::connection::{connection_id_data::ConnectionIdData}; | ||
| use crate::udp::connection::client_id::ClientId; | ||
| use crate::udp::connection::timestamp_32::Timestamp32; | ||
|
|
||
| #[test] | ||
| fn it_contains_a_client_id() { | ||
| fn it_should_be_instantiated_from_a_client_id_and_timestamp32() { | ||
| let client_id = ClientId::from_socket_address(&SocketAddr::new(IpAddr::V4(Ipv4Addr::new(127, 0, 0, 1)), 8081)); | ||
|
|
||
| let connection_id = ConnectionIdData { | ||
| client_id: ClientId::from_slice(&[0u8; 4]), | ||
| expiration_timestamp: 0u32.into(), | ||
| }; | ||
| let expiration_timestamp: Timestamp32 = (current_timestamp() + 120).into(); | ||
|
|
||
| assert_eq!(connection_id.client_id, ClientId::from_slice(&[0u8; 4])); | ||
| } | ||
|
|
||
| #[test] | ||
| fn it_contains_an_expiration_timestamp() { | ||
| let connection_id = ConnectionIdData::from_client_id_and_timestamp(client_id, expiration_timestamp); | ||
|
|
||
| let connection_id = ConnectionIdData { | ||
| client_id: ClientId::from_slice(&[0u8; 4]), | ||
| expiration_timestamp: 0u32.into(), | ||
| }; | ||
|
|
||
| assert_eq!(connection_id.expiration_timestamp, 0u32.into()); | ||
| assert_eq!(connection_id.client_id(), client_id.value.as_slice()); | ||
| assert_eq!(connection_id.timestamp(), expiration_timestamp.0) | ||
| } | ||
|
|
||
| #[test] | ||
| fn it_should_be_converted_to_a_byte_array() { | ||
| fn it_should_be_instantiated_from_a_byte_array() { | ||
| let bytes = [0, 0, 0, 0, 255, 255, 255, 255]; | ||
|
|
||
| let connection_id = ConnectionIdData { | ||
| client_id: ClientId::from_slice(&[0u8; 4]), | ||
| expiration_timestamp: (u32::MAX).into(), | ||
| }; | ||
| let connection_id = ConnectionIdData::from_bytes(&bytes); | ||
|
|
||
| assert_eq!(connection_id.to_bytes(), [0, 0, 0, 0, 255, 255, 255, 255]); | ||
| assert_eq!(connection_id.as_bytes(), &bytes); | ||
| } | ||
|
|
||
| #[test] | ||
| fn it_should_be_instantiated_from_a_byte_array() { | ||
|
|
||
| let connection_id = ConnectionIdData::from_bytes(&[0, 0, 0, 0, 255, 255, 255, 255]); | ||
| fn it_should_have_a_timestamp_that_equals_u32max() { | ||
| let bytes = [0, 0, 0, 0, 255, 255, 255, 255]; | ||
|
|
||
| let expected_connection_id = ConnectionIdData { | ||
| client_id: ClientId::from_slice(&[0, 0, 0, 0]), | ||
| expiration_timestamp: (u32::MAX).into(), | ||
| }; | ||
| let connection_id = ConnectionIdData::from_bytes(&bytes); | ||
|
|
||
| assert_eq!(connection_id, expected_connection_id); | ||
| assert_eq!(connection_id.timestamp(), u32::MAX); | ||
| } | ||
| } | ||
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
hi @WarmBeer, The whole point of this struct was to deliberately create a higher abstraction for what we store inside the connection id. I did not want to deal with low-level byte operations, only for the constructor and the converter.
I wanted to make it more explicit what a ConnectionId contains.
With your current changes is more like a parser.
Maybe your approach is more clear because right now, it's a parser/converter but then we should rename it to
ConnectionIdDataPackeror something like that.Anyway, I would make sure we will not have any extra behaviour for this struct. For example, we could need to check if two
ConnectionIdDatahave the sameClientIdor we could move some logic to check if the ConnectionId has expired. If we have such changes in the future, I would revert the change because I prefer to work with structs instead of byte arrays. I prefer to keep the byte arrays in the lower layers. I'm afraid the app could end up having a lot of cryptic byte array operations.There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Hey @josecelano , the reason why I wanted to store the whole ConnectionIdData as one bytes array instead of two is because the following function returns a new owned
[u8; 8]of two already existing[u8; 4]:My thought process was that ideally we'd want to return
&[u8; 8]from two&[u8; 4], but unfortunately Rust does not support this yet. We can do the reverse however and that is why I chose to store the whole ConnectionIdData as one bytes array instead and then pass ClientId and Timestamp32 by reference:&[u8]as a subset of[u8; 8].Passing by reference is usually the preferred way in Rust because it saves memory in most cases. However, in this case I'm not even sure if it will save memory since the data types we are passing are so small.
I'm fine with whatever you decide to keep or change. If you want to keep the existing ConnectionIdData format, we could change the above function so that it returns a moved value instead of a copied value:
Not tested.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@WarmBeer interesting ...
My thought process is: sometimes you need to convert a Model object into another type. And sometimes, just to keep it simple, you decide to put the converter inside the Model like this:
The
toJson()method creates a new object, but you do not want to change the original Model object.You could separate responsibilities like this:
In our case, we only need to use the
fromJsonandtoJson.If we use your approach,
into_bytesfor me would be a little bit weird because it seems you are destroying the Model object and converting it into a different thing.In that case, I would call it
ConnectionIdDataBuilderor something like that.I have not found a place in the code where the ConnectionIdData is used after calling
to_bytes. I suppose I found it weird because, in other languages, you would not be sure if that is going to happen elsewhere, so you do not do it (in fact, you can't). But in Rust, if that were the case, the compiler would tell you.For example, in this function:
I would not be able to call
connection_id_data.client_idafterencrypt_connection_id_databecause internally it converts theconnection_id_datainto the byte array.Besides, I suppose Rust would drop the variables as soon as possible. If I create a new array and the ConenctionIdData is not used anymore, the ConenctionIdData would be dropped.
In that example, I would not expect
connection_id_datato be a byte array. It would be surprising if, after callingencrypt_connection_id_datatheconnection_id_datahas been mutated. People would think: why do you need to change theconnection_id_datain order to build a new encrypted version?Uh oh!
There was an error while loading. Please reload this page.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I know it might seem a little strange if you are used to OOP languages, but in Rust this is preferred. We don't want to waste memory by keeping two identical pieces of data in memory if they will always be identical for as long as they both live. We should either pass the original data by reference or move ownership of the data.
Exceptions for this are usually very small data types such as u8, u16, u32, i8, i6, i32 etc.. Passing these values as copied takes about as much memory as passing them as reference. This is why you never see &u32 etc..
If you still need
connection_id_dataafter encrypting, then it does not make sense to move its data to another place. Instead we should likely pass a read only/shared reference of the data to the encrypting function. Eg:&ConnectionIdData. But if we don't need to retain the data inconnection_id_dataanymore after encrypting it. It makes sense to just move ownership ofconnection_id_datato the encryption function instead of passing it a temporary copy ofconnection_id.Bare in mind that a function that takes
selfinstead of&selfcan only be called from an owned reference. You can not call these functions from a shared reference, so you always know that the original data can not be suddenly manipulated or moved from another function.There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@WarmBeer thank you for helping me to understand Rust better.
I see how powerful the concept is in terms of saving memory. You can reuse the same allocated memory and pass the ownership safely because you always know the owner.
But the thing I do not like it's that it seems Rust "forces" you to change the internal representation of your objects just to save memory. Since you know you want to convert the ConnectionIdData into a byte array, store it as a byte array to avoid having both objects in memory for a while.
My reasoning is:
For example, in this new function:
You return a byte array instead of a ClientId, and that's used in the verification:
I suppose that only works because you know that internally the
ClientIdis a[u8; 4].But the
ClientIdimplementation could change. In fact, you changed theClientId:Making the value public.
You can fix it by constructing the initial value again:
But it seems weird to me because you are storing values in a low-level format and converting back and forth when you need them inside your struct.
Sorry for being picky. This helps me to understand Rust and also rusty style better.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Hey @josecelano , I only decided to store the client_id and timestamp in low-level format because they both only contain a small byte array. If the client_id had another field such as:
It would be easier to just store the entire client_id as struct in connection_id_data. When the client_id is needed, we just return a reference from the connection_id_data:
This way we won't have to copy any data where we don't need ownership of.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We're not changing the internal representation of the object. We are forcibly dropping the object (instead of waiting for Rust to drop it on the end of scope) and recycling some if its data for a new object :).
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
hi @WarmBeer, the discussion is becoming even more interesting.
String, but if you have some rules, you can create anEmailabstraction. In this case, you are using the abstraction in the constructor to make sure we validate theClientIdandTimestamp32. But then you do not need them anymore. The getters for them return the lower-level values. The problem with this approach is consumers of this struct have to deal with those low-level representations ofClientIdandTimesampt32:They do not even know those values are a
ClientIdandTimestamp32.How do you know that two ClientIds are equal if their low-level representation are equal.
That's something known only by those structs.
2.- You are prioritizing the use of the bytes array. You are prioritizing memory consumption over CPU. If you have many calls to
client_idandtimestampmethods, you would constantly be somehow re-constructing those objects. It seems the object has two responsibilities: storing information you want to retrieve later and transforming that information into a low-level dto format. It's OK if we want to merge those responsibilities, but I would change the getters to generate the original values in the constructor.3.- Owernship
This made me think a lot about ownership. It seems you do not want to repeat twice different representations of the same data in memory to consume less memory. It's like a DRY principle but for memory.
I think we build classes (structs) that have data and behaviour. Just because two classes use the same data, that does not mean we should build only one class. For example, a domain Model and its Dto representation. They could have the same data:
Both
UserandUserDtouse two strings, but in the model, you want to check things.I suppose you are doing something like:
For me, ownership should be used to ensure only one owner can mutate the state of the struct at a given time. The process would be as follows: first, you design your entities, then control the state with the ownership.
In this case, I have the feeling we are doing the opposite. Since we know
UserandUserDtowill not mutate state independently, we join them together to save memory.I feel that this strategy, in the long term, can lead to a poor design or at least a design close to the machine with a lot of low level data structures.
My question is would you do the same if you had infinite memory? I mean, just because we are using Rust or code priority is to costume less memory rather than maybe make it more readable. I'm totally fine with that approach. I suppose for this kind of service; performance is key. But I think, in general, this kind of software is also very cryptic.
In our example:
We know that after calling
encrypt_connection_id_datawe do not need theconnection_id_data. It's going to be dropped anyway soon, but if you want to save memory we could:I think with this solution; we explicitly save memory by dropping variables we are sure we will not use anymore. But I guess that should not improve memory consumption too much because it will be dropped anyway.
This leads me to another thought: small methods reduce memory consumption because you do not have to keep variables around too much. But I'm sure the Rust compiler does that
dropin the middle of the method.There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
For me storing the client_id and timestamp in one byte array was just a neat trick to avoid the function that returns the whole_connection_id_data as a new byte array ([u8; 8]). I only considered doing this because both ClientId and Timestamp32 are only a
[u8; 4]and will probably only ever be just that. If ClientId and/or Timestamp32 are mostly needed to be borrowed in struct form (so that you can use specialized impl for these structs) then it would make more sense to store them in struct form instead of low-level.From your example I would do the following (Rust playground: link):
This way, the UserDto struct only hold references to the original Strings instead of having owned copies of the strings. The implication of this is that
User::as_dto()can only be called as long as theUseris still in scope. You can test this in the Rust playground by addingstd::mem::drop(user);afterlet user_dto = user.as_dto();.There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'd think that passing a shared reference instead of allocating new memory for a copy would not only save memory, but would also be quicker. I think this is highly dependent on how big the data type is that you are trying to pass/copy though.