Initial Commit

This commit is contained in:
Logan 2024-04-28 17:11:48 -05:00
commit 72b700daef
55 changed files with 8139 additions and 0 deletions

4
.gitignore vendored Normal file
View file

@ -0,0 +1,4 @@
/target
lgatlin.dev.cert
lgatlin.dev.priv
/logs

56
404.html Normal file
View file

@ -0,0 +1,56 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<link type="image/x-icon" href="resources/favicon.svg" rel="icon" />
<link rel="stylesheet" href="/styles/main.css" />
<link href="/styles/extend.css" rel="stylesheet" />
</head>
<body>
<div class="pure-g">
<div class="home-menu pure-menu pure-menu-horizontal">
<ul class="pure-menu-list">
<li class="pure-menu-item">
<a href="/home" class="pure-menu-link">
Home
</a>
</li>
<li class="pure-menu-item">
<a href="/projects" class="pure-menu-link">
Projects
</a>
</li>
<li class="pure-menu-item">
<a class="pure-menu-link" href="/blog">
Blog
</a>
</li>
<li class="pure-menu-item">
<a class="pure-menu-link" href="/about">
About
</a>
</li>
</ul>
</div>
<div class="pure-u-1-1">
<div class="header">
<h1>
Page does not exist
</h1>
<h2>
Error 404
</h2>
<a class="link" href="/home">
<button class="pure-button pure-button-primary">
Return Home
</button>
</a>
</div>
</div>
</div>
</body>
</html>

2462
Cargo.lock generated Normal file

File diff suppressed because it is too large Load diff

15
Cargo.toml Normal file
View file

@ -0,0 +1,15 @@
[package]
name = "server"
version = "0.1.0"
edition = "2021"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[dependencies]
tiny_http = {version="0.12", features=["ssl-openssl"]}
log4rs = "1.3"
log = "0.4"
tokio = {version = "1", features = ["full"]}
ascii = "1.1"
fishbowl = {path="../fishbowl"}

BIN
html Executable file

Binary file not shown.

3
hyper-build/404.html Normal file
View file

@ -0,0 +1,3 @@
<div class="header"><h1> Page does not exist </h1><h2> Error 404 </h2></div><a href="/home" class="link"><button class="pure-button pure-button-primary button-xlarge">
Return Home
</button></a>

358
hyper-build/home.html Normal file

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

1
hyper-src/404.html Normal file
View file

@ -0,0 +1 @@
<NotFound/>

108
hyper-src/home.html Normal file
View file

@ -0,0 +1,108 @@
<Container>
<div class="header">
<h1 id="title" />
<h2>
<a class="link" href="/resume.pdf"> My Resume </a> -
<a class="link" href="http://github.com/Xterminate1818"> GitHub </a> -
<a class="link" href="mailto:logan@gatlintc.com"> Email Me </a>
</h2>
</div>
<script>
var i = 0;
var txt = 'Welcome to my lgatlin.dev';
var speed = 30;
function typeWriter() {
if (i < txt.length) {
document.getElementById("title").innerHTML += txt.charAt(i);
i++;
setTimeout(typeWriter, speed + Math.random() * 25.0);
}
}
typeWriter()
</script>
<div class="content">
<h2 class="distinct"> Dev Posts </h2>
<Card href="/projects/http-server">
<RustIcon />
<CloudflareIcon />
<h1> Web Server </h1>
<h3> Back End - TCP - SSL </h3>
<h2>
Currently serving you this website!
</h2>
</Card>
<Card href="/projects/html-templating">
<RustIcon />
<HtmlIcon />
<h1> HTML Templating Engine </h1>
<h3> Front End - Parser Design </h3>
<h2>
Used to generate this page!
</h2>
</Card>
<Card href="/projects/forte">
<RustIcon />
<WasmIcon />
<h1> Forte Assembly Language </h1>
<h3> Programming Language - Hackathon </h3>
<h2>
Radically different machine code. A creative-coding endeavor
</h2>
</Card>
<Card href="/projects/fishbowl">
<RustIcon />
<WgpuIcon />
<h1> Fishbowl </h1>
<h3> Image Encoding - Hardware Rendering </h3>
<h2> Kinematic image processing with GPU acceleration </h2>
</Card>
<Card href="/projects/math-interpreter">
<RustIcon />
<WasmIcon />
<h1> Math Interpreter </h1>
<h3> Parser Design </h3>
<h2>
Interpret and evaluate plain-text math expressions
</h2>
</Card>
<Card href="/projects/nd-range">
<RustIcon />
<h1> nd-range </h1>
<h3> Vector Math - Standard Library </h3>
<h2>
An extension of Rust's 'Range' type
using the Cartesian Product Algorithm
</h2>
</Card>
<Card href="/projects/fractal-explorer">
<RustIcon />
<RaylibIcon />
<h1> Fractal Explorer </h1>
<h3> Parallel Algorithms - Optimization - Hackathon </h3>
<h2>
A Mandelbrot Fractal viewer using CPU parallelism
and the derivative bail algorithm
</h2>
</Card>
<Card href="/projects/pokedex">
<PythonIcon />
<h1> Pokédex </h1>
<h3> TKinter - Web APIs - Native UI </h3>
<h2>
A TKinter app for viewing the original Pokédex, with
stats scraped from online sources
</h2>
</Card>
<Card href="/projects/stock-trading">
<PythonIcon />
<h1> Stock Trading A.I. </h1>
<h3> Command Line App - Web APIs </h3>
<h2>
A simple heuristic trading algorithm
</h2>
</Card>
</div>
</Container>

View file

@ -0,0 +1,115 @@
<Container>
<div class="header">
<h1> Fishbowl <GithubIcon href="https://github.com/Xterminate1818/fishbowl"/> </h1>
<h2> Vector Math - Standard Library </h2>
</div>
<div class="content">
<h2 class="distinct"> Gallery </h2>
<div class="pure-g">
<div class="pure-u-1-3"> <img class="pure-img" src="/resources/mona_lisa.gif" title="Mona Lisa - Leonardo da Vinci" />
</div>
<div class="pure-u-1-3">
<img class="pure-img" src="/resources/wanderer.gif"
title="Wanderer above the Sea of Fog - Caspar David Friederich" />
</div>
<div class="pure-u-1-3">
<img class="pure-img" src="/resources/starry_night.gif" title="The Starry Night - Vincent van Gogh" />
</div>
</div>
<h2 class="distinct"> Motivation </h2>
Physics solvers tend to be very complex software. In the
past, I toyed around with physics solvers for different kinds of geometries, but
struggled to implement these algorithms in a way that was both convincing
and performant. The idea for Fishbowl came from a video by the creator
Pezzza. They describe the high-level implementation details of a Verlet
Integration solver. This approach caught my interest.
<div class="video">
<iframe src="https://www.youtube-nocookie.com/embed/lS_qeBy3aQI?si=gLrHCWO-XUSP0T0j" title="YouTube video player"
frameborder="0"
allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share"
referrerpolicy="strict-origin-when-cross-origin" allowfullscreen>
</iframe>
</div>
Pezza's work is very impressive, particularly the demonstration at the end
which proves the program is deterministic. However, the focus of Pezzza's
project was to create a performant real-time application. I was instead
interested in pre-rendered images and animations. I decided to expand the
one-off demonstration at the end of Pezzza's video into a fully fledged
app.
<h2 class="distinct"> Approach </h2>
At the center of any physics solver is the physics 'step', a fixed interval of
time where objects move and collide. The physics step of Fishbowl consists of five
distinct phases.
<@code lang="rust">
// Keep all objects inside the simulation bounds
self.constrain_rect();
// Sort objects from left-most to right-most.
self.sort();
// Apply collision impulses
self.collide();
// Calculate new velocities and accelerations
self.integrate();
// Finally, advance the simulation clock
self.clock += 1;
</@code>
Because the program doesn't run in real time, simulating over one second
may take more or less than a second. Calculating a larger number of shorter
time steps will make the simulation more accurate, but will take longer to
process. Fishbowl uses an number of optimizations to reduce processing time.
By keeping the array of objects sorted from left to right, the number of collision
checks is significantly reduced. The rendering system is decoupled from the simulation,
and the two are pipelined together and run in parallel.
<h2 class="distinct"> Rendering </h2>
The method I used for drawing circles to a frame went through numerous iterations.
Rendering performance is a major bottleneck, and so it was important to do so in
parallel. After writing and benchmarking several software renderers, I landed on
WebGPU. While quite bloated, WebGPU provides a fast, low level, and cross platform
rendering API designed for async programs. This allows Fishbowl to easily integrate
with both my server, which is entirely async, and the simulation, which is mostly
synchronous. While I would have liked to use a bespoke solution, GPU accelerated
rendering is far too quick to realistically consider any other option.
<h2 class="distinct"> Try It Out </h2>
You can try out Fishbowl below, or download the CLI tool from my GitHub.
It works best on images that are roughly square and don't contain any text.
Keep in mind the web version below will be slower and generate lower quality
results than the native application, as it is being run on my home server with
limited resources.
<br/><br/>
<img class="pure-img centered" width=512 height=512 id="result_image" hidden />
<form class="pure-form-stacked" style="justify-content: center;" method="post" enctype="multipart/form-data" id="fishForm">
<fieldset>
<label for="file"> Select an image </label>
<input id="file" type="file" name="image" required
accept="image/bmp,image/gif,image/vnd.microsoft.icon,image/jpeg,image/png,image/tiff,image/webp"/>
<br/>
<input class="pure-button pure-button-primary" type="submit" value="Run Fishbowl"/>
</fieldset>
</form>
<!-- Handle displaying image -->
<script>
const form = document.getElementById("fishForm");
const image = document.getElementById("result_image");
form.onsubmit = async (ev) => {
ev.preventDefault();
image.src = "";
image.hidden = false;
image.classList.add("loading");
const form = ev.currentTarget;
const action = form.action;
try {
const form_data = new FormData(form);
const response = await fetch(action, {
method: 'POST',
body: form_data,
});
const blob = await response.blob();
image.src = URL.createObjectURL(blob);
image.classList.remove("loading");
} catch (error) {
console.log(error);
}
}
</script>
</div>
</Container>

View file

@ -0,0 +1,203 @@
<Container>
<div class="header">
<h1> Forte <GithubIcon href="https://github.com/Xterminate1818/forte"/> </h1>
<h2> Programming Language - Hackathon </h2>
</div>
<div class="content">
<h2 class="distinct"> Motivation </h2>
In February of 2024, I competed at RowdyHacks during the 9th
annual Hackathon. This was my second time attending, having
<a class="link" href="/projects/fractal-viewer">
placed second the previous year
</a>.
My understanding of programming and of Rust had grown significantly over
that time, and I wanted to one-up myself. I had just finished my Computer
Organization class, where we learned to program in x86 assembly. The class
made me realize that the design of an assembly language has a profound
impact on every higher level language built on-top of it. As a Rust
programmer who cares a great deal about safety and soundness guarantees, it
troubles me that I have to compile to an inherently unsafe assembly
language. I wanted to find out how feasible it would be to write a new
assembly language designed for easy static analysis and safety guarantees.
I am very satisfied with the result, which I named Forte.
<h2 class="distinct"> Specifications </h2>
<h3> Instructions </h3>
Forte is an assembly language and bytecode for a hypothetical 128-bit
processor. It contains 26 instructions, but no directly exposed registers.
<div class="pure-g">
<table class="striped pure-u-1-2">
<thead>
<th> Name </th>
<th> Mnemonic </th>
</thead>
<tbody>
<tr>
<td> Push </td>
<td> push </td>
</tr>
<tr>
<td> Pop </td> <td> pop </td>
</tr>
<tr>
<td> Duplicate </td>
<td> dup </td>
</tr>
<tr>
<td> Add </td>
<td> add </td>
</tr>
<tr>
<td> Difference </td>
<td> diff </td>
</tr>
<tr>
<td> Multiply </td>
<td> mul </td>
</tr>
<tr>
<td> Divide </td>
<td> div </td>
</tr>
<tr>
<td> Remainder </td>
<td> rem </td>
</tr>
<tr>
<td> Bitwise And </td>
<td> and </td>
</tr>
<tr>
<td> Bitwise Or </td>
<td> or </td>
</tr>
<tr>
<td> Bitwise Xor </td>
<td> xor </td>
</tr>
<tr>
<td> Shift Right </td>
<td> shr </td>
</tr>
<tr>
<td> Shift Left </td>
<td> shl </td>
</tr>
</tbody>
</table>
<table class="striped pure-u-1-2">
<thead>
<th> Name (cont.) </th>
<th> Mnemonic (cont.) </th>
</thead>
<tbody>
<tr>
<td> Branch Equal </td>
<td> beq </td>
</tr>
<tr>
<td> Branch Unequal </td>
<td> bne </td>
</tr>
<tr>
<td> Branch Greater </td>
<td> bgt </td>
</tr>
<tr>
<td> Branch Lesser </td>
<td> blt </td>
</tr>
<tr>
<td> Function Start </td>
<td> fun </td>
</tr>
<tr>
<td> Call </td>
<td> call </td>
</tr>
<tr>
<td> Return </td>
<td> ret </td>
</tr>
<tr>
<td> Loop </td>
<td> loop </td>
</tr>
<tr>
<td> Iterate Loop </td>
<td> iter </td>
</tr>
<tr>
<td> Begin Execution </td>
<td> exe </td>
</tr>
<tr>
<td> Store </td>
<td> sto </td>
</tr>
<tr>
<td> Load </td>
<td> lod </td>
</tr>
<tr>
<td> Stack Length </td>
<td> len </td>
</tr>
</tbody>
</table>
</div>
<h3> The Three Stacks </h3>
The stack is an essential part of any assembly language, however it is
often a point of vulnerability. Stack-smashing and stack-overflow
vulnerabilities can very easily allow attackers to overwright function
return addresses, and and execute arbitrary code. This is why Forte uses
three distinct stacks.
<h3> The Program Stack </h3>
The program stack, or p-stack for short, contains all of the state for the
user-space program. When you push or pop a value in Forte, you are
interacting with the p-stack. What's most interesting about the p-stack is
what it does not contain: pointers. There is no way to jump to an address
on the p-stack, as its contents are considered untrusted.
<h3> The Control Stack </h3>
In order to call functions, return addresses must be stored somewhere.
While most assembly languages place these on the stack alongside other
values, Forte segregates them into a separate control stack (c-stack for
short.) This is similar to the "shadow stack" option some compilers use,
except implemented at a hardware level. The only way to interact with the
c-stack is through call and return instructions.
<h3> The Function Stack </h3>
Most assembly languages have a "jump" instruction which moves the program
counter to an arbitrary point in memory. This is useful but not
particularly safe. Forte only allows jumping to valid functions. The
locations of these functions are kept in the function stack (f-stack.)
Functions are added to the f-stack after they are validated during the
warmup phase, which I will discuss next.
<h3> Warmup </h3>
When a Forte program begins, it does not immediately start executing
instructions. Instead, it validates the programs correctness and builds
the f-stack. Every <code>fun</code> (function start) instruction will
push the address of that instruction to the f-stack. Every instruction that
interacts with the stack will increment or decrement an internal register
accordingly. If this value value falls below zero or above the maximum
stack size, then the program is determined to be unsafe and execution
is cancelled before it begins. The warmup phase will take O(N) time,
where N is the number of instructions.
<h3> Recital </h3>
After the <code>exe</code> (begin execution) instruction is reached, the
Recital phase begins. The function pointer returns to the address at the
top of the f-stack, which was the last defined function. When the program
counter reaches the <code>exe</code> again, the program has terminated
successfully. This combination of design decisions means that the program
counter can never be lower than the first function address, or larger than
the execution instruction. This makes arbitrary code execution attacks more
difficult to pull off.
<h2 class="distinct"> Safety </h2>
Creating a memory safe assembly language presents much different challenges
from a compiled language like Rust. There are no compile-time checks or
guarantees, any string of bytes could be interpreted as a "program." The core
design principle of Forte is to make unsecure programs impossible (or at least
very difficult) to express, and to make static analysis simple. This is a delicate
balance to strike - limiting Forte's capabilities necessarily makes it more
cumbersome and less performant.
</div>
</Container>

View file

@ -0,0 +1,97 @@
<Container>
<div class="header">
<h1> Fractal Explorer <GithubIcon href="https://github.com/Xterminate1818/rowdyhacks-2023"/> </h1>
<h2> Parallel Algorithms - Optimization - Hackathon Finalist </h2>
</div>
<div class="content">
<h2 class="distinct"> Motivation </h2>
In March of 2023 I attended RowdyHacks 8, my first hackathon. I was
in the middle of my Calculus II class, which I was enjoying a lot. With
Math at the front of my mind, I decided to write a program to visualize
the Mandelbrot Fractal. I had written a similar program in Snap, a visual
block-based programming language, so I was confident I could achieve results
within the 24 hour time limit. I wanted to specifically optimize the program
for speed without using GPU acceleration, because the laptop I was using did
not have one.
<h2 class="distinct"> Approach </h2>
The premise behind the Mandelbrot Fractal is to take a complex number, and
insert it into a recursive function. If the function does not diverge to
infinity, then that number is part of the Mandelbrot Set, and it gets plotted
on the screen.
Calculating the Mandelbrot Set is deceptively simple. The hard part is doing so
with a very large degree of precision, and repeating the calculations millions of
times (prefferably in parallel.)
<@code lang="rust">
// Snippet from my initial naive algorithm
//
// This will produce the value of one pixel on the screen
pub fn fractal(val: Complex, max_iterations: usize) -> usize {
let mut iterations = 0;
let mut last = Complex::new(0.0, 0.0);
let mut squared = Complex::new(0.0, 0.0);
// Values exceeding 4 usually diverge. If the number
// does not diverge after `max_iterations`, it probably
// never will.
while squared.re + squared.im <= 4.0 &&
iterations < max_iterations {
let im = 2.0 * last.re * last.im + val.im;
let re = squared.re - squared.im + val.re;
last = Complex { re, im };
squared.re = re.powi(2);
squared.im = im.powi(2);
iterations += 1;
}
iterations
}
</@code>
While researching the Mandelbrot set, I found a faster solution called the derivative
bail algorithm. This algorithm considers the first derivative of the function rather
than the functions value itself, which is more accurate and performant most of the time.
<@code lang="rust">
// Snippet from my derivative bail implementation
pub fn get_dbail(val: Complex, max_dvt: f64, max_iter: usize) -> usize {
let max_dvt = max_dvt.powi(2);
let mut it = 0;
let mut last = Complex::new(0.0, 0.0);
let mut squared = Complex::new(0.0, 0.0);
let mut deriv = Complex::new(0.0, 0.0);
while squared.re + squared.im <= 4.0
&& deriv.re.powi(2) + deriv.im.powi(2) <= max_dvt
&& it <= max_iter
{
deriv.re = 2.0 * (deriv.re * val.re - deriv.im * val.im);
deriv.im = 2.0 * (deriv.re * val.im + deriv.im * val.re);
let im = 2.0 * last.re * last.im + val.im;
let re = squared.re - squared.im + val.re;
last = Complex { re, im };
squared.re = re.powi(2);
squared.im = im.powi(2);
it += 1;
}
it
}
</@code>
Deciding whether a number does or does not fall into the set is more of a heuristic
than an analytic process. A good algorithm balances accuracy with efficiency. The
most glaring limitation of any calculation is the precision of the floating point
numbers used, especially when zooming in to the fractal. I deliberated the pros and
cons of using a floating point library with very high, or even arbitrary precision.
After extensive benchmarking, I was unable to get any of these to be efficient enough.
<h2 class="distinct"> Multi-Threading </h2>
In order to display the fractal, I needed an array of pixel data I could push to the
screen. The X and Y coordinates represent real and imaginary components respectively.
This means I need to run the function for every pixel on the screen. The Mandelbrot
Fractal is an obvious candidate for multi-threading, because the result of every
calculation is independent, and there are many calculations that need to be done.
I achieve this by breaking the pixel buffer into N partitions, and spawning a thread
to handle each (where N can be changed at runtime). This allowed me to rapidly iterate,
and find the optimal number of threads for the best performance.
<h2 class="distinct"> Results </h2>
I finished the project right before the deadline. This was my first time competing at
a hackathon, and the most code I had ever written in a 24 hour period. I was absolutely
exhausted, with no real idea of what to expect. I presented my project to the judges and
the other hackers at the event. The reception was incredibly positive, and I met many
brilliant people I stay in contact with to this day. My project won 2nd prize overall
at the event, for which I am extremely honored and greatful.
</div>
</Container>

View file

@ -0,0 +1,111 @@
<Container>
<div class="header">
<h1> HTML Templating Engine <GithubIcon href="https://github.com/Xterminate1818/html"/> </h1>
<h2> Front End - Parser Design </h2>
</div>
<div class="content">
<h2 class="distinct"> Motivation </h2>
HTML is a powerful tool for creating static web pages, but
is not easily modular or scalable. Front end frameworks like
React provide reusable components. I want to use components
without the overhead of a front-end framework. I want the final output to be statically generated rather than generated on the fly
by the server or the client, minified and with inlined CSS and
JavaScript.
<h2 class="distinct"> Approach </h2>
After looking at some of the HTML parsing libraries, I was
unsatisfied with the approaches most developers took. The DOM
is represented as a tree structure, where each node keeps a
pointer to its children.
<@code lang="rust">
// From the html_parser crate
pub enum Node {
Text(String),
Element(Element),
Comment(String),
}
pub struct Element {
pub name: String,
pub attributes: HashMap<String, Option<String>>,
pub children: Vec<Node>,
// other fields omitted
}
</@code>
This is bad for cache locality and effectively forces the use
of recursion in order to traverse the DOM. Recursion is undesirable
for performance and robustness reasons. My approach is to create a
flat array of elements. Parsing is done using a non-recursive parser
combinator model.
<@code lang="rust">
// My implementation (fields omitted)
pub enum HtmlElement {
DocType,
Comment(/* */),
OpenTag { /* */ },
CloseTag { /* */ },
Text( /* */ ),
Script { /* */ },
Style { /* */ },
Directive { /* */ },
}
</@code>
This approach lends itself to linear iterative algorithms. An
element's children can be represented as a slice of the DOM array,
from the opening tag to its close tag.
<h2 class="distinct"> Templates </h2>
I define a "template" as a user defined element which expands into
a larger piece of HTML. Templates can pass on children and attributes
to the final output. Some special templates can perform other functions,
like inlining a CSS file or syntax highlighting a code block.
<@code lang="html">
<!-- Here is a template definition -->
<Echo>
<h1 class=@class1>
<@children/>
</h1>
<h2 class=@class2>
<@children/>
</h2>
<h3 class=@class3>
<@children/>
</h3>
</Echo>
<!-- And this is how to use the template -->
<Echo class1="some_class1" class2="some_class2" class3="some_class3">
Echo!
</Echo>
<!-- Which expands to this -->
<h1 class="some_class1">
Echo!
</h1>
<h2 class="some_class2">
Echo!
</h2>
<h3 class="some_class3">
Echo!
</h3>
</@code>
The example above shows how templates can inherit attributes and child
elements from their invocation. Special templates begin with the '@'
symbol, and perform some sort of meta-function. In addition to
&#60;@children/&#62;, &#60;@code/&#62; performs syntax highlighting, and
&#60;@style/&#62; inlines a CSS file.
<h2 class="distinct"> Dependencies </h2>
The templating engine only relies on two crates for performing syntax
highlighting of code blocks. It uses inkjet, a thin wrapper over
treesitter. Treesitter is an efficient error-tolerant language parser
commonly used in IDEs. While it is fast, it is by far the slowest and
most cumbersome part of the program. Good looking formatted code is
important for my website, and implementing parsers for every language
I use is out of scope, so this is a necessary evil.
<@code lang="toml">
// Snippet from Cargo.toml
[dependencies]
inkjet = "0.10"
v_htmlescape = "0.15"
</@code>
</div>
</Container>

View file

@ -0,0 +1,62 @@
<Container>
<div class="header">
<h1> Rust HTTP Server </h1>
<h2> Keywords: Back End - TCP - SSL </h2>
</div>
<div class="content">
<h2 class="distinct"> Motivation </h2>
As a systems programmer, I want to understand the technologies
I use at a very low level. Modern web stacks abstract away the
process of handling TCP connections, serving SSL certificates,
and parsing HTTP requests. The best way to understand a technology
is to implement it from the ground up, I studied the HTTP 1.1
specification and wrote a server to host my website.
<h2 class="distinct"> Approach </h2>
At the most basic level, the server will translate the request URL
into a local file path, and respond with the file matching that path
if it exists. Request URLs are sanitized to prevent accessing files
outside of the server's directory. Two tables are used for rewriting
and routing URLs. This makes handling URLs predictable and robust.
<@code lang="rust">
// Snippet from the request handling code
pub async fn construct_response(&self, request_url: &str) -> Response {
let sanitized_url = rewrite_url(request_url);
if sanitized_url != request_url {
return Self::redirect(&sanitized_url);
}
let routed_url = self.perform_routing(&sanitized_url).await;
if let Some(response) =
self.perform_redirecting(&routed_url).await {
return response;
}
match self.get_resource(&routed_url).await {
Some(bytes) => Response::from_data(bytes),
None => Self::not_found(),
}
}
</@code>
I use Cloudflare to manage my domain name, DNS records, and SSL certificates.
Cloudflare automatically caches resources, rewrites HTTP requests to HTTPS,
and limits request sizes. This allows my server to only listen on port 443
and neglect caching server-side.
<h2 class="distinct"> Dependencies </h2>
My goal with this project was to provide a functional server using as few
libraries as possible. The final project directly depends on four crates:
<@code lang=toml>
// Snippet from Cargo.toml
[dependencies]
log = "0.4"
log4rs = "1.3"
tiny_http = "0.12"
tokio = {version = "1", features = ["full"]}
</@code>
The first two crates, log and log4rs, provide logging functions which aren't critical to the
server's functionality. The tiny_http crate provides a simple wrapper for the standard
library TCP stream, and performs SSL encryption. It is used as the backbone
for many Rust web frameworks. While I could implement these features myself, I
decided against it for security and browser compatibility reasons. Finally there is
tokio, an async runtime. This is necessary because Rust does not provide a runtime
by default, and building one is out of scope.
</div>
</Container>

View file

@ -0,0 +1 @@
<Wip/>

View file

@ -0,0 +1,108 @@
<Container>
<div class="header">
<h1> nd-range </h1>
<h2> Vector Math - Standard Library </h2>
</div>
<div class="content">
<h2 class="distinct"> Motivation </h2>
The Rust standard library provides several 'Range' types which
represent integers inside a given bounds (i.e. 1 &#8804; n &#8804; 100).
Ranges can be iterated over, and used for bounds testing of numbers.
In designing video games, it is often necessary to express 2 or 3
dimensional quantities or bounds. Problems like iterating over
voxels in a 3D world, or testing for an intersection between rectangles
lend themselves to the idea of Ranges.
<@code lang="rust">
// A 3D array representing a 128x128x128 voxel world
let world_data = [[[0; 128]; 128]; 128];
// The '0..128' here is a range from 0 to 127 inclusive
for x in 0..128 {
for y in 0..128 {
for z in 0..128 {
let voxel = &world[x][y][z];
// Do something with voxel
}
}
}
</@code>
However, in these contexts the Range type is not particularly helpful.
I am forced to use three different ranges and a triple nested 'for' loop.
The idea for nd-range is to expand the native Range type so that it
can be used over arbitrary dimensions. This allows us to iterate over
X, Y, and Z coordinates in a single loop.
<@code lang="rust">
let world_data = [[[0; 128]; 128]; 128];
for [x, y, z] in nrange!(0..128, 0..128, 0..128) {
let voxel = &world[x][y][z];
// Do something with voxel
}
</@code>
<h2 class="distinct"> Approach </h2>
An nd-range is similar in concept to an axis-aligned bounding box,
or AABB. These are common abstractions used in game development,
and there are well established algorithms that test for overlap.
Iterating over is slightly more complex, requiring some vector
algebra. If we interpret the range of values over each axis as a
vector, we can apply the cartesian product algorithm to find every
position within the bounded space. Below is a diagram from Wikipedia
(CC BY-SA 3.0).
<img src="https://upload.wikimedia.org/wikipedia/commons/4/4e/Cartesian_Product_qtl1.svg"
class="centered"
/>
Because a range is contiguous by definition, generating a cartesian
product is simple and performant. The iterator object has a space
complexity of O(N), where n is the number of dimensions. Rust's const generics
allow the entire struct to exist on the stack, which is a boon to performance.
<h2 class="distinct"> Performance </h2>
My points of comparison are the itertools and cartesian crates, which provide
comparable algorithms. Despite its popularity, I was shocked to discover how
slow the itertools implementation of the cartesian product is. The cartesian-rs
crate is lesser known, and uses a creative macro-based solution. I benchmarked
all three approaches, as well as the nested for-loop base case, over a 100x100x100
range.
<table class="striped">
<thead>
<th scope="col"> Implementation </th>
<th scope="col"> Average Time (ns) </th>
<th scope="col"> Error (+/- ns) </th>
</thead>
<tbody>
<tr>
<th scope="row">
<a href="https://docs.rs/itertools/latest/itertools/trait.Itertools.html#method.cartesian_product">
itertools
</a>
</th>
<td> 1,821,573 </td>
<td> 31,501 </td>
</tr>
<tr>
<th scope="row">
<a href="https://crates.io/crates/cartesian">
cartesian-rs
</a>
</th>
<td> 989,242 </td>
<td> 50,835 </td>
</tr>
<tr>
<th scope="row"> nd-range </th>
<td> 968,853 </td>
<td> 15,792 </td>
</tr>
<tr>
<th scope="row"> nested loops </th>
<td> 911,853 </td>
<td> 46,066 </td>
</tr>
</tbody>
</table>
<h2 class="distinct"> Pros and Cons</h2>
My implementation is competitive with, and possibly faster than
its competitors for my use case. While nd-range only works for
ranges of contiguous integers, itertools and cartesian-rs work
for any iterators. However, by restricting my use case I can
extract more performance gains and integrate better with the Rust
standard library.
</div>
</Container>

View file

@ -0,0 +1 @@
<Wip/>

View file

@ -0,0 +1 @@
<Wip />

Binary file not shown.

4
resources/award.svg Normal file
View file

@ -0,0 +1,4 @@
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" class="bi bi-award" viewBox="0 0 16 16">
<path d="M9.669.864 8 0 6.331.864l-1.858.282-.842 1.68-1.337 1.32L2.6 6l-.306 1.854 1.337 1.32.842 1.68 1.858.282L8 12l1.669-.864 1.858-.282.842-1.68 1.337-1.32L13.4 6l.306-1.854-1.337-1.32-.842-1.68zm1.196 1.193.684 1.365 1.086 1.072L12.387 6l.248 1.506-1.086 1.072-.684 1.365-1.51.229L8 10.874l-1.355-.702-1.51-.229-.684-1.365-1.086-1.072L3.614 6l-.25-1.506 1.087-1.072.684-1.365 1.51-.229L8 1.126l1.356.702z"/>
<path d="M4 11.794V16l4-1 4 1v-4.206l-2.018.306L8 13.126 6.018 12.1z"/>
</svg>

After

Width:  |  Height:  |  Size: 620 B

View file

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 128 128"><path fill="#FFF" d="m115.679 69.288-15.591-8.94-2.689-1.163-63.781.436v32.381h82.061z"/><path fill="#F38020" d="M87.295 89.022c.763-2.617.472-5.015-.8-6.796-1.163-1.635-3.125-2.58-5.488-2.689l-44.737-.581c-.291 0-.545-.145-.691-.363s-.182-.509-.109-.8c.145-.436.581-.763 1.054-.8l45.137-.581c5.342-.254 11.157-4.579 13.192-9.885l2.58-6.723c.109-.291.145-.581.073-.872-2.906-13.158-14.644-22.97-28.672-22.97-12.938 0-23.913 8.359-27.838 19.952a13.35 13.35 0 0 0-9.267-2.58c-6.215.618-11.193 5.597-11.811 11.811-.145 1.599-.036 3.162.327 4.615C10.104 70.051 2 78.337 2 88.549c0 .909.073 1.817.182 2.726a.895.895 0 0 0 .872.763h82.57c.472 0 .909-.327 1.054-.8l.617-2.216z"/><path fill="#FAAE40" d="M101.542 60.275c-.4 0-.836 0-1.236.036-.291 0-.545.218-.654.509l-1.744 6.069c-.763 2.617-.472 5.015.8 6.796 1.163 1.635 3.125 2.58 5.488 2.689l9.522.581c.291 0 .545.145.691.363.145.218.182.545.109.8-.145.436-.581.763-1.054.8l-9.924.582c-5.379.254-11.157 4.579-13.192 9.885l-.727 1.853c-.145.363.109.727.509.727h34.089c.4 0 .763-.254.872-.654.581-2.108.909-4.325.909-6.614 0-13.447-10.975-24.422-24.458-24.422"/></svg>

After

Width:  |  Height:  |  Size: 1.1 KiB

4
resources/favicon.svg Normal file
View file

@ -0,0 +1,4 @@
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" class="bi bi-ethernet" viewBox="0 0 16 16">
<path d="M14 13.5v-7a.5.5 0 0 0-.5-.5H12V4.5a.5.5 0 0 0-.5-.5h-1v-.5A.5.5 0 0 0 10 3H6a.5.5 0 0 0-.5.5V4h-1a.5.5 0 0 0-.5.5V6H2.5a.5.5 0 0 0-.5.5v7a.5.5 0 0 0 .5.5h11a.5.5 0 0 0 .5-.5M3.75 11h.5a.25.25 0 0 1 .25.25v1.5a.25.25 0 0 1-.25.25h-.5a.25.25 0 0 1-.25-.25v-1.5a.25.25 0 0 1 .25-.25m2 0h.5a.25.25 0 0 1 .25.25v1.5a.25.25 0 0 1-.25.25h-.5a.25.25 0 0 1-.25-.25v-1.5a.25.25 0 0 1 .25-.25m1.75.25a.25.25 0 0 1 .25-.25h.5a.25.25 0 0 1 .25.25v1.5a.25.25 0 0 1-.25.25h-.5a.25.25 0 0 1-.25-.25zM9.75 11h.5a.25.25 0 0 1 .25.25v1.5a.25.25 0 0 1-.25.25h-.5a.25.25 0 0 1-.25-.25v-1.5a.25.25 0 0 1 .25-.25m1.75.25a.25.25 0 0 1 .25-.25h.5a.25.25 0 0 1 .25.25v1.5a.25.25 0 0 1-.25.25h-.5a.25.25 0 0 1-.25-.25z"/>
<path d="M2 0a2 2 0 0 0-2 2v12a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V2a2 2 0 0 0-2-2zM1 2a1 1 0 0 1 1-1h12a1 1 0 0 1 1 1v12a1 1 0 0 1-1 1H2a1 1 0 0 1-1-1z"/>
</svg>

After

Width:  |  Height:  |  Size: 993 B

View file

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 128 128"><g fill="#181616"><path fill-rule="evenodd" clip-rule="evenodd" d="M64 5.103c-33.347 0-60.388 27.035-60.388 60.388 0 26.682 17.303 49.317 41.297 57.303 3.017.56 4.125-1.31 4.125-2.905 0-1.44-.056-6.197-.082-11.243-16.8 3.653-20.345-7.125-20.345-7.125-2.747-6.98-6.705-8.836-6.705-8.836-5.48-3.748.413-3.67.413-3.67 6.063.425 9.257 6.223 9.257 6.223 5.386 9.23 14.127 6.562 17.573 5.02.542-3.903 2.107-6.568 3.834-8.076-13.413-1.525-27.514-6.704-27.514-29.843 0-6.593 2.36-11.98 6.223-16.21-.628-1.52-2.695-7.662.584-15.98 0 0 5.07-1.623 16.61 6.19C53.7 35 58.867 34.327 64 34.304c5.13.023 10.3.694 15.127 2.033 11.526-7.813 16.59-6.19 16.59-6.19 3.287 8.317 1.22 14.46.593 15.98 3.872 4.23 6.215 9.617 6.215 16.21 0 23.194-14.127 28.3-27.574 29.796 2.167 1.874 4.097 5.55 4.097 11.183 0 8.08-.07 14.583-.07 16.572 0 1.607 1.088 3.49 4.148 2.897 23.98-7.994 41.263-30.622 41.263-57.294C124.388 32.14 97.35 5.104 64 5.104z"/><path d="M26.484 91.806c-.133.3-.605.39-1.035.185-.44-.196-.685-.605-.543-.906.13-.31.603-.395 1.04-.188.44.197.69.61.537.91zm2.446 2.729c-.287.267-.85.143-1.232-.28-.396-.42-.47-.983-.177-1.254.298-.266.844-.14 1.24.28.394.426.472.984.17 1.255zM31.312 98.012c-.37.258-.976.017-1.35-.52-.37-.538-.37-1.183.01-1.44.373-.258.97-.025 1.35.507.368.545.368 1.19-.01 1.452zm3.261 3.361c-.33.365-1.036.267-1.552-.23-.527-.487-.674-1.18-.343-1.544.336-.366 1.045-.264 1.564.23.527.486.686 1.18.333 1.543zm4.5 1.951c-.147.473-.825.688-1.51.486-.683-.207-1.13-.76-.99-1.238.14-.477.823-.7 1.512-.485.683.206 1.13.756.988 1.237zm4.943.361c.017.498-.563.91-1.28.92-.723.017-1.308-.387-1.315-.877 0-.503.568-.91 1.29-.924.717-.013 1.306.387 1.306.88zm4.598-.782c.086.485-.413.984-1.126 1.117-.7.13-1.35-.172-1.44-.653-.086-.498.422-.997 1.122-1.126.714-.123 1.354.17 1.444.663zm0 0"/></g></svg>

After

Width:  |  Height:  |  Size: 1.8 KiB

1
resources/godot-icon.svg Normal file
View file

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 128 128"><path d="M114.906 84.145s-.172-1.04-.27-1.028l-18.823 1.817a3.062 3.062 0 00-2.77 2.84l-.516 7.413-14.566 1.04-.988-6.72a3.089 3.089 0 00-3.04-2.62H54.067a3.089 3.089 0 00-3.039 2.62l-.988 6.72-14.566-1.04-.516-7.414a3.058 3.058 0 00-2.77-2.84l-18.835-1.816c-.094-.012-.168 1.028-.266 1.028l-.024 4.074 15.954 2.574.52 7.477a3.084 3.084 0 002.843 2.847l20.059 1.434c.078.004.152.008.226.008a3.087 3.087 0 003.031-2.621l1.02-6.915h14.57l1.02 6.915a3.088 3.088 0 003.254 2.613l20.062-1.434a3.084 3.084 0 002.844-2.847l.52-7.477 15.945-2.586zm0 0" fill="#fff"/><path d="M13.086 53.422v30.723c.059 0 .113.003.168.007L32.09 85.97a2.027 2.027 0 011.828 1.875l.582 8.316 16.426 1.172 1.133-7.672a2.03 2.03 0 012.007-1.734h19.868a2.03 2.03 0 012.007 1.734l1.133 7.672 16.43-1.172.578-8.316a2.027 2.027 0 011.828-1.875l18.828-1.817c.055-.004.11-.007.168-.007V81.69h.008V53.42c2.652-3.335 5.16-7.019 7.086-10.116-2.941-5.008-6.543-9.48-10.395-13.625a101.543 101.543 0 00-10.316 6.004c-1.64-1.633-3.484-2.965-5.3-4.36-1.782-1.43-3.79-2.48-5.696-3.703.566-4.223.848-8.379.96-12.719-4.913-2.476-10.155-4.113-15.456-5.293-2.117 3.559-4.055 7.41-5.738 11.176-2-.332-4.008-.457-6.02-.48V20.3c-.016 0-.027.004-.039.004s-.023-.004-.04-.004v.004c-2.01.023-4.019.148-6.019.48-1.683-3.765-3.62-7.617-5.738-11.176-5.3 1.18-10.543 2.817-15.457 5.293.113 4.34.395 8.496.961 12.72-1.906 1.222-3.914 2.273-5.695 3.702-1.813 1.395-3.66 2.727-5.301 4.36a101.543 101.543 0 00-10.316-6.004C12.543 33.824 8.94 38.297 6 43.305c2.313 3.629 4.793 7.273 7.086 10.117zm0 0" fill="#478cbf"/><path d="M98.008 89.84l-.582 8.36a2.024 2.024 0 01-1.88 1.878l-20.062 1.434c-.046.004-.097.004-.144.004-.996 0-1.86-.73-2.004-1.73l-1.152-7.806H55.816l-1.152 7.805a2.026 2.026 0 01-2.148 1.727l-20.063-1.434a2.024 2.024 0 01-1.879-1.879l-.582-8.36-16.937-1.632c.008 1.82.03 3.816.03 4.211 0 17.887 22.692 26.484 50.88 26.582h.07c28.188-.098 50.871-8.695 50.871-26.582 0-.402.024-2.39.031-4.211zm0 0" fill="#478cbf"/><path d="M48.652 65.895c0 6.27-5.082 11.351-11.351 11.351-6.266 0-11.348-5.082-11.348-11.351 0-6.266 5.082-11.344 11.348-11.344 6.27 0 11.351 5.078 11.351 11.344" fill="#fff"/><path d="M45.922 66.566a7.531 7.531 0 01-7.535 7.532 7.534 7.534 0 01-7.535-7.532 7.534 7.534 0 017.535-7.53 7.531 7.531 0 017.535 7.53" fill="#414042"/><path d="M64 78.277c-2.02 0-3.652-1.488-3.652-3.32v-10.45c0-1.831 1.632-3.32 3.652-3.32 2.016 0 3.656 1.489 3.656 3.32v10.45c0 1.832-1.64 3.32-3.656 3.32m15.348-12.382c0 6.27 5.082 11.351 11.351 11.351 6.266 0 11.348-5.082 11.348-11.351 0-6.266-5.082-11.344-11.348-11.344-6.27 0-11.351 5.078-11.351 11.344" fill="#fff"/><path d="M82.078 66.566a7.53 7.53 0 007.531 7.532 7.531 7.531 0 100-15.063 7.53 7.53 0 00-7.53 7.531" fill="#414042"/></svg>

After

Width:  |  Height:  |  Size: 2.7 KiB

1
resources/html5-icon.svg Normal file
View file

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 128 128"><path fill="#E44D26" d="M19.037 113.876L9.032 1.661h109.936l-10.016 112.198-45.019 12.48z"/><path fill="#F16529" d="M64 116.8l36.378-10.086 8.559-95.878H64z"/><path fill="#EBEBEB" d="M64 52.455H45.788L44.53 38.361H64V24.599H29.489l.33 3.692 3.382 37.927H64zm0 35.743l-.061.017-15.327-4.14-.979-10.975H33.816l1.928 21.609 28.193 7.826.063-.017z"/><path fill="#fff" d="M63.952 52.455v13.763h16.947l-1.597 17.849-15.35 4.143v14.319l28.215-7.82.207-2.325 3.234-36.233.335-3.696h-3.708zm0-27.856v13.762h33.244l.276-3.092.628-6.978.329-3.692z"/></svg>

After

Width:  |  Height:  |  Size: 607 B

1
resources/java-icon.svg Normal file
View file

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 128 128"><path fill="#0074BD" d="M52.581 67.817s-3.284 1.911 2.341 2.557c6.814.778 10.297.666 17.805-.753 0 0 1.979 1.237 4.735 2.309-16.836 7.213-38.104-.418-24.881-4.113zm-2.059-9.415s-3.684 2.729 1.945 3.311c7.28.751 13.027.813 22.979-1.103 0 0 1.373 1.396 3.536 2.157-20.352 5.954-43.021.469-28.46-4.365z"/><path fill="#EA2D2E" d="M67.865 42.431c4.151 4.778-1.088 9.074-1.088 9.074s10.533-5.437 5.696-12.248c-4.519-6.349-7.982-9.502 10.771-20.378.001 0-29.438 7.35-15.379 23.552z"/><path fill="#0074BD" d="M90.132 74.781s2.432 2.005-2.678 3.555c-9.716 2.943-40.444 3.831-48.979.117-3.066-1.335 2.687-3.187 4.496-3.576 1.887-.409 2.965-.334 2.965-.334-3.412-2.403-22.055 4.719-9.469 6.762 34.324 5.563 62.567-2.506 53.665-6.524zm-35.97-26.134s-15.629 3.713-5.534 5.063c4.264.57 12.758.439 20.676-.225 6.469-.543 12.961-1.704 12.961-1.704s-2.279.978-3.93 2.104c-15.874 4.175-46.533 2.23-37.706-2.038 7.463-3.611 13.533-3.2 13.533-3.2zM82.2 64.317c16.135-8.382 8.674-16.438 3.467-15.353-1.273.266-1.845.496-1.845.496s.475-.744 1.378-1.063c10.302-3.62 18.223 10.681-3.322 16.345 0 0 .247-.224.322-.425z"/><path fill="#EA2D2E" d="M72.474 1.313s8.935 8.939-8.476 22.682c-13.962 11.027-3.184 17.313-.006 24.498-8.15-7.354-14.128-13.828-10.118-19.852 5.889-8.842 22.204-13.131 18.6-27.328z"/><path fill="#0074BD" d="M55.749 87.039c15.484.99 39.269-.551 39.832-7.878 0 0-1.082 2.777-12.799 4.981-13.218 2.488-29.523 2.199-39.191.603 0 0 1.98 1.64 12.158 2.294z"/><path fill="#EA2D2E" d="M94.866 100.181h-.472v-.264h1.27v.264h-.47v1.317h-.329l.001-1.317zm2.535.066h-.006l-.468 1.251h-.216l-.465-1.251h-.005v1.251h-.312v-1.581h.457l.431 1.119.432-1.119h.454v1.581h-.302v-1.251zm-44.19 14.79c-1.46 1.266-3.004 1.978-4.391 1.978-1.974 0-3.045-1.186-3.045-3.085 0-2.055 1.146-3.56 5.738-3.56h1.697v4.667h.001zm4.031 4.548v-14.077c0-3.599-2.053-5.973-6.997-5.973-2.886 0-5.416.714-7.473 1.622l.592 2.493c1.62-.595 3.715-1.147 5.771-1.147 2.85 0 4.075 1.147 4.075 3.521v1.779h-1.424c-6.921 0-10.044 2.685-10.044 6.723 0 3.479 2.058 5.456 5.933 5.456 2.49 0 4.351-1.028 6.088-2.533l.316 2.137h3.163v-.001zm13.452 0h-5.027l-6.051-19.689h4.391l3.756 12.099.835 3.635c1.896-5.258 3.24-10.596 3.912-15.733h4.271c-1.143 6.481-3.203 13.598-6.087 19.688zm19.288-4.548c-1.465 1.266-3.01 1.978-4.392 1.978-1.976 0-3.046-1.186-3.046-3.085 0-2.055 1.149-3.56 5.736-3.56h1.701v4.667h.001zm4.033 4.548v-14.077c0-3.599-2.059-5.973-6.999-5.973-2.889 0-5.418.714-7.475 1.622l.593 2.493c1.62-.595 3.718-1.147 5.774-1.147 2.846 0 4.074 1.147 4.074 3.521v1.779h-1.424c-6.923 0-10.045 2.685-10.045 6.723 0 3.479 2.056 5.456 5.93 5.456 2.491 0 4.349-1.028 6.091-2.533l.318 2.137h3.163v-.001zm-56.693 3.346c-1.147 1.679-3.005 3.008-5.037 3.757l-1.989-2.345c1.547-.794 2.872-2.075 3.489-3.269.532-1.063.753-2.43.753-5.701V92.891h4.284v22.173c0 4.375-.348 6.144-1.5 7.867z"/></svg>

After

Width:  |  Height:  |  Size: 2.8 KiB

BIN
resources/mona_lisa.gif Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.4 MiB

View file

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 128 128"><path fill="#FFD845" d="M49.33 62h29.159C86.606 62 93 55.132 93 46.981V19.183c0-7.912-6.632-13.856-14.555-15.176-5.014-.835-10.195-1.215-15.187-1.191-4.99.023-9.612.448-13.805 1.191C37.098 6.188 35 10.758 35 19.183V30h29v4H23.776c-8.484 0-15.914 5.108-18.237 14.811-2.681 11.12-2.8 17.919 0 29.53C7.614 86.983 12.569 93 21.054 93H31V79.952C31 70.315 39.428 62 49.33 62zm-1.838-39.11c-3.026 0-5.478-2.479-5.478-5.545 0-3.079 2.451-5.581 5.478-5.581 3.015 0 5.479 2.502 5.479 5.581-.001 3.066-2.465 5.545-5.479 5.545zm74.789 25.921C120.183 40.363 116.178 34 107.682 34H97v12.981C97 57.031 88.206 65 78.489 65H49.33C41.342 65 35 72.326 35 80.326v27.8c0 7.91 6.745 12.564 14.462 14.834 9.242 2.717 17.994 3.208 29.051 0C85.862 120.831 93 116.549 93 108.126V97H64v-4h43.682c8.484 0 11.647-5.776 14.599-14.66 3.047-9.145 2.916-17.799 0-29.529zm-41.955 55.606c3.027 0 5.479 2.479 5.479 5.547 0 3.076-2.451 5.579-5.479 5.579-3.015 0-5.478-2.502-5.478-5.579 0-3.068 2.463-5.547 5.478-5.547z"/></svg>

After

Width:  |  Height:  |  Size: 1 KiB

BIN
resources/raylib-logo.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.4 KiB

1
resources/rust-icon.svg Normal file

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 5.9 KiB

BIN
resources/starry_night.gif Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 7 MiB

BIN
resources/wanderer.gif Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.6 MiB

View file

@ -0,0 +1,5 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 107.62 107.62"><defs><style>
.cls-1 {
fill: #654ff0;
}
</style></defs><title>web-assembly-icon</title><g id="Layer_2" data-name="Layer 2"><g id="Notch_-_Purple" data-name="Notch - Purple"><g id="icon"><path class="cls-1" d="M66.12,0c0,.19,0,.38,0,.58a12.34,12.34,0,1,1-24.68,0c0-.2,0-.39,0-.58H0V107.62H107.62V0ZM51.38,96.1,46.14,70.17H46L40.39,96.1H33.18L25,58h7.13L37,83.93h.09L42.94,58h6.67L54.9,84.25H55L60.55,58h7L58.46,96.1Zm39.26,0-2.43-8.48H75.4L73.53,96.1H66.36L75.59,58H86.83L98,96.1Z"/><polygon class="cls-1" points="79.87 67.39 76.76 81.37 86.44 81.37 82.87 67.39 79.87 67.39"/></g></g></g></svg>

After

Width:  |  Height:  |  Size: 685 B

1
resources/wgpu-logo.svg Normal file
View file

@ -0,0 +1 @@
<svg height="298.90323" viewBox="0 0 61.657897 79.084803" width="233.03775" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink"><linearGradient id="a" gradientUnits="userSpaceOnUse" x1="67.733323" x2="67.733323" y1="31.883125" y2="50.933122"><stop offset="0" stop-color="#91e12a"/><stop offset="1" stop-color="#7b8a49"/></linearGradient><linearGradient id="b" gradientUnits="userSpaceOnUse" x1="84.666658" x2="84.666658" y1="23.416458" y2="65.749786"><stop offset="0" stop-color="#2263ea"/><stop offset="1" stop-color="#33c0f4"/></linearGradient><linearGradient id="c" gradientUnits="userSpaceOnUse" x1="50.799992" x2="50.799992" y1="23.416458" y2="65.749786"><stop offset="0" stop-color="#e12400"/><stop offset="1" stop-color="#febe00"/></linearGradient><linearGradient id="d" gradientUnits="userSpaceOnUse" x1="52.91666" x2="52.91666" y1="40.349789" y2="65.749786"><stop offset="0" stop-color="#f6d339"/><stop offset="1" stop-color="#ffd55f"/></linearGradient><linearGradient id="e" gradientUnits="userSpaceOnUse" x1="82.549988" x2="82.549988" y1="40.349789" y2="65.749786"><stop offset="0" stop-color="#99def2"/><stop offset="1" stop-color="#84d2f8"/></linearGradient><g transform="translate(-36.904387 -20.770627)"><text font-family="Aria" font-size="33.8667" letter-spacing="-1.03188" stroke-width=".264583" x="38.161156" y="90.082375"><tspan font-family="Arial" font-weight="bold" letter-spacing="-.978958" stroke-width=".264583" x="38.161156" y="90.082375">gpu</tspan></text><path d="m61.383325 40.34979 6.35-8.466666 6.34999 8.466666-6.34999 10.583331z" fill="url(#a)"/><path d="m50.799994 23.41646-8.466667 42.333327 19.049997-25.399997z" fill="url(#c)"/><path d="m93.133323 65.749787-19.049999-25.399997 10.583329-16.93333z" fill="url(#b)"/><path d="m42.333327 65.749787 19.049997-25.399997 6.35 10.583331-8.466666 14.816666z" fill="url(#d)"/><path d="m67.733324 50.933121 6.349998-10.583331 19.049999 25.399997h-16.933333z" fill="url(#e)"/></g></svg>

After

Width:  |  Height:  |  Size: 1.9 KiB

BIN
resume.pdf Normal file

Binary file not shown.

121
src/forms.rs Normal file
View file

@ -0,0 +1,121 @@
use std::collections::HashMap;
type Parse<'a, T> = Option<(T, &'a [u8])>;
fn parse_byte(i: &[u8], byte: u8) -> Parse<u8> {
let first = i.first()?;
if first == &byte {
Some((byte, &i[1..]))
} else {
None
}
}
fn parse_escaped_codepoint(i: &[u8]) -> Parse<u8> {
let (_, i) = parse_byte(i, b'%')?;
let slice = i.get(..2)?;
let i = i.get(2..)?;
let num = u8::from_str_radix(std::str::from_utf8(slice).ok()?, 16).ok()?;
Some((num, i))
}
pub fn url_unescape(mut i: &[u8]) -> Option<Vec<u8>> {
let mut bytes = Vec::with_capacity(i.len());
while let Some(b) = i.first() {
if b == &b'%' {
let (escaped, new_i) = parse_escaped_codepoint(i)?;
bytes.push(escaped);
i = new_i;
} else {
bytes.push(*b);
i = &i[1..];
}
}
Some(bytes)
}
/// Parses form bodies with enctype=x-www-form-urlencoded
/// (default)
pub fn parse_urlencoded(i: &[u8]) -> Option<HashMap<String, Vec<u8>>> {
let mut output = HashMap::new();
let args = i.split(|b| b == &b'&');
for arg in args {
let unescaped = url_unescape(arg)?;
let (key, value) = unescaped.split_once(|b| b == &b'=')?;
output.insert(String::from_utf8_lossy(key).to_string(), value.to_vec());
}
Some(output)
}
pub fn parse_form(request: &mut tiny_http::Request) -> Option<HashMap<String, Vec<u8>>> {
let content_type = request.headers().iter().find_map(|header| {
if header.field.as_str().to_ascii_lowercase() == "content-type" {
Some(header.value.clone())
} else {
None
}
})?;
if let Some(s) = content_type
.as_str()
.strip_prefix("multipart/form-data; boundary=")
{
let mut boundary = vec![b'-', b'-'];
boundary.append(&mut s.as_bytes().to_vec());
return parse_multipart_form(request, boundary);
} else if content_type.as_str() == "application/x-www-form-urlencoded" {
let mut bytes = vec![];
request.as_reader().read_to_end(&mut bytes).ok()?;
return parse_urlencoded(&bytes);
} else {
log::warn!("Unrecognized form type: {}", content_type.as_str());
None
}
}
fn position_of(slice: &[u8], pattern: &[u8]) -> Option<usize> {
for index in 0..slice.len() {
if slice[..index].ends_with(pattern) {
return Some(index - pattern.len());
}
}
None
}
/// Parses forms with enctype=multipart/form-data
pub fn parse_multipart_form(
request: &mut tiny_http::Request,
boundary: Vec<u8>,
) -> Option<HashMap<String, Vec<u8>>> {
let mut bytes = vec![];
request.as_reader().read_to_end(&mut bytes).unwrap();
// Strip out cruft to make it easy to parse
let mut bytes = bytes
.strip_prefix(b"--")?
.strip_suffix(b"--\r\n")?
.strip_suffix(boundary.as_slice())?;
let mut map = HashMap::new();
while !bytes.is_empty() {
bytes = &bytes[boundary.len()..];
// Find next boundary, or end of body
let end = position_of(bytes, &boundary).unwrap_or(bytes.len());
let slice = &bytes[..end].strip_suffix(b"\r\n")?;
bytes = &bytes[end..];
// Find end of headers
let end = position_of(slice, b"\r\n\r\n").unwrap_or(0);
let headers = &slice[..end];
let string = String::from_utf8_lossy(headers);
let name = string
.lines()
.find_map(|line| line.strip_prefix("Content-Disposition: "))?
.split("; ")
.find_map(|arg| arg.strip_prefix("name="))?
.trim();
let name = name.strip_prefix('"').unwrap_or(name);
let name = name.strip_suffix('"').unwrap_or(name);
let body = &slice[end + 4..];
map.insert(name.into(), body.to_vec());
}
Some(map)
}

106
src/get.rs Normal file
View file

@ -0,0 +1,106 @@
use std::{
collections::HashMap,
path::{Component, Path, PathBuf},
};
use tiny_http::Header;
use tokio::sync::RwLock;
use crate::util::*;
/// Makes the uri less spooky - removes leading /, rejects
/// ../ and ./
pub fn rewrite_url(url: &str) -> String {
let (url, query) = url.split_once('?').unwrap_or((url, ""));
let target_path = Path::new(url);
let mut corrected_path = PathBuf::new();
for c in target_path.components() {
match c {
Component::Normal(_) | Component::RootDir => {
corrected_path.push(c);
},
Component::ParentDir => {
corrected_path.pop();
},
_ => {},
}
}
corrected_path.to_string_lossy().to_string()
}
pub struct Router {
/// Reinterpret A as B server-side
route_table: RwLock<HashMap<String, String>>,
/// Send a 301 redirect from A to B
routing_table: RwLock<HashMap<String, String>>,
}
impl Router {
const NOT_FOUND: &'static [u8] = include_bytes!("../hyper-build/404.html");
pub fn new() -> Self {
Self {
route_table: RwLock::new(Default::default()),
routing_table: RwLock::new(Default::default()),
}
}
fn redirect(to: &str) -> Response {
Response::from_data(&[])
.with_status_code(301)
.with_header(header("Location", to))
}
fn not_found() -> Response {
Response::from_data(Self::NOT_FOUND).with_status_code(404)
}
pub async fn construct_response(&self, request_url: &str) -> Response {
let sanitized_url = rewrite_url(request_url);
if sanitized_url != request_url {
return Self::redirect(&sanitized_url);
}
let routed_url = self.perform_routing(&sanitized_url).await;
if let Some(response) = self.perform_redirecting(&routed_url).await {
return response;
}
let mime_type = mime_from_file_ext(&routed_url);
match self.get_resource(&routed_url).await {
Some(bytes) => Response::from_data(bytes)
.with_header(header("Content-Type", &mime_type)),
None => Self::not_found(),
}
}
pub async fn create_route(&self, from: &str, to: &str) {
let from = rewrite_url(from);
let to = rewrite_url(to);
let mut w = self.route_table.write().await;
w.insert(from.into(), to.into());
}
async fn perform_routing(&self, uri: &str) -> String {
let r = self.route_table.read().await;
r.get(uri).cloned().unwrap_or(uri.to_string())
}
pub async fn create_redirect(&self, from: &str, to: &str) {
let from = rewrite_url(from);
let to = rewrite_url(to);
let mut w = self.routing_table.write().await;
w.insert(from.into(), to.into());
}
async fn perform_redirecting(&self, uri: &str) -> Option<Response> {
let r = self.routing_table.read().await;
r.get(uri).map(|url| Self::redirect(url))
}
pub async fn get_resource(&self, uri: &str) -> Option<Vec<u8>> {
let uri = uri.trim_start_matches('/');
tokio::fs::read(&uri).await.ok()
}
}

46
src/logging.rs Normal file
View file

@ -0,0 +1,46 @@
pub fn init() -> log4rs::Handle {
use log4rs::append::console::ConsoleAppender;
use log4rs::append::rolling_file::{
policy::compound::*, RollingFileAppender,
};
use log4rs::config::{Appender, Config, Logger, Root};
use log4rs::encode::pattern::PatternEncoder;
let stdout = ConsoleAppender::builder()
.encoder(Box::new(PatternEncoder::new("{d(%H:%M)} {l} - {m}{n}")))
.build();
let connections = RollingFileAppender::builder()
.encoder(Box::new(PatternEncoder::new(
"{d(%Y-%m-%d %H:%M:%S)} {l} - {m}{n}",
)))
.build(
"logs/connections.log",
Box::new(CompoundPolicy::new(
Box::new(trigger::size::SizeTrigger::new(1_000_000)),
Box::new(
roll::fixed_window::FixedWindowRoller::builder()
.base(1)
.build("logs/connections-{}.log", 5)
.unwrap(),
),
)),
)
.unwrap();
let config = Config::builder()
.appender(Appender::builder().build("stdout", Box::new(stdout)))
.appender(Appender::builder().build("connections", Box::new(connections)))
.logger(
Logger::builder()
.appender("connections")
.build("server", log::LevelFilter::Info),
)
.build(
Root::builder()
.appender("stdout")
.build(log::LevelFilter::Info),
)
.unwrap();
log4rs::init_config(config).unwrap()
}

130
src/main.rs Normal file
View file

@ -0,0 +1,130 @@
#![feature(slice_split_once)]
mod forms;
mod get;
mod logging;
mod post;
mod util;
use std::{io::Write, sync::Arc};
use get::Router;
use post::PostHandler;
use tiny_http::{Request, Server};
// Exit process if its already running
fn graceful_cleanup() {
const PID_PATH: &'static str = "./temp/pid";
match {
let s = std::fs::read_to_string(PID_PATH).unwrap_or("".to_string());
s.parse::<u32>().ok()
} {
Some(last_pid) => {
std::process::Command::new("pkill")
.arg(format!("{}", last_pid))
.output()
.unwrap();
},
None => {
log::warn!("Did not find previous PID");
},
};
let pid = std::process::id();
let mut file = std::fs::File::create(PID_PATH).unwrap();
file.write(format!("{}", pid).as_bytes()).unwrap();
}
#[tokio::main]
async fn main() {
// graceful_cleanup();
let _h = logging::init();
// let ssl = tiny_http::SslConfig {
// certificate:
// include_bytes!("../lgatlin.dev.cert").to_vec(),
// private_key:
// include_bytes!("../../lgatlin.dev.priv").to_vec(), };
// let server = Server::https("0.0.0.0:443", ssl).unwrap();
let server = Server::http("0.0.0.0:8080").unwrap();
let router = Arc::new(Router::new());
let post = Arc::new(PostHandler);
router.create_redirect("/", "/home").await;
router
.create_redirect("/favicon.ico", "/resources/favicon.svg")
.await;
router.create_route("/home", "/hyper-build/home.html").await;
router
.create_route("/projects", "/hyper-build/projects.html")
.await;
router
.create_route("/about", "hyper-build/about.html")
.await;
router
.create_route(
"/projects/http-server",
"hyper-build/projects/http-server.html",
)
.await;
router
.create_route(
"/projects/html-templating",
"hyper-build/projects/html-templating.html",
)
.await;
router
.create_route("/projects/forte", "hyper-build/projects/forte.html")
.await;
router
.create_route("/projects/fishbowl", "hyper-build/projects/fishbowl.html")
.await;
router
.create_route(
"/projects/math-interpreter",
"hyper-build/projects/math-interpreter.html",
)
.await;
router
.create_route("/projects/nd-range", "hyper-build/projects/nd-range.html")
.await;
router
.create_route(
"/projects/fractal-explorer",
"hyper-build/projects/fractal-explorer.html",
)
.await;
router
.create_route("/projects/pokedex", "hyper-build/projects/pokedex.html")
.await;
router
.create_route(
"/projects/stock-trading",
"hyper-build/projects/stocktrading.html",
)
.await;
for request in server.incoming_requests() {
log::info!("{:?} AT {:?}", request.method(), request.url(),);
let router = router.clone();
let post = post.clone();
tokio::spawn(async move {
handle_connection(request, router, post).await;
});
}
}
async fn handle_connection(
mut request: Request,
route: Arc<Router>,
post: Arc<PostHandler>,
) {
use tiny_http::*;
let response = match request.method() {
Method::Get | Method::Head => route.construct_response(request.url()).await,
Method::Post => match post.handle_post(&mut request).await {
Some(response) => response,
None => return,
},
_ => Response::from_string("Error occurred").with_status_code(405),
};
let _ = request.respond(response);
}

27
src/post.rs Normal file
View file

@ -0,0 +1,27 @@
use crate::util::*;
pub struct PostHandler;
impl PostHandler {
async fn fishbowl_endpoint(&self, file: &[u8]) -> Vec<u8> {
fishbowl::make_gif(&file, fishbowl::make_quickdraw().await.unwrap())
.await
.unwrap()
}
pub async fn handle_post(&self, request: &mut tiny_http::Request) -> Option<Response> {
let form = crate::forms::parse_form(request)?;
match request.url() {
"/projects/fishbowl" => {
let image = form.get("image")?;
let bytes = self.fishbowl_endpoint(&image).await;
return Some(Response::from_data(bytes).with_header(header("Content-Type", "image/gif")));
}
_ => {
log::warn!("Unhandled POST at {}", request.url());
return None;
}
}
}
}

22
src/util.rs Normal file
View file

@ -0,0 +1,22 @@
use std::path::Path;
use tiny_http::Header;
pub type Response = tiny_http::Response<std::io::Cursor<Vec<u8>>>;
pub fn mime_from_file_ext(url: &str) -> &'static str {
let as_path = Path::new(url);
match as_path.extension().unwrap_or_default().as_encoded_bytes() {
b"css" => "text/css",
b"gif" => "image/gif",
b"html" | b"htm" => "text/html",
b"js" | b"mjs" => "text/javascript",
b"svg" => "image/svg+xml",
b"woff2" => "font/woff2",
b"wasm" => "application/wasm",
_ => "application/octet-stream",
}
}
pub fn header(key: &str, value: &str) -> Header {
Header::from_bytes(key.as_bytes(), value.as_bytes()).unwrap()
}

5
start.sh Normal file
View file

@ -0,0 +1,5 @@
#!/bin/bash
cargo build --release
watchexec -e html,css,js -w hyper-src -w templates -w styles ./html &
./target/release/server

309
styles/extend.css Normal file
View file

@ -0,0 +1,309 @@
@font-face {
font-family: Cascadia;
src: url("/resources/CascadiaMonoPL.woff2");
}
body {
color: #444;
font-size: 1.2em;
}
.link {
text-decoration: none;
color: #1e90ff;
}
.link :visited {
color: #1e90ff;
}
table {
display: block;
margin-top: 1em;
margin-bottom: 1em;
margin-left: auto;
margin-right: auto;
width: 60%;
text-align: left;
border-collapse: collapse;
}
th {
padding: 0.25em 0.25em 0.25em 0.25em;
border: 2px solid;
border-collapse: collapse;
}
td {
padding: 0.25em 0.25em 0.25em 0.25em;
border: 2px solid;
border-collapse: collapse;
}
/* https://css-loaders.com/spinner/ */
/* HTML: <div class="loader"></div> */
.loading {
width: 512px;
padding: 8px;
aspect-ratio: 1;
border-radius: 50%;
background: #25b09b;
--_m:
conic-gradient(#0000 10%,#000),
linear-gradient(#000 0 0) content-box;
-webkit-mask: var(--_m);
mask: var(--_m);
-webkit-mask-composite: source-out;
mask-composite: subtract;
animation: l3 1s infinite linear;
}
@keyframes l3 {to{transform: rotate(1turn)}}
.centered {
display: block;
margin-top: 1em;
margin-bottom: 1em;
margin-left: auto;
margin-right: auto;
width: 50%;
}
.pure-img-responsive { max-width: 100%; height: auto;
}
.cascadia {
font-family: Cascadia;
}
.home-menu {
padding: 0.5em;
text-align: center;
box-shadow: 0 1px 1px rgba(0,0,0, 0.10);
}
.home-menu {
background: #333;
}
.home-menu a {
color: #ccc;
}
.home-menu li a:hover,
.home-menu li a:focus {
animation: menu-hover-anim 0.25s ease-out;
animation-fill-mode: forwards;
background: none;
border: none;
}
.link {
color: #00f;
text-decoration: none;
}
.video {
text-align: center;
}
.video iframe {
width: 32em;
height: 18em;
frameborder: "0";
}
/*
Add transition to containers so they can push in and out.
*/
#layout,
#menu,
.menu-link {
-webkit-transition: all 0.2s ease-out;
-moz-transition: all 0.2s ease-out;
-ms-transition: all 0.2s ease-out;
-o-transition: all 0.2s ease-out;
transition: all 0.2s ease-out;
}
/*
This is the parent `<div>` that contains the menu and the content area.
*/
#layout {
position: relative;
left: 0;
padding-left: 0;
}
#layout.active #menu {
left: 150px;
width: 150px;
}
#layout.active .menu-link {
left: 150px;
}
/*
The content `<div>` is where all your content goes.
*/
.content {
margin: 0 auto;
padding: 0 2em;
max-width: 800px;
margin-bottom: 50px;
line-height: 1.6em;
text-align: justify;
}
.header {
margin: 0;
color: #444;
text-align: center;
padding: 2.5em 4em 0;
border-bottom: 1px solid #eee;
}
.header h1 {
margin: 0.2em 0;
font-size: 2.5em;
font-weight: bold;
}
.header h2 {
font-weight: 300;
color: #888;
padding: 0;
margin-top: 0;
}
.distinct {
background-color: #444;
color: #fff;
padding: 0.5em 0.5em 0.5em 0.5em;
}
@keyframes card-hover-anim {
to {background-color: #ccc;}
}
@keyframes card-text-hover-anim {
to {color: #222;}
}
.spaced {
padding: 0.5em 0.5em 0.5em 0.5em;
margin: 1em 0em 1em 0em;
}
h1 a svg {
vertical-align: middle;
}
.card {
background-color: #eee;
text-decoration: none;
text-align: left;
}
.card h1 {
color: #444;
}
.card h2 {
color: #777;
}
.card h3 {
color: #999;
}
.card a {
text-decoration: none;
display: block;
}
.card:hover {
animation: card-hover-anim 0.25s ease-out;
animation-fill-mode: forwards;
}
/* .card:hover h3 { */
/* animation: card-text-hover-anim 0.25s ease-out; */
/* animation-fill-mode: forwards; */
/* } */
.card img {
vertical-align: bottom;
float: right;
padding: 0em 0.1em 0em 0.1em;
width: 60px;
}
.card svg {
vertical-align: bottom;
float: right;
padding: 0em 0.1em 0em 0.1em;
width: 3em;
height: auto;
}
.content-subhead {
margin: 50px 0 20px 0;
font-weight: 300;
color: #444;
}
code {
font-family: Cascadia;
}
.code-block {
/* background-color: #fdf6e3; */
background-color: #eee;
color: #002b36;
font-family: Cascadia;
font-size: 0.85em;
padding: 0em 0.5em 0em 0.5em;
margin: 1em 0em 1em 0em;
page-break-inside: avoid;
display: block;
overflow: auto;
word-wrap: break-word;
max-width: 100%;
}
.code-block pre {
display: block;
margin: 0 0 0 0;
padding: 0 0 0 0;
}
.code-block pre code {
font-family: Cascadia;
display: block;
white-space: pre-wrap;
}
.code-block span {
/* background-color: #fdf6e3; */
background-color: #eee;
}
.keyword {
color: #6c71c4;
}
.constant {
color: #cb4b16;
}
.function {
color: #268bd2;
}
.operator {
color: #6c71c4;
}
.punctuation {
color: #586e75;
}
.string {
color: #859900;
}
.type {
color: #2aa198;
}
.property {
color: #586e75;
}

11
styles/main.css Normal file

File diff suppressed because one or more lines are too long

1
temp/pid Normal file
View file

@ -0,0 +1 @@
7429

249
templates/blueprints.html Normal file

File diff suppressed because one or more lines are too long