The main program

You are now able to parse skippable frames. Let's write a program which displays, for each skippable frame, its magic number and content. Also, the program will check that the file is a well-formed zstd file.

Command-line arguments

We want our program to act like this:

$ cargo run -- --help
Usage: net7212 [OPTIONS] <FILE_NAME>

Arguments:
  <FILE_NAME>  File name to decompress

Options:
  -i, --info     Dump information about frames instead of outputing the result
  -h, --help     Print help
  -V, --version  Print version
  
$ cargo run -- --info skippables.zst
SkippableFrame(
    SkippableFrame {
        magic: 0x184d2a53,
        data: [
            0x10,
            0x20,
            0x30,
        ],
    },
)
SkippableFrame(
    SkippableFrame {
        magic: 0x184d2a51,
        data: [
            0x42,
        ],
    },
)

(you can download skippables.zst)

Create the main program

✅ Create the src/main.rs file containing a main function.

✅ Use clap to implement command-line arguments handling.

You can read a file using std::fs::read(). Do not print anything yet, we will do it step by step.

Decoding a frame

As previously explained, a decoding phase must happen after the parsing of a frame when --info is not used as an argument to the main program.

✅ Implement a decode(self) method on a Frame returning the decoded data in a Vec<u8>.

Note that self means that the Frame object no longer exist after decode() has been called. This is expected, as some decoding operations will consume frame data.

So, at this time:

  • You can decode a SkippableFrame as it decodes to nothing (it is skipped after all).
  • You may use todo!() while decoding a ZstandardFrame as not even the parsing is implemented.

Iterating over the frames

Since all frames must be parsed in sequence, one can use an iterator to transform a forward byte parser into a succession of Frame objects.

✅ Add a FrameIterator in the frame module, which encapsulates a ForwardByteParser.

✅ Implement the Iterator trait for FrameIterator, the Item being Result<Frame>.

The iterator's next() method will:

  • Return None if the parser has no content left to indicate the end of the iterator.
  • Return Some(Frame::parse(&mut self.parser)) otherwise.

Note that this will automatically signal an error if extra characters are present at the end of an otherwise valid file, as the next iteration will attempt to parse another frame which will be invalid.

Writing the main program body

✅ Complete the main program so that it can either print information or decode frames from a zstd files in sequence.

Of course you must stop if the frame iterator returns an error.

Do not forget that:

  • You can derive the Debug trait automatically on many types.
  • You can display something implementing the Debug trait with the {:?} construct.
  • You can use an alternate representation with the {:#?} construct (for example indented structures).
  • You can use an alternate representation and print numbers in hexadecimal with {:#x?}.

Also, when --info is not used, the decoded data (even though there is nothing to display yet) must be sent to the standard output. You can use the write_all() method to write on std::io::stdout() after locking it.

Note that if you use the color_eyre crate, you can make your main() function return a color_eyre::eyre::Result<()> and let color_eyre display a pretty error if there is one. You can also use the anyhow crate as an alternative.

Adding tracing information

Since your program will use the standard output when decoding a file, you must not use it yourself to print debugging information. You should use the log crate to add traces if you need them, or the tracing/tracing_subscriber couple.

✅ Check that you can decode a file made only of skippable frames, as well as an empty file (which is a valid zstd file with zero frames). Check that you signal an error if you try to decode an invalid file.