Add Serde and Bincode for Data Serialization

Mục tiêu: Update the Cargo.toml file to include the serde and bincode crates. This prepares the project for serializing and deserializing data structures, which is essential for sending file metadata over a network.


Excellent work completing the entire project setup and command-line argument parsing step! You now have a robust CLI foundation that can distinguish between send and receive commands and correctly capture user input. This structure is essential for building the rest of our application.

Now, we pivot from the user interface to the core networking logic. Before we can send a single byte of the actual file, the sender and receiver need to communicate some essential information. The receiver needs to know, “What is the name of the file I’m about to receive?” and “How large is it?” This information is what we call metadata. Our first task in defining our custom communication protocol is to decide how to send this metadata from the sender to the receiver.

The Challenge: Sending Structured Data Over a Network

A network connection, like the TcpStream we will use later, is fundamentally a stream of bytes. It doesn’t inherently understand the concept of a “filename” or a “file size”. If we just sent the text “my_document.pdf” followed by the number “1024768”, the receiver would get a sequence of bytes and have no reliable way to know where the filename ends and the size begins.

To solve this, we need a process called Serialization. Serialization is the process of converting a complex data structure in memory (like a Rust struct) into a specific, well-defined format of bytes that can be stored or transmitted. The reverse process, converting those bytes back into the original data structure, is called Deserialization.

Introducing serde and bincode: The Dynamic Duo of Rust Serialization

To handle this in our project, we will use two of the most popular and powerful crates in the Rust ecosystem:

  1. serde (SERialize/DEserialize): This is the de-facto serialization framework for Rust. It provides the core traits and machinery that allow your custom data types (like our upcoming FileMetadata struct) to be “serializable”. A key feature of serde is that it’s format-agnostic. It provides the “how” of serialization but not the “what format”. This is where bincode comes in.
  2. bincode: This crate is a specific serialization format. It works with serde to take your data structures and convert them into a very compact, efficient, and fast binary format. For a peer-to-peer file transfer application where performance is key and human-readability of the metadata isn’t required, bincode is an excellent choice over text-based formats like JSON or XML.

Just like we did for clap, our first step is to declare these crates as dependencies in our project’s manifest file.

Adding the Dependencies to Cargo.toml

Open your Cargo.toml file at the root of your project directory. Find the [dependencies] section and add the lines for serde and bincode.

[package]
name = "p2p_file_sharer"
version = "0.1.0"
edition = "2021"

# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html

[dependencies]
clap = { version = "4.5.4", features = ["derive"] }
# HIGHLIGHT START
# Add serde for serialization/deserialization. The "derive" feature enables
# powerful macros that will automatically implement the necessary traits for us.
serde = { version = "1.0", features = ["derive"] }

# Add bincode as our chosen format for converting our structs into bytes.
bincode = "1.3.3"
# HIGHLIGHT END

Let’s examine these new lines:

  • serde = { version = "1.0", features = ["derive"] }:

    • This adds the serde crate.
    • version = "1.0": We are specifying a version of serde. It’s a very stable and foundational crate, so 1.0 is appropriate.
    • features = ["derive"]: This is a crucial piece. Just like with clap, enabling the derive feature for serde gives us access to procedural macros. This means we’ll be able to make our FileMetadata struct serializable by simply adding #[derive(Serialize, Deserialize)] above its definition. serde will then generate all the complex implementation code for us at compile time, which is incredibly convenient and less error-prone.
  • bincode = "1.3.3":

    • This adds the bincode crate.
    • version = "1.3.3": We pin the dependency to a specific, stable version to ensure our builds are reproducible.

What Happens Next?

You’ve now successfully declared your new dependencies. You won’t see any change in your program’s behavior yet, but you’ve given Cargo the instructions it needs. The next time you run cargo build, cargo run, or cargo check, Cargo will automatically:

  1. Read the updated Cargo.toml file.
  2. Download the specified versions of serde and bincode from crates.io.
  3. Compile them and cache the results for future use.

With these powerful tools now part of our project, we are perfectly set up for the next task: creating the FileMetadata struct that will represent the information we need to send before the file transfer begins.

Further Reading

Define a Rust Struct for File Metadata

Mục tiêu: Create a public FileMetadata struct in Rust with filename (String) and size (u64) fields. This struct will serve as the data blueprint for information sent before a file transfer in a P2P application.


Excellent, you’ve successfully added the serde and bincode crates to your project’s dependencies. You’ve equipped your application with the tools for serialization, but these tools need something to work on. Now, it’s time to define the exact data structure that will carry our file’s metadata across the network.

Defining the Blueprint: The FileMetadata Struct

Before we transfer a file, the sender must tell the receiver two critical pieces of information: the name of the file and its total size. To group this related information together into a single, clean package, we will use one of Rust’s most fundamental features: the struct.

A struct, short for structure, is a custom data type that lets you name and package together multiple related values that make up a meaningful group. Think of it as a blueprint for a concept. In our case, the concept is “file metadata,” and its blueprint will contain a filename and a file size.

We will create this struct in our src/main.rs file. For now, it can live near the top, as it’s a core data type that both our sender and receiver logic will eventually need.

Open src/main.rs and add the following FileMetadata struct definition. A good place is right after the use statements and before the Cli struct definition.

// src/main.rs

use clap::{Parser, Subcommand};
use std::path::PathBuf;

// NEW: Define the structure for our file metadata.
// This struct will be serialized and sent over the network before the file content.
// `pub` makes the struct and its fields accessible from other modules we'll create later.
pub struct FileMetadata {
    /// The name of the file being transferred.
    pub filename: String,
    /// The total size of the file in bytes.
    pub size: u64,
}

/// A simple Peer-to-Peer (P2P) file sharing CLI application written in Rust.
#[derive(Parser, Debug)]
// ... rest of the file

Deconstructing the FileMetadata Struct

Let’s break down this small but crucial piece of code.

pub struct FileMetadata

  • struct FileMetadata: This line declares a new struct named FileMetadata. The name is clear and descriptive, which is a hallmark of good Rust code.
  • pub: The pub keyword is short for “public”. In Rust, all items (structs, functions, enums, etc.) are private by default, meaning they can only be used within the module they are defined in. By marking the struct pub, we are making it visible and usable by other parts of our application. We will soon create separate sender.rs and receiver.rs modules, and they will both need to know about FileMetadata. Making it public now is good forward-thinking.

The Fields: filename and size

Inside the curly braces {}, we define the fields of the struct, which are the individual pieces of data it holds.

  • pub filename: String:
    • We declare a field named filename. We also make the field itself pub so that code outside the module can access it.
    • Its type is String. A String in Rust is an owned, heap-allocated, and growable text type. This is the perfect choice for a filename, which can have a variable length.
  • pub size: u64:
    • We declare a field named size, also public.
    • Its type is u64. This choice is deliberate and important for building a robust application.
      • The u means it’s an unsigned integer, which is correct because a file size can never be negative.
      • The 64 means it’s a 64-bit integer. This allows us to represent incredibly large numbers (up to 18.4 quintillion bytes, or 18.4 exabytes). Using a smaller type like u32 would limit our application to files under 4 gigabytes, which is an unnecessary restriction for a modern tool. Choosing u64 ensures our application can handle very large files without any issues.

You have now successfully defined the data contract for our communication protocol. This simple FileMetadata struct is the blueprint for the information that will precede every file transfer, ensuring the receiver is fully prepared for the data it’s about to receive.

The next logical step is to connect this struct to the serde framework we just added. We will do this by adding a derive macro to our new struct, which will automatically teach it how to be serialized and deserialized.

Further Reading

Enable Serialization for a Rust Struct with Serde

Mục tiêu: Add the Serialize and Deserialize derive macros from the serde crate to the FileMetadata struct, enabling it to be converted into a byte stream for network communication.


You’ve just created the blueprint for our communication protocol by defining the FileMetadata struct. This is a fantastic step! However, right now, it’s just a standard Rust struct. It exists in your program’s memory, but it has no inherent ability to be transformed into a stream of bytes suitable for sending over a network. To bridge this gap, we need to teach our struct how to speak the language of serialization.

Activating Serialization with Serde’s Derive Macros

In the first step of this project, you became familiar with Rust’s powerful derive macros when you used #[derive(Parser)] from clap. These macros are a form of metaprogramming that allows us to add new behaviors and capabilities to our data types with a single line of code. The serde crate provides a similar mechanism for serialization.

By adding #[derive(Serialize, Deserialize)] to our FileMetadata struct, we are instructing serde to automatically generate all the necessary code to implement its core traits:

  • Serialize: This trait provides the logic for converting our FileMetadata struct into a data format that a library like bincode can understand. It’s the “encoding” part of the process.
  • Deserialize: This trait provides the reverse logic. It defines how to take a data format (produced by bincode) and convert it back into a FileMetadata struct instance. This is the “decoding” part that the receiver will use.

Let’s make this simple but powerful change. Open your src/main.rs file and add the use statement for serde’s traits and the derive macro to your struct definition.

// src/main.rs

use clap::{Parser, Subcommand};
// HIGHLIGHT START
// Import the Serialize and Deserialize traits from the serde crate.
// We need these in scope for the derive macro to work.
use serde::{Serialize, Deserialize};
// HIGHLIGHT END
use std::path::PathBuf;

// HIGHLIGHT START
// Add the derive macro here. This line automatically implements the
// `Serialize` and `Deserialize` traits for our FileMetadata struct.
#[derive(Serialize, Deserialize)]
// HIGHLIGHT END
pub struct FileMetadata {
    /// The name of the file being transferred.
    pub filename: String,
    /// The total size of the file in bytes.
    pub size: u64,
}

/// A simple Peer-to-Peer (P2P) file sharing CLI application written in Rust.
// ... rest of the file

Deconstructing the Change

Let’s analyze what these two small additions accomplish.

  1. use serde::{Serialize, Deserialize}; This line brings the Serialize and Deserialize traits into the current scope. While the derive macro itself is the main actor, these traits must be accessible for the generated code to be valid.
  2. #[derive(Serialize, Deserialize)] This is the core of the task. When the Rust compiler sees this line, it invokes serde’s code-generation logic. serde inspects the FileMetadata struct and sees that it has two fields: filename of type String and size of type u64. Crucially, serde already knows how to serialize and deserialize common standard library types like String and u64. Because all the fields within our struct are themselves serializable, serde can automatically generate the correct implementation for the entire FileMetadata struct. It effectively writes the boilerplate code for us, which would look something like this (conceptually):

    To Serialize:

    1. Serialize the filename field.
    2. Serialize the size field.
    3. Combine them in a defined order.

    To Deserialize:

    1. Deserialize a String for the filename field.
    2. Deserialize a u64 for the size field.
    3. Construct a new FileMetadata instance with this data.

By using the derive macro, we avoid having to write this tedious and error-prone logic by hand. Our code remains clean, declarative, and focused on the data structure itself, not the mechanics of its serialization.

Our FileMetadata struct is now “serialization-aware”. It has all the necessary capabilities to be converted into a compact byte stream by bincode and then perfectly reconstructed on the other end. The next logical step is to prove that this works. We will write a small test function to create an instance of our struct, serialize it to bytes, and then deserialize it back to ensure no data was lost in the process.

Further Reading

Create a Unit Test for the FileMetadata Struct in Rust

Mục tiêu: Add a new test module to the main.rs file using conditional compilation #[cfg(test)]. Create a test function that instantiates the FileMetadata struct to lay the groundwork for serialization testing.


Fantastic! You’ve successfully equipped your FileMetadata struct with the power of serialization and deserialization by adding the #[derive(Serialize, Deserialize)] macro. This is a crucial step, but in software engineering, we follow a principle: “trust, but verify.” Before we start building our networking logic around this struct, we need to be absolutely certain that the serialization process works flawlessly. The best way to gain this confidence is by writing an automated test.

The Importance of Testing and Rust’s Built-in Framework

Writing tests is a fundamental practice in modern software development. It allows us to verify that our code behaves as expected, catch bugs early, and refactor with confidence, knowing that our tests will alert us if we break something.

Fortunately, Rust has a first-class, built-in testing framework that is incredibly easy to use. You don’t need to install any external test runners or libraries. The testing functionality is provided directly by Cargo and the Rust compiler. We’ll leverage this now to create a small test that validates our FileMetadata struct.

Creating a Test Module and Function

In Rust, it’s a common convention to place unit tests in the same file as the code they are testing. To keep the test code separate and ensure it’s not included in your final application binary, we’ll create a special module at the bottom of our src/main.rs file.

Open src/main.rs and add the following code block to the very end of the file.

// src/main.rs

// ... (all your existing code from main.rs should be here) ...

// HIGHLIGHT START: This is a new code block added to the end of the file.

// This is a special attribute that tells the Rust compiler to only compile
// and run this module's code when we execute `cargo test`. It will be
// completely ignored during a normal `cargo build` or `cargo run`.
#[cfg(test)]
mod tests {
    // The `use super::*;` line brings all the items from the parent module
    // (in this case, the main part of `main.rs`) into the scope of our `tests`
    // module. This is necessary so we can access `FileMetadata` inside our test.
    use super::*;

    // This attribute marks the following function as a test function.
    // When you run `cargo test`, the test runner will look for and execute
    // any function annotated with `#[test]`.
    #[test]
    fn test_metadata_creation() {
        // Here, we create a sample instance of our `FileMetadata` struct.
        // We use "struct literal" syntax, specifying the value for each field.
        let metadata = FileMetadata {
            filename: String::from("test_file.txt"),
            size: 12345,
        };

        // For this task, we are just creating the instance.
        // In the upcoming tasks, we will add assertions here to verify
        // the serialization and deserialization process.
    }
}
// HIGHLIGHT END

Deconstructing the Test Code

Let’s break down this new block of code to understand each part thoroughly.

  • #[cfg(test)]: This is an attribute for conditional compilation. It’s a directive to the Rust compiler that says, “Only compile the following module (mod tests) if we are building for a test configuration.” When you run the cargo test command, Cargo sets this configuration. When you run cargo build or cargo run, it does not, so this entire block of code is effectively invisible and won’t be included in your final executable. This is a powerful feature that lets you write tests alongside your code without bloating your production binary.
  • mod tests { ... }: This declares a new module named tests. Modules are Rust’s way of organizing code into separate scopes and namespaces. It’s a strong convention to put unit tests for a file inside a tests submodule within that same file.
  • use super::*;: This is an important line. The code inside mod tests is in a different scope than the code outside of it. super refers to the parent module (the root of our main.rs file). The * is a glob operator that means “import everything.” So, this line translates to: “Bring all public items from the parent module into the current scope.” This is what allows our test_metadata_creation function to see and use the FileMetadata struct you defined earlier.
  • #[test]: This is the attribute that officially designates a function as a test. The test runner built into Rust will automatically discover any function with this attribute and execute it. A test function passes if it runs to completion without panicking. If it panics (for example, if an assert! macro fails), the test is marked as failed.
  • fn test_metadata_creation() { ... }: This is our test function. The name is descriptive, indicating its purpose. Inside, we perform the main action for this task:

    • let metadata = FileMetadata { ... }; We create a new instance of our FileMetadata struct. This is done using struct literal syntax, where we provide a value for each field defined in the struct.
    • filename: String::from("test_file.txt"): We create a new String for the filename.
    • size: 12345: We provide a simple u64 value for the size.

You can now run your test suite for the first time! Go to your terminal and execute:

cargo test

You should see output indicating that your test passed:

   Compiling p2p_file_sharer v0.1.0 (/path/to/p2p_file_sharer)
    Finished test [unoptimized + debuginfo] target(s) in 0.5s
     Running unittests (target/debug/deps/p2p_file_sharer-...)

running 1 test
test tests::test_metadata_creation ... ok

test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s

You have successfully created a test harness for your project and written your first test! Although this test doesn’t verify much yet, it lays the groundwork. In the very next task, you will build upon this function by adding the code to serialize this metadata instance into a vector of bytes using bincode.

Further Reading

Serialize a Rust Struct to Bytes with bincode

Mục tiêu: Learn how to use the bincode::serialize function to convert a Rust struct that implements serde::Serialize into a byte vector (Vec). This task involves calling the serialization function within a test, handling the Result with .unwrap(), and inspecting the resulting binary data.


You have successfully created a test function and instantiated your FileMetadata struct. This is the perfect setup to verify our serialization logic. Now, we’ll take that in-memory struct and convert it into the raw stream of bytes that we can eventually send over a network connection.

Turning Structs into Bytes with bincode

In the previous tasks, you added the bincode crate and used serde’s derive macro to make your FileMetadata struct “serializable.” It’s now time to use bincode’s primary function: bincode::serialize.

This function is the heart of the encoding process. It takes a reference to any data structure that implements serde’s Serialize trait (which ours does, thanks to #[derive(Serialize)]) and transforms it into a Vec<u8>.

A Vec<u8> (a vector of 8-bit unsigned integers) is the standard and most idiomatic way to represent a dynamic, growable array of raw bytes in Rust. This is exactly the format we need for network transmission.

The bincode::serialize function returns a Result<Vec<u8>, Box<dyn std::error::Error>>. This is a key aspect of Rust’s error handling philosophy. Serialization isn’t guaranteed to succeed (though for a simple struct like ours, it’s very unlikely to fail). By returning a Result, the function forces us to acknowledge and handle the possibility of an error. In our test, if serialization fails, we want the test to fail immediately. The easiest way to achieve this is by calling .unwrap() on the Result. The .unwrap() method will either return the Vec<u8> if the result is Ok, or it will panic if the result is Err, which in turn causes the test to fail—exactly the behavior we want.

Let’s update your test_metadata_creation function to perform this serialization.

Here are the changes for src/main.rs. We are only adding code inside the test function you created earlier.

// src/main.rs

// ... (keep all the existing code at the top of the file) ...

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn test_metadata_creation() {
        // This is the instance you created in the previous task.
        let metadata = FileMetadata {
            filename: String::from("test_file.txt"),
            size: 12345,
        };

        // --- NEW CODE FOR THIS TASK ---
        // Here we call the core serialization function from the bincode crate.
        // We pass a reference `&metadata` to the function.
        // It returns a `Result`, which we `.unwrap()` because in a test,
        // we want the program to panic if serialization fails.
        let serialized_data: Vec<u8> = bincode::serialize(&metadata).unwrap();

        // For learning purposes, let's inspect the serialized bytes.
        // This will print the length of the byte vector and the bytes themselves.
        // The output is not human-readable text, but a compact binary representation.
        println!(
            "Serialized metadata ({} bytes): {:?}",
            serialized_data.len(),
            serialized_data
        );
        // --- END OF NEW CODE ---
    }
}

Deconstructing the New Code

  • let serialized_data: Vec<u8> = ...: We declare a new variable, serialized_data, and explicitly annotate its type as Vec<u8> for clarity.
  • bincode::serialize(&metadata): This is the function call. We pass &metadata, which is an immutable reference to our metadata instance. We use a reference because the serialization process only needs to read the data; it doesn’t need to take ownership of or change the original metadata struct. This is an important concept in Rust related to ownership and borrowing.
  • .unwrap(): As explained above, this extracts the Vec<u8> from the Ok variant of the Result. If the result were an Err, this would cause the test to panic and fail, immediately alerting us to a problem.
  • println!(...): This line is purely for you to see what’s happening.
    • serialized_data.len(): We print the number of bytes the serialized data occupies. This can be useful for debugging.
    • {:?}: This is the “debug” format specifier. When used on a vector of integers, it prints them in a readable, comma-separated list.

Now, run your test again:

cargo test

Because of our println!, the test output will now include the serialized data. You’ll see something similar to this (the exact output can vary slightly with bincode versions, but the structure will be the same):

running 1 test
Serialized metadata (23 bytes): [13, 0, 0, 0, 0, 0, 0, 0, 116, 101, 115, 116, 95, 102, 105, 108, 101, 46, 116, 120, 116, 57, 48, 0, 0, 0, 0, 0, 0]
test tests::test_metadata_creation ... ok

(Note: I updated the byte array to reflect a more accurate potential output from bincode which includes the length of the string and the u64 value)

What are you seeing? It’s not the text “test_file.txt”. It’s bincode’s efficient binary representation. It includes the length of the string (13), the string’s characters as their UTF-8 byte values, and finally, the number 12345 encoded as a 64-bit integer. bincode handles all the low-level details like byte order (endianness) for you, ensuring the data can be correctly interpreted on different machine architectures.

You have now successfully converted an in-memory Rust struct into a vector of bytes. This is the “sender” side of the metadata exchange. In the final task of this step, we will complete the loop by deserializing these bytes back into a FileMetadata struct to prove that our protocol is robust and lossless.

Further Reading

In the same test, use bincode::deserialize to convert the Vec back into a FileMetadata struct and assert its contents are unchanged.

Mục tiêu:


You’ve successfully serialized your FileMetadata struct into a Vec<u8>, which is a perfect simulation of the data a sender would prepare for transmission. Now, we must complete the round trip to prove our protocol is sound. We need to take those bytes, simulate what a receiver would do, and turn them back into a meaningful FileMetadata struct, ensuring that no information was lost or corrupted in the process.

Completing the Round Trip: Deserialization and Verification

This final task is about deserialization and assertion. We will use bincode’s counterpart to serialize, the deserialize function, to convert our byte vector back into a struct. Then, we will use Rust’s built-in testing macros to assert that the reconstructed struct is identical to the original one.

A Prerequisite for Comparison: The PartialEq and Debug Traits

Before we can compare our original metadata struct with the deserialized one, we have to teach Rust how to do it. The assert_eq!(a, b) macro, which we will use to verify equality, requires that the items being compared implement the PartialEq trait. This trait provides the logic for the == operator.

Additionally, if the assertion fails, the macro needs to print the values of the two structs to show you the difference. To do this, the structs must implement the Debug trait, which provides a developer-friendly string representation of a type (the one you see with the {:?} format specifier).

Luckily, just like with Serialize and Deserialize, we can ask the compiler to automatically generate the correct implementations for us using the derive macro.

Let’s update our FileMetadata struct to include these two new traits.

Open src/main.rs and modify the derive line above your FileMetadata struct:

// src/main.rs

// ... (use statements)

// HIGHLIGHT START
// Add `PartialEq` and `Debug` to the derive macro.
// - `PartialEq` is needed for `assert_eq!` to compare two instances.
// - `Debug` is needed for `assert_eq!` to print the structs if they are not equal.
#[derive(Serialize, Deserialize, PartialEq, Debug)]
// HIGHLIGHT END
pub struct FileMetadata {
    pub filename: String,
    pub size: u64,
}

// ... (rest of the file)

With this simple change, our struct is now fully equipped for testing.

Deserializing and Asserting in Our Test

Now, we can add the final pieces to our test function. We will call bincode::deserialize and then use assert_eq! to confirm the round trip was successful.

Let’s modify the test_metadata_creation function at the bottom of src/main.rs:

// src/main.rs

// ...

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn test_metadata_creation() {
        // 1. Create the original data (as before)
        let metadata = FileMetadata {
            filename: String::from("test_file.txt"),
            size: 12345,
        };

        // 2. Serialize it into bytes (as before)
        let serialized_data: Vec<u8> = bincode::serialize(&metadata).unwrap();

        // (Optional) We can now remove the println! from the previous step
        // for a cleaner test output.

        // --- NEW CODE FOR THIS TASK ---
        // 3. Deserialize the bytes back into a struct.
        // The `deserialize` function takes a byte slice (`&[u8]`) as input.
        // We must explicitly tell it which type we expect to get back,
        // which we do here with the type annotation `: FileMetadata`.
        let deserialized_metadata: FileMetadata = bincode::deserialize(&serialized_data).unwrap();

        // 4. Assert that the original struct and the deserialized one are identical.
        // `assert_eq!` is a macro that compares its two arguments.
        // If they are equal, the test continues silently.
        // If they are not equal, the macro will panic, causing the test to fail,
        // and it will print both values to show the difference.
        assert_eq!(metadata, deserialized_metadata);
        // --- END OF NEW CODE ---
    }
}

Deconstructing the Final Test Logic

  1. let deserialized_metadata: FileMetadata = ...: We declare a new variable to hold the result of our deserialization. The type annotation : FileMetadata is crucial here. The deserialize function is generic; it can deserialize into any type that implements serde::Deserialize. We must tell the compiler our intended target type so it knows how to interpret the bytes.
  2. bincode::deserialize(&serialized_data): This is the inverse of serialize. It takes a slice of bytes (we pass a reference to our serialized_data vector) and attempts to construct a FileMetadata instance from it. Like serialize, it returns a Result, which we .unwrap() for the same reason as before: a failure here should fail the test.
  3. assert_eq!(metadata, deserialized_metadata): This is the moment of truth. This macro is the heart of verification in Rust tests. It compares the metadata instance we created at the start with the deserialized_metadata instance we just reconstructed. Because we added #[derive(PartialEq)], Rust knows how to compare them field by field. If metadata.filename == deserialized_metadata.filename AND metadata.size == deserialized_metadata.size, the assertion passes, and so does our test.

Now, run the test one last time:

cargo test

The output should be a clean, simple success message, confirming that your protocol works perfectly:

running 1 test
test tests::test_metadata_creation ... ok

test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s

Congratulations! You have successfully defined a communication protocol, created a data structure for your metadata, and written an automated test that proves you can reliably serialize it into bytes and deserialize it back into its original form without any data loss. This is a robust and essential foundation for the networking logic you’ll build in the next step, where you will implement the core logic for the sender.

Further Reading