use crate::{error::Error, fs::ChunkType};
use ic_stable_structures::storable::Bound;
use serde::{Deserialize, Serialize};
pub const FILE_CHUNK_SIZE_V1: usize = 4096;
pub const DEFAULT_FILE_CHUNK_SIZE_V2: usize = 16384;
pub const MAX_FILE_CHUNK_SIZE_V2: usize = 65536;
pub const MAX_FILE_NAME: usize = 255;
pub const MAX_FILE_CHUNK_COUNT: u32 = u32::MAX - 10;
pub const MAX_FILE_SIZE: u64 = (MAX_FILE_CHUNK_COUNT as u64) * FILE_CHUNK_SIZE_V1 as u64;
pub const MAX_FILE_ENTRY_INDEX: u32 = u32::MAX - 10;
pub const DUMMY_DOT_ENTRY_INDEX: u32 = u32::MAX - 5;
pub const DUMMY_DOT_DOT_ENTRY_INDEX: u32 = u32::MAX - 4;
pub const DUMMY_DOT_ENTRY: (DirEntryIndex, DirEntry) = (
DUMMY_DOT_ENTRY_INDEX,
DirEntry {
name: FileName {
length: 1,
bytes: {
let mut arr = [0u8; 255];
arr[0] = b'.';
arr
},
},
node: 0,
entry_type: None,
},
);
pub const DUMMY_DOT_DOT_ENTRY: (DirEntryIndex, DirEntry) = (
DUMMY_DOT_DOT_ENTRY_INDEX,
DirEntry {
name: FileName {
length: 2,
bytes: {
let mut arr = [0u8; 255];
arr[0] = b'.';
arr[1] = b'.';
arr
},
},
node: 0,
entry_type: None,
},
);
pub type Node = u64;
pub type FileSize = u64;
pub type FileChunkIndex = u32;
pub type FileChunkPtr = u64;
pub static ZEROES: [u8; MAX_FILE_CHUNK_SIZE_V2] = [0u8; MAX_FILE_CHUNK_SIZE_V2];
#[derive(Debug, PartialEq, Eq)]
pub(crate) struct ChunkHandle {
pub index: FileChunkIndex,
pub offset: FileSize,
pub len: FileSize,
}
#[derive(Clone, Debug, PartialEq)]
pub struct FileChunk {
pub bytes: [u8; FILE_CHUNK_SIZE_V1],
}
impl Default for FileChunk {
fn default() -> Self {
Self {
bytes: [0; FILE_CHUNK_SIZE_V1],
}
}
}
impl ic_stable_structures::Storable for FileChunk {
fn to_bytes(&'_ self) -> std::borrow::Cow<'_, [u8]> {
std::borrow::Cow::Borrowed(&self.bytes)
}
fn into_bytes(self) -> Vec<u8> {
self.bytes.to_vec()
}
fn from_bytes(bytes: std::borrow::Cow<[u8]>) -> Self {
Self {
bytes: bytes.as_ref().try_into().unwrap(),
}
}
const BOUND: Bound = Bound::Bounded {
max_size: FILE_CHUNK_SIZE_V1 as u32,
is_fixed_size: true,
};
}
#[derive(Clone, Debug, Default, Serialize, Deserialize)]
pub struct Header {
pub version: u32,
pub next_node: Node,
}
impl ic_stable_structures::Storable for Header {
fn to_bytes(&'_ self) -> std::borrow::Cow<'_, [u8]> {
let mut buf = vec![];
ciborium::ser::into_writer(&self, &mut buf).unwrap();
std::borrow::Cow::Owned(buf)
}
fn into_bytes(self) -> Vec<u8> {
let mut buf = vec![];
ciborium::ser::into_writer(&self, &mut buf).unwrap();
buf
}
fn from_bytes(bytes: std::borrow::Cow<[u8]>) -> Self {
ciborium::de::from_reader(bytes.as_ref()).unwrap()
}
const BOUND: Bound = Bound::Unbounded;
}
#[repr(C, align(8))]
#[derive(Clone, Debug, Default, Serialize, Deserialize, PartialEq)]
pub struct Metadata {
pub node: Node,
pub file_type: FileType,
pub link_count: u64,
pub size: FileSize,
pub times: Times,
pub first_dir_entry: Option<DirEntryIndex>, pub last_dir_entry: Option<DirEntryIndex>, pub chunk_type: Option<ChunkType>,
pub maximum_size_allowed: Option<FileSize>,
}
impl ic_stable_structures::Storable for Metadata {
fn to_bytes(&'_ self) -> std::borrow::Cow<'_, [u8]> {
let mut buf = vec![];
ciborium::ser::into_writer(&self, &mut buf).unwrap();
std::borrow::Cow::Owned(buf)
}
fn into_bytes(self) -> Vec<u8> {
let mut buf = vec![];
ciborium::ser::into_writer(&self, &mut buf).unwrap();
buf
}
fn from_bytes(bytes: std::borrow::Cow<[u8]>) -> Self {
ciborium::de::from_reader(bytes.as_ref()).unwrap()
}
const BOUND: Bound = Bound::Unbounded;
}
#[derive(Clone, Copy, Default, Debug, PartialEq, Eq, Serialize, Deserialize)]
pub enum FileType {
Directory = 3,
#[default]
RegularFile = 4,
SymbolicLink = 7,
}
impl TryFrom<u8> for FileType {
type Error = Error;
fn try_from(value: u8) -> Result<Self, Self::Error> {
match value {
3 => Ok(FileType::Directory),
4 => Ok(FileType::RegularFile),
7 => Ok(FileType::SymbolicLink),
_ => Err(Error::InvalidArgument),
}
}
}
impl From<FileType> for u8 {
fn from(val: FileType) -> Self {
match val {
FileType::Directory => 3,
FileType::RegularFile => 4,
FileType::SymbolicLink => 7,
}
}
}
#[derive(Clone, Copy, Debug, Default, Serialize, Deserialize, PartialEq)]
pub struct Times {
pub accessed: u64,
pub modified: u64,
pub created: u64,
}
use std::cmp::{Ord, Ordering, PartialOrd};
#[derive(Clone, Debug, Serialize, Deserialize)]
pub struct FileName {
pub length: u8,
#[serde(
deserialize_with = "deserialize_file_name",
serialize_with = "serialize_file_name"
)]
pub bytes: [u8; MAX_FILE_NAME],
}
impl Eq for FileName {}
impl PartialEq for FileName {
fn eq(&self, other: &Self) -> bool {
self.length == other.length
&& self.bytes[..self.length as usize] == other.bytes[..other.length as usize]
}
}
impl PartialOrd for FileName {
fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
Some(self.cmp(other))
}
}
impl Ord for FileName {
fn cmp(&self, other: &Self) -> Ordering {
let min_len = self.length.min(other.length) as usize;
match self.bytes[..min_len].cmp(&other.bytes[..min_len]) {
Ordering::Equal => self.length.cmp(&other.length),
ord => ord,
}
}
}
use ic_stable_structures::Storable;
impl Storable for FileName {
fn to_bytes(&'_ self) -> std::borrow::Cow<'_, [u8]> {
let mut buf = [0u8; MAX_FILE_NAME + 1];
buf[0] = self.length;
buf[1..256].copy_from_slice(&self.bytes);
std::borrow::Cow::Owned(buf.to_vec())
}
fn into_bytes(self) -> Vec<u8> {
let mut buf = [0u8; MAX_FILE_NAME + 1];
buf[0] = self.length;
buf[1..256].copy_from_slice(&self.bytes);
buf.to_vec()
}
fn from_bytes(bytes: std::borrow::Cow<[u8]>) -> Self {
let mut arr = [0u8; MAX_FILE_NAME];
arr.copy_from_slice(&bytes[1..MAX_FILE_NAME + 1]);
FileName {
length: bytes[0],
bytes: arr,
}
}
const BOUND: ic_stable_structures::storable::Bound =
ic_stable_structures::storable::Bound::Bounded {
max_size: 256,
is_fixed_size: true,
};
}
fn serialize_file_name<S>(bytes: &[u8; MAX_FILE_NAME], serializer: S) -> Result<S::Ok, S::Error>
where
S: serde::Serializer,
{
serde_bytes::Bytes::new(bytes).serialize(serializer)
}
fn deserialize_file_name<'de, D>(deserializer: D) -> Result<[u8; MAX_FILE_NAME], D::Error>
where
D: serde::Deserializer<'de>,
{
let bytes: Vec<u8> = serde_bytes::deserialize(deserializer).unwrap();
let len = bytes.len();
let bytes_array: [u8; MAX_FILE_NAME] = bytes
.try_into()
.map_err(|_| serde::de::Error::invalid_length(len, &"expected MAX_FILE_NAME bytes"))?;
Ok(bytes_array)
}
impl Default for FileName {
fn default() -> Self {
Self {
length: 0,
bytes: [0; MAX_FILE_NAME],
}
}
}
impl FileName {
pub fn new(name: &[u8]) -> Result<Self, Error> {
let len = name.len();
if len > MAX_FILE_NAME {
return Err(Error::FilenameTooLong);
}
let mut bytes = [0; MAX_FILE_NAME];
bytes[0..len].copy_from_slice(name);
Ok(Self {
length: len as u8,
bytes,
})
}
}
impl std::fmt::Display for FileName {
fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
write!(f, "{}", unsafe {
std::str::from_utf8_unchecked(&self.bytes[..(self.length as usize)])
})
}
}
pub type DirEntryIndex = u32;
#[derive(Clone, Debug, Default, Serialize, Deserialize)]
pub struct DirEntry {
pub name: FileName,
pub node: Node,
pub entry_type: Option<FileType>,
}
impl ic_stable_structures::Storable for DirEntry {
fn to_bytes(&'_ self) -> std::borrow::Cow<'_, [u8]> {
let mut buf = vec![];
ciborium::ser::into_writer(&self, &mut buf).unwrap();
std::borrow::Cow::Owned(buf)
}
fn into_bytes(self) -> Vec<u8> {
let mut buf = vec![];
ciborium::ser::into_writer(&self, &mut buf).unwrap();
buf
}
fn from_bytes(bytes: std::borrow::Cow<[u8]>) -> Self {
ciborium::de::from_reader(bytes.as_ref()).unwrap()
}
const BOUND: ic_stable_structures::storable::Bound = Bound::Unbounded;
}
pub enum MountedFileSizePolicy {
PreviousOrZero,
PreviousOrMemoryPages,
Explicit(FileSize),
MemoryPages,
}
impl MountedFileSizePolicy {
pub fn get_mounted_file_size(
&self,
previous_size: Option<FileSize>,
current_memory_pages: FileSize,
) -> FileSize {
let current_size = current_memory_pages * ic_cdk::stable::WASM_PAGE_SIZE_IN_BYTES;
match self {
MountedFileSizePolicy::PreviousOrZero => {
if let Some(old_size) = previous_size
&& old_size <= current_size
{
old_size
} else {
0
}
}
MountedFileSizePolicy::PreviousOrMemoryPages => {
if let Some(old_size) = previous_size
&& old_size <= current_size
{
old_size
} else {
current_size
}
}
MountedFileSizePolicy::Explicit(size) => *size,
MountedFileSizePolicy::MemoryPages => current_size,
}
}
}
#[cfg(test)]
mod tests {
use crate::{fs::ChunkType, storage::types::MountedFileSizePolicy};
use super::{DirEntryIndex, FileSize, FileType, Node, Times};
use serde::{Deserialize, Serialize};
#[derive(Clone, Debug, Default, Serialize, Deserialize, PartialEq)]
pub struct MetadataOld {
pub node: Node,
pub file_type: FileType,
pub link_count: u64,
pub size: FileSize,
pub times: Times,
pub first_dir_entry: Option<DirEntryIndex>,
pub last_dir_entry: Option<DirEntryIndex>,
}
#[derive(Clone, Debug, Default, Serialize, Deserialize, PartialEq)]
pub struct MetadataNew {
pub node: Node,
pub file_type: FileType,
pub link_count: u64,
pub size: FileSize,
pub times: Times,
pub _first_dir_entry: Option<DirEntryIndex>, pub _last_dir_entry: Option<DirEntryIndex>, pub chunk_type: Option<ChunkType>,
}
fn meta_to_bytes(meta: &'_ MetadataOld) -> std::borrow::Cow<'_, [u8]> {
let mut buf = vec![];
ciborium::ser::into_writer(meta, &mut buf).unwrap();
std::borrow::Cow::Owned(buf)
}
fn meta_from_bytes(bytes: std::borrow::Cow<[u8]>) -> MetadataNew {
ciborium::de::from_reader(bytes.as_ref()).unwrap()
}
#[test]
fn store_old_load_new() {
let meta_old = MetadataOld {
node: 23,
file_type: FileType::RegularFile,
link_count: 3,
size: 123,
times: Times::default(),
first_dir_entry: Some(23),
last_dir_entry: Some(35),
};
let bytes = meta_to_bytes(&meta_old);
let meta_new = meta_from_bytes(bytes);
assert_eq!(meta_new.node, meta_old.node);
assert_eq!(meta_new.file_type, meta_old.file_type);
assert_eq!(meta_new.link_count, meta_old.link_count);
assert_eq!(meta_new.size, meta_old.size);
assert_eq!(meta_new.times, meta_old.times);
assert_eq!(meta_new.chunk_type, None);
}
#[test]
fn store_old_load_new_both_none() {
let meta_old = MetadataOld {
node: 23,
file_type: FileType::RegularFile,
link_count: 3,
size: 123,
times: Times::default(),
first_dir_entry: None,
last_dir_entry: None,
};
let bytes = meta_to_bytes(&meta_old);
let meta_new = meta_from_bytes(bytes);
assert_eq!(meta_new.node, meta_old.node);
assert_eq!(meta_new.file_type, meta_old.file_type);
assert_eq!(meta_new.link_count, meta_old.link_count);
assert_eq!(meta_new.size, meta_old.size);
assert_eq!(meta_new.times, meta_old.times);
assert_eq!(meta_new.chunk_type, None);
}
#[test]
fn store_old_load_new_first_none() {
let meta_old = MetadataOld {
node: 23,
file_type: FileType::RegularFile,
link_count: 3,
size: 123,
times: Times::default(),
first_dir_entry: None,
last_dir_entry: Some(23),
};
let bytes = meta_to_bytes(&meta_old);
let meta_new = meta_from_bytes(bytes);
assert_eq!(meta_new.node, meta_old.node);
assert_eq!(meta_new.file_type, meta_old.file_type);
assert_eq!(meta_new.link_count, meta_old.link_count);
assert_eq!(meta_new.size, meta_old.size);
assert_eq!(meta_new.times, meta_old.times);
assert_eq!(meta_new.chunk_type, None);
}
#[test]
fn size_policy_test() {
use ic_cdk::stable::WASM_PAGE_SIZE_IN_BYTES as PAGE_SIZE;
let p = MountedFileSizePolicy::PreviousOrZero;
assert_eq!(p.get_mounted_file_size(Some(100000), 1), 0);
assert_eq!(p.get_mounted_file_size(Some(100000), 2), 100000);
assert_eq!(p.get_mounted_file_size(None, 2), 0);
let p = MountedFileSizePolicy::PreviousOrMemoryPages;
assert_eq!(p.get_mounted_file_size(Some(100000), 1), PAGE_SIZE);
assert_eq!(p.get_mounted_file_size(Some(100000), 2), 100000);
assert_eq!(p.get_mounted_file_size(None, 2), PAGE_SIZE * 2);
let p = MountedFileSizePolicy::Explicit(3000);
assert_eq!(p.get_mounted_file_size(Some(100000), 1), 3000);
assert_eq!(p.get_mounted_file_size(Some(100000), 2), 3000);
assert_eq!(p.get_mounted_file_size(None, 2), 3000);
let p = MountedFileSizePolicy::MemoryPages;
assert_eq!(p.get_mounted_file_size(Some(100000), 1), PAGE_SIZE);
assert_eq!(p.get_mounted_file_size(Some(100000), 2), PAGE_SIZE * 2);
assert_eq!(p.get_mounted_file_size(None, 2), PAGE_SIZE * 2);
}
}