commit d052cb8152e1adec84e385da03caa4c389875eb2
Author: kp <kp@sys@patchii.net>
Date:   Wed Feb 19 14:28:12 2025 -0600

    Initial commit

diff --git a/.editorconfig b/.editorconfig
new file mode 100644
index 0000000..7dacfe3
--- /dev/null
+++ b/.editorconfig
@@ -0,0 +1,6 @@
+[*]
+insert_final_newline = true
+
+[*.{rs}]
+indent_style = tab
+indent_size = 4
diff --git a/.gitignore b/.gitignore
new file mode 100644
index 0000000..ea8c4bf
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1 @@
+/target
diff --git a/Cargo.lock b/Cargo.lock
new file mode 100644
index 0000000..c2a1100
--- /dev/null
+++ b/Cargo.lock
@@ -0,0 +1,1039 @@
+# This file is automatically @generated by Cargo.
+# It is not intended for manual editing.
+version = 4
+
+[[package]]
+name = "adler2"
+version = "2.0.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "512761e0bb2578dd7380c6baaa0f4ce03e84f95e960231d1dec8bf4d7d6e2627"
+
+[[package]]
+name = "aligned-vec"
+version = "0.5.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "4aa90d7ce82d4be67b64039a3d588d38dbcc6736577de4a847025ce5b0c468d1"
+
+[[package]]
+name = "anyhow"
+version = "1.0.95"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "34ac096ce696dc2fcabef30516bb13c0a68a11d30131d3df6f04711467681b04"
+
+[[package]]
+name = "arbitrary"
+version = "1.4.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "dde20b3d026af13f561bdd0f15edf01fc734f0dafcedbaf42bba506a9517f223"
+
+[[package]]
+name = "arg_enum_proc_macro"
+version = "0.3.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0ae92a5119aa49cdbcf6b9f893fe4e1d98b04ccbf82ee0584ad948a44a734dea"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn",
+]
+
+[[package]]
+name = "arrayvec"
+version = "0.7.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7c02d123df017efcdfbd739ef81735b36c5ba83ec3c59c80a9d7ecc718f92e50"
+
+[[package]]
+name = "autocfg"
+version = "1.4.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ace50bade8e6234aa140d9a2f552bbee1db4d353f69b8217bc503490fc1a9f26"
+
+[[package]]
+name = "av1-grain"
+version = "0.2.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "6678909d8c5d46a42abcf571271e15fdbc0a225e3646cf23762cd415046c78bf"
+dependencies = [
+ "anyhow",
+ "arrayvec",
+ "log",
+ "nom",
+ "num-rational",
+ "v_frame",
+]
+
+[[package]]
+name = "avif-serialize"
+version = "0.8.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e335041290c43101ca215eed6f43ec437eb5a42125573f600fc3fa42b9bddd62"
+dependencies = [
+ "arrayvec",
+]
+
+[[package]]
+name = "bit_field"
+version = "0.10.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "dc827186963e592360843fb5ba4b973e145841266c1357f7180c43526f2e5b61"
+
+[[package]]
+name = "bitflags"
+version = "1.3.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a"
+
+[[package]]
+name = "bitstream-io"
+version = "2.6.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "6099cdc01846bc367c4e7dd630dc5966dccf36b652fae7a74e17b640411a91b2"
+
+[[package]]
+name = "built"
+version = "0.7.7"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "56ed6191a7e78c36abdb16ab65341eefd73d64d303fffccdbb00d51e4205967b"
+
+[[package]]
+name = "bumpalo"
+version = "3.17.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1628fb46dfa0b37568d12e5edd512553eccf6a22a78e8bde00bb4aed84d5bdbf"
+
+[[package]]
+name = "bytemuck"
+version = "1.21.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ef657dfab802224e671f5818e9a4935f9b1957ed18e58292690cc39e7a4092a3"
+
+[[package]]
+name = "byteorder"
+version = "1.5.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b"
+
+[[package]]
+name = "byteorder-lite"
+version = "0.1.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8f1fe948ff07f4bd06c30984e69f5b4899c516a3ef74f34df92a2df2ab535495"
+
+[[package]]
+name = "bzip2"
+version = "0.5.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "75b89e7c29231c673a61a46e722602bcd138298f6b9e81e71119693534585f5c"
+dependencies = [
+ "bzip2-sys",
+]
+
+[[package]]
+name = "bzip2-sys"
+version = "0.1.12+1.0.8"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "72ebc2f1a417f01e1da30ef264ee86ae31d2dcd2d603ea283d3c244a883ca2a9"
+dependencies = [
+ "cc",
+ "libc",
+ "pkg-config",
+]
+
+[[package]]
+name = "cc"
+version = "1.2.14"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0c3d1b2e905a3a7b00a6141adb0e4c0bb941d11caf55349d863942a1cc44e3c9"
+dependencies = [
+ "jobserver",
+ "libc",
+ "shlex",
+]
+
+[[package]]
+name = "cfg-expr"
+version = "0.15.8"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d067ad48b8650848b989a59a86c6c36a995d02d2bf778d45c3c5d57bc2718f02"
+dependencies = [
+ "smallvec",
+ "target-lexicon",
+]
+
+[[package]]
+name = "cfg-if"
+version = "1.0.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd"
+
+[[package]]
+name = "color_quant"
+version = "1.1.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "3d7b894f5411737b7867f4827955924d7c254fc9f4d91a6aad6b097804b1018b"
+
+[[package]]
+name = "crc32fast"
+version = "1.4.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a97769d94ddab943e4510d138150169a2758b5ef3eb191a9ee688de3e23ef7b3"
+dependencies = [
+ "cfg-if",
+]
+
+[[package]]
+name = "crossbeam-deque"
+version = "0.8.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9dd111b7b7f7d55b72c0a6ae361660ee5853c9af73f70c3c2ef6858b950e2e51"
+dependencies = [
+ "crossbeam-epoch",
+ "crossbeam-utils",
+]
+
+[[package]]
+name = "crossbeam-epoch"
+version = "0.9.18"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5b82ac4a3c2ca9c3460964f020e1402edd5753411d7737aa39c3714ad1b5420e"
+dependencies = [
+ "crossbeam-utils",
+]
+
+[[package]]
+name = "crossbeam-utils"
+version = "0.8.21"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d0a5c400df2834b80a4c3327b3aad3a4c4cd4de0629063962b03235697506a28"
+
+[[package]]
+name = "crunchy"
+version = "0.2.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "43da5946c66ffcc7745f48db692ffbb10a83bfe0afd96235c5c2a4fb23994929"
+
+[[package]]
+name = "either"
+version = "1.13.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "60b1af1c220855b6ceac025d3f6ecdd2b7c4894bfe9cd9bda4fbb4bc7c0d4cf0"
+
+[[package]]
+name = "equivalent"
+version = "1.0.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f"
+
+[[package]]
+name = "exr"
+version = "1.73.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f83197f59927b46c04a183a619b7c29df34e63e63c7869320862268c0ef687e0"
+dependencies = [
+ "bit_field",
+ "half",
+ "lebe",
+ "miniz_oxide",
+ "rayon-core",
+ "smallvec",
+ "zune-inflate",
+]
+
+[[package]]
+name = "fdeflate"
+version = "0.3.7"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1e6853b52649d4ac5c0bd02320cddc5ba956bdb407c4b75a2c6b75bf51500f8c"
+dependencies = [
+ "simd-adler32",
+]
+
+[[package]]
+name = "flate2"
+version = "1.0.35"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c936bfdafb507ebbf50b8074c54fa31c5be9a1e7e5f467dd659697041407d07c"
+dependencies = [
+ "crc32fast",
+ "miniz_oxide",
+]
+
+[[package]]
+name = "getrandom"
+version = "0.2.15"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c4567c8db10ae91089c99af84c68c38da3ec2f087c3f82960bcdbf3656b6f4d7"
+dependencies = [
+ "cfg-if",
+ "libc",
+ "wasi",
+]
+
+[[package]]
+name = "gif"
+version = "0.13.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "3fb2d69b19215e18bb912fa30f7ce15846e301408695e44e0ef719f1da9e19f2"
+dependencies = [
+ "color_quant",
+ "weezl",
+]
+
+[[package]]
+name = "half"
+version = "2.4.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "6dd08c532ae367adf81c312a4580bc67f1d0fe8bc9c460520283f4c0ff277888"
+dependencies = [
+ "cfg-if",
+ "crunchy",
+]
+
+[[package]]
+name = "hashbrown"
+version = "0.15.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "bf151400ff0baff5465007dd2f3e717f3fe502074ca563069ce3a6629d07b289"
+
+[[package]]
+name = "heck"
+version = "0.5.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea"
+
+[[package]]
+name = "image"
+version = "0.25.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "cd6f44aed642f18953a158afeb30206f4d50da59fbc66ecb53c66488de73563b"
+dependencies = [
+ "bytemuck",
+ "byteorder-lite",
+ "color_quant",
+ "exr",
+ "gif",
+ "image-webp",
+ "num-traits",
+ "png",
+ "qoi",
+ "ravif",
+ "rayon",
+ "rgb",
+ "tiff",
+ "zune-core",
+ "zune-jpeg",
+]
+
+[[package]]
+name = "image-webp"
+version = "0.2.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b77d01e822461baa8409e156015a1d91735549f0f2c17691bd2d996bef238f7f"
+dependencies = [
+ "byteorder-lite",
+ "quick-error",
+]
+
+[[package]]
+name = "imgref"
+version = "1.11.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d0263a3d970d5c054ed9312c0057b4f3bde9c0b33836d3637361d4a9e6e7a408"
+
+[[package]]
+name = "indexmap"
+version = "2.7.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8c9c992b02b5b4c94ea26e32fe5bccb7aa7d9f390ab5c1221ff895bc7ea8b652"
+dependencies = [
+ "equivalent",
+ "hashbrown",
+]
+
+[[package]]
+name = "interpolate_name"
+version = "0.2.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c34819042dc3d3971c46c2190835914dfbe0c3c13f61449b2997f4e9722dfa60"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn",
+]
+
+[[package]]
+name = "itertools"
+version = "0.12.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ba291022dbbd398a455acf126c1e341954079855bc60dfdda641363bd6922569"
+dependencies = [
+ "either",
+]
+
+[[package]]
+name = "jobserver"
+version = "0.1.32"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "48d1dbcbbeb6a7fec7e059840aa538bd62aaccf972c7346c4d9d2059312853d0"
+dependencies = [
+ "libc",
+]
+
+[[package]]
+name = "jpeg-decoder"
+version = "0.3.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f5d4a7da358eff58addd2877a45865158f0d78c911d43a5784ceb7bbf52833b0"
+
+[[package]]
+name = "l2decodeV5"
+version = "0.1.0"
+dependencies = [
+ "byteorder",
+ "bzip2",
+ "image",
+]
+
+[[package]]
+name = "lebe"
+version = "0.5.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "03087c2bad5e1034e8cace5926dec053fb3790248370865f5117a7d0213354c8"
+
+[[package]]
+name = "libc"
+version = "0.2.169"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b5aba8db14291edd000dfcc4d620c7ebfb122c613afb886ca8803fa4e128a20a"
+
+[[package]]
+name = "libfuzzer-sys"
+version = "0.4.9"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "cf78f52d400cf2d84a3a973a78a592b4adc535739e0a5597a0da6f0c357adc75"
+dependencies = [
+ "arbitrary",
+ "cc",
+]
+
+[[package]]
+name = "log"
+version = "0.4.25"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "04cbf5b083de1c7e0222a7a51dbfdba1cbe1c6ab0b15e29fff3f6c077fd9cd9f"
+
+[[package]]
+name = "loop9"
+version = "0.1.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0fae87c125b03c1d2c0150c90365d7d6bcc53fb73a9acaef207d2d065860f062"
+dependencies = [
+ "imgref",
+]
+
+[[package]]
+name = "maybe-rayon"
+version = "0.1.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8ea1f30cedd69f0a2954655f7188c6a834246d2bcf1e315e2ac40c4b24dc9519"
+dependencies = [
+ "cfg-if",
+ "rayon",
+]
+
+[[package]]
+name = "memchr"
+version = "2.7.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "78ca9ab1a0babb1e7d5695e3530886289c18cf2f87ec19a575a0abdce112e3a3"
+
+[[package]]
+name = "minimal-lexical"
+version = "0.2.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "68354c5c6bd36d73ff3feceb05efa59b6acb7626617f4962be322a825e61f79a"
+
+[[package]]
+name = "miniz_oxide"
+version = "0.8.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b3b1c9bd4fe1f0f8b387f6eb9eb3b4a1aa26185e5750efb9140301703f62cd1b"
+dependencies = [
+ "adler2",
+ "simd-adler32",
+]
+
+[[package]]
+name = "new_debug_unreachable"
+version = "1.0.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "650eef8c711430f1a879fdd01d4745a7deea475becfb90269c06775983bbf086"
+
+[[package]]
+name = "nom"
+version = "7.1.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d273983c5a657a70a3e8f2a01329822f3b8c8172b73826411a55751e404a0a4a"
+dependencies = [
+ "memchr",
+ "minimal-lexical",
+]
+
+[[package]]
+name = "noop_proc_macro"
+version = "0.3.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0676bb32a98c1a483ce53e500a81ad9c3d5b3f7c920c28c24e9cb0980d0b5bc8"
+
+[[package]]
+name = "num-bigint"
+version = "0.4.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a5e44f723f1133c9deac646763579fdb3ac745e418f2a7af9cd0c431da1f20b9"
+dependencies = [
+ "num-integer",
+ "num-traits",
+]
+
+[[package]]
+name = "num-derive"
+version = "0.4.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ed3955f1a9c7c0c15e092f9c887db08b1fc683305fdf6eb6684f22555355e202"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn",
+]
+
+[[package]]
+name = "num-integer"
+version = "0.1.46"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7969661fd2958a5cb096e56c8e1ad0444ac2bbcd0061bd28660485a44879858f"
+dependencies = [
+ "num-traits",
+]
+
+[[package]]
+name = "num-rational"
+version = "0.4.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f83d14da390562dca69fc84082e73e548e1ad308d24accdedd2720017cb37824"
+dependencies = [
+ "num-bigint",
+ "num-integer",
+ "num-traits",
+]
+
+[[package]]
+name = "num-traits"
+version = "0.2.19"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841"
+dependencies = [
+ "autocfg",
+]
+
+[[package]]
+name = "once_cell"
+version = "1.20.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "945462a4b81e43c4e3ba96bd7b49d834c6f61198356aa858733bc4acf3cbe62e"
+
+[[package]]
+name = "paste"
+version = "1.0.15"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "57c0d7b74b563b49d38dae00a0c37d4d6de9b432382b2892f0574ddcae73fd0a"
+
+[[package]]
+name = "pkg-config"
+version = "0.3.31"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "953ec861398dccce10c670dfeaf3ec4911ca479e9c02154b3a215178c5f566f2"
+
+[[package]]
+name = "png"
+version = "0.17.16"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "82151a2fc869e011c153adc57cf2789ccb8d9906ce52c0b39a6b5697749d7526"
+dependencies = [
+ "bitflags",
+ "crc32fast",
+ "fdeflate",
+ "flate2",
+ "miniz_oxide",
+]
+
+[[package]]
+name = "ppv-lite86"
+version = "0.2.20"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "77957b295656769bb8ad2b6a6b09d897d94f05c41b069aede1fcdaa675eaea04"
+dependencies = [
+ "zerocopy",
+]
+
+[[package]]
+name = "proc-macro2"
+version = "1.0.93"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "60946a68e5f9d28b0dc1c21bb8a97ee7d018a8b322fa57838ba31cc878e22d99"
+dependencies = [
+ "unicode-ident",
+]
+
+[[package]]
+name = "profiling"
+version = "1.0.16"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "afbdc74edc00b6f6a218ca6a5364d6226a259d4b8ea1af4a0ea063f27e179f4d"
+dependencies = [
+ "profiling-procmacros",
+]
+
+[[package]]
+name = "profiling-procmacros"
+version = "1.0.16"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a65f2e60fbf1063868558d69c6beacf412dc755f9fc020f514b7955fc914fe30"
+dependencies = [
+ "quote",
+ "syn",
+]
+
+[[package]]
+name = "qoi"
+version = "0.4.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7f6d64c71eb498fe9eae14ce4ec935c555749aef511cca85b5568910d6e48001"
+dependencies = [
+ "bytemuck",
+]
+
+[[package]]
+name = "quick-error"
+version = "2.0.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a993555f31e5a609f617c12db6250dedcac1b0a85076912c436e6fc9b2c8e6a3"
+
+[[package]]
+name = "quote"
+version = "1.0.38"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0e4dccaaaf89514f546c693ddc140f729f958c247918a13380cccc6078391acc"
+dependencies = [
+ "proc-macro2",
+]
+
+[[package]]
+name = "rand"
+version = "0.8.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "34af8d1a0e25924bc5b7c43c079c942339d8f0a8b57c39049bef581b46327404"
+dependencies = [
+ "libc",
+ "rand_chacha",
+ "rand_core",
+]
+
+[[package]]
+name = "rand_chacha"
+version = "0.3.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e6c10a63a0fa32252be49d21e7709d4d4baf8d231c2dbce1eaa8141b9b127d88"
+dependencies = [
+ "ppv-lite86",
+ "rand_core",
+]
+
+[[package]]
+name = "rand_core"
+version = "0.6.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c"
+dependencies = [
+ "getrandom",
+]
+
+[[package]]
+name = "rav1e"
+version = "0.7.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "cd87ce80a7665b1cce111f8a16c1f3929f6547ce91ade6addf4ec86a8dda5ce9"
+dependencies = [
+ "arbitrary",
+ "arg_enum_proc_macro",
+ "arrayvec",
+ "av1-grain",
+ "bitstream-io",
+ "built",
+ "cfg-if",
+ "interpolate_name",
+ "itertools",
+ "libc",
+ "libfuzzer-sys",
+ "log",
+ "maybe-rayon",
+ "new_debug_unreachable",
+ "noop_proc_macro",
+ "num-derive",
+ "num-traits",
+ "once_cell",
+ "paste",
+ "profiling",
+ "rand",
+ "rand_chacha",
+ "simd_helpers",
+ "system-deps",
+ "thiserror",
+ "v_frame",
+ "wasm-bindgen",
+]
+
+[[package]]
+name = "ravif"
+version = "0.11.11"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "2413fd96bd0ea5cdeeb37eaf446a22e6ed7b981d792828721e74ded1980a45c6"
+dependencies = [
+ "avif-serialize",
+ "imgref",
+ "loop9",
+ "quick-error",
+ "rav1e",
+ "rayon",
+ "rgb",
+]
+
+[[package]]
+name = "rayon"
+version = "1.10.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b418a60154510ca1a002a752ca9714984e21e4241e804d32555251faf8b78ffa"
+dependencies = [
+ "either",
+ "rayon-core",
+]
+
+[[package]]
+name = "rayon-core"
+version = "1.12.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1465873a3dfdaa8ae7cb14b4383657caab0b3e8a0aa9ae8e04b044854c8dfce2"
+dependencies = [
+ "crossbeam-deque",
+ "crossbeam-utils",
+]
+
+[[package]]
+name = "rgb"
+version = "0.8.50"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "57397d16646700483b67d2dd6511d79318f9d057fdbd21a4066aeac8b41d310a"
+
+[[package]]
+name = "rustversion"
+version = "1.0.19"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f7c45b9784283f1b2e7fb61b42047c2fd678ef0960d4f6f1eba131594cc369d4"
+
+[[package]]
+name = "serde"
+version = "1.0.217"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "02fc4265df13d6fa1d00ecff087228cc0a2b5f3c0e87e258d8b94a156e984c70"
+dependencies = [
+ "serde_derive",
+]
+
+[[package]]
+name = "serde_derive"
+version = "1.0.217"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5a9bf7cf98d04a2b28aead066b7496853d4779c9cc183c440dbac457641e19a0"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn",
+]
+
+[[package]]
+name = "serde_spanned"
+version = "0.6.8"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "87607cb1398ed59d48732e575a4c28a7a8ebf2454b964fe3f224f2afc07909e1"
+dependencies = [
+ "serde",
+]
+
+[[package]]
+name = "shlex"
+version = "1.3.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64"
+
+[[package]]
+name = "simd-adler32"
+version = "0.3.7"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d66dc143e6b11c1eddc06d5c423cfc97062865baf299914ab64caa38182078fe"
+
+[[package]]
+name = "simd_helpers"
+version = "0.1.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "95890f873bec569a0362c235787f3aca6e1e887302ba4840839bcc6459c42da6"
+dependencies = [
+ "quote",
+]
+
+[[package]]
+name = "smallvec"
+version = "1.14.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7fcf8323ef1faaee30a44a340193b1ac6814fd9b7b4e88e9d4519a3e4abe1cfd"
+
+[[package]]
+name = "syn"
+version = "2.0.98"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "36147f1a48ae0ec2b5b3bc5b537d267457555a10dc06f3dbc8cb11ba3006d3b1"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "unicode-ident",
+]
+
+[[package]]
+name = "system-deps"
+version = "6.2.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a3e535eb8dded36d55ec13eddacd30dec501792ff23a0b1682c38601b8cf2349"
+dependencies = [
+ "cfg-expr",
+ "heck",
+ "pkg-config",
+ "toml",
+ "version-compare",
+]
+
+[[package]]
+name = "target-lexicon"
+version = "0.12.16"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "61c41af27dd6d1e27b1b16b489db798443478cef1f06a660c96db617ba5de3b1"
+
+[[package]]
+name = "thiserror"
+version = "1.0.69"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b6aaf5339b578ea85b50e080feb250a3e8ae8cfcdff9a461c9ec2904bc923f52"
+dependencies = [
+ "thiserror-impl",
+]
+
+[[package]]
+name = "thiserror-impl"
+version = "1.0.69"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "4fee6c4efc90059e10f81e6d42c60a18f76588c3d74cb83a0b242a2b6c7504c1"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn",
+]
+
+[[package]]
+name = "tiff"
+version = "0.9.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ba1310fcea54c6a9a4fd1aad794ecc02c31682f6bfbecdf460bf19533eed1e3e"
+dependencies = [
+ "flate2",
+ "jpeg-decoder",
+ "weezl",
+]
+
+[[package]]
+name = "toml"
+version = "0.8.20"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "cd87a5cdd6ffab733b2f74bc4fd7ee5fff6634124999ac278c35fc78c6120148"
+dependencies = [
+ "serde",
+ "serde_spanned",
+ "toml_datetime",
+ "toml_edit",
+]
+
+[[package]]
+name = "toml_datetime"
+version = "0.6.8"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0dd7358ecb8fc2f8d014bf86f6f638ce72ba252a2c3a2572f2a795f1d23efb41"
+dependencies = [
+ "serde",
+]
+
+[[package]]
+name = "toml_edit"
+version = "0.22.24"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "17b4795ff5edd201c7cd6dca065ae59972ce77d1b80fa0a84d94950ece7d1474"
+dependencies = [
+ "indexmap",
+ "serde",
+ "serde_spanned",
+ "toml_datetime",
+ "winnow",
+]
+
+[[package]]
+name = "unicode-ident"
+version = "1.0.17"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "00e2473a93778eb0bad35909dff6a10d28e63f792f16ed15e404fca9d5eeedbe"
+
+[[package]]
+name = "v_frame"
+version = "0.3.8"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d6f32aaa24bacd11e488aa9ba66369c7cd514885742c9fe08cfe85884db3e92b"
+dependencies = [
+ "aligned-vec",
+ "num-traits",
+ "wasm-bindgen",
+]
+
+[[package]]
+name = "version-compare"
+version = "0.2.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "852e951cb7832cb45cb1169900d19760cfa39b82bc0ea9c0e5a14ae88411c98b"
+
+[[package]]
+name = "wasi"
+version = "0.11.0+wasi-snapshot-preview1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423"
+
+[[package]]
+name = "wasm-bindgen"
+version = "0.2.100"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1edc8929d7499fc4e8f0be2262a241556cfc54a0bea223790e71446f2aab1ef5"
+dependencies = [
+ "cfg-if",
+ "once_cell",
+ "rustversion",
+ "wasm-bindgen-macro",
+]
+
+[[package]]
+name = "wasm-bindgen-backend"
+version = "0.2.100"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "2f0a0651a5c2bc21487bde11ee802ccaf4c51935d0d3d42a6101f98161700bc6"
+dependencies = [
+ "bumpalo",
+ "log",
+ "proc-macro2",
+ "quote",
+ "syn",
+ "wasm-bindgen-shared",
+]
+
+[[package]]
+name = "wasm-bindgen-macro"
+version = "0.2.100"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7fe63fc6d09ed3792bd0897b314f53de8e16568c2b3f7982f468c0bf9bd0b407"
+dependencies = [
+ "quote",
+ "wasm-bindgen-macro-support",
+]
+
+[[package]]
+name = "wasm-bindgen-macro-support"
+version = "0.2.100"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8ae87ea40c9f689fc23f209965b6fb8a99ad69aeeb0231408be24920604395de"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn",
+ "wasm-bindgen-backend",
+ "wasm-bindgen-shared",
+]
+
+[[package]]
+name = "wasm-bindgen-shared"
+version = "0.2.100"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1a05d73b933a847d6cccdda8f838a22ff101ad9bf93e33684f39c1f5f0eece3d"
+dependencies = [
+ "unicode-ident",
+]
+
+[[package]]
+name = "weezl"
+version = "0.1.8"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "53a85b86a771b1c87058196170769dd264f66c0782acf1ae6cc51bfd64b39082"
+
+[[package]]
+name = "winnow"
+version = "0.7.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "59690dea168f2198d1a3b0cac23b8063efcd11012f10ae4698f284808c8ef603"
+dependencies = [
+ "memchr",
+]
+
+[[package]]
+name = "zerocopy"
+version = "0.7.35"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1b9b4fd18abc82b8136838da5d50bae7bdea537c574d8dc1a34ed098d6c166f0"
+dependencies = [
+ "byteorder",
+ "zerocopy-derive",
+]
+
+[[package]]
+name = "zerocopy-derive"
+version = "0.7.35"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "fa4f8080344d4671fb4e831a13ad1e68092748387dfc4f55e356242fae12ce3e"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn",
+]
+
+[[package]]
+name = "zune-core"
+version = "0.4.12"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "3f423a2c17029964870cfaabb1f13dfab7d092a62a29a89264f4d36990ca414a"
+
+[[package]]
+name = "zune-inflate"
+version = "0.2.54"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "73ab332fe2f6680068f3582b16a24f90ad7096d5d39b974d1c0aff0125116f02"
+dependencies = [
+ "simd-adler32",
+]
+
+[[package]]
+name = "zune-jpeg"
+version = "0.4.14"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "99a5bab8d7dedf81405c4bb1f2b83ea057643d9cb28778cea9eecddeedd2e028"
+dependencies = [
+ "zune-core",
+]
diff --git a/Cargo.toml b/Cargo.toml
new file mode 100644
index 0000000..239a32e
--- /dev/null
+++ b/Cargo.toml
@@ -0,0 +1,9 @@
+[package]
+name = "l2decodeV5"
+version = "0.1.0"
+edition = "2021"
+
+[dependencies]
+byteorder = "1.4"
+bzip2 = "0.5.1"
+image = "0.25.5"
\ No newline at end of file
diff --git a/README.md b/README.md
new file mode 100644
index 0000000..ebc553d
--- /dev/null
+++ b/README.md
@@ -0,0 +1,11 @@
+# l2decodeV5
+
+This is a prototype WSR-88D Level-II (NEXRAD weather radar) data decoder and image generator written in Rust. I've never really used Rust before, so I decided to rewrite one of my old C routines to get a feel for how things work. This code probably isn't the best, but it's a start. I've come to like Rust a lot in the process of writing this over the past couple of days!
+
+Notes:
+- Only processes reflectivity data for the first elevation scan. The code is structured such that it's possible to get other products out of it though.
+- Expected input is an Archive-II file assuming RDA build 19 and later. It probably wouldn't take much to get it to support data from sites running older builds of the RDA software.
+
+## Output Example
+
+![KRAX data c. 2020](https://i.imgur.com/2hn5zWR.png)
diff --git a/output.png b/output.png
new file mode 100644
index 0000000..c195a6e
Binary files /dev/null and b/output.png differ
diff --git a/src/main.rs b/src/main.rs
new file mode 100644
index 0000000..77946ce
--- /dev/null
+++ b/src/main.rs
@@ -0,0 +1,841 @@
+//
+// NEXRAD Level-II data decoder and image generator with WSR-88D Build
+// 10 Message 31 support
+//
+use std::io::{self, Read, Seek};
+use std::fs::File;
+use std::mem;
+use std::str;
+use std::cmp;
+use byteorder::{BigEndian, ReadBytesExt};
+use bzip2::read::{BzDecoder};
+use image::{ImageBuffer, Rgba};
+
+const MSG_LENGTH: usize = 2432;
+const MOMENT_LENGTH: usize = 2400;
+
+#[repr(u8)]
+enum RadialStatus {
+	StartElevationScan,
+	InterRadial,
+	EndElevationScan,
+	StartVolumeScan,
+	EndVolumeScan
+}
+
+#[repr(u8)]
+enum DataBlockType {
+	Volume,
+	Elevation,
+	Radial,
+	Reflectivity,
+	Velocity,
+	SpectrumWidth,
+	Zdr, // Differential reflectivity
+	Phi, // Differential phase
+	Rho, // Correlation coefficient
+}
+
+// --------
+// ARCHIVE II STRUCTS
+// These are implemented based on what's written in the ICD from NOAA:
+// https://www.roc.noaa.gov/public-documents/icds/2620002Y.pdf
+// --------
+
+// At the start of every Archive II file is a 24-byte record
+// describing certain attributes of the volume scan.
+#[repr(C, packed)]
+struct VolumeHeader {
+	ver: [u8; 2],
+	period: [u8; 1],
+	ext: [u8; 3],
+	modified_julian_date: i32,
+	modified_time_millis: i32,
+	icao_id: [u8; 4]
+}
+
+// Level II data packet -- used for all message types
+#[repr(C, packed)]
+struct Packet {
+	ctm: [i16; 6],
+	size: u16,
+	id: u8,
+	message_type: u8,
+	seq: u16,
+	gen_julian_date: u16,
+	gen_time_millis: u32,
+	num_message_segments: u16,
+	message_segment: u16,
+	data: [u8; 2404]
+}
+
+// Header data for message 31 (digital radar data)
+#[repr(C, packed)]
+#[derive(Copy, Clone)]
+struct Message31Header {
+	site_id: [u8; 4],
+    collection_time: u32,
+    modified_julian_date: u16,
+    radial_idx: u16,
+    radial_azimuth: f32,
+    compression_indicator: u8,
+    spare: u8,
+    radial_length: u16,
+    azimuth_res_spacing_code: u8,
+    radial_status: u8,
+    elevation_number: u8,
+    cut_sector_number: u8,
+    elevation_angle: f32,
+    radial_spot_blanking_status: u8,
+    azimuth_indexing_mode: u8,
+    data_block_count: u16,
+    data_block_pointers: [u32; 9]
+}
+
+// Data block (descriptor of generic data moment type)
+#[repr(C, packed)]
+#[derive(Copy, Clone)]
+struct DataBlock {
+	block_type: u8,
+	moment_name: [u8; 3]
+}
+
+impl Default for DataBlock {
+	fn default() -> Self {
+		Self {
+			block_type: 0,
+			moment_name: [0; 3]
+		}
+	}
+}
+
+#[repr(C, packed)]
+#[derive(Default, Copy, Clone)]
+struct VolumeData {
+	data_block: DataBlock,
+	size: u16,
+	ver_major: u8,
+	ver_minor: u8,
+	lat: f32,
+	lon: f32,
+	site_height: i16,
+	feedhorn_height: u16,
+	calibration_constant: f32,
+	hor_tx_power: f32,
+	ver_tx_power: f32,
+	system_diff_refl: f32,
+	initial_system_diff_refl: f32,
+	vcp_num: u16,
+	processing_status: u16
+}
+
+#[repr(C, packed)]
+#[derive(Default, Copy, Clone)]
+struct ElevationData {
+	data_block: DataBlock,
+	size: u16,
+	atmos: [u8; 2],
+	calibration_constant: f32
+}
+
+#[repr(C, packed)]
+#[derive(Default, Copy, Clone)]
+struct RadialData {
+	data_block: DataBlock,
+	size: u16,
+	unambiguous_range: u16,
+	noise_level_hor: f32,
+	noise_level_ver: f32,
+	nyquist_velocity: u16,
+	spare: [u8; 2],
+	calibration_constant_channel_hor: f32,
+	calibration_constant_channel_ver: f32
+}
+
+#[repr(C, packed)]
+#[derive(Default, Copy, Clone)]
+struct GenericDataMoment {
+	data_block: DataBlock,
+	reserved: u32,
+	num_gates: u16,
+	range: u16,
+	range_sample_interval: u16,
+	threshold: u16,
+	snr_threshold: u16,
+	control_flags: i8,
+	word_size: u8,
+	scale: f32,
+	offset: f32
+}
+
+#[repr(C, packed)]
+#[derive(Copy, Clone)]
+struct DataMoment {
+	generic_data_moment: GenericDataMoment,
+	data: [u16; MOMENT_LENGTH]
+}
+
+impl Default for DataMoment {
+	fn default() -> Self {
+		Self {
+			generic_data_moment: GenericDataMoment::default(),
+			data: [69; MOMENT_LENGTH]
+		}
+	}
+}
+
+// --------
+// PROCESSED STRUCTS
+// These aren't set to 1-byte alignment as these are *separate* from
+// structs meant to be written to from raw data
+// --------
+
+#[derive(Copy, Clone)]
+struct Message31 {
+	header: Message31Header,
+	volume_data: VolumeData,
+	elevation_data: ElevationData,
+	radial_data: RadialData,
+	reflectivity_data: DataMoment,
+	velocity_data: DataMoment,
+	spectrum_width_data: DataMoment,
+	zdr_data: DataMoment,
+	phi_data: DataMoment,
+	rho_data: DataMoment,
+	cfp_data: DataMoment
+}
+
+impl Message31 {
+	fn new(header: Message31Header) -> Self {
+		Self {
+			header,
+			volume_data: VolumeData::default(),
+			elevation_data: ElevationData::default(),
+			radial_data: RadialData::default(),
+			reflectivity_data: DataMoment::default(),
+			velocity_data: DataMoment::default(),
+			spectrum_width_data: DataMoment::default(),
+			zdr_data: DataMoment::default(),
+			phi_data: DataMoment::default(),
+			rho_data: DataMoment::default(),
+			cfp_data: DataMoment::default()
+		}
+	}
+}
+
+// Structure to hold radial data (radials), which contains momentary data (bins)
+struct ElevationScan {
+	radials: Vec<Message31>,
+	azimuth: [f32; 800],     // Expecting 720 radials, but
+	                         // overscan/underscan can occur...
+	first_radial_idx: u16,
+	first_radial_az: f32,
+	num_radials: i32,
+	num_gates: i32,
+	mean_elev_angle: f32,
+	accum_elev_angle: f32,
+	gate_size: f32,
+	first_gate: f32
+}
+
+impl ElevationScan {
+	fn new() -> Self {
+		Self {
+			radials: Vec::new(),
+			azimuth: [0.0; 800],
+			first_radial_idx: 0,
+			first_radial_az: 0.0,
+			num_radials: 0,
+			num_gates: 0,
+			mean_elev_angle: 0.0,
+			accum_elev_angle: 0.0,
+			gate_size: 0.0,
+			first_gate: 0.0
+		}
+	}
+}
+
+// Aggregate structure for any and all data coming from message packets
+struct VolumeScan {
+	volume_header: VolumeHeader,
+	elevation_scans: Vec<ElevationScan>
+}
+
+impl VolumeScan {
+	fn new(volume_header: VolumeHeader) -> Self {
+		Self { volume_header, elevation_scans: Vec::new() }
+	}
+}
+
+fn check_for_volume_scan<R: Read + Seek>(mut stream: R) -> Option<VolumeScan> {
+	let magic_str = b"AR2V00";
+	let mut magic_pos = 0;
+	let mut buf = [0u8; 1];
+
+	while magic_pos < magic_str.len() {
+		if stream.read_exact(&mut buf).is_err() {
+			println!("Stream error; EOF?");
+			return None;
+		}
+		if buf[0] == magic_str[magic_pos] {
+			magic_pos += 1;
+		} else {
+			magic_pos = 0;
+		}
+	}
+
+	println!("Found AR2V00xx identifier");
+
+	Some(process_volume_scan(stream))
+}
+
+fn process_volume_scan<R: Read>(mut stream: R) -> VolumeScan {
+	// Read volume header
+	// ------------------
+	let mut header_buffer = [0u8; mem::size_of::<VolumeHeader>()];
+	stream.read_exact(&mut header_buffer);
+	let volume_header: VolumeHeader = unsafe {
+		std::ptr::read(header_buffer.as_ptr() as *const _)
+	};
+	// Test the ICAO ID from the volume header real quick...
+	// TODO: We should include some kind of check against an expected
+	// ICAO ID via command line arguments
+	let icao = str::from_utf8(&volume_header.icao_id).unwrap();
+	println!("Detected ICAO ID: {}", icao);
+	
+	// Read rest of the data
+	// ---------------------
+	let mut volume_scan = VolumeScan::new(volume_header);
+	let mut control_word: i32 = 0;
+	let mut verify_control_word: i32 = 0;
+
+	let mut bail = false;
+	while !bail {
+		let mut raw_buffer = Vec::new();
+		let mut raw_buffer_size = 0;
+		let mut decompressed_data = Vec::new();
+		// Read in the control word (size) of the LDM record. There's
+		// also a chance this is a new volume scan, so we need to
+		// check for the "magic string" we encountered earlier.
+		let mut control_word: i32 = 0;
+		match stream.read_i32::<BigEndian>() {
+			Ok(res) => {
+				control_word = res;
+			},
+			Err(e) => {
+				bail = true;
+			}
+		}
+		if control_word == 0x56325241 /* "AR2V" but big endian */ {
+			// We've got another volume scan. We're bailing here, but
+			// really we ought to just start the decoding process over
+			// again from this byte.
+			println!("Encountered magic string twice!");
+			bail = true;
+			continue;
+		}
+
+		// Decompress the data
+		// -------------------
+		// TODO: check if it's actually bzip2-compressed first...?
+		if control_word >= raw_buffer_size {
+			raw_buffer.resize(control_word as usize, 0);
+			raw_buffer_size = control_word;
+		}
+		stream.read_exact(&mut raw_buffer);
+		let mut decoder = BzDecoder::new(&raw_buffer[..]);
+		let mut decoding = true;
+		let mut buffer = [0u8; 8192];
+		while decoding {
+			match decoder.read(&mut buffer) {
+				Ok(bytes_read) => {
+					if bytes_read == 0 {
+						decoding = false;
+						break;
+					}
+					decompressed_data.extend_from_slice(&buffer[..bytes_read]);
+				},
+				Err(error) => match error.kind() {
+					DataMagic => {
+						// If we've made it here then the bzip2 magic
+						// string is missing. In this case, we'll
+						// assume it's uncompressed data, although I
+						// feel like it'd probably be better to check
+						// for the string ahead of time prior to
+						// decompressing it. Doing it this way allows
+						// us to deal with Archive II files with data
+						// blocks that may or may not be
+						// bzip2-compressed though...
+						decompressed_data.extend_from_slice(&raw_buffer[..raw_buffer_size as usize]);
+					},
+					_ => panic!("Problem decompressing bzip2 data: {error:?}")
+				}
+			}
+			// If we have no data left, we can assume we're done.
+			if decompressed_data.len() == 0 {
+				decoding = false;
+				bail = true;
+			}
+		}
+
+		// Process decompressed data
+		// -------------------------
+		let mut msg_length: usize = MSG_LENGTH;
+		let mut i: usize = 0;
+		while i < decompressed_data.len() {
+			let slice = &decompressed_data[i..i+mem::size_of::<Packet>()];
+			let packet: Packet = unsafe {
+				std::ptr::read(slice.as_ptr() as *const _)
+			};
+			
+			// Message 31 packets are variable length; we must
+			// readjust. If this isn't a message 31 packet though,
+			// then return to standard length.
+			if packet.message_type == 31 {
+				msg_length = u16::from_be(packet.size) as usize * 2 + 12;
+			} else {
+				msg_length = MSG_LENGTH;
+			}
+
+			if packet.message_type != 0 {
+				// Handle messages accordingly. We'll break out
+				// message processing into separate routines since
+				// some of them can be pretty involved (e.g. 1, 31)
+				match packet.message_type {
+					31 => {
+						// println!("Processing message 31... length: {}", msg_length);
+						let raw_data = &decompressed_data[i + mem::size_of::<Packet>() + mem::size_of::<Message31Header>() - packet.data.len()..];
+						bail |= process_message_31(&mut volume_scan, packet, raw_data);
+						if bail {
+							// Bail as soon as the elevation (not
+							// volume!) scan is done.
+							// This works for my purposes, but isn't
+							// isn't ideal for loading a whole archive
+							// at once.
+							render_image(&mut volume_scan);
+						}
+					},
+					_ => {
+						println!("Message {} functionality unimplemented...", packet.message_type);
+					}
+				}
+			}
+			
+			i += msg_length;
+		}
+	}
+
+	return volume_scan;
+}
+
+fn convert_float(float: f32) -> f32 {
+	return f32::from_bits(float.to_bits().swap_bytes());
+}
+
+fn process_message_31(volume_scan: &mut VolumeScan, packet: Packet, raw_data: &[u8]) -> bool {
+	let slice = &packet.data[..mem::size_of::<Message31Header>()];
+	let header: Message31Header = unsafe {
+		std::ptr::read(slice.as_ptr() as *const _)
+	};
+
+	// Set up our new elevation scan (really not doing this the best
+	// way...)
+	let elevation_number: usize = header.elevation_number as usize;
+	if volume_scan.elevation_scans.len() < elevation_number {
+		volume_scan.elevation_scans.push(ElevationScan::new());
+	}
+	let elevation_scan: &mut ElevationScan = &mut volume_scan.elevation_scans[elevation_number - 1];
+
+	// Bump up the radial counter. This should contain the # of
+	// radials when we're done.
+	elevation_scan.num_radials += 1;
+
+	// Add the elevation angle of this radial (it varies) to the
+	// elevation scan structure
+	elevation_scan.accum_elev_angle += convert_float(header.elevation_angle);
+	elevation_scan.mean_elev_angle = elevation_scan.accum_elev_angle / elevation_scan.num_radials as f32;
+
+	// Note that NEXRAD radials are numbered starting at 1, but for
+	// our purposes we will number them starting at zero.
+	let radial: usize = u16::from_be(header.radial_idx) as usize - 1;
+
+	// Record the azimuth of the start of this radial. 'angle' is
+	// actually the center of the radial so to get the start we will
+	// subtract off 1/2 of the radial spacing (which will currently
+	// either be 1 or 0.5 degrees).
+	elevation_scan.azimuth[radial] = match header.azimuth_res_spacing_code {
+		1 => convert_float(header.radial_azimuth) - 0.25,
+		2 => convert_float(header.radial_azimuth) - 0.5,
+		_ => convert_float(header.radial_azimuth)
+	};
+
+	// If we go negative from the above operation, then correct
+	if elevation_scan.azimuth[radial] < 0.0 {
+		elevation_scan.azimuth[radial] = 360.0 + elevation_scan.azimuth[radial]; 
+	}
+
+	// Set our azimuth and index of the northernmost radial here.
+	// We'll also check if the azimuth of this radial is further
+	// clockwise than the ones we've already recorded. It's nice to
+	// know where the first radial is.
+	let wrap_around = elevation_scan.azimuth[radial] < elevation_scan.first_radial_az;
+	if radial == 0 || wrap_around {
+		elevation_scan.first_radial_az = elevation_scan.azimuth[radial];
+		elevation_scan.first_radial_idx = radial as u16;
+	}
+
+	// If we've wrapped around (i.e. crossed 0deg north) and the
+	// azimuth of this radial is further clockwise than the first one
+	// we've processed, then this indicates an overscan condition. In
+	// other words, we have scanned past 360 degrees. In this case,
+	// we'll effectively nullify this and all subsequent radials by
+	// decrementing the number of radials, making these not count.
+	if wrap_around && elevation_scan.azimuth[radial] > elevation_scan.azimuth[0] {
+		elevation_scan.num_radials -= 1;
+	}
+
+	// Build 19 added CFP (clutter filter power removed) data, an
+	// ABI-breaking change. Since our Message31Header structure is
+	// still set up to have only 9 data blocks, we'll need to check
+	// the RDA software version and bump up our byte index should this
+	// data be present. For now though, we'll hardcode it (cool)
+	let cfp_present_hack: usize = 4;
+
+	// Build 20 introduced another ABI-breaking change with the "RPG
+	// weighted mean ZDR bias estimate" field. It's 2 bytes w/ 6
+	// leftover as spare memory. We'll address it here too.
+	let rpg_weighted_mean_zdr_bias_present_hack = 0;
+
+	// Copy over the data to our block structure
+	let mut byte_idx = cfp_present_hack;
+	for i in 0..u16::from_be(header.data_block_count) {
+		let slice = &raw_data[byte_idx..byte_idx + mem::size_of::<DataBlock>()];
+		let data_block: DataBlock = unsafe { std::ptr::read(slice.as_ptr() as *const _) };
+		let name = str::from_utf8(&data_block.moment_name).unwrap();
+		if radial >= elevation_scan.radials.len() {
+			// This will be where the read data goes. We'll insert this into
+			// the elevation scan's radial data vector once we're done.
+			elevation_scan.radials.push(Message31::new(header));
+		}
+		match name {
+			"VOL" => {
+				byte_idx += mem::size_of::<VolumeData>(); 
+			},
+			"ELV" => {
+				byte_idx += mem::size_of::<ElevationData>();
+			},
+			"RAD" => {
+				byte_idx += mem::size_of::<RadialData>();
+			},
+			"REF"|"VEL"|"SW "|"ZDR"|"PHI"|"RHO"|"CFP" => {
+				let slice = &raw_data[byte_idx..byte_idx + mem::size_of::<GenericDataMoment>()];
+				let generic_moment: GenericDataMoment = unsafe {
+					std::ptr::read(slice.as_ptr() as *const _)
+				};
+				byte_idx += mem::size_of::<GenericDataMoment>();
+
+				// Per the ICD:
+				// 
+				// LDM is the amount of space in bytes required for a
+				// data moment array and equals ((NG * DWS) / 8) where
+				// NG is the number of gates at the gate spacing
+				// resolution specified and DWS is the number of bits
+				// stored for each gate (DWS is always a multiple of
+				// 8).
+				// 
+				// Means "Length of Data Moment"; no relation to
+				// UCAR's related LDM software :P
+				let num_gates = u16::from_be(generic_moment.num_gates);
+				let ldm = {num_gates * generic_moment.word_size as u16 / 8} as usize;
+
+				// Record adjusted gate information
+				elevation_scan.num_gates = cmp::max(elevation_scan.num_gates, num_gates as i32);
+				elevation_scan.gate_size = u16::from_be(generic_moment.range_sample_interval) as f32;
+				elevation_scan.first_gate = u16::from_be(generic_moment.range) as f32;
+
+				// Record radial data (gross nested match here but whatever)
+				let mut radial_block: &mut Message31 = &mut elevation_scan.radials[radial];
+				let mut moment: DataMoment = DataMoment::default();
+				let radar = &raw_data[byte_idx..];
+				for (i, chunk) in radar.chunks_exact(1).enumerate().take(MOMENT_LENGTH) {
+					moment.data[i] = chunk[0] as u16;
+					// For 16-bit data types (e.g. ZDR) we'd probably want this instead:
+					// moment.data[i] = u16::from_le_bytes([chunk[0], chunk[1]]);
+				}
+				match name {
+					"REF" => { radial_block.reflectivity_data = moment; },
+					"VEL" => { radial_block.velocity_data = moment; },
+					"SW " => { radial_block.spectrum_width_data = moment; },
+					"ZDR" => { radial_block.zdr_data = moment; },
+					"PHI" => { radial_block.phi_data = moment; },
+					"RHO" => { radial_block.rho_data = moment; },
+					"CFP" => { radial_block.cfp_data = moment; }
+					_ => {}
+				}
+				byte_idx += ldm;
+				
+			}
+			_ => {
+				// panic!("Unexpected block type!");
+			}
+		}
+
+		// Debug:
+		// println!("Block name: {}", name);
+	}
+
+	// For our purposes we only want to process one elevation scan for
+	// now... if we're at the end of the elevation (not volume) scan,
+	// we're done.
+	if header.radial_status == RadialStatus::EndElevationScan as u8 {
+		return true;
+	}
+	
+	return false;
+}
+
+fn get_dbz(r: f32, theta: f32, elevation_scan: &ElevationScan, smooth: bool) -> f32 {
+	let num_radials: i32 = elevation_scan.num_radials as i32;
+	let last_radial: i32 = num_radials - 1;
+	
+	let native_scale_r = elevation_scan.num_gates as f32 / 458.0;       // Gates per km
+	let native_scale_theta = elevation_scan.num_radials as f32 / 360.0; // Radials per deg
+
+	// Determine the range to the first gate that contains data (this
+	// is in the metadata from the radar). There is no data for the
+	// first km or two close to the radar site. It's the so-called
+	// "cone of silence". If we're asked for data in this region we'll
+	// return 0 dBZ.
+	let range_to_first_gate_km = elevation_scan.first_gate / 1000.0 - elevation_scan.gate_size / 1000.0 / 2.0;
+	if r <= range_to_first_gate_km {
+		return 0.0;
+	}
+
+	// Translate r (which is in km) to a gate number by subtracting
+	// off the range to the first gate data (which is also in km),
+	// then multiplying by the native_scale_r value (which is in
+	// gates/km). We need two gate numbers, one 'below' the actual r
+	// (i.e. closer to the radar site) and one 'above' the actual r
+	// (i.e. further from the radar site). These top and bottom gates
+	// will be used for interpolation.
+	let bottom = ((r - range_to_first_gate_km) * native_scale_r) as usize;
+	let top = (bottom + 1) as usize;
+
+	// Not only do we need the gate numbers for the top and bottom,
+	// but we also need the relative distance of r from the top and
+	// bottom gates. This distance is used as an interpolation weight,
+	// or to determine the nearest neighbor. For now we're only doing
+	// nearest neighbor interpolation.
+	let dist_bottom: f32 = (r - range_to_first_gate_km) * native_scale_r - bottom as f32;
+	let dist_top: f32 = 1.0 - dist_bottom;
+
+	// Identify the radial immediately preceeding the northernmost radial. It crosses the 0 degree threshold
+	let mut trans_radial = elevation_scan.first_radial_idx as i32;
+	if trans_radial < 0 {
+		trans_radial = last_radial;
+	}
+
+	// Here's a cool trick. Estimate the proper radial num (0 -
+	// lastradial) by subtracting the azimuth of the northernmost
+	// radial then taking the integer portion of the remainder. This
+	// will provide an index, relative to the northernmost radial, of
+	// the radial that we need. Once we have an index, we have to
+	// resolve it to an actual radial number by adding it to the
+	// radial number of the northernmost radial. If we exceed
+	// lastradial, then we have simply wrapped around.
+	let mut radial_idx: i32 = ((theta - elevation_scan.first_radial_az) * native_scale_theta + elevation_scan.first_radial_idx as f32) as i32;
+	if radial_idx > last_radial {
+		radial_idx -= num_radials;
+	}
+
+	// Check to see if our estimated radial index was correct. Usually
+	// it will be, however in some cases it might not. We check for
+	// overestimates and underestimates and starting at the estimate
+	// we work back and forth trying to find the true value. This was
+	// done to improve interpolation accuracy for some sites that have
+	// very uneven spacing between radial azimuths. A safety check is
+	// provided to prevent hangs. Also, the transition radial (the one
+	// right before 0 degrees) is exempted from this check for obvious
+	// reasons. This could be made better, but meh.
+	let mut safety = 10;
+	while theta < elevation_scan.azimuth[radial_idx as usize] && radial_idx != trans_radial && safety != 0 {
+		safety -= 1;
+		if radial_idx > 0 {
+			radial_idx -= 1;
+		}
+		if radial_idx < 0 {
+			radial_idx = last_radial;
+		}
+	}
+	safety = 10;
+	while theta > elevation_scan.azimuth[radial_idx as usize + 1] && radial_idx != trans_radial && safety != 0 {
+		safety -= 1;
+		radial_idx += 1;
+		if radial_idx > last_radial {
+			radial_idx -= num_radials;
+		}
+	}
+	if safety == 0 {
+		return 0.0;
+	}
+
+	// Determine the left and right radial numbers to use for interpolation.
+	let mut left: usize = 0;
+	let mut right: usize = 0;
+	let mut dist_left: f32 = 0.0;
+	let mut dist_right: f32 = 0.0;
+	if radial_idx == trans_radial {
+		// We're processing the radial immediately before the northernmost one
+		left = trans_radial as usize;
+		right = elevation_scan.first_radial_idx as usize;
+		if theta < 180.0 {
+			dist_left = theta + 360.0 - elevation_scan.azimuth[left];
+			dist_right = elevation_scan.azimuth[right] - theta;
+		} else {
+			dist_left = theta - elevation_scan.azimuth[left];
+			dist_right = elevation_scan.azimuth[right] + 360.0 - theta;
+		}
+	} else {
+		left = radial_idx as usize;
+		right = left + 1;
+		if right > last_radial as usize {
+			right = 0;
+		}
+		dist_left = theta - elevation_scan.azimuth[left];
+		dist_right = elevation_scan.azimuth[right] - theta;
+	}
+
+	// Grab the dBZ values for all 4 neighboring points
+	let mut dbz_ll = elevation_scan.radials[left].reflectivity_data.data[bottom] as f32;
+	let mut dbz_ul = elevation_scan.radials[left].reflectivity_data.data[top] as f32;
+	let mut dbz_lr = elevation_scan.radials[right].reflectivity_data.data[bottom] as f32;
+	let mut dbz_ur = elevation_scan.radials[right].reflectivity_data.data[top] as f32;
+
+	// Apply a dBZ threshold (don't want anything below 0 for reasons)
+	dbz_ll = f32::max(dbz_ll, 66.0);
+	dbz_ul = f32::max(dbz_ul, 66.0);
+	dbz_lr = f32::max(dbz_lr, 66.0);
+	dbz_ur = f32::max(dbz_ur, 66.0);
+
+	// Perform interpolation
+	let mut dbz_top: f32 = 0.0;
+	let mut dbz_bottom: f32 = 0.0;
+	let mut dbz: f32 = 0.0;
+	if smooth {
+		dbz_top = ((dbz_ul * dist_right) + (dbz_ur * dist_left)) / (dist_left + dist_right);
+		dbz_bottom = ((dbz_ll * dist_right) + (dbz_lr * dist_left)) / (dist_left + dist_right);
+		dbz = ((dbz_top * dist_bottom) + (dbz_bottom * dist_top)) / (dist_top + dist_bottom);
+	} else {
+		if dist_left < dist_right {
+			dbz_top = dbz_ul;
+			dbz_bottom = dbz_ll;
+		} else {
+			dbz_top = dbz_ur;
+			dbz_bottom = dbz_lr;
+		}
+		if dist_top < dist_bottom {
+			dbz = dbz_top;
+		} else {
+			dbz = dbz_bottom;
+		}
+	}
+	
+	return (dbz - 2.0) / 2.0 - 32.0;
+}
+
+const NUMINTERVALS: usize = 8;
+
+fn render_image(volume_scan: &mut VolumeScan) {
+	let elevation_scan: &ElevationScan = &volume_scan.elevation_scans[0];
+
+	// We *could* use a 16-color scale, but this 128-color interpolated one is cooler
+	let mut color_scale: [[u8; 3]; 16 * NUMINTERVALS] = [[0; 3]; 16 * NUMINTERVALS];
+	build_smooth_color_scale(&mut color_scale);
+
+	// Render :D
+	let width = 1920;
+	let height = 1920;
+	let mut img: ImageBuffer<Rgba<u8>, Vec<u8>> = ImageBuffer::new(width, height);
+	let mut x1: f32 = 0.0;
+	let mut y1: f32 = 0.0;
+	for (x, y, pixel) in img.enumerate_pixels_mut() {
+		x1 = x as f32 - width as f32 / 2.0;
+		y1 = height as f32 / 2.0 - y as f32;
+		let r = f32::sqrt(x1 * x1 + y1 * y1) * 0.25;
+		let mut theta = (1.570795 - f32::atan2(y1, x1)) * 57.29583; // (90deg-atan2(y1,x1))*180/pi)
+		if theta < 0.0 {
+			theta = 360.0 + theta;
+		}
+		let mut dbz = get_dbz(r, theta, elevation_scan, false);
+		let color_idx: usize = (dbz * NUMINTERVALS as f32 / 5.0 + 0.5) as usize;
+		if color_idx >= 16 * NUMINTERVALS || dbz <= 0.0 {
+			continue;
+		}
+		let out_r = color_scale[color_idx][0];
+		let out_g = color_scale[color_idx][1];
+		let out_b = color_scale[color_idx][2];
+		*pixel = Rgba([out_r, out_g, out_b, 0xFF]); 
+	}
+	img.save("output.png").expect("Failed to save image");
+}
+
+fn build_smooth_color_scale(scolorscale: &mut [[u8; 3]; 16 * NUMINTERVALS]) {
+    let colorscale: [[u8; 3]; 16] = [
+        [0, 0, 0],
+        [0, 72, 144],
+        [0, 96, 240],
+        [128, 224, 80],
+        [100, 184, 64],
+        [72, 144, 48],
+        [44, 104, 32],
+        [16, 64, 16],
+        [240, 160, 16],
+        [240, 118, 32],
+        [240, 16, 32],
+        [144, 0, 0],
+        [176, 32, 128],
+        [202, 64, 160],
+        [229, 96, 192],
+        [225, 128, 224],
+    ];
+    let numintervals = NUMINTERVALS as f32;
+    let mut step = 0;
+    while step < 15 {
+        let (curr, curg, curb) = (colorscale[step][0], colorscale[step][1], colorscale[step][2]);
+        let (nextr, nextg, nextb) = (colorscale[step + 1][0], colorscale[step + 1][1], colorscale[step + 1][2]);
+
+        // Compute the deltas between the colors
+        let deltar = (nextr as f32 - curr as f32) / numintervals;
+        let deltag = (nextg as f32 - curg as f32) / numintervals;
+        let deltab = (nextb as f32 - curb as f32) / numintervals;
+
+        for t in 0..numintervals as usize {
+            scolorscale[step * numintervals as usize + t][0] = (curr as f32 + deltar * t as f32).round() as u8;
+            scolorscale[step * numintervals as usize + t][1] = (curg as f32 + deltag * t as f32).round() as u8;
+            scolorscale[step * numintervals as usize + t][2] = (curb as f32 + deltab * t as f32).round() as u8;
+        }
+
+        step += 1;
+    }
+    scolorscale[step * numintervals as usize][0] = colorscale[step][0];
+    scolorscale[step * numintervals as usize][1] = colorscale[step][1];
+    scolorscale[step * numintervals as usize][2] = colorscale[step][2];
+
+    // Last bit of color scale is all white
+    for t in 1..numintervals as usize {
+        scolorscale[step * numintervals as usize + t][0] = (248 + t) as u8;
+        scolorscale[step * numintervals as usize + t][1] = (248 + t) as u8;
+        scolorscale[step * numintervals as usize + t][2] = (248 + t) as u8;
+    }
+}
+
+fn main() -> io::Result<()> {
+	println!("Processing file...");
+	let file = File::open("test/KRAX.dat")?;
+	if let Some(scan) = check_for_volume_scan(file) {
+		println!("Successfully processed volume scan");
+	} else {
+		println!("Failed to find volume scan data");
+	}
+	Ok(())
+}
diff --git a/test/KRAX.dat b/test/KRAX.dat
new file mode 100644
index 0000000..f6a08d8
Binary files /dev/null and b/test/KRAX.dat differ