Refreshing Rust
About five years ago I first heard of Rust and went through a tutorial. I was intrigued by the ownership concept: finally a new language that isn’t just a “me too” copy of another language with slightly adapted syntax. I kept up with the release notes over the years and made a few small patches to projects here and there, but never really applied the language. Recently I came across a simple task that felt like a great way to see what I remembered and whether Rust had evolved, and it did not disappoint.
Parsing a file and merging entries
ddrescue is a forensic disk recovery utility. It writes a log file with a header followed by a bunch of lines in a simple line-based text format:
# header
# lines
0x0000 0x0100 +
0x0100 0x0001 ?
0x0101 0x0100 +
0x0200 0x0200 ?
The ddrescue log file format: a few header lines followed by data lines. The header is simplified; one line actually holds the current position and is not prefixed with #.
In my case, I had a 700 MB log file where most chunks were tiny but (almost) consecutive and shared the same status. In the example above, the first two data lines could be merged into one by dropping the second line and adding its size to the first block. Merging my real-life file produced a file of only a few kilobytes. That makes a huge difference when loading the visual representation of the process in ddrescueview, a viewer for that log format.
The problem doesn’t need Rust: a simple awk, Python or Perl script would have done the trick. But I was curious how Rust would fare, and I was not disappointed, even though it took me a lot longer than a scripted solution would have. My weekly quota for trying out new things wasn’t used up just yet.
Visualization of a ddrescue log file. Red blocks are damaged drive sectors, green was read successfully and gray still needs to be read. Blue areas need further (automated) attention.
Great compiler errors and warnings
Rust already had useful error messages years ago, and the tradition continues, helping newcomers get on board:
115 | fn reader_from_str(&String log) -> BufRead {
| --------^^^
| | |
| | expected one of `:`, `@`, or `|`
| help: declare the type after the parameter binding: `<identifier>: <type>`
Rust notices that I’m used to declarations as Type variable_name and suggests turning them around.
A linter is included, and it isn’t overly obsessive. Some linters bark at everything; Rust’s seems quite conservative, yet strong enough to create a sense of somewhat standardized practice in the community.
warning: unnecessary parentheses around `if` condition
--> src/main.rs:47:16
|
47 | if (skip_next) {
| ^ ^
A linter message with great attention to detail, like the correct positioning of the visual hints.
I could add more examples, but you get the idea.
A unique and beautiful language
Rust is unique in quite a few ways, and some very sharp minds are active in the compiler scene. Big kudos to everyone involved in making the ownership model more transparent to the casual user (lifetime elision, for example). It hardly ever gets in my way anymore.
I love that all possibilities have to be handled in a match (switch) statement, pattern matching for destructuring objects in if clauses (if let Foo(x) = myfoo { println!(x); }), enum with parameters, a macro language that doesn’t drive you crazy when you want more than small substitutions, variables that are immutable unless marked mutable, and much more. IDE integration with VS Code is flawless, too.
Easy to add tests
It’s worth noting how easily tests can be added to the respective files and modules.
#[cfg(test)]
mod tests {
use super::*;
use std::io::Cursor;
#[test]
fn test_merge_question_mark_entries() {
let log = "0x00000000 0x00000100 ?\n0x00000100 0x00000200 ?\n0x00000300 0x00000400 ?";
let expected_output = "0x00000000 0x00000700 ?\n";
let mut result = Vec::new();
process_ddrescue_log_file(&mut Cursor::new(log), &mut result).unwrap();
let result_str = String::from_utf8(result).unwrap();
assert_eq!(result_str, expected_output);
}
// ...
}
Tests can be added right where the code resides.
running 4 tests
test tests::test_unparsable_hex ... ok
test tests::test_merge_question_mark_entries ... ok
test tests::test_skip_header ... ok
test tests::test_merge_entries ... ok
Running the tests is a cargo test away.
Cargo
Cargo is the companion tool to the Rust compiler. It sets up the project (cargo new project-name), installs and manages modules and calls the compiler, and it has been there from the start.
Rust delivers
Best of all, I’ve never seen a language where, once a piece of code compiles, it (mostly) does what it’s supposed to. I’m not as productive in Rust as in Python just yet, but the language has not disappointed and has taken a step forward over the last few years. It’s also great to see Rust gaining traction in critical environments like the Linux kernel.
Still, to get a grasp of ownership and lifetimes, you should be familiar with how lower-level languages without garbage collection work, and with concepts like stack and heap, for Rust to be a truly lovable language.