ext4_metadata/
lib.rs

1// Copyright 2023 The Fuchsia Authors. All rights reserved.
2// Use of this source code is governed by a BSD-style license that can be
3// found in the LICENSE file.
4
5use serde::{Deserialize, Serialize};
6use std::collections::BTreeMap;
7use thiserror::Error;
8
9// This is deliberately the same as ext4.
10pub const ROOT_INODE_NUM: u64 = 2;
11
12pub const S_IFDIR: u16 = 0x4000;
13pub const S_IFREG: u16 = 0x8000;
14pub const S_IFLNK: u16 = 0xa000;
15pub const S_IFMT: u16 = 0xf000;
16
17#[derive(Error, Debug)]
18pub enum MetadataError {
19    #[error("Node not found")]
20    NotFound,
21    #[error("Node is not a directory")]
22    NotDir,
23    #[error("Failed to deserialize metadata (corrupt?)")]
24    FailedToDeserialize,
25}
26
27#[derive(Serialize, Deserialize)]
28pub struct Metadata {
29    nodes: BTreeMap<u64, Node>,
30}
31
32impl Metadata {
33    pub fn new() -> Self {
34        Self { nodes: BTreeMap::new() }
35    }
36
37    /// Finds the node with name `name` in directory with inode number `parent`.
38    pub fn lookup(&self, parent: u64, name: &str) -> Result<u64, MetadataError> {
39        self.nodes
40            .get(&parent)
41            .ok_or(MetadataError::NotFound)?
42            .directory()
43            .ok_or(MetadataError::NotDir)?
44            .children
45            .get(name)
46            .ok_or(MetadataError::NotFound)
47            .cloned()
48    }
49
50    /// Returns the node with inode number `inode_num`.
51    pub fn get(&self, inode_num: u64) -> Option<&Node> {
52        self.nodes.get(&inode_num)
53    }
54
55    /// Deserializes the metadata.
56    pub fn deserialize(bytes: &[u8]) -> Result<Self, MetadataError> {
57        bincode::deserialize(bytes).map_err(|_| MetadataError::FailedToDeserialize)
58    }
59
60    /// Serializes the metadata.
61    pub fn serialize(&self) -> Vec<u8> {
62        bincode::serialize(&self).unwrap()
63    }
64
65    /// Add a child at `path` with inode number `inode_num`.
66    pub fn add_child(&mut self, path: &[&str], inode_num: u64) {
67        for component in path {
68            assert!(
69                *component != "."
70                    && *component != ".."
71                    && !component.contains(|c| c == '/' || c == '\0'),
72                "Invalid path component {component:?} in {path:?}"
73            );
74        }
75
76        let mut node = self.nodes.get_mut(&ROOT_INODE_NUM).unwrap().directory_mut().unwrap();
77        let mut iter = path.iter();
78        let name = iter.next_back().unwrap();
79        for component in iter {
80            let inode_num = *node.children.get_mut(*component).unwrap();
81            node = self.nodes.get_mut(&inode_num).unwrap().directory_mut().unwrap();
82        }
83        node.children.insert(name.to_string(), inode_num);
84    }
85
86    /// Inserts a directory node.  This will not add a child to a directory; see `add_child`.
87    pub fn insert_directory(
88        &mut self,
89        inode_num: u64,
90        mode: u16,
91        uid: u16,
92        gid: u16,
93        extended_attributes: ExtendedAttributes,
94    ) {
95        assert_eq!(mode & S_IFMT, S_IFDIR);
96        self.nodes.insert(
97            inode_num,
98            Node {
99                info: NodeInfo::Directory(Directory { children: BTreeMap::new() }),
100                mode,
101                uid,
102                gid,
103                extended_attributes,
104            },
105        );
106    }
107
108    /// Inserts a file node.  This will not add a child to a directory; see `add_child`.
109    pub fn insert_file(
110        &mut self,
111        inode_num: u64,
112        mode: u16,
113        uid: u16,
114        gid: u16,
115        extended_attributes: ExtendedAttributes,
116    ) {
117        assert_eq!(mode & S_IFMT, S_IFREG);
118        self.nodes.insert(
119            inode_num,
120            Node { info: NodeInfo::File(File), mode, uid, gid, extended_attributes },
121        );
122    }
123
124    /// Inserts a symlink node.  This will not add a child to a directory; see `add_child`.
125    pub fn insert_symlink(
126        &mut self,
127        inode_num: u64,
128        target: String,
129        mode: u16,
130        uid: u16,
131        gid: u16,
132        extended_attributes: ExtendedAttributes,
133    ) {
134        assert_eq!(mode & S_IFMT, S_IFLNK);
135        self.nodes.insert(
136            inode_num,
137            Node {
138                info: NodeInfo::Symlink(Symlink { target }),
139                mode,
140                uid,
141                gid,
142                extended_attributes,
143            },
144        );
145    }
146}
147
148pub type ExtendedAttributes = BTreeMap<Box<[u8]>, Box<[u8]>>;
149
150#[derive(Serialize, Deserialize)]
151pub struct Node {
152    info: NodeInfo,
153    pub mode: u16,
154    pub uid: u16,
155    pub gid: u16,
156    pub extended_attributes: ExtendedAttributes,
157}
158
159impl Node {
160    pub fn info(&self) -> &NodeInfo {
161        &self.info
162    }
163
164    pub fn directory(&self) -> Option<&Directory> {
165        match &self.info {
166            NodeInfo::Directory(d) => Some(d),
167            _ => None,
168        }
169    }
170
171    pub fn directory_mut(&mut self) -> Option<&mut Directory> {
172        match &mut self.info {
173            NodeInfo::Directory(d) => Some(d),
174            _ => None,
175        }
176    }
177
178    pub fn file(&self) -> Option<&File> {
179        match &self.info {
180            NodeInfo::File(f) => Some(f),
181            _ => None,
182        }
183    }
184
185    pub fn symlink(&self) -> Option<&Symlink> {
186        match &self.info {
187            NodeInfo::Symlink(s) => Some(s),
188            _ => None,
189        }
190    }
191}
192
193#[derive(Debug, Serialize, Deserialize)]
194pub enum NodeInfo {
195    Directory(Directory),
196    File(File),
197    Symlink(Symlink),
198}
199
200#[derive(Debug, Serialize, Deserialize)]
201pub struct Directory {
202    pub children: BTreeMap<String, u64>,
203}
204
205#[derive(Debug, Serialize, Deserialize)]
206pub struct File;
207
208#[derive(Debug, Serialize, Deserialize)]
209pub struct Symlink {
210    pub target: String,
211}
212
213#[cfg(test)]
214mod tests {
215    use super::{Metadata, NodeInfo, ROOT_INODE_NUM, S_IFDIR, S_IFLNK, S_IFREG};
216    use assert_matches::assert_matches;
217    use std::collections::BTreeMap;
218
219    #[test]
220    fn test_serialize_and_deserialize() {
221        let mut m = Metadata::new();
222        let xattr: BTreeMap<_, _> =
223            [((*b"a").into(), (*b"apple").into()), ((*b"b").into(), (*b"ball").into())].into();
224        m.insert_directory(ROOT_INODE_NUM, S_IFDIR | 0o755, 2, 3, Default::default());
225        m.insert_directory(3, S_IFDIR | 0o775, 2, 3, xattr.clone());
226        m.add_child(&["foo"], 3);
227        m.insert_file(4, S_IFREG | 0o644, 2, 3, xattr.clone());
228        m.add_child(&["foo", "bar"], 4);
229        m.insert_symlink(5, "symlink-target".to_string(), S_IFLNK | 0o777, 2, 3, xattr.clone());
230        m.add_child(&["foo", "baz"], 5);
231
232        let m = Metadata::deserialize(&m.serialize()).expect("deserialize failed");
233        let node = m.get(ROOT_INODE_NUM).expect("root not found");
234        assert_matches!(node.info(), NodeInfo::Directory(_));
235        assert_eq!(node.mode, S_IFDIR | 0o755);
236        assert_eq!(node.uid, 2);
237        assert_eq!(node.gid, 3);
238        assert_eq!(node.extended_attributes, [].into());
239
240        assert_eq!(m.lookup(ROOT_INODE_NUM, "foo").expect("foo not found"), 3);
241        let node = m.get(3).expect("root not found");
242        assert_matches!(node.info(), NodeInfo::Directory(_));
243        assert_eq!(node.mode, S_IFDIR | 0o775);
244        assert_eq!(node.uid, 2);
245        assert_eq!(node.gid, 3);
246        assert_eq!(&node.extended_attributes, &xattr);
247
248        assert_eq!(m.lookup(3, "bar").expect("foo/bar not found"), 4);
249        let node = m.get(4).expect("root not found");
250        assert_matches!(node.info(), NodeInfo::File(_));
251        assert_eq!(node.mode, S_IFREG | 0o644);
252        assert_eq!(node.uid, 2);
253        assert_eq!(node.gid, 3);
254        assert_eq!(&node.extended_attributes, &xattr);
255
256        assert_eq!(m.lookup(3, "baz").expect("foo/baz not found"), 5);
257        let node = m.get(5).expect("root not found");
258        assert_matches!(node.info(), NodeInfo::Symlink(_));
259        assert_eq!(node.mode, S_IFLNK | 0o777);
260        assert_eq!(node.uid, 2);
261        assert_eq!(node.gid, 3);
262        assert_eq!(&node.extended_attributes, &xattr);
263    }
264
265    #[test]
266    fn test_serialization_is_deterministic() {
267        // Builds a Metadata instance with fixed contents.
268        let build_fs = || {
269            let mut m = Metadata::new();
270            let xattr: BTreeMap<_, _> =
271                [((*b"a").into(), (*b"apple").into()), ((*b"b").into(), (*b"ball").into())].into();
272            m.insert_directory(ROOT_INODE_NUM, S_IFDIR | 0o755, 2, 3, Default::default());
273            m.insert_directory(3, S_IFDIR | 0o775, 2, 3, xattr.clone());
274            m.add_child(&["foo"], 3);
275            m.insert_file(4, S_IFREG | 0o644, 2, 3, xattr.clone());
276            m.add_child(&["foo", "bar"], 4);
277            m.insert_symlink(5, "symlink-target".to_string(), S_IFLNK | 0o777, 2, 3, xattr.clone());
278            m.add_child(&["foo", "baz"], 5);
279            m
280        };
281
282        // Build it twice and verify that the serialized representations match.
283        let data1 = build_fs().serialize();
284        let data2 = build_fs().serialize();
285        assert_eq!(data1, data2);
286    }
287}