Rust is a general-purpose programming language emphasizing performance, type safety, and concurrency. It enforces memory safety, meaning that all references point to valid memory. It does so without a traditional garbage collector; instead, both memory safety errors and data races are prevented by the “borrow checker”, which tracks the object lifetime of references at compile time.
reasons
- I want to add a systems programming language to my go-to languages
- compile time type safety
- compile to any target, including windows
- interesting concurrency model
- use in cyber security
- secure and performant security tooling
- secure and performant malware
rust cybersec quotes
![[Rust The Secrets of Rising Star in Cybersecurity - CloudThat Resources]]
rust project ideas
- customising Nostr relay nostr-rs-relay
- boflab rust version
- rewriting rasplogger
- turning tkconv into grafana
rust codebases
- rustscanner (nmap)
- ferox (file enumeration)
- libafl (fuzzer)
- lsniffglue (sniffer)
courses
- primeagen frontendmasters (rust for typescript developers)
- rust in 24 lunches
- tip https://www.udemy.com/course/ultimate-rust-crash-course/
libraries to investigate
- Serde is a powerful serialization and deserialization framework for Rust.
- Tokio is an asynchronous runtime for Rust, providing an event-driven platform for building fast, reliable, and lightweight network applications
- Rowan for parsing.
- web servers
- Anyhow is used for flexible error handling in Rust.
- Jiff for dates.
- Diesel is a safe, extensible ORM (Object-Relational Mapper) and Query Builder for Rust.
- Tantivy as an alternative to Lucene.
rust topics
types
- only u8 ints be converted to chars
- .len returns number of bytes, not the amount of characters
- . chars().count() returns amount of chars in string
- add _ to numbers for readability, they are ignored e.g 1_000_000
functions
- skinny arrow indicates return type
- line without ; indicates value that gets returned
- last line of function is the return
blocks
- capture variables in strings in curlies “var: {var}“
- variable lifetime ends outside of block
logging
- pretty print datastructures other than strings with {:?}
- multiline pretty print with {:#?}
traits
- ”what a type can do”
variables
- drill down into class const’s with :: , e.g. u8::MAX
- make variables mutable with mut
- you can’t change types of mut variable
- when shadowing variables in blocks you can recover the original value after the block scope is closed
- variables can be declared uninitialised, and then used within scopes without dying.
memory
- memory on stack needs fixed sizes
- memory on heap can have unknown data length, but it’s slower
- sometimes heap is not even available (embedded devices)
references
- rust reference is a “memory safe” pointer: a reference to “owned” memory
- references point to memory of other value: “borrowing” values
- references are prefixed with &
- let my_reference = &my_value
mem::size_of::<u8>
shows size of typemem::size_of_val(&reference)
shows size of value- functions cannot return references to values that “die” in the function scope
-
- dereferences a reference and asked you to operate on the value
ownership
- you can pass objects that you own around
- you can pass a references to data
- then function can view the data but not mutate it
- you can have unlimited unmutable references to values (read only)
- you can have only one mutable reference to a value (write)
moving / borrowing
- when passing an owned object to a function (
var: String
), the function takes ownership and value will die at the end of the scope. This is called a “move” as the data is moved to a different scope - when passing a
var: &String
, the value doesn’t move or change ownership, value can be viewed not mutated and will not die - when passing a
var: &mut String
mutable reference, value doesn’t change ownership, doesn’t die and is mutable.
strings
- defining a regular fixed length string results in an &str
- is a reference with known length to memory ref-stir or string slice
- a String object has data on the heap, owns it’s data and has functionality to mutate the data
- format! Macro is like println but creates a String object
copy / clone
- copy types (ints floats bool) always get copied when passed to a Function
- strings don’t get copied but are clone-able
printing
- multiline strings are allowed
- use raw to print loads of escaped characters
println!(r#"string with bunch " of " escapable chars"#)
- use b to print string as bytes
- use
to print hex
- use
to print pointer adress, as in the memory adress the pointer points to
\u{hex}
to print Unicode character- printing can also specify padding alignment and padding character
arrays
- typing arrays type first then length
[&String; 2]
- prefill array with
let mut buffer = [0; 640]
- slicing array with position, count. Exclusive:
&array[0..2]
inclusive&array[0..=2]
vectors
- kind of like String for &str is Vector for &array
Vec<String>
vector of stringsVec<(i32,i32)>
vector of tuple of 2 i32s- all items within vector have the same type
vector.capacity()
shows capacity in vector before value is copied and doubled, usually 4 when there is 1 value.Vec::with_capacity(8)
when you know the future vector size avoids reallocation, which is bettervector: Vec<_> = [1,2,3].into()
tuples
- tuples can contain multiple different types
- access tuple members with dot:
tuple.0
- destructure vectors with tuple definition
let vector = vec!["one", "two", "three"]
let (a, _, c) = (vector[0],vector[1],vector[2])
match
- match is like a switch but needs to be exhaustive
- code block, fat arrows and “arms” for each case
fn main() {
let my_number: u8 = 5;
match my_number {
0 => println!("it's zero"),
1 => println!("it's one"),
2 => println!("it's two"),
_ => println!("It's some other number"),
}
}
- ”match guards” are if statements within a match
- matches also work with tuples, allowing you to check multiple things and match them to a specific case, and catching all other cases
- match stops when it finds a hit
- match always returns the same type
- use @ to create a variable for the match value to use it in a print statement
number @ 13 => println!("{} is unlucky
structs
- unit struct, has nothing:
struct FileDirectory
- unnamed Tuple struct, only has types:
struct Color(u8,u8,u8)
- named struct, has tuple with values:
struct Light { on: bool, brightness: u8, color: Color}
- structuring allows you to not have to assign the values if they are named the same:
let on = true;
let brightness = 50;
let color = (50,50,50);
let light = Light {
on, brightness, color,
}
enum
- use struct if you have many different things, use enum if you have one thing with many options
enum States { Play, Pause, Stop }
- enums combine perfectly with match to name the states:
match state {
ThingsInTheSky::Sun => println!("I can see the sun!"),
ThingsInTheSky::Stars => println!("I can see the stars!")
}
- enums can contain data when created:
enum ThingsInTheSky {
Sun(String), // Now each variant has a string
Stars(String),
}
- import enums to allow us to use the members directly:
fn match_mood(mood: &Mood) -> i32 {
use Mood::*; // We imported everything in Mood. Now we can just write Happy, Sleepy, etc.
let happiness_level = match mood {
Happy => 10, // We don't have to write Mood:: anymore
Sleepy => 6,
NotBad => 7,
Angry => 2,
};
happiness_level
}
- enums members have indexes, get them by casting the value to an i32 if you change them the numbering goes on
- if you want to create a vector with different types, you can use enum with multiple values
loops
loop {}
loops indefinitely until break; is called- you can name loops with
'loop_outer: loop { break 'loop_outer; }
for number in [0..2] { }
to loop, use _ if you don’t need the variable- break can return a value that can be assigned from the loop
implementing structs and enums
- methods: take “self” and mutate on object
- associated functions: don’t take self, are related
enum Mood {
Good,
Bad,
Sleepy,
}
impl Mood {
fn check(&self) {
match self {
Mood::Good => println!("Feeling good!"),
Mood::Bad => println!("Eh, not feeling so good"),
Mood::Sleepy => println!("Need sleep NOW"),
}
}
}
fn main() {
let my_mood = Mood::Sleepy;
my_mood.check();
}
attributes
- added to structs and enums
#[derive(Debug)]
dereferencing
- when using the dot operator . you don’t have to worry about dereferencing a reference:
let reference_item = &item
let double_reference_item = &reference_item
double_reference_item.compary_number(8)
works
generics
- Used if there are multiple types able to be passed to a function
fn print_value<T>(value: T) { println!("{}", value)}
- Generic is usually called T
- specify which trait, for instance debug, is implemented on a generic:
fn print_value <T: Debug>(value: T)
- if you want to compare generics, you need
PartialOrd
option
- use option as return type when you deal with values that might exist, or might not exist
- use None and Some in code where you need to return an option
fn take_fifth(value: Vec<i32>) -> Option<i32> {
if (..) {
None
} else {
Some(value[4])
}
}
- the None or Some values can be “unwrapped”. You only want to unwrap if you are sure there is a value inside
- we can use a match on an option to safely deal with the None value
- we can use is_some() on an option to see if it contains a Some value
result and error handling
- result is similar to option, but about ok or error instead of no value or value
- implementing error handling:
fn give_result(input: i32) -> Result<(), ()> {
if input % 2 == 0) {
return Ok(())
} else {
return Err("input couldnt be processed")
}
}
fn main() {
if give_result(5).is_ok() {
println!("it was ok");
} else
println!("it was an error");
}
}
errors
- you can define your own errors, similar to this standard library function
pub fn from_utf8(vec: Vec<u8>) -> Result<String, FromUtf8Error>
- if you only want to do something when the value is not None:
let my_vec = vec![2, 3, 4];
for index in 0..10 {
if let Some(number) = my_vec.get(index) {
println!("the number is {}", number)
}
}
- you can do something similar with while let, e.g. popping a vector until it returns none
while let Some(information) = city.pop()
hashmap
- hashmaps are not ordered
- create with
HashMap::new()
- populate with
hashmap.insert(key, value)
hashmap.get()
returns an Option- inserting with existing key will overwrite value
- the
.entry()
method returns a mutable reference to the entry if the key exists, allowing you to change its value - if we chain
.entry(key).or_insert(0)
the key with value 0 will be inserted into the hashmap if it didn’t exist yet - we can then use that reference to for instance increase a counter
*reference += 1;
- you can also use or_insert to create vectors with items as values
btreemap
- btreemaps exactly like hashmaps but are sorted by key
- iterate with
for (key, value)