liteboxfs 0.2.0

A modern POSIX filesystem in a SQLite database
Documentation
use std::{collections::HashMap, fmt, iter::FusedIterator, str::FromStr};

#[cfg(feature = "chunking")]
use crate::cdc::{ContentDefinedChunker, ContentDefinedChunkerConfig};
use crate::{
    CreateOptions, FilesystemId,
    chunker::{Chunker, FixedSizeChunker},
    errors::InternalError,
};

/// The SQLite `application_id`, which serves as the magic bytes for LiteboxFS.
///
/// The SQLite Application ID is a 32-bit signed big-endian integer.
///
/// This spells "lbox" in ASCII.
pub const SQLITE_APPLICATION_ID: i32 = 0x6c626f78;

/// The SQLite `user_version`, which is an application-defined integer that LiteboxFS uses to
/// version the on-disk format.
pub const SQLITE_USER_VERSION: u32 = 1;

#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
pub struct FormatVersion(u32);

impl FormatVersion {
    pub const CURRENT: Self = Self(SQLITE_USER_VERSION);
}

impl From<u32> for FormatVersion {
    fn from(value: u32) -> Self {
        Self(value)
    }
}

#[derive(Debug, Clone, Copy)]
pub enum Compression {
    None,
    Zstd { level: i32 },
}

impl Compression {
    pub(crate) const ZSTD: Self = Self::Zstd { level: 1 };

    const NONE_KEY: &str = "none";
    const ZSTD_KEY: &str = "zstd";

    fn key(&self) -> &'static str {
        match self {
            Self::None => Self::NONE_KEY,
            Self::Zstd { .. } => Self::ZSTD_KEY,
        }
    }
}

#[derive(Debug, Clone, Copy)]
pub enum Chunking {
    FastCdc {
        min_size: usize,
        max_size: usize,
        avg_size: usize,
    },
    Fixed {
        size: usize,
    },
}

impl Chunking {
    pub(crate) fn new_cdc(block_size: usize) -> Self {
        Self::FastCdc {
            min_size: block_size / 4,
            avg_size: block_size,
            max_size: block_size * 4,
        }
    }

    const FASTCDC_KEY: &str = "fastcdc";
    const FIXED_KEY: &str = "fixed";

    fn key(&self) -> &'static str {
        match self {
            Self::FastCdc { .. } => Self::FASTCDC_KEY,
            Self::Fixed { .. } => Self::FIXED_KEY,
        }
    }
}

#[derive(Debug, Clone, Copy)]
enum SettingsKey {
    Uuid,
    Version,
    Compression,
    CompressionLevel,
    Chunking,
    MinChunkSize,
    MaxChunkSize,
    AvgChunkSize,
    FixedChunkSize,
    LogicalBlockSize,
    MerkleBranchFactor,
    SmallWriteThreshold,
    MinWriteBufferSize,
}

#[cfg_attr(coverage_nightly, coverage(off))]
impl fmt::Display for SettingsKey {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        write!(f, "{}", self.as_ref())
    }
}

impl AsRef<str> for SettingsKey {
    fn as_ref(&self) -> &str {
        use SettingsKey::*;

        match self {
            Uuid => "uuid",
            Version => "version",
            Compression => "compression",
            CompressionLevel => "compression_level",
            Chunking => "chunking",
            MinChunkSize => "min_chunk_size",
            MaxChunkSize => "max_chunk_size",
            AvgChunkSize => "avg_chunk_size",
            FixedChunkSize => "fixed_chunk_size",
            LogicalBlockSize => "logical_block_size",
            MerkleBranchFactor => "merkle_branch_factor",
            SmallWriteThreshold => "small_write_threshold",
            MinWriteBufferSize => "min_write_buffer_size",
        }
    }
}

#[derive(Debug, Clone)]
pub struct Settings {
    pub uuid: FilesystemId,
    pub version: FormatVersion,
    pub compression: Compression,
    pub chunking: Chunking,
    pub logical_block_size: usize,
    pub merkle_branch_factor: u32,
    pub small_write_threshold: usize,
    pub min_write_buffer_size: usize,
}

impl Default for Settings {
    fn default() -> Self {
        CreateOptions::default().try_into().expect(
            "The default settings should always be valid, even when all features are disabled.",
        )
    }
}

impl Settings {
    pub fn largest_block_size(&self) -> usize {
        match self.chunking {
            Chunking::FastCdc { max_size, .. } => max_size,
            Chunking::Fixed { size } => size,
        }
    }

    pub fn chunker(&self) -> Box<dyn Chunker> {
        match self.chunking {
            Chunking::Fixed { size } => Box::new(FixedSizeChunker::new(size as u32)),
            #[cfg(feature = "chunking")]
            Chunking::FastCdc {
                min_size,
                avg_size,
                max_size,
            } => Box::new(ContentDefinedChunker::new(ContentDefinedChunkerConfig {
                min_size: min_size as u32,
                avg_size: avg_size as u32,
                max_size: max_size as u32,
            })),
            #[cfg(not(feature = "chunking"))]
            Chunking::FastCdc { .. } => unimplemented!(),
        }
    }
}

#[derive(Debug)]
pub struct SettingsIter {
    inner: std::vec::IntoIter<(String, String)>,
}

impl Iterator for SettingsIter {
    type Item = (String, String);

    fn next(&mut self) -> Option<Self::Item> {
        self.inner.next()
    }
}

impl FusedIterator for SettingsIter {}

impl IntoIterator for &Settings {
    type Item = (String, String);
    type IntoIter = SettingsIter;

    fn into_iter(self) -> Self::IntoIter {
        let mut pairs = vec![
            (SettingsKey::Uuid, self.uuid.to_string()),
            (SettingsKey::Version, self.version.0.to_string()),
            (SettingsKey::Compression, self.compression.key().to_string()),
            (SettingsKey::Chunking, self.chunking.key().to_string()),
            (
                SettingsKey::LogicalBlockSize,
                self.logical_block_size.to_string(),
            ),
            (
                SettingsKey::MerkleBranchFactor,
                self.merkle_branch_factor.to_string(),
            ),
            (
                SettingsKey::SmallWriteThreshold,
                self.small_write_threshold.to_string(),
            ),
            (
                SettingsKey::MinWriteBufferSize,
                self.min_write_buffer_size.to_string(),
            ),
        ];

        if let Compression::Zstd { level } = self.compression {
            pairs.push((SettingsKey::CompressionLevel, level.to_string()));
        }

        match self.chunking {
            Chunking::FastCdc {
                min_size,
                max_size,
                avg_size,
            } => {
                pairs.push((SettingsKey::MinChunkSize, min_size.to_string()));
                pairs.push((SettingsKey::MaxChunkSize, max_size.to_string()));
                pairs.push((SettingsKey::AvgChunkSize, avg_size.to_string()));
            }
            Chunking::Fixed { size } => {
                pairs.push((SettingsKey::FixedChunkSize, size.to_string()));
            }
        }

        SettingsIter {
            inner: pairs
                .into_iter()
                .map(|(key, value)| (key.to_string(), value))
                .collect::<Vec<_>>()
                .into_iter(),
        }
    }
}

#[derive(Debug)]
pub struct SettingsValidator {
    map: HashMap<String, String>,
}

impl SettingsValidator {
    fn get<V>(&self, key: impl AsRef<str>) -> crate::Result<V>
    where
        V: FromStr,
    {
        self.map
            .get(key.as_ref())
            .ok_or::<crate::Error>(InternalError::MalformedSettings.into())?
            .parse::<V>()
            .map_err(|_| InternalError::MalformedSettings.into())
    }
}

impl TryFrom<SettingsValidator> for Settings {
    type Error = crate::Error;

    fn try_from(value: SettingsValidator) -> Result<Self, Self::Error> {
        let compression = match value.get::<String>(SettingsKey::Compression)?.as_str() {
            Compression::ZSTD_KEY if !cfg!(feature = "compression") => {
                return Err(InternalError::CompressionDisabled.into());
            }
            Compression::ZSTD_KEY => Compression::Zstd {
                level: value.get(SettingsKey::CompressionLevel)?,
            },
            Compression::NONE_KEY => Compression::None,
            _ => return Err(InternalError::MalformedSettings.into()),
        };

        let chunking = match value.get::<String>(SettingsKey::Chunking)?.as_str() {
            Chunking::FASTCDC_KEY if !cfg!(feature = "chunking") => {
                return Err(InternalError::ChunkingDisabled.into());
            }
            Chunking::FASTCDC_KEY => Chunking::FastCdc {
                min_size: value.get(SettingsKey::MinChunkSize)?,
                max_size: value.get(SettingsKey::MaxChunkSize)?,
                avg_size: value.get(SettingsKey::AvgChunkSize)?,
            },
            Chunking::FIXED_KEY => Chunking::Fixed {
                size: value.get(SettingsKey::FixedChunkSize)?,
            },
            _ => return Err(InternalError::MalformedSettings.into()),
        };

        let version = FormatVersion::from(value.get::<u32>(SettingsKey::Version)?);

        if version > FormatVersion::CURRENT {
            return Err(crate::Error::UnsupportedFormatVersion);
        }

        Ok(Self {
            uuid: FilesystemId::from_str(&value.get::<String>(SettingsKey::Uuid)?)?,
            version,
            compression,
            chunking,
            logical_block_size: value.get(SettingsKey::LogicalBlockSize)?,
            merkle_branch_factor: value.get(SettingsKey::MerkleBranchFactor)?,
            small_write_threshold: value.get(SettingsKey::SmallWriteThreshold)?,
            min_write_buffer_size: value.get(SettingsKey::MinWriteBufferSize)?,
        })
    }
}

impl FromIterator<(String, String)> for SettingsValidator {
    fn from_iter<T: IntoIterator<Item = (String, String)>>(iter: T) -> Self {
        Self {
            map: iter.into_iter().collect::<HashMap<_, _>>(),
        }
    }
}

impl<'a> FromIterator<(&'a str, &'a str)> for SettingsValidator {
    fn from_iter<T: IntoIterator<Item = (&'a str, &'a str)>>(iter: T) -> Self {
        Self {
            map: iter
                .into_iter()
                .map(|(key, value)| (key.to_string(), value.to_string()))
                .collect::<HashMap<_, _>>(),
        }
    }
}