39 minute read


These are the notes i'm taking on my journey to learn Rust.  
I use `TCM Security Rust 101 course`, and ZeroPointSecurity `Rust for n00bs` course.  
Enjoy the notes and drop me any feedback ! ;)

Table of contents:


Class notes

integers

Unsigned int and signed int from 8 to 128. No unsigned floating point types in Rust. f32 and f64 no more.

fn main() {
    println!("Hello, world!");      // ! after println denotes that we're calling a macro, rather than another function.  
                                    // Macros in Rust are a form of metaprogramming
    
    println!("max size of a u32: {}", u32::MAX);
    println!("max size of a u128: {}", u128::MAX);
    println!("min size of a i128: {}", i128::MIN);
    println!("max size of a f32: {}", f32::MAX);
}
/*
Hello, world!
max size of a u32: 4294967295
max size of a u128: 340282366920938463463374607431768211455
min size of a i128: -170141183460469231731687303715884105728
max size of a f32: 340282350000000000000000000000000000000 */

type of variables

const NUMBER: i32 = 18;scopescope

fn main() {
    let mut hello: &str = "hello world!";
    println!("{}", hello);

    hello = "hello again";
    println!("{}", hello);

    let x = 1;
    let y: i32 = 2;
    println!("x + y = {}", x+y);

    println!("number: {}", NUMBER);
}
/*
hello world!
hello again
x + y = 3
number: 18 */

scope and shadowing

Created with {}

fn main() {
    // outter scope
    let x = 1;
    let y: i32 = 3;
    {
        // inner scope
        let y: i32 = 2;
        println!("inner scope x + y = {}", x+y);
    }
    // outer scope
    println!("outer scope x + y = {}", x+y);
}
/*
x + y = 3
x + y = 4 */

suffixes and underscores

fn main() {
    let x: f32 = 42_000f32;
    let y: i32 = 1_000_000;
    println!("x = {}", x);
    println!("y = {}", y);
}
/*
x = 42000
y = 1000000 */

tuples

() - compound type, collection - multiple values of different data type - fixed size

fn main() {
    let student_a = ("toto", "A", 3.8);

    /*
    let student_name = student_a.0;
    let student_grade = student_a.1;
    let student_gpa = student_a.2;
    */

    let (student_name, student_grade, student_gpa) = student_a; // replaces the above
    println!("my name is {} my grade is {} my gpa is {}", student_name, student_grade, student_gpa);

    let tuple: (&str, &str, i32) = ("Charles", "Dickens", 1812);
    let (first_name: &str, last_name: &str, dob: i32) = tuple;
    println!("{} {} was born in {}.", first_name, last_name, dob);
}
/* 
my name is toto my grade is A my gpa is 3.8 
Charles Dickens was born in 1812 */

arrays

[] - compound type, collection - multiple values of a single data type - fixed size

fn main() {
    let students = ["heath", "bob", "linda"];
    println!("first student is {}", students[0]);

    let array: [i32;5] = [1,2,3,4,5];
    println!("{:?}", array);

    let arr: [i32; 1000] = [0; 1000];
    println!("{:?}", arr);
}
/* 
first student is heath 
[1, 2, 3, 4, 5]
3
[0, 0, 0, 0, 0, 0, ......... 0] */

slices

References to a contiguous sequence of elements within a collection (array, string) - can use a portion of data without owning it - debug format {:?}

fn main() {
    let mut arr = [1,2,3,4,5];
    let slice = &mut arr[1..3];
    println!("{:?}", slice);
    
    /*
    let slice = &arr[4..5];
    println!("{:?}", slice);
    */

    slice[0] = 6;
    slice[1] = 7;
    println!("{:?}", arr);
}
/*
[2, 3]
[1, 6, 7, 4, 5] */

strings

Strings: String, &str
String: heap-allocated - owned - mutable. Converted to string slice with & or as_str(). Can use push_str() on mut.
&str: string slice - borrowed - immutable - often used to pass string data between functions or to extract substrings from a larger string. Converted to a String using .to_string() or String::from()
Concatenate with concat() or format!() or append a slice to a str with +.

fn main() {
    let mut hello: &str = "hello world!";
    let mut name = "toto";

    name.push_str("     test");

    println!("{}", name);
}
// ERROR push_str doesn't exist
//-----------------------------------------------------------------------------

fn main() {
    let mut name = String::new();            // creates an empty, mutable String
    name.push_str("     test");              // push_str() appends a string slice (&str) to the end of the String.
    println!("{}", name);
}
/*         test */                           // WORKS bcz push_str exists
//-----------------------------------------------------------------------------

fn main() {
    let mut name: String = String::new();
    name.push_str("im");
    name.push_str(" toto");
    name.push_str("     test");
    println!("{}", name);
}
/* im toto     test */
//-----------------------------------------------------------------------------

fn main() {
    let name = "toto".to_string();            // converts the &str into a heap-allocated String => owned and mutable
    println!("{}", name);
    let name: String = String::from("toto");  // create a String from a &str, allocates a new String with the same value as the input string slice.
                                              // creates a new variable that shadows the previous name
    println!("{}", name);
}

/*
toto
toto */

escaping

/*
\: backslash
": double quote
': single quote
n: newline
r: carriage return
t: tab
0: null character
xNN: hexadecimal escape sequence, where NN is a two-digit hexadecimal number that represents a Unicode scalar value
u{NNNN}: Unicode escape sequence, where NNNN is a four-digit hexadecimal number that represents a Unicode scalar value
*/

fn main() {
    let message = "1hello, \"world\"!\n";
    println!("{}", message);
    println!("2hello,
    world");

    println!("3hello, \
    world");

    println!("{}", concat!("4hello, ", "world"));

    let heart = '\u{2764}';
    println!("{}", heart);

    let message = r#"5hello, "world"!\n"#;
    println!("{}", message);
}
/*
1hello, "world"!
2hello,
    world
3hello, world
4hello, world
❤
5hello, "world"!\n */

math operators

fn main() {
    let x = 10;
    let y = 3;
    let x_float = x as f64;
    let y_float = y as f64;
    println!("{} + {} = {}", x, y, x + y);
    println!("{} - {} = {}", x, y, x - y);
    println!("{} * {} = {}", x, y, x * y);
    println!("{} / {} = {}", x, y, x / y);
    println!("{} / {} = {}", x_float, y_float, x_float / y_float);
    println!("{} ^{} = {}", x, y, u32::pow(x, y));
}
/*
10 + 3 = 13
10 - 3 = 7
10 * 3 = 30
10 / 3 = 3
10 / 3 = 3.3333333333333335
10 ^3 = 1000
10 ^3 = 1000 */

dependencies

Add “rand = 0.8.5” in Cargo.toml or from CLI cargo add rand

use std::io;
use rand::Rng;

fn main() {
    let x = rand::thread_rng().gen_range(1..101);
    let y = 3;
    let x_float = x as f64;
    let y_float = y as f64;
    println!("{} + {} = {}", x, y, x + y);
    println!("{} - {} = {}", x, y, x - y);
    println!("{} * {} = {}", x, y, x * y);
    println!("{} / {} = {}", x, y, x / y);
    println!("{} / {} = {}", x_float, y_float, x_float / y_float);
    println!("{} ^{} = {}", x, y, u32::pow(x, y));
    println!("{} ^{} = {}", x, y, i32::pow(x.try_into().unwrap(), y)); // convert a i32 into u32
}
/*
37 + 3 = 40
37 - 3 = 34
37 * 3 = 111
37 / 3 = 12
37 / 3 = 12.333333333333334
37 ^3 = 50653
37 ^3 = 50653 */

comparison operators

fn main() {
    let a = 5;
    let b = 10;
    let c = true;
    let d = false;

    println!("a > b: {}", a > b); // false
    println!("a >= b: {}", a >= b); // false
    println!("a < b: {}", a < b); // true
    println!("a <= b: {}", a <= b); // true
    println!("a == b: {}", a == b); // false
    println!("a != b: {}", a != b); // true
    println!("True or False: {}", c || d); //true
    println!("True or True: {}", c || c); //true
    println!("False or False: {}", d || d); //false
    println!("True and False: {}", c && d); //false
    println!("True and True: {}", c && c); //true
    println!("False and False: {}", d && d); //false
}

conditionnal statements

use std::io::{self, Read};

fn main() {
    println!("how much money do u have?");

    let mut input_money = String::new();
    io::stdin().read_line(&mut input_money);
    let money: i32 = input_money.trim().parse().expect("input not integer");

    println!("how old r u?");

    let mut input_age = String::new();
    io::stdin().read_line(&mut input_age);
    let age: i32 = input_age.trim().parse().expect("input not integer");

    if (age >= 21) && (money >= 5) {
        println!("were getting a drink!");
    } else if (age >= 21) && (money < 5) {
        println!("need more money");
    } else if (age < 21) && (money >= 5) {
        println!("nice try");
    } else {
        println!("not old enough, no money, no drink");
    }
}
/*
how much money do u have?
3
how old r u?
2
not old enough, no money, no drink */

match

pattern matching - arm && all possible values must be covered

use std::cmp::Ordering;

fn main() {
    let candidacy_age = 28;

    match candidacy_age {
        25 | 26 | 27 | 29 => println!("cannot run for the house"),
        1..=24 => println!("cannot hold office!"),
        30..=34 => println!("cant run for senate"),
        35..=i32::MAX => println!("can run for president"),
        _ => println!("are you an infant?")
    };

    let my_age = 25;
    let drinking_age = 21;

    match my_age.cmp(&drinking_age) {
        Ordering::Less => println!("cant drink!"),
        Ordering::Equal => println!("can drink"),
        Ordering::Greater => println!("woow can drink!")
    };
}
/*
are you an infant?
woow can drink! */

loops

use std::{i32, io::{self, Read}};
use std::cmp::Ordering;

fn main() {
    // FOR
    let mut veggie = ["cucumber", "spinach", "cabbage"];
    for x in veggie.iter() {
        println!("{}", x);
    }

    // 192.168.1.1 - 192.168.1.254
    /* let mut ip = 1..254;
    for y in ip {
        println!("{}", y);
    } */

    // WHILE
    let mut i = 1;
    while i < 3 {
        println!("{}", i);
        i += 1;
    }

    // INFINITE
    let mut y = 0;
    println!("counting");
    loop {
        y += 1;
        println!("{}", y);
        if y == 5 {
            println!("reached 5!");
            continue;;
        }
        if y == 10 {
            println!("reached 10! exiting");
            break;
        }
    }
}
/*
cucumber
spinach
cabbage
1
2
counting
1
2
3
4
5
reached 5!
6
7
8
9
10
reached 10! exiting */

functions

use std::{i32, io::{self, Read}, os::unix::fs::PermissionsExt};
use std::cmp::Ordering;

fn who_am_i() {
    let name = "toto";
    let age = 32;
    println!("my name is {} and my age is {}", name, age);
}

fn add_stuff(num: i32) {
    println!("{}", num + 100);
}

fn add(x: i32, y: i32) {
    println!("{}", x+y);
}

fn mult(x: i32, y: i32) -> i32 {
    x * y
}

fn add_mult(x: i32, y: i32) -> (i32, i32) {
    (x+y, x*y)   // in main(): first result will be passed to add, second to mult
}

fn main() {
    who_am_i();
    who_am_i();
    add_stuff(100);
    add(7, 7);
    println!("{}", mult(5, 4));
    let (added, multiplied) = add_mult(4, 7);
    println!("added: {}", added);
    println!("multiplied: {}", multiplied);
}
/*
my name is toto and my age is 32
my name is toto and my age is 32
200
14
20
added: 11
multiplied: 28 */

Collections

vectors

sequence collection - single data type - uses generics (Vec<T>: allows for any datatypes) variable length - slower than arrays but more flexible => resizable & dynamic. Has specific get() function.
Useful when checking for program arguments: dont know how much there will be => use vectors

use std::env::args;

fn main() {
    let mut vec1: Vec<i32> = Vec::new();        // vec1, a vector that stores i32 integers
    let mut vec2: Vec<i32> = vec![1, 2, 3];

    vec1.push(1);                               // add an element to `vec1`

    vec2.push(4);
    let second_element = vec2[1];

    for element in vec1.iter() {
        println!("vec1 element: {}", element);
    }

    for element in vec2.iter() {
        println!("vec2 element: {}", element);
    }

    println!("the second vec2 element is: {}", second_element);
    println!("the length of vec2 is: {}", vec2.len());

    let first_arg = args[1];
    let second_arg = args[2];
}
/*
vec1 element: 1
vec2 element: 1
vec2 element: 2
vec2 element: 3
vec2 element: 4
the second vec2 element is: 2
the length of vec2 is: 4 */

maps

stores data in key-value pair - HashMap & BtreeMap - Btree: sorted data - HashMap: unsorted data.

use std::collections::{HashMap, BTreeMap};

fn main() {
    ///////////////// HASHMAP
    let mut hashmap = HashMap::new();
    hashmap.insert(1, "number 1");
    hashmap.insert(2, "number 2");
    hashmap.insert(3, "number 3");
    for kvp in hashmap.iter() {
        println!("h_key: {} h_value: {}", kvp.0, kvp.1);
    }

    ///////////////// BTREEMAP
    let mut btreemap = BTreeMap::new();
    btreemap.insert(1, "number 10");
    btreemap.insert(2, "number 11");
    btreemap.insert(3, "number 12");

    println!();
    for kvp in btreemap.iter() {
        println!("b_key: {} b_value: {}", kvp.0, kvp.1);
    }

    let old_value = btreemap.get(&3);
    match old_value {
        Some(v) => println!("b_key 3 old value: {}", v),
        None => println!("not found"),
    }

    // NO COLLISION CHECKS, OLD VALUE OVERWRITTEN
    btreemap.insert(3, "number 9898798");
    let new_value = btreemap.get(&3);
    match new_value {
        Some(v) => println!("b_key 3 new value: {}", v),
        None => println!("no value changed"),
    }
    
    // add collision check manually:
    println!("[*] checking possible collisions on b_key 1");
    if !btreemap.contains_key(&1) {
        btreemap.insert(1, "number 1 not present in btreemap => insertion done");
    }
    else {
        println!("[!] b_key 1 already present ... no changes applied")
    }
    for kvp in btreemap.iter() {
        println!("b_key: {} b_value: {}", kvp.0, kvp.1);
    }    
}
/* 
h_key: 3 h_value: number 3
h_key: 2 h_value: number 2
h_key: 1 h_value: number 1

b_key: 1 b_value: number 10
b_key: 2 b_value: number 11
b_key: 3 b_value: number 12
b_key 3 old value: number 12
b_key 3 new value: number 9898798
[*] checking possible collisions on b_key 1
[!] b_key 1 already present ... no changes applied
b_key: 1 b_value: number 10
b_key: 2 b_value: number 11
b_key: 3 b_value: number 9898798 */

structures

Structs - data structure containing key:value pairs Can be used in other structs.

fn main() {
    struct Car {
        make: String,
        model: String,
        year: u32,
        price: f64,
    }

    let mut huracan: Car = Car {
        make: String::from("lambo"),
        model: String::from("huracan"),
        year: 2020,
        price: 320_000.00
    };
    println!("the cost of a {} {} {} is ${}", huracan.year, huracan.make, huracan.model, huracan.price);
}

impl & associated functions

impl used to define custom implementation on a type (here on “Person” struct). Linking functions to structs: can be standalone calls like Foo::bar();.
If self keyword is passed in as parameter, can then be called like foo.bar()

struct Person {
    first_name: String,
    last_name: String,
    date_of_birth: u16,
    status: Status
}

enum Status {
    ALIVE,
    DEAD
}

impl Person {
    fn resurrect(&mut self) {           // linking function "resurrect" to Person struct
        self.status = Status::ALIVE;
    }
}

fn main() {
    let mut person = Person {
        first_name: String::from("charlie"),
        last_name: String::from("chaplin"),
        date_of_birth: 1889,
        status: Status::DEAD
    };
    
    person.resurrect();
    println!("brought back {} {}!", person.first_name, person.last_name);

    match person.status {
        Status::ALIVE => println!("{} {} is alive hooray!", person.first_name, person.last_name),
        Status::DEAD => println!("{} {} is dead!", person.first_name, person.last_name)
    }
}

The linked functions can also return data:

fn main() {
    struct Rectangle {
        width: u16,
        length: u16
    }
    
    let rect = Rectangle { width: 30, length: 12 };
    let rect_area = rect.width * rect.length;
    println!("first rectangle of width {} length {} has an area of {}", rect.width, rect.length, rect_area);

    // defines an "area" function/method associated/tied to the "Rectangle" struct
    impl Rectangle {
        fn area(&self) -> u16 {               // = rect.area()
            self.width * self.length
        }
    } 

    // "self" keyword passed to "area" function allows calling of rect.area() method
    println!("first rectangle of width {} length {} has a rect.area of {}", rect.width, rect.length, rect.area());


    impl Rectangle {
        fn new(length: u16, width: u16) -> Self {
            Self {
                length,
                width
            }
        }

        fn calculate_area(&self) -> u16 {
            self.width * self.length          // = return self.length * self.width;
        }
    }

    let second_rect = Rectangle::new(10, 40);
    let second_area = second_rect.calculate_area();
    println!("second rectangle of width {} length {} has an area of {}", second_rect.length, second_rect.width, second_area);
}
/* 
first rectangle of width 30 length 12 has an area of 360
first rectangle of width 30 length 12 has a rect.area of 360
second rectangle of width 10 length 40 has an area of 400 */

enums / or enumeration

A type representing a set of named values

use std::io;

fn main() {

    #[derive(Debug)] // implement `Debug` trait into Direction
    enum Direction {
        Up,
        Down,
        Left,
        Right,
    }

    let up = Direction::Up;
    let down = Direction::Down;

    impl Direction {
        fn opposite(&self) -> Direction {
          match *self {
            Direction::Up => Direction::Down,
            Direction::Down => Direction::Up,
            Direction::Left => Direction::Right,
            Direction::Right => Direction::Left,
           }
        }
    }

    let direction = Direction::Up;
    let opposite_direction = direction.opposite();

    #[derive(Debug)]           // without this, when we println circle its gonna crash
    enum Shape {
        Circle(f32),
        Rectangle(f32, f32),
    }

    let circle = Shape::Circle(10.0);
    let rectangle = Shape::Rectangle(32.0, 12.0);

    println!("{:?}", circle);
}
/* Circle(10.0) */

traits

Like interfaces in other languages.
Enforces a given type to implement a set of methods - allows to write generic code allowing to work with wide range of types - agnostic code - re-usable code acorss different types

fn main() {
    trait Damage {                  // 'Damage' interface that datatypes can implement
        fn damage(self: &mut Self);
    }

    #[derive(Debug)]
    struct HP {
        hp_remaining: i32,          // one type: hp_remaining
    }

    impl Damage for HP {            // implement the TRAIT into the struct with this
        fn damage(self: &mut Self) {
            self.hp_remaining -= 1;
        }
    }

    let mut hp = HP {hp_remaining: 100};

    hp.damage();
    println!("[!] you took a hit ! {:?}", hp);
    hp.damage();
    println!("[!] you took a hit ! {:?}", hp);
    hp.damage();
    println!("[!] you took a hit ! {:?}", hp);
    hp.damage();
    println!("[!] you took a hit ! {:?}", hp);

    ///////////////////////////////////////////////////////////////////////////////
    trait Drawable {
        fn draw(&self);
    }

    #[derive(Debug)]
    struct Circle {
        radius: f32,
    }

    impl Drawable for Circle {
        fn draw(&self) {
            println!("[*] drawing a circle of radius {}", self.radius);
        }
    }

    fn draw_shape<T: Drawable>(shape: &T) {
        shape.draw();
    }

    let circle = Circle {radius: 10.2};
    
    circle.draw();
    println!("{:?}", circle);

    draw_shape(&circle);
    println!("{:?}", circle);
}
/*
[!] you took a hit ! HP { hp_remaining: 99 }
[!] you took a hit ! HP { hp_remaining: 98 }
[!] you took a hit ! HP { hp_remaining: 97 }
[!] you took a hit ! HP { hp_remaining: 96 } 
[*] drawing a circle of radius 10.2
Circle { radius: 10.2 }
[*] drawing a circle of radius 10.2
Circle { radius: 10.2 } */

ownership / moving ownership

In Rust, memory is managed through a system of ownership and borrowing. Allows freeing of memory for the allocated value. Each value in Rust has an owner, which is responsible for managing the memory used by that value. When a value goes out of scope, its memory is automatically freed. This eliminates the need for manual memory management or garbage collection, which can lead to bugs, performance issues, and security vulnerabilities.

Stack vs Heap:
Stack is fast. Values are stored in order and all are fix-sized. Uses LIFO. Heap is slower. Values are unordered and of a variable size. The heap uses a return address for requested space called a pointer.

Can force moving ownership with move (see concurrency part example).

fn main() {
    let name = String::from("toto");        // name owns "toto"
    let new_new_name = name.clone();        // clone returns a copy or name
    
    let new_name = name;                    // "toto" ownership is passed to new_name
    // let new_name = &name;                => no ownership issue ("&" allows to associate the address of "name" to "new_name")
    // let new_name = name.clone()          => no ownership issue (clone implement copy trait)
    
    println!("hello my name is {}", new_name);
    println!("hey mein name ist {}", new_new_name);
}
/* 
hello my name is toto
hey mein name ist toto */

ownership

3 rules:
Each value has an owner (owned by a variable) || There can only be one owner at a time || When the owner goes out of scope, the memory becomes free.

fn main() {
    // message_1 "content" (stored on the stack) is a pointer to the address of "hello world". 
    // The String value ("hello world") is stored on the heap.
    let message_1 = String::from("hello world");
    //  --------- move occurs because `message_1` has type `String`, which does not implement the `Copy` trait
    
    println!("{}", message_1);

    let message_2 = message_1;
    //              --------- value moved here: we moved the address of message_1 into message_2
    println!("{}", message_1);
    //             ^^^^^^^^^ value borrowed here after move: because we moved the address of message_1, we dont know where to find it   
}
/* 
error[E0382]: borrow of moved value: `message_1`
--> src/main.rs:6:20 */

borrowing

Allows another variable to reference the data, without deallocating the data (previous example) using pointers. Pointer to data in memory using &, prints its address using :p

fn main() {
    let mut message_1 = String::from("hello world");
    println!("message_1: {}", message_1);

    let message_2 = &message_1;         // message_2 now contains "hello world" heap memory address and message_1 hasn't been moved
    println!("message_2: {}", message_2);
    println!("message_2 content: {:p}", message_2);
    println!("message_1 lives at: {:p}", message_2);

    let message_3 = &mut message_1;
    *message_3 = String::from("toto");
    println!("message_1 now contains pointer to: {}", message_1);
    println!("message_1 still lives at: {:p}", &message_1);
}
/* 
message_1: hello world
message_2: hello world
message_2 content: 0x16d3525a0
message_1 lives at: 0x16d3525a0
message_1 now contains pointer to: toto
message_1 still lives at: 0x16d3525a0 */
// passing ownership (moving ownership, no memory reallocation or data copy) to functions
fn take_ownership(s: String) {
    // s comes into scope
    println!("{}", s);              // s is valid
    // s is dropped here
}

fn main() {
    let s = String::from("hello");
    take_ownership(s);
    // println!("{}", s);           // Error: s is invalid here
}
// returning ownership
fn return_ownership() -> String {
    let s = String::from("hello");
    return s;                       // ownership is moved to the caller
}

fn main() {
    let s = return_ownership();
    println!("{}", s);              // s is valid

references & borrowing

Using a value without taking ownership - mutable reference can modify the value referenced

fn main() {
    let a = String::from("toto");               // a owns String "toto"
    let b = &a;                                 // a borrow to a (immutable)
    println!("name is {}", b);                  // b can read a

    let s1 = String::from("hello");             // s1 owns String hello
    let len = compute_length(&s1);              // pass a reference to s1 into function
    println!("the length of {} is {}", s1, len);

    fn compute_length(s: &String) -> usize {    // s is a reference to a String value
        s.len()                                 // returns length of string pointed to by s
    } // s goes out of scope, but because it's a reference and doesn't own the string, nothing happens 

    let mut x = 10;
    let y = &mut x;
    println!("y: {}", *y);

    *y += 1;                // works here
    println!("x: {}", x);   // immutable borrow occurs here
    //  *y += 1;
    
    x += 1;
    println!("{}", x);
}
/* 
name is toto
the length of hello is 5
y: 10
x: 11
12 */

input/output

user input

use std::io;

fn main() {
    println!("who goes there");

    let mut name = String::new();
    io::stdin().read_line(&mut name);

    let enter = "you may enter";
    println!("hello there {}, {}", name.trim_end(), enter); // trim_end will remove the new line char induced previously
}
/*
who goes there
Rodolf Gandalf
hello there Rodolf Gandalf, you may enter */
use std::io;
use std::io::Write;

fn main() {
    loop {
        // println not used so user can type on the same line
        print!("> ");
        // stdout forced flush so '>' isn't printed with a newline
        let _ = io::stdout().flush();

        // create a buffer to hold the input
        let mut input = String::new();

        // reads from stdin
        // read_line() returns a `Result` which can be used to catch errors
        let _ = io::stdin().read_line(&mut input);
        println!("you said: {}", input);

        if input.trim_end().eq_ignore_ascii_case("exit") {
            break
        }
    }
}
/* 
> hello
you said: hello

> exit
you said: exit
## program stops */

file input & output

use std::{fs, io};
use std::fs::{File, OpenOptions};   // openoption allows us to append
use std::io::prelude::*;

fn main() {
    let mut file = File::create("src/hello.txt").expect("failed to create file");
    file.write_all("hello world\n".as_bytes());
    file.write_all(b"hello again\n").expect("failed to write to file"); // same as above

    let mut file = OpenOptions::new().append(true).open("src/hello.txt").expect("failed to write to file");
    file.write_all(b"yipikayeee").expect("failed to write to file");

    let mut file = File::open("src/hello.txt").expect("unable to open file");
    let mut file_content = String::new();
    file.read_to_string(&mut file_content).unwrap();             // read content and write into file_content

    println!("{}", file_content);

    fs::remove_file("src/hello.txt").expect("unable to delete the file") // deletes file
}
/*
hello world
hello again
yipikayeee */

error handling - Result, Ok, Err, ?, unwrap() and the panic! macro

Result enum builds on Option by allowing a function to return a reason for the failure. Returning a reason is more useful because it allows the caller to handle the error in more specific ways, or to return a more accurate error message to the user. Enum, Result<T, E> has two variants: Ok and Err.
Ok represents a successful computation with a value - Err represents an error with an associated message.
Instead of Result we can use ? which removes the matching and display Ok() - propagate the error of the Err variant of a Result type up the call stack. HELPER METHODS - unwrap(), expect(): two types of errors: recoverable and unrecoverable: RECOVERABLE - can still goes on - includes result enum & option enum |||| UNRECOVERABLE - crash - includes panic! & panic macro

unwrap() - returns the value of the “Ok()” variant of a “Result” type.
panic! - panic with a custom error message

Panic

Rust encounters runtime error => panic. This code will compile (vectors are flexible: size can change at runtime), but throws a panic when running:

fn main() {
    let vector = vec![0; 5];
    println!("vector[5]: {}", vector[5]);
}
/* 
thread 'main' panicked at src/main.rs:3:37:
index out of bounds: the len is 5 but the index is 5
note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace */

Option

Vectors have get() function returning enum Option<T>: two values None and Some.
T = generic - can return any datatype inside this enum. Use with Match for error handling:

fn main() {
    let vector = vec![1,2,3];
    for i in 0..=3 {
        let option = vector.get(i);
        match option {
            None => println!("no data found at index {}", i),
            Some(d) => println!("data at index {}: {}", i, d)
        }
    }
}
/* 
data at index 0: 1
data at index 1: 2
data at index 2: 3
no data found at index 5 */

Result

Result enum built on Option : allows a function to return a reason for failure which allows the caller to handle the error in wanted/specific way.

use std::fs;

fn main() {
    // Result<T, E> variants: `Ok` and `Err`
    let result = fs::File::open("./test.txt");
    match result {
        Ok(_) => println!("file opened"),
        Err(e) => println!("error opening file: {}", e)
    }
}
/* file opened */

Error propagation

use std::fs::File;
use std::io::{Error, Read};

fn open_file(path: &str) -> Result<File, Error> {           // returns Result<T,E>
    File::open(path)                                        // eliminate need for `match` + returns Result<File> directly
}

fn read_file(path: &str) -> Result<String, Error> {         // returns Result<T,E>
    let mut file = open_file(path)?;                        // if check fail ('?') sends error to main()
    let mut buf = String::new();
    let _ = file.read_to_string(&mut buf);                  // if no error, reads the file

    Ok(buf)
}

fn main() {                                         // only function to need a match
    let result = read_file("./test.txt");
    match result {
        Ok(content) => println!("{}", content),
        Err(error) => println!("error: {}", error)
    }
}
/* error: No such file or directory (os error 2) */

Variant:

use std::fs::File;
use std::io::{Error, Read};

fn open_file(path: &str) -> Result<File, Error> {           // returns Result<T,E>
    File::open(path)                                        // eliminate need for `match` + returns Result<File> directly
}

fn read_file(path: &str) -> Result<String, Error> {         // returns Result<T,E>
    let mut file = open_file(path)?;                        // if check fail ('?') sends error to main()
    let mut buf = String::new();
    let _ = file.read_to_string(&mut buf);                  // if no error, reads the file

    Ok(buf)
}

fn main() {
    let result1 = read_file("./test.txt");
    match result1 {
        Ok(content) => println!("{}", content),
        Err(error) => println!("error: {}", error)
    }

    // if no file present, no panic, keeps going:
    let result2 = read_file("./test.txt");
    match result2 {
        Ok(content) => println!("bidi {}", content),
        Err(error) => println!("bidi error: {}", error)
    }
}
/* error: No such file or directory (os error 2)
bidi error: No such file or directory (os error 2) */
use std::{f32::consts::E, fs::File, io::Read};

fn divide(x: i32, y: i32) -> Result<i32, String> {
    if y == 0 {
        return Err(String::from("cant divide by zero"));
    }
    Ok(x / y)
}

fn divide2(x: i32, y: i32) -> i32 {
    if y == 0 {
        panic!("cannot divide by zero");
    }
    x / y
}

fn read_file(path: &str) -> Result<String, std::io::Error> {
    let mut file = File::open(path)?;
    let mut contents = String::new();
    file.read_to_string(&mut contents)?;
    Ok(contents)
}

fn main() {
    let result = divide(10, 2);

    match result {
        Ok(value) => println!("Result: {}", value),
        Err(msg) => println!("Error: {}", msg),
    }

    let result = divide(10, 0);     // throw an error but program continues
    
    match result {
        Ok(value) => println!("Result: {}", value),
        Err(msg) => println!("Error: {}", msg),
    }

    println!("the show must go on!");

    let result = divide(10, 2).unwrap();            // case match so all good
    println!("Result:{}", result);

    /* let result = divide(10, 0).unwrap();                   // case doesnt match, panic
    println!("Result:{}", result);
    let result = divide(10, 0).expect("divide by zero error");
    println!("Result:{}", result);                            // case doesnt match, panic */
    
    let result = divide2(10, 2);
    println!("Result: {}", result);
    
    let result = read_file("src/test.txt");                  // clean error no panic
    match result {
        Ok(contents) => println!("File contents: {}", contents),
        Err(err) => println!("Error reading file: {}", err),
    }
}
/*
Result: 5
Error: cant divide by zero
the show must go on!
Result:5
Result: 5
File contents: hello world */

Debugging

dbg! maccro: quick/dirty debugging. Printing content with dgb!:

use std::fs;

fn main() {
    let vector = vec![1,2,3];
    dbg!(vector);
}
/* [src/main.rs:5:5] vector = [
    1,
    2,
    3,
] */

Managing error:

use std::fs;

fn main() {
    let file_handle = fs::File::open("./test.txt");
    match file_handle {
        Ok(_) => println!("file opened"),
        Err(e) => { dbg!(e); }
    }
}
/* [src/main.rs:7:21] e = Os {
    code: 2,
    kind: NotFound,
    message: "No such file or directory",
} */

Generics & bounds

Generics: a type representing values of any types - add flexibility - remove the need to declare type early on.
This function only accept and returns u8:

fn add_integers(int_1: u8, int_2: u8) -> u8 {
    return int_1 + int_2;
}

Can take and return any type using generic + a bound: specify requirement + T Ouput datatype:

use std::ops::Add;

// <T: Add<Output = T>> is a bound: ensure datatypes that are passed can be added
// specifying that T needs to support the Add trait
fn add_integers <T: Add<Output = T>> (int_1: T, int_2: T) -> T {
    // only those two need to be of same type:
    return int_1 + int_2;  
}

fn main() {
    let result = add_integers(8, 3);
    println!("{}", result);
    let result = add_integers(823497209, 92070);
    println!("{}", result);
}
/* 
11
823589279 */

Can be done using Where:

fn add_integers(int_1: T, int_2: T) -> T 
where T: Add<Output = T> {
    // only those two need to be of same type:
    return int_1 + int_2;  
}

fn main() {
    let result = add_integers(8, 3);
    println!("{}", result);
    let result = add_integers(823497209, 92070);
    println!("{}", result);
}
/* 
11
823589279 */
fn main() {
    struct Items<T> {
            x: T,
            y: T,
    }
    
    let i = Items{
        x:1.0, y: 2.0
    };
    println!("x: {}, y: {}", i.x, i.y);
}
/* x: 1, y: 2 */

Closures

Function that doesn’t have a name and is declared inline within an existing function. Declared using two pipes, || followed by {}. Can capture the enclosing environment - convenient for on the fly usage - allow to use variable outside of closure scope. Both input and return types can be inferred and input variable names must be specified. Regular functions can’t refer to variables in the enclosing environment

fn main() {
    // closures are anonymous
    || {
        // closure declared inside main()
    };

    // assigning closure to a variable to execute it
    let closure = |message| {
        println!("{}", message);
    };
    closure("hello world!");

    // for data to be returned by a closure we need to indicate the return type
    let closure_two = |name: &str| -> String {
        format!("hello {}", name)
    };
    let message = closure_two("Rasta");
    println!("{}", message);
}
/* 
hello world!
hello Rasta */
fn main() {
    let outer_var = 42;
    
    // uncomment this and compiler will throw an error + suggest that we define a closure instead.
    /* fn function(inside_var: i32) -> i32 {
        inside_var + outer_var 
    }*/   

    // Annotation is identical to function annotation but is optional
    // as are the `{}` wrapping the body. These nameless functions
    // are assigned to appropriately named variables.
    let closure_annotated = |inside_var: i32| -> i32 {
        inside_var + outer_var
    };
    
    // The return type is inferred.
    let closure_inferred  = |inside_var| inside_var + outer_var  ;

    // Call the closures.
    println!("closure_annotated: {}", closure_annotated(1));
    println!("closure_inferred: {}", closure_inferred(1));

    // Once closure's type has been inferred, it cannot be inferred again with another type.
    //println!("cannot reuse closure_inferred with another type: {}", closure_inferred(42i64));     // this would break
    println!("cannot reuse closure_inferred with another type: {}", closure_inferred(42i32));       // this not

    // A closure taking no arguments which returns an `i32`. Return type is inferred
    let one = || 1;
    println!("closure returning one: {}", one());
}

Concurrency

Easiest way: creates new thread with thread::spawn()(returns a JoinHandle) and a closure ||. Brings blocage: code executed in thread wont keep going until thread is done.

Threads

Thread that doesnt return a value:

use std::thread;

fn main() {
    let handle = thread::spawn( || {        // JoinHandle<()>
        println!("hello from thread!");
    });

    let result = handle.join();             // Result<()>
    match result {
        Ok(_) => println!("thread finished successfully"),
        Err(_) => println!("thread didnt finish successfully")
    }
}
/* 
hello from thread!
thread finished successfully */

Thread that returns a value + ownership moved from value to handle closure so it can use it in its scope:

use std::thread;

fn main() {
    let value = 20;
    let handle = thread::spawn(move || -> i32 {
        value * 2
    });

    let result = handle.join();
    match result {
        Ok(res) => println!("result: {}", res),
        Err(_) => println!("thread didnt finish successfully")
    }
}
/* result: 40 */

Channels

Can pass messages in and out of threads with channels - can be used as ‘signals’ to indicate something is treated/done. mspc = multi-producer, single-consumer - multiple parties can send data into channel but only one should receive.
Creating one sender and one receiver:

use std::{sync::mpsc, thread};

fn main() {
    let (snder, rcver) = mpsc::channel();

    let _thread = thread::spawn( move || {
        let _first_sender = snder.send("hello from thread!");
    });

    match rcver.recv() {
        Ok(message_received) => println!("{}", message_received),
        Err(_) => {}
    }
}
/* hello from thread! */

If we add another sender, but dont add another receiver, second message is sent but not received/printed:

use std::{sync::mpsc, thread};

fn main() {
    let (snder, rcver) = mpsc::channel();

    let _thread = thread::spawn( move || {
        let _first_sender = snder.send("hello from thread!");
        let _second_sender = snder.send("second sender here alloooo");
    });

    match rcver.recv() {
        Ok(message_received) => println!("{}", message_received),
        Err(_) => {}
    }
}
/* hello from thread! */

Fix:

use std::{sync::mpsc, thread};

fn main() {
    let (snder, rcver) = mpsc::channel();

    let _thread = thread::spawn( move || {
        let _first_sender = snder.send("hello from thread!");
        let _second_sender = snder.send("second sender here alloooo");
    });

    match rcver.recv() {
        Ok(message_received) => println!("{}", message_received),
        Err(_) => {}
    }

    match rcver.recv() {
        Ok(message_received) => println!("{}", message_received),
        Err(_) => {}
    }
}
/* hello from thread!
second sender here alloooo */

Challenges

challenge 1

const BIRTHDAY: u32 = 1;

fn main() {
    /* Challenge 1 - Build a program that has the following:
    1) Has a global constant integer named 'birthday' with a value of 1
    2) Has a local string variable named 'my_name' with your name as the value
    3) Has a local string variable named 'my_birthday' with your birth month/day (no year) as the value
    4) Has a local mutable integer variable named 'age' with your current age as the value
    5) Has a local integer variable named 'new_age' with your age after your birthday as the value
    6) Prints out 'My name is X and I am X years old. I will turn X on X'
    */

    let my_name: &str = "john";
    let my_birthday: &str = "april";
    let mut age = 38;
    let new_age = age + BIRTHDAY;
    println!("my name is {} and im {} years old. i will turn {} on {}", my_name, age, new_age, my_birthday);
}

challenge 2

use std::io::{self, Read};

fn main() {
    /* Build a simple calculator that takes two user inputs
       then calculates the addition, subtraction, multiplication, and division
       of those two inputs.
    */

    // DEFINE X
    println!("give me some numbers so i can math on them!");
    let mut x = String::new();
    io::stdin().read_line(&mut x);
    let x: i32 = x.trim().parse().expect("input not an integer");
    let x_float = x as f32;

    // DEFINE Y
    let mut y = String::new();
    io::stdin()
        .read_line(&mut y)
        .expect("failed to read number");
    let y: i32 = y.trim().parse().expect("input not an integer");
    let y_float = y as f32;

    println!("{} + {} = {}", x, y, x + y);
    println!("{} - {} = {}", x, y, x - y);
    println!("{} * {} = {}", x, y, x * y);
    println!("{} / {} = {}", x, y, x / y);
    println!("{} / {} = {}", x_float, y_float, x_float / y_float);
    //println!("{} ^{} = {}", x, y, i32::pow(x, y));
    println!("{} ^{} = {}", x, y, i32::pow(x.try_into().unwrap(), y.try_into().unwrap())); // convert a i32 into u32
}
/* 
give me some numbers so i can math on them!
3
4
3 + 4 = 7
3 - 4 = -1
3 * 4 = 12
3 / 4 = 0
3 / 4 = 0.75
3 ^4 = 81 */

challenge 3

use std::{i32, io::{self, Read}, os::unix::fs::PermissionsExt};
use std::cmp::Ordering;

fn add(x: i32, y: i32) -> i32 {
    x + y
}

fn sub(x: i32, y: i32) -> i32 {
    x - y
}

fn mul(x: i32, y: i32) -> i32 {
    x * y
}

fn div(x: f32, y: f32) -> f32 {
    x / y
}

fn main() {
    // Create a calculator that takes three user inputs (x, y, and operator)
    // Create functions for +, -, *, /
    // Use if/else or Match for operator
    // Might take a little research!
    
    println!("Welcome to ZE calculator lets calculate some calcultations shall we");
    
    println!("give me a first number: ");
    let mut input_x = String::new();
    io::stdin().read_line(&mut input_x);
    let input_x: i32 = input_x.trim().parse().expect("not an integer, try again");
    let x_float = input_x as f32;

    println!("give me an operator: +, -, *, or /");
    let mut operator = String::new();
    io::stdin().read_line(&mut operator);
    let operator: &str = operator.trim();

    println!("give me a second number: ");
    let mut input_y = String::new();
    io::stdin().read_line(&mut input_y);
    let input_y: i32 = input_y.trim().parse().expect("not an integer, try again");
    let y_float = input_y as f32;
    
    match operator {
        "+" => println!("result: {}", add(input_x, input_y)),
        "-" => println!("result: {}", sub(input_x, input_y)),
        "*" => println!("result: {}", mul(input_x, input_y)),
        "/" => println!("result: {}", div(x_float, y_float)),
        _ => println!("not an operator!")
    };
}
/*
Welcome to ZE calculator lets calculate some calcultations shall we
give me a first number:
3
give me an operator: +, -, * or /
/
give me a second number:
6
result: 0.5 */
/*
Welcome to ZE calculator lets calculate some calcultations shall we
give me a first number:
3
give me an operator: +, -, * or /
a
give me a second number:
6
not an operator!*/

Projects

Password generator

Generates a 40 char password

use rand::Rng;

const CHARSET: &[u8] = b"ABCDEFGHIJKLMNOPQRSTUVWXYZ\
                        abcdefghijklmnopqrstuvwxyz\
                        0123456789)(*&^%$#@!~+-=_[]{}|";
const PASSWORD_LEN: usize = 40;

fn main() {
    let mut rng = rand::thread_rng();

    let password: String = (0..PASSWORD_LEN)
        .map(|_| {
            let idx = rng.gen_range(0..CHARSET.len());
            CHARSET[idx] as char
        })
        .collect();

    println!("{:?}", password);
}

SHA 256 Password Cracker

use std::{
    env,
    fs::File,
    io::{self, BufRead},
    path::Path,
    process::exit,
};
use sha2::{Digest, Sha256};


fn read_lines<P>(filename: P) -> io::Result<io::Lines<io::BufReader<File>>>
where P: AsRef<Path>,
{
    let file = File::open(filename)?;
    Ok(io::BufReader::new(file).lines())
}


fn main() {
    let args: Vec<String> = env::args().collect();

    if args.len() != 3 {
        println!("Wrong number of arguments given !");
        println!("Usage: cargo run <hashed password> <password file>");
        exit(1);
    }

    let wanted_hash = &args[1];
    let password_file = &args[2];

    if let Ok(password_list) = read_lines(password_file) {
        for (attempt, password) in password_list.flatten().enumerate() {
            let password = password.trim();
            let password_hash = format!("{:x}", Sha256::digest(&password));

            if password_hash == *wanted_hash {
                println!("hash cracked at attempt {} || hash: {} || password: {:?}", attempt + 1, wanted_hash, password);
            };
        }
    } else {
        println!("Failed to open password file");
        exit(1);
    }
}

Improved version:

use std::{
    env,
    fs::File,
    io::{self, BufRead},
    path::Path,
    process::exit,
};
use sha2::{Digest, Sha256};


fn read_lines<P>(filename: P) -> io::Result<io::Lines<io::BufReader<File>>> 
where P: AsRef<Path>, 
{
    let file = File::open(filename)?;anything that the Rust compiler spits out
    Ok(io::BufReader::new(file).lines())
}


fn main() -> Result<(), Box<dyn std::error::Error>> {
    let args: Vec<String> = env::args().collect();

    if args.len() != 3 {
        println!("Wrong number of arguments given !");
        println!("Usage: cargo run <hashed password> <password file>");
        exit(1);
    }

    let wanted_hash = &args[1];
    let password_file = &args[2];

    let password_list = read_lines(password_file)?;
    let result = password_list
        .map_while(Result::ok)
        .enumerate()
        .find_map(|(i, password)| {
            let password_hash = format!("{:x}", Sha256::digest(password.trim()));
            if password_hash == *wanted_hash {
                Some((i, password_hash))
            } else {
                None
            }
        });
    if let Some((attempt, password)) = result {
        println!(
            "hash cracked at attempt: {attempt} || hash: {wanted_hash} || password: {password:?}",
        );
    }
    Ok(())
}

Some of the ressources used:

URL shortener

use std::{
    env,
    fs::{File, OpenOptions},
    io::{self, stdin, BufRead, BufReader, Write},
    process::exit
};

use rand::{
    self,
    distributions::Alphanumeric,
    thread_rng,
    Rng
};

const URL_LENGTH: usize = 8;

fn show_usage() {
    println!("Usage: cargo run <www.example.com> <urls_file.txt>");
}


fn main() {
    let args: Vec<String> = env::args().collect();
    if args.len() != 3 {
        println!("Wrong numbers of arguments given.");
        show_usage();
        exit(1);
    }
    
    let url_passed = &args[1];
    let urls_path = &args[2];
    
    // open file, if doesnt exist, offer user the possibility to create it
    let mut urls_file = match OpenOptions::new().append(true).open(urls_path) {
        Ok(file) => file,
        Err(_) => {
            println!("Error opening the file: file doesn't exist");
            print!("Do you want to create the file? (yY) or (nN) > ");
            
            let _ = io::stdout().flush();
            let mut input = String::new();
            let _ = stdin().read_line(&mut input);
            
            if input.trim_end().eq_ignore_ascii_case("y") {
                let urls_file = File::create(urls_path).expect("Failed to create file");
                println!("File created at {}", urls_path);
                urls_file
            }
            else if input.trim_end().eq_ignore_ascii_case("n") {
                println!("Ok, exiting");
                exit(1);
            }
            else {
                println!("Wrong choice, exiting");
                exit(1);
            }
        } 
    };

    // shorten URL if long
    if url_passed.starts_with("www") {
        let short_url: String = thread_rng()
            .sample_iter(&Alphanumeric)
            .take(URL_LENGTH)
            .map(char::from)
            .collect();
        println!("'{}' shortened into '{}'", short_url, url_passed);
    
        // map urls and write them into mapping file
        let mapped_urls = format!("{},{}", short_url, url_passed);
        write!(urls_file, "{}\n", mapped_urls).expect("Failed to write to file");
    } 
    else if url_passed.len() != URL_LENGTH {
        println!("Please enter valid Url");
        show_usage();
        exit(1);
    }
    // if short url is given
    else {
        let mut found_match = false;

        let urls_file = match File::open(urls_path) {
            Ok(file) => file,
            Err(_) => {
                println!("Error reading the urls file");
                return;
            }
        };
        
        // if file could be opened, create a reader to parse lines
        let reader = BufReader::new(urls_file);
        for line in reader.lines() {
            let mapping = match line {
                Ok(line) => line,
                Err(_) => {
                    println!("Error reading mapping file");
                    continue; 
                }
            };

            // create iterable vector to store lines content 
            let parts: Vec<&str> = mapping.split(',').collect();
            
            // if not two urls, dont care
            if parts.len() != 2{
                continue;
            }
            // if two urls, look for match
            else {
                let short_url = parts[0];
                let long_url = parts[1];
                if url_passed == short_url {
                    println!("[*] Match: {} gives: {}", url_passed, long_url);
                    found_match = true;
                }
                else {
                    continue;
                }
            }
        }

        if !found_match {
            println!("[!] No match found for: {}", url_passed);
        }
    }
}
/* 
Concepts used:
When taking user input:
- The standard output (stdout) stream is line-buffered, which means text isn't usually flushed to the console until a newline appears.  
Since we want the > character to appear without printing a new line, we have to manually flush stdout so that it appears.
- We used "trim_end() on user input to remove the new line char added (its a string, it ends with a newline)"
*/

Quizz game

use std::{
    env,
    io::{self, stdin, Write},
    process::exit
};

fn show_usage() {
    println!("Usage: cargo run");
}

fn show_welcome_message() {
    println!("Hey whats your name?");
    print!("> ");
    let _ = io::stdout().flush();
}

fn check_correctness(correct_choice: &str) -> bool {
    let _ = io::stdout().flush();
    let mut user_answer = String::new();
    let _ = stdin().read_line(&mut user_answer);
    if user_answer.trim().eq_ignore_ascii_case(correct_choice) {
        println!("Correct!");
        true
    }
    else {
        println!("Wrong!");
        false
    }
}

fn show_question_1() {
    println!("1. What is the capital city of France?");
    println!("A. London");
    println!("B. Paris");
    println!("C: Rome");
    print!("Your answer: ");
    let _ = io::stdout().flush();
}

fn show_question_2() {
    println!("\n2. What is the largest country in the world by area?");
    println!("A. Russia");
    println!("B. Canada");
    println!("C: China");
    print!("Your answer: ");
    let _ = io::stdout().flush();
}

fn show_question_3() {
    println!("\n3. Who is credited with inventing the World Wide Web?");
    println!("A. Bill Gates");
    println!("B. Tim Berners-Lee");
    println!("C. Steve Jobs");
    print!("Your answer: ");
    let _ = io::stdout().flush();
}


fn main() {
    let args: Vec<String> = env::args().collect();
    if args.len() != 1 {
        println!("Wrong numbers of arguments given");
        show_usage();
        exit(1);
    }
    
    let total_answers = 3;
    let mut correct_answers = 0;

    show_welcome_message();
    let mut user_name = String::new();
    let _ = stdin().read_line(&mut user_name);
    if user_name.trim().len() == 0 {
        println!("[!] No data entered, please enter a user name");
        exit(1);
    }
    else {
        println!("Welcome to the game {}", user_name.trim());
    }

    show_question_1();
    if check_correctness("B") {
        correct_answers += 1;
    }

    show_question_2();
    if check_correctness("A") {
        correct_answers += 1;
    }

    show_question_3();
    if check_correctness("B") {
        correct_answers += 1;
    }

    let percentage = (correct_answers as f32 / total_answers as f32) * 100.0;
    println!("Your score: {:.2}%", percentage);
}

Some concepts:

  • Crate: anything that the Rust compiler spits out, more often used to describe librairies.
  • Rust has two data type subsets: scalar (holds a single value - integers, chars, floats and booleans), and compound (can hold multiple values aka collections - arrays and tuples).
  • Data types => “primitive”, built into Rust’s standard library and stored on the stack.
  • Own custom data structures => stored on the heap.
  • stack allocated data => inexpensive to copy: integers (i32, u64), booleans (bool), floating-point numbers (f32, f64), and character types (char) implement the Copy trait.
  • heap allocated data => ownership matters: data like String or Vec<T> don’t implement Copy (size can change at runtime). String implement Clone (reallocate memory for the clone).
  • structs, enums, impl blocks, etc. can be declared in (almost) any order within the same scope because compiler processes the entire module as a unit.
  • Rust forces user to declare how data is shared between threads
  • Concurrency pit falls:
    • when multiple threads try to modify the same object at the same time;
    • when one thread locks access to an object, thus preventing another thread from accessing it.
    • when multiple threads are waiting a resource that they each have (deadlocks).
  • Resource Acquisition Is Initialization (RAII): when an object is created, it acquires resources; when it goes out of scope, it releases them automatically: when owner goes out of scope, drop is called to free memory (no dangling, no double-free).
  • getter => collect the data, setter => define the data.
  • pub allows target to be called from outside the module.

Questions:

If we pass two u8 values that when added together result in more than the maximum value:
if the binary is compiled in Debug mode: cause a runtime panic.
if compiled in Release mode: will perform a rollover: it will rollover from it’s highest value, 255 back to 0.


Acknowledgments

Thanks Nicolas Haquet for having clarified some concepts i had issues to fully grasp and for the improved project 1 script. —

Tags:

Categories:

Updated: