Add print-ready PDF (#127)
authorLuca Palmieri <20745048+LukeMathWalker@users.noreply.github.com>
Tue, 6 Aug 2024 15:09:04 +0000 (17:09 +0200)
committerGitHub <noreply@github.com>
Tue, 6 Aug 2024 15:09:04 +0000 (17:09 +0200)
.github/workflows/ci.yml
Cargo.lock
Cargo.toml
book/book.toml
book/link2alias.json [new file with mode: 0644]
helpers/json2redirects.sh [new file with mode: 0755]
helpers/mdbook-exercise-linker/src/lib.rs
helpers/mdbook-link-shortener/Cargo.toml [new file with mode: 0644]
helpers/mdbook-link-shortener/src/lib.rs [new file with mode: 0644]
helpers/mdbook-link-shortener/src/main.rs [new file with mode: 0644]
site/redirects [new file with mode: 0644]

index 1d793ad..c0c2aac 100644 (file)
@@ -16,8 +16,10 @@ jobs:
         with:
           fetch-depth: 0
       - uses: actions-rust-lang/setup-rust-toolchain@v1
-      - name: Install plugin
+      - name: Install exercise plugin
         run: cargo install --path helpers/mdbook-exercise-linker
+      - name: Install link shortener plugin
+        run: cargo install --path helpers/mdbook-link-shortener
       - name: Install mdbook-pandoc and related dependencies
         run: |
           cargo install mdbook-pandoc --locked --version 0.7.1
@@ -63,6 +65,8 @@ jobs:
         with:
           tool: mdbook
       - name: Build book
+        env:
+          LINK_SHORTENER_VERIFY: "true"
         run: |
           cd book
           mdbook build
@@ -82,11 +86,25 @@ jobs:
           # When you support multiple formats, the output directory changes
           # to include the format in its path.
           path: book/book/html
-      # Upload the PDF book as an artifact
       - uses: actions/upload-artifact@v4
         with:
-          name: paperback
+          name: online-pdf
           path: book/book/pandoc/pdf/100-exercises-to-learn-rust.pdf
+      - uses: actions/upload-artifact@v4
+        with:
+          name: paperback
+          path: book/book/pandoc/paperback/100-exercises-to-learn-rust.pdf
+
+  is_fresh:
+    runs-on: ubuntu-latest
+    steps:
+      - uses: actions/checkout@v3
+      - run: sudo apt-get update && sudo apt-get install -y jq
+      - run: |
+          ./helpers/json2redirects.sh book/link2alias.json > site/redirects
+      # Verify nothing has changed, meaning that the redirect file is up-to-date
+      - run: |
+          git diff --exit-code site/redirects
 
   formatter:
     runs-on: ubuntu-latest
index 2bf677f..d560369 100644 (file)
@@ -163,6 +163,15 @@ version = "0.21.7"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "9d297deb1925b89f2ccc13d7635fa0714f12c87adce1c75356b39ca9b7178567"
 
+[[package]]
+name = "bimap"
+version = "0.6.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "230c5f1ca6a325a32553f8640d31ac9b49f2411e901e427570154868b46da4f7"
+dependencies = [
+ "serde",
+]
+
 [[package]]
 name = "bitflags"
 version = "1.3.2"
@@ -280,6 +289,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "0fbb260a053428790f3de475e304ff84cdbc4face759ea7a3e64c1edd938a7fc"
 dependencies = [
  "clap_builder",
+ "clap_derive",
 ]
 
 [[package]]
@@ -304,6 +314,18 @@ dependencies = [
  "clap",
 ]
 
+[[package]]
+name = "clap_derive"
+version = "4.5.13"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "501d359d5f3dcaf6ecdeee48833ae73ec6e42723a1e52419c79abf9507eec0a0"
+dependencies = [
+ "heck",
+ "proc-macro2",
+ "quote",
+ "syn",
+]
+
 [[package]]
 name = "clap_lex"
 version = "0.7.2"
@@ -448,6 +470,12 @@ dependencies = [
 name = "drop"
 version = "0.1.0"
 
+[[package]]
+name = "either"
+version = "1.13.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "60b1af1c220855b6ceac025d3f6ecdd2b7c4894bfe9cd9bda4fbb4bc7c0d4cf0"
+
 [[package]]
 name = "elasticlunr-rs"
 version = "3.0.2"
@@ -667,6 +695,15 @@ dependencies = [
  "version_check",
 ]
 
+[[package]]
+name = "getopts"
+version = "0.2.21"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "14dbbfd5c71d70241ecf9e6f13737f7b5ce823821063188d7e46c41d371eebd5"
+dependencies = [
+ "unicode-width",
+]
+
 [[package]]
 name = "getrandom"
 version = "0.2.15"
@@ -771,6 +808,12 @@ dependencies = [
 name = "heap"
 version = "0.1.0"
 
+[[package]]
+name = "heck"
+version = "0.5.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea"
+
 [[package]]
 name = "hermit-abi"
 version = "0.3.9"
@@ -1037,6 +1080,15 @@ dependencies = [
  "ticket_fields",
 ]
 
+[[package]]
+name = "itertools"
+version = "0.13.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "413ee7dfc52ee1a4949ceeb7dbc8a33f2d6c088194d9f922fb8318faf1f01186"
+dependencies = [
+ "either",
+]
+
 [[package]]
 name = "itoa"
 version = "1.0.11"
@@ -1182,7 +1234,7 @@ dependencies = [
  "once_cell",
  "opener",
  "pathdiff",
- "pulldown-cmark",
+ "pulldown-cmark 0.10.3",
  "regex",
  "serde",
  "serde_json",
@@ -1206,6 +1258,21 @@ dependencies = [
  "serde_json",
 ]
 
+[[package]]
+name = "mdbook-link-shortener"
+version = "0.1.0"
+dependencies = [
+ "anyhow",
+ "bimap",
+ "clap",
+ "itertools",
+ "mdbook",
+ "pulldown-cmark 0.11.0",
+ "pulldown-cmark-to-cmark",
+ "semver",
+ "serde_json",
+]
+
 [[package]]
 name = "memchr"
 version = "2.7.4"
@@ -1615,7 +1682,20 @@ checksum = "76979bea66e7875e7509c4ec5300112b316af87fa7a252ca91c448b32dfe3993"
 dependencies = [
  "bitflags 2.6.0",
  "memchr",
- "pulldown-cmark-escape",
+ "pulldown-cmark-escape 0.10.1",
+ "unicase",
+]
+
+[[package]]
+name = "pulldown-cmark"
+version = "0.11.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8746739f11d39ce5ad5c2520a9b75285310dbfe78c541ccf832d38615765aec0"
+dependencies = [
+ "bitflags 2.6.0",
+ "getopts",
+ "memchr",
+ "pulldown-cmark-escape 0.11.0",
  "unicase",
 ]
 
@@ -1625,6 +1705,21 @@ version = "0.10.1"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "bd348ff538bc9caeda7ee8cad2d1d48236a1f443c1fa3913c6a02fe0043b1dd3"
 
+[[package]]
+name = "pulldown-cmark-escape"
+version = "0.11.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "007d8adb5ddab6f8e3f491ac63566a7d5002cc7ed73901f72057943fa71ae1ae"
+
+[[package]]
+name = "pulldown-cmark-to-cmark"
+version = "15.0.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b9c77db841443d89a57ae94f22d29c022f6d9f41b00bddbf1f4024dbaf4bdce1"
+dependencies = [
+ "pulldown-cmark 0.11.0",
+]
+
 [[package]]
 name = "quote"
 version = "1.0.36"
@@ -2290,6 +2385,12 @@ dependencies = [
  "tinyvec",
 ]
 
+[[package]]
+name = "unicode-width"
+version = "0.1.13"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0336d538f7abc86d282a4189614dfaa90810dfc2c6f6427eaf88e16311dd225d"
+
 [[package]]
 name = "unwrap"
 version = "0.1.0"
index ef73559..52e8a87 100644 (file)
@@ -1,5 +1,11 @@
 [workspace]
-members = ["exercises/*/*", "helpers/common", "helpers/mdbook-exercise-linker", "helpers/ticket_fields"]
+members = [
+  "exercises/*/*",
+  "helpers/common",
+  "helpers/mdbook-exercise-linker",
+  "helpers/mdbook-link-shortener",
+  "helpers/ticket_fields",
+]
 resolver = "2"
 
 # This is needed to guarantee the expected behaviour on that specific exercise,
index 10b0188..3580ba5 100644 (file)
@@ -39,8 +39,44 @@ header-includes = [
   "\\DefineVerbatimEnvironment{Highlighting}{Verbatim}{commandchars=\\\\\\{\\},fontsize=\\small}",
 ]
 
+[output.pandoc.profile.paperback]
+output-file = "100-exercises-to-learn-rust.pdf"
+to = "latex"
+highlight-style = "monochrome"
+# We use `lualatext` because, right now, it's the only engine
+# that supports fallback fonts, which we need for emojis.
+pdf-engine = "lualatex"
+
+[output.pandoc.profile.paperback.variables]
+subtitle = "A hands-on course by Mainmatter"
+# You can get these fonts here: https://fonts.google.com/selection?query=noto+color+
+mainfont = "Noto Serif"
+sansfont = "Noto Sans"
+monofont = "Noto Sans Mono"
+mainfontfallback = ["Noto Color Emoji:mode=harf"]
+sansfontfallback = ["Noto Color Emoji:mode=harf"]
+monofontfallback = [
+  "Noto Color Emoji:mode=harf",
+]
+urlstyle = "rm"
+documentclass = "book"
+fontsize = "11pt"
+geometry = "papersize={8in,10in},top=2cm,bottom=2cm,left=2.4cm,right=2.4cm"
+header-includes = [
+  # Reduce font size of code blocks
+  "\\DefineVerbatimEnvironment{Highlighting}{Verbatim}{commandchars=\\\\\\{\\},fontsize=\\small}",
+]
+links-as-notes = true
+
 [output.html]
 git-repository-url = "https://github.com/mainmatter/100-exercises-to-learn-rust"
 
 [preprocessor.exercise-linker]
 exercise_root_url = "https://github.com/mainmatter/100-exercises-to-learn-rust/tree/main/exercises"
+
+[preprocessor.link-shortener]
+base_url = "https://ruex.io"
+renderers = ["pandoc"]
+mapping = "link2alias.json"
+verify = false
+after = ["exercise-linker"]
diff --git a/book/link2alias.json b/book/link2alias.json
new file mode 100644 (file)
index 0000000..c2f3c3b
--- /dev/null
@@ -0,0 +1,189 @@
+{
+  "https://blog.acolyer.org/2019/05/28/cheri-abi/": "f2u",
+  "https://crates.io": "f4q",
+  "https://crates.io/crates/cargo-modules": "f2n",
+  "https://doc.rust-lang.org/book/ch03-02-data-types.html#integer-types": "ffr",
+  "https://doc.rust-lang.org/book/title-page.html": "f6t",
+  "https://doc.rust-lang.org/cargo/reference/cargo-targets.html#cargo-targets": "f4m",
+  "https://doc.rust-lang.org/cargo/reference/profiles.html": "ffc",
+  "https://doc.rust-lang.org/cargo/reference/specifying-dependencies.html": "f45",
+  "https://doc.rust-lang.org/nomicon/": "f6u",
+  "https://doc.rust-lang.org/reference/expressions/operator-expr.html#numeric-cast": "f2z",
+  "https://doc.rust-lang.org/reference/items/implementations.html#trait-implementation-coherence": "fzf",
+  "https://doc.rust-lang.org/reference/lifetime-elision.html": "f4c",
+  "https://doc.rust-lang.org/std/cell/struct.UnsafeCell.html": "fxy",
+  "https://doc.rust-lang.org/std/cmp/index.html": "fzm",
+  "https://doc.rust-lang.org/std/cmp/trait.PartialEq.html": "fzz",
+  "https://doc.rust-lang.org/std/cmp/trait.PartialOrd.html": "fzb",
+  "https://doc.rust-lang.org/std/convert/trait.From.html#implementors": "fzp",
+  "https://doc.rust-lang.org/std/convert/trait.Into.html#implementors": "fzl",
+  "https://doc.rust-lang.org/std/iter/trait.FusedIterator.html": "f4s",
+  "https://doc.rust-lang.org/std/iter/trait.Iterator.html": "fxf",
+  "https://doc.rust-lang.org/std/keyword.for.html": "ffj",
+  "https://doc.rust-lang.org/std/keyword.while.html": "ffh",
+  "https://doc.rust-lang.org/std/macro.panic.html": "ffl",
+  "https://doc.rust-lang.org/std/mem/fn.size_of.html": "f27",
+  "https://doc.rust-lang.org/std/ops/index.html": "fzn",
+  "https://doc.rust-lang.org/std/ops/trait.Add.html": "fz4",
+  "https://doc.rust-lang.org/std/ops/trait.Deref.html#deref-coercion": "fzt",
+  "https://doc.rust-lang.org/std/ops/trait.Div.html": "fzv",
+  "https://doc.rust-lang.org/std/ops/trait.Mul.html": "fz6",
+  "https://doc.rust-lang.org/std/ops/trait.Rem.html": "fz8",
+  "https://doc.rust-lang.org/std/ops/trait.Sub.html": "fzx",
+  "https://doc.rust-lang.org/std/prelude/index.html": "f2c",
+  "https://doc.rust-lang.org/std/primitive.i32.html#associatedconstant.MAX": "ffe",
+  "https://doc.rust-lang.org/std/primitive.i32.html#associatedconstant.MIN": "ff7",
+  "https://doc.rust-lang.org/std/primitive.u32.html#associatedconstant.MAX": "ffw",
+  "https://doc.rust-lang.org/std/slice/struct.Iter.html": "f4d",
+  "https://doc.rust-lang.org/std/string/struct.String.html": "f26",
+  "https://doc.rust-lang.org/std/sync/atomic/index.html": "fxh",
+  "https://doc.rust-lang.org/std/vec/struct.Vec.html#method.iter": "f4j",
+  "https://docs.rs/dhat/latest/dhat/": "f2y",
+  "https://docs.rs/itertools/": "fx2",
+  "https://docs.rs/thiserror/latest/thiserror/": "f4n",
+  "https://docs.rs/tokio-stream/latest/tokio_stream/": "f65",
+  "https://docs.rs/tokio-stream/latest/tokio_stream/trait.StreamExt.html#method.merge": "f6m",
+  "https://docs.rs/tokio-util/latest/tokio_util/sync/struct.CancellationToken.html": "f63",
+  "https://docs.rs/tokio/latest/tokio/task/struct.JoinError.html": "f6z",
+  "https://docs.rust-embedded.org/book/": "f6k",
+  "https://en.wikipedia.org/wiki/Dangling_pointer": "f2h",
+  "https://en.wikipedia.org/wiki/Data_segment": "fx7",
+  "https://en.wikipedia.org/wiki/Memory_address": "f2r",
+  "https://en.wikipedia.org/wiki/Stack_overflow": "f2e",
+  "https://en.wikipedia.org/wiki/Two%27s_complement": "ff9",
+  "https://en.wikipedia.org/wiki/UTF-8": "f2v",
+  "https://exercism.io": "f6r",
+  "https://github.com/LukeMathWalker/cargo-chef": "ffb",
+  "https://github.com/LukeMathWalker/wiremock-rs": "ffm",
+  "https://github.com/dtolnay/cargo-expand": "fzq",
+  "https://github.com/dtolnay/proc-macro-workshop": "fzw",
+  "https://github.com/mainmatter/100-exercises-to-learn-rust": "ff6",
+  "https://github.com/mainmatter/100-exercises-to-learn-rust/tree/main/exercises/01_intro/00_welcome": "ff3",
+  "https://github.com/mainmatter/100-exercises-to-learn-rust/tree/main/exercises/01_intro/01_syntax": "ffq",
+  "https://github.com/mainmatter/100-exercises-to-learn-rust/tree/main/exercises/02_basic_calculator/00_intro": "ff5",
+  "https://github.com/mainmatter/100-exercises-to-learn-rust/tree/main/exercises/02_basic_calculator/01_integers": "fft",
+  "https://github.com/mainmatter/100-exercises-to-learn-rust/tree/main/exercises/02_basic_calculator/02_variables": "ffy",
+  "https://github.com/mainmatter/100-exercises-to-learn-rust/tree/main/exercises/02_basic_calculator/03_if_else": "ffu",
+  "https://github.com/mainmatter/100-exercises-to-learn-rust/tree/main/exercises/02_basic_calculator/04_panics": "ffk",
+  "https://github.com/mainmatter/100-exercises-to-learn-rust/tree/main/exercises/02_basic_calculator/05_factorial": "ffs",
+  "https://github.com/mainmatter/100-exercises-to-learn-rust/tree/main/exercises/02_basic_calculator/06_while": "ffg",
+  "https://github.com/mainmatter/100-exercises-to-learn-rust/tree/main/exercises/02_basic_calculator/07_for": "ffd",
+  "https://github.com/mainmatter/100-exercises-to-learn-rust/tree/main/exercises/02_basic_calculator/08_overflow": "f2f",
+  "https://github.com/mainmatter/100-exercises-to-learn-rust/tree/main/exercises/02_basic_calculator/09_saturating": "f22",
+  "https://github.com/mainmatter/100-exercises-to-learn-rust/tree/main/exercises/02_basic_calculator/10_as_casting": "f24",
+  "https://github.com/mainmatter/100-exercises-to-learn-rust/tree/main/exercises/03_ticket_v1/00_intro": "f2x",
+  "https://github.com/mainmatter/100-exercises-to-learn-rust/tree/main/exercises/03_ticket_v1/01_struct": "f28",
+  "https://github.com/mainmatter/100-exercises-to-learn-rust/tree/main/exercises/03_ticket_v1/02_validation": "f2b",
+  "https://github.com/mainmatter/100-exercises-to-learn-rust/tree/main/exercises/03_ticket_v1/03_modules": "f2m",
+  "https://github.com/mainmatter/100-exercises-to-learn-rust/tree/main/exercises/03_ticket_v1/04_visibility": "f23",
+  "https://github.com/mainmatter/100-exercises-to-learn-rust/tree/main/exercises/03_ticket_v1/05_encapsulation": "f2q",
+  "https://github.com/mainmatter/100-exercises-to-learn-rust/tree/main/exercises/03_ticket_v1/06_ownership": "f25",
+  "https://github.com/mainmatter/100-exercises-to-learn-rust/tree/main/exercises/03_ticket_v1/07_setters": "f2w",
+  "https://github.com/mainmatter/100-exercises-to-learn-rust/tree/main/exercises/03_ticket_v1/08_stack": "f29",
+  "https://github.com/mainmatter/100-exercises-to-learn-rust/tree/main/exercises/03_ticket_v1/09_heap": "f2p",
+  "https://github.com/mainmatter/100-exercises-to-learn-rust/tree/main/exercises/03_ticket_v1/10_references_in_memory": "f2l",
+  "https://github.com/mainmatter/100-exercises-to-learn-rust/tree/main/exercises/03_ticket_v1/11_destructor": "f2g",
+  "https://github.com/mainmatter/100-exercises-to-learn-rust/tree/main/exercises/03_ticket_v1/12_outro": "f2j",
+  "https://github.com/mainmatter/100-exercises-to-learn-rust/tree/main/exercises/04_traits/00_intro": "f2d",
+  "https://github.com/mainmatter/100-exercises-to-learn-rust/tree/main/exercises/04_traits/01_trait": "f2a",
+  "https://github.com/mainmatter/100-exercises-to-learn-rust/tree/main/exercises/04_traits/02_orphan_rule": "fz2",
+  "https://github.com/mainmatter/100-exercises-to-learn-rust/tree/main/exercises/04_traits/03_operator_overloading": "fz3",
+  "https://github.com/mainmatter/100-exercises-to-learn-rust/tree/main/exercises/04_traits/04_derive": "fz7",
+  "https://github.com/mainmatter/100-exercises-to-learn-rust/tree/main/exercises/04_traits/05_trait_bounds": "fz9",
+  "https://github.com/mainmatter/100-exercises-to-learn-rust/tree/main/exercises/04_traits/06_str_slice": "fzr",
+  "https://github.com/mainmatter/100-exercises-to-learn-rust/tree/main/exercises/04_traits/07_deref": "fzy",
+  "https://github.com/mainmatter/100-exercises-to-learn-rust/tree/main/exercises/04_traits/08_sized": "fzu",
+  "https://github.com/mainmatter/100-exercises-to-learn-rust/tree/main/exercises/04_traits/09_from": "fzk",
+  "https://github.com/mainmatter/100-exercises-to-learn-rust/tree/main/exercises/04_traits/10_assoc_vs_generic": "fzs",
+  "https://github.com/mainmatter/100-exercises-to-learn-rust/tree/main/exercises/04_traits/11_clone": "fzh",
+  "https://github.com/mainmatter/100-exercises-to-learn-rust/tree/main/exercises/04_traits/12_copy": "fzg",
+  "https://github.com/mainmatter/100-exercises-to-learn-rust/tree/main/exercises/04_traits/13_drop": "fzj",
+  "https://github.com/mainmatter/100-exercises-to-learn-rust/tree/main/exercises/04_traits/14_outro": "fzc",
+  "https://github.com/mainmatter/100-exercises-to-learn-rust/tree/main/exercises/05_ticket_v2/00_intro": "fza",
+  "https://github.com/mainmatter/100-exercises-to-learn-rust/tree/main/exercises/05_ticket_v2/01_enum": "f4f",
+  "https://github.com/mainmatter/100-exercises-to-learn-rust/tree/main/exercises/05_ticket_v2/02_match": "f42",
+  "https://github.com/mainmatter/100-exercises-to-learn-rust/tree/main/exercises/05_ticket_v2/03_variants_with_data": "f4z",
+  "https://github.com/mainmatter/100-exercises-to-learn-rust/tree/main/exercises/05_ticket_v2/04_if_let": "f44",
+  "https://github.com/mainmatter/100-exercises-to-learn-rust/tree/main/exercises/05_ticket_v2/05_nullability": "f4x",
+  "https://github.com/mainmatter/100-exercises-to-learn-rust/tree/main/exercises/05_ticket_v2/06_fallibility": "f46",
+  "https://github.com/mainmatter/100-exercises-to-learn-rust/tree/main/exercises/05_ticket_v2/07_unwrap": "f4v",
+  "https://github.com/mainmatter/100-exercises-to-learn-rust/tree/main/exercises/05_ticket_v2/08_error_enums": "f48",
+  "https://github.com/mainmatter/100-exercises-to-learn-rust/tree/main/exercises/05_ticket_v2/09_error_trait": "f4b",
+  "https://github.com/mainmatter/100-exercises-to-learn-rust/tree/main/exercises/05_ticket_v2/10_packages": "f43",
+  "https://github.com/mainmatter/100-exercises-to-learn-rust/tree/main/exercises/05_ticket_v2/11_dependencies": "f4w",
+  "https://github.com/mainmatter/100-exercises-to-learn-rust/tree/main/exercises/05_ticket_v2/12_thiserror": "f47",
+  "https://github.com/mainmatter/100-exercises-to-learn-rust/tree/main/exercises/05_ticket_v2/13_try_from": "f4e",
+  "https://github.com/mainmatter/100-exercises-to-learn-rust/tree/main/exercises/05_ticket_v2/14_source": "f49",
+  "https://github.com/mainmatter/100-exercises-to-learn-rust/tree/main/exercises/05_ticket_v2/15_outro": "f4y",
+  "https://github.com/mainmatter/100-exercises-to-learn-rust/tree/main/exercises/06_ticket_management/00_intro": "f4u",
+  "https://github.com/mainmatter/100-exercises-to-learn-rust/tree/main/exercises/06_ticket_management/01_arrays": "f4p",
+  "https://github.com/mainmatter/100-exercises-to-learn-rust/tree/main/exercises/06_ticket_management/02_vec": "f4l",
+  "https://github.com/mainmatter/100-exercises-to-learn-rust/tree/main/exercises/06_ticket_management/03_resizing": "f4k",
+  "https://github.com/mainmatter/100-exercises-to-learn-rust/tree/main/exercises/06_ticket_management/04_iterators": "f4h",
+  "https://github.com/mainmatter/100-exercises-to-learn-rust/tree/main/exercises/06_ticket_management/05_iter": "f4g",
+  "https://github.com/mainmatter/100-exercises-to-learn-rust/tree/main/exercises/06_ticket_management/06_lifetimes": "f4a",
+  "https://github.com/mainmatter/100-exercises-to-learn-rust/tree/main/exercises/06_ticket_management/07_combinators": "fxz",
+  "https://github.com/mainmatter/100-exercises-to-learn-rust/tree/main/exercises/06_ticket_management/08_impl_trait": "fx4",
+  "https://github.com/mainmatter/100-exercises-to-learn-rust/tree/main/exercises/06_ticket_management/09_impl_trait_2": "fxx",
+  "https://github.com/mainmatter/100-exercises-to-learn-rust/tree/main/exercises/06_ticket_management/10_slices": "fx6",
+  "https://github.com/mainmatter/100-exercises-to-learn-rust/tree/main/exercises/06_ticket_management/11_mutable_slices": "fxv",
+  "https://github.com/mainmatter/100-exercises-to-learn-rust/tree/main/exercises/06_ticket_management/12_two_states": "fx8",
+  "https://github.com/mainmatter/100-exercises-to-learn-rust/tree/main/exercises/06_ticket_management/13_index": "fxb",
+  "https://github.com/mainmatter/100-exercises-to-learn-rust/tree/main/exercises/06_ticket_management/14_index_mut": "fxn",
+  "https://github.com/mainmatter/100-exercises-to-learn-rust/tree/main/exercises/06_ticket_management/15_hashmap": "fxm",
+  "https://github.com/mainmatter/100-exercises-to-learn-rust/tree/main/exercises/06_ticket_management/16_btreemap": "fx3",
+  "https://github.com/mainmatter/100-exercises-to-learn-rust/tree/main/exercises/07_threads/00_intro": "fxq",
+  "https://github.com/mainmatter/100-exercises-to-learn-rust/tree/main/exercises/07_threads/01_threads": "fxw",
+  "https://github.com/mainmatter/100-exercises-to-learn-rust/tree/main/exercises/07_threads/02_static": "fxe",
+  "https://github.com/mainmatter/100-exercises-to-learn-rust/tree/main/exercises/07_threads/03_leak": "fx9",
+  "https://github.com/mainmatter/100-exercises-to-learn-rust/tree/main/exercises/07_threads/04_scoped_threads": "fxr",
+  "https://github.com/mainmatter/100-exercises-to-learn-rust/tree/main/exercises/07_threads/05_channels": "fxt",
+  "https://github.com/mainmatter/100-exercises-to-learn-rust/tree/main/exercises/07_threads/06_interior_mutability": "fxu",
+  "https://github.com/mainmatter/100-exercises-to-learn-rust/tree/main/exercises/07_threads/07_ack": "fxp",
+  "https://github.com/mainmatter/100-exercises-to-learn-rust/tree/main/exercises/07_threads/08_client": "fxl",
+  "https://github.com/mainmatter/100-exercises-to-learn-rust/tree/main/exercises/07_threads/09_bounded": "fxk",
+  "https://github.com/mainmatter/100-exercises-to-learn-rust/tree/main/exercises/07_threads/10_patch": "fxs",
+  "https://github.com/mainmatter/100-exercises-to-learn-rust/tree/main/exercises/07_threads/11_locks": "fxj",
+  "https://github.com/mainmatter/100-exercises-to-learn-rust/tree/main/exercises/07_threads/12_rw_lock": "fxd",
+  "https://github.com/mainmatter/100-exercises-to-learn-rust/tree/main/exercises/07_threads/13_without_channels": "fxc",
+  "https://github.com/mainmatter/100-exercises-to-learn-rust/tree/main/exercises/07_threads/14_sync": "fxa",
+  "https://github.com/mainmatter/100-exercises-to-learn-rust/tree/main/exercises/08_futures/00_intro": "f6f",
+  "https://github.com/mainmatter/100-exercises-to-learn-rust/tree/main/exercises/08_futures/01_async_fn": "f62",
+  "https://github.com/mainmatter/100-exercises-to-learn-rust/tree/main/exercises/08_futures/02_spawn": "f64",
+  "https://github.com/mainmatter/100-exercises-to-learn-rust/tree/main/exercises/08_futures/03_runtime": "f6x",
+  "https://github.com/mainmatter/100-exercises-to-learn-rust/tree/main/exercises/08_futures/04_future": "f66",
+  "https://github.com/mainmatter/100-exercises-to-learn-rust/tree/main/exercises/08_futures/05_blocking": "f68",
+  "https://github.com/mainmatter/100-exercises-to-learn-rust/tree/main/exercises/08_futures/06_async_aware_primitives": "f6b",
+  "https://github.com/mainmatter/100-exercises-to-learn-rust/tree/main/exercises/08_futures/07_cancellation": "f6q",
+  "https://github.com/mainmatter/100-exercises-to-learn-rust/tree/main/exercises/08_futures/08_outro": "f6e",
+  "https://github.com/mainmatter/100-exercises-to-learn-rust/tree/solutions": "ffz",
+  "https://github.com/mainmatter/rust-advanced-testing-workshop": "fzd",
+  "https://github.com/rust-lang/rustlings": "f69",
+  "https://huonw.github.io/blog/2016/04/myths-and-legends-about-integer-overflow-in-rust/": "ffa",
+  "https://lexi-lambda.github.io/blog/2019/11/05/parse-don-t-validate/": "f4r",
+  "https://mainmatter.com/contact/": "ff2",
+  "https://mainmatter.com/rust-consulting/": "fff",
+  "https://marabos.nl/atomics/": "fxg",
+  "https://nostarch.com/rust-rustaceans": "f6p",
+  "https://owasp.org/www-community/vulnerabilities/Doubly_freeing_memory": "f2k",
+  "https://owasp.org/www-community/vulnerabilities/Using_freed_memory": "f2s",
+  "https://pavex.dev": "ffn",
+  "https://play.rust-lang.org/?version=stable&mode=debug&edition=2021&gist=36e5ddbe3b3f741dfa9f74c956622bac": "ffp",
+  "https://play.rust-lang.org/?version=stable&mode=debug&edition=2021&gist=afedf7062298ca8f5a248bc551062eaa": "fx5",
+  "https://rust-exercises.com/100-exercises-to-learn-rust.pdf": "ffx",
+  "https://rust-exercises.com/100-exercises/": "ff4",
+  "https://rust-exercises.com/advanced-testing/": "f6s",
+  "https://rust-exercises.com/telemetry/": "f6h",
+  "https://rust-lang.github.io/api-guidelines/naming.html#casing-conforms-to-rfc-430-c-case": "fze",
+  "https://rust-lang.github.io/wg-async/vision/submitted_stories/status_quo/barbara_battles_buffered_streams.html": "f6w",
+  "https://ryhl.io/blog/async-what-is-blocking/": "f6v",
+  "https://tokio.rs/tokio/tutorial/select": "f6n",
+  "https://valgrind.org/docs/manual/dh-manual.html": "f2t",
+  "https://veykril.github.io/tlborm/": "fz5",
+  "https://without.boats/blog/the-scoped-task-trilemma/": "f67",
+  "https://www.lpalmieri.com/": "ffv",
+  "https://www.lpalmieri.com/posts/2020-12-11-zero-to-production-6-domain-modelling/": "f4t",
+  "https://www.oreilly.com/library/view/programming-rust-2nd/9781492052586/": "f6y",
+  "https://www.youtube.com/playlist?list=PLqbS7AVVErFirH9armw8yXlE6dacF-A6z": "f6l",
+  "https://zero2prod.com": "ff8"
+}
\ No newline at end of file
diff --git a/helpers/json2redirects.sh b/helpers/json2redirects.sh
new file mode 100755 (executable)
index 0000000..2b17989
--- /dev/null
@@ -0,0 +1,12 @@
+#!/bin/bash
+
+# Ensure the JSON file is provided as an argument
+if [ "$#" -ne 1 ]; then
+  echo "Usage: $0 <input_json_file>"
+  exit 1
+fi
+
+input_file=$1
+
+# Use jq to parse the JSON and format the output
+jq -r 'to_entries[] | "/" + .value + " " + .key' "$input_file"
index 4f7fb1f..38700ae 100644 (file)
@@ -3,7 +3,6 @@ use mdbook::book::Book;
 use mdbook::preprocess::{Preprocessor, PreprocessorContext};
 use mdbook::BookItem;
 
-/// A no-op preprocessor.
 pub struct ExerciseLinker;
 
 impl ExerciseLinker {
diff --git a/helpers/mdbook-link-shortener/Cargo.toml b/helpers/mdbook-link-shortener/Cargo.toml
new file mode 100644 (file)
index 0000000..944aedd
--- /dev/null
@@ -0,0 +1,15 @@
+[package]
+name = "mdbook-link-shortener"
+version = "0.1.0"
+edition = "2021"
+
+[dependencies]
+anyhow = "1.0.86"
+bimap = { version = "0.6.3", features = ["serde"] }
+clap = { version = "4.5.4", features = ["derive"] }
+itertools = "0.13.0"
+mdbook = "0.4.40"
+pulldown-cmark = "0.11.0"
+pulldown-cmark-to-cmark = "15"
+semver = "1.0.23"
+serde_json = "1.0.117"
diff --git a/helpers/mdbook-link-shortener/src/lib.rs b/helpers/mdbook-link-shortener/src/lib.rs
new file mode 100644 (file)
index 0000000..28bd556
--- /dev/null
@@ -0,0 +1,224 @@
+use anyhow::{Context, Error};
+use bimap::BiHashMap;
+use itertools::Itertools;
+use mdbook::book::{Book, Chapter};
+use mdbook::preprocess::{Preprocessor, PreprocessorContext};
+use mdbook::BookItem;
+use std::collections::{BTreeMap, BTreeSet};
+use std::fs::File;
+use std::path::PathBuf;
+use std::str::FromStr;
+
+pub struct LinkShortener;
+
+struct AliasGenerator {
+    cursors: [usize; 3],
+}
+
+impl AliasGenerator {
+    const ALPHABET: &'static [u8] = b"f2z4x6v8bnm3q5w7e9rtyuplkshgjdca";
+
+    fn new() -> AliasGenerator {
+        AliasGenerator { cursors: [0, 0, 0] }
+    }
+
+    /// Generate a 4 alphanumeric long alias, starting from "aaaa" and incrementing by one each time
+    /// until "9999", using only lowercase letters and numbers.
+    /// We skip ambiguous characters like "0", "o", "1", "l".
+    fn next(&mut self) -> String {
+        let mut alias = String::with_capacity(4);
+        for cursor in &mut self.cursors {
+            alias.push(Self::ALPHABET[*cursor] as char);
+        }
+
+        for cursor in self.cursors.iter_mut().rev() {
+            if *cursor == Self::ALPHABET.len() - 1 {
+                *cursor = 0;
+            } else {
+                *cursor += 1;
+                break;
+            }
+        }
+
+        alias
+    }
+
+    /// Generate a unique alias that is not already used by the `link2alias` map.
+    fn next_until_unique(&mut self, link2alias: &BiHashMap<String, String>) -> String {
+        let mut alias = self.next();
+        while link2alias.contains_right(&alias) {
+            alias = self.next();
+        }
+        alias
+    }
+}
+
+impl LinkShortener {
+    pub fn new() -> LinkShortener {
+        LinkShortener
+    }
+}
+
+impl Preprocessor for LinkShortener {
+    fn name(&self) -> &str {
+        "link-shortener"
+    }
+
+    fn run(&self, ctx: &PreprocessorContext, mut book: Book) -> Result<Book, Error> {
+        let config = ctx
+            .config
+            .get_preprocessor(self.name())
+            .context("Failed to get preprocessor configuration")?;
+        let root_url = {
+            let root_url = config.get("base_url").context("Failed to get `base_url`")?;
+            root_url
+                .as_str()
+                .context("`base_url` is not a string")?
+                .to_owned()
+        };
+        let mapping = {
+            let mapping = config.get("mapping").context("Failed to get `mapping`")?;
+            let mapping = mapping
+                .as_str()
+                .context("`mapping` is not a string")?
+                .to_owned();
+            PathBuf::from_str(&mapping).context("Failed to parse `mapping` as a path")?
+        };
+        let mut link2alias = {
+            match File::open(&mapping) {
+                Ok(file) => {
+                    serde_json::from_reader(file).context("Failed to parse existing mapping")?
+                }
+                Err(e) => {
+                    if e.kind() == std::io::ErrorKind::NotFound {
+                        BiHashMap::new()
+                    } else {
+                        return Err(e).context("Failed to open existing mapping");
+                    }
+                }
+            }
+        };
+        let verify = config
+            .get("verify")
+            .context("Failed to get `verify`")?
+            .as_bool()
+            .context("`verify` is not a boolean")?;
+        // Env var overrides config
+        let verify = std::env::var("LINK_SHORTENER_VERIFY")
+            .map(|v| v == "true")
+            .unwrap_or(verify);
+
+        let mut alias_gen = AliasGenerator::new();
+
+        book.sections.iter_mut().for_each(|i| {
+            if let BookItem::Chapter(c) = i {
+                c.content = replace_anchors(c, &root_url, &mut alias_gen, &mut link2alias, verify)
+                    .expect("Error converting links for chapter");
+                for i in c.sub_items.iter_mut() {
+                    if let BookItem::Chapter(sub_chapter) = i {
+                        sub_chapter.content = replace_anchors(
+                            sub_chapter,
+                            &root_url,
+                            &mut alias_gen,
+                            &mut link2alias,
+                            verify,
+                        )
+                        .expect("Error converting links for subchapter");
+                    }
+                }
+            }
+        });
+
+        if !verify {
+            std::fs::create_dir_all(mapping.parent().expect("Mapping file path has no parent"))?;
+            let mut file = File::create(&mapping).context("Failed to upsert mapping file")?;
+            let ordered = link2alias.iter().collect::<BTreeMap<_, _>>();
+            serde_json::to_writer_pretty(&mut file, &ordered)?;
+        }
+
+        Ok(book)
+    }
+
+    fn supports_renderer(&self, _renderer: &str) -> bool {
+        true
+    }
+}
+
+fn replace_anchors(
+    chapter: &mut Chapter,
+    root_url: &str,
+    alias_gen: &mut AliasGenerator,
+    link2alias: &mut BiHashMap<String, String>,
+    verify: bool,
+) -> Result<String, anyhow::Error> {
+    use pulldown_cmark::{CowStr, Event, LinkType, Options, Parser, Tag};
+    use pulldown_cmark_to_cmark::cmark;
+
+    let mut buf = String::with_capacity(chapter.content.len());
+
+    let mut unshortened_links = BTreeSet::new();
+    let events = Parser::new_ext(&chapter.content, Options::all())
+        .map(|e| {
+            let Event::Start(Tag::Link {
+                link_type,
+                dest_url,
+                title,
+                id,
+            }) = &e
+            else {
+                return e;
+            };
+
+            match link_type {
+                LinkType::Autolink
+                | LinkType::Shortcut
+                | LinkType::Inline
+                | LinkType::Reference
+                | LinkType::Collapsed => {
+                    if dest_url.starts_with("http") {
+                        let alias = if let Some(alias) = link2alias.get_by_left(dest_url.as_ref()) {
+                            alias.to_owned()
+                        } else {
+                            if verify {
+                                unshortened_links.insert(dest_url.to_string());
+                                return e;
+                            }
+                            let alias = alias_gen.next_until_unique(&link2alias);
+                            alias
+                        };
+                        link2alias.insert(dest_url.to_string(), alias.clone());
+
+                        Event::Start(Tag::Link {
+                            link_type: link_type.to_owned(),
+                            dest_url: CowStr::from(format!(
+                                "{root_url}/{alias}",
+                                root_url = root_url,
+                                alias = alias
+                            )),
+                            title: title.clone(),
+                            id: id.clone(),
+                        })
+                    } else {
+                        e
+                    }
+                }
+                LinkType::Email
+                | LinkType::ReferenceUnknown
+                | LinkType::CollapsedUnknown
+                | LinkType::ShortcutUnknown => e,
+            }
+        })
+        .collect_vec();
+
+    if verify && !unshortened_links.is_empty() {
+        let unshortened_links = unshortened_links.iter().join(", ");
+        return Err(anyhow::anyhow!(
+            "The following links are not shortened: {unshortened_links}\nRun again with `LINK_SHORTENER_VERIFY=false` to update the mapping \
+            with the shortened links."
+        ));
+    }
+
+    cmark(events.into_iter(), &mut buf)
+        .map(|_| buf)
+        .map_err(|err| anyhow::anyhow!("Markdown serialization failed: {err}"))
+}
diff --git a/helpers/mdbook-link-shortener/src/main.rs b/helpers/mdbook-link-shortener/src/main.rs
new file mode 100644 (file)
index 0000000..a69d424
--- /dev/null
@@ -0,0 +1,66 @@
+use clap::Parser;
+use mdbook::errors::Error;
+use mdbook::preprocess::{CmdPreprocessor, Preprocessor};
+use mdbook_link_shortener::LinkShortener;
+use semver::{Version, VersionReq};
+use std::io;
+use std::process;
+
+#[derive(clap::Parser, Debug)]
+#[command(version, about)]
+pub struct Cli {
+    #[command(subcommand)]
+    sub: Option<SubCommand>,
+}
+
+#[derive(clap::Parser, Debug)]
+pub enum SubCommand {
+    #[clap(name = "supports")]
+    Supports(Supports),
+}
+
+#[derive(clap::Parser, Debug)]
+pub struct Supports {
+    #[arg(long)]
+    renderer: String,
+}
+
+fn main() -> Result<(), anyhow::Error> {
+    let cli = Cli::parse();
+    let preprocessor = LinkShortener::new();
+
+    if let Some(SubCommand::Supports(Supports { renderer })) = cli.sub {
+        let code = if preprocessor.supports_renderer(&renderer) {
+            0
+        } else {
+            1
+        };
+        process::exit(code);
+    }
+
+    handle_preprocessing(&preprocessor)?;
+
+    Ok(())
+}
+
+fn handle_preprocessing(pre: &dyn Preprocessor) -> Result<(), Error> {
+    let (ctx, book) = CmdPreprocessor::parse_input(io::stdin())?;
+
+    let book_version = Version::parse(&ctx.mdbook_version)?;
+    let version_req = VersionReq::parse(mdbook::MDBOOK_VERSION)?;
+
+    if !version_req.matches(&book_version) {
+        eprintln!(
+            "Warning: The {} plugin was built against version {} of mdbook, \
+             but we're being called from version {}",
+            pre.name(),
+            mdbook::MDBOOK_VERSION,
+            ctx.mdbook_version
+        );
+    }
+
+    let processed_book = pre.run(&ctx, book)?;
+    serde_json::to_writer(io::stdout(), &processed_book)?;
+
+    Ok(())
+}
diff --git a/site/redirects b/site/redirects
new file mode 100644 (file)
index 0000000..761e454
--- /dev/null
@@ -0,0 +1,187 @@
+/f2u https://blog.acolyer.org/2019/05/28/cheri-abi/
+/f4q https://crates.io
+/f2n https://crates.io/crates/cargo-modules
+/ffr https://doc.rust-lang.org/book/ch03-02-data-types.html#integer-types
+/f6t https://doc.rust-lang.org/book/title-page.html
+/f4m https://doc.rust-lang.org/cargo/reference/cargo-targets.html#cargo-targets
+/ffc https://doc.rust-lang.org/cargo/reference/profiles.html
+/f45 https://doc.rust-lang.org/cargo/reference/specifying-dependencies.html
+/f6u https://doc.rust-lang.org/nomicon/
+/f2z https://doc.rust-lang.org/reference/expressions/operator-expr.html#numeric-cast
+/fzf https://doc.rust-lang.org/reference/items/implementations.html#trait-implementation-coherence
+/f4c https://doc.rust-lang.org/reference/lifetime-elision.html
+/fxy https://doc.rust-lang.org/std/cell/struct.UnsafeCell.html
+/fzm https://doc.rust-lang.org/std/cmp/index.html
+/fzz https://doc.rust-lang.org/std/cmp/trait.PartialEq.html
+/fzb https://doc.rust-lang.org/std/cmp/trait.PartialOrd.html
+/fzp https://doc.rust-lang.org/std/convert/trait.From.html#implementors
+/fzl https://doc.rust-lang.org/std/convert/trait.Into.html#implementors
+/f4s https://doc.rust-lang.org/std/iter/trait.FusedIterator.html
+/fxf https://doc.rust-lang.org/std/iter/trait.Iterator.html
+/ffj https://doc.rust-lang.org/std/keyword.for.html
+/ffh https://doc.rust-lang.org/std/keyword.while.html
+/ffl https://doc.rust-lang.org/std/macro.panic.html
+/f27 https://doc.rust-lang.org/std/mem/fn.size_of.html
+/fzn https://doc.rust-lang.org/std/ops/index.html
+/fz4 https://doc.rust-lang.org/std/ops/trait.Add.html
+/fzt https://doc.rust-lang.org/std/ops/trait.Deref.html#deref-coercion
+/fzv https://doc.rust-lang.org/std/ops/trait.Div.html
+/fz6 https://doc.rust-lang.org/std/ops/trait.Mul.html
+/fz8 https://doc.rust-lang.org/std/ops/trait.Rem.html
+/fzx https://doc.rust-lang.org/std/ops/trait.Sub.html
+/f2c https://doc.rust-lang.org/std/prelude/index.html
+/ffe https://doc.rust-lang.org/std/primitive.i32.html#associatedconstant.MAX
+/ff7 https://doc.rust-lang.org/std/primitive.i32.html#associatedconstant.MIN
+/ffw https://doc.rust-lang.org/std/primitive.u32.html#associatedconstant.MAX
+/f4d https://doc.rust-lang.org/std/slice/struct.Iter.html
+/f26 https://doc.rust-lang.org/std/string/struct.String.html
+/fxh https://doc.rust-lang.org/std/sync/atomic/index.html
+/f4j https://doc.rust-lang.org/std/vec/struct.Vec.html#method.iter
+/f2y https://docs.rs/dhat/latest/dhat/
+/fx2 https://docs.rs/itertools/
+/f4n https://docs.rs/thiserror/latest/thiserror/
+/f65 https://docs.rs/tokio-stream/latest/tokio_stream/
+/f6m https://docs.rs/tokio-stream/latest/tokio_stream/trait.StreamExt.html#method.merge
+/f63 https://docs.rs/tokio-util/latest/tokio_util/sync/struct.CancellationToken.html
+/f6z https://docs.rs/tokio/latest/tokio/task/struct.JoinError.html
+/f6k https://docs.rust-embedded.org/book/
+/f2h https://en.wikipedia.org/wiki/Dangling_pointer
+/fx7 https://en.wikipedia.org/wiki/Data_segment
+/f2r https://en.wikipedia.org/wiki/Memory_address
+/f2e https://en.wikipedia.org/wiki/Stack_overflow
+/ff9 https://en.wikipedia.org/wiki/Two%27s_complement
+/f2v https://en.wikipedia.org/wiki/UTF-8
+/f6r https://exercism.io
+/ffb https://github.com/LukeMathWalker/cargo-chef
+/ffm https://github.com/LukeMathWalker/wiremock-rs
+/fzq https://github.com/dtolnay/cargo-expand
+/fzw https://github.com/dtolnay/proc-macro-workshop
+/ff6 https://github.com/mainmatter/100-exercises-to-learn-rust
+/ff3 https://github.com/mainmatter/100-exercises-to-learn-rust/tree/main/exercises/01_intro/00_welcome
+/ffq https://github.com/mainmatter/100-exercises-to-learn-rust/tree/main/exercises/01_intro/01_syntax
+/ff5 https://github.com/mainmatter/100-exercises-to-learn-rust/tree/main/exercises/02_basic_calculator/00_intro
+/fft https://github.com/mainmatter/100-exercises-to-learn-rust/tree/main/exercises/02_basic_calculator/01_integers
+/ffy https://github.com/mainmatter/100-exercises-to-learn-rust/tree/main/exercises/02_basic_calculator/02_variables
+/ffu https://github.com/mainmatter/100-exercises-to-learn-rust/tree/main/exercises/02_basic_calculator/03_if_else
+/ffk https://github.com/mainmatter/100-exercises-to-learn-rust/tree/main/exercises/02_basic_calculator/04_panics
+/ffs https://github.com/mainmatter/100-exercises-to-learn-rust/tree/main/exercises/02_basic_calculator/05_factorial
+/ffg https://github.com/mainmatter/100-exercises-to-learn-rust/tree/main/exercises/02_basic_calculator/06_while
+/ffd https://github.com/mainmatter/100-exercises-to-learn-rust/tree/main/exercises/02_basic_calculator/07_for
+/f2f https://github.com/mainmatter/100-exercises-to-learn-rust/tree/main/exercises/02_basic_calculator/08_overflow
+/f22 https://github.com/mainmatter/100-exercises-to-learn-rust/tree/main/exercises/02_basic_calculator/09_saturating
+/f24 https://github.com/mainmatter/100-exercises-to-learn-rust/tree/main/exercises/02_basic_calculator/10_as_casting
+/f2x https://github.com/mainmatter/100-exercises-to-learn-rust/tree/main/exercises/03_ticket_v1/00_intro
+/f28 https://github.com/mainmatter/100-exercises-to-learn-rust/tree/main/exercises/03_ticket_v1/01_struct
+/f2b https://github.com/mainmatter/100-exercises-to-learn-rust/tree/main/exercises/03_ticket_v1/02_validation
+/f2m https://github.com/mainmatter/100-exercises-to-learn-rust/tree/main/exercises/03_ticket_v1/03_modules
+/f23 https://github.com/mainmatter/100-exercises-to-learn-rust/tree/main/exercises/03_ticket_v1/04_visibility
+/f2q https://github.com/mainmatter/100-exercises-to-learn-rust/tree/main/exercises/03_ticket_v1/05_encapsulation
+/f25 https://github.com/mainmatter/100-exercises-to-learn-rust/tree/main/exercises/03_ticket_v1/06_ownership
+/f2w https://github.com/mainmatter/100-exercises-to-learn-rust/tree/main/exercises/03_ticket_v1/07_setters
+/f29 https://github.com/mainmatter/100-exercises-to-learn-rust/tree/main/exercises/03_ticket_v1/08_stack
+/f2p https://github.com/mainmatter/100-exercises-to-learn-rust/tree/main/exercises/03_ticket_v1/09_heap
+/f2l https://github.com/mainmatter/100-exercises-to-learn-rust/tree/main/exercises/03_ticket_v1/10_references_in_memory
+/f2g https://github.com/mainmatter/100-exercises-to-learn-rust/tree/main/exercises/03_ticket_v1/11_destructor
+/f2j https://github.com/mainmatter/100-exercises-to-learn-rust/tree/main/exercises/03_ticket_v1/12_outro
+/f2d https://github.com/mainmatter/100-exercises-to-learn-rust/tree/main/exercises/04_traits/00_intro
+/f2a https://github.com/mainmatter/100-exercises-to-learn-rust/tree/main/exercises/04_traits/01_trait
+/fz2 https://github.com/mainmatter/100-exercises-to-learn-rust/tree/main/exercises/04_traits/02_orphan_rule
+/fz3 https://github.com/mainmatter/100-exercises-to-learn-rust/tree/main/exercises/04_traits/03_operator_overloading
+/fz7 https://github.com/mainmatter/100-exercises-to-learn-rust/tree/main/exercises/04_traits/04_derive
+/fz9 https://github.com/mainmatter/100-exercises-to-learn-rust/tree/main/exercises/04_traits/05_trait_bounds
+/fzr https://github.com/mainmatter/100-exercises-to-learn-rust/tree/main/exercises/04_traits/06_str_slice
+/fzy https://github.com/mainmatter/100-exercises-to-learn-rust/tree/main/exercises/04_traits/07_deref
+/fzu https://github.com/mainmatter/100-exercises-to-learn-rust/tree/main/exercises/04_traits/08_sized
+/fzk https://github.com/mainmatter/100-exercises-to-learn-rust/tree/main/exercises/04_traits/09_from
+/fzs https://github.com/mainmatter/100-exercises-to-learn-rust/tree/main/exercises/04_traits/10_assoc_vs_generic
+/fzh https://github.com/mainmatter/100-exercises-to-learn-rust/tree/main/exercises/04_traits/11_clone
+/fzg https://github.com/mainmatter/100-exercises-to-learn-rust/tree/main/exercises/04_traits/12_copy
+/fzj https://github.com/mainmatter/100-exercises-to-learn-rust/tree/main/exercises/04_traits/13_drop
+/fzc https://github.com/mainmatter/100-exercises-to-learn-rust/tree/main/exercises/04_traits/14_outro
+/fza https://github.com/mainmatter/100-exercises-to-learn-rust/tree/main/exercises/05_ticket_v2/00_intro
+/f4f https://github.com/mainmatter/100-exercises-to-learn-rust/tree/main/exercises/05_ticket_v2/01_enum
+/f42 https://github.com/mainmatter/100-exercises-to-learn-rust/tree/main/exercises/05_ticket_v2/02_match
+/f4z https://github.com/mainmatter/100-exercises-to-learn-rust/tree/main/exercises/05_ticket_v2/03_variants_with_data
+/f44 https://github.com/mainmatter/100-exercises-to-learn-rust/tree/main/exercises/05_ticket_v2/04_if_let
+/f4x https://github.com/mainmatter/100-exercises-to-learn-rust/tree/main/exercises/05_ticket_v2/05_nullability
+/f46 https://github.com/mainmatter/100-exercises-to-learn-rust/tree/main/exercises/05_ticket_v2/06_fallibility
+/f4v https://github.com/mainmatter/100-exercises-to-learn-rust/tree/main/exercises/05_ticket_v2/07_unwrap
+/f48 https://github.com/mainmatter/100-exercises-to-learn-rust/tree/main/exercises/05_ticket_v2/08_error_enums
+/f4b https://github.com/mainmatter/100-exercises-to-learn-rust/tree/main/exercises/05_ticket_v2/09_error_trait
+/f43 https://github.com/mainmatter/100-exercises-to-learn-rust/tree/main/exercises/05_ticket_v2/10_packages
+/f4w https://github.com/mainmatter/100-exercises-to-learn-rust/tree/main/exercises/05_ticket_v2/11_dependencies
+/f47 https://github.com/mainmatter/100-exercises-to-learn-rust/tree/main/exercises/05_ticket_v2/12_thiserror
+/f4e https://github.com/mainmatter/100-exercises-to-learn-rust/tree/main/exercises/05_ticket_v2/13_try_from
+/f49 https://github.com/mainmatter/100-exercises-to-learn-rust/tree/main/exercises/05_ticket_v2/14_source
+/f4y https://github.com/mainmatter/100-exercises-to-learn-rust/tree/main/exercises/05_ticket_v2/15_outro
+/f4u https://github.com/mainmatter/100-exercises-to-learn-rust/tree/main/exercises/06_ticket_management/00_intro
+/f4p https://github.com/mainmatter/100-exercises-to-learn-rust/tree/main/exercises/06_ticket_management/01_arrays
+/f4l https://github.com/mainmatter/100-exercises-to-learn-rust/tree/main/exercises/06_ticket_management/02_vec
+/f4k https://github.com/mainmatter/100-exercises-to-learn-rust/tree/main/exercises/06_ticket_management/03_resizing
+/f4h https://github.com/mainmatter/100-exercises-to-learn-rust/tree/main/exercises/06_ticket_management/04_iterators
+/f4g https://github.com/mainmatter/100-exercises-to-learn-rust/tree/main/exercises/06_ticket_management/05_iter
+/f4a https://github.com/mainmatter/100-exercises-to-learn-rust/tree/main/exercises/06_ticket_management/06_lifetimes
+/fxz https://github.com/mainmatter/100-exercises-to-learn-rust/tree/main/exercises/06_ticket_management/07_combinators
+/fx4 https://github.com/mainmatter/100-exercises-to-learn-rust/tree/main/exercises/06_ticket_management/08_impl_trait
+/fxx https://github.com/mainmatter/100-exercises-to-learn-rust/tree/main/exercises/06_ticket_management/09_impl_trait_2
+/fx6 https://github.com/mainmatter/100-exercises-to-learn-rust/tree/main/exercises/06_ticket_management/10_slices
+/fxv https://github.com/mainmatter/100-exercises-to-learn-rust/tree/main/exercises/06_ticket_management/11_mutable_slices
+/fx8 https://github.com/mainmatter/100-exercises-to-learn-rust/tree/main/exercises/06_ticket_management/12_two_states
+/fxb https://github.com/mainmatter/100-exercises-to-learn-rust/tree/main/exercises/06_ticket_management/13_index
+/fxn https://github.com/mainmatter/100-exercises-to-learn-rust/tree/main/exercises/06_ticket_management/14_index_mut
+/fxm https://github.com/mainmatter/100-exercises-to-learn-rust/tree/main/exercises/06_ticket_management/15_hashmap
+/fx3 https://github.com/mainmatter/100-exercises-to-learn-rust/tree/main/exercises/06_ticket_management/16_btreemap
+/fxq https://github.com/mainmatter/100-exercises-to-learn-rust/tree/main/exercises/07_threads/00_intro
+/fxw https://github.com/mainmatter/100-exercises-to-learn-rust/tree/main/exercises/07_threads/01_threads
+/fxe https://github.com/mainmatter/100-exercises-to-learn-rust/tree/main/exercises/07_threads/02_static
+/fx9 https://github.com/mainmatter/100-exercises-to-learn-rust/tree/main/exercises/07_threads/03_leak
+/fxr https://github.com/mainmatter/100-exercises-to-learn-rust/tree/main/exercises/07_threads/04_scoped_threads
+/fxt https://github.com/mainmatter/100-exercises-to-learn-rust/tree/main/exercises/07_threads/05_channels
+/fxu https://github.com/mainmatter/100-exercises-to-learn-rust/tree/main/exercises/07_threads/06_interior_mutability
+/fxp https://github.com/mainmatter/100-exercises-to-learn-rust/tree/main/exercises/07_threads/07_ack
+/fxl https://github.com/mainmatter/100-exercises-to-learn-rust/tree/main/exercises/07_threads/08_client
+/fxk https://github.com/mainmatter/100-exercises-to-learn-rust/tree/main/exercises/07_threads/09_bounded
+/fxs https://github.com/mainmatter/100-exercises-to-learn-rust/tree/main/exercises/07_threads/10_patch
+/fxj https://github.com/mainmatter/100-exercises-to-learn-rust/tree/main/exercises/07_threads/11_locks
+/fxd https://github.com/mainmatter/100-exercises-to-learn-rust/tree/main/exercises/07_threads/12_rw_lock
+/fxc https://github.com/mainmatter/100-exercises-to-learn-rust/tree/main/exercises/07_threads/13_without_channels
+/fxa https://github.com/mainmatter/100-exercises-to-learn-rust/tree/main/exercises/07_threads/14_sync
+/f6f https://github.com/mainmatter/100-exercises-to-learn-rust/tree/main/exercises/08_futures/00_intro
+/f62 https://github.com/mainmatter/100-exercises-to-learn-rust/tree/main/exercises/08_futures/01_async_fn
+/f64 https://github.com/mainmatter/100-exercises-to-learn-rust/tree/main/exercises/08_futures/02_spawn
+/f6x https://github.com/mainmatter/100-exercises-to-learn-rust/tree/main/exercises/08_futures/03_runtime
+/f66 https://github.com/mainmatter/100-exercises-to-learn-rust/tree/main/exercises/08_futures/04_future
+/f68 https://github.com/mainmatter/100-exercises-to-learn-rust/tree/main/exercises/08_futures/05_blocking
+/f6b https://github.com/mainmatter/100-exercises-to-learn-rust/tree/main/exercises/08_futures/06_async_aware_primitives
+/f6q https://github.com/mainmatter/100-exercises-to-learn-rust/tree/main/exercises/08_futures/07_cancellation
+/f6e https://github.com/mainmatter/100-exercises-to-learn-rust/tree/main/exercises/08_futures/08_outro
+/ffz https://github.com/mainmatter/100-exercises-to-learn-rust/tree/solutions
+/fzd https://github.com/mainmatter/rust-advanced-testing-workshop
+/f69 https://github.com/rust-lang/rustlings
+/ffa https://huonw.github.io/blog/2016/04/myths-and-legends-about-integer-overflow-in-rust/
+/f4r https://lexi-lambda.github.io/blog/2019/11/05/parse-don-t-validate/
+/ff2 https://mainmatter.com/contact/
+/fff https://mainmatter.com/rust-consulting/
+/fxg https://marabos.nl/atomics/
+/f6p https://nostarch.com/rust-rustaceans
+/f2k https://owasp.org/www-community/vulnerabilities/Doubly_freeing_memory
+/f2s https://owasp.org/www-community/vulnerabilities/Using_freed_memory
+/ffn https://pavex.dev
+/ffp https://play.rust-lang.org/?version=stable&mode=debug&edition=2021&gist=36e5ddbe3b3f741dfa9f74c956622bac
+/fx5 https://play.rust-lang.org/?version=stable&mode=debug&edition=2021&gist=afedf7062298ca8f5a248bc551062eaa
+/ffx https://rust-exercises.com/100-exercises-to-learn-rust.pdf
+/ff4 https://rust-exercises.com/100-exercises/
+/f6s https://rust-exercises.com/advanced-testing/
+/f6h https://rust-exercises.com/telemetry/
+/fze https://rust-lang.github.io/api-guidelines/naming.html#casing-conforms-to-rfc-430-c-case
+/f6w https://rust-lang.github.io/wg-async/vision/submitted_stories/status_quo/barbara_battles_buffered_streams.html
+/f6v https://ryhl.io/blog/async-what-is-blocking/
+/f6n https://tokio.rs/tokio/tutorial/select
+/f2t https://valgrind.org/docs/manual/dh-manual.html
+/fz5 https://veykril.github.io/tlborm/
+/f67 https://without.boats/blog/the-scoped-task-trilemma/
+/ffv https://www.lpalmieri.com/
+/f4t https://www.lpalmieri.com/posts/2020-12-11-zero-to-production-6-domain-modelling/
+/f6y https://www.oreilly.com/library/view/programming-rust-2nd/9781492052586/
+/f6l https://www.youtube.com/playlist?list=PLqbS7AVVErFirH9armw8yXlE6dacF-A6z
+/ff8 https://zero2prod.com