use std::any::Any;
use std::collections::hash_map::Drain;
use std::collections::HashMap;
use std::convert::TryFrom;
use std::fmt::{Debug, Formatter};
use std::fs::File;
use std::io::{self, Error};
use std::ops::Deref;
use std::os::unix::io::AsRawFd;
use std::path::Path;
use std::sync::{Arc, Mutex};
use arc_swap::ArcSwap;
use fuse_backend_rs::api::filesystem::ZeroCopyWriter;
use fuse_backend_rs::file_buf::FileVolatileSlice;
use fuse_backend_rs::file_traits::FileReadWriteVolatile;
use nydus_api::ConfigV2;
use nydus_utils::compress;
use nydus_utils::crypt::{self, Cipher, CipherContext};
use nydus_utils::digest::{self, RafsDigest};
use crate::cache::BlobCache;
use crate::factory::BLOB_FACTORY;
pub(crate) const BLOB_FEATURE_INCOMPAT_MASK: u32 = 0x0000_ffff;
pub(crate) const BLOB_FEATURE_INCOMPAT_VALUE: u32 = 0x0000_0fff;
bitflags! {
pub struct BlobFeatures: u32 {
const ALIGNED = 0x0000_0001;
const INLINED_FS_META = 0x0000_0002;
const CHUNK_INFO_V2 = 0x0000_0004;
const ZRAN = 0x0000_0008;
const SEPARATE = 0x0000_0010;
const INLINED_CHUNK_DIGEST = 0x0000_0020;
const TARFS = 0x0000_0040;
const BATCH = 0x0000_0080;
const ENCRYPTED = 0x0000_0100;
const HAS_TAR_HEADER = 0x1000_0000;
const HAS_TOC = 0x2000_0000;
const CAP_TAR_TOC = 0x4000_0000;
const _V5_NO_EXT_BLOB_TABLE = 0x8000_0000;
const IS_CHUNKDICT_GENERATED = 0x0000_0200;
const IS_SEPARATED_WITH_PREFETCH_FILES = 0x0000_0400;
const EXTERNAL = 0x0000_0800;
}
}
impl Default for BlobFeatures {
fn default() -> Self {
BlobFeatures::empty()
}
}
impl BlobFeatures {
pub fn is_tarfs(&self) -> bool {
self.contains(BlobFeatures::CAP_TAR_TOC) && self.contains(BlobFeatures::TARFS)
}
}
impl TryFrom<u32> for BlobFeatures {
type Error = Error;
fn try_from(value: u32) -> Result<Self, Self::Error> {
if value & BLOB_FEATURE_INCOMPAT_MASK & !BLOB_FEATURE_INCOMPAT_VALUE != 0
|| value & BlobFeatures::_V5_NO_EXT_BLOB_TABLE.bits() != 0
{
Err(einval!(format!("invalid blob features: 0x{:x}", value)))
} else {
Ok(unsafe { BlobFeatures::from_bits_unchecked(value) })
}
}
}
#[derive(Clone, Debug, Default)]
pub struct BlobInfo {
blob_index: u32,
blob_id: String,
blob_features: BlobFeatures,
compressed_size: u64,
uncompressed_size: u64,
chunk_size: u32,
chunk_count: u32,
compressor: compress::Algorithm,
cipher: crypt::Algorithm,
digester: digest::Algorithm,
prefetch_offset: u32,
prefetch_size: u32,
is_legacy_stargz: bool,
meta_ci_compressor: u32,
meta_ci_offset: u64,
meta_ci_compressed_size: u64,
meta_ci_uncompressed_size: u64,
blob_toc_digest: [u8; 32],
blob_meta_digest: [u8; 32],
blob_meta_size: u64,
blob_toc_size: u32,
fs_cache_file: Option<Arc<File>>,
meta_path: Arc<Mutex<String>>,
cipher_object: Arc<Cipher>,
cipher_ctx: Option<CipherContext>,
is_chunkdict_generated: bool,
}
impl BlobInfo {
pub fn new(
blob_index: u32,
blob_id: String,
uncompressed_size: u64,
compressed_size: u64,
chunk_size: u32,
chunk_count: u32,
blob_features: BlobFeatures,
) -> Self {
let blob_id = blob_id.trim_end_matches('\0').to_string();
let mut blob_info = BlobInfo {
blob_index,
blob_id,
blob_features,
uncompressed_size,
compressed_size,
chunk_size,
chunk_count,
compressor: compress::Algorithm::None,
cipher: crypt::Algorithm::None,
digester: digest::Algorithm::Blake3,
prefetch_offset: 0,
prefetch_size: 0,
is_legacy_stargz: false,
meta_ci_compressor: 0,
meta_ci_offset: 0,
meta_ci_compressed_size: 0,
meta_ci_uncompressed_size: 0,
blob_toc_digest: [0u8; 32],
blob_meta_digest: [0u8; 32],
blob_meta_size: 0,
blob_toc_size: 0,
fs_cache_file: None,
meta_path: Arc::new(Mutex::new(String::new())),
cipher_object: Default::default(),
cipher_ctx: None,
is_chunkdict_generated: false,
};
blob_info.compute_features();
blob_info
}
pub fn set_chunk_count(&mut self, count: usize) {
self.chunk_count = count as u32;
}
pub fn set_compressed_size(&mut self, size: usize) {
self.compressed_size = size as u64;
}
pub fn set_uncompressed_size(&mut self, size: usize) {
self.uncompressed_size = size as u64;
}
pub fn set_meta_ci_compressed_size(&mut self, size: usize) {
self.meta_ci_compressed_size = size as u64;
}
pub fn set_meta_ci_uncompressed_size(&mut self, size: usize) {
self.meta_ci_uncompressed_size = size as u64;
}
pub fn set_meta_ci_offset(&mut self, size: usize) {
self.meta_ci_offset = size as u64;
}
pub fn set_chunkdict_generated(&mut self, is_chunkdict_generated: bool) {
self.is_chunkdict_generated = is_chunkdict_generated;
}
pub fn is_chunkdict_generated(&self) -> bool {
self.is_chunkdict_generated
}
pub fn blob_index(&self) -> u32 {
self.blob_index
}
pub fn blob_id(&self) -> String {
if (!self.has_feature(BlobFeatures::EXTERNAL)
&& self.has_feature(BlobFeatures::INLINED_FS_META)
&& !self.has_feature(BlobFeatures::SEPARATE))
|| !self.has_feature(BlobFeatures::CAP_TAR_TOC)
{
let guard = self.meta_path.lock().unwrap();
if !guard.is_empty() {
return guard.deref().clone();
}
}
self.blob_id.clone()
}
pub fn set_blob_id(&mut self, blob_id: String) {
self.blob_id = blob_id
}
pub fn raw_blob_id(&self) -> &str {
&self.blob_id
}
pub fn compressed_data_size(&self) -> u64 {
if self.has_feature(BlobFeatures::SEPARATE) {
self.compressed_size
} else if self.has_feature(BlobFeatures::CAP_TAR_TOC) {
if self.meta_ci_is_valid() {
if self.has_feature(BlobFeatures::HAS_TAR_HEADER) {
self.meta_ci_offset - 0x200
} else {
self.meta_ci_offset
}
} else {
if self.has_feature(BlobFeatures::HAS_TAR_HEADER) {
self.compressed_size - 0x200
} else {
self.compressed_size
}
}
} else {
self.compressed_size
}
}
pub fn compressed_size(&self) -> u64 {
self.compressed_size
}
pub fn uncompressed_size(&self) -> u64 {
self.uncompressed_size
}
pub fn chunk_size(&self) -> u32 {
self.chunk_size
}
pub fn chunk_count(&self) -> u32 {
self.chunk_count
}
pub fn compressor(&self) -> compress::Algorithm {
self.compressor
}
pub fn set_compressor(&mut self, compressor: compress::Algorithm) {
self.compressor = compressor;
self.compute_features();
}
pub fn cipher(&self) -> crypt::Algorithm {
self.cipher
}
pub fn set_cipher(&mut self, cipher: crypt::Algorithm) {
self.cipher = cipher;
}
pub fn cipher_object(&self) -> Arc<Cipher> {
self.cipher_object.clone()
}
pub fn cipher_context(&self) -> Option<CipherContext> {
self.cipher_ctx.clone()
}
pub fn set_cipher_info(
&mut self,
cipher: crypt::Algorithm,
cipher_object: Arc<Cipher>,
cipher_ctx: Option<CipherContext>,
) {
self.cipher = cipher;
self.cipher_object = cipher_object;
self.cipher_ctx = cipher_ctx;
}
pub fn digester(&self) -> digest::Algorithm {
self.digester
}
pub fn set_digester(&mut self, digester: digest::Algorithm) {
self.digester = digester;
}
pub fn prefetch_offset(&self) -> u64 {
self.prefetch_offset as u64
}
pub fn prefetch_size(&self) -> u64 {
self.prefetch_size as u64
}
pub fn set_prefetch_info(&mut self, offset: u64, size: u64) {
self.prefetch_offset = offset as u32;
self.prefetch_size = size as u32;
}
pub fn is_legacy_stargz(&self) -> bool {
self.is_legacy_stargz
}
pub fn set_blob_meta_info(
&mut self,
offset: u64,
compressed_size: u64,
uncompressed_size: u64,
compressor: u32,
) {
self.meta_ci_compressor = compressor;
self.meta_ci_offset = offset;
self.meta_ci_compressed_size = compressed_size;
self.meta_ci_uncompressed_size = uncompressed_size;
}
pub fn meta_ci_compressor(&self) -> compress::Algorithm {
if self.meta_ci_compressor == compress::Algorithm::Lz4Block as u32 {
compress::Algorithm::Lz4Block
} else if self.meta_ci_compressor == compress::Algorithm::GZip as u32 {
compress::Algorithm::GZip
} else if self.meta_ci_compressor == compress::Algorithm::Zstd as u32 {
compress::Algorithm::Zstd
} else {
compress::Algorithm::None
}
}
pub fn meta_ci_offset(&self) -> u64 {
self.meta_ci_offset
}
pub fn meta_ci_compressed_size(&self) -> u64 {
self.meta_ci_compressed_size
}
pub fn meta_ci_uncompressed_size(&self) -> u64 {
self.meta_ci_uncompressed_size
}
pub fn meta_ci_is_valid(&self) -> bool {
self.meta_ci_compressed_size != 0 && self.meta_ci_uncompressed_size != 0
}
pub fn set_fscache_file(&mut self, file: Option<Arc<File>>) {
self.fs_cache_file = file;
}
#[cfg(target_os = "linux")]
pub(crate) fn get_fscache_file(&self) -> Option<Arc<File>> {
self.fs_cache_file.clone()
}
pub fn features(&self) -> BlobFeatures {
self.blob_features
}
pub fn has_feature(&self, features: BlobFeatures) -> bool {
self.blob_features.bits() & features.bits() == features.bits()
}
pub fn is_external(&self) -> bool {
self.has_feature(BlobFeatures::EXTERNAL)
}
fn compute_features(&mut self) {
if self.chunk_count == 0 {
self.blob_features |= BlobFeatures::_V5_NO_EXT_BLOB_TABLE;
}
if self.compressor == compress::Algorithm::GZip
&& !self.has_feature(BlobFeatures::CHUNK_INFO_V2)
{
self.is_legacy_stargz = true;
}
}
pub fn set_separated_with_prefetch_files_feature(&mut self, is_prefetchblob: bool) {
if is_prefetchblob {
self.blob_features |= BlobFeatures::IS_SEPARATED_WITH_PREFETCH_FILES;
}
}
pub fn blob_toc_digest(&self) -> &[u8; 32] {
&self.blob_toc_digest
}
pub fn set_blob_toc_digest(&mut self, digest: [u8; 32]) {
self.blob_toc_digest = digest;
}
pub fn blob_toc_size(&self) -> u32 {
self.blob_toc_size
}
pub fn set_blob_toc_size(&mut self, sz: u32) {
self.blob_toc_size = sz;
}
pub fn blob_meta_digest(&self) -> &[u8; 32] {
&self.blob_meta_digest
}
pub fn set_blob_meta_digest(&mut self, digest: [u8; 32]) {
self.blob_meta_digest = digest;
}
pub fn blob_meta_size(&self) -> u64 {
self.blob_meta_size
}
pub fn set_blob_meta_size(&mut self, size: u64) {
self.blob_meta_size = size;
}
pub fn set_blob_id_from_meta_path(&self, path: &Path) -> Result<(), Error> {
*self.meta_path.lock().unwrap() = Self::get_blob_id_from_meta_path(path)?;
Ok(())
}
pub fn get_blob_id_from_meta_path(path: &Path) -> Result<String, Error> {
let mut id = path.file_name().ok_or_else(|| {
einval!(format!(
"failed to get blob id from meta file path {}",
path.display()
))
})?;
loop {
let id1 = Path::new(id).file_stem().ok_or_else(|| {
einval!(format!(
"failed to get blob id from meta file path {}",
path.display()
))
})?;
if id1.is_empty() {
return Err(einval!(format!(
"failed to get blob id from meta file path {}",
path.display()
)));
} else if id == id1 {
break;
} else {
id = id1;
}
}
let id = id.to_str().ok_or_else(|| {
einval!(format!(
"failed to get blob id from meta file path {}",
path.display()
))
})?;
Ok(id.to_string())
}
pub fn get_blob_meta_id(&self) -> Result<String, Error> {
assert!(self.has_feature(BlobFeatures::SEPARATE));
let id = if self.has_feature(BlobFeatures::INLINED_FS_META) {
let guard = self.meta_path.lock().unwrap();
if guard.is_empty() {
return Err(einval!("failed to get blob id from meta file name"));
}
guard.deref().clone()
} else {
hex::encode(self.blob_meta_digest)
};
Ok(id)
}
pub fn get_cipher_info(&self) -> (crypt::Algorithm, Arc<Cipher>, Option<CipherContext>) {
(
self.cipher,
self.cipher_object.clone(),
self.cipher_ctx.clone(),
)
}
}
bitflags! {
pub struct BlobChunkFlags: u32 {
const COMPRESSED = 0x0000_0001;
const _HOLECHUNK = 0x0000_0002;
const ENCRYPTED = 0x0000_0004;
const BATCH = 0x0000_0008;
const HAS_CRC32 = 0x0000_0010;
}
}
impl Default for BlobChunkFlags {
fn default() -> Self {
BlobChunkFlags::empty()
}
}
pub trait BlobChunkInfo: Any + Sync + Send {
fn chunk_id(&self) -> &RafsDigest;
fn id(&self) -> u32;
fn blob_index(&self) -> u32;
fn compressed_offset(&self) -> u64;
fn compressed_size(&self) -> u32;
fn compressed_end(&self) -> u64 {
self.compressed_offset() + self.compressed_size() as u64
}
fn uncompressed_offset(&self) -> u64;
fn uncompressed_size(&self) -> u32;
fn uncompressed_end(&self) -> u64 {
self.uncompressed_offset() + self.uncompressed_size() as u64
}
fn is_batch(&self) -> bool;
fn is_compressed(&self) -> bool;
fn is_encrypted(&self) -> bool;
fn has_crc32(&self) -> bool;
fn crc32(&self) -> u32;
fn as_any(&self) -> &dyn Any;
}
#[derive(Clone)]
pub struct BlobIoChunk(Arc<dyn BlobChunkInfo>);
impl From<Arc<dyn BlobChunkInfo>> for BlobIoChunk {
fn from(v: Arc<dyn BlobChunkInfo>) -> Self {
BlobIoChunk(v)
}
}
impl BlobChunkInfo for BlobIoChunk {
fn chunk_id(&self) -> &RafsDigest {
self.0.chunk_id()
}
fn id(&self) -> u32 {
self.0.id()
}
fn blob_index(&self) -> u32 {
self.0.blob_index()
}
fn compressed_offset(&self) -> u64 {
self.0.compressed_offset()
}
fn compressed_size(&self) -> u32 {
self.0.compressed_size()
}
fn uncompressed_offset(&self) -> u64 {
self.0.uncompressed_offset()
}
fn uncompressed_size(&self) -> u32 {
self.0.uncompressed_size()
}
fn is_batch(&self) -> bool {
self.0.is_batch()
}
fn is_compressed(&self) -> bool {
self.0.is_compressed()
}
fn is_encrypted(&self) -> bool {
self.0.is_encrypted()
}
fn has_crc32(&self) -> bool {
self.0.has_crc32()
}
fn crc32(&self) -> u32 {
self.0.crc32()
}
fn as_any(&self) -> &dyn Any {
self
}
}
#[derive(Clone)]
pub struct BlobIoDesc {
pub blob: Arc<BlobInfo>,
pub chunkinfo: BlobIoChunk,
pub offset: u32,
pub size: u32,
pub(crate) user_io: bool,
}
impl BlobIoDesc {
pub fn new(
blob: Arc<BlobInfo>,
chunkinfo: BlobIoChunk,
offset: u32,
size: u32,
user_io: bool,
) -> Self {
BlobIoDesc {
blob,
chunkinfo,
offset,
size,
user_io,
}
}
pub fn is_continuous(&self, next: &BlobIoDesc, max_gap: u64) -> bool {
let prev_end = self.chunkinfo.compressed_offset() + self.chunkinfo.compressed_size() as u64;
let next_offset = next.chunkinfo.compressed_offset();
if self.chunkinfo.is_batch() || next.chunkinfo.is_batch() {
return next.chunkinfo.uncompressed_offset() - self.chunkinfo.uncompressed_end()
<= max_gap;
}
if self.chunkinfo.blob_index() == next.chunkinfo.blob_index() && next_offset >= prev_end {
if next.blob.is_legacy_stargz() {
next_offset - prev_end <= max_gap * 8
} else {
next_offset - prev_end <= max_gap
}
} else {
false
}
}
}
impl Debug for BlobIoDesc {
fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
f.debug_struct("BlobIoDesc")
.field("blob_index", &self.blob.blob_index)
.field("chunk_index", &self.chunkinfo.id())
.field("compressed_offset", &self.chunkinfo.compressed_offset())
.field("file_offset", &self.offset)
.field("size", &self.size)
.field("user", &self.user_io)
.finish()
}
}
pub struct BlobIoVec {
bi_blob: Arc<BlobInfo>,
bi_size: u64,
pub(crate) bi_vec: Vec<BlobIoDesc>,
}
impl BlobIoVec {
pub fn new(bi_blob: Arc<BlobInfo>) -> Self {
BlobIoVec {
bi_blob,
bi_size: 0,
bi_vec: Vec::with_capacity(128),
}
}
pub fn push(&mut self, desc: BlobIoDesc) {
assert_eq!(self.bi_blob.blob_index(), desc.blob.blob_index());
assert_eq!(self.bi_blob.blob_id(), desc.blob.blob_id());
assert!(self.bi_size.checked_add(desc.size as u64).is_some());
self.bi_size += desc.size as u64;
self.bi_vec.push(desc);
}
pub fn append(&mut self, mut vec: BlobIoVec) {
assert_eq!(self.bi_blob.blob_id(), vec.bi_blob.blob_id());
assert!(self.bi_size.checked_add(vec.bi_size).is_some());
self.bi_vec.append(vec.bi_vec.as_mut());
self.bi_size += vec.bi_size;
}
pub fn reset(&mut self) {
self.bi_size = 0;
self.bi_vec.truncate(0);
}
pub fn len(&self) -> usize {
self.bi_vec.len()
}
pub fn is_empty(&self) -> bool {
self.bi_vec.is_empty()
}
pub fn size(&self) -> u64 {
self.bi_size
}
pub fn blob_io_desc(&self, index: usize) -> Option<&BlobIoDesc> {
if index < self.bi_vec.len() {
Some(&self.bi_vec[index])
} else {
None
}
}
pub fn blob_index(&self) -> u32 {
self.bi_blob.blob_index()
}
pub fn is_target_blob(&self, blob_index: u32) -> bool {
self.bi_blob.blob_index() == blob_index
}
pub fn has_same_blob(&self, desc: &BlobIoVec) -> bool {
self.bi_blob.blob_index() == desc.bi_blob.blob_index()
}
}
impl Debug for BlobIoVec {
fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
f.debug_struct("BlobIoDesc")
.field("blob_index", &self.bi_blob.blob_index)
.field("size", &self.bi_size)
.field("decriptors", &self.bi_vec)
.finish()
}
}
#[derive(Default)]
pub struct BlobIoMerge {
map: HashMap<String, BlobIoVec>,
current: String,
}
impl BlobIoMerge {
pub fn append(&mut self, desc: BlobIoVec) {
if !desc.is_empty() {
let id = desc.bi_blob.blob_id.as_str();
if self.current != id {
self.current = id.to_string();
}
if let Some(prev) = self.map.get_mut(id) {
prev.append(desc);
} else {
self.map.insert(id.to_string(), desc);
}
}
}
pub fn drain(&mut self) -> Drain<'_, String, BlobIoVec> {
self.map.drain()
}
pub fn get_current_element(&mut self) -> Option<&mut BlobIoVec> {
self.map.get_mut(&self.current)
}
}
#[derive(Clone, Debug, Default)]
pub(crate) struct BlobIoSegment {
pub offset: u32,
pub len: u32,
}
impl BlobIoSegment {
pub fn new(offset: u32, len: u32) -> Self {
Self { offset, len }
}
#[inline]
pub fn append(&mut self, offset: u32, len: u32) {
assert!(offset.checked_add(len).is_some());
assert_eq!(offset, 0);
self.len += len;
}
pub fn is_empty(&self) -> bool {
self.offset == 0 && self.len == 0
}
}
#[derive(Clone, Debug)]
pub(crate) enum BlobIoTag {
User(BlobIoSegment),
Internal,
}
impl BlobIoTag {
pub fn is_user_io(&self) -> bool {
matches!(self, BlobIoTag::User(_))
}
}
#[derive(Default, Clone)]
pub struct BlobIoRange {
pub(crate) blob_info: Arc<BlobInfo>,
pub(crate) blob_offset: u64,
pub(crate) blob_size: u64,
pub(crate) chunks: Vec<Arc<dyn BlobChunkInfo>>,
pub(crate) tags: Vec<BlobIoTag>,
}
impl Debug for BlobIoRange {
fn fmt(&self, f: &mut Formatter) -> std::fmt::Result {
f.debug_struct("BlobIoRange")
.field("blob_id", &self.blob_info.blob_id())
.field("blob_offset", &self.blob_offset)
.field("blob_size", &self.blob_size)
.field("tags", &self.tags)
.finish()
}
}
impl BlobIoRange {
pub fn new(bio: &BlobIoDesc, capacity: usize) -> Self {
let blob_size = bio.chunkinfo.compressed_size() as u64;
let blob_offset = bio.chunkinfo.compressed_offset();
assert!(blob_offset.checked_add(blob_size).is_some());
let mut chunks = Vec::with_capacity(capacity);
let mut tags = Vec::with_capacity(capacity);
tags.push(Self::tag_from_desc(bio));
chunks.push(bio.chunkinfo.0.clone());
BlobIoRange {
blob_info: bio.blob.clone(),
blob_offset,
blob_size,
chunks,
tags,
}
}
pub fn merge(&mut self, bio: &BlobIoDesc, _max_gap: u64) {
let end = self.blob_offset + self.blob_size;
let offset = bio.chunkinfo.compressed_offset();
let size = bio.chunkinfo.compressed_size() as u64;
let size = if end == offset {
assert!(offset.checked_add(size).is_some());
size
} else {
assert!(offset > end);
size + (offset - end)
};
assert!(end.checked_add(size).is_some());
self.blob_size += size;
self.tags.push(Self::tag_from_desc(bio));
self.chunks.push(bio.chunkinfo.0.clone());
}
fn tag_from_desc(bio: &BlobIoDesc) -> BlobIoTag {
if bio.user_io {
BlobIoTag::User(BlobIoSegment::new(bio.offset, bio.size as u32))
} else {
BlobIoTag::Internal
}
}
}
pub struct BlobPrefetchRequest {
pub blob_id: String,
pub offset: u64,
pub len: u64,
}
pub trait BlobObject: AsRawFd {
fn base_offset(&self) -> u64;
fn is_all_data_ready(&self) -> bool;
fn fetch_range_compressed(&self, offset: u64, size: u64, prefetch: bool) -> io::Result<()>;
fn fetch_range_uncompressed(&self, offset: u64, size: u64) -> io::Result<()>;
fn prefetch_chunks(&self, range: &BlobIoRange) -> io::Result<()>;
}
#[derive(Clone, Default)]
pub struct BlobDevice {
blobs: Arc<ArcSwap<Vec<Arc<dyn BlobCache>>>>,
blob_count: usize,
}
impl BlobDevice {
pub fn new(config: &Arc<ConfigV2>, blob_infos: &[Arc<BlobInfo>]) -> io::Result<BlobDevice> {
let mut blobs = Vec::with_capacity(blob_infos.len());
for blob_info in blob_infos.iter() {
let blob = BLOB_FACTORY.new_blob_cache(config, blob_info)?;
blobs.push(blob);
}
Ok(BlobDevice {
blobs: Arc::new(ArcSwap::new(Arc::new(blobs))),
blob_count: blob_infos.len(),
})
}
pub fn update(
&self,
config: &Arc<ConfigV2>,
blob_infos: &[Arc<BlobInfo>],
fs_prefetch: bool,
) -> io::Result<()> {
if self.blobs.load().len() != blob_infos.len() {
return Err(einval!(
"number of blobs doesn't match when update 'BlobDevice' object"
));
}
let mut blobs = Vec::with_capacity(blob_infos.len());
for blob_info in blob_infos.iter() {
let blob = BLOB_FACTORY.new_blob_cache(config, blob_info)?;
blobs.push(blob);
}
if fs_prefetch {
self.stop_prefetch();
}
self.blobs.store(Arc::new(blobs));
if fs_prefetch {
self.start_prefetch();
}
Ok(())
}
pub fn close(&self) -> io::Result<()> {
Ok(())
}
pub fn has_device(&self) -> bool {
self.blob_count > 0
}
pub fn read_to(&self, w: &mut dyn ZeroCopyWriter, desc: &mut BlobIoVec) -> io::Result<usize> {
if desc.bi_vec.is_empty() {
if desc.bi_size == 0 {
Ok(0)
} else {
Err(einval!("BlobIoVec size doesn't match."))
}
} else if desc.blob_index() as usize >= self.blob_count {
Err(einval!("BlobIoVec has out of range blob_index."))
} else {
let size = desc.bi_size;
let mut f = BlobDeviceIoVec::new(self, desc);
w.write_from(&mut f, size as usize, 0)
}
}
pub fn prefetch(
&self,
io_vecs: &[&BlobIoVec],
prefetches: &[BlobPrefetchRequest],
) -> io::Result<()> {
for idx in 0..prefetches.len() {
if let Some(blob) = self.get_blob_by_id(&prefetches[idx].blob_id) {
let _ = blob.prefetch(blob.clone(), &prefetches[idx..idx + 1], &[]);
}
}
for io_vec in io_vecs.iter() {
if let Some(blob) = self.get_blob_by_iovec(io_vec) {
let _ = blob
.prefetch(blob.clone(), &[], &io_vec.bi_vec)
.map_err(|e| {
error!("failed to prefetch blob data, {}", e);
});
}
}
Ok(())
}
pub fn start_prefetch(&self) {
for blob in self.blobs.load().iter() {
let _ = blob.start_prefetch();
}
}
pub fn stop_prefetch(&self) {
for blob in self.blobs.load().iter() {
let _ = blob.stop_prefetch();
}
}
pub fn fetch_range_synchronous(&self, prefetches: &[BlobPrefetchRequest]) -> io::Result<()> {
for req in prefetches {
if req.len == 0 {
continue;
}
if let Some(cache) = self.get_blob_by_id(&req.blob_id) {
trace!(
"fetch blob {} offset {} size {}",
req.blob_id,
req.offset,
req.len
);
if let Some(obj) = cache.get_blob_object() {
obj.fetch_range_uncompressed(req.offset as u64, req.len as u64)
.map_err(|e| {
warn!(
"Failed to prefetch data from blob {}, offset {}, size {}, {}",
cache.blob_id(),
req.offset,
req.len,
e
);
e
})?;
} else {
error!("No support for fetching uncompressed blob data");
return Err(einval!("No support for fetching uncompressed blob data"));
}
}
}
Ok(())
}
pub fn all_chunks_ready(&self, io_vecs: &[BlobIoVec]) -> bool {
for io_vec in io_vecs.iter() {
if let Some(blob) = self.get_blob_by_iovec(io_vec) {
let chunk_map = blob.get_chunk_map();
for desc in io_vec.bi_vec.iter() {
if !chunk_map.is_ready(&desc.chunkinfo).unwrap_or(false) {
return false;
}
}
} else {
return false;
}
}
true
}
pub fn create_io_chunk(&self, blob_index: u32, chunk_index: u32) -> Option<BlobIoChunk> {
if (blob_index as usize) < self.blob_count {
let state = self.blobs.load();
let blob = &state[blob_index as usize];
blob.get_chunk_info(chunk_index).map(|v| v.into())
} else {
None
}
}
pub fn get_chunk_info(
&self,
blob_index: u32,
chunk_index: u32,
) -> Option<Arc<dyn BlobChunkInfo>> {
if (blob_index as usize) < self.blob_count {
let state = self.blobs.load();
let blob = &state[blob_index as usize];
blob.get_chunk_info(chunk_index)
} else {
None
}
}
fn get_blob_by_iovec(&self, iovec: &BlobIoVec) -> Option<Arc<dyn BlobCache>> {
let blob_index = iovec.blob_index();
if (blob_index as usize) < self.blob_count {
return Some(self.blobs.load()[blob_index as usize].clone());
}
None
}
fn get_blob_by_id(&self, blob_id: &str) -> Option<Arc<dyn BlobCache>> {
for blob in self.blobs.load().iter() {
if blob.blob_id() == blob_id {
return Some(blob.clone());
}
}
None
}
}
struct BlobDeviceIoVec<'a> {
dev: &'a BlobDevice,
iovec: &'a mut BlobIoVec,
}
impl<'a> BlobDeviceIoVec<'a> {
fn new(dev: &'a BlobDevice, iovec: &'a mut BlobIoVec) -> Self {
BlobDeviceIoVec { dev, iovec }
}
}
impl FileReadWriteVolatile for BlobDeviceIoVec<'_> {
fn read_volatile(&mut self, _slice: FileVolatileSlice) -> Result<usize, Error> {
unimplemented!();
}
fn write_volatile(&mut self, _slice: FileVolatileSlice) -> Result<usize, Error> {
unimplemented!();
}
fn read_at_volatile(&mut self, slice: FileVolatileSlice, offset: u64) -> Result<usize, Error> {
let buffers = [slice];
self.read_vectored_at_volatile(&buffers, offset)
}
fn read_vectored_at_volatile(
&mut self,
buffers: &[FileVolatileSlice],
_offset: u64,
) -> Result<usize, Error> {
let index = self.iovec.blob_index();
let blobs = &self.dev.blobs.load();
if (index as usize) < blobs.len() {
blobs[index as usize].read(self.iovec, buffers)
} else {
let msg = format!(
"failed to get blob object for BlobIoVec, index {}, blob array len: {}",
index,
blobs.len()
);
Err(einval!(msg))
}
}
fn write_at_volatile(
&mut self,
_slice: FileVolatileSlice,
_offset: u64,
) -> Result<usize, Error> {
unimplemented!()
}
}
pub mod v5 {
use super::*;
pub trait BlobV5ChunkInfo: BlobChunkInfo {
fn index(&self) -> u32;
fn file_offset(&self) -> u64;
fn flags(&self) -> BlobChunkFlags;
fn as_base(&self) -> &dyn BlobChunkInfo;
}
}
#[cfg(test)]
mod tests {
use std::path::PathBuf;
use super::*;
use crate::test::MockChunkInfo;
#[test]
fn test_blob_io_chunk() {
let chunk: Arc<dyn BlobChunkInfo> = Arc::new(MockChunkInfo {
block_id: Default::default(),
blob_index: 0,
flags: Default::default(),
compress_size: 0x100,
uncompress_size: 0x200,
compress_offset: 0x1000,
uncompress_offset: 0x2000,
file_offset: 0,
index: 3,
crc32: 0,
});
let iochunk: BlobIoChunk = chunk.clone().into();
assert_eq!(iochunk.id(), 3);
assert_eq!(iochunk.compressed_offset(), 0x1000);
assert_eq!(iochunk.compressed_size(), 0x100);
assert_eq!(iochunk.uncompressed_offset(), 0x2000);
assert_eq!(iochunk.uncompressed_size(), 0x200);
assert!(!iochunk.is_compressed());
}
#[test]
fn test_chunk_is_continuous() {
let blob_info = Arc::new(BlobInfo::new(
1,
"test1".to_owned(),
0x200000,
0x100000,
0x100000,
512,
BlobFeatures::_V5_NO_EXT_BLOB_TABLE,
));
let chunk1 = Arc::new(MockChunkInfo {
block_id: Default::default(),
blob_index: 1,
flags: BlobChunkFlags::empty(),
compress_size: 0x800,
uncompress_size: 0x1000,
compress_offset: 0,
uncompress_offset: 0,
file_offset: 0,
index: 0,
crc32: 0,
}) as Arc<dyn BlobChunkInfo>;
let chunk2 = Arc::new(MockChunkInfo {
block_id: Default::default(),
blob_index: 1,
flags: BlobChunkFlags::empty(),
compress_size: 0x800,
uncompress_size: 0x1000,
compress_offset: 0x800,
uncompress_offset: 0x1000,
file_offset: 0x1000,
index: 1,
crc32: 0,
}) as Arc<dyn BlobChunkInfo>;
let chunk3 = Arc::new(MockChunkInfo {
block_id: Default::default(),
blob_index: 1,
flags: BlobChunkFlags::empty(),
compress_size: 0x800,
uncompress_size: 0x1000,
compress_offset: 0x1800,
uncompress_offset: 0x3000,
file_offset: 0x3000,
index: 1,
crc32: 0,
}) as Arc<dyn BlobChunkInfo>;
let desc1 = BlobIoDesc {
blob: blob_info.clone(),
chunkinfo: chunk1.into(),
offset: 0,
size: 0x1000,
user_io: true,
};
let desc2 = BlobIoDesc {
blob: blob_info.clone(),
chunkinfo: chunk2.into(),
offset: 0,
size: 0x1000,
user_io: true,
};
let desc3 = BlobIoDesc {
blob: blob_info,
chunkinfo: chunk3.into(),
offset: 0,
size: 0x1000,
user_io: true,
};
assert!(desc1.is_continuous(&desc2, 0x0));
assert!(desc1.is_continuous(&desc2, 0x1000));
assert!(!desc2.is_continuous(&desc1, 0x1000));
assert!(!desc2.is_continuous(&desc1, 0x0));
assert!(!desc1.is_continuous(&desc3, 0x0));
assert!(!desc1.is_continuous(&desc3, 0x400));
assert!(!desc1.is_continuous(&desc3, 0x800));
assert!(desc1.is_continuous(&desc3, 0x1000));
assert!(!desc2.is_continuous(&desc3, 0x0));
assert!(!desc2.is_continuous(&desc3, 0x400));
assert!(desc2.is_continuous(&desc3, 0x800));
assert!(desc2.is_continuous(&desc3, 0x1000));
}
#[test]
fn test_append_same_blob_with_diff_index() {
let blob1 = Arc::new(BlobInfo::new(
1,
"test1".to_owned(),
0x200000,
0x100000,
0x100000,
512,
BlobFeatures::_V5_NO_EXT_BLOB_TABLE,
));
let chunk1 = Arc::new(MockChunkInfo {
block_id: Default::default(),
blob_index: 1,
flags: BlobChunkFlags::empty(),
compress_size: 0x800,
uncompress_size: 0x1000,
compress_offset: 0,
uncompress_offset: 0,
file_offset: 0,
index: 0,
crc32: 0,
}) as Arc<dyn BlobChunkInfo>;
let mut iovec = BlobIoVec::new(blob1.clone());
iovec.push(BlobIoDesc::new(blob1, BlobIoChunk(chunk1), 0, 0x1000, true));
let blob2 = Arc::new(BlobInfo::new(
2, "test1".to_owned(), 0x200000,
0x100000,
0x100000,
512,
BlobFeatures::_V5_NO_EXT_BLOB_TABLE,
));
let chunk2 = Arc::new(MockChunkInfo {
block_id: Default::default(),
blob_index: 2,
flags: BlobChunkFlags::empty(),
compress_size: 0x800,
uncompress_size: 0x1000,
compress_offset: 0x800,
uncompress_offset: 0x1000,
file_offset: 0x1000,
index: 1,
crc32: 0,
}) as Arc<dyn BlobChunkInfo>;
let mut iovec2 = BlobIoVec::new(blob2.clone());
iovec2.push(BlobIoDesc::new(blob2, BlobIoChunk(chunk2), 0, 0x1000, true));
iovec.append(iovec2);
assert_eq!(0x2000, iovec.bi_size);
}
#[test]
fn test_extend_large_blob_io_vec() {
let size = 0x2_0000_0000; let chunk_size = 0x10_0000; let chunk_count = (size / chunk_size as u64) as u32;
let large_blob = Arc::new(BlobInfo::new(
0,
"blob_id".to_owned(),
size,
size,
chunk_size,
chunk_count,
BlobFeatures::default(),
));
let mut iovec = BlobIoVec::new(large_blob.clone());
let mut iovec2 = BlobIoVec::new(large_blob.clone());
for chunk_idx in 0..chunk_count {
let chunk = Arc::new(MockChunkInfo {
block_id: Default::default(),
blob_index: large_blob.blob_index,
flags: BlobChunkFlags::empty(),
compress_size: chunk_size,
compress_offset: chunk_idx as u64 * chunk_size as u64,
uncompress_size: 2 * chunk_size,
uncompress_offset: 2 * chunk_idx as u64 * chunk_size as u64,
file_offset: 2 * chunk_idx as u64 * chunk_size as u64,
index: chunk_idx as u32,
crc32: 0,
}) as Arc<dyn BlobChunkInfo>;
let desc = BlobIoDesc::new(large_blob.clone(), BlobIoChunk(chunk), 0, chunk_size, true);
if chunk_idx < chunk_count / 2 {
iovec.push(desc);
} else {
iovec2.push(desc)
}
}
iovec.append(iovec2);
assert_eq!(size, iovec.size());
assert_eq!(chunk_count, iovec.len() as u32);
}
#[test]
fn test_blob_info_blob_meta_id() {
let blob_info = BlobInfo::new(
1,
"blob_id".to_owned(),
0,
0,
0,
1,
BlobFeatures::SEPARATE | BlobFeatures::INLINED_FS_META,
);
let root_dir = &std::env::var("CARGO_MANIFEST_DIR").expect("$CARGO_MANIFEST_DIR");
let mut source_path = PathBuf::from(root_dir);
source_path.push("../tests/texture/blobs/be7d77eeb719f70884758d1aa800ed0fb09d701aaec469964e9d54325f0d5fef");
assert!(blob_info
.set_blob_id_from_meta_path(source_path.as_path())
.is_ok());
let id = blob_info.get_blob_meta_id();
assert!(id.is_ok());
assert_eq!(
id.unwrap(),
"be7d77eeb719f70884758d1aa800ed0fb09d701aaec469964e9d54325f0d5fef".to_owned()
);
}
}