diff --git a/content/posts/a-fresh-coat-of-paint.md b/content/posts/a-fresh-coat-of-paint.md
index 034c25c..03069a1 100644
--- a/content/posts/a-fresh-coat-of-paint.md
+++ b/content/posts/a-fresh-coat-of-paint.md
@@ -1,155 +1,58 @@
+++
-title = "A Fresh Coat of Paint"
-description = "It's time for something new."
-url = "posts/a-fresh-coat-of-paint"
-date = 2021-12-31T21:04:00-08:00
+title = "A Fresh Coat Of Paint"
+description = "Migrating to Zola"
+author = "Ian Andwati"
+date = 2023-08-24
[taxonomies]
-tags = ["webdev", "CSS", "HTML"]
+tags = ["zola", "blog", "tera", "rust"]
+++
-I'm starting the new year with a new job. To paraphrase a friend, "it's just
-moving from one `$BIGCORP` to another", but it's still exciting. I worked my
-last gig for 5 years, so I'm nervous, but also very ready to do something new.
-While I'm doing one new thing I might as well do another. Taking some time off
-between jobs has given me enough breathing room to redo my website.
+
+Using Zola as my preferred static site generator
+After using Hugo for over eight months I got tired of it and looked for other alternatives. That's when I stumbled upon Zola. Zola is a static site generator (SSG), similar to [Hugo](https://gohugo.io/), [Pelican](https://blog.getpelican.com/), and [Jekyll](https://jekyllrb.com/).
-## New Features
+It is written in Rust and uses the Tera template engine, similar to Jinja2, Django templates, Liquid, and Twig. SSGs use dynamic templates to transform content into static HTML pages. Static sites are thus very fast and require no databases, making them easy to host.
-If you've been here before you'll probably have noticed a significant visual
-overhaul. The site is now in dark mode, has a more varied color palette, and is
-more responsive to differently sized viewports.
+My previous Hugo site used a theme because I wasn't really ready to learn Go's template engine. I'm already familiar with Python Django and its template system so using Tera wasn't hard because it's similar to jinja2 and the Django template syntax.
-In addition to the visual changes on this site there are new features as well!
+Content is written in [CommonMark](https://commonmark.org/), a strongly defined, highly compatible specification of [Markdown](https://www.markdownguide.org/). Zola uses [pulldown-cmark](https://github.com/raphlinus/pulldown-cmark#pulldown-cmark) to parse markdown files. The goal of this library is 100% compliance with the CommonMark spec. It adds a few additional features such as parsing footnotes, Github-flavored tables, Github-flavored task lists, and strikethrough.
-1. [Posts] now have summaries thanks to Zola's [summary] feature, which allows
- you to use any content before a
- ``
- comment in a page's Markdown.
+Zola comes with built-in syntax highlighting, but you must first enable it in the configuration file.
-2. There are [tags] as well! These were actually always there, but I only put
- them in post [front matter] and didn't expose them on any pages. Tags are
- supported via Zola's [taxonomies], which are much more complicated and
- powerful than simple tags demand.
+Zola is very minimalistic, it is installed only as a single binary, with no extra dependencies needed. It is configured in `toml` and you only use what is needed. For example here is the configuration for this site `config.toml`:
-3. The metadata for site pages now includes [Open Graph protocol] and
- [Twitter card] support for a better display in social media.
+```toml
+title = "Ian Andwati"
+description = "Ian Andwati's personal website"
+base_url = "https://andwati.github.io"
-## What I Learned
+generate_categories_pages = false
+generate_tags_pages = false
+generate_feed = true
+feed_filename = "rss.xml"
+minify_html = false
+taxonomies = [{ name = 'tags', feed = false }]
+compile_sass = true
-Every time I update this website I learn something new. I continued to use
-vanilla HTML and CSS and eschew JavaScript, but was still blown away by how
-little I know in the webdev space.
+[markdown]
+highlight_code = true
+highlight_theme = "gruvbox-dark"
-In the interest of chronicling my newfound knowledge, here are a handful of the
-things I learned.
+[link_checker]
-- There are a whole lot of [semantic] HTML tags! I was already using tags like
- ``, ``, and `
` tag of our website’s main index page. This script allows the user to redirect back to the `/admin/` path after completing the login with the Netlify Identity Widget:
+
+```html
+
+```
+
+**Step 5**
+
+Now we’re all set. Since we set our registration preferences to **“Invite Only”**, we need to invite ourselves as site users. To do this, select the Identity tab from the Netlify site dashboard and select the Invite Users button. Add your email address and send an invitation. You will receive an email. In your email, click **“accept the invitation”**. You will be directed to your website admin sign-up panel. Choose a password. Now you are ready to log in to Netlify CMS.
+
+# Setting up a custom domain
+
+I bought a custom domain from [Namecheap](https://namecheap.com), they provide some pretty good deals. After pointing the nameservers for the domain to the Netlify nameservers, voila! everything worked smoothly. For HTTPS support, Netlify enables automatic TLS certificates with Let’s Encrypt.
diff --git a/content/posts/javascript-exporting-modules.md b/content/posts/javascript-exporting-modules.md
new file mode 100644
index 0000000..6edba96
--- /dev/null
+++ b/content/posts/javascript-exporting-modules.md
@@ -0,0 +1,188 @@
++++
+title = "Export default and module.exports in Javascript"
+description = "Modules are the building blocks of modern JavaScript applications, allowing for the encapsulation of code into reusable components. Two primary systems govern the exportation of these modules: export default in ES6 and module.exports in CommonJS. "
+author = "Ian Andwati"
+date = 2024-01-17
+[taxonomies]
+tags = ["javascript", "node"]
++++
+
+Javascript is a popular programming language that can run both in the browser and on the server. However, different environments may have different ways of organizing and importing Javascript modules. In this article, we will compare two common methods of exporting and importing modules: **export default** and **module.exports**.
+
+
+## What are modules?
+Modules are reusable pieces of code that can be separated into different files. Modules can export values, functions, classes, or objects that can be imported by other modules. This way, modules can avoid polluting the global scope and enable code reuse and modularity.
+
+## How to export modules in Node.js?
+Node.js is a runtime environment that allows Javascript to run on the server. Node.js uses a module system called CommonJS, which is based on the require and module.exports syntax.
+
+To export a module in Node.js, you can assign the value that you want to export to the module.exports object. For example:
+
+```javascript
+// my-module.js
+// export a function
+module.exports = function add(a, b) {
+ return a + b;
+};
+
+// export an object
+module.exports = {
+ name: "Alice",
+ age: 25,
+};
+```
+
+To import a module in Node.js, you can use the require function and pass the path of the module file as an argument. The require function will return the value of module.exports from the module file. For example:
+
+```javascript
+// main.js
+// import a function
+const add = require("./my-module.js");
+console.log(add(2, 3)); // 5
+
+// import an object
+const person = require("./my-module.js");
+console.log(person.name); // Alice
+console.log(person.age); // 25
+```
+
+Note that Node.js will cache the modules that are imported with require, so if you import the same module multiple times, you will get the same instance of the module.
+
+## How to export modules in ES6?
+ES6 is the version of Javascript that introduced many new features, including a native module system. ES6 modules use the `import` and `export` syntax.
+
+To export a module in ES6, you can use the `export` keyword followed by the declaration of the value that you want to export. You can also use the export keyword with curly braces to export a list of named values. For example:
+
+```javascript
+// my-module.js
+// export a function
+export function add(a, b) {
+ return a + b;
+}
+
+// export a class
+export class Person {
+ constructor(name, age) {
+ this.name = name;
+ this.age = age;
+ }
+}
+
+// export an object
+export const person = {
+ name: "Alice",
+ age: 25,
+};
+```
+
+To import a module in ES6, you can use the `import` keyword followed by the name of the value that you want to import and the path of the module file. You can also use the import keyword with curly braces to import a list of named values. For example:
+
+```javascript
+// main.js
+// import a function
+import { add } from "./my-module.js";
+console.log(add(2, 3)); // 5
+
+// import a class
+import { Person } from "./my-module.js";
+const person = new Person("Bob", 30);
+console.log(person.name); // Bob
+console.log(person.age); // 30
+
+// import an object
+import { person as Alice } from "./my-module.js";
+console.log(Alice.name); // Alice
+console.log(Alice.age); // 25
+```
+Note that ES6 modules are imported and executed only once, so if you import the same module multiple times, you will get the same instance of the module.
+
+## What is the difference between export default and module.exports?
+One of the main differences between export default and module.exports is that export default is an ES6 feature, while module.exports is a CommonJS feature. Therefore, they are not compatible with each other by default. However, some tools like Babel or Webpack can transpile ES6 modules to CommonJS modules, or vice versa, to make them work together.
+
+Another difference is that export default can only export one value per module, while module.exports can export any value, including multiple values. For example:
+
+```javascript
+// my-module.js
+// export default a function
+export default function add(a, b) {
+ return a + b;
+}
+
+// export default an object
+export default {
+ name: "Alice",
+ age: 25,
+};
+
+// export default a class
+export default class Person {
+ constructor(name, age) {
+ this.name = name;
+ this.age = age;
+ }
+}
+```
+
+```javascript
+// my-module.js
+// export a function
+module.exports = function add(a, b) {
+ return a + b;
+};
+
+// export an object
+module.exports = {
+ name: "Alice",
+ age: 25,
+};
+
+// export a class
+module.exports = class Person {
+ constructor(name, age) {
+ this.name = name;
+ this.age = age;
+ }
+};
+```
+
+Note that you can only have one export default statement per module, but you can have multiple module.exports statements. However, each module.exports statement will overwrite the previous one, so only the last one will be exported.
+
+To import a default export, you can use the import keyword without curly braces and assign any name to the imported value. For example:
+
+```javascript
+// main.js
+// import a default export
+import add from "./my-module.js";
+console.log(add(2, 3)); // 5
+
+// import another default export
+import person from "./my-module.js";
+console.log(person.name); // Alice
+console.log(person.age); // 25
+
+// import another default export
+import Person from "./my-module.js";
+const person = new Person("Bob", 30);
+console.log(person.name); // Bob
+console.log(person.age); // 30
+```
+
+To import a module.exports value, you can use the require function and assign any name to the imported value. For example:
+```javascript
+// main.js
+// import a module.exports value
+const add = require("./my-module.js");
+console.log(add(2, 3)); // 5
+
+// import another module.exports value
+const person = require("./my-module.js");
+console.log(person.name); // Alice
+console.log(person.age); // 25
+
+// import another module.exports value
+const Person = require("./my-module.js");
+const person = new Person("Bob", 30);
+console.log(person.name); // Bob
+console.log(person.age); // 30
+```
+Node’s JavaScript has diverged from that of the browser. Understanding both CommonJS modules and ES6 modules is your first step in becoming a 10X developer. Happy coding!
\ No newline at end of file
diff --git a/content/posts/node-example-com-is-an-ip-address/confused-jeff-bridges.webp b/content/posts/node-example-com-is-an-ip-address/confused-jeff-bridges.webp
deleted file mode 100644
index 2de2c7b..0000000
Binary files a/content/posts/node-example-com-is-an-ip-address/confused-jeff-bridges.webp and /dev/null differ
diff --git a/content/posts/node-example-com-is-an-ip-address/index.md b/content/posts/node-example-com-is-an-ip-address/index.md
deleted file mode 100644
index dd63be1..0000000
--- a/content/posts/node-example-com-is-an-ip-address/index.md
+++ /dev/null
@@ -1,608 +0,0 @@
-+++
-title = "node.example.com Is An IP Address"
-description = "node.example.com is an IP address."
-url = "posts/node-example-com-is-an-ip-address"
-date = 2020-12-28T06:13:05-08:00
-[taxonomies]
-tags = ["Python", "Testing", "Networking"]
-+++
-Hello! Welcome to the once-yearly blog post! This year I'd like to examine the
-most peculiar bug I encountered at work. To set the stage, let's start with a
-little background.
-
-
-
-When we write [URLs][URL] with a [non-standard][standard ports] [port] we
-specify the port after a `:`. With [hostnames][hostname] and [IPv4] addresses
-this is straightforward. Here's some [Python] code to show how easy it is.
-
-```python
->>> url = urllib.parse.urlparse("https://node.example.com:8000")
->>> (url.hostname, url.port)
-('node.example.com', 8000)
->>>
->>> url = urllib.parse.urlparse("https://192.168.0.1:8000")
->>> (url.hostname, url.port)
-('192.168.0.1', 8000)
-```
-
-Unfortunately, when [IPv6] addresses are involved some ambiguity is introduced.
-
-```python
->>> url = urllib.parse.urlparse(
-... "https://fdc8:bf8b:e62c:abcd:1111:2222:3333:4444:8000"
-... )
-...
->>> url.hostname
-'fdc8'
->>> try:
-... url.port
-... except ValueError as error:
-... print(error)
-...
-Port could not be cast to integer value as 'bf8b:e62c:abcd:1111:2222:3333:4444:8000'
-```
-
-Since IPv6 addresses use a "colon-hex" format with [hexadecimal] fields
-separated by `:` we can't tell a port apart from a normal field. Notice in the
-example above that the hostname is truncated after the first `:`, not the one
-just before `8000`.
-
-Fortunately, the spec for URLs recognizes this ambiguity and gives us a way to
-handle it. [RFC 2732 (_Format for Literal IPv6 Addresses in URL's_)][RFC 2732]
-says
-
-> To use a literal IPv6 address in a URL, the literal address should be
-> enclosed in "[" and "]" characters.
-
-Update our example above to include `[` and `]` and voilà! It just works.
-
-```python
->>> url = urllib.parse.urlparse(
-... "https://[fdc8:bf8b:e62c:abcd:1111:2222:3333:4444]:8000"
-... )
-...
->>> (url.hostname, url.port)
-('fdc8:bf8b:e62c:abcd:1111:2222:3333:4444', 8000)
-```
-
-Armed with that knowledge we can dive into the problem. 🤿
-
-## Works On My Machine
-
-A few months ago a co-worker of mine wrote a seemingly innocuous function.
-
-```python
-from ipaddress import ip_address
-
-
-def safe_host(host):
- """Surround `host` with brackets if it is an IPv6 address."""
- try:
- if ip_address(host).version == 6:
- return "[{}]".format(host)
- except ValueError:
- pass
- return host
-```
-
-Elsewhere in the code it was invoked something like this, so that hostnames,
-IPv4 addresses, and IPv6 addresses could all be safely interpolated.
-
-```python
-url = "https://{host}:8000/some/path/".format(host=safe_host(host))
-```
-
-Since my co-worker is awesome they wrote tests to validate their code. ✅
-
-```python
-def test_safe_host_with_hostname():
- """Hostnames should be unchanged."""
- assert safe_host("node.example.com") == "node.example.com"
-
-
-def test_safe_host_with_ipv4_address():
- """IPv4 addresses should be unchanged."""
- assert safe_host("192.168.0.1") == "192.168.0.1"
-
-
-def test_safe_host_with_ipv6_address():
- """IPv6 addresses should be surrounded by brackets."""
- assert (
- safe_host("fdc8:bf8b:e62c:abcd:1111:2222:3333:4444")
- == "[fdc8:bf8b:e62c:abcd:1111:2222:3333:4444]"
- )
-```
-
-Thank goodness they did. The Python 2 tests failed ([don't look at me like
-that][drop-python2] 😒).
-
-```ini
-✖ FAIL py27 in 1.83 seconds
-✔ OK py36 in 2.82 seconds
-✔ OK py37 in 2.621 seconds
-✔ OK py38 in 2.524 seconds
-✔ OK py39 in 2.461 seconds
-```
-
-Both the hostname and IPv6 address tests failed. But _**why**_ did they fail?
-And why did the Python 3 tests pass? 🤔
-
-We'll start with the hostname failure and try to isolate the bug.
-
-```python
-E AssertionError: assert '[node.example.com]' == 'node.example.com'
-E - [node.example.com]
-E ? - -
-E + node.example.com
-```
-
-The failure says `node.example.com` was surrounded by brackets, but that's
-only supposed to happen for IPv6 addresses! Let's crack open a Python 2
-interpreter for a quick sanity check.
-
-```python
->>> ipaddress.ip_address("node.example.com").version
-6
-```
-
-
-
-## What On Htrae?
-
-If, like Jeff Bridges, you were confused by that result, _relax_. We're
-probably not in a [Bizarro World] where `node.example.com` is a valid IPv6
-address. There must be an explanation for this behavior.
-
-Things start to become a little more clear when we see the result of the
-[`ip_address()`][ip_address] function for ourselves.
-
-```python
->>> ipaddress.ip_address("node.example.com")
-IPv6Address(u'6e6f:6465:2e65:7861:6d70:6c65:2e63:6f6d')
-```
-
-At first glance that looks like madness. Python 3 behaves in an entirely
-different manner.
-
-```python
->>> try:
-... ipaddress.ip_address("node.example.com")
-... except ValueError as error:
-... print(error)
-...
-'node.example.com' does not appear to be an IPv4 or IPv6 address
-```
-
-Python 3 knows that's not an IPv6 address, so why doesn't Python 2? The answer
-is in how differently the two Python versions handle text.
-
-## Text Is Hard
-
-Computers don't operate on text as humans think of it. They operate on numbers.
-That's part of why we have IP addresses to begin with. In order to represent
-human-readable text with computers we had to assign meaning to the numbers.
-Thus, [ASCII] was born.
-
-ASCII is a [character encoding], which means it specifies how to interpret
-[bytes][byte] as text we understand (provided you speak English). So, when your
-computer sees `01101110` in [binary] (`110` in [decimal]) you see `n` because
-that's what ASCII says it is.
-
-You can see the number to text conversion in action right in the Python
-interpreter.
-
-```python
->>> ord("n")
-110
->>> chr(110)
-'n'
-```
-
-In fact, it doesn't matter what numbering system you use. If you specify
-binary, [octal], decimal, hexadecimal, whatever... If it can be understood as
-the right integer it will be displayed correctly.
-
-```python
->>> chr(0b01101110)
-'n'
->>> chr(0o156)
-'n'
->>> chr(110)
-'n'
->>> chr(0x6e)
-'n'
-```
-
-Neat, but what does that information do for us?
-
-## It's Numbers All The Way Down
-
-Just for giggles, humor me and let's look at the character-number translations
-for `node.example.com`. We'll leave out binary and octal, because they make
-this table uglier than it already is.
-
-
-
-
-
Character
-
n
-
o
-
d
-
e
-
.
-
e
-
x
-
a
-
m
-
p
-
l
-
e
-
.
-
c
-
o
-
m
-
-
-
Decimal
-
110
-
111
-
100
-
101
-
46
-
101
-
120
-
97
-
109
-
112
-
108
-
101
-
46
-
99
-
111
-
109
-
-
-
Hexadecimal
-
6e
-
6f
-
64
-
65
-
2e
-
65
-
78
-
61
-
6d
-
70
-
6c
-
65
-
2e
-
63
-
6f
-
6d
-
-
-
-
-Hey, hold on a second... If you tilt your head sideways and squint that last
-row looks kinda like an IPv6 address, doesn't it?
-
-We should verify, just to be absolutely certain. You've still got that Python 2
-interpreter open, right?
-
-```python
->>> # Convert the characters in the hostname to hexadecimal.
->>> hostname = "node.example.com"
->>> hostname_as_hexadecimal = "".join(hex(ord(c))[2:] for c in hostname)
->>> hostname_as_hexadecimal
-'6e6f64652e6578616d706c652e636f6d'
->>>
->>> # Convert the "IP address" to text.
->>> address = ipaddress.ip_address(hostname)
->>> str(address)
-'6e6f:6465:2e65:7861:6d70:6c65:2e63:6f6d'
->>>
->>> # Remove the colons from that text.
->>> address_without_colons = str(address).replace(":", "")
->>> address_without_colons
-'6e6f64652e6578616d706c652e636f6d'
->>>
->>> # Compare the results and see they're equal.
->>> hostname_as_hexadecimal == address_without_colons
-True
-```
-
-Sure enough, when you boil them both down to numbers they're the same mess of
-hexadecimal.
-
-## The Belly Of The Beast
-
-If we dig into the source code for the Python 2 version of the
-[`ipaddress`][ipaddress2] module we ultimately come to a
-[curious set of lines].
-
-```python
-# Constructing from a packed address
-if isinstance(address, bytes):
- self._check_packed_address(address, 16)
- bvs = _compat_bytes_to_byte_vals(address)
- self._ip = _compat_int_from_byte_vals(bvs, 'big')
- return
-```
-
-It turns out that, under certain conditions, the `ipaddress` module can create
-IPv6 addresses from raw bytes. My assumption is that it offers this behavior as
-a convenient way to parse IP addresses from data fresh off the [wire].
-
-Does `node.example.com` meet those certain conditions? You bet it does. Because
-we're using Python 2 it's just `bytes` and it happens to be 16 characters long.
-
-```python
->>> isinstance("node.example.com", bytes)
-True
->>> # `self._check_packed_address` basically just checks how long it is.
->>> len("node.example.com") == 16
-True
-```
-
-The rest of the `ipaddress` lines say to interpret the sequence of bytes as a
-[big-endian][endianness] integer. That's [magic][struct unpacking] best left
-for another blog post, but the gist is that hexadecimal interpretation of
-`node.example.com` is condensed into a single, **huge** number.
-
-```python
->>> int("6e6f64652e6578616d706c652e636f6d", 16)
-146793460745001871434687145741037825901L
-```
-
-That's an absolutely massive number, but not so massive it won't fit within the
-[IPv6 address space].
-
-```python
->>> ip_address(146793460745001871434687145741037825901L)
-IPv6Address(u'6e6f:6465:2e65:7861:6d70:6c65:2e63:6f6d')
-```
-
-As it turns out, if you're liberal in your interpretation, `node.example.com`
-_can_ be an IPv6 address!
-
-## You Will Be Reading Meanings
-
-Obviously that's hogwash. Bizarro might be proud, but that's not what we wanted
-to happen.
-
-There's a quote about numbers which is apocryphally attributed to [W.E.B. Du
-Bois], but that actually comes from [Harold Geneen]'s book,
-[_Managing_][Managing].
-
-> When you have mastered the numbers, you will in fact no longer be reading
-> numbers, any more than you read words when reading a book. You will be
-> reading meanings.
-
-Having not read the book I'm probably taking the quote way out of context, but
-I think it fits our situation well.
-
-As we've seen above, we can freely convert characters to numbers and back
-again. The root of our problem is that when we use Python 2 it considers text
-to be bytes. There's not a deeper, inherent meaning. Maybe the bytes are meant
-to be ASCII, maybe they're meant to be a long number, maybe they're meant to be
-an IP address. The interpretation of those bytes is up to us.
-
-Python 2 doesn't differentiate between bytes and text by default. In fact, the
-`bytes` type is just an [alias][bytes-str-alias] for `str`.
-
-```python
->>> bytes
-
->>> bytes is str
-True
-```
-
-To make that even more concrete, see how Python 2 considers `n` to be the same
-as this sequence of raw bytes.
-
-```python
->>> "n" == b"\x6e"
-True
-```
-
-Our Python 2 code doesn't work the way we want it to because raw bytes can have
-arbitrary meaning and we haven't told it to use our intended meaning.
-
-So now we know why Python 2 interprets `node.example.com` as an IPv6 address,
-but why does Python 3 behave differently? More importantly, how can we
-reconcile the two?
-
-
-## 256 Characters Ought To Be Enough For Anybody
-
-ASCII looked like a good idea in the 1960's. With decades of hindsight we
-know the 256 characters afforded to us by [Extended ASCII] are insufficient to
-handle all of the world's writing systems. Thus, [Unicode] was born.
-
-There are scads of blog posts, Wikipedia articles, and technical documents that
-will do a better job than I can of explaining Unicode in detail. You should
-read them if you care to, but here's my gist.
-
-Unicode is a set of character encodings. [UTF-8] is the dominant encoding.
-UTF-8 overlaps with ASCII, so ASCII characters are still just one byte. To
-handle the multitude of other characters, however, multiple bytes can express a
-single character.
-
-```python
->>> "n".encode("utf-8").hex() # 1 character (U+006E), 1 byte.
-'6e'
->>> "🤿".encode("utf-8").hex() # 1 character (U+1F93F), 4 bytes.
-'f09fa4bf'
->>> "悟り".encode("utf-8").hex() # 2 characters (U+609F, U+308A), 6 bytes.
-'e6829fe3828a'
-```
-
-Every programming language I know of that respects the difference between
-raw bytes and Unicode text maintains a strict separation between the two
-datatypes.
-
-In Python 3 this strict separation is enabled by default. Notice that it
-doesn't consider `n` and this sequence of raw bytes to be the same thing.
-
-```python
->>> "n" == b"\x6e"
-False
-```
-
-Even better, it doesn't consider `str` and `bytes` to be the same type.
-
-```python
->>> bytes is str
-False
->>> bytes
-
-```
-
-If we can get Python 2 to understand Unicode like Python 3 does, then we can
-probably fix our bug.
-
-As an aside, if you want to learn more about how to handle Unicode in Python,
-check out [Ned Batchelder]'s talk on [_Pragmatic Unicode_][unipain].
-
-## How Did We Fix It?
-
-Python 2 does actually know about Unicode, but it considers Unicode text to
-be separate from "normal" text. At some point in Python 2 history the
-[`unicode`][python2-unicode] type was bolted onto the side of the language and
-not enabled by default. Hard to get excited about it, but it does the trick.
-At least they knew it's a pain to type `unicode()` all the time, so there's a
-handy literal syntax using a `u` prefix.
-
-```python
->>> unicode("node.example.com") == u"node.example.com"
-True
-```
-
-This is _not_ the best fix, but it did in a pinch. We added a line converting
-the hostname to Unicode right off the bat. We also applied the same
-transformation to the line with brackets. This way we always process the
-hostname as Unicode and we always return a Unicode value.
-
-```diff
- def safe_host(host):
- """Surround `host` with brackets if it is an IPv6 address."""
-+ host = u"{}".format(host)
- try:
- if ip_address(host).version == 6:
-- return "[{}]".format(host)
-+ return u"[{}]".format(host)
- except ValueError:
- pass
-```
-
-Luckily for us the `u` prefix also works in Python 3 whereas `unicode()` does not
-(because [all text is Unicode by default][python3-str-type], so the type has no
-business existing). In Python 3 the `u` is treated as a [no-op].
-
-The Python 2 interpreter graciously understands the `unicode` type is not just
-raw `bytes`.
-
-```python
->>> isinstance(u"node.example.com", bytes)
-False
-```
-
-When we use the `unicode` type the `ipaddress` module no longer tries to
-interpret `node.example.com` as `bytes` and convert those bytes to an IP
-address. We get just what we expect
-
-```python
->>> try:
-... ipaddress.ip_address(u"node.example.com")
-... except ValueError as error:
-... print(error)
-...
-u'node.example.com' does not appear to be an IPv4 or IPv6 address
-```
-
-and our tests pass!
-
-```ini
-✔ OK py27 in 1.728 seconds
-✔ OK py36 in 2.775 seconds
-✔ OK py37 in 2.717 seconds
-✔ OK py38 in 2.674 seconds
-✔ OK py39 in 2.506 seconds
-```
-
-## Reflection
-
-I mentioned above that our fix wasn't the best. Given more time, how can we do
-better?
-
-
The first (and best) solution here is to
-drop Python 2 support. It's 2020
-now and Python 2 is officially no longer supported. The original code worked on
-Python 3. The best long-term decision is to migrate the code to run on Python 3
-only and avoid the hassle of Python 2 maintenance. Unfortunately many of
-the people running this code still depend on it working on Python 2, so we'll
-have to make that transition gracefully.
-
-If a migration away from Python 2 isn't possible in the near-term, the next
-best thing to do is update our code so that it uses a compatibility layer like
-[`future`][future] or [`six`][six]. Those libraries are designed to modernize
-Python 2 and help smooth over issues like this one.
-
-It also wouldn't hurt for us to take a page from [Alexis King]'s
-[_Parse, don't validate_][parse-dont-validate] school of thought. When the
-hostname enters our program via user input it should **immediately** be
-converted to the `unicode` type (or maybe even an IP address type) so we don't
-end up solving this problem in several different places throughout the code.
-
-Finally, though our program doesn't currently handle any hostnames in languages
-other than English, it's probably best to be thinking in Unicode anyway. Again,
-it's 2020 and [internationalized domain names] like [https://Яндекс.рф][yandex]
-are a thing.
-
-If you made it this far, thanks for reading. It was fun to turn a brief
-debugging session with my co-worker into a treatise on the perils of Python 2
-and the value of Unicode. See you next year! 😂
-
-[URL]: https://en.wikipedia.org/wiki/URL
-[standard ports]: https://en.wikipedia.org/wiki/List_of_TCP_and_UDP_port_numbers
-[port]: https://en.wikipedia.org/wiki/Port_(computer_networking)
-[hostname]: https://en.wikipedia.org/wiki/Hostname
-[IPv4]: https://en.wikipedia.org/wiki/IPv4#Addressing
-[Python]: https://www.python.org/
-[IPv6]: https://en.wikipedia.org/wiki/IPv6#Addressing
-[hexadecimal]: https://en.wikipedia.org/wiki/Hexadecimal
-[RFC 2732]: https://www.ietf.org/rfc/rfc2732.txt
-[drop-python2]: /#drop-python2
-[Bizarro World]: https://en.wikipedia.org/wiki/Bizarro_World
-[ip_address]: https://github.com/python/cpython/blob/v3.9.0/Lib/ipaddress.py#L27-L54
-[ASCII]: https://en.wikipedia.org/wiki/ASCII
-[character encoding]: https://en.wikipedia.org/wiki/Character_encoding
-[byte]: https://en.wikipedia.org/wiki/Byte
-[binary]: https://en.wikipedia.org/wiki/Binary_number
-[decimal]: https://en.wikipedia.org/wiki/Decimal
-[octal]: https://en.wikipedia.org/wiki/Octal
-[W.E.B. Du Bois]: https://en.wikipedia.org/wiki/W._E._B._Du_Bois
-[Harold Geneen]: https://en.wikipedia.org/wiki/Harold_Geneen
-[Managing]: https://en.wikiquote.org/wiki/Harold_Geneen
-[bytes-str-alias]: https://docs.python.org/3/whatsnew/2.6.html#pep-3112-byte-literals
-[Extended ASCII]: https://en.wikipedia.org/wiki/ASCII#8-bit_codes
-[Unicode]: https://en.wikipedia.org/wiki/Unicode
-[UTF-8]: https://en.wikipedia.org/wiki/UTF-8
-[Ned Batchelder]: https://nedbatchelder.com/site/aboutned.html
-[unipain]: https://nedbatchelder.com/text/unipain.html
-[no-op]: https://en.wikipedia.org/wiki/NOP_(code)
-[ipaddress2]: https://github.com/phihag/ipaddress/blob/v1.0.23/ipaddress.py
-[curious set of lines]: https://github.com/phihag/ipaddress/blob/v1.0.23/ipaddress.py#L2026-L2031
-[wire]: https://en.wikipedia.org/wiki/Wire_data
-[endianness]: https://en.wikipedia.org/wiki/Endianness
-[struct unpacking]: https://docs.python.org/3.9/library/struct.html#struct.unpack
-[IPv6 address space]: https://en.wikipedia.org/wiki/IPv6#Larger_address_space
-[python2-unicode]: https://docs.python.org/2.7/library/functions.html#unicode
-[python3-str-type]: https://docs.python.org/3/library/stdtypes.html#text-sequence-type-str
-[future]: https://python-future.org/
-[six]: https://six.readthedocs.io/
-[Alexis King]: https://lexi-lambda.github.io/about.html
-[parse-dont-validate]: https://lexi-lambda.github.io/blog/2019/11/05/parse-don-t-validate/
-[internationalized domain names]: https://en.wikipedia.org/wiki/Internationalized_domain_name
-[yandex]: https://Яндекс.рф
diff --git a/content/posts/obsidian-sync.md b/content/posts/obsidian-sync.md
new file mode 100644
index 0000000..9804ade
--- /dev/null
+++ b/content/posts/obsidian-sync.md
@@ -0,0 +1,120 @@
++++
+title = "Setting up Obsidian sync in Windows"
+description = "Syncing Obsidian in Windows with Windows Task Scheduler with git"
+author = "Ian Andwati"
+date = 2023-10-01
+[taxonomies]
+tags = ["obsidian", "git","sync", "windows", "cron", "task scheduler", "powershell"]
++++
+
+Syncing Obsidian in Windows with Windows Task Scheduler and git
+
+
+
+[Obsidian](https://obsidian.md/) is a powerful note-taking and knowledge management tool that has gained a dedicated following among researchers, writers, and knowledge workers. To make the most of Obsidian, it's essential to keep your notes synchronized across devices and ensure your data is regularly backed up.Obsidian is the private and flexible writing app that adapts to the way you think.
+
+Obsidian is free for personal use.It does not charge based on features or usage.You only pay if you use Obsidian commercially.
+
+In this article, we'll guide you through the process of setting up automated synchronization for your Obsidian vault on Windows using Windows Task Scheduler and a PowerShell script.[^note]
+
+## Prerequisites
+
+Before we dive into setting up the synchronization task, ensure you have the following prerequisites in place:
+
+1. **Obsidian Installed:** Make sure Obsidian is [installed](https://obsidian.md/download) on your Windows computer, and you have an existing Obsidian vault that you want to sync.
+
+1. **Git Installed:** You'll need Git installed and configured with your Obsidian vault's repository. If you haven't already done this, you can download Git from the [official website](https://git-scm.com/downloads) and follow the installation instructions.
+
+1. **PowerShell Script:** The provided PowerShell script, sync.ps1, should be located in your Obsidian vault folder.
+
+## Understanding the PowerShell Script
+
+The provided PowerShell script, sync.ps1, is the heart of our synchronization task. It automates the process of committing changes in your Obsidian vault's Git repository and pushing them to your remote repository (e.g., GitHub or GitLab). Here's a brief overview of the script:
+
+```ps1
+# Define the folder path
+$FolderPath = "C:\path\to\obsidian-vault"
+
+# Change directory to the repository folder
+Set-Location -Path $FolderPath
+
+# Check if the working tree is clean
+$IsClean = (git diff-index --quiet HEAD --) -or (git diff-files --quiet)
+
+if ($IsClean) {
+ Write-Host "Working tree is clean. Skipping commit and pushing changes."
+ git push
+} else {
+ # Get the current date in the desired format (Day - Date)
+ $CommitMessage = "$(Get-Date -Format 'dddd - yyyy-MM-dd HH:mm:ss')"
+
+ # Commit the changes
+ git add .
+ git commit -m $CommitMessage
+
+ # Push the changes
+ git push
+}
+```
+
+This script does the following:
+
+- Sets the folder path to your Obsidian vault.
+- Checks if there are any changes to commit. If the vault is clean (no changes), it skips the commit and only pushes changes.
+- If there are changes, it commits them with a timestamped message and then pushes them to the remote repository.
+
+## Setting up Windows Task Scheduler
+
+Now that we have our PowerShell script ready, let's set up a scheduled task using Windows Task Scheduler to automate the synchronization process. Follow these steps:
+
+1. **Open an Elevated PowerShell Prompt:** To run the necessary commands, open PowerShell as an administrator.
+
+1. **Define Task Variables:** Execute the following commands to define variables for your task:
+
+ ```ps1
+ $ScriptPath = "C:\path\to\obsidian-vault\sync.ps1"
+ $TaskName = "obsidian-sync"
+ $UserAccount = "$env:USERDOMAIN\$env:USERNAME"
+ ```
+
+ Replace $ScriptPath with the actual path to your sync.ps1 script, $TaskName with a name for your task.
+
+1. **Configure Task Trigger:** Set up a trigger for your task. In this example, we'll schedule the task to run daily at 9:00 PM:
+
+ ```ps1
+ $Trigger = New-ScheduledTaskTrigger -Daily -At '9:00 PM'
+ ```
+
+ You can adjust the schedule according to your preference.
+
+1. Define Task Principal and Settings: Configure the task's principal (the user account under which the task will run) and settings:
+
+ ```ps1
+ $Principal = New-ScheduledTaskPrincipal -UserId $UserAccount -RunLevel Highest
+ $Settings = New-ScheduledTaskSettingsSet -AllowStartIfOnBatteries -RunOnlyIfNetworkAvailable -WakeToRun
+ ```
+
+ These settings ensure that the task runs with the necessary privileges and conditions.
+
+1. **Define Task Action:** Specify the action that the task will perform, which is running the PowerShell script:
+
+ ```ps1
+ $Action = New-ScheduledTaskAction -Execute 'PowerShell.exe' -Argument "-ExecutionPolicy Bypass -WindowStyle Hidden -File `"$ScriptPath`""
+ ```
+
+ This action runs PowerShell with the provided script.
+
+1. **Create the Scheduled Task:** Finally, create the scheduled task and register it with Windows Task Scheduler:
+
+ ```ps1
+ $Task = New-ScheduledTask -Action $Action -Trigger $Trigger -Principal $Principal -Settings $Settings
+ Register-ScheduledTask -TaskName $TaskName -InputObject $Task
+ ```
+
+The Obsidian synchronization task is now set up and ready to run automatically based on the schedule you defined.
+
+# Inspiration
+
+When looking up on how to implement this I found some breakthrough from this [stack overflow](https://stackoverflow.com/questions/7195503/setting-up-a-cron-job-in-windows) post. There are suggestions on how to do this using cron jobs in wsl. They are worth a look at. This article on [powershell](https://learn.microsoft.com/en-us/powershell/module/scheduledtasks/new-scheduledtask?view=windowsserver2022-ps) with windows server was also helpful.
+
+[^note]: This method is only optimal for desktop sync. To sync across devices obsidian has a paid plan that has some extra features Such as Sync notes across devices, End-to-end encryption, Version history, Priority email support. Consider checking it out at [https://obsidian.md/pricing](https://obsidian.md/pricing)
diff --git a/content/posts/parsing-tftp-in-rust/index.md b/content/posts/parsing-tftp-in-rust/index.md
deleted file mode 100644
index 9177a0f..0000000
--- a/content/posts/parsing-tftp-in-rust/index.md
+++ /dev/null
@@ -1,1000 +0,0 @@
-+++
-title = "Parsing TFTP in Rust"
-description = "Parsing TFTP in Rust"
-url = "posts/parsing-tftp-in-rust"
-date = 2022-12-31T16:45:00-08:00
-[taxonomies]
-tags = ["Rust", "TFTP", "Networking", "Parsing", "nom"]
-+++
-Several years ago I did a take-home interview which asked me to write a [TFTP]
-server in [Go]. The job wasn't the right fit for me, but I enjoyed the
-assignment. Lately, in my spare time, I've been tinkering with a [Rust]
-implementation. Here's what I've done to parse the protocol.
-
-
-
-## Caveat Lector
-
-It's natural to write a technical blog post like this in a somewhat
-authoritative tone. However, I am not an authority. There will be mistakes.
-Techniques, libraries, and even protocols change over time. Keep in mind that I
-am learning too and will happily accept corrections and critiques.
-
-## Why Rust?
-
-Much has been written on the merits of Rust by more qualified people. I
-encourage you to seek their writing and make your own decisions. For my part, I
-try my best to write fast, safe, and correct code. Rust lets me be more
-confident about my solutions without the baggage (and danger) of the last 40
-years of C/C++. Recent [statements][memory-safe-android] and
-[events][rust-linux-kernel] would seem to agree.
-
-If you know me, you might be surprised that this is my first post on Rust since
-I've been hyping up the language for the last 7 years. Better late than never.
-😂
-
-## What Is TFTP?
-
-If you already know the ins and outs of TFTP feel free to skip to the
-[type design](#type-design) or [parsing](#parsing) sections.
-
-For those who don't know, TFTP is the [Trivial File Transfer Protocol][TFTP], a
-simple means of reading and writing files over a network. Initially defined in
-the early 80s, the protocol was updated by [RFC 1350] in 1992. In this post
-I'll only cover RFC 1350. Extensions like [RFC 2347], which adds a 6th packet
-type, won't be covered.
-
-### Security
-
-TFTP is _not_ a secure protocol. It offers no access controls, no
-authentication, no encryption, nothing. If you're running a TFTP server assume
-that any other host on the network can read the files hosted by it. You should
-not run a TFTP server on the open Internet.
-
-### Why Use TFTP?
-
-If TFTP is old, insecure, and protocols like [HTTP] & [SSH] exist, you
-might wonder why you'd even bother. Fair enough. If you have other options, you
-probably don't need to use it.
-
-That said, TFTP is still widely used, especially in server and lab environments
-where there are closed networks. Combined with [DHCP] and [PXE] it provides an
-efficient means of [network booting] due to its small memory footprint. This is
-especially important for embedded devices where memory is scarce. Additionally,
-if your server supports the experimental [multicast] option with [RFC 2090],
-files can be read by multiple clients concurrently.
-
-## Protocol Overview
-
-TFTP is implemented atop [UDP], which means it can't benefit from the
-retransmission and reliability inherent in [TCP]. Clients and servers must
-maintain their own connections. For this reason operations are carried out in
-lock-step, requiring acknowledgement at each point, so that nothing is lost or
-misunderstood.
-
-Because files might be larger than what can fit into a single packet or even in
-memory, TFTP operates on chunks of a file, which it calls "blocks". In RFC 1350
-these blocks are always 512 bytes or less, but [RFC 1783] allows clients to
-negotiate different sizes which might be better on a particular network.
-
-By default, initial requests are received on port `69`, the offical port
-assigned to TFTP by [IANA]. Thereafter, the rest of a transfer is continued on
-a random port chosen by the server. This keeps the primary port free to receive
-additional requests.
-
-### Reading
-
-To read a file, a client sends a read request packet. If the request is
-valid, the server responds with the first block of data. The client sends an
-acknowledgement of this block and the server responds with the next block of
-data. The two continue this dance until there's nothing more to read.
-
-
-
-
-### Writing
-
-Writing a file to a server is the inverse of reading. The client sends a write
-request packet and the server responds with an acknowledgement. Then the client
-sends the first block of data and the server responds with another
-acknowledgement. Rinse and repeat until the full file is transferred.
-
-
-
-
-### Errors
-
-Errors are a valid response to any other packet. Most, if not all, errors are
-terminal. Errors are a courtesy and are neither acknowledged nor retransmitted.
-
-## Packet Types
-
-To cover the interactions above, RFC 1350 defines five packet types, each
-starting with a different 2 byte opcode. I'll elaborate on each of them in
-turn.
-
-| Opcode | Operation | Abbreviation |
-|--------|:--------------------------|--------------|
-| 1 | [Read Request](#rrq-wrq) | `RRQ` |
-| 2 | [Write Request](#rrq-wrq) | `WRQ` |
-| 3 | [Data](#data) | `DATA` |
-| 4 | [Acknowledgement](#ack) | `ACK` |
-| 5 | [Error](#error) | `ERROR` |
-
-### `RRQ` / `WRQ`
-
-Read and write requests share a representation, differing only by opcode. They
-contain a filename and a mode as [null-terminated strings].
-
-| 2 bytes | string | 1 byte | string | 1 byte |
-|:--------|:---------|:-------|:-------|:-------|
-| opcode | filename | 0 | mode | 0 |
-
-Here's an example of the raw bytes in an `RRQ` for a file called `foobar.txt`
-in `octet` mode.
-
-
let rrq = b"\x00\x01foobar.txt\x00octet\x00";
-
-And here's a `WRQ` for the same file in the same mode.
-
-
let wrq = b"\x00\x02foobar.txt\x00octet\x00";
-
-#### Modes
-
-TFTP defines modes of transfer which describe how the bytes being transferred
-should be handled on the other end. There are three default modes.
-
-| Mode | Meaning |
-|:---------|:---------------------------------------------------------------|
-| netascii | 8-bit [ASCII]; specifies control characters & line endings |
-| octet | raw 8-bit bytes; byte-for-byte identical on both ends |
-| mail | email the bytes to a user; obsolete even in 1992 |
-
-The protocol allows for other modes to be defined by cooperating hosts, but I
-can't recommend that. Honestly, `octet` mode is probably sufficient for most
-modern needs.
-
-### `DATA`
-
-Data packets contain the block number being sent and the corresponding data as
-raw bytes.
-
-| 2 bytes | 2 bytes | n bytes |
-|:--------|:------------|:----------|
-| opcode | block # | data |
-
-Here's an example of the raw bytes in a `DATA` packet for the first block of a
-transfer with the contents `Hello, World!`.
-
-
let data = b"\x00\x03\x00\x01Hello, World!";
-
-### `ACK`
-
-Acknowledgements need only contain the block number they correspond to.
-
-| 2 bytes | 2 bytes |
-|:--------|:------------|
-| opcode | block # |
-
-Here's an example of the raw bytes in an `ACK` packet for the first block of a
-transfer.
-
-
let ack = b"\x00\x04\x00\x01";
-
-### `ERROR`
-
-Errors contain a numeric error code and a human-readable, null-terminated
-string error message.
-
-| 2 bytes | 2 bytes | string | 1 byte |
-|:--------|:-----------|:--------------|:-------|
-| opcode | error code | error message | 0 |
-
-Here's an example of the raw bytes in an `ERROR` packet for a "File not found"
-error.
-
-
let error = b"\x00\x05\x00\x01File not found\x00";
-
-By default, TFTP defines eight error codes. Since the error code is a 16-bit
-integer there's enough space for you and your friends to define 65,528 of your
-own. In practice, maybe don't.
-
-| Value | Meaning |
-|--------|:-----------------------------------------|
-| 0 | Not defined, see error message (if any). |
-| 1 | File not found. |
-| 2 | Access violation. |
-| 3 | Disk full or allocation exceeded. |
-| 4 | Illegal TFTP operation. |
-| 5 | Unknown transfer ID. |
-| 6 | File already exists. |
-| 7 | No such user. |
-| ... | ... |
-| 65,535 | Go wild, do whatever. |
-
-
-## Type Design
-
-Now we all know entirely too much about TFTP. Let's write some code already!
-
-Before I start parsing anything I find it helpful to design the resulting
-types. Even in application code I put on my library developer hat so I'm not
-annoyed by my own abstractions later.
-
-Let's motivate this design by looking at some code that would use it.
-
-```rust
-let mut buffer = [0; 512];
-let socket = UdpSocket::bind("127.0.0.1:6969")?;
-let length = socket.recv(&mut buffer)?;
-
-let data = &buffer[..length];
-todo!("Get our packet out of data!");
-```
-
-In both [`std::net::UdpSocket`][std-udpsocket] and
-[`tokio::net::UdpSocket`][tokio-udpsocket] the interface that we have to
-work with knows nothing about packets, only raw `&[u8]` (a [slice] of
-[bytes][u8]).
-
-So, our task is to turn a `&[u8]` into something else. But what? In other
-implementations I've seen it's common to think of all 5 packet types as
-variations on a theme. We could follow suit, doing the Rusty thing and define
-an enum.
-
-```rust
-enum Packet {
- Rrq,
- Wrq,
- Data,
- Ack,
- Error,
-}
-```
-
-I might have liked my Go implemenation to look like this. If Go even had enums!
-😒
-
-This design choice has an unintended consequence though. As mentioned earlier,
-`RRQ` and `WRQ` only really matter on initial request. The remainder of the
-transfer isn't concerned with those variants. Even so, Rust's (appreciated)
-insistence on exhaustively matching patterns would make us write code like
-this.
-
-```rust
-match packet(&data)? {
- Packet::Data => handle_data(),
- Packet::Ack => handle_ack(),
- Packet::Error => handle_error(),
- _ => unreachable!("Didn't we already handle this?"),
-}
-```
-
-Also, you might be tempted to use [`unreachable!`][unreachable] for such code,
-but it actually _is_ reachable. An ill-behaved client could send a request
-packet mid-connection and this design would allow it!
-
-Instead, what if we were more strict with our types and split the initial
-`Request` from the rest of the `Transfer`?
-
-### Requests
-
-Before we can talk about a `Request` we should talk about its parts. When we
-talked about packet types we saw that `RRQ` and `WRQ` only differed by opcode
-and the rest of the packet was the same, a `filename` and a `mode`.
-
-A `Mode` is another natural enum, but for our purposes we'll only bother with
-the `Octet` variant for now.
-
-```rust
-pub enum Mode {
- // Netascii, for completeness.
- Octet,
- // Mail, if only to gracefully send an ERROR.
-}
-```
-
-As an added convenience later on we'll add a [`Display`][display] impl for
-`Mode` so we can convert it to a string.
-
-```rust
-impl Display for Mode {
- fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
- match self {
- Self::Octet => write!(f, "octet"),
- }
- }
-}
-```
-
-A `Mode` combined with a `filename` make up the "inner type", which I'll call a
-`Payload` for lack of a better term. I've taken some liberties by declaring
-`filename` a [`PathBuf`][pathbuf], which we'll touch on _briefly_ in the
-[parsing section](#parsing).
-
-```rust
-pub struct Payload {
- pub filename: PathBuf,
- pub mode: Mode,
-}
-```
-
-Now we can define a `Request` as an enum where each variant has a `Payload`.
-
-```rust
-pub enum Request {
- Read(Payload),
- Write(Payload),
-}
-```
-
-### Transfers
-
-`Request` takes care of `RRQ` and `WRQ` packets, so a `Transfer` enum needs
-to take care of the remaining `DATA`, `ACK`, & `ERROR` packets. Transfers
-are the meat of the protocol and more complex than requests. Let's break down
-each variant.
-
-#### `Data`
-
-The `Data` variant needs to contain the `block` number, which is 2 bytes and
-fits neatly into a [`u16`][u16]. It also needs to contain the raw bytes of the
-`data`. There are many ways to represent this, including using a
-[`Vec`][Vec] or a [`bytes::Bytes`][Bytes]. However, I think the most
-straightforward is as a `&[u8]` even though it introduces a [lifetime].
-
-#### `Ack`
-
-The `Ack` packet is the simplest and only needs a `block` number. We'll use a
-solitary `u16` for that.
-
-#### `Error`
-
-The `Error` variant warrants more consideration because of the well-defined
-[error codes](#error). I abhor [magic numbers] in my code, so I'll prefer to
-define another enum called `ErrorCode` for those. For the `message` a
-`String` should suffice.
-
-##### `ErrorCode`
-
-Defining an `ErrorCode` involves more boilerplate than I'd like, so I'll show
-three variants and leave the remainder as an exercise for the reader.
-
-```rust
-#[derive(Copy, Clone)]
-pub enum ErrorCode {
- Undefined,
- FileNotFound,
- // ...
- Unknown(u16),
-}
-```
-
-The `Undefined` variant is, humorously, defined, but the `Unknown` variant I've
-added here is not part of RFC 1350. It merely acts as a catch-all for the
-remaining error space. Conveniently, Rust enums allow variants to contain other
-data.
-
-Because of this `Unknown` variant I didn't opt for a C-style enum like
-
-```rust
-enum ErrorCode {
- Undefined = 0,
- FileNotFound = 1,
- // ...
-}
-```
-
-so we can't cast an `ErrorCode` to a `u16`.
-
-```rust
-// This explodes! 💣💥
-let code = ErrorCode::Unknown(42) as u16;
-```
-
-However, we can add [`From`][from] implementations. One to convert
-from an `ErrorCode` to a `u16`.
-
-```rust
-impl From for u16 {
- fn from(error_code: ErrorCode) -> Self {
- match error_code {
- ErrorCode::Undefined => 0,
- ErrorCode::FileNotFound => 1,
- // ...
- ErrorCode::Unknown(n) => n,
- }
- }
-}
-```
-
-And another to convert from a `u16` to an `ErrorCode`.
-
-```rust
-
-impl From for ErrorCode {
- fn from(code: u16) -> Self {
- match code {
- 0 => Self::Undefined,
- 1 => Self::FileNotFound,
- // ...
- n => Self::Unknown(n),
- }
- }
-}
-```
-
-That way we still have a convenient method for conversions.
-
-```rust
-let code = 42;
-let error: ErrorCode = code.into();
-assert_eq!(error, ErrorCode::Unknown(42));
-```
-
-#### Putting It All Together
-
-With each variant considered, we arrive at an `enum` that looks like this.
-
-```rust
-pub enum Transfer<'a> {
- Data { block: u16, data: &'a [u8] },
- Ack { block: u16 },
- Error { code: ErrorCode, message: String },
-}
-```
-
-I could have defined structs to hold the inner data for each variant like I did
-with `Payload` earlier, but because none of the variants had the same shape I
-felt less inclined to do so.
-
-## Parsing
-
-Now that we have a high-level type design to match the low-level network
-representation we can bridge the two by parsing. There are as many ways to
-shave this [Yacc] as there were enums in our packet types, but I settled on the
-[`nom`][nom] library.
-
-### What Is nom?
-
-nom's own [readme][nom-readme] does a better job of describing itself than I
-ever could, so I'll just let it do the talking.
-
-> nom is a parser combinators library written in Rust. Its goal is to provide
-> tools to build safe parsers without compromising the speed or memory
-> consumption. To that end, it uses extensively Rust's strong typing and memory
-> safety to produce fast and correct parsers, and provides functions, macros
-> and traits to abstract most of the error prone plumbing.
-
-That sounds good and all, but what the heck is a parser combinator? Once again,
-nom has a great [description][nom-combinators] which I encourage you to read.
-The gist is that, unlike other approaches, parser combinators encourage you to
-give your parsing a functional flair. You construct small functions to parse
-the simplest patterns and gradually compose them to handle more complex inputs.
-
-nom has an extra advantage in that it is byte-oriented. It uses `&[u8]` as its
-base type, which makes it convenient for parsing network protocols. This is
-exactly the type we receive off the wire.
-
-### Defining Combinators
-
-It's finally time to define some combinators and do some parsing! Even if
-you're familiar with Rust, nom combinators might look more like Greek to you.
-I'll explain the first one in depth to show how they work and then explain only
-the more confusing parts as we go along. First, a small primer.
-
-nom combinators return [`IResult`][iresult], a type alias for a
-[`Result`][result] that's generic over three types instead of the usual two.
-
-```rust
-pub type IResult> = Result<(I, O), Err>;
-```
-
-These types are the input type`I`, the output type `O`,
-and the error type `E` (usually a [nom error][nom-error]). I understand this
-type to mean that `I` will be parsed to produce `O` and any leftover `I` as
-long as no error `E` happens. For our purposes `I` is `&[u8]` and we'll have a
-couple different `O` types.
-
-#### Null Strings
-
-`null` references are famously a "[billion dollar mistake]" and I can't say I
-like `null` any better in this protocol.
-
-> Like all other strings, it is terminated with a zero byte.
-> — RFC 1350, smugly
-
-Or, you know, just tell me how long the darn string is. You're the one who put
-it in the packet... Yes, I know why you did it, but I don't have to like it. 🤪
-
-Mercifully, the nom toolkit has everything we need to slay this beast.
-
-
-
-Let's work inside out to understand what `null_str` is doing.
-
-1. [`take_till`][take-till] accepts a function (here we use a closure with `b`
- for each byte) and collects up bytes from the `input` until one of
- the bytes matches the null byte, `b'\x00'`. This gets us a `&[u8]` up until,
- _but not including_, our zero byte.
-
-2. [`tag`][tag] here just recognizes the zero byte for completeness, but we'll
- discard it later.
-
-3. [`tuple`][tuple] applies a tuple of parsers one by one and returns their
- results as a tuple.
-
-4. [`map_res`][map-res] applies a function returning a [`Result`][result] over
- the result of a parser. This gives us a nice way to call a fallible function
- on the results of earlier parsing, `take_till` and `tag` in this case.
-
-5. [`std::str::from_utf8`][from-utf-8], the fallible function inside our
- outermost closure, converts our `&[u8]` (now sans zero byte) into a Rust
- [`&str`][str], which is **not** terminated with a zero byte.
-
-6. [`IResult<&[u8], &str>`][iresult] ties it all together at the end in
- `null_str`'s return signature returning any unmatched `&[u8]` and a `&str`
- if successful.
-
-It's important to note that I'm taking another huge liberty here
-by converting these bytes to a Rust string at all. Rust strings are guaranteed
-to be valid [UTF-8][utf-8]. TFTP [predates UTF-8][utf-8-history], so the
-protocol did not specify that these strings should be Unicode. Later, I might
-look into an [`OsString`][osstring], but for now non-Unicode strings will cause
-failures.
-
-> Please, only send me UTF-8 strings.
-> — Me, wearily
-
-#### Request Combinators
-
-Since `Request` only concerns itself with the first two packet types, `RRQ` and
-`WRQ` we can start parsing by matching only those opcodes. For convenience I
-used the [`num-derive`][num-derive] crate to create a `RequestOpCode` enum so I
-could use [`FromPrimitive::from_u16`][from-u16].
-
-The `request_opcode` combinator uses [`map_opt`][map-opt] and
-[`be_u16`][be-u16] combinators to parse a `u16` out of the `input` and pass it
-to `from_u16` to construct a `RequestOpCode`.
-
-```rust
-use num_derive::FromPrimitive;
-use num_traits::FromPrimitive;
-
-#[derive(FromPrimitive)]
-enum RequestOpCode {
- Rrq = 1,
- Wrq = 2,
-}
-
-fn request_opcode(input: &[u8]) -> IResult<&[u8], RequestOpCode> {
- map_opt(be_u16, RequestOpCode::from_u16)(input)
-}
-```
-
-To parse a `Mode` we [`map`][nom-map] the result of
-[`tag_no_case`][tag-no-case] onto our `Mode` constructor. This function would
-need to be _slightly_ more complex if we were supporting more than `octet` mode
-right now, but not by much.
-
-
-
-For a `Payload` we can use [`tuple`][tuple] with our `mode` combinator and
-`null_str` to match our filename. We then use a provided [`Into`][into] impl to
-convert our filename `&str` to a `PathBuf`.
-
-```rust
-fn payload(input: &[u8]) -> IResult<&[u8], Payload> {
- let (input, (filename, mode)) = tuple((null_str, mode))(input)?;
- Ok((
- input,
- Payload {
- filename: filename.into(),
- mode,
- },
- ))
-}
-```
-
-Finally, we reach the top level of parsing and put all the rest together. The
-`request` function is not, itself, a combinator, which is why you see the
-[`Finish::finish`][nom-finish] calls here. We use
-[`all_consuming`][all-consuming] to ensure no input remains after parsing with
-`payload` and map the result to our respective `Read` and `Write` variants. We
-also hide nom errors inside a [custom error](#parsing-failures).
-
-```rust
-pub fn request(input: &[u8]) -> Result {
- let iresult = match request_opcode(input).finish()? {
- (input, RequestOpCode::Rrq) => map(all_consuming(payload), Request::Read)(input),
- (input, RequestOpCode::Wrq) => map(all_consuming(payload), Request::Write)(input),
- };
-
- iresult
- .finish()
- .map(|(_, request)| request)
- .map_err(ParsePacketError::from)
-}
-```
-
-With our combinators in order we can add a `Request::deserialize` method to our
-enum to hide the implementation details, making it much easier to switch
-parsing logic later if we want.
-
-```rust
-impl Request {
- pub fn deserialize(bytes: &[u8]) -> Result {
- parse::request(bytes)
- }
-}
-```
-
-#### Parsing Failures
-
-You might have wondered where that `ParsePacketError` came from. It's right
-here. I used the [`thiserror`][thiserror] crate because it's invaluable when
-crafting custom errors. Thanks, [`@dtolnay`][dtolnay]!
-
-```rust
-#[derive(Debug, PartialEq, thiserror::Error)]
-#[error("Error parsing packet")]
-pub struct ParsePacketError(nom::error::Error>);
-
-// Custom From impl because thiserror's #[from] can't tranlate this for us.
-impl From> for ParsePacketError {
- fn from(err: nom::error::Error<&[u8]>) -> Self {
- ParsePacketError(nom::error::Error::new(err.input.to_vec(), err.code))
- }
-}
-```
-
-You might also wonder why I converted from the original
-`nom::error::Error<&[u8]>` to `nom::error::Error>`. Apparently
-[`std::error::Error::source()`][error-source] requires errors to be
-`dyn Error + 'static`, so non-[static lifetimes][static-lifetimes] aren't
-allowed if you want to provide a backtrace, which I might like to do at some
-point. Also, it just seems reasonable for an `Error` type to own its data.
-
-While we were careful to split up our `Request` and `Transfer` types I didn't
-see a whole lot of benefit in having separate error types, so I reused
-`ParsePacketError` for `Transfer` as well.
-
-
-#### Transfer Combinators
-
-The `Transfer` combinators are very similar to what we did for `Request`. The
-opcode handling is basically the same, but with different numeric values so we
-can't accidentally parse any other opcodes.
-
-```rust
-use num_derive::FromPrimitive;
-use num_traits::FromPrimitive;
-
-#[derive(FromPrimitive)]
-enum TransferOpCode {
- Data = 3,
- Ack = 4,
- Error = 5,
-}
-
-fn transfer_opcode(input: &[u8]) -> IResult<&[u8], TransferOpCode> {
- map_opt(be_u16, TransferOpCode::from_u16)(input)
-}
-```
-
-For `Data` we just peel off the `u16` block number and then retain the
-[`rest`][rest] as the original `&[u8]`. The type alias here isn't necessary,
-but I like to do small things like this for organizational purposes.
-
-```rust
-type Data<'a> = (u16, &'a [u8]);
-
-fn data(input: &[u8]) -> IResult<&[u8], Data> {
- tuple((be_u16, rest))(input)
-}
-```
-
-`Ack` is, once again, the simplest. Just a named wrapper around [`be_u16`][be-u16].
-
-```rust
-type Ack = u16;
-
-fn ack(input: &[u8]) -> IResult<&[u8], Ack> {
- be_u16(input)
-}
-```
-
-The `Error` variant is nearly as simple, but we need a call to
-[`Result::map`][result-map] to call [`Into`][into] impls and convert `code`
-from `u16` to `ErrorCode` and `message` from `&str` to `String`.
-
-```rust
-type Error = (ErrorCode, String);
-
-fn error(input: &[u8]) -> IResult<&[u8], Error> {
- tuple((be_u16, null_str))(input)
- .map(|(input, (code, message))| (input, (code.into(), message.into())))
-}
-```
-
-When we put it all these combinators together in a `transfer` function it looks
-more complex than our earlier `request` function. That's only because there
-are more variants and my choice to use anonymous struct variants instead of
-tuple structs means there's no easy constructor, so we map over a closure.
-Otherwise the idea is the same as before.
-
-```rust
-pub fn transfer(input: &[u8]) -> Result {
- let iresult = match opcode(input).finish()? {
- (input, TransferOpCode::Data) => map(all_consuming(data), |(block, data)| {
- Transfer::Data { block, data }
- })(input),
- (input, TransferOpCode::Ack) => {
- map(all_consuming(ack), |block| Transfer::Ack { block })(input)
- }
- (input, TransferOpCode::Error) => map(all_consuming(error), |(code, message)| {
- Transfer::Error { code, message }
- })(input),
- };
-
- iresult
- .finish()
- .map(|(_, transfer)| transfer)
- .map_err(ParsePacketError::from)
-}
-```
-
-Just like with `Request` we create a `Transfer::deserialize` method to hide
-these parsing details from the rest of our code.
-
-```rust
-impl<'a> Transfer<'a> {
- pub fn deserialize(bytes: &'a [u8]) -> Result {
- parse::transfer(bytes)
- }
-}
-```
-
-## Serialization
-
-We can now read bytes into packets, which is handy, but astute readers will
-have noticed that you need to do the reverse if you're going to have a full
-TFTP conversation. Luckily, this serialization is (mostly) infallible, so
-there's less to explain.
-
-I used [`BytesMut`][bytes-mut] because I was already using the
-[`bytes`][bytes] crate for the extension methods on the [`BufMut`][buf-mut]
-trait like [`put_slice`][put-slice]. Plus, this way I avoid an accidental panic
-if I pass a `&mut [u8]` and forget to size it appropriately.
-
-### Serializing `Request`
-
-Serializing a `Request` packet is deceptively straightfoward. We use a `match`
-expression to pull our `Payload` out of the request and associate with a
-`RequestOpCode`. Then we just serialize the opcode as a `u16` with
-[`put_u16`][put-u16]. The `filename` and `mode` we serialize as null-terminated
-strings using a combo of [`put_slice`][put-slice] and [`put_u8`][put-u8].
-
-```rust
-impl Request {
- pub fn serialize(&self, buffer: &mut BytesMut) {
- let (opcode, payload) = match self {
- Request::Read(payload) => (RequestOpCode::Rrq, payload),
- Request::Write(payload) => (RequestOpCode::Wrq, payload),
- };
-
- buffer.put_u16(opcode as u16);
- buffer.put_slice(payload.filename.to_string_lossy().as_bytes());
- buffer.put_u8(0x0);
- buffer.put_slice(payload.mode.to_string().as_bytes());
- buffer.put_u8(0x0);
- }
-}
-```
-
-Converting our `mode` with [`as_bytes`][as-bytes] through a
-[`to_string`][to-string] is possible thanks to our earlier [`Display`][display]
-impl for `Mode`. The `filename` conversion to bytes through `PathBuf`'s
-[`to_string_lossy`][to-string-lossy] might reasonably raise some eyebrows.
-Unlike strings a Rust path is not guaranteed to be UTF-8, so any non-Unicode
-characters will be replaced with [� (U+FFFD)][replacement-character]. For now,
-given my earlier Unicode decision I'm comfortable with this, but a more robust
-method is desirable.
-
-### Serializing `Transfer`
-
-Serializing a `Transfer` packet is more straightforward.
-
-```rust
-impl Transfer<'_> {
- pub fn serialize(&self, buffer: &mut BytesMut) {
- match *self {
- Self::Data { block, data } => {
- buffer.put_u16(TransferOpCode::Data as u16);
- buffer.put_u16(block);
- buffer.put_slice(data);
- }
- Self::Ack { block } => {
- buffer.put_u16(TransferOpCode::Ack as u16);
- buffer.put_u16(block);
- }
- Self::Error { code, ref message } => {
- buffer.put_u16(TransferOpCode::Error as u16);
- buffer.put_u16(code.into());
- buffer.put_slice(message.as_bytes());
- buffer.put_u8(0x0);
- }
- }
- }
-}
-```
-
-As before, with each variant we serialize a `u16` for the `TransferOpCode` and then do
-variant-specific serialization.
-
-- For `Data` we serialize a `u16` for the block number and then the
- remainder of the data.
-- For `Ack` we also serialize a `u16` block number.
-- For `Error` we use our [`From`][from] impl from earlier to serialize the
- `ErrorCode` as a `u16` and then serialize the `message` as a null-terminated
- string.
-
-That's it! Now we can read and write structured data to and from raw bytes! 🎉
-
-## Tests
-
-A post on parsing wouldn't be complete without some tests showing that our code
-works as expected. First, we'll use the marvelous [`test-case`][test-case]
-crate to bang out a few negative tests on things we expect to be errors.
-
-
#[test_case(b"\x00" ; "too small")]
-#[test_case(b"\x00\x00foobar.txt\x00octet\x00" ; "too low")]
-#[test_case(b"\x00\x03foobar.txt\x00octet\x00" ; "too high")]
-fn invalid_request(input: &[u8]) {
-let actual = Request::deserialize(input);
-// We don't care about the nom details, so ignore them with ..
-assert!(matches!(actual, Err(ParsePacketError(..))));
-}
-
-
-And, for good measure, we'll show that we can round-trip an `RRQ` packet from
-raw bytes with a stop at a proper enum in between.
-
-
#[test]
-fn roundtrip_rrq() -> Result<(), ParsePacketError> {
-let before = b"\x00\x01foobar.txt\x00octet\x00";
-let expected = Request::Read(Payload {
- filename: "foobar.txt".into(),
- mode: Mode::Octet,
- });
-
-let packet = Request::deserialize(before)?;
-// Use an under-capacity buffer to test panics.
-let mut after = BytesMut::with_capacity(4);
- packet.serialize(&mut after);
-
-assert_eq!(packet, expected);
-assert_eq!(&before[..], after);
-}
-
-
-Unless you want to copy/paste all this code you'll have to trust me that the
-tests pass. 😉 Don't worry, I've written many more tests, but this is a blog
-post, not a test suite, so I'll spare you the details.
-
-## `Ack`nowledgements
-
-Wow. You actually read all the way to the end. Congrats, and more importantly,
-thank you! 🙇♂️
-
-All of the work above is part of a personal project I chip away at
-in my spare time, but I don't do it alone. I owe a huge debt of gratitude to my
-friend & Rust mentor, [Zefira], who has spent countless hours letting me
-pick her brain on every minute detail of
-this TFTP code. I could not have written this blog post without her!
-
-I also need to thank Yiannis M ([`@oblique`][oblique]) for their work on the
-[`async-tftp-rs`][async-tftp-rs] crate, from which I have borrowed liberally
-and learned a great deal. You may recognize some combinators if you dive into
-that code.
-
-Finally, I can't thank my wife enough helping me edit this. There are many
-fewer mistakes as a result.
-
-The source code for the rest of the project is not currently public, but when
-I'm more confident in it I'll definitely share more details. Meanwhile, I
-welcome any and all suggestions on how to make what I've written here more
-efficient and safe.
-
-
-
-[Go]: https://go.dev/
-[TFTP]: https://en.wikipedia.org/wiki/Trivial_File_Transfer_Protocol
-[DHCP]: https://en.wikipedia.org/wiki/Dynamic_Host_Configuration_Protocol
-[PXE]: https://en.wikipedia.org/wiki/Preboot_Execution_Environment
-[network booting]: https://en.wikipedia.org/wiki/Network_booting
-[multicast]: https://en.wikipedia.org/wiki/Multicast
-[RFC 2090]: https://www.rfc-editor.org/rfc/rfc2090
-[Rust]: https://www.rust-lang.org/
-[memory-safe-android]: https://security.googleblog.com/2022/12/memory-safe-languages-in-android-13.html
-[rust-linux-kernel]: https://lwn.net/Articles/910762/
-[RFC 1350]: https://www.rfc-editor.org/rfc/rfc1350
-[RFC 2347]: https://www.rfc-editor.org/rfc/rfc2347
-[RFC 1783]: https://www.rfc-editor.org/rfc/rfc1783
-[IANA]: https://en.wikipedia.org/wiki/Internet_Assigned_Numbers_Authority
-[HTTP]: https://en.wikipedia.org/wiki/Hypertext_Transfer_Protocol
-[SSH]: https://en.wikipedia.org/wiki/Secure_Shell
-[UDP]: https://en.wikipedia.org/wiki/User_Datagram_Protocol
-[TCP]: https://en.wikipedia.org/wiki/Transmission_Control_Protocol
-[ASCII]: https://en.wikipedia.org/wiki/ASCII
-[null-terminated strings]: https://en.wikipedia.org/wiki/Null-terminated_string
-[std-udpsocket]: https://doc.rust-lang.org/std/net/struct.UdpSocket.html
-[tokio-udpsocket]: https://docs.rs/tokio/1.23.0/tokio/net/struct.UdpSocket.html
-[slice]: https://doc.rust-lang.org/std/primitive.slice.html
-[unreachable]: https://doc.rust-lang.org/std/macro.unreachable.html
-[pathbuf]: https://doc.rust-lang.org/std/path/struct.PathBuf.html
-[u16]: https://doc.rust-lang.org/std/primitive.u16.html
-[u8]: https://doc.rust-lang.org/std/primitive.u8.html
-[lifetime]: https://doc.rust-lang.org/rust-by-example/scope/lifetime.html
-[magic numbers]: https://en.wikipedia.org/wiki/Magic_number_(programming)
-[from]: https://doc.rust-lang.org/std/convert/trait.From.html
-[Yacc]: https://en.wikipedia.org/wiki/Yacc
-[nom]: https://crates.io/crates/nom
-[nom-readme]: https://github.com/rust-bakery/nom/blob/6860641f1b003781f9dc1a91d0f631ff17400d1b/README.md
-[nom-combinators]: https://github.com/rust-bakery/nom/blob/6860641f1b003781f9dc1a91d0f631ff17400d1b/README.md#parser-combinators
-[billion dollar mistake]: https://www.infoq.com/presentations/Null-References-The-Billion-Dollar-Mistake-Tony-Hoare/
-[take-till]: https://docs.rs/nom/7.1.1/nom/bytes/complete/fn.take_till.html
-[tag]: https://docs.rs/nom/7.1.1/nom/bytes/complete/fn.tag.html
-[tuple]: https://docs.rs/nom/7.1.1/nom/sequence/fn.tuple.html
-[map-res]: https://docs.rs/nom/7.1.1/nom/combinator/fn.map_res.html
-[result]: https://doc.rust-lang.org/core/result/enum.Result.html
-[from-utf-8]: https://doc.rust-lang.org/std/str/fn.from_utf8.html
-[str]: https://doc.rust-lang.org/std/primitive.str.html
-[iresult]: https://docs.rs/nom/7.1.1/nom/type.IResult.html
-[nom-error]: https://docs.rs/nom/7.1.1/nom/error/struct.Error.html
-[utf-8]: https://en.wikipedia.org/wiki/UTF-8
-[utf-8-history]: https://en.wikipedia.org/wiki/UTF-8#History
-[osstring]: https://doc.rust-lang.org/std/ffi/struct.OsString.html
-[num-derive]: https://crates.io/crates/num-derive
-[from-u16]: https://docs.rs/num-traits/0.2.15/num_traits/cast/trait.FromPrimitive.html#method.from_u16
-[map-opt]: https://docs.rs/nom/7.1.1/nom/combinator/fn.map_opt.html
-[be-u16]: https://docs.rs/nom/7.1.1/nom/number/complete/fn.be_u16.html
-[nom-map]: https://docs.rs/nom/7.1.1/nom/combinator/fn.map.html
-[tag-no-case]: https://docs.rs/nom/7.1.1/nom/bytes/complete/fn.tag_no_case.html
-[into]: https://doc.rust-lang.org/std/convert/trait.Into.html
-[nom-finish]: https://docs.rs/nom/7.1.1/nom/trait.Finish.html#tymethod.finish
-[all-consuming]: https://docs.rs/nom/7.1.1/nom/combinator/fn.all_consuming.html
-[thiserror]: https://crates.io/crates/thiserror
-[dtolnay]: https://github.com/dtolnay
-[error-source]: https://doc.rust-lang.org/std/error/trait.Error.html#method.source
-[static-lifetimes]: https://doc.rust-lang.org/rust-by-example/scope/lifetime/static_lifetime.html
-[rest]: https://docs.rs/nom/7.1.1/nom/combinator/fn.rest.html
-[result-map]: https://doc.rust-lang.org/std/result/enum.Result.html#method.map
-[bytes-mut]: https://docs.rs/bytes/1.3.0/bytes/struct.BytesMut.html
-[buf-mut]: https://docs.rs/bytes/1.3.0/bytes/trait.BufMut.html
-[bytes]: https://crates.io/crates/bytes
-[put-u16]: https://docs.rs/bytes/1.3.0/bytes/trait.BufMut.html#method.put_u16
-[put-slice]: https://docs.rs/bytes/1.3.0/bytes/trait.BufMut.html#method.put_slice
-[put-u8]: https://docs.rs/bytes/1.3.0/bytes/trait.BufMut.html#method.put_u8
-[display]: https://doc.rust-lang.org/std/fmt/trait.Display.html
-[as-bytes]: https://doc.rust-lang.org/std/string/struct.String.html#method.as_bytes
-[to-string]: https://doc.rust-lang.org/std/string/trait.ToString.html#tymethod.to_string
-[to-string-lossy]: https://doc.rust-lang.org/std/path/struct.PathBuf.html#method.to_string_lossy
-[replacement-character]: https://doc.rust-lang.org/std/char/constant.REPLACEMENT_CHARACTER.html
-[Vec]: https://doc.rust-lang.org/std/vec/struct.Vec.html
-[Bytes]: https://docs.rs/bytes/1.3.0/bytes/struct.Bytes.html
-[test-case]: https://crates.io/crates/test-case
-[Zefira]: https://zefira.dev/
-[oblique]: https://github.com/oblique
-[async-tftp-rs]: https://crates.io/crates/async-tftp
diff --git a/content/posts/parsing-tftp-in-rust/rrq.svg b/content/posts/parsing-tftp-in-rust/rrq.svg
deleted file mode 100644
index af8c4e9..0000000
--- a/content/posts/parsing-tftp-in-rust/rrq.svg
+++ /dev/null
@@ -1 +0,0 @@
-
diff --git a/content/posts/parsing-tftp-in-rust/wrq.svg b/content/posts/parsing-tftp-in-rust/wrq.svg
deleted file mode 100644
index e813909..0000000
--- a/content/posts/parsing-tftp-in-rust/wrq.svg
+++ /dev/null
@@ -1 +0,0 @@
-
diff --git a/content/posts/resolving-a-dns-issue/after.svg b/content/posts/resolving-a-dns-issue/after.svg
deleted file mode 100644
index 3ab58f8..0000000
--- a/content/posts/resolving-a-dns-issue/after.svg
+++ /dev/null
@@ -1,4 +0,0 @@
-
-
\ No newline at end of file
diff --git a/content/posts/resolving-a-dns-issue/before.svg b/content/posts/resolving-a-dns-issue/before.svg
deleted file mode 100644
index 54536ef..0000000
--- a/content/posts/resolving-a-dns-issue/before.svg
+++ /dev/null
@@ -1,4 +0,0 @@
-
-
\ No newline at end of file
diff --git a/content/posts/resolving-a-dns-issue/index.md b/content/posts/resolving-a-dns-issue/index.md
deleted file mode 100644
index aec1508..0000000
--- a/content/posts/resolving-a-dns-issue/index.md
+++ /dev/null
@@ -1,316 +0,0 @@
-+++
-title = "Resolving A DNS Issue"
-description = "Resolving my DNS issue."
-date = 2018-06-24T13:21:00-07:00
-[taxonomies]
-tags = ["DNS", "HTTP", "Slow", "Internet"]
-+++
-Haha. Get it? _Resolving_ a DNS issue. OK, that was bad. You don't have to read
-anymore, but I'm `SOA` into this. You might even say I'm in the zone. I think
-it's gonna be `A` great read, so consider sticking around, 'cuz there's no
-TLD;R.
-
-
-
-## Symptoms
-
-A few months ago I switched ISPs to try to support a smaller regional ISP
-instead of one of the [monopolistic][comcast] [behemoths][warner]. The
-transition was largely pain free, but after a little while I noticed that
-something wasn't right on the new network. Periodically my web browsing would
-be much slower, but it didn't seem to be specific to any particular site.
-Furthermore, reloading a site I was having a problem with was usually without
-issue.
-
-I let this go on for longer than I'm comfortable admitting, but finally got
-fed up when I was trying to `curl` pages from the same site in a loop. I
-noticed that the same pages took different amounts of time to fetch between
-runs of the loop, which seemed very wrong to me since they were static pages.
-
-## Troubleshooting
-
-My first step in narrowing down the problem was to pick a website known for its
-speed and reliability see if I had problems there. I `curl`ed
-[google.com][google] in a loop every 10 seconds and sure enough, every
-so often the problem would happen there too. The connection would just hang
-for a few seconds, but eventually complete.
-
-Next, I wondered how I could narrow it down further to make sure the issue wasn't
-caused on my home network. Then I remembered that my gateway could be
-accessed by its IP of [192.168.1.1][router-ip] and the hostname
-[router.asus.com][asus]. I pointed `curl` at my router's hostname. The same
-problem was happening with a domain that resolved to my local network!
-
-OK, so it didn't matter whether I was leaving my network or not. Traffic was
-_slooow_ either way. I changed my `curl` loop to point at my router by its IP.
-No issues whatsoever. At this point I recalled the age-old sysadmin wisdom.
-It's **always** DNS!
-
-
-
-
-It's Always DNS. Converted to SVG from a Reddit post by u/tehrabbitt. View full size.
-
-
-
-## Gathering Data
-
-Clearly this was an intermittent DNS issue. I could have gone right into fixing
-the problem, but I wanted to know just how bad it was. In order to do that I
-needed data. I collected timing information for simultaneous HTTP GET requests
-resolving by IP address and by hostname for the same domain over a 2 hour
-period with the help of this script.
-
-```zsh
-#!/usr/bin/env zsh
-
-# 720 * 10s = 2hrs
-: ${INTERVAL=10}
-: ${NUM_TESTS=720}
-
-: ${ROUTER_IP="192.168.1.1"}
-: ${ROUTER_HOSTNAME="router.asus.com"}
-
-: ${OUTPUT_FILE="./http-test-results.csv"}
-
-function now() {
- # ISO 8601 FTW!
- date -u "+%Y-%m-%dT%H:%M:%SZ"
-}
-
-function http-test() {
- local host=$1
- /usr/bin/time -f "%e" curl -s $host > /dev/null
-}
-
-function run-tests() {
- # Create CSV header.
- echo "Datetime,Seconds for HTTP GET by IP,Seconds for HTTP GET by Hostname"
-
- # A sample CSV row will look like
- # 2018-03-26T03:37:29Z,0.01,0.02
-
- # For each iteration spit out the current time, how long it takes to curl
- # with an IP address, and how long it takes to curl with a hostname.
- for t in {1..${NUM_TESTS}}
- do
- # Redirect stderr to stdout with 2>&1 to capture GNU time's output.
- echo "$(now),$(http-test ${ROUTER_IP} 2>&1),$(http-test ${ROUTER_HOSTNAME} 2>&1)"
- sleep ${INTERVAL}
- done
-}
-
-run-tests >> ${OUTPUT_FILE}
-```
-
-That was going to give me the data I needed, but reading raw CSVs is a pain.
-I don't do graphing very often, but I figured visualization would help, so I
-did a quick survey of Python graphing libraries and found the [Pygal][pygal]
-library. It turned out to be much more flexible than Matplotlib for my needs
-and gave me great looking SVGs customized to match the style of this site.
-
-```python
-#!/usr/bin/env python3.6
-
-import sys
-from typing import Iterator
-
-import pandas as pd
-from pygal import Line
-from pygal.style import Style
-
-
-class GruvboxStyle(Style):
- """ A gruvbox-inspired Pygal style. """
-
- background = '#282828'
- plot_background = '#1d2021'
- foreground = '#fdf4c1'
- foreground_strong = '#fdf4c1'
- foreground_subtle = '#fdf4c1'
- colors = ('#8ec07c', '#fa5c4b')
-
-
-def dilute_datetimes(datetimes: pd.Series, factor: int) -> Iterator[str]:
- """ Lots of datetimes overlap and become unreadable, make some space. """
- dilute = lambda t: t[1] if t[0] % factor == 0 else ''
- yield from map(dilute, enumerate(datetimes))
-
-
-def generate_chart(data: pd.DataFrame) -> Line:
- line_chart = Line(
- js=(), # The tooltips are really nice, but I don't want any JS.
- style=GruvboxStyle,
- x_label_rotation=30
- )
-
- # Water those datetimes down so they don't overlap and we can read them!
- datetimes = data['Datetime']
- dilution_factor = datetimes.shape[0] // 10
- datetimes = dilute_datetimes(datetimes, factor=dilution_factor)
-
- line_chart.title = 'HTTP GET by IP vs. HTTP GET by Hostname'
- line_chart.y_title = 'Seconds'
- line_chart.x_labels = datetimes
- line_chart.add(
- title='By IP',
- values=data['Seconds for HTTP GET by IP']
- )
- line_chart.add(
- title='By Hostname',
- values=data['Seconds for HTTP GET by Hostname']
- )
- return line_chart
-
-
-def main(argv: list) -> None:
- data = pd.read_csv(sys.argv[1])
- output = sys.argv[2]
- chart = generate_chart(data)
- chart.render_to_file(output)
-
-
-if __name__ == '__main__':
- main(sys.argv)
-```
-
-Now I could run a few simple commands and get a chart to help me understand
-what I was dealing with.
-
-```zsh
-#!/usr/bin/env zsh
-export OUTPUT_FILE=before.csv
-zsh http-test.zsh && python3.6 chart.py $OUTPUT_FILE ${OUTPUT_FILE:r}.svg
-```
-
-I ended up truncating the results for the graphs below to only show 30 minutes
-of the 2 hour data to help readability, but the data looks just about the same
-anyway.
-
-
-
-
-HTTP GET by IP vs. HTTP GET by Hostname before the fix. View full size.
-
-
-
-**Wow!** That's _really_ bad. You can't even see the _By IP_ series because
-it's dwarfed by these enormous outliers from _By Hostname_. These were local
-DNS requests. There's no reason they should have been taking **greater than 5
-seconds** to
-complete.
-
-## Resolution
-
-I logged into my router's administrative console and sure enough, right there
-in the logs was a DNS error.
-
-```
-Mar 25 21:44:03 dnsmasq[31700]: nameserver 208.76.152.1 refused to do a recursive query
-```
-
-I was still using the default upstream DNS given by my ISP and it was refusing
-[recursive DNS queries][recursive-dns]. My OS wasn't running a caching DNS
-resolver locally, so I was totally at the mercy of my router. When the router
-went out to lunch trying to satisfy a DNS request my connections did too.
-
-Curiously, only one of the two nameservers my router was configured to use
-appeared in the logs with that error message. Not knowing exactly how the
-router manufacturer configured DNSMasq or how the upstream DNS servers were
-configured my working hypothesis is that the one refusing recursive queries
-was the primary and requests that weren't cached always had to go to two
-servers.
-
-My assumption is that DNSMasq is pretty badly misconfigured on my router as I
-wouldn't expect [router.asus.com][asus] to even need a recursive query.
-Ultimately I should probably run my own local DNS. Maybe I'll even take
-[trust-dns] for a spin. That would be a long-term project though, so I opted
-instead to simply change my nameservers to [Google's public ones][google-dns].
-The problem finally went away once recursive queries were being supported!
-
-## Measuring Success
-
-Once I was confident I'd fixed the problem I ran the same chart generation
-commands as before, being careful to set `OUTPUT_FILE="after.csv"` so as not to
-overwrite my previous data.
-
-
-
-
-HTTP GET by IP vs. HTTP GET by Hostname after the fix. View full size.
-
-
-
-Much better! Even the most extreme request was under two tenths of a second.
-Just how much did the situation improve though? I wrote one last script to find
-out.
-
-```zsh
-#!/usr/bin/env zsh
-function percent_failures() {
- local file="${1}"
- local pattern="${2}"
- local num_failures=$(grep $pattern $file | wc -l)
- local num_results=$(wc -l $file | cut -d\ -f1)
- echo "scale=4; ${num_failures} / ${num_results} * 100" | bc -l
-}
-
-percent_failures before.csv '5\.'
-percent_failures after.csv '5\.'
-```
-
-Prior to fixing the issue 11.66% of all requests took longer than 5 seconds.
-After applying the fix that dropped to 0.13%. I'd say that's a noticeable
-improvement!
-
-#### Bonus
-
-In case you were curious about how the [_It's Always DNS_][its-always-dns]
-diagram above was generated, here's the [Graphviz][graphviz] source and `dot`
-command to reproduce it.
-
-```dot
-// File: its-always-dns.dot
-
-digraph G {
- graph [ splines=ortho nodesep=2 fontcolor="#fdf4c1" ];
- node [ style=filled color="#282828" fillcolor="#282828", fontcolor="#fdf4c1" ];
- edge [ color="#282828" fontcolor="#282828" ];
-
- is_it_dns [shape=diamond label="Is it DNS?"];
- check_bad_records [shape=box label="Check DNS for Bad Records"];
- is_it_resolved [shape=diamond label="Is the Issue Resolved?"];
- you_missed_something [shape=box label="You Obviously Missed Something"];
- its_always_dns [shape=box label="It's Always DNS"];
-
- is_it_dns -> check_bad_records [xlabel="Yes"];
- check_bad_records -> is_it_resolved;
- is_it_resolved -> its_always_dns [xlabel="Yes"];
- is_it_resolved -> you_missed_something [xlabel="No"];
- you_missed_something -> check_bad_records;
- you_missed_something -> is_it_dns [xlabel="No"];
-}
-```
-
-```zsh
-#!/usr/bin/env zsh
-dot -Tsvg its-always-dns.dot -o its-always-dns.svg
-```
-
-In the interest of making this post maximally reproducible I've put all the
-raw data and scripts used in this post in a
-[GitHub repo][resolving-a-dns-issue] for anyone who wants to play with them
-on their own network.
-
-[comcast]: https://en.wikipedia.org/wiki/Comcast
-[warner]: https://en.wikipedia.org/wiki/WarnerMedia
-[google]: https://google.com/
-[router-ip]: https://192.168.1.1
-[asus]: https://router.asus.com
-[graphviz]: https://www.graphviz.org/
-[pygal]: https://github.com/Kozea/pygal
-[recursive-dns]: https://unix.stackexchange.com/a/24391/82338
-[trust-dns]: https://github.com/bluejekyll/trust-dns
-[google-dns]: https://developers.google.com/speed/public-dns/
-[its-always-dns]: its-always-dns.svg
-[resolving-a-dns-issue]: https://github.com/reillysiemens/resolving-a-dns-issue
diff --git a/content/posts/resolving-a-dns-issue/its-always-dns.svg b/content/posts/resolving-a-dns-issue/its-always-dns.svg
deleted file mode 100644
index 869d5d9..0000000
--- a/content/posts/resolving-a-dns-issue/its-always-dns.svg
+++ /dev/null
@@ -1,83 +0,0 @@
-
-
-
-
-
diff --git a/content/posts/semantic-version-and-conventional-commits.md b/content/posts/semantic-version-and-conventional-commits.md
new file mode 100644
index 0000000..fb85220
--- /dev/null
+++ b/content/posts/semantic-version-and-conventional-commits.md
@@ -0,0 +1,89 @@
++++
+title= "Semantic Versioning(SemVer) and Conventional Commits"
+description = "Adopting best practices in your git and build workflows."
+date= 2023-01-24T15:34:14.160Z
+author = "Ian Andwati"
+[taxonomies]
+tags= ["git", "semver","workflow", "conventional commits"]
++++
+
+Adopting best practices in your git and build workflows.
+
+
+
+# What is Semver?
+
+Semantic versioning or just SemVer is a dependency naming guidline that aims to bring some sanity into naming libraries and packages in software development. It is a standard versioning system created by **Tom Preston Werner** (co-founder of Github) that’s used to communicate what changes are in a release.
+
+An efficient and smooth versioning becomes crucial for efficient software delivery expecially for projects that expose a public API. This saves the developers the chaos that is **dependency hell**.Dependency hell is a colloquial term for the frustration of some software users who have installed software packages which have dependencies on specific versions of other software packages.
+
+The dependency issue arises when several packages have dependencies on the same shared packages or libraries, but they depend on different and incompatible versions of the shared packages. If the shared package or library can only be installed in a single version, the user may need to address the problem by obtaining newer or older versions of the dependent packages. This, in turn, may break other dependencies and push the problem to another set of packages.
+
+## Semantic Versioning Specifications
+
+### Summary
+
+Semantic versioning is based on three numbers that indicate the versions of the software and the compatibilities.They are `X.Y.Z` corresponding to `Major.Minor.Patch`
+
+- **Major Version (X):**Introduces major changes to the code and breaking changes toward backward compatibility
+- **Minor Version (Y):** These code changes don’t introduce breaking changes. This could be adding a new feature that doesn't break changes but are not exactly bug fixes.
+- **Patches (Z):** Mainly correspond to bug fixes
+
+### Examples in common packages
+
+#### React(v18.2.0)
+
+
+
+- **Major -> 18**
+- **Minor -> 2**
+- **Patch -> 0**
+
+---
+
+#### Pipx(v1.1.0)
+
+
+
+- **Major -> 1**
+- **Minor -> 1**
+- **Patch -> 0**
+
+---
+
+## Start at v0.1.0 or v1.0.0?
+
+Convention recommends that in the initial development phase the starting version should be 0.1.0 which makes sense since it’s not in production yet.
+
+The version should only be bumped to 1.0.0 only if it is being used in production and stable.
+
+If your software is being used in production, it should probably already be 1.0.0. If you have a stable API on which users have come to depend, you should be 1.0.0. If you’re worrying a lot about backwards compatibility, you should probably already be 1.0.0.
+
+# Conventional Commits
+
+Conventional Commits is a convention on top of commit messages that are used to automate version changes. It provides an easy set of rules for creating commit messages and works well with SemVer by describing features, fixes and breaking changes that correspond to SemVer standards.
+
+## Summary
+
+The commit message should be structured as follows:
+
+```
+[optional scope]:
+
+[optional body]
+
+[optional footer(s)]
+```
+
+The commit contains the following structural elements, to communicate intent to the consumers of your library:
+
+1. fix: a commit of the type fix patches a bug in your codebase (this correlates with PATCH in Semantic Versioning).
+2. feat: a commit of the type feat introduces a new feature to the codebase (this correlates with MINOR in Semantic Versioning).
+3. BREAKING CHANGE: a commit that has a footer BREAKING CHANGE:, or appends a ! after the type/scope, introduces a breaking API change (correlating with MAJOR in Semantic Versioning). A BREAKING CHANGE can be part of commits of any type.
+4. types other than fix: and feat: are allowed, for example @commitlint/config-conventional (based on the Angular convention) recommends build:, chore:, ci:, docs:, style:, refactor:, perf:, test:, and others.
+5. footers other than BREAKING CHANGE: may be provided and follow a convention similar to git trailer format.
+
+# Resources
+
+https://semver.org/
+https://www.conventionalcommits.org/en/v1.0.0/
diff --git a/content/posts/stateful-callbacks-in-python.md b/content/posts/stateful-callbacks-in-python.md
deleted file mode 100644
index d767083..0000000
--- a/content/posts/stateful-callbacks-in-python.md
+++ /dev/null
@@ -1,241 +0,0 @@
-+++
-title = "Stateful Callbacks in Python"
-description = "Stateful callbacks in Python."
-date = 2017-07-10T07:54:00-08:00
-[taxonomies]
-tags = ["Python", "callbacks"]
-+++
-If you're unfamiliar with what a callback is, don't worry, we can sort that out
-quickly. If callbacks are old hat for you you might want to skip to
-[the interesting bit](#stateful-callbacks).
-
-Simply put, a callback is a function that is passed as an argument to
-another function which may execute it.
-
-
-
-Take, for example, these functions:
-
-```python
-def bar():
- return "I'm the callback!"
-
-def foo(func):
- return func()
-```
-
-If we call `foo` like this
-
-```python
->>> foo(bar)
-"I'm the callback!"
-```
-
-then `bar` is a callback.
-
-## Why Should I Use A Callback?
-
-There are many reasons to use callbacks. For me, the most compelling is
-customization. Let's take a look at a Python built-in as an example. Say we
-have a list of users as dictionaries with a `name` and an `age`:
-
-```python
-users = [
- dict(age=77, name='John Cleese'),
- dict(age=74, name='Eric Idle'),
-]
-```
-
-Imagine that we want to sort our users. If we had just a list of ages or a list
-of names we could easily do this with the built-in `sorted` function, but by
-default Python has no idea how to compare our dictionaries during sorting.
-
-```python
->>> sorted(users)
-Traceback (most recent call last):
- File "", line 1, in
-TypeError: unorderable types: dict() < dict()
-```
-
-Should it sort by `age`? By `name`? We need to tell Python how this should be
-done. Fortunately Python provides and the `sorted` function has a keyword
-argument called `key` that takes, you guessed it, a callback. Let's create some
-of our own!
-
-```python
-def by_age(user):
- return user['age']
-
-def by_name(user):
- return user['name']
-```
-
-Armed with these callbacks we can sort our users.
-
-```python
->>> sorted(users, key=by_age)
-[{'age': 74, 'name': 'Michael Palin'}, {'age': 77, 'name': 'John Cleese'}]
->>> sorted(users, key=by_name)
-[{'age': 77, 'name': 'John Cleese'}, {'age': 74, 'name': 'Michael Palin'}]
-```
-
-Since the `sorted` function takes a callback for the `key` argument we are free
-to customize its behavior. All we have to do is define a function that returns
-the key we intend to sort by and as long as that's an orderable type Python
-will take care of the rest.
-
-## What Does It Mean to Have State?
-
-So, by now we have something of an idea of what callbacks are, how we can use
-them, and why, but what's the point of state? State is most easily described as
-a memory of prior events. This is the core of what every program does and we
-use it all the time, even if we don't realize it. Heck, even saving a variable
-involves keeping track of state.
-
-```python
->>> baz = 1 # The Python interpreter is now tracking the state of 'baz'.
->>> print(baz) # We can recall that state at a later point.
-1
-```
-
-Basically, we need state if we care to remember what happened previously so
-that we can make decisions about what to do next.
-
-## What Normally Happens to State Inside a Callback?
-
-In our first callback function we didn't define any names. To demonstrate what
-typically happens to state inside the scope of a callback let's make a function
-that creates some state.
-
-```python
-def quux():
- plugh = "xyzzy"
- return plugh
-```
-
-When we execute this function we get the expected result.
-
-```python
->>> quux()
-'xyzzy'
-```
-
-After the function is executed we can see that the `plugh` name is not defined.
-
-```python
->>> plugh
-Traceback (most recent call last):
- File "", line 1, in
-NameError: name 'plugh' is not defined
-```
-
-This is because when the function is finished executing its frame is removed
-from the [call stack][call stack] along with any locally defined variables. By
-itself our callback can't remember anything.
-
-## Stateful Callbacks
-
-Alright, so we know what callbacks are, we know what state is. How can we
-combine the two to make a callback that retains its state? As we saw above we
-can't rely on any state that we define inside our callback. The trick to making
-a stateful callback is to rely on names bound to an external scope.
-
-To motivate creating a stateful callback let's say that we still want to sort
-users like we did above, only now we have 1 Million users. It's going to take
-a while to sort those users, so it would be nice to have a progress report so
-we know something is still happening, maybe once per 10,000 users.
-
-### Using Functions
-
-To use names bound to an external scope with a plain ol' function as our
-callback we'll need to take advantage of [closures][closures] (which could be
-an entirely separate post). Here's a function that allows us to use our
-original `by_age` and `by_name` sorters while still giving us progress.
-
-```python
-def sort_reporter(func):
- state = dict(count=0) # We can't just call this 'count'...
-
- def _sort_reporter(user):
- state['count'] += 1 # Because we'd get an UnboundLocalError here.
- if state['count'] % 10000 == 0:
- print("Sorted {count} users.".format(count=state['count']))
- return func(user)
-
- return _sort_reporter
-```
-
-We can use it like so.
-
-```python
->>> sorted_users = sorted(users, key=sort_reporter(by_name))
-Sorted 10000 users.
-Sorted 20000 users.
-# Lots more of this...
-```
-
-How does it work? The key is the `state` dictionary. It lets us keep a mutable
-reference to a name defined outside the scope of the actual reporter function,
-`_sort_reporter`. As the `sorted` built-in is processing our `users` each new
-call to `_sort_reporter` still gets to refer to the original `state`.
-
-Note: We could avoid having a `state` dictionary by using Python
-3's `nonlocal` keyword, but then I'd miss an opportunity in the [bonus](#bonus)
-section.
-
-### Using Classes
-
-If the functional approach doesn't suit you we can also tackle this problem
-from an object-oriented angle. Python lets classes define a
-[`__call__` method][__call__] which makes them callable. This isn't strictly
-necessary for an OOP approach, but when we're making callbacks it's nice to be
-able to treat our instances as functions.
-
-```python
-class SortReporter:
- def __init__(self, func):
- self.func = func
- self.count = 0
-
- def __call__(self, user):
- self.count += 1
- if self.count % 10000 == 0:
- print("Sorted {count} users.".format(count=self.count))
- return self.func(user)
-```
-
-Just as easy to use as our functional option.
-
-```python
->>> sorted_users = sorted(users, key=SortReporter(by_age))
-Sorted 10000 users.
-Sorted 20000 users.
-# One eternity later...
-```
-
-Conceptually, this works for much the same reason that the functional approach
-does. The `SortReporter` instance and all its associated state lives on because
-the `sorted` built-in is carrying around a reference to it and it just pretends
-to be a plain ol' function whenever `sorted` needs it to be one.
-
-### Which Should I Use?
-
-Neither approach is any more or less valid than the other. For this particular
-example there isn't much more code or complexity either way. I generally regard
-functions as being simpler than classes, so I prefer those when possible, but
-classes also provide good structure for more complex callbacks. Try them both!
-
-#### Bonus
-
-As homework a bonus, try instantiating a `SortReporter` and examining
-its `__dict__` attribute. Meditate on what you find there and how it relates
-to the `state` dictionary in the functional approach.
-
-If you get really bold and want to try for extra credit assign the return value
-of the `sort_reporter` function to some variable and examine its `__closure__`
-attribute. This may help you explain why the `state` dictionary doesn't
-disappear after the `sort_reporter` function is called.
-
-[call stack]: https://en.wikipedia.org/wiki/Call_stack
-[closures]: https://en.wikipedia.org/wiki/Closure_(computer_programming)
-[__call__]: https://docs.python.org/3/reference/datamodel.html#object.__call__
diff --git a/content/posts/subresource-integrity.md b/content/posts/subresource-integrity.md
new file mode 100644
index 0000000..6bb8593
--- /dev/null
+++ b/content/posts/subresource-integrity.md
@@ -0,0 +1,122 @@
++++
+title= "Subresource Integrity"
+description = "Understanding Subresource Integrity"
+date= 2023-04-29
+author = "Ian Andwati"
+[taxonomies]
+tags= ["subresource integrity", "csp", "cors"]
++++
+
+Understanding Subresource Integrity
+
+
+
+In my junior year while I was learning HTML and CSS I got to use the Bootstrap css library.The easiest way to obtain the files was from a Content Delivery Network(CDN). I could see links such as:
+
+```html
+
+```
+
+I didn't bother with the `integrity` attribute of the link(guess I wasn't curios enough). The integrity attribute has a cryptographic hash value that is important in CDNs and its called the Subresource Integrity.
+
+Subresource Integrity (SRI) is a security feature in web browsers that helps prevent malicious or compromised scripts from being loaded and executed on a website.a security feature that enables browsers to verify that resources they fetch (for example, from a CDN) are delivered without unexpected manipulation. It works by allowing you to provide a cryptographic hash that a fetched resource must match.
+
+When a resource, such as a script or stylesheet, fails the SRI check, the browser will block the resource from being loaded and displayed on the website.
+
+# How Subresource Integrity Helps
+
+Subresource Integrity (SRI) is a security feature that helps protect against certain types of attacks, such as those that involve malicious modifications to third-party resources that a website loads.
+
+SRI works by allowing a website to specify a cryptographic hash for a particular external resource (such as a JavaScript file or a CSS stylesheet) that it expects to load. When a user's browser fetches that resource, it checks the hash against the actual contents of the resource. If the hash doesn't match, the browser will refuse to load the resource, indicating that it may have been tampered with.
+
+This can help prevent a variety of attacks, such as those involving a compromised content delivery network (CDN) or a man-in-the-middle attacker who intercepts and modifies network traffic. By using SRI, a website can help ensure that it is loading only the resources it expects, and that those resources have not been tampered with in transit.
+
+# Using it
+
+Using Subresource Integrity (SRI) involves a few steps, but it is relatively straightforward.An integrity value begins with at least one string, with each string including a prefix indicating a particular hash algorithm (currently the allowed prefixes are sha256, sha384, and sha512), followed by a dash, and ending with the actual base64-encoded hash.
+
+- Generate a cryptographic hash(a base64-encoded cryptographic hash) of the external resource you want to load on your website. This can be done using a hash function such as SHA-256.
+
+- Add the hash as an attribute to the tag that loads the external resource. For example, for a JavaScript file, you would add the integrity attribute to the `
+```
+
+> An integrity value may contain multiple hashes separated by whitespace. A resource will be loaded if it matches one of those hashes.
+
+Make sure the hash you've generated matches the actual contents of the external resource.
+
+Test your website to ensure that the external resource is loading correctly and that the SRI feature is working as intended.
+
+# Tools for generating SRI hashes
+
+You can use a tool such as the [SRI Hash Generator](https://www.srihash.org/) to generate the hash and check it against the file's contents.
+
+You can generate SRI hashes from the command-line with openssl using a command invocation such as:
+
+```sh
+cat FILENAME.py | openssl dgst -sha384 -binary | openssl base64 -A
+```
+
+or with shasum using a command invocation such as:
+
+```sh
+shasum -b -a 384 FILENAME.js | awk '{ print $1 }' | xxd -r -p | base64
+```
+
+> The pipe-through xxd step takes the hexadecimal output from shasum and converts it to binary.
+> The pipe-through awk step is necessary because shasum will pass the hashed filename in its output to xxd. That can have disastrous consequences if the filename happens to have valid hex characters in it — because xxd will also decode that and pass it to base64.
+
+In a Windows environment, you can create a tool for generating SRI hashes with the following code:
+
+```bat
+@echo off
+set bits=384
+openssl dgst -sha%bits% -binary %1% | openssl base64 -A > tmp
+set /p a= < tmp
+del tmp
+echo sha%bits%-%a%
+pause
+```
+
+To use that code:
+
+1. Save that code in a file named sri-hash.bat in the Windows SendTo folder in your environment (for example, C:\Users\USER\AppData\Roaming\Microsoft\Windows\SendTo).
+1. Right-click a file in the File Explorer, select Send to…, and then select sri-hash. You will see the integrity value in a command box.
+1. Select the integrity value and right-click to copy it to the Clipboard.
+1. Press any key to dismiss the command box
+
+# Cross-Origin Resource Sharing and Subresource Integrity
+
+When verifying the subresource integrity of a resource that originates from a different domain than the document it's embedded in, web browsers will check the resource using Cross-Origin Resource Sharing (CORS) to ensure that the domain serving the resource allows it to be shared with the requesting domain.
+As a result, the resource must be served with an Access-Control-Allow-Origin header that permits the resource to be shared with the requesting domain. For example, the header could include a wildcard (\*) to allow any domain to access the resource.
+
+```
+Access-Control-Allow-Origin: *
+```
+
+# How browsers handle Subresource Integrity
+
+Web browsers employ several mechanisms to handle Subresource Integrity (SRI). When a **script** or **link** element with an integrity attribute is encountered by the browser, it compares the script or stylesheet to the expected hash given in the integrity value before executing the script or applying the stylesheet.
+
+For resources served from a different domain than the document in which it's embedded, the browser also checks the resource using Cross-Origin Resource Sharing (CORS) to ensure that the origin serving the resource allows it to be shared with the requesting domain. This additional check is important for preventing unauthorized access to sensitive resources.
+
+If the script or stylesheet fails to match its associated integrity value, the browser will refuse to execute the script or apply the stylesheet and return a network error, indicating that the fetching of that script or stylesheet failed. This behavior ensures that the resource being loaded is trustworthy and helps to protect users from potential security threats.
+
+# References
+
+- [https://developer.mozilla.org/en-US/docs/Web/HTTP/CSP](https://developer.mozilla.org/en-US/docs/Web/HTTP/CSP)
+- [https://www.srihash.org/](https://www.srihash.org/)
+- [https://developer.mozilla.org/en-US/docs/Web/Security/Subresource_Integrity](https://developer.mozilla.org/en-US/docs/Web/Security/Subresource_Integrity)
+- [https://html.spec.whatwg.org/multipage/semantics.html#attr-script-integrity](https://html.spec.whatwg.org/multipage/semantics.html#attr-script-integrity)
+- [https://html.spec.whatwg.org/multipage/semantics.html#attr-link-integrity](https://html.spec.whatwg.org/multipage/semantics.html#attr-link-integrity)
+- [https://w3c.github.io/webappsec-subresource-integrity/#the-integrity-attribute](https://w3c.github.io/webappsec-subresource-integrity/#the-integrity-attribute)
\ No newline at end of file
diff --git a/content/posts/utterances-comments.md b/content/posts/utterances-comments.md
new file mode 100644
index 0000000..8e16c8d
--- /dev/null
+++ b/content/posts/utterances-comments.md
@@ -0,0 +1,67 @@
++++
+title = "What say you?"
+description = "Adding a comment system by utterances"
+author = "Ian Andwati"
+date = 2023-08-24T15:34:14.160Z
+[taxonomies]
+tags = ["blog", "utterances"]
++++
+
+Free speech enhanced
+
+
+
+There is no point in writing online if your readers cannot provide feedback for your writings. You might as well be shouting at a void. My ideal commenting system has to be free, open source and should not be a hassle to setup. Luckily, [utterances 🔮](https://github.com/utterance/utterances) checks all of these conditions
+
+It is a lightweight comments widget built on GitHub issues. Use GitHub issues for blog comments, wiki pages and more! The notable features are:
+
+- Open source. 🙌
+- No tracking, no ads, always free. 📡🚫
+- No lock-in. All data stored in GitHub issues. 🔓
+- Styled with Primer, the css toolkit that powers GitHub. 💅
+- Dark theme. 🌘
+- Lightweight. Vanilla TypeScript. No font downloads, JavaScript frameworks or polyfills for evergreen browsers. 🐦🌲
+
+## How it works
+
+When Utterances loads, the GitHub [issue search API](https://developer.github.com/v3/search/#search-issues) is used to find the issue associated with the page based on url, pathname or title. If it cannot find an issue that matches the page, [utterances-bot](https://github.com/utterances-bot) will automatically create an issue the first time someone comments.
+
+To comment, users must authorize the utterances app to post on their behalf using the GitHub [OAuth flow](https://developer.github.com/v3/oauth/#web-application-flow). Alternatively, users can comment on the GitHub issue directly.
+
+# Setting up Utterances
+
+### Repository
+
+Choose the repository utterances will connect to.
+
+Make sure the repo is public, otherwise your readers will not be able to view the issues/comments.
+Make sure the utterances app is installed on the repo, otherwise users will not be able to post comments.
+If your repo is a fork, navigate to its settings tab and confirm the issues feature is turned on.
+
+```
+repo:
+ username/repo
+```
+
+A public GitHub repository. This is where the blog post issues and issue-comments will be posted.
+
+### Theme
+
+Choose an Utterances theme that matches your blog.
+
+### Enable Utterances
+
+Add the following script tag to your blog's template. Position it where you want the comments to appear. Customize the layout using the .utterances and .utterances-frame selectors.
+
+```js
+
+```
+
+The complete documentation can be found at [https://utteranc.es](https://utteranc.es/)
diff --git a/public/404.mp4 b/public/404.mp4
new file mode 100644
index 0000000..03b02e0
Binary files /dev/null and b/public/404.mp4 differ
diff --git a/public/50x.html b/public/50x.html
new file mode 100644
index 0000000..b61e9b3
--- /dev/null
+++ b/public/50x.html
@@ -0,0 +1,54 @@
+
+
+
+
+
+ 50x Borked
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
50[0234] Borked
+
+
+
+
+ Video from sakugabooru
+
+
+
+
+
+
diff --git a/public/50x.mp4 b/public/50x.mp4
new file mode 100644
index 0000000..8e2b55f
Binary files /dev/null and b/public/50x.mp4 differ
diff --git a/public/favicon.svg b/public/favicon.svg
new file mode 100644
index 0000000..a44e839
--- /dev/null
+++ b/public/favicon.svg
@@ -0,0 +1,3 @@
+
diff --git a/public/self.jpg b/public/self.jpg
new file mode 100644
index 0000000..669ad0f
Binary files /dev/null and b/public/self.jpg differ
diff --git a/public/site.css b/public/site.css
new file mode 100644
index 0000000..69133f7
--- /dev/null
+++ b/public/site.css
@@ -0,0 +1 @@
+@import url("https://fonts.googleapis.com/css?family=Noto+Sans|Noto+Serif|Noto+Sans+Mono");:root{--default-serif-font: "Noto Serif", serif;--default-sans-font: "Noto Sans", sans-serif;--default-mono-font: "Noto Sans Mono", monospace;--default-font-size: 16px;--header-font-size: 2.0em;--default-line-height: 1.5;--background-color: #1d2021;--foreground-color: #ebdbb2;--link-color: #458588;--muted-color: #928374;--primary-accent-color: #b16286;--secondary-accent-color: #689d6a;--code-background-color: #282828;--inline-code-background-color: #3c3836}.accent{color:var(--primary-accent-color)}html,body{height:100%}body{display:flex;flex-direction:column;margin:0;line-height:var(--default-line-height);font-size:var(--default-font-size);font-family:var(--default-serif-font);background-color:var(--background-color);color:var(--foreground-color);border-top:.5em solid var(--primary-accent-color)}main{flex:1 0 auto;}#content{max-width:48em;margin-left:auto;margin-right:auto;padding-right:1.4em;padding-left:1.4em;padding-bottom:1.4em}header{overflow:hidden;margin-top:1.4em;margin-bottom:1.4em;margin-left:1em;margin-right:1em}header>h1{text-align:center;font-size:var(--header-font-size);font-family:var(--default-sans-font);padding-top:2.8rem;margin-top:0;margin-bottom:.7rem}header .post-meta{text-align:center;padding-top:.5em;padding-bottom:.9em;margin-bottom:0}.post-meta time{color:var(--muted-color)}h2,h3,h4,h5,h6{padding-top:1.2rem;margin-bottom:.2rem}p{padding-top:.4em;margin-bottom:1em}blockquote{font-family:var(--default-sans-font);font-style:italic;margin:0 1.25rem;padding-left:1.25rem;padding-right:1.25rem;border-left:3px solid var(--secondary-accent-color)}a{color:var(--link-color);text-decoration:none}:where(h2,h3,h4,h5,h6)>a[href^="#"]::before{content:"#";display:inline-block;width:1.2rem;margin-left:-1.2rem;opacity:0}:where(h2,h3,h4,h5,h6):hover>a[href^="#"]::before{opacity:1}:where(h2,h3,h4,h5,h6):target>a[href^="#"]::before{opacity:1;color:var(--primary-accent-color)}img,video{width:100%;height:auto}figure{margin:0}figcaption{font-size:.85rem;color:var(--muted-color)}pre{display:block;padding:.25rem 1.25rem;font-size:85%;border-left:.25rem solid var(--primary-accent-color)}code{padding:0 .4rem;background-color:var(--inline-code-background-color);border-radius:2px}pre,code{overflow:auto;font-family:var(--default-mono-font)}pre>code{padding:0;background-color:var(--code-background-color);border-radius:unset}table{display:block;overflow-x:auto;margin-left:auto;margin-right:auto;text-align:left;border-collapse:collapse}th[scope=col]{padding-top:.4em;border-bottom:3px solid var(--secondary-accent-color)}th[scope=row]{border-right:3px solid var(--secondary-accent-color)}td{text-align:center;border:1px solid var(--muted-color)}th,td{vertical-align:bottom;padding-left:.4em;padding-right:.4em;border:1px solid var(--muted-color)}footer{border-top:.5em solid var(--primary-accent-color);text-align:center;background-color:var(--code-background-color);padding:1.4em;flex-shrink:0;}footer a{color:var(--secondary-accent-color)}footer>nav>a{margin:0 .4em}footer p{padding-top:0}footer p,footer a{font-family:var(--default-sans-font);font-variant-caps:all-small-caps}@media (min-width: 470px){:root{--default-font-size: 17px}header{margin-left:2.8em;margin-right:2.8em}}@media (min-width: 625px){:root{--default-font-size: 18px}}@media (min-width: 802px){:root{--default-font-size: 19px}}@media (min-width: 1003px){:root{--default-font-size: 20px}}@media (min-width: 1225px){:root{--default-font-size: 21px}:where(h2,h3,h4,h5,h6)>a[href^="#"]::before{width:1.8rem;margin-left:-1.8rem}}@media (min-width: 1496px){:root{--default-font-size: 22px}}
\ No newline at end of file
diff --git a/static/keybase.txt b/static/keybase.txt
deleted file mode 100644
index 0cc8b80..0000000
--- a/static/keybase.txt
+++ /dev/null
@@ -1,82 +0,0 @@
-==================================================================
-https://keybase.io/reillysiemens
---------------------------------------------------------------------
-
-I hereby claim:
-
- * I am an admin of https://tuckersiemens.com
- * I am reillysiemens (https://keybase.io/reillysiemens) on keybase.
- * I have a public key with fingerprint EA26 C944 1A6C 40E4 0E73 B4EC 1B45 8E7C 7CB1 54A9
-
-To do so, I am signing this object:
-
-{
- "body": {
- "client": {
- "name": "keybase.io node.js client",
- "version": "0.7.5"
- },
- "key": {
- "fingerprint": "ea26c9441a6c40e40e73b4ec1b458e7c7cb154a9",
- "host": "keybase.io",
- "key_id": "1B458E7C7CB154A9",
- "uid": "eb4bae03913d61f8dbaecec09f80f300",
- "username": "reillysiemens"
- },
- "merkle_root": {
- "ctime": 1429866644,
- "hash": "e0d63404eea56d53642bc5843899d777ab377056b09c5e7bb71264f686bed4ebf84e74a7a8eb3bcce875a736563ddb16b8dc8e64407c1faf37954a0e44a29570",
- "seqno": 209926
- },
- "service": {
- "hostname": "tuckersiemens.com",
- "protocol": "https:"
- },
- "type": "web_service_binding",
- "version": 1
- },
- "ctime": 1429866655,
- "expire_in": 157680000,
- "prev": "2fc94aa693b2d1a220a6dfe6d250809aab5495ba850356e72d03998d8763e716",
- "seqno": 39,
- "tag": "signature"
-}
-
-which yields the signature:
-
------BEGIN PGP MESSAGE-----
-Version: GnuPG v2
-
-owFdkntQFVUcxwHTkAli5CGJpm1SqQzu8+zuDZoAHQokwGCkgLmes3suLI+717v3
-8hBuNJSOxYwCyjNEM0EaJwswJrMZ5BED0oMgEsTBGXVUBGdUfDQ5UrsMTY8z+8c5
-ez6/7+/3/f1OhfcSNw/34K37S0rakj93H5pGbikmzyPFBFLlIsJUTEi5CrY6jJ0V
-5mHCROTgIgQ1HKao66yqjMOytXWLTCiRj+2aolp1igzjwzjCFWrgRrBFsWZiu82u
-GFoEhjSQRJalIJBYEusfzyAWSxRiOQHzEi8himOhqEtmqZrjP1mJBU2zIut/qSid
-38pH89FROh9p8M6FC4xYBDHJiBQjA8oiyPpJwhIpWgTSwpCkAWrYvmjJjpXc3CJN
-wXnYqhlF52F7Ti4221V1wbnkUAyOYmlRAACwrF4W1LKMPKQMGJZkMYYckDkGsDSS
-OIFlBFGUeZ6HiOF5kgOIFCUO8wjxFA1YCxAAwjKLkUVgMc9CHgoYMUiSsMBzkGcA
-BxhZRhRAgiwJWM9I8hJlgRaGF/W+6B1jIS1yvOFDw7usKmGiSVGkgcs42/MVCRt1
-G71btOhwSjnGcBYshklqnh5ps6sOVVJz9essh8OmmQzrZxxFNiOgACPzopQZKVZZ
-n9+/B0zp6P/awnGhBC60KXZsVgyC44FA6stIhPN1SdqizxxCIDKIlilI0yQEsgUD
-meZIgRQhRBwrcggKHMlwAPO0rA9QFGSBBwzmKfCPWUYMJRwwU9fUlEwrdDjtmHB9
-5LH6KTd3D7dlSz2MJ+zmtdz373c9ctTX7XjvsaiJMVS1q6JPLpkJeHK8o6g9/7U/
-fc5eS2xRAiK+OPhZGrFFVR6/DjegNd53/5iLVHePdAWP1JMDE/1Nxzbv/bY5zhZI
-zFb7ppcWSVeeHXQNQ1fjzHB5Z5LPvam15fs/LM7a26V1HZxMfyGjNPz9edT6/TPj
-c/FPKpNnU6PiY+JP3a1Ju1lxO2zl7QDbxd9PvHLO78seT+W5lx9S2RkhiaaX3pge
-W4LO91zwKqu49vb0YPV41cTHcUmxsTEtLS9qDW2zYw8P3DsyKp+ZTH++vKB228z2
-5DeTuiamX3XUXf457daB6k/mmh94Td2vs9TFlx4+nXD7pHAxpj4R3v1uvjf1xLuX
-BzZvd86UtkzdTIisT63daC+81DlaadoZiOev7gv64Zss6kLNjbLrV5ed7/QJVHz9
-Sk5zO/qTen+LWNUY51yRMThX3QdDhwtii9lNYlXkoeD+X8fvfRW4vpyt+6W0z/RB
-fnx0dPfaB0ebGvbYUobYMk9tU3jXeEVffPudiMOtA+tvmMse/RjkvXRVlEpGt7ra
-x84OZdf03hA8rKk/3W8FY9u6H0+OiiFPX38nZLA3ZnvT11GpG9+64+8XvK9yx60N
-59jKFPHWxCkqIbJxdf8W1/KV7o4VHbUh4WsCOm5Wzp1sejQ6fOm9+aQrzXt27h08
-9GlDm9O9LNPfGRPUbYYm/xO7ewqL/wI=
-=JQ/H
------END PGP MESSAGE-----
-
-And finally, I am proving ownership of this host by posting or
-appending to this document.
-
-View my publicly-auditable identity here: https://keybase.io/reillysiemens
-
-==================================================================