Move the last of our Matcha-specific stuff out of the Tock tree

Change-Id: Ieb4e987dc4153bb5e873cc8c7c983db2df2b87d4
diff --git a/blob_fs/.gitignore b/blob_fs/.gitignore
new file mode 100644
index 0000000..a9d37c5
--- /dev/null
+++ b/blob_fs/.gitignore
@@ -0,0 +1,2 @@
+target
+Cargo.lock
diff --git a/blob_fs/Cargo.toml b/blob_fs/Cargo.toml
new file mode 100644
index 0000000..b99aa17
--- /dev/null
+++ b/blob_fs/Cargo.toml
@@ -0,0 +1,4 @@
+[package]
+name = "blob_fs"
+version = "0.1.0"
+edition = "2018"
diff --git a/blob_fs/src/bit_vector.rs b/blob_fs/src/bit_vector.rs
new file mode 100644
index 0000000..336b4a2
--- /dev/null
+++ b/blob_fs/src/bit_vector.rs
@@ -0,0 +1,293 @@
+use crate::errors::*;
+use crate::utils::*;
+
+pub struct BitVector<'a> {
+    pub bits: &'a mut [u32],
+}
+
+impl<'a> BitVector<'a> {
+    pub fn new(bits: &'a mut [u32]) -> Self {
+        return BitVector { bits: bits };
+    }
+
+    pub fn check_pos(&self, pos: usize) -> Result<(), BFSErr> {
+        dcheck!(pos <= self.bits.len() * 32, BFSErr::OutOfBounds);
+        return Ok(());
+    }
+
+    pub fn check_range(&self, begin: usize, end: usize) -> Result<(), BFSErr> {
+        self.check_pos(begin)?;
+        self.check_pos(end)?;
+        dcheck!(end >= begin, BFSErr::OutOfBounds);
+        return Ok(());
+    }
+
+    pub fn clear_all(&mut self) {
+        for i in 0..self.bits.len() {
+            self.bits[i] = 0x00000000;
+        }
+    }
+
+    pub fn set_all(&mut self) {
+        for i in 0..self.bits.len() {
+            self.bits[i] = 0xFFFFFFFF;
+        }
+    }
+
+    pub fn get_bit(&self, pos: usize) -> Result<u32, BFSErr> {
+        self.check_pos(pos)?;
+        return Ok((self.bits[pos >> 5] >> (pos & 31)) & 1);
+    }
+
+    pub fn set_bit(&mut self, pos: usize) -> Result<(), BFSErr> {
+        self.check_pos(pos)?;
+        self.bits[pos >> 5] |= 1 << (pos & 31);
+        return Ok(());
+    }
+
+    pub fn clear_bit(&mut self, pos: usize) -> Result<(), BFSErr> {
+        self.check_pos(pos)?;
+        self.bits[pos >> 5] &= !(1 << (pos & 31));
+        return Ok(());
+    }
+
+    fn bit_mask(head: usize, tail: usize) -> Result<u32, BFSErr> {
+        dcheck!(head < 32, BFSErr::OutOfBounds);
+        dcheck!(tail < 32, BFSErr::OutOfBounds);
+
+        let a = 0xFFFFFFFF << head;
+        let b = 0xFFFFFFFF >> (32 - tail - 1);
+        return Ok(a & b);
+    }
+
+    pub fn set_range(&mut self, begin: usize, end: usize) -> Result<(), BFSErr> {
+        self.check_range(begin, end)?;
+
+        let block_head = begin >> 5;
+        let block_tail = (end - 1) >> 5;
+        let bit_head = begin & 31;
+        let bit_tail = (end - 1) & 31;
+
+        if block_head == block_tail {
+            let mask = BitVector::bit_mask(bit_head, bit_tail)?;
+            self.bits[block_head as usize] |= mask;
+        } else {
+            let mask_head = BitVector::bit_mask(bit_head, 31)?;
+            self.bits[block_head as usize] |= mask_head;
+
+            for i in block_head + 1..block_tail {
+                self.bits[i as usize] = 0xFFFFFFFF;
+            }
+
+            let mask_tail = BitVector::bit_mask(0, bit_tail)?;
+            self.bits[block_tail as usize] |= mask_tail;
+        }
+
+        return Ok(());
+    }
+
+    pub fn clear_range(&mut self, begin: usize, end: usize) -> Result<(), BFSErr> {
+        self.check_range(begin, end)?;
+
+        let block_head = begin >> 5;
+        let block_tail = (end - 1) >> 5;
+        let bit_head = begin & 31;
+        let bit_tail = (end - 1) & 31;
+
+        if block_head == block_tail {
+            let mask = BitVector::bit_mask(bit_head, bit_tail)?;
+            self.bits[block_head] &= !mask;
+        } else {
+            let mask_head = BitVector::bit_mask(bit_head, 31)?;
+            self.bits[block_head] &= !mask_head;
+
+            for i in block_head + 1..block_tail {
+                self.bits[i] = 0x00000000;
+            }
+
+            let mask_tail = BitVector::bit_mask(0, bit_tail)?;
+            self.bits[block_tail] &= !mask_tail;
+        }
+
+        return Ok(());
+    }
+
+    pub fn count_range(&mut self, begin: usize, end: usize) -> Result<usize, BFSErr> {
+        self.check_range(begin, end)?;
+
+        let block_head = begin >> 5;
+        let block_tail = (end - 1) >> 5;
+        let bit_head = begin & 31;
+        let bit_tail = (end - 1) & 31;
+
+        let mut count: usize = 0;
+
+        if block_head == block_tail {
+            let mask = BitVector::bit_mask(bit_head, bit_tail)?;
+            count += (self.bits[block_head] & mask).count_ones() as usize;
+        } else {
+            let mask_head = BitVector::bit_mask(bit_head, 31)?;
+            count += (self.bits[block_head] & mask_head).count_ones() as usize;
+
+            for i in block_head + 1..block_tail {
+                count += self.bits[i].count_ones() as usize;
+            }
+
+            let mask_tail = BitVector::bit_mask(0, bit_tail)?;
+            count += (self.bits[block_tail] & mask_tail).count_ones() as usize;
+        }
+
+        return Ok(count);
+    }
+
+    pub fn find_hole(&self, mut begin: usize, end: usize, width: usize) -> Result<usize, BFSErr> {
+        self.check_range(begin, end)?;
+        dcheck!(width <= end - begin, BFSErr::NotFound);
+
+        let mut skip = false;
+        let mut block_head = begin >> 5;
+        let block_tail = (end - 1) >> 5;
+
+        while (block_head <= block_tail) && (self.bits[block_head] == 0xFFFFFFFF) {
+            skip = true;
+            block_head = block_head + 1;
+        }
+
+        if block_head > block_tail {
+            return Err(BFSErr::NotFound);
+        }
+
+        if skip {
+            begin = block_head << 5;
+        }
+
+        for mut i in begin..=(end - width) {
+            skip = false;
+            for j in (i..i + width).rev() {
+                if self.get_bit(j)? == 1 {
+                    i = j + 1;
+                    skip = true;
+                    break;
+                }
+            }
+            if !skip {
+                return Ok(i);
+            }
+        }
+
+        return Err(BFSErr::NotFound);
+    }
+
+    pub fn find_span(&self, mut begin: usize, end: usize, width: usize) -> Result<usize, BFSErr> {
+        self.check_range(begin, end)?;
+        dcheck!(width <= end - begin, BFSErr::NotFound);
+
+        let mut skip = false;
+        let mut block_head = begin >> 5;
+        let block_tail = (end - 1) >> 5;
+
+        while (block_head <= block_tail) && (self.bits[block_head] == 0x00000000) {
+            skip = true;
+            block_head = block_head + 1;
+        }
+
+        if block_head > block_tail {
+            return Err(BFSErr::NotFound);
+        }
+
+        if skip {
+            begin = block_head << 5;
+        }
+
+        for mut i in begin..=(end - width) {
+            skip = false;
+            for j in (i..i + width).rev() {
+                if self.get_bit(j)? == 0 {
+                    i = j + 1;
+                    skip = true;
+                    break;
+                }
+            }
+            if !skip {
+                return Ok(i);
+            }
+        }
+
+        return Err(BFSErr::NotFound);
+    }
+}
+
+/// For all vector sizes up to <N> and all possible hole sizes & positions in
+/// that range, create a vector consisting of set bits outside the hole and
+/// cleared bits inside the hole.
+///
+/// Verify that findHole() always finds holes equal to or smaller than the one
+/// punched and fails to find holes larger than the one punched.
+///
+/// (This test is a bit slow, so we limit maximum bit vector size to 96 and run
+/// tests in this crate in optimized mode)
+
+#[test]
+fn test_find_hole() {
+    for size in 1..=96 as usize {
+        let block_count = (size + 31) / 32;
+        let mut bits: Vec<u32> = vec![0xFFFFFFFF as u32; block_count];
+        let mut bit_vec = BitVector::new(bits.as_mut());
+        for width in 1..=size - 1 {
+            for begin in 0..=(size - width) {
+                // Punch a hole in the bit vector.
+                let end = begin + width;
+                assert_ok!(bit_vec.clear_range(begin, end));
+
+                // We should be able to find the hole if we look for it.
+                assert_ok!(bit_vec.find_hole(0, size, width));
+
+                // We should be able to find a hole smaller than the one we punched.
+                assert_ok!(bit_vec.find_hole(0, size, width - 1));
+
+                // If we look for a hole larger than the one we punched, we should
+                // find nothing.
+                assert_err!(bit_vec.find_hole(0, size, width + 1));
+
+                // Fill the hole back up.
+                bit_vec.set_range(begin, end).unwrap();
+
+                // We should no longer be able to find it.
+                assert_err!(bit_vec.find_hole(0, size, width));
+            }
+        }
+    }
+}
+
+/// Same as above, but set bits (spans) instead of holes.
+#[test]
+fn test_find_span() {
+    for size in 1..=96 {
+        let block_count = (size + 31) / 32;
+        let mut bits: Vec<u32> = vec![0x00000000 as u32; block_count];
+        let mut bit_vec = BitVector::new(bits.as_mut());
+        for width in 1..=size - 1 {
+            for begin in 0..=(size - width) {
+                // Create a span in the bit vector.
+                let end = begin + width;
+                assert_ok!(bit_vec.set_range(begin, end));
+
+                // We should be able to find the hole if we look for it.
+                assert_ok!(bit_vec.find_span(0, size, width));
+
+                // We should be able to find a hole smaller than the one we punched.
+                assert_ok!(bit_vec.find_span(0, size, width - 1));
+
+                // If we look for a hole larger than the one we punched, we should
+                // find nothing.
+                assert_err!(bit_vec.find_span(0, size, width + 1));
+
+                // Erase the span
+                bit_vec.clear_range(begin, end).unwrap();
+
+                // We should no longer be able to find it.
+                assert_err!(bit_vec.find_span(0, size, width));
+            }
+        }
+    }
+}
diff --git a/blob_fs/src/blob_device.rs b/blob_fs/src/blob_device.rs
new file mode 100644
index 0000000..0a6c48f
--- /dev/null
+++ b/blob_fs/src/blob_device.rs
@@ -0,0 +1,206 @@
+/// A wrapper around BlockDevice that adds helper methods for navigating a
+/// BlobFS filesystem.
+use crate::bit_vector::*;
+use crate::block_device::*;
+use crate::errors::*;
+use crate::structs::*;
+use crate::utils::*;
+
+use core::mem;
+
+pub struct BlobDevice<'a> {
+    pub bd: &'a mut dyn BlockDevice,
+}
+
+impl<'a> BlobDevice<'a> {
+    const MAGIC_0: u64 = 0xac2153479e694d21;
+    const MAGIC_1: u64 = 0x985000d4d4d3d314;
+
+    pub fn new(bd: &'a mut dyn BlockDevice) -> Self {
+        return BlobDevice { bd: bd };
+    }
+
+    pub fn sanity_check(&self) -> Result<(), BFSErr> {
+        return Ok(());
+    }
+
+    /// Base address fetchers
+
+    pub fn superblock_base(&self) -> usize {
+        return self.bd.geom().block_size * 0;
+    }
+
+    pub fn blockmap_base(&self) -> usize {
+        return self.bd.geom().block_size * 1;
+    }
+
+    pub fn nodemap_base(&self) -> usize {
+        return self.bd.geom().block_size * 2;
+    }
+
+    pub fn journal_base(&self) -> usize {
+        return self.bd.geom().block_size * 3;
+    }
+
+    pub fn block_base(&self) -> usize {
+        return self.bd.geom().block_size * 4;
+    }
+
+    /// Format a whole device to support BlobFS.
+
+    pub fn format(&mut self) -> Result<(), BFSErr> {
+        // Erase the whole device.
+        for i in 0..self.bd.geom().block_count {
+            self.bd.erase_block(i)?;
+        }
+
+        // Write the initial superblock.
+        self.format_superblock()?;
+
+        // Write the initial blockmap.
+        self.format_blockmap()?;
+
+        return Ok(());
+    }
+
+    pub fn read_superblock(&self, superblock: &mut Superblock) -> Result<(), BFSErr> {
+        let blob = as_unsafe_blob_mut(superblock);
+        self.bd.read_range(0, blob)?;
+
+        // Sanity check the superblock
+        dcheck!(superblock.magic0 == BlobDevice::MAGIC_0, BFSErr::Corrupt);
+        dcheck!(superblock.magic1 == BlobDevice::MAGIC_1, BFSErr::Corrupt);
+
+        return Ok(());
+    }
+
+    pub fn write_superblock(&mut self, superblock: &Superblock) -> Result<(), BFSErr> {
+        // Sanity check the superblock
+        dcheck!(superblock.magic0 == BlobDevice::MAGIC_0, BFSErr::Corrupt);
+        dcheck!(superblock.magic1 == BlobDevice::MAGIC_1, BFSErr::Corrupt);
+
+        let blob = as_unsafe_blob(superblock);
+        self.bd.write_range(0, blob)?;
+        return Ok(());
+    }
+
+    pub fn format_superblock(&mut self) -> Result<(), BFSErr> {
+        let block_size = self.bd.geom().block_size;
+        let block_count = self.bd.geom().block_count;
+
+        let superblock_count = 1;
+        let bitmap_block_count = 1;
+        let node_block_count = 1;
+        let journal_block_count = 1;
+        let data_block_count = block_count
+            - superblock_count
+            - bitmap_block_count
+            - node_block_count
+            - journal_block_count;
+
+        let inodes_per_block = block_size / mem::size_of::<Inode>();
+
+        // Write the superblock to offset 0
+        let superblock = Superblock {
+            magic0: BlobDevice::MAGIC_0,
+            magic1: BlobDevice::MAGIC_1,
+            version: 0,
+            flags: 0,
+            block_size: block_size as u32,
+            unused: 0,
+            data_block_count: data_block_count as u64,
+            journal_block_count: journal_block_count as u64,
+            inode_count: inodes_per_block as u64,
+            alloc_block_count: 0,
+            alloc_inode_count: 0,
+        };
+        self.write_superblock(&superblock)?;
+        return Ok(());
+    }
+
+    pub fn format_blockmap(&mut self) -> Result<(), BFSErr> {
+        let bits: [u8; 1] = [0xF0];
+        self.bd.write_range(self.blockmap_base(), &bits)?;
+        return Ok(());
+    }
+
+    pub fn read_blockmap(&self, bitmap: &mut BitVector) -> Result<(), BFSErr> {
+        self.bd.read_range(self.blockmap_base(), slice_as_unsafe_blob_mut(bitmap.bits))?;
+        return Ok(());
+    }
+
+    pub fn write_blockmap(&mut self, bitmap: &mut BitVector) -> Result<(), BFSErr> {
+        self.bd.write_range(self.blockmap_base(), slice_as_unsafe_blob(bitmap.bits))?;
+        return Ok(());
+    }
+
+    pub fn read_node_header(&self, index: usize, node: &mut NodeHeader) -> Result<(), BFSErr> {
+        let base = self.nodemap_base();
+        let offset = index * mem::size_of::<Inode>() * index;
+        let blob = as_unsafe_blob_mut(node);
+        self.bd.read_range(base + offset, blob)?;
+        return Ok(());
+    }
+
+    pub fn read_inode(&self, index: usize, inode: &mut Inode) -> Result<(), BFSErr> {
+        let base = self.nodemap_base();
+        let offset = index * mem::size_of::<Inode>();
+        let blob = as_unsafe_blob_mut(inode);
+        self.bd.read_range(base + offset, blob)?;
+        return Ok(());
+    }
+
+    pub fn write_inode(&mut self, index: usize, inode: &Inode) -> Result<(), BFSErr> {
+        dcheck!((inode.header.flags & NodeHeader::FLAG_INODE) != 0, BFSErr::InvalidArg);
+        let base = self.nodemap_base();
+        let offset = index * mem::size_of::<Inode>();
+        let blob = as_unsafe_blob(inode);
+        self.bd.write_range(base + offset, blob)?;
+        return Ok(());
+    }
+
+    /// Invalidate an inode by zeroing out its header. The inode cannot be reused
+    /// until the block containing it is erased.
+    pub fn invalidate_inode(&mut self, index: usize) -> Result<(), BFSErr> {
+        let base = self.nodemap_base();
+        let offset = index * mem::size_of::<Inode>();
+        let blob = [0; mem::size_of::<NodeHeader>()];
+        self.bd.overwrite_range(base + offset, &blob)?;
+        return Ok(());
+    }
+
+    pub fn blob_size_in_blocks(&self, blob: &[u8]) -> u16 {
+        let block_size = self.bd.geom().block_size;
+        let result = (blob.len() + block_size + 1) / block_size;
+        assert!(result < u16::MAX as usize);
+        return result as u16;
+    }
+
+    pub fn read_blob(&self, extent: Extent, blob_out: &mut [u8]) -> Result<(), BFSErr> {
+        let block_size = self.bd.geom().block_size;
+        let mut cursor = extent.offset();
+        for chunk in blob_out.chunks_mut(block_size) {
+            self.bd.read_block(cursor, chunk)?;
+            cursor = cursor + 1;
+        }
+        return Ok(());
+    }
+
+    pub fn write_blob(&mut self, extent: Extent, blob_in: &[u8]) -> Result<(), BFSErr> {
+        let block_size = self.bd.geom().block_size;
+        let mut cursor = extent.offset();
+        for chunk in blob_in.chunks(block_size) {
+            self.bd.write_block(cursor, &chunk)?;
+            cursor = cursor + 1;
+        }
+        return Ok(());
+    }
+
+    pub fn delete_blob(&mut self, extent: Extent) -> Result<(), BFSErr> {
+        let offset = extent.offset();
+        for i in 0..extent.size as usize {
+            self.bd.erase_block(offset + i)?;
+        }
+        return Ok(());
+    }
+}
diff --git a/blob_fs/src/blob_fs.rs b/blob_fs/src/blob_fs.rs
new file mode 100644
index 0000000..72e72d5
--- /dev/null
+++ b/blob_fs/src/blob_fs.rs
@@ -0,0 +1,234 @@
+use crate::bit_vector::*;
+use crate::blob_device::*;
+use crate::block_device::*;
+use crate::errors::*;
+use crate::structs::*;
+use crate::utils::*;
+
+pub struct BlobFS<'a> {
+    pub bd: BlobDevice<'a>,
+    pub superblock: Superblock,
+    pub blockmap: BitVector<'a>,
+}
+
+// Public impl
+
+impl<'a> BlobFS<'a> {
+    pub fn new(bd: &'a mut dyn BlockDevice, blockmap_bits: &'a mut [u32]) -> Self {
+        let result = BlobFS {
+            bd: BlobDevice::new(bd),
+            superblock: Superblock::default(),
+            blockmap: BitVector::new(blockmap_bits),
+        };
+
+        return result;
+    }
+
+    pub fn format(&mut self) -> Result<(), BFSErr> {
+        // Format the device.
+        self.bd.format()?;
+
+        return Ok(());
+    }
+
+    pub fn mount(&mut self) -> Result<(), BFSErr> {
+        // Read the superblock from the device.
+        self.bd.read_superblock(&mut self.superblock)?;
+
+        // Read the allocation bitmap from the device
+        self.bd.read_blockmap(&mut self.blockmap)?;
+
+        return Ok(());
+    }
+
+    pub fn sanity_check(&self) -> Result<(), BFSErr> {
+        return Ok(());
+    }
+
+    pub fn get_blob_size(&self, hash: u64) -> Result<usize, BFSErr> {
+        let result = self.find_inode(hash)?;
+        let inode = result.1;
+        return Ok(inode.blob_size as usize);
+    }
+
+    pub fn get_blob(&self, hash: u64, blob_out: &mut [u8]) -> Result<(), BFSErr> {
+        let result = self.find_inode(hash)?;
+        let inode = result.1;
+        dcheck!(inode.blob_size as usize <= blob_out.len(), BFSErr::OutOfBounds);
+
+        let dst = &mut blob_out[..inode.blob_size as usize];
+        self.bd.read_blob(inode.inline_extent, dst)?;
+
+        return Ok(());
+    }
+
+    pub fn put_blob(&mut self, hash: u64, blob_in: &[u8]) -> Result<(), BFSErr> {
+        if self.find_inode(hash).is_ok() {
+            return Err(BFSErr::Duplicate);
+        }
+
+        let blob_block_count = self.bd.blob_size_in_blocks(blob_in);
+
+        // Find a place to put the blob.
+        let extent = self.find_extent(blob_block_count)?;
+
+        // Copy the blob to the block device.
+        self.bd.write_blob(extent, blob_in)?;
+
+        // Set the corresponding bits in our local bitmap
+        self.blockmap.clear_range(extent.offset(), extent.offset() + extent.size as usize)?;
+        // FIXME flush bitmap to disk?
+
+        // Create the inode for the new blob
+        let inode = Inode {
+            header: NodeHeader {
+                flags: NodeHeader::FLAG_INODE,
+                version: 0x0,
+                next_node: 0xFFFFFFFF, // FIXME
+            },
+            hash0: hash, // FIXME just using 64-bit hash
+            hash1: hash,
+            hash2: hash,
+            hash3: hash,
+            blob_size: blob_in.len() as u64,
+            block_count: blob_block_count as u32,
+            extent_count: 1,
+            padding: 0xFFFF,
+            inline_extent: extent,
+        };
+
+        // Put the inode in the inode table
+        let inode_idx = self.find_free_inode()?;
+        self.bd.write_inode(inode_idx, &inode)?;
+
+        return Ok(());
+    }
+
+    pub fn delete_blob(&mut self, hash: u64) -> Result<(), BFSErr> {
+        let result = self.find_inode(hash)?;
+        let inode_idx = result.0;
+        let extent = result.1.inline_extent;
+
+        self.bd.invalidate_inode(inode_idx)?;
+        self.blockmap.set_range(extent.offset(), extent.offset() + extent.size as usize)?;
+        self.bd.delete_blob(extent)?;
+        self.bd.delete_blob(extent)?;
+        return Ok(());
+    }
+}
+
+// Private impl
+
+impl<'a> BlobFS<'a> {
+    fn find_inode(&self, hash: u64) -> Result<(usize, Inode), BFSErr> {
+        for i in 0..self.superblock.inode_count as usize {
+            let mut inode = Inode::default();
+            self.bd.read_inode(i, &mut inode)?;
+            if inode.header.flags == NodeHeader::FLAG_INODE && inode.hash0 == hash {
+                return Ok((i, inode));
+            }
+        }
+        return Err(BFSErr::NotFound);
+    }
+
+    // FIXME quick and dirty scan entire inode table for empty slot
+
+    fn find_free_inode(&self) -> Result<usize, BFSErr> {
+        for i in 0..self.superblock.inode_count as usize {
+            let mut header = NodeHeader::default();
+            self.bd.read_node_header(i, &mut header)?;
+            if header.flags == u16::MAX {
+                return Ok(i);
+            }
+        }
+        assert!(false);
+        return Err(BFSErr::NotFound);
+    }
+
+    #[allow(dead_code)]
+    fn count_inodes(&self) -> Result<usize, BFSErr> {
+        let mut count = 0;
+        for i in 0..self.superblock.inode_count as usize {
+            let mut header = NodeHeader::default();
+            self.bd.read_node_header(i, &mut header)?;
+            if header.flags == NodeHeader::FLAG_INODE {
+                count = count + 1;
+            }
+        }
+        return Ok(count);
+    }
+
+    fn find_extent(&self, blob_block_count: u16) -> Result<Extent, BFSErr> {
+        let block_count = self.bd.bd.geom().block_count;
+        let offset = self.blockmap.find_span(0, block_count, blob_block_count as usize)?;
+
+        return Ok(Extent {
+            size: blob_block_count as u16,
+            //offset_hi: (offset >> 32) as u16,
+            offset_hi: 0 as u16,
+            offset_lo: offset as u32,
+        });
+    }
+}
+
+// Unit tests
+
+#[test]
+fn test_basic() {
+    use crate::test_device::*;
+    const BLOCK_SIZE: usize = 8192;
+    const BLOCK_COUNT: usize = 32;
+
+    let geom = BlockDeviceGeometry { block_size: BLOCK_SIZE, block_count: BLOCK_COUNT };
+    let mut buf: [u8; BLOCK_SIZE * BLOCK_COUNT] = [0; BLOCK_SIZE * BLOCK_COUNT];
+    let mut dirty_bits = [0; BLOCK_SIZE * BLOCK_COUNT / 32];
+    let bd: &mut dyn BlockDevice = &mut TestDevice::new(geom, buf.as_mut_ptr(), &mut dirty_bits);
+
+    let mut blockmap_bits: [u32; BLOCK_COUNT / 32] = [0xFFFFFFFF; BLOCK_COUNT / 32];
+
+    let mut fs = BlobFS::new(bd, blockmap_bits.as_mut());
+
+    assert_ok!(fs.format());
+    assert_ok!(fs.mount());
+    assert_ok!(fs.sanity_check());
+
+    // Block map should start with 4 reserved blocks for metadata
+    assert_eq!(fs.blockmap.count_range(0, BLOCK_COUNT).unwrap(), BLOCK_COUNT - 4);
+
+    // Store a small blob in the filesystem
+    let blob_hash: u64 = 0xDEADBEEFF00DCAFE;
+    let blob_text = "This is the contents of a blob";
+    let blob_contents = blob_text.as_bytes();
+    assert_ok!(fs.put_blob(blob_hash, blob_contents));
+
+    // Block map should have lost one free block
+    assert_eq!(fs.blockmap.count_range(0, BLOCK_COUNT).unwrap(), BLOCK_COUNT - 5);
+
+    // Storing it a second time should fail.
+    assert_err!(fs.put_blob(blob_hash, blob_contents));
+
+    // Read it back out
+    let blob_len = fs.get_blob_size(blob_hash).unwrap();
+    let mut blob_contents: Vec<u8> = vec![0; blob_len];
+    assert_ok!(fs.get_blob(blob_hash, &mut blob_contents));
+
+    // Contents should match.
+    let new_blob_text = core::str::from_utf8(&blob_contents).unwrap();
+    assert_eq!(blob_text, new_blob_text);
+
+    // Delete it and lookups should fail
+    assert_ok!(fs.delete_blob(blob_hash));
+    assert_err!(fs.get_blob_size(blob_hash));
+
+    // Deleting it a second time should also fail.
+    assert_err!(fs.delete_blob(blob_hash));
+
+    // Block map should have gained one free block
+    assert_eq!(fs.blockmap.count_range(0, BLOCK_COUNT).unwrap(), BLOCK_COUNT - 4);
+
+    // Reading a non-existent blob should cause an error
+    let bad_hash: u64 = 0xAAAAAAAAAAAAAAAA;
+    assert_err!(fs.get_blob_size(bad_hash));
+    let mut blob_contents: [u8; 256] = [0; 256];
+    assert_err!(fs.get_blob(bad_hash, &mut blob_contents));
+}
diff --git a/blob_fs/src/block_device.rs b/blob_fs/src/block_device.rs
new file mode 100644
index 0000000..71c76f2
--- /dev/null
+++ b/blob_fs/src/block_device.rs
@@ -0,0 +1,18 @@
+use crate::errors::*;
+
+#[derive(Debug, Copy, Clone)]
+pub struct BlockDeviceGeometry {
+    pub block_size: usize,
+    pub block_count: usize,
+}
+
+pub trait BlockDevice {
+    fn geom(&self) -> BlockDeviceGeometry;
+    fn read_block(&self, block: usize, block: &mut [u8]) -> Result<(), BFSErr>;
+    fn write_block(&mut self, block: usize, block: &[u8]) -> Result<(), BFSErr>;
+    fn erase_block(&mut self, block: usize) -> Result<(), BFSErr>;
+
+    fn read_range(&self, addr: usize, data: &mut [u8]) -> Result<(), BFSErr>;
+    fn write_range(&mut self, addr: usize, data: &[u8]) -> Result<(), BFSErr>;
+    fn overwrite_range(&mut self, addr: usize, data: &[u8]) -> Result<(), BFSErr>;
+}
diff --git a/blob_fs/src/errors.rs b/blob_fs/src/errors.rs
new file mode 100644
index 0000000..cf92156
--- /dev/null
+++ b/blob_fs/src/errors.rs
@@ -0,0 +1,14 @@
+#[derive(Debug, PartialEq)]
+pub enum BFSErr {
+    DeviceErr,    // The underlying block device had an error.
+    NotFound,     // The resource was not found.
+    CleanRead,    // Tried to read a byte that had never been written.
+    DirtyWrite,   // Tried to write a byte that had already been written.
+    OutOfBounds,  // Tried to read off the end of the device.
+    BadOverwrite, // Tried to change a bit from 0->1 without an erase.
+    BadErase,     // Tried to erase a clean block
+    Corrupt,      // Something was corrupt...
+    Full,         // No free blocks left.
+    Duplicate,    // Tried to add a blob that already existed.
+    InvalidArg,   // Bad argument passed to API
+}
diff --git a/blob_fs/src/lib.rs b/blob_fs/src/lib.rs
new file mode 100644
index 0000000..ce93b78
--- /dev/null
+++ b/blob_fs/src/lib.rs
@@ -0,0 +1,14 @@
+#![cfg_attr(not(test), no_std)]
+#![macro_use]
+
+pub mod bit_vector;
+pub mod blob_device;
+pub mod blob_fs;
+pub mod block_device;
+pub mod errors;
+pub mod memmap_device;
+pub mod structs;
+pub mod utils;
+
+#[cfg(test)]
+pub mod test_device;
diff --git a/blob_fs/src/memmap_device.rs b/blob_fs/src/memmap_device.rs
new file mode 100644
index 0000000..3e570d5
--- /dev/null
+++ b/blob_fs/src/memmap_device.rs
@@ -0,0 +1,276 @@
+use crate::block_device::*;
+use crate::errors::*;
+use crate::utils::*;
+
+pub struct MemmapDevice {
+    pub geom: BlockDeviceGeometry,
+    pub flash_base: *mut u8,
+}
+
+impl MemmapDevice {
+    pub fn new(geom: BlockDeviceGeometry, flash_base: *mut u8) -> Self {
+        let result = MemmapDevice { geom: geom, flash_base: flash_base };
+        return result;
+    }
+}
+
+impl MemmapDevice {
+    pub fn check_is_block_sized(&self, data: &[u8]) -> Result<(), BFSErr> {
+        dcheck!(data.len() == self.geom.block_size, BFSErr::OutOfBounds);
+        return Ok(());
+    }
+
+    pub fn check_block_index(&self, iblock: usize) -> Result<(), BFSErr> {
+        dcheck!(iblock < self.geom.block_count, BFSErr::OutOfBounds);
+        return Ok(());
+    }
+
+    pub fn check_fits_in_block(&self, addr: usize, data: &[u8]) -> Result<(), BFSErr> {
+        let block_a = addr / self.geom.block_size;
+        let block_b = (addr + data.len() - 1) / self.geom.block_size;
+        dcheck!(block_a == block_b, BFSErr::OutOfBounds);
+        return Ok(());
+    }
+
+    pub fn check_read_block(&self, iblock: usize, block: &[u8]) -> Result<(), BFSErr> {
+        self.check_block_index(iblock)?;
+        self.check_fits_in_block(iblock * self.geom.block_size, block)?;
+        return Ok(());
+    }
+
+    pub fn check_write_block(&self, iblock: usize, block: &[u8]) -> Result<(), BFSErr> {
+        self.check_block_index(iblock)?;
+        self.check_fits_in_block(iblock * self.geom.block_size, block)?;
+        return Ok(());
+    }
+
+    pub fn check_erase_block(&self, iblock: usize) -> Result<(), BFSErr> {
+        self.check_block_index(iblock)?;
+        return Ok(());
+    }
+
+    pub fn check_erase_dirty_block(&self, iblock: usize) -> Result<(), BFSErr> {
+        self.check_erase_block(iblock)?;
+        return Ok(());
+    }
+
+    pub fn check_range(&self, addr: usize, size: usize) -> Result<(), BFSErr> {
+        let bs = self.geom.block_size;
+        let bc = self.geom.block_count;
+        dcheck!(addr + size < (bc * bs), BFSErr::OutOfBounds);
+        return Ok(());
+    }
+
+    pub fn check_read_range(&self, addr: usize, data: &mut [u8]) -> Result<(), BFSErr> {
+        self.check_range(addr, data.len())?;
+        return Ok(());
+    }
+
+    pub fn check_write_range(&self, addr: usize, data: &[u8]) -> Result<(), BFSErr> {
+        self.check_range(addr, data.len())?;
+        return Ok(());
+    }
+
+    /// Check that this overwrite only changes bits from 1->0
+    pub fn check_overwrite_range(&self, addr: usize, data: &[u8]) -> Result<(), BFSErr> {
+        self.check_range(addr, data.len())?;
+        for i in 0..data.len() {
+            let src = data[i];
+            unsafe {
+                let dst = self.flash_base.add(addr + i).read();
+                dcheck!((src & !dst) == 0, BFSErr::BadOverwrite);
+            }
+        }
+        return Ok(());
+    }
+}
+
+impl BlockDevice for MemmapDevice {
+    fn geom(&self) -> BlockDeviceGeometry {
+        self.geom
+    }
+
+    /// Read a chunk of flash contained in a single block.
+    fn read_block(&self, iblock: usize, block: &mut [u8]) -> Result<(), BFSErr> {
+        self.check_read_block(iblock, block)?;
+
+        let bs = self.geom.block_size;
+        unsafe {
+            self.flash_base.add(iblock * bs).copy_to(block.as_mut_ptr(), block.len());
+        }
+
+        return Ok(());
+    }
+
+    /// Write a a chunk of flash contained in a single block.
+    fn write_block(&mut self, iblock: usize, block: &[u8]) -> Result<(), BFSErr> {
+        self.check_write_block(iblock, block)?;
+
+        let bs = self.geom.block_size;
+        unsafe {
+            self.flash_base.add(iblock * bs).copy_from(block.as_ptr(), bs);
+        }
+
+        return Ok(());
+    }
+
+    /// Erase an entire block of flash.
+    fn erase_block(&mut self, iblock: usize) -> Result<(), BFSErr> {
+        self.check_erase_block(iblock)?;
+
+        let bs = self.geom.block_size;
+        unsafe {
+            self.flash_base.add(iblock * bs).write_bytes(0xFF, bs);
+        }
+
+        return Ok(());
+    }
+
+    /// Read a range of bytes in flash, checking first that those bytes have
+    /// been written since they were erased.
+    fn read_range(&self, addr: usize, data: &mut [u8]) -> Result<(), BFSErr> {
+        self.check_read_range(addr, data)?;
+
+        unsafe {
+            self.flash_base.add(addr).copy_to(data.as_mut_ptr(), data.len());
+        }
+
+        return Ok(());
+    }
+
+    /// Write to a range of bytes in flash, checking first that those bytes have
+    /// not been written since they were erased.
+    fn write_range(&mut self, addr: usize, data: &[u8]) -> Result<(), BFSErr> {
+        self.check_write_range(addr, data)?;
+
+        unsafe {
+            self.flash_base.add(addr).copy_from(data.as_ptr(), data.len());
+        }
+
+        return Ok(());
+    }
+
+    /// Write to a range of bytes in flash, but _do_ allow writing over dirty
+    /// bytes - the result in flash will be the logical AND of the old and new
+    /// data.
+    fn overwrite_range(&mut self, addr: usize, data: &[u8]) -> Result<(), BFSErr> {
+        self.check_overwrite_range(addr, data)?;
+
+        unsafe {
+            for i in 0..data.len() {
+                let ptr = self.flash_base.add(addr + i);
+                ptr.write(ptr.read() & data[i]);
+            }
+        }
+
+        return Ok(());
+    }
+}
+
+/// Overwriting existing data should write the logical AND of the old and new
+/// data.
+
+#[test]
+fn test_overwrite() {
+    let geom = BlockDeviceGeometry { block_size: 16, block_count: 4 };
+    let mut buf: Vec<u8> = vec![0; geom.block_count * geom.block_size];
+    let bd: &mut dyn BlockDevice = &mut MemmapDevice::new(geom, buf.as_mut_ptr());
+
+    let write1: u32 = 0xFF0FF0FF;
+    let write2: u32 = 0x0F00F000;
+
+    assert_ok!(bd.write_range(0, &write1.to_le_bytes()));
+    assert_ok!(bd.overwrite_range(0, &write2.to_le_bytes()));
+
+    let mut result: u32 = 0;
+    assert_ok!(bd.read_range(0, as_unsafe_blob_mut(&mut result)));
+    assert_eq!(result, 0xFF0FF0FF & 0x0FF0FF00);
+}
+
+/// Trying to set bits that have already been cleared by a previous write should
+/// cause a panic in the test device.
+
+#[test]
+#[should_panic]
+fn test_bad_overwrite() {
+    let geom = BlockDeviceGeometry { block_size: 16, block_count: 4 };
+    let mut buf: Vec<u8> = vec![0; geom.block_count * geom.block_size];
+    let bd: &mut dyn BlockDevice = &mut MemmapDevice::new(geom, buf.as_mut_ptr());
+
+    let all_1: u32 = 0xFFFFFFFF;
+    let all_0: u32 = 0x00000000;
+
+    assert_ok!(bd.write_range(0, &all_0.to_le_bytes()));
+    assert_err!(bd.overwrite_range(0, &all_1.to_le_bytes()));
+}
+
+/// Trying to read blocks that have never been written should cause a panic in
+/// the test device.
+
+#[test]
+#[should_panic]
+fn test_read_unwritten_block() {
+    let geom = BlockDeviceGeometry { block_size: 16, block_count: 4 };
+    let mut buf: Vec<u8> = vec![0; geom.block_count * geom.block_size];
+    let bd: &mut dyn BlockDevice = &mut MemmapDevice::new(geom, buf.as_mut_ptr());
+
+    let mut dst_block = vec![0; geom.block_size];
+    assert_err!(bd.read_block(0, &mut dst_block));
+}
+
+/// Trying to read blocks that have been written and then erased should cause a
+/// panic in the test device.
+
+#[test]
+#[should_panic]
+fn test_read_erased_block() {
+    let geom = BlockDeviceGeometry { block_size: 16, block_count: 4 };
+    let mut buf: Vec<u8> = vec![0; geom.block_count * geom.block_size];
+    let bd: &mut dyn BlockDevice = &mut MemmapDevice::new(geom, buf.as_mut_ptr());
+
+    let mut src_block = vec![0; geom.block_size];
+    let mut dst_block = vec![0; geom.block_size];
+
+    for i in 0..geom.block_size {
+        src_block[i] = i as u8;
+    }
+
+    assert_ok!(bd.write_block(0, &mut src_block));
+    assert_ok!(bd.erase_block(0));
+    assert_err!(bd.read_block(0, &mut dst_block));
+}
+
+/// All blocks in a device should be readable and writable.
+
+#[test]
+fn test_read_write() {
+    let geom = BlockDeviceGeometry { block_size: 16, block_count: 4 };
+    let mut buf: Vec<u8> = vec![0; geom.block_count * geom.block_size];
+    let bd: &mut dyn BlockDevice = &mut MemmapDevice::new(geom, buf.as_mut_ptr());
+
+    let mut src_block = vec![0; geom.block_size];
+    let mut dst_block = vec![0; geom.block_size];
+
+    for i in 0..geom.block_size {
+        src_block[i] = i as u8;
+    }
+
+    // Writing clean blocks should succeed.
+    for i in 0..geom.block_count {
+        assert_ok!(bd.write_block(i, &src_block));
+    }
+
+    // Reading those blocks back should succeed, and the contents should match
+    // the original block.
+    for i in 0..geom.block_count {
+        assert_ok!(bd.read_block(i, &mut dst_block));
+        for j in 0..geom.block_size {
+            assert_eq!(src_block[j], dst_block[j]);
+        }
+    }
+
+    // Erasing blocks should succeed.
+    for i in 0..geom.block_count {
+        assert_ok!(bd.erase_block(i));
+    }
+}
diff --git a/blob_fs/src/structs.rs b/blob_fs/src/structs.rs
new file mode 100644
index 0000000..6bcd1f7
--- /dev/null
+++ b/blob_fs/src/structs.rs
@@ -0,0 +1,86 @@
+#[repr(C, packed)]
+#[derive(Debug, Copy, Clone, Default)]
+pub struct Superblock {
+    pub magic0: u64,
+    pub magic1: u64,
+    pub version: u32,
+    pub flags: u32,
+    pub block_size: u32,
+    pub unused: u32,
+    pub data_block_count: u64,
+    pub journal_block_count: u64,
+    pub inode_count: u64,
+    pub alloc_block_count: u64,
+    pub alloc_inode_count: u64,
+}
+
+#[repr(C, packed)]
+#[derive(Debug, Copy, Clone, Default)]
+pub struct Extent {
+    pub size: u16,
+    pub offset_hi: u16,
+    pub offset_lo: u32,
+}
+
+impl Extent {
+    pub fn offset(&self) -> usize {
+        //let hi = self.offset_hi as usize;
+        //let lo = self.offset_lo as usize;
+        //return (hi << 32) | lo;
+        return self.offset_lo as usize;
+    }
+}
+
+#[repr(C, packed)]
+#[derive(Debug, Copy, Clone, Default)]
+pub struct NodeHeader {
+    pub flags: u16,
+    pub version: u16,
+    pub next_node: u32,
+}
+
+impl NodeHeader {
+    pub const FLAG_INODE: u16 = 0b0000000000000001;
+    pub const FLAG_EXTENT: u16 = 0b0000000000000010;
+}
+
+#[repr(C, packed)]
+#[derive(Debug, Copy, Clone, Default)]
+pub struct Inode {
+    pub header: NodeHeader,
+    pub hash0: u64,
+    pub hash1: u64,
+    pub hash2: u64,
+    pub hash3: u64,
+    pub blob_size: u64,
+    pub block_count: u32,
+    pub extent_count: u16,
+    pub padding: u16,
+    pub inline_extent: Extent,
+}
+
+#[repr(C, packed)]
+#[derive(Debug, Copy, Clone)]
+pub struct ExtentContainer {
+    pub header: NodeHeader,
+    pub previous_node: u32,
+    pub extent_count: u16,
+    pub reserved: u16,
+    pub extents: [Extent; 6],
+}
+
+#[repr(C, packed)]
+#[derive(Debug, Copy, Clone)]
+pub struct Transaction {
+    pub op: u16,
+    pub size: u16,
+    pub addr: u32,
+}
+
+#[test]
+fn size_check() {
+    use core::mem::*;
+    assert_eq!(size_of::<Extent>(), 8);
+    assert_eq!(size_of::<Inode>(), 64);
+    assert_eq!(size_of::<ExtentContainer>(), 64);
+}
diff --git a/blob_fs/src/test_device.rs b/blob_fs/src/test_device.rs
new file mode 100644
index 0000000..69fbf2e
--- /dev/null
+++ b/blob_fs/src/test_device.rs
@@ -0,0 +1,341 @@
+use crate::bit_vector::*;
+use crate::block_device::*;
+use crate::errors::*;
+use crate::utils::*;
+
+pub struct TestDevice<'a> {
+    pub geom: BlockDeviceGeometry,
+    pub flash_base: *mut u8,
+    pub dirty: BitVector<'a>,
+}
+
+impl<'a> TestDevice<'a> {
+    pub fn new(geom: BlockDeviceGeometry, flash_base: *mut u8, dirty_bits: &'a mut [u32]) -> Self {
+        assert_eq!(geom.block_size * geom.block_count, dirty_bits.len() * 32);
+        let result =
+            TestDevice { geom: geom, flash_base: flash_base, dirty: BitVector::new(dirty_bits) };
+        return result;
+    }
+
+    pub fn dirty_count(&self, addr: usize, size: usize) -> Result<usize, BFSErr> {
+        let mut result: usize = 0;
+        for i in addr..addr + size {
+            result += self.dirty.get_bit(i)? as usize;
+        }
+        return Ok(result);
+    }
+
+    pub fn mark_dirty(&mut self, addr: usize, size: usize) -> Result<(), BFSErr> {
+        self.dirty.set_range(addr, addr + size)?;
+        return Ok(());
+    }
+
+    pub fn mark_clean(&mut self, addr: usize, size: usize) -> Result<(), BFSErr> {
+        self.dirty.clear_range(addr, addr + size)?;
+        return Ok(());
+    }
+}
+
+/// Preconditions for TestDevice.
+/// FIXME - I loosened these up because they were annoying, not sure how strict
+/// we should be...
+
+impl<'a> TestDevice<'a> {
+    pub fn check_dirty(&self, addr: usize, size: usize) -> Result<(), BFSErr> {
+        for i in addr..addr + size {
+            dcheck!(self.dirty.get_bit(i)? == 1, BFSErr::CleanRead);
+        }
+        return Ok(());
+    }
+
+    pub fn check_clean(&self, addr: usize, size: usize) -> Result<(), BFSErr> {
+        for i in addr..addr + size {
+            dcheck!(self.dirty.get_bit(i)? == 0, BFSErr::DirtyWrite);
+        }
+        return Ok(());
+    }
+
+    pub fn check_is_block_sized(&self, data: &[u8]) -> Result<(), BFSErr> {
+        dcheck!(data.len() == self.geom.block_size, BFSErr::OutOfBounds);
+        return Ok(());
+    }
+
+    pub fn check_block_index(&self, iblock: usize) -> Result<(), BFSErr> {
+        dcheck!(iblock < self.geom.block_count, BFSErr::OutOfBounds);
+        return Ok(());
+    }
+
+    pub fn check_fits_in_block(&self, addr: usize, data: &[u8]) -> Result<(), BFSErr> {
+        let block_a = addr / self.geom.block_size;
+        let block_b = (addr + data.len() - 1) / self.geom.block_size;
+        dcheck!(block_a == block_b, BFSErr::OutOfBounds);
+        return Ok(());
+    }
+
+    pub fn check_read_block(&self, iblock: usize, block: &[u8]) -> Result<(), BFSErr> {
+        self.check_block_index(iblock)?;
+        self.check_fits_in_block(iblock * self.geom.block_size, block)?;
+        //self.check_dirty(iblock * self.geom.block_size, block.len())?;
+        return Ok(());
+    }
+
+    pub fn check_write_block(&self, iblock: usize, block: &[u8]) -> Result<(), BFSErr> {
+        self.check_block_index(iblock)?;
+        self.check_fits_in_block(iblock * self.geom.block_size, block)?;
+        self.check_clean(iblock * self.geom.block_size, block.len())?;
+        return Ok(());
+    }
+
+    pub fn check_erase_block(&self, iblock: usize) -> Result<(), BFSErr> {
+        self.check_block_index(iblock)?;
+        return Ok(());
+    }
+
+    pub fn check_erase_dirty_block(&self, iblock: usize) -> Result<(), BFSErr> {
+        self.check_erase_block(iblock)?;
+        let dirty_count = self.dirty_count(iblock * self.geom.block_size, self.geom.block_size)?;
+        dcheck!(dirty_count > 0, BFSErr::BadErase);
+        return Ok(());
+    }
+
+    pub fn check_range(&self, addr: usize, size: usize) -> Result<(), BFSErr> {
+        let bs = self.geom.block_size;
+        let bc = self.geom.block_count;
+        dcheck!(addr + size < (bc * bs), BFSErr::OutOfBounds);
+        return Ok(());
+    }
+
+    pub fn check_read_range(&self, addr: usize, data: &mut [u8]) -> Result<(), BFSErr> {
+        self.check_range(addr, data.len())?;
+        //self.check_dirty(addr, data.len())?;
+        return Ok(());
+    }
+
+    pub fn check_write_range(&self, addr: usize, data: &[u8]) -> Result<(), BFSErr> {
+        self.check_range(addr, data.len())?;
+        self.check_clean(addr, data.len())?;
+        return Ok(());
+    }
+
+    /// Check that this overwrite only changes bits from 1->0
+    pub fn check_overwrite_range(&self, addr: usize, data: &[u8]) -> Result<(), BFSErr> {
+        self.check_range(addr, data.len())?;
+        for i in 0..data.len() {
+            let src = data[i];
+            unsafe {
+                let dst = self.flash_base.add(addr + i).read();
+                dcheck!((src & !dst) == 0, BFSErr::BadOverwrite);
+            }
+        }
+        return Ok(());
+    }
+}
+
+impl<'a> BlockDevice for TestDevice<'a> {
+    fn geom(&self) -> BlockDeviceGeometry {
+        self.geom
+    }
+
+    /// Read a chunk of flash contained in a single block.
+    fn read_block(&self, iblock: usize, block: &mut [u8]) -> Result<(), BFSErr> {
+        self.check_read_block(iblock, block)?;
+
+        let bs = self.geom.block_size;
+        unsafe {
+            self.flash_base.add(iblock * bs).copy_to(block.as_mut_ptr(), block.len());
+        }
+
+        return Ok(());
+    }
+
+    /// Write a a chunk of flash contained in a single block.
+    fn write_block(&mut self, iblock: usize, block: &[u8]) -> Result<(), BFSErr> {
+        self.check_write_block(iblock, block)?;
+
+        let bs = self.geom.block_size;
+        unsafe {
+            self.flash_base.add(iblock * bs).copy_from(block.as_ptr(), bs);
+        }
+
+        self.mark_dirty(iblock * bs, bs)?;
+        return Ok(());
+    }
+
+    /// Erase an entire block of flash.
+    fn erase_block(&mut self, iblock: usize) -> Result<(), BFSErr> {
+        self.check_erase_block(iblock)?;
+
+        let bs = self.geom.block_size;
+        self.mark_clean(iblock * bs, bs)?;
+        unsafe {
+            self.flash_base.add(iblock * bs).write_bytes(0xFF, bs);
+        }
+
+        return Ok(());
+    }
+
+    /// Read a range of bytes in flash, checking first that those bytes have
+    /// been written since they were erased.
+    fn read_range(&self, addr: usize, data: &mut [u8]) -> Result<(), BFSErr> {
+        self.check_read_range(addr, data)?;
+
+        unsafe {
+            self.flash_base.add(addr).copy_to(data.as_mut_ptr(), data.len());
+        }
+
+        return Ok(());
+    }
+
+    /// Write to a range of bytes in flash, checking first that those bytes have
+    /// not been written since they were erased.
+    fn write_range(&mut self, addr: usize, data: &[u8]) -> Result<(), BFSErr> {
+        self.check_write_range(addr, data)?;
+
+        unsafe {
+            self.flash_base.add(addr).copy_from(data.as_ptr(), data.len());
+        }
+
+        self.mark_dirty(addr, data.len())?;
+        return Ok(());
+    }
+
+    /// Write to a range of bytes in flash, but _do_ allow writing over dirty
+    /// bytes - the result in flash will be the logical AND of the old and new
+    /// data.
+    fn overwrite_range(&mut self, addr: usize, data: &[u8]) -> Result<(), BFSErr> {
+        self.check_overwrite_range(addr, data)?;
+
+        unsafe {
+            for i in 0..data.len() {
+                let ptr = self.flash_base.add(addr + i);
+                ptr.write(ptr.read() & data[i]);
+            }
+        }
+
+        self.mark_dirty(addr, data.len())?;
+        return Ok(());
+    }
+}
+
+/// Overwriting existing data should write the logical AND of the old and new
+/// data.
+
+#[test]
+fn test_overwrite() {
+    const BLOCK_SIZE: usize = 16;
+    const BLOCK_COUNT: usize = 4;
+    let geom = BlockDeviceGeometry { block_size: BLOCK_SIZE, block_count: BLOCK_COUNT };
+    let mut buf: [u8; BLOCK_SIZE * BLOCK_COUNT] = [0; BLOCK_SIZE * BLOCK_COUNT];
+    let mut dirty_bits = [0; BLOCK_SIZE * BLOCK_COUNT / 32];
+    let bd: &mut dyn BlockDevice = &mut TestDevice::new(geom, buf.as_mut_ptr(), &mut dirty_bits);
+
+    let write1: u32 = 0xFF0FF0FF;
+    let write2: u32 = 0x0F00F000;
+
+    assert_ok!(bd.write_range(0, &write1.to_le_bytes()));
+    assert_ok!(bd.overwrite_range(0, &write2.to_le_bytes()));
+
+    let mut result: u32 = 0;
+    assert_ok!(bd.read_range(0, as_unsafe_blob_mut(&mut result)));
+    assert_eq!(result, 0xFF0FF0FF & 0x0FF0FF00);
+}
+
+/// Trying to set bits that have already been cleared by a previous write should
+/// cause a panic in the test device.
+
+#[test]
+#[should_panic]
+fn test_bad_overwrite() {
+    const BLOCK_SIZE: usize = 16;
+    const BLOCK_COUNT: usize = 4;
+    let geom = BlockDeviceGeometry { block_size: BLOCK_SIZE, block_count: BLOCK_COUNT };
+    let mut buf: [u8; BLOCK_SIZE * BLOCK_COUNT] = [0; BLOCK_SIZE * BLOCK_COUNT];
+    let mut dirty_bits = [0; BLOCK_SIZE * BLOCK_COUNT / 32];
+    let bd: &mut dyn BlockDevice = &mut TestDevice::new(geom, buf.as_mut_ptr(), &mut dirty_bits);
+
+    let all_1: u32 = 0xFFFFFFFF;
+    let all_0: u32 = 0x00000000;
+
+    assert_ok!(bd.write_range(0, &all_0.to_le_bytes()));
+    assert_err!(bd.overwrite_range(0, &all_1.to_le_bytes()));
+}
+
+/// Trying to read blocks that have never been written should cause a panic in
+/// the test device.
+
+#[test]
+#[should_panic]
+fn test_read_unwritten_block() {
+    const BLOCK_SIZE: usize = 16;
+    const BLOCK_COUNT: usize = 4;
+    let geom = BlockDeviceGeometry { block_size: BLOCK_SIZE, block_count: BLOCK_COUNT };
+    let mut buf: [u8; BLOCK_SIZE * BLOCK_COUNT] = [0; BLOCK_SIZE * BLOCK_COUNT];
+    let mut dirty_bits = [0; BLOCK_SIZE * BLOCK_COUNT / 32];
+    let bd: &mut dyn BlockDevice = &mut TestDevice::new(geom, buf.as_mut_ptr(), &mut dirty_bits);
+
+    let mut dst_block = [0; BLOCK_SIZE];
+    assert_err!(bd.read_block(0, &mut dst_block));
+}
+
+/// Trying to read blocks that have been written and then erased should cause a
+/// panic in the test device.
+
+#[test]
+#[should_panic]
+fn test_read_erased_block() {
+    const BLOCK_SIZE: usize = 16;
+    const BLOCK_COUNT: usize = 4;
+    let geom = BlockDeviceGeometry { block_size: BLOCK_SIZE, block_count: BLOCK_COUNT };
+    let mut buf: [u8; BLOCK_SIZE * BLOCK_COUNT] = [0; BLOCK_SIZE * BLOCK_COUNT];
+    let mut dirty_bits = [0; BLOCK_SIZE * BLOCK_COUNT / 32];
+    let bd: &mut dyn BlockDevice = &mut TestDevice::new(geom, buf.as_mut_ptr(), &mut dirty_bits);
+
+    let mut src_block = [0; BLOCK_SIZE];
+    let mut dst_block = [0; BLOCK_SIZE];
+
+    for i in 0..geom.block_size {
+        src_block[i] = i as u8;
+    }
+
+    assert_ok!(bd.write_block(0, &mut src_block));
+    assert_ok!(bd.erase_block(0));
+    assert_err!(bd.read_block(0, &mut dst_block));
+}
+
+/// All blocks in a device should be readable and writable.
+
+#[test]
+fn test_read_write() {
+    const BLOCK_SIZE: usize = 16;
+    const BLOCK_COUNT: usize = 4;
+    let geom = BlockDeviceGeometry { block_size: BLOCK_SIZE, block_count: BLOCK_COUNT };
+    let mut buf: [u8; BLOCK_SIZE * BLOCK_COUNT] = [0; BLOCK_SIZE * BLOCK_COUNT];
+    let mut dirty_bits = [0; BLOCK_SIZE * BLOCK_COUNT / 32];
+    let bd: &mut dyn BlockDevice = &mut TestDevice::new(geom, buf.as_mut_ptr(), &mut dirty_bits);
+
+    let mut src_block = [0; BLOCK_SIZE];
+    let mut dst_block = [0; BLOCK_SIZE];
+
+    for i in 0..geom.block_size {
+        src_block[i] = i as u8;
+    }
+
+    // Writing clean blocks should succeed.
+    for i in 0..geom.block_count {
+        assert_ok!(bd.write_block(i, &src_block));
+    }
+
+    // Reading those blocks back should succeed, and the contents should match
+    // the original block.
+    for i in 0..geom.block_count {
+        assert_ok!(bd.read_block(i, &mut dst_block));
+        for j in 0..geom.block_size {
+            assert_eq!(src_block[j], dst_block[j]);
+        }
+    }
+
+    // Erasing blocks should succeed.
+    for i in 0..geom.block_count {
+        assert_ok!(bd.erase_block(i));
+    }
+}
diff --git a/blob_fs/src/utils.rs b/blob_fs/src/utils.rs
new file mode 100644
index 0000000..e0c9814
--- /dev/null
+++ b/blob_fs/src/utils.rs
@@ -0,0 +1,88 @@
+use core::mem;
+use core::slice;
+
+/// Trigger an assertion if "cond" fails in debug builds, return an error in
+/// release builds.
+macro_rules! dcheck {
+    ($cond:expr, $err:expr) => {
+        let cond = $cond;
+        debug_assert!(cond);
+        if !(cond) {
+            return Err($err);
+        }
+    };
+}
+pub(crate) use dcheck;
+
+/// Assert that a Result is_ok(). Why is this not in the standard library?
+#[cfg(test)]
+macro_rules! assert_ok {
+    ($cond:expr) => {
+        let result = $cond;
+        assert!(result.is_ok());
+    };
+}
+#[cfg(test)]
+pub(crate) use assert_ok;
+
+/// Assert that a Result is_err(). Why is this not in the standard library?
+#[cfg(test)]
+macro_rules! assert_err {
+    ($cond:expr) => {
+        let result = $cond;
+        assert!(result.is_err());
+    };
+}
+#[cfg(test)]
+pub(crate) use assert_err;
+
+// FIXME these are quick and dirty hacks to make serialization of POD types
+// easier
+
+pub fn as_unsafe_blob<T: Sized>(p: &T) -> &[u8] {
+    unsafe {
+        let tp: *const T = p as *const T;
+        let pp: *const u8 = tp as *const u8;
+        slice::from_raw_parts(pp, mem::size_of::<T>())
+    }
+}
+
+pub fn as_unsafe_blob_mut<T: Sized>(p: &mut T) -> &mut [u8] {
+    unsafe {
+        let tp: *mut T = p as *mut T;
+        let pp: *mut u8 = tp as *mut u8;
+        slice::from_raw_parts_mut(pp, mem::size_of::<T>())
+    }
+}
+
+pub fn from_unsafe_blob<T: Sized>(p: &[u8]) -> &T {
+    unsafe {
+        let pp: *const u8 = p.as_ptr();
+        let tp: *const T = pp as *const T;
+        return &*tp;
+    }
+}
+
+pub fn from_unsafe_blob_mut<T: Sized>(p: &mut [u8]) -> &mut T {
+    unsafe {
+        let pp: *mut u8 = p.as_mut_ptr();
+        let tp: *mut T = pp as *mut T;
+        return &mut *tp;
+    }
+}
+
+pub fn slice_as_unsafe_blob_mut<T: Sized>(p: &mut [T]) -> &mut [u8] {
+    unsafe {
+        let tp: *mut T = p.as_mut_ptr();
+        let pp: *mut u8 = tp as *mut u8;
+        slice::from_raw_parts_mut(pp, mem::size_of::<T>())
+    }
+}
+
+pub fn slice_as_unsafe_blob<T: Sized>(p: &[T]) -> &[u8] {
+    unsafe {
+        let tp: *const T = p.as_ptr();
+        let pp: *const u8 = tp as *const u8;
+        slice::from_raw_parts(pp, mem::size_of::<T>())
+    }
+}
diff --git a/board/Cargo.toml b/board/Cargo.toml
index 414bb3c..2d4ec52 100644
--- a/board/Cargo.toml
+++ b/board/Cargo.toml
@@ -10,9 +10,11 @@
 rv32i      = { path = "../../tock/arch/rv32i" }
 capsules   = { path = "../../tock/capsules" }
 kernel     = { path = "../../tock/kernel" }
-matcha     = { path = "../chip" }
 lowrisc    = { path = "../../tock/chips/lowrisc" }
-blob_fs    = { path = "../../tock/libraries/blob_fs" }
+
+matcha          = { path = "../chip" }
+blob_fs         = { path = "../blob_fs" }
+matcha-capsules = { path = "../capsules" }
 
 [features]
 # OpenTitan Matcha SoC design can be synthesized or compiled for different targets. A
@@ -22,10 +24,6 @@
 #
 # OpenTitan Matcha CPU and possibly other components must be configured appropriately
 # for a specific target:
-#    - fpga_nexysvideo:
-#      OpenTitan Matcha SoC design running on Nexys Video Artix-7 FPGA.
-#
 #    - sim_verilator:
 #      OpenTitan Matcha SoC design simulated in Verilator.
-fpga_nexysvideo = ["matcha/config_fpga_nexysvideo"]
 sim_verilator = ["matcha/config_sim_verilator"]
diff --git a/board/src/main.rs b/board/src/main.rs
index 7a82465..c1dcffb 100644
--- a/board/src/main.rs
+++ b/board/src/main.rs
@@ -8,7 +8,7 @@
 #![cfg_attr(not(doc), no_main)]
 #![feature(const_in_array_repeat_expressions)]
 
-use capsules::debug_uart::DebugUart;
+use matcha_capsules::debug_uart::DebugUart;
 use capsules::virtual_alarm::{MuxAlarm, VirtualMuxAlarm};
 use capsules::virtual_hmac::VirtualMuxHmac;
 use kernel::capabilities;
@@ -83,7 +83,7 @@
             capsules::alarm::DRIVER_NUM => f(Some(self.alarm)),
             capsules::low_level_debug::DRIVER_NUM => f(Some(self.lldb)),
             capsules::i2c_master::DRIVER_NUM => f(Some(self.i2c_master)),
-            capsules::debug_uart::DRIVER_NUM => f(Some(self.debug_uart)),
+            matcha_capsules::debug_uart::DRIVER_NUM => f(Some(self.debug_uart)),
             capsules::storage_manager::DRIVER_NUM => f(Some(self.storage_manager)),
             _ => f(None),
         }
diff --git a/capsules/Cargo.toml b/capsules/Cargo.toml
new file mode 100644
index 0000000..39159ad
--- /dev/null
+++ b/capsules/Cargo.toml
@@ -0,0 +1,10 @@
+[package]
+name = "matcha-capsules"
+version = "0.1.0"
+edition = "2018"
+
+[dependencies]
+kernel = { path = "../../tock/kernel" }
+enum_primitive = { path = "../../tock/libraries/enum_primitive" }
+blob_fs = { path = "../blob_fs" }
+
diff --git a/capsules/rust-toolchain b/capsules/rust-toolchain
new file mode 100644
index 0000000..b18a3f3
--- /dev/null
+++ b/capsules/rust-toolchain
@@ -0,0 +1 @@
+nightly-2020-06-03
diff --git a/capsules/src/debug_uart.rs b/capsules/src/debug_uart.rs
new file mode 100644
index 0000000..a2b9035
--- /dev/null
+++ b/capsules/src/debug_uart.rs
@@ -0,0 +1,74 @@
+//! Trivial capsule to provide minimal debug printing support for userspace.
+//!
+//! The default TockOS Console class does heavier-weight stuff with UART
+//! multiplexing and asynchronous sending and such, which is usually _not_ what
+//! is wanted in a debug serial port. This capsule implements only one command,
+//! which dumps data from an allow'ed buffer directly to a memory-mapped UART
+//! peripheral.
+//!
+//! Instantiation:
+//!   let debug_uart = static_init!(
+//!     DebugUart,
+//!     DebugUart {
+//!         tx_busy: StaticRef::new(TX_BUSY_ADDR as *const ReadOnly<u32>),
+//!         tx_port: StaticRef::new(TX_PORT_ADDR as *const WriteOnly<u32>),
+//!         app_data_grant: board_kernel.create_grant(&memory_allocation_cap)
+//!     }
+//!   );
+//!
+//! where TX_BUSY_ADDR is a register whose low bit is 1 if the UART's fifo is
+//! full and TX_PORT_ADDR is the register we write bytes to.
+//!
+//! Usage - send buffer directly to UART:
+//!   let driver_num = capsules::debug_uart::DRIVER_NUM;
+//!   let allow = syscalls::allow(driver_num, 0, &mut buffer);
+//!   let result = syscalls::command(driver_num, 0, buffer.len(), 0);
+//!   drop(allow);
+
+//use crate::driver;
+use kernel::common::registers::{ReadOnly, WriteOnly};
+use kernel::common::StaticRef;
+use kernel::{AppId, AppSlice, Callback, Driver, Grant, ReturnCode, Shared};
+
+//pub const DRIVER_NUM: usize = driver::NUM::DebugUart as usize;
+pub const DRIVER_NUM: usize = 0x00009 as usize;
+
+#[derive(Default)]
+pub struct AppData {
+    pub buffer: Option<AppSlice<Shared, u8>>,
+}
+
+pub struct DebugUart {
+    pub tx_busy: StaticRef<ReadOnly<u32>>,
+    pub tx_port: StaticRef<WriteOnly<u32>>,
+    pub app_data_grant: Grant<AppData>,
+}
+
+impl Driver for DebugUart {
+    fn subscribe(&self, _: usize, _: Option<Callback>, _: AppId) -> ReturnCode {
+        ReturnCode::EINVAL
+    }
+
+    fn command(&self, minor_num: usize, r2: usize, _: usize, app_id: AppId) -> ReturnCode {
+        if minor_num != 0 {
+            return ReturnCode::EINVAL;
+        }
+
+        let _ = self.app_data_grant.enter(app_id, |app_data, _| {
+            if let Some(buf) = &app_data.buffer {
+                for i in 0..r2 {
+                    while (self.tx_busy.get() & 1) != 0 {}
+                    self.tx_port.set(buf.as_ref()[i] as u32);
+                }
+            }
+        });
+        return ReturnCode::SUCCESS;
+    }
+
+    fn allow(&self, app_id: AppId, _: usize, slice: Option<AppSlice<Shared, u8>>) -> ReturnCode {
+        let _ = self.app_data_grant.enter(app_id, |app_data, _| {
+            app_data.buffer = slice;
+        });
+        return ReturnCode::SUCCESS;
+    }
+}
diff --git a/capsules/src/lib.rs b/capsules/src/lib.rs
new file mode 100644
index 0000000..78117c5
--- /dev/null
+++ b/capsules/src/lib.rs
@@ -0,0 +1,5 @@
+#![feature(const_fn)]
+#![forbid(unsafe_code)]
+#![no_std]
+
+pub mod debug_uart;
diff --git a/chip/src/chip_config.rs b/chip/src/chip_config.rs
index 9a26ff9..c4ec321 100644
--- a/chip/src/chip_config.rs
+++ b/chip/src/chip_config.rs
@@ -24,18 +24,6 @@
     pub uart_baudrate: u32,
 }
 
-/// Config for running Matcha on an FPGA. Also the default configuration.
-#[cfg(any(
-    feature = "config_fpga_nexysvideo",
-    not(feature = "config_disable_default")
-))]
-pub const CONFIG: Config = Config {
-    name: "fpga_nexysvideo",
-    cpu_freq: 10_000_000,
-    peripheral_freq: 2_500_000,
-    uart_baudrate: 115200,
-};
-
 /// Config for running Matcha in a verilog simulator.
 #[cfg(feature = "config_sim_verilator")]
 pub const CONFIG: Config = Config {