diff --git a/Cargo.lock b/Cargo.lock index 3c11aa8c63..8f8205fa90 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -318,6 +318,12 @@ version = "0.4.27" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "13dc2df351e3202783a1fe0d44375f7295ffb4049267b0f3018346dc122a1d94" +[[package]] +name = "lz4_flex" +version = "0.11.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "08ab2867e3eeeca90e844d1940eab391c9dc5228783db2ed999acbc0a9ed375a" + [[package]] name = "memchr" version = "2.7.5" @@ -454,6 +460,7 @@ dependencies = [ "libc", "libredox", "log", + "lz4_flex", "range-tree", "redox-path", "redox-scheme", diff --git a/Cargo.toml b/Cargo.toml index 91415b1ee5..f13e0210ca 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -40,6 +40,7 @@ getrandom = { version = "0.2.5", optional = true } libc = "0.2" libredox = { version = "0.1.6", features = ["call"], optional = true } log = { version = "0.4.14", default-features = false, optional = true } +lz4_flex = { version = "0.11", default-features = false, features = ["checked-decode"] } range-tree = { version = "0.1", optional = true } redox-path = "0.3.0" redox-scheme = { version = "0.7.0", optional = true } diff --git a/src/allocator.rs b/src/allocator.rs index 286695c2bf..ff35b370c5 100644 --- a/src/allocator.rs +++ b/src/allocator.rs @@ -2,7 +2,7 @@ use alloc::vec::Vec; use core::{fmt, mem, ops, slice}; use endian_num::Le; -use crate::{BlockAddr, BlockLevel, BlockPtr, BlockTrait, BLOCK_SIZE}; +use crate::{BlockAddr, BlockLevel, BlockMeta, BlockPtr, BlockTrait, BLOCK_SIZE}; pub const ALLOC_LIST_ENTRIES: usize = (BLOCK_SIZE as usize - mem::size_of::>()) / mem::size_of::(); @@ -52,10 +52,10 @@ impl Allocator { /// Find a free block of the given level, mark it as "used", and return its address. /// Returns [`None`] if there are no free blocks with this level. - pub fn allocate(&mut self, block_level: BlockLevel) -> Option { + pub fn allocate(&mut self, meta: BlockMeta) -> Option { // First, find the lowest level with a free block let mut index_opt = None; - let mut level = block_level.0; + let mut level = meta.level.0; // Start searching at the level we want. Smaller levels are too small! while level < self.levels.len() { if !self.levels[level].is_empty() { @@ -68,13 +68,13 @@ impl Allocator { // If a free block was found, split it until we find a usable block of the right level. // The left side of the split block is kept free, and the right side is allocated. let index = index_opt?; - while level > block_level.0 { + while level > meta.level.0 { level -= 1; let level_size = 1 << level; self.levels[level].push(index + level_size); } - Some(unsafe { BlockAddr::new(index, block_level) }) + Some(unsafe { BlockAddr::new(index, meta) }) } /// Try to allocate the exact block specified, making all necessary splits. @@ -112,7 +112,7 @@ impl Allocator { } } - Some(unsafe { BlockAddr::new(index_opt?, exact_addr.level()) }) + Some(unsafe { BlockAddr::new(index_opt?, exact_addr.meta()) }) } /// Deallocate the given block, marking it "free" so that it can be re-used later. diff --git a/src/block.rs b/src/block.rs index 2df0e7773b..3b6c95b9fb 100644 --- a/src/block.rs +++ b/src/block.rs @@ -9,38 +9,67 @@ const BLOCK_LIST_ENTRIES: usize = BLOCK_SIZE as usize / mem::size_of:: Self { - // Level must only use the lowest four bits - if level.0 > 0xF { - panic!("block level used more than four bits"); + pub(crate) unsafe fn new(index: u64, meta: BlockMeta) -> Self { + // Level must fit within LEVEL_MASK + if meta.level.0 > Self::LEVEL_MASK as usize { + panic!("block level too large"); } - // Index must not use the highest four bits + // Decomp level must fit within DECOMP_LEVEL_MASK + let decomp_level = meta.decomp_level.unwrap_or_default(); + if (decomp_level.0 << Self::DECOMP_LEVEL_SHIFT) > Self::DECOMP_LEVEL_MASK as usize { + panic!("decompressed block level too large"); + } + + // Index must not use the metadata bits let inner = index - .checked_shl(4) - .expect("block index used highest four bits") - | (level.0 as u64); + .checked_shl(Self::INDEX_SHIFT as u32) + .expect("block index too large") + | ((decomp_level.0 as u64) << Self::DECOMP_LEVEL_SHIFT) + | (meta.level.0 as u64); Self(inner) } - pub fn null(level: BlockLevel) -> Self { - unsafe { Self::new(0, level) } + pub fn null(meta: BlockMeta) -> Self { + unsafe { Self::new(0, meta) } } pub fn index(&self) -> u64 { // The first four bits store the level - self.0 >> 4 + self.0 >> Self::INDEX_SHIFT } pub fn level(&self) -> BlockLevel { // The first four bits store the level - BlockLevel((self.0 & 0xF) as usize) + BlockLevel((self.0 & Self::LEVEL_MASK) as usize) + } + + pub fn decomp_level(&self) -> Option { + let value = (self.0 & Self::DECOMP_LEVEL_MASK) >> Self::DECOMP_LEVEL_SHIFT; + if value != 0 { + Some(BlockLevel(value as usize)) + } else { + None + } + } + + pub fn meta(&self) -> BlockMeta { + BlockMeta { + level: self.level(), + decomp_level: self.decomp_level(), + } } pub fn is_null(&self) -> bool { @@ -48,6 +77,28 @@ impl BlockAddr { } } +#[derive(Clone, Copy, Debug, Default, Eq, Hash, PartialEq)] +pub struct BlockMeta { + pub(crate) level: BlockLevel, + pub(crate) decomp_level: Option, +} + +impl BlockMeta { + pub fn new(level: BlockLevel) -> Self { + Self { + level, + decomp_level: None, + } + } + + pub fn new_compressed(level: BlockLevel, decomp_level: BlockLevel) -> Self { + Self { + level, + decomp_level: Some(decomp_level), + } + } +} + /// The size of a block. /// /// Level 0 blocks are blocks of [`BLOCK_SIZE`] bytes. @@ -211,9 +262,9 @@ pub struct BlockPtr { } impl BlockPtr { - pub fn null(level: BlockLevel) -> Self { + pub fn null(meta: BlockMeta) -> Self { Self { - addr: BlockAddr::null(level).0.into(), + addr: BlockAddr::null(meta).0.into(), hash: 0.into(), phantom: PhantomData, } diff --git a/src/filesystem.rs b/src/filesystem.rs index 15b86665e3..e13b67bb19 100644 --- a/src/filesystem.rs +++ b/src/filesystem.rs @@ -5,7 +5,9 @@ use xts_mode::{get_tweak_default, Xts128}; #[cfg(feature = "std")] use crate::{AllocEntry, AllocList, BlockData, BlockTrait, Key, KeySlot, Node, Salt, TreeList}; -use crate::{Allocator, BlockAddr, BlockLevel, Disk, Header, Transaction, BLOCK_SIZE, HEADER_RING}; +use crate::{ + Allocator, BlockAddr, BlockLevel, BlockMeta, Disk, Header, Transaction, BLOCK_SIZE, HEADER_RING, +}; /// A file system pub struct FileSystem { @@ -174,12 +176,12 @@ impl FileSystem { // Set tree and alloc pointers and write header generation one fs.tx(|tx| unsafe { let tree = BlockData::new( - BlockAddr::new(HEADER_RING + 1, BlockLevel::default()), + BlockAddr::new(HEADER_RING + 1, BlockMeta::default()), TreeList::empty(BlockLevel::default()).unwrap(), ); let mut alloc = BlockData::new( - BlockAddr::new(HEADER_RING + 2, BlockLevel::default()), + BlockAddr::new(HEADER_RING + 2, BlockMeta::default()), AllocList::empty(BlockLevel::default()).unwrap(), ); @@ -199,7 +201,7 @@ impl FileSystem { fs.tx(|tx| unsafe { let mut root = BlockData::new( - BlockAddr::new(HEADER_RING + 3, BlockLevel::default()), + BlockAddr::new(HEADER_RING + 3, BlockMeta::default()), Node::new(Node::MODE_DIR | 0o755, 0, 0, ctime, ctime_nsec), ); root.data_mut().set_links(1); @@ -254,12 +256,12 @@ impl FileSystem { if count < 0 { for i in 0..-count { //TODO: replace assert with error? - let addr = BlockAddr::new(index + i as u64, BlockLevel::default()); + let addr = BlockAddr::new(index + i as u64, BlockMeta::default()); assert_eq!(self.allocator.allocate_exact(addr), Some(addr)); } } else { for i in 0..count { - let addr = BlockAddr::new(index + i as u64, BlockLevel::default()); + let addr = BlockAddr::new(index + i as u64, BlockMeta::default()); self.allocator.deallocate(addr); } } diff --git a/src/lib.rs b/src/lib.rs index 25d056a069..6f9de0d16e 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -16,7 +16,7 @@ pub const BLOCK_SIZE: u64 = 4096; pub const RECORD_LEVEL: usize = 5; pub const RECORD_SIZE: u64 = BLOCK_SIZE << RECORD_LEVEL; pub const SIGNATURE: &[u8; 8] = b"RedoxFS\0"; -pub const VERSION: u64 = 7; +pub const VERSION: u64 = 8; pub const DIR_ENTRY_MAX_LENGTH: usize = 252; pub static IS_UMT: AtomicUsize = AtomicUsize::new(0); @@ -25,7 +25,7 @@ pub use self::allocator::{AllocEntry, AllocList, Allocator, ALLOC_LIST_ENTRIES}; #[cfg(feature = "std")] pub use self::archive::{archive, archive_at}; pub use self::block::{ - BlockAddr, BlockData, BlockLevel, BlockList, BlockPtr, BlockRaw, BlockTrait, + BlockAddr, BlockData, BlockLevel, BlockList, BlockMeta, BlockPtr, BlockRaw, BlockTrait, }; pub use self::dir::{DirEntry, DirList}; pub use self::disk::*; diff --git a/src/record.rs b/src/record.rs index e7815a42c0..7586cd9aed 100644 --- a/src/record.rs +++ b/src/record.rs @@ -5,7 +5,7 @@ use crate::{BlockLevel, BlockTrait, RECORD_LEVEL}; //TODO: this is a box to prevent stack overflows #[derive(Clone)] -pub struct RecordRaw(Box<[u8]>); +pub struct RecordRaw(pub(crate) Box<[u8]>); unsafe impl BlockTrait for RecordRaw { fn empty(level: BlockLevel) -> Option { diff --git a/src/transaction.rs b/src/transaction.rs index d51c1eae8c..8d60c13150 100644 --- a/src/transaction.rs +++ b/src/transaction.rs @@ -14,9 +14,9 @@ use syscall::error::{ use crate::{ htree::{self, HTreeHash, HTreeNode, HTreePtr}, - AllocEntry, AllocList, Allocator, BlockAddr, BlockData, BlockLevel, BlockPtr, BlockTrait, - DirEntry, DirList, Disk, FileSystem, Header, Node, NodeLevel, RecordRaw, TreeData, TreePtr, - ALLOC_GC_THRESHOLD, ALLOC_LIST_ENTRIES, DIR_ENTRY_MAX_LENGTH, HEADER_RING, + AllocEntry, AllocList, Allocator, BlockAddr, BlockData, BlockLevel, BlockMeta, BlockPtr, + BlockTrait, DirEntry, DirList, Disk, FileSystem, Header, Node, NodeLevel, RecordRaw, TreeData, + TreePtr, ALLOC_GC_THRESHOLD, ALLOC_LIST_ENTRIES, DIR_ENTRY_MAX_LENGTH, HEADER_RING, }; pub struct Transaction<'a, D: Disk> { @@ -57,11 +57,11 @@ impl<'a, D: Disk> Transaction<'a, D> { // MARK: block operations // - /// Allocate a new block of size `level`, returning its address. + /// Allocate a new block of size defined by `meta`, returning its address. /// - returns `Err(ENOSPC)` if a block of this size could not be alloated. /// - unsafe because order must be done carefully and changes must be flushed to disk - pub(crate) unsafe fn allocate(&mut self, level: BlockLevel) -> Result { - match self.allocator.allocate(level) { + pub(crate) unsafe fn allocate(&mut self, meta: BlockMeta) -> Result { + match self.allocator.allocate(meta) { Some(addr) => { self.allocator_log.push_back(AllocEntry::allocate(addr)); Ok(addr) @@ -180,7 +180,7 @@ impl<'a, D: Disk> Transaction<'a, D> { while new_blocks.len() * ALLOC_LIST_ENTRIES <= self.allocator_log.len() + self.deallocate.len() { - new_blocks.push(unsafe { self.allocate(BlockLevel::default())? }); + new_blocks.push(unsafe { self.allocate(BlockMeta::default())? }); } // De-allocate old blocks (after allocation to prevent re-use) @@ -321,8 +321,9 @@ impl<'a, D: Disk> Transaction<'a, D> { ptr: BlockPtr, ) -> Result> { if ptr.is_null() { - match T::empty(ptr.addr().level()) { - Some(empty) => Ok(BlockData::new(BlockAddr::default(), empty)), + let addr = ptr.addr(); + match T::empty(addr.level()) { + Some(empty) => Ok(BlockData::new(addr, empty)), None => { #[cfg(feature = "log")] log::error!("READ_BLOCK_OR_EMPTY: INVALID BLOCK LEVEL FOR TYPE"); @@ -336,15 +337,44 @@ impl<'a, D: Disk> Transaction<'a, D> { unsafe fn read_record>( &mut self, - ptr: BlockPtr, + mut ptr: BlockPtr, level: BlockLevel, ) -> Result> { - let record = unsafe { self.read_block_or_empty(ptr)? }; + // Set null pointers to correct size (reduces number of copies below) + if ptr.is_null() { + ptr = BlockPtr::::null(BlockMeta::new(level)); + } + + // Read record from disk, or construct empty one for null pointers + let mut record = unsafe { self.read_block_or_empty(ptr)? }; if record.addr().level() >= level { // Return record if it is larger than or equal to requested level return Ok(record); } + // Attempt to decompress if address metadata indicates compression + if let Some(decomp_level) = record.addr().decomp_level() { + // First 2 bytes store compressed data length + // This means only compressed record sizes up to 64 KiB are supported + let mut decomp = match T::empty(decomp_level) { + Some(empty) => empty, + None => { + #[cfg(feature = "log")] + log::error!("READ_RECORD: INVALID DECOMPRESSED BLOCK LEVEL FOR TYPE"); + return Err(Error::new(ENOENT)); + } + }; + let comp_len = record.data()[0] as usize | ((record.data()[1] as usize) << 8); + if let Err(err) = + lz4_flex::decompress_into(&record.data()[2..(comp_len + 2)], &mut decomp) + { + #[cfg(feature = "log")] + log::error!("READ_RECORD: FAILED TO DECOMPRESS: {:?}", err); + return Err(Error::new(EIO)); + } + record = BlockData::new(BlockAddr::null(BlockMeta::new(decomp_level)), decomp); + } + // If a larger level was requested, // create a fake record with the requested level // and fill it with the data in the original record. @@ -359,7 +389,8 @@ impl<'a, D: Disk> Transaction<'a, D> { }; let len = min(raw.len(), old_raw.len()); raw[..len].copy_from_slice(&old_raw[..len]); - Ok(BlockData::new(BlockAddr::null(level), raw)) + + Ok(BlockData::new(BlockAddr::null(BlockMeta::new(level)), raw)) } /// Write block data to a new address, returning new address @@ -368,8 +399,8 @@ impl<'a, D: Disk> Transaction<'a, D> { mut block: BlockData, ) -> Result> { // Swap block to new address - let level = block.addr().level(); - let old_addr = block.swap_addr(unsafe { self.allocate(level)? }); + let meta = block.addr().meta(); + let old_addr = block.swap_addr(unsafe { self.allocate(meta)? }); // Deallocate old address (will only take effect after sync_allocator, which helps to // prevent re-use before a new header is written if !old_addr.is_null() { @@ -773,7 +804,7 @@ impl<'a, D: Disk> Transaction<'a, D> { unsafe { let parent = self.read_tree(parent_ptr)?; let node_block_data = BlockData::new( - self.allocate(BlockLevel::default())?, + self.allocate(BlockMeta::default())?, Node::new( mode, parent.data().uid(), @@ -1594,7 +1625,8 @@ impl<'a, D: Disk> Transaction<'a, D> { let mut node_changed = false; let record_level = node.data().record_level(); - let node_records = node.data().size().div_ceil(record_level.bytes()); + let node_size = node.data().size(); + let node_records = node_size.div_ceil(record_level.bytes()); let mut i = 0; while i < buf.len() { @@ -1605,30 +1637,64 @@ impl<'a, D: Disk> Transaction<'a, D> { let mut record_ptr = if node_records > (*offset / record_level.bytes()) { self.node_record_ptr(node, *offset / record_level.bytes())? } else { - BlockPtr::null(level) + BlockPtr::null(BlockMeta::new(level)) }; let mut record = unsafe { self.read_record(record_ptr, level)? }; + // If record has changed if buf[i..i + len] != record.data()[j..j + len] { - unsafe { - // CoW record using its current level - let mut old_addr = record.swap_addr(self.allocate(record.addr().level())?); + // Update record in memory + record.data_mut()[j..j + len].copy_from_slice(&buf[i..i + len]); - // If the record was resized we need to dealloc the original ptr - if old_addr.is_null() { - old_addr = record_ptr.addr(); - } - - record.data_mut()[j..j + len].copy_from_slice(&buf[i..i + len]); - record_ptr = self.write_block(record)?; - - if !old_addr.is_null() { - self.deallocate(old_addr); + // Handle record compression, if record is larger than one block + let decomp_level = record.addr().level(); + if decomp_level.0 > 0 { + // Maximum compressed record size is 64 KiB + let mut comp = vec![0; 64 * 1024]; + // First two bytes store compressed data length + match lz4_flex::compress_into(record.data(), &mut comp[2..]) { + Ok(comp_len) => { + comp[0] = comp_len as u8; + comp[1] = (comp_len >> 8) as u8; + let comp_level = BlockLevel::for_bytes((comp_len as u64) + 2); + comp.truncate(comp_level.bytes() as usize); + // Replace record with compressed record, if it saves space + if comp_level < decomp_level { + record = BlockData::new( + BlockAddr::null(BlockMeta::new_compressed( + comp_level, + decomp_level, + )), + RecordRaw(comp.into_boxed_slice()), + ); + } + } + Err(_err) => { + // Failures to compress can be ignored, with the original record data used + } } } + // CoW record using its current level + let mut old_addr = + unsafe { record.swap_addr(self.allocate(record.addr().meta())?) }; + + // If the record was resized we need to dealloc the original ptr + if old_addr.is_null() { + old_addr = record_ptr.addr(); + } + + // Write record to disk + record_ptr = unsafe { self.write_block(record)? }; + + // Update record pointer self.sync_node_record_ptr(node, *offset / record_level.bytes(), record_ptr)?; node_changed = true; + + // Deallocate old record + if !old_addr.is_null() { + unsafe { self.deallocate(old_addr) }; + } } i += len;