stable_fs/storage/
types.rs

1use crate::{error::Error, fs::ChunkType};
2use ic_stable_structures::storable::Bound;
3use serde::{Deserialize, Serialize};
4
5pub const FILE_CHUNK_SIZE_V1: usize = 4096;
6
7pub const DEFAULT_FILE_CHUNK_SIZE_V2: usize = 16384;
8pub const MAX_FILE_CHUNK_SIZE_V2: usize = 65536;
9
10pub const MAX_FILE_NAME: usize = 255;
11
12// maximum chunk index. (reserve last 10 chunks for custom needs)
13pub const MAX_FILE_CHUNK_COUNT: u32 = u32::MAX - 10;
14
15// maximum file size supported by the file system.
16pub const MAX_FILE_SIZE: u64 = (MAX_FILE_CHUNK_COUNT as u64) * FILE_CHUNK_SIZE_V1 as u64;
17
18// The unique identifier of a node, which can be a file or a directory.
19// Also known as inode in WASI and other file systems.
20pub type Node = u64;
21
22// An integer type for representing file sizes and offsets.
23pub type FileSize = u64;
24
25// An index of a file chunk.
26pub type FileChunkIndex = u32;
27
28// The address in memory where the V2 chunk is stored.
29pub type FileChunkPtr = u64;
30
31// An array filled with 0 used to fill memory with 0 via copy.
32pub static ZEROES: [u8; MAX_FILE_CHUNK_SIZE_V2] = [0u8; MAX_FILE_CHUNK_SIZE_V2];
33
34// A handle used for writing files in chunks.
35#[derive(Debug, PartialEq, Eq)]
36pub(crate) struct ChunkHandle {
37    pub index: FileChunkIndex,
38    pub offset: FileSize,
39    pub len: FileSize,
40}
41
42// A file consists of multiple file chunks.
43#[derive(Clone, Debug, PartialEq)]
44pub struct FileChunk {
45    pub bytes: [u8; FILE_CHUNK_SIZE_V1],
46}
47
48impl Default for FileChunk {
49    fn default() -> Self {
50        Self {
51            bytes: [0; FILE_CHUNK_SIZE_V1],
52        }
53    }
54}
55
56impl ic_stable_structures::Storable for FileChunk {
57    fn to_bytes(&self) -> std::borrow::Cow<[u8]> {
58        std::borrow::Cow::Borrowed(&self.bytes)
59    }
60
61    fn from_bytes(bytes: std::borrow::Cow<[u8]>) -> Self {
62        Self {
63            bytes: bytes.as_ref().try_into().unwrap(),
64        }
65    }
66
67    const BOUND: Bound = Bound::Bounded {
68        max_size: FILE_CHUNK_SIZE_V1 as u32,
69        is_fixed_size: true,
70    };
71}
72
73#[derive(Clone, Debug, Default, Serialize, Deserialize)]
74pub struct Header {
75    pub version: u32,
76    pub next_node: Node,
77}
78
79impl ic_stable_structures::Storable for Header {
80    fn to_bytes(&self) -> std::borrow::Cow<[u8]> {
81        let mut buf = vec![];
82        ciborium::ser::into_writer(&self, &mut buf).unwrap();
83        std::borrow::Cow::Owned(buf)
84    }
85
86    fn from_bytes(bytes: std::borrow::Cow<[u8]>) -> Self {
87        ciborium::de::from_reader(bytes.as_ref()).unwrap()
88    }
89
90    const BOUND: Bound = Bound::Unbounded;
91}
92
93#[repr(C, align(8))]
94#[derive(Clone, Debug, Default, Serialize, Deserialize, PartialEq)]
95pub struct Metadata {
96    pub node: Node,
97    pub file_type: FileType,
98    pub link_count: u64,
99    pub size: FileSize,
100    pub times: Times,
101    pub first_dir_entry: Option<DirEntryIndex>,
102    pub last_dir_entry: Option<DirEntryIndex>,
103    pub chunk_type: Option<ChunkType>,
104    pub maximum_size_allowed: Option<FileSize>,
105}
106
107impl ic_stable_structures::Storable for Metadata {
108    fn to_bytes(&self) -> std::borrow::Cow<[u8]> {
109        let mut buf = vec![];
110        ciborium::ser::into_writer(&self, &mut buf).unwrap();
111        std::borrow::Cow::Owned(buf)
112    }
113
114    fn from_bytes(bytes: std::borrow::Cow<[u8]>) -> Self {
115        ciborium::de::from_reader(bytes.as_ref()).unwrap()
116    }
117
118    const BOUND: Bound = Bound::Unbounded;
119}
120
121// The type of a node.
122#[derive(Clone, Copy, Default, Debug, PartialEq, Eq, Serialize, Deserialize)]
123pub enum FileType {
124    Directory = 3,
125    #[default]
126    RegularFile = 4,
127    SymbolicLink = 7,
128}
129
130impl TryFrom<u8> for FileType {
131    type Error = Error;
132
133    fn try_from(value: u8) -> Result<Self, Self::Error> {
134        match value {
135            3 => Ok(FileType::Directory),
136            4 => Ok(FileType::RegularFile),
137            7 => Ok(FileType::SymbolicLink),
138            _ => Err(Error::InvalidArgument),
139        }
140    }
141}
142
143impl From<FileType> for u8 {
144    fn from(val: FileType) -> Self {
145        match val {
146            FileType::Directory => 3,
147            FileType::RegularFile => 4,
148            FileType::SymbolicLink => 7,
149        }
150    }
151}
152
153// The time stats of a node.
154#[derive(Clone, Copy, Debug, Default, Serialize, Deserialize, PartialEq)]
155pub struct Times {
156    pub accessed: u64,
157    pub modified: u64,
158    pub created: u64,
159}
160
161// The name of a file or a directory. Most operating systems limit the max file
162// name length to 255.
163#[derive(Clone, Debug, Serialize, Deserialize)]
164pub struct FileName {
165    pub length: u8,
166    #[serde(
167        deserialize_with = "deserialize_file_name",
168        serialize_with = "serialize_file_name"
169    )]
170    pub bytes: [u8; MAX_FILE_NAME],
171}
172
173fn serialize_file_name<S>(bytes: &[u8; MAX_FILE_NAME], serializer: S) -> Result<S::Ok, S::Error>
174where
175    S: serde::Serializer,
176{
177    serde_bytes::Bytes::new(bytes).serialize(serializer)
178}
179
180fn deserialize_file_name<'de, D>(deserializer: D) -> Result<[u8; MAX_FILE_NAME], D::Error>
181where
182    D: serde::Deserializer<'de>,
183{
184    let bytes: Vec<u8> = serde_bytes::deserialize(deserializer).unwrap();
185    let len = bytes.len();
186    let bytes_array: [u8; MAX_FILE_NAME] = bytes
187        .try_into()
188        .map_err(|_| serde::de::Error::invalid_length(len, &"expected MAX_FILE_NAME bytes"))?;
189    Ok(bytes_array)
190}
191
192impl Default for FileName {
193    fn default() -> Self {
194        Self {
195            length: 0,
196            bytes: [0; MAX_FILE_NAME],
197        }
198    }
199}
200
201impl FileName {
202    pub fn new(name: &[u8]) -> Result<Self, Error> {
203        let len = name.len();
204        if len > MAX_FILE_NAME {
205            return Err(Error::FilenameTooLong);
206        }
207        let mut bytes = [0; MAX_FILE_NAME];
208        bytes[0..len].copy_from_slice(name);
209        Ok(Self {
210            length: len as u8,
211            bytes,
212        })
213    }
214}
215
216// An index of a directory entry.
217pub type DirEntryIndex = u32;
218
219// A directory contains a list of directory entries.
220// Each entry describes a name of a file or a directory.
221#[derive(Clone, Debug, Default, Serialize, Deserialize)]
222pub struct DirEntry {
223    pub name: FileName,
224    pub node: Node,
225    pub next_entry: Option<DirEntryIndex>,
226    pub prev_entry: Option<DirEntryIndex>,
227}
228
229impl ic_stable_structures::Storable for DirEntry {
230    fn to_bytes(&self) -> std::borrow::Cow<[u8]> {
231        let mut buf = vec![];
232        ciborium::ser::into_writer(&self, &mut buf).unwrap();
233        std::borrow::Cow::Owned(buf)
234    }
235
236    fn from_bytes(bytes: std::borrow::Cow<[u8]>) -> Self {
237        ciborium::de::from_reader(bytes.as_ref()).unwrap()
238    }
239
240    const BOUND: ic_stable_structures::storable::Bound = Bound::Unbounded;
241}
242
243#[cfg(test)]
244mod tests {
245    use crate::fs::ChunkType;
246
247    use super::{DirEntryIndex, FileSize, FileType, Node, Times};
248    use serde::{Deserialize, Serialize};
249
250    // Old node structure.
251    #[derive(Clone, Debug, Default, Serialize, Deserialize, PartialEq)]
252    pub struct MetadataOld {
253        pub node: Node,
254        pub file_type: FileType,
255        pub link_count: u64,
256        pub size: FileSize,
257        pub times: Times,
258        pub first_dir_entry: Option<DirEntryIndex>,
259        pub last_dir_entry: Option<DirEntryIndex>,
260    }
261
262    // New node structure.
263    #[derive(Clone, Debug, Default, Serialize, Deserialize, PartialEq)]
264    pub struct MetadataNew {
265        pub node: Node,
266        pub file_type: FileType,
267        pub link_count: u64,
268        pub size: FileSize,
269        pub times: Times,
270        pub first_dir_entry: Option<DirEntryIndex>,
271        pub last_dir_entry: Option<DirEntryIndex>,
272        pub chunk_type: Option<ChunkType>,
273    }
274
275    fn meta_to_bytes(meta: &MetadataOld) -> std::borrow::Cow<[u8]> {
276        let mut buf = vec![];
277        ciborium::ser::into_writer(meta, &mut buf).unwrap();
278        std::borrow::Cow::Owned(buf)
279    }
280
281    fn meta_from_bytes(bytes: std::borrow::Cow<[u8]>) -> MetadataNew {
282        ciborium::de::from_reader(bytes.as_ref()).unwrap()
283    }
284
285    #[test]
286    fn store_old_load_new() {
287        let meta_old = MetadataOld {
288            node: 23,
289            file_type: FileType::RegularFile,
290            link_count: 3,
291            size: 123,
292            times: Times::default(),
293            first_dir_entry: Some(23),
294            last_dir_entry: Some(35),
295        };
296
297        let bytes = meta_to_bytes(&meta_old);
298
299        let meta_new = meta_from_bytes(bytes);
300
301        assert_eq!(meta_new.node, meta_old.node);
302        assert_eq!(meta_new.file_type, meta_old.file_type);
303        assert_eq!(meta_new.link_count, meta_old.link_count);
304        assert_eq!(meta_new.size, meta_old.size);
305        assert_eq!(meta_new.times, meta_old.times);
306        assert_eq!(meta_new.first_dir_entry, meta_old.first_dir_entry);
307        assert_eq!(meta_new.last_dir_entry, meta_old.last_dir_entry);
308        assert_eq!(meta_new.chunk_type, None);
309    }
310
311    #[test]
312    fn store_old_load_new_both_none() {
313        let meta_old = MetadataOld {
314            node: 23,
315            file_type: FileType::RegularFile,
316            link_count: 3,
317            size: 123,
318            times: Times::default(),
319            first_dir_entry: None,
320            last_dir_entry: None,
321        };
322
323        let bytes = meta_to_bytes(&meta_old);
324
325        let meta_new = meta_from_bytes(bytes);
326
327        assert_eq!(meta_new.node, meta_old.node);
328        assert_eq!(meta_new.file_type, meta_old.file_type);
329        assert_eq!(meta_new.link_count, meta_old.link_count);
330        assert_eq!(meta_new.size, meta_old.size);
331        assert_eq!(meta_new.times, meta_old.times);
332        assert_eq!(meta_new.first_dir_entry, meta_old.first_dir_entry);
333        assert_eq!(meta_new.last_dir_entry, meta_old.last_dir_entry);
334        assert_eq!(meta_new.chunk_type, None);
335    }
336
337    #[test]
338    fn store_old_load_new_first_none() {
339        let meta_old = MetadataOld {
340            node: 23,
341            file_type: FileType::RegularFile,
342            link_count: 3,
343            size: 123,
344            times: Times::default(),
345            first_dir_entry: None,
346            last_dir_entry: Some(23),
347        };
348
349        let bytes = meta_to_bytes(&meta_old);
350
351        let meta_new = meta_from_bytes(bytes);
352
353        assert_eq!(meta_new.node, meta_old.node);
354        assert_eq!(meta_new.file_type, meta_old.file_type);
355        assert_eq!(meta_new.link_count, meta_old.link_count);
356        assert_eq!(meta_new.size, meta_old.size);
357        assert_eq!(meta_new.times, meta_old.times);
358        assert_eq!(meta_new.first_dir_entry, meta_old.first_dir_entry);
359        assert_eq!(meta_new.last_dir_entry, meta_old.last_dir_entry);
360        assert_eq!(meta_new.chunk_type, None);
361    }
362}