use std::any::Any;
use std::collections::HashSet;
use std::convert::{TryFrom, TryInto};
use std::ffi::{OsStr, OsString};
use std::fmt::{Debug, Display, Formatter, Result as FmtResult};
use std::fs::OpenOptions;
use std::io::{Error, ErrorKind, Result};
use std::ops::Deref;
use std::os::unix::ffi::OsStrExt;
use std::path::{Component, Path, PathBuf};
use std::str::FromStr;
use std::sync::Arc;
use std::time::Duration;
use anyhow::bail;
use fuse_backend_rs::abi::fuse_abi::Attr;
use fuse_backend_rs::api::filesystem::Entry;
use nydus_api::{ConfigV2, RafsConfigV2};
use nydus_storage::device::{
BlobChunkInfo, BlobDevice, BlobFeatures, BlobInfo, BlobIoMerge, BlobIoVec,
};
use nydus_storage::meta::toc::TocEntryList;
use nydus_utils::compress;
use nydus_utils::digest::{self, RafsDigest};
use serde::Serialize;
use self::layout::v5::RafsV5PrefetchTable;
use self::layout::v6::RafsV6PrefetchTable;
use self::layout::{XattrName, XattrValue, RAFS_SUPER_VERSION_V5, RAFS_SUPER_VERSION_V6};
use self::noop::NoopSuperBlock;
use crate::fs::{RAFS_DEFAULT_ATTR_TIMEOUT, RAFS_DEFAULT_ENTRY_TIMEOUT};
use crate::{RafsError, RafsIoReader, RafsIoWrite, RafsResult};
mod md_v5;
mod md_v6;
mod noop;
pub mod cached_v5;
pub mod chunk;
pub mod direct_v5;
pub mod direct_v6;
pub mod inode;
pub mod layout;
pub use nydus_storage::{RAFS_DEFAULT_CHUNK_SIZE, RAFS_MAX_CHUNK_SIZE};
pub const RAFS_BLOB_ID_MAX_LENGTH: usize = 64;
pub const RAFS_ATTR_BLOCK_SIZE: u32 = 4096;
pub const RAFS_MAX_NAME: usize = 255;
pub const RAFS_MAX_METADATA_SIZE: usize = 0x8000_0000;
pub const DOT: &str = ".";
pub const DOTDOT: &str = "..";
pub type Inode = u64;
pub trait RafsSuperInodes {
fn get_max_ino(&self) -> Inode;
fn get_inode(&self, ino: Inode, validate_inode: bool) -> Result<Arc<dyn RafsInode>>;
fn get_extended_inode(&self, ino: Inode, validate_inode: bool)
-> Result<Arc<dyn RafsInodeExt>>;
}
pub trait RafsSuperBlock: RafsSuperInodes + Send + Sync {
fn load(&mut self, r: &mut RafsIoReader) -> Result<()>;
fn update(&self, r: &mut RafsIoReader) -> RafsResult<()>;
fn destroy(&mut self);
fn get_blob_infos(&self) -> Vec<Arc<BlobInfo>>;
fn root_ino(&self) -> u64;
fn get_chunk_info(&self, _idx: usize) -> Result<Arc<dyn BlobChunkInfo>>;
fn set_blob_device(&self, blob_device: BlobDevice);
}
pub enum RafsInodeWalkAction {
Continue,
Break,
}
pub type RafsInodeWalkHandler<'a> = &'a mut dyn FnMut(
Option<Arc<dyn RafsInode>>,
OsString,
u64,
u64,
) -> Result<RafsInodeWalkAction>;
pub trait RafsInode: Any {
fn validate(&self, max_inode: Inode, chunk_size: u64) -> Result<()>;
fn alloc_bio_vecs(
&self,
device: &BlobDevice,
offset: u64,
size: usize,
user_io: bool,
) -> Result<Vec<BlobIoVec>>;
fn collect_descendants_inodes(
&self,
descendants: &mut Vec<Arc<dyn RafsInode>>,
) -> Result<usize>;
fn get_entry(&self) -> Entry;
fn get_attr(&self) -> Attr;
fn ino(&self) -> u64;
fn rdev(&self) -> u32;
fn projid(&self) -> u32;
fn is_dir(&self) -> bool;
fn is_symlink(&self) -> bool;
fn is_reg(&self) -> bool;
fn is_hardlink(&self) -> bool;
fn has_xattr(&self) -> bool;
fn get_xattr(&self, name: &OsStr) -> Result<Option<XattrValue>>;
fn get_xattrs(&self) -> Result<Vec<XattrName>>;
fn get_symlink(&self) -> Result<OsString>;
fn get_symlink_size(&self) -> u16;
fn walk_children_inodes(&self, entry_offset: u64, handler: RafsInodeWalkHandler) -> Result<()>;
fn get_child_by_name(&self, name: &OsStr) -> Result<Arc<dyn RafsInodeExt>>;
fn get_child_by_index(&self, idx: u32) -> Result<Arc<dyn RafsInodeExt>>;
fn get_child_count(&self) -> u32;
fn get_child_index(&self) -> Result<u32>;
fn size(&self) -> u64;
fn is_empty_size(&self) -> bool {
self.size() == 0
}
fn get_chunk_count(&self) -> u32;
fn as_any(&self) -> &dyn Any;
}
pub trait RafsInodeExt: RafsInode {
fn as_inode(&self) -> &dyn RafsInode;
fn parent(&self) -> u64;
fn name(&self) -> OsString;
fn get_name_size(&self) -> u16;
fn flags(&self) -> u64;
fn get_digest(&self) -> RafsDigest;
fn get_chunk_info(&self, idx: u32) -> Result<Arc<dyn BlobChunkInfo>>;
}
pub trait RafsStore {
fn store(&self, w: &mut dyn RafsIoWrite) -> Result<usize>;
}
bitflags! {
#[derive(Serialize)]
pub struct RafsSuperFlags: u64 {
const COMPRESSION_NONE = 0x0000_0001;
const COMPRESSION_LZ4 = 0x0000_0002;
const HASH_BLAKE3 = 0x0000_0004;
const HASH_SHA256 = 0x0000_0008;
const EXPLICIT_UID_GID = 0x0000_0010;
const HAS_XATTR = 0x0000_0020;
const COMPRESSION_GZIP = 0x0000_0040;
const COMPRESSION_ZSTD = 0x0000_0080;
const INLINED_CHUNK_DIGEST = 0x0000_0100;
}
}
impl Default for RafsSuperFlags {
fn default() -> Self {
RafsSuperFlags::empty()
}
}
impl Display for RafsSuperFlags {
fn fmt(&self, f: &mut Formatter) -> FmtResult {
write!(f, "{:?}", self)?;
Ok(())
}
}
impl From<RafsSuperFlags> for digest::Algorithm {
fn from(flags: RafsSuperFlags) -> Self {
match flags {
x if x.contains(RafsSuperFlags::HASH_BLAKE3) => digest::Algorithm::Blake3,
x if x.contains(RafsSuperFlags::HASH_SHA256) => digest::Algorithm::Sha256,
_ => digest::Algorithm::Blake3,
}
}
}
impl From<digest::Algorithm> for RafsSuperFlags {
fn from(d: digest::Algorithm) -> RafsSuperFlags {
match d {
digest::Algorithm::Blake3 => RafsSuperFlags::HASH_BLAKE3,
digest::Algorithm::Sha256 => RafsSuperFlags::HASH_SHA256,
}
}
}
impl From<RafsSuperFlags> for compress::Algorithm {
fn from(flags: RafsSuperFlags) -> Self {
match flags {
x if x.contains(RafsSuperFlags::COMPRESSION_NONE) => compress::Algorithm::None,
x if x.contains(RafsSuperFlags::COMPRESSION_LZ4) => compress::Algorithm::Lz4Block,
x if x.contains(RafsSuperFlags::COMPRESSION_GZIP) => compress::Algorithm::GZip,
x if x.contains(RafsSuperFlags::COMPRESSION_ZSTD) => compress::Algorithm::Zstd,
_ => compress::Algorithm::Lz4Block,
}
}
}
impl From<compress::Algorithm> for RafsSuperFlags {
fn from(c: compress::Algorithm) -> RafsSuperFlags {
match c {
compress::Algorithm::None => RafsSuperFlags::COMPRESSION_NONE,
compress::Algorithm::Lz4Block => RafsSuperFlags::COMPRESSION_LZ4,
compress::Algorithm::GZip => RafsSuperFlags::COMPRESSION_GZIP,
compress::Algorithm::Zstd => RafsSuperFlags::COMPRESSION_ZSTD,
}
}
}
#[derive(Clone, Copy, Debug)]
pub struct RafsSuperConfig {
pub version: RafsVersion,
pub compressor: compress::Algorithm,
pub digester: digest::Algorithm,
pub chunk_size: u32,
pub explicit_uidgid: bool,
}
impl RafsSuperConfig {
pub fn check_compatibility(&self, meta: &RafsSuperMeta) -> Result<()> {
if self.chunk_size != meta.chunk_size {
return Err(einval!(format!(
"Inconsistent configuration of chunk_size: {} vs {}",
self.chunk_size, meta.chunk_size
)));
}
if self.explicit_uidgid != meta.explicit_uidgid() {
return Err(einval!(format!(
"Using inconsistent explicit_uidgid setting {:?}, target explicit_uidgid setting {:?}",
self.explicit_uidgid,
meta.explicit_uidgid()
)));
}
if u32::from(self.version) != meta.version {
return Err(einval!(format!(
"Using inconsistent RAFS version {:?}, target RAFS version {:?}",
self.version,
RafsVersion::try_from(meta.version)?
)));
}
if self.version == RafsVersion::V5 && self.digester != meta.get_digester() {
return Err(einval!(format!(
"RAFS v5 can not support different digest algorithm due to inode digest, {} vs {}",
self.digester,
meta.get_digester()
)));
}
Ok(())
}
}
#[derive(Clone, Copy, Debug, Serialize)]
pub struct RafsSuperMeta {
pub magic: u32,
pub version: u32,
pub sb_size: u32,
pub root_inode: Inode,
pub chunk_size: u32,
pub inodes_count: u64,
pub flags: RafsSuperFlags,
pub inode_table_entries: u32,
pub inode_table_offset: u64,
pub blob_table_size: u32,
pub blob_table_offset: u64,
pub extended_blob_table_offset: u64,
pub extended_blob_table_entries: u32,
pub prefetch_table_offset: u64,
pub prefetch_table_entries: u32,
pub attr_timeout: Duration,
pub entry_timeout: Duration,
pub is_chunk_dict: bool,
pub meta_blkaddr: u32,
pub root_nid: u16,
pub chunk_table_offset: u64,
pub chunk_table_size: u64,
}
impl RafsSuperMeta {
pub fn is_v5(&self) -> bool {
self.version == RAFS_SUPER_VERSION_V5
}
pub fn is_v6(&self) -> bool {
self.version == RAFS_SUPER_VERSION_V6
}
pub fn is_chunk_dict(&self) -> bool {
self.is_chunk_dict
}
pub fn explicit_uidgid(&self) -> bool {
self.flags.contains(RafsSuperFlags::EXPLICIT_UID_GID)
}
pub fn has_xattr(&self) -> bool {
self.flags.contains(RafsSuperFlags::HAS_XATTR)
}
pub fn has_inlined_chunk_digest(&self) -> bool {
self.is_v6() && self.flags.contains(RafsSuperFlags::INLINED_CHUNK_DIGEST)
}
pub fn get_compressor(&self) -> compress::Algorithm {
if self.is_v5() || self.is_v6() {
self.flags.into()
} else {
compress::Algorithm::None
}
}
pub fn get_digester(&self) -> digest::Algorithm {
if self.is_v5() || self.is_v6() {
self.flags.into()
} else {
digest::Algorithm::Blake3
}
}
pub fn get_config(&self) -> RafsSuperConfig {
RafsSuperConfig {
version: self.version.try_into().unwrap_or_default(),
compressor: self.get_compressor(),
digester: self.get_digester(),
chunk_size: self.chunk_size,
explicit_uidgid: self.explicit_uidgid(),
}
}
}
impl Default for RafsSuperMeta {
fn default() -> Self {
RafsSuperMeta {
magic: 0,
version: 0,
sb_size: 0,
inodes_count: 0,
root_inode: 0,
chunk_size: 0,
flags: RafsSuperFlags::empty(),
inode_table_entries: 0,
inode_table_offset: 0,
blob_table_size: 0,
blob_table_offset: 0,
extended_blob_table_offset: 0,
extended_blob_table_entries: 0,
prefetch_table_offset: 0,
prefetch_table_entries: 0,
attr_timeout: Duration::from_secs(RAFS_DEFAULT_ATTR_TIMEOUT),
entry_timeout: Duration::from_secs(RAFS_DEFAULT_ENTRY_TIMEOUT),
meta_blkaddr: 0,
root_nid: 0,
is_chunk_dict: false,
chunk_table_offset: 0,
chunk_table_size: 0,
}
}
}
#[derive(Clone, Copy, Debug, PartialEq)]
pub enum RafsVersion {
V5,
V6,
}
impl Default for RafsVersion {
fn default() -> Self {
RafsVersion::V5
}
}
impl TryFrom<u32> for RafsVersion {
type Error = Error;
fn try_from(version: u32) -> std::result::Result<Self, Self::Error> {
if version == RAFS_SUPER_VERSION_V5 {
return Ok(RafsVersion::V5);
} else if version == RAFS_SUPER_VERSION_V6 {
return Ok(RafsVersion::V6);
}
Err(einval!(format!("invalid RAFS version number {}", version)))
}
}
impl From<RafsVersion> for u32 {
fn from(v: RafsVersion) -> Self {
match v {
RafsVersion::V5 => RAFS_SUPER_VERSION_V5,
RafsVersion::V6 => RAFS_SUPER_VERSION_V6,
}
}
}
impl RafsVersion {
pub fn is_v5(&self) -> bool {
self == &Self::V5
}
pub fn is_v6(&self) -> bool {
self == &Self::V6
}
}
#[derive(Clone, Debug, Eq, PartialEq)]
pub enum RafsMode {
Direct,
Cached,
}
impl Default for RafsMode {
fn default() -> Self {
RafsMode::Direct
}
}
impl FromStr for RafsMode {
type Err = Error;
fn from_str(s: &str) -> std::result::Result<Self, Self::Err> {
match s {
"direct" => Ok(Self::Direct),
"cached" => Ok(Self::Cached),
_ => Err(einval!("rafs mode should be direct or cached")),
}
}
}
impl Display for RafsMode {
fn fmt(&self, f: &mut Formatter) -> FmtResult {
match self {
Self::Direct => write!(f, "direct"),
Self::Cached => write!(f, "cached"),
}
}
}
pub struct RafsSuper {
pub mode: RafsMode,
pub validate_digest: bool,
pub meta: RafsSuperMeta,
pub superblock: Arc<dyn RafsSuperBlock>,
}
impl Default for RafsSuper {
fn default() -> Self {
Self {
mode: RafsMode::Direct,
validate_digest: false,
meta: RafsSuperMeta::default(),
superblock: Arc::new(NoopSuperBlock::new()),
}
}
}
impl RafsSuper {
pub fn new(conf: &RafsConfigV2) -> Result<Self> {
Ok(Self {
mode: RafsMode::from_str(conf.mode.as_str())?,
validate_digest: conf.validate,
..Default::default()
})
}
pub fn destroy(&mut self) {
Arc::get_mut(&mut self.superblock)
.expect("Inodes are no longer used.")
.destroy();
}
pub fn load_from_file<P: AsRef<Path>>(
path: P,
config: Arc<ConfigV2>,
validate_digest: bool,
is_chunk_dict: bool,
) -> Result<(Self, RafsIoReader)> {
let mut rs = RafsSuper {
mode: RafsMode::Direct,
validate_digest,
..Default::default()
};
rs.meta.is_chunk_dict = is_chunk_dict;
let file = OpenOptions::new()
.read(true)
.write(false)
.open(path.as_ref())?;
let mut reader = Box::new(file) as RafsIoReader;
let mut blob_accessible = config.internal.blob_accessible();
if let Err(e) = rs.load(&mut reader) {
let id = BlobInfo::get_blob_id_from_meta_path(path.as_ref())?;
let new_path = match TocEntryList::extract_rafs_meta(&id, config.clone()) {
Ok(v) => v,
Err(_e) => {
debug!("failed to load inlined RAFS meta, {}", _e);
return Err(e);
}
};
let file = OpenOptions::new().read(true).write(false).open(new_path)?;
reader = Box::new(file) as RafsIoReader;
rs.load(&mut reader)?;
rs.set_blob_id_from_meta_path(path.as_ref())?;
blob_accessible = true;
} else {
let mut fixed = false;
let blobs = rs.superblock.get_blob_infos();
for blob in blobs.iter() {
if blob.has_feature(BlobFeatures::INLINED_FS_META) {
blob.set_blob_id_from_meta_path(path.as_ref())?;
fixed = true;
}
}
if !fixed && !blob_accessible && !blobs.is_empty() {
let last = blobs.len() - 1;
let blob = &blobs[last];
if !blob.has_feature(BlobFeatures::CAP_TAR_TOC) {
rs.set_blob_id_from_meta_path(path.as_ref())?;
}
}
}
if blob_accessible
&& (validate_digest || config.is_chunk_validation_enabled())
&& rs.meta.has_inlined_chunk_digest()
{
rs.create_blob_device(config)?;
}
Ok((rs, reader))
}
pub(crate) fn load(&mut self, r: &mut RafsIoReader) -> Result<()> {
if self.try_load_v5(r)? {
return Ok(());
}
if self.try_load_v6(r)? {
return Ok(());
}
Err(Error::new(ErrorKind::Other, "invalid RAFS superblock"))
}
pub fn set_blob_id_from_meta_path(&self, meta_path: &Path) -> Result<()> {
let blobs = self.superblock.get_blob_infos();
for blob in blobs.iter() {
if blob.has_feature(BlobFeatures::INLINED_FS_META)
|| !blob.has_feature(BlobFeatures::CAP_TAR_TOC)
{
blob.set_blob_id_from_meta_path(meta_path)?;
}
}
Ok(())
}
pub fn create_blob_device(&self, config: Arc<ConfigV2>) -> Result<()> {
let blobs = self.superblock.get_blob_infos();
let device = BlobDevice::new(&config, &blobs)?;
self.superblock.set_blob_device(device);
Ok(())
}
pub fn update(&self, r: &mut RafsIoReader) -> RafsResult<()> {
if self.meta.is_v5() {
self.skip_v5_superblock(r)
.map_err(RafsError::FillSuperblock)?;
}
self.superblock.update(r)
}
pub fn get_max_ino(&self) -> Inode {
self.superblock.get_max_ino()
}
pub fn get_inode(&self, ino: Inode, validate_inode: bool) -> Result<Arc<dyn RafsInode>> {
self.superblock.get_inode(ino, validate_inode)
}
pub fn get_extended_inode(
&self,
ino: Inode,
validate_inode: bool,
) -> Result<Arc<dyn RafsInodeExt>> {
self.superblock.get_extended_inode(ino, validate_inode)
}
pub fn ino_from_path(&self, f: &Path) -> Result<Inode> {
let root_ino = self.superblock.root_ino();
if f == Path::new("/") {
return Ok(root_ino);
} else if !f.starts_with("/") {
return Err(einval!());
}
let entries = f
.components()
.filter(|comp| *comp != Component::RootDir)
.map(|comp| match comp {
Component::Normal(name) => Some(name),
Component::ParentDir => Some(OsStr::from_bytes(DOTDOT.as_bytes())),
Component::CurDir => Some(OsStr::from_bytes(DOT.as_bytes())),
_ => None,
})
.collect::<Vec<_>>();
if entries.is_empty() {
warn!("Path can't be parsed {:?}", f);
return Err(enoent!());
}
let mut parent = self.get_extended_inode(root_ino, self.validate_digest)?;
for p in entries {
match p {
None => {
error!("Illegal specified path {:?}", f);
return Err(einval!());
}
Some(name) => {
parent = parent.get_child_by_name(name).map_err(|e| {
warn!("File {:?} not in RAFS filesystem, {}", name, e);
enoent!()
})?;
}
}
}
Ok(parent.ino())
}
pub fn prefetch_files(
&self,
device: &BlobDevice,
r: &mut RafsIoReader,
root_ino: Inode,
files: Option<Vec<Inode>>,
fetcher: &dyn Fn(&mut BlobIoVec, bool),
) -> RafsResult<bool> {
if let Some(files) = files {
let mut hardlinks: HashSet<u64> = HashSet::new();
let mut state = BlobIoMerge::default();
for f_ino in files {
self.prefetch_data(device, f_ino, &mut state, &mut hardlinks, fetcher)
.map_err(|e| RafsError::Prefetch(e.to_string()))?;
}
for (_id, mut desc) in state.drain() {
fetcher(&mut desc, true);
}
Ok(false)
} else if self.meta.is_v5() {
self.prefetch_data_v5(device, r, root_ino, fetcher)
} else if self.meta.is_v6() {
self.prefetch_data_v6(device, r, root_ino, fetcher)
} else {
Err(RafsError::Prefetch(
"Unknown filesystem version, prefetch disabled".to_string(),
))
}
}
#[inline]
fn prefetch_inode(
device: &BlobDevice,
inode: &Arc<dyn RafsInode>,
state: &mut BlobIoMerge,
hardlinks: &mut HashSet<u64>,
fetcher: &dyn Fn(&mut BlobIoVec, bool),
) -> Result<()> {
if inode.is_hardlink() {
if hardlinks.contains(&inode.ino()) {
return Ok(());
} else {
hardlinks.insert(inode.ino());
}
}
let descs = inode.alloc_bio_vecs(device, 0, inode.size() as usize, false)?;
for desc in descs {
state.append(desc);
if let Some(desc) = state.get_current_element() {
fetcher(desc, false);
}
}
Ok(())
}
fn prefetch_data(
&self,
device: &BlobDevice,
ino: u64,
state: &mut BlobIoMerge,
hardlinks: &mut HashSet<u64>,
fetcher: &dyn Fn(&mut BlobIoVec, bool),
) -> Result<()> {
let inode = self
.superblock
.get_inode(ino, self.validate_digest)
.map_err(|_e| enoent!("Can't find inode"))?;
if inode.is_dir() {
let mut descendants = Vec::new();
let _ = inode.collect_descendants_inodes(&mut descendants)?;
for i in descendants.iter() {
Self::prefetch_inode(device, i, state, hardlinks, fetcher)?;
}
} else if !inode.is_empty_size() && inode.is_reg() {
Self::prefetch_inode(device, &inode, state, hardlinks, fetcher)?;
}
Ok(())
}
}
impl RafsSuper {
pub fn path_from_ino(&self, ino: Inode) -> Result<PathBuf> {
if ino == self.superblock.root_ino() {
return Ok(self.get_extended_inode(ino, false)?.name().into());
}
let mut path = PathBuf::new();
let mut cur_ino = ino;
let mut inode;
loop {
inode = self.get_extended_inode(cur_ino, false)?;
let e: PathBuf = inode.name().into();
path = e.join(path);
if inode.ino() == self.superblock.root_ino() {
break;
} else {
cur_ino = inode.parent();
}
}
Ok(path)
}
pub fn get_prefetched_inos(&self, bootstrap: &mut RafsIoReader) -> Result<Vec<u32>> {
if self.meta.is_v5() {
let mut pt = RafsV5PrefetchTable::new();
pt.load_prefetch_table_from(
bootstrap,
self.meta.prefetch_table_offset,
self.meta.prefetch_table_entries as usize,
)?;
Ok(pt.inodes)
} else {
let mut pt = RafsV6PrefetchTable::new();
pt.load_prefetch_table_from(
bootstrap,
self.meta.prefetch_table_offset,
self.meta.prefetch_table_entries as usize,
)?;
Ok(pt.inodes)
}
}
pub fn walk_directory<P: AsRef<Path>>(
&self,
ino: Inode,
parent: Option<P>,
cb: &mut dyn FnMut(&dyn RafsInodeExt, &Path) -> anyhow::Result<()>,
) -> anyhow::Result<()> {
let inode = self.get_extended_inode(ino, false)?;
if !inode.is_dir() {
bail!("inode {} is not a directory", ino);
}
self.do_walk_directory(inode.deref(), parent, cb)
}
#[allow(clippy::only_used_in_recursion)]
fn do_walk_directory<P: AsRef<Path>>(
&self,
inode: &dyn RafsInodeExt,
parent: Option<P>,
cb: &mut dyn FnMut(&dyn RafsInodeExt, &Path) -> anyhow::Result<()>,
) -> anyhow::Result<()> {
let path = if let Some(parent) = parent {
parent.as_ref().join(inode.name())
} else {
PathBuf::from("/")
};
cb(inode, &path)?;
if inode.is_dir() {
for idx in 0..inode.get_child_count() {
let child = inode.get_child_by_index(idx)?;
self.do_walk_directory(child.deref(), Some(&path), cb)?;
}
}
Ok(())
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_rafs_mode() {
assert!(RafsMode::from_str("").is_err());
assert!(RafsMode::from_str("directed").is_err());
assert!(RafsMode::from_str("Direct").is_err());
assert!(RafsMode::from_str("Cached").is_err());
assert_eq!(RafsMode::from_str("direct").unwrap(), RafsMode::Direct);
assert_eq!(RafsMode::from_str("cached").unwrap(), RafsMode::Cached);
assert_eq!(&format!("{}", RafsMode::Direct), "direct");
assert_eq!(&format!("{}", RafsMode::Cached), "cached");
}
#[test]
fn test_rafs_compressor() {
assert_eq!(
compress::Algorithm::from(RafsSuperFlags::COMPRESSION_NONE),
compress::Algorithm::None
);
assert_eq!(
compress::Algorithm::from(RafsSuperFlags::COMPRESSION_GZIP),
compress::Algorithm::GZip
);
assert_eq!(
compress::Algorithm::from(RafsSuperFlags::COMPRESSION_LZ4),
compress::Algorithm::Lz4Block
);
assert_eq!(
compress::Algorithm::from(RafsSuperFlags::COMPRESSION_ZSTD),
compress::Algorithm::Zstd
);
assert_eq!(
compress::Algorithm::from(
RafsSuperFlags::COMPRESSION_ZSTD | RafsSuperFlags::COMPRESSION_LZ4,
),
compress::Algorithm::Lz4Block
);
assert_eq!(
compress::Algorithm::from(RafsSuperFlags::empty()),
compress::Algorithm::Lz4Block
);
}
#[test]
fn test_rafs_digestor() {
assert_eq!(
digest::Algorithm::from(RafsSuperFlags::HASH_BLAKE3),
digest::Algorithm::Blake3
);
assert_eq!(
digest::Algorithm::from(RafsSuperFlags::HASH_SHA256),
digest::Algorithm::Sha256
);
assert_eq!(
digest::Algorithm::from(RafsSuperFlags::HASH_SHA256 | RafsSuperFlags::HASH_BLAKE3,),
digest::Algorithm::Blake3
);
assert_eq!(
digest::Algorithm::from(RafsSuperFlags::empty()),
digest::Algorithm::Blake3
);
}
}