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