Skip to main content

blackout_target/
static_tree.rs

1// Copyright 2019 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
5//! Generate a random directory tree that can be commited to disk all at once. The general formula
6//! goes like this -
7//!
8//! ```
9//! use {rand::{Rng, SeedableRng as _, rngs::StdRng}, static_tree::{EntryDistribution, DirectoryEntry}};
10//! let mut rng = StdRng::seed_from_u64(seed);
11//! let dist = EntryDistribution::new(depth);
12//! let tree: DirectoryEntry = rng.sample(&dist);
13//! tree.write_tree_at(root).expect("failed to write tree");
14//! ```
15
16use anyhow::Error;
17use fidl_fuchsia_io as fio;
18use rand::distr::{Bernoulli, Distribution, StandardUniform};
19use rand::{Rng, RngExt as _};
20
21/// A random distribution specialized to generation of random directory trees. This distribution
22/// decreases the likelyhood of a directory being generated linearly relative to the depth of the
23/// subset of the tree being generated, until it's 0% at the maximum depth.
24#[derive(Clone, Copy, Debug, Eq, PartialEq)]
25pub struct EntryDistribution {
26    depth: u32,
27    max_depth: u32,
28}
29
30impl EntryDistribution {
31    /// Create a new `EntryDistribution` with a maximum depth of `max_depth`. This distribution is
32    /// used for generating [`DirectoryEntry`]s and [`Entry`]s.
33    pub fn new(max_depth: u32) -> EntryDistribution {
34        EntryDistribution { depth: 1, max_depth }
35    }
36
37    /// get the entry distribution for the next level down on the directory tree.
38    fn next_level(&self) -> EntryDistribution {
39        debug_assert!(
40            self.depth <= self.max_depth,
41            "directory tree exceeded max depth ({} vs max of {}). programming error.",
42            self.depth,
43            self.max_depth
44        );
45        EntryDistribution { depth: self.depth + 1, max_depth: self.max_depth }
46    }
47
48    /// creates a bernoulli distribution where the likelyhood is progressively decreased at further
49    /// depths until it's 0% at max_depth.
50    fn directory_distribution(&self) -> Bernoulli {
51        Bernoulli::from_ratio(self.max_depth - self.depth, self.max_depth).unwrap()
52    }
53}
54
55/// A file entry in the generated tree. Contains a randomly generated u64 for a name and randomly
56/// generated byte contents. The file size is random, in a range of 1 byte to 2^16 bytes (~65kb).
57#[derive(Debug, Clone, Eq, PartialEq)]
58pub struct FileEntry {
59    name: u64,
60    contents: Vec<u8>,
61}
62
63impl Distribution<FileEntry> for StandardUniform {
64    fn sample<R: Rng + ?Sized>(&self, rng: &mut R) -> FileEntry {
65        let size = usize::from(rng.random::<u16>());
66        // unfortunately, we can't use sample_iter to generate the content here. the trait
67        // definition for distribution requires the Rng type parameter to have ?Sized (or "maybe
68        // sized"), but the sample_iter function requires the provided Rng be Sized, either through
69        // the explicit Self: Sized on the Rng sample_iter function, or the implicit Sized present
70        // on type parameters for the equivalent Distribution function.
71        let mut contents = vec![0; size];
72        rng.fill(contents.as_mut_slice());
73        FileEntry { name: rng.random(), contents }
74    }
75}
76
77impl FileEntry {
78    async fn write_file_at(self, root: &fio::DirectoryProxy) -> Result<(), Error> {
79        let file = fuchsia_fs::directory::open_file(
80            root,
81            &self.name.to_string(),
82            fio::Flags::FLAG_MAYBE_CREATE | fio::PERM_WRITABLE,
83        )
84        .await?;
85        fuchsia_fs::file::write(&file, &self.contents).await?;
86        Ok(())
87    }
88}
89
90/// A directory entry in the generated tree. Contains a randomly generated u64 for a name and a
91/// vector of randomly generated directory entries, with a number of entries somewhere in the range
92/// of [0, 6). The sample function generates the entire subtree depth-first before returning, and is
93/// mutually recursive with the [`Entry`] sample function.
94#[derive(Clone, Debug, Eq, PartialEq)]
95pub struct DirectoryEntry {
96    name: u64,
97    entries: Vec<Entry>,
98}
99
100impl Distribution<DirectoryEntry> for EntryDistribution {
101    fn sample<R: Rng + ?Sized>(&self, rng: &mut R) -> DirectoryEntry {
102        // each directory has a random number of entries in the range [0, 6)
103        let num_entries = rng.random_range(0..6);
104        let mut entries = vec![];
105        let entry_dist = self.next_level();
106        for _ in 0..num_entries {
107            entries.push(rng.sample(&entry_dist));
108        }
109        DirectoryEntry { name: rng.random(), entries }
110    }
111}
112
113impl DirectoryEntry {
114    /// get the path the directory entry will be written to relative to a given root.
115    pub fn get_name(&self) -> String {
116        self.name.to_string()
117    }
118
119    /// Take the entire randomly generated tree and commit it to disk.
120    pub fn write_tree_at<'a>(
121        self,
122        root: &'a fio::DirectoryProxy,
123    ) -> futures::future::BoxFuture<'a, Result<(), Error>> {
124        Box::pin(async {
125            let this = fuchsia_fs::directory::create_directory(
126                root,
127                &self.get_name(),
128                fio::PERM_READABLE | fio::PERM_WRITABLE,
129            )
130            .await?;
131            for entry in self.entries {
132                match entry {
133                    Entry::File(file_entry) => file_entry.write_file_at(&this).await?,
134                    Entry::Directory(dir_entry) => dir_entry.write_tree_at(&this).await?,
135                }
136            }
137            Ok(())
138        })
139    }
140}
141
142/// An entry of either File or Directory. The chance of it being a directory is relative to the depth
143/// recorded in EntryDistribution. The associated entry is also then generated.
144#[derive(Clone, Debug, Eq, PartialEq)]
145pub enum Entry {
146    /// This entry is a file.
147    File(FileEntry),
148    /// This entry is a directory
149    Directory(DirectoryEntry),
150}
151
152impl Distribution<Entry> for EntryDistribution {
153    fn sample<R: Rng + ?Sized>(&self, rng: &mut R) -> Entry {
154        if rng.sample(self.directory_distribution()) {
155            Entry::Directory(rng.sample(self))
156        } else {
157            Entry::File(rng.sample(StandardUniform))
158        }
159    }
160}
161
162#[cfg(test)]
163mod tests {
164    use super::{DirectoryEntry, Entry, EntryDistribution, FileEntry};
165    use fs_management::Minfs;
166    use ramdevice_client::RamdiskClient;
167    use rand::RngExt as _;
168
169    struct StepRng {
170        state: u64,
171        increment: u64,
172    }
173
174    impl StepRng {
175        pub fn new(initial: u64, increment: u64) -> Self {
176            Self { state: initial, increment: increment }
177        }
178    }
179
180    impl rand::TryRng for StepRng {
181        type Error = core::convert::Infallible;
182
183        fn try_next_u32(&mut self) -> Result<u32, Self::Error> {
184            Ok(self.try_next_u64()? as u32)
185        }
186
187        fn try_next_u64(&mut self) -> Result<u64, Self::Error> {
188            let r = self.state;
189            self.state = self.state.wrapping_add(self.increment);
190            Ok(r)
191        }
192
193        fn try_fill_bytes(&mut self, dst: &mut [u8]) -> Result<(), Self::Error> {
194            rand::rand_core::utils::fill_bytes_via_next_word(dst, || self.try_next_u64())
195        }
196    }
197
198    // this fixture will have to get updated any time the generation logic changes. depending on the
199    // nature of the change, the comparison logic may have to be changed as well.
200    fn get_fixture(initial: u64, increment: u64, depth: u32) -> DirectoryEntry {
201        // confirm that the rng and depth used to generate this tree are the same.
202        assert_eq!(initial, 0xFFFF0000, "initial value changed - update fixture");
203        assert_eq!(increment, 1, "increment value changed - update fixture");
204        assert_eq!(depth, 2, "depth value changed - update fixture");
205        DirectoryEntry {
206            name: 4294901785,
207            entries: vec![
208                Entry::File(FileEntry { name: 4294901764, contents: vec![3, 0] }),
209                Entry::File(FileEntry { name: 4294901768, contents: vec![7, 0, 255, 255, 0, 0] }),
210                Entry::File(FileEntry {
211                    name: 4294901773,
212                    contents: vec![11, 0, 255, 255, 0, 0, 0, 0, 12, 0],
213                }),
214                Entry::File(FileEntry {
215                    name: 4294901778,
216                    contents: vec![16, 0, 255, 255, 0, 0, 0, 0, 17, 0, 255, 255, 0, 0, 0],
217                }),
218                Entry::File(FileEntry {
219                    name: 4294901784,
220                    contents: vec![
221                        21, 0, 255, 255, 0, 0, 0, 0, 22, 0, 255, 255, 0, 0, 0, 0, 23, 0, 255, 255,
222                    ],
223                }),
224            ],
225        }
226    }
227
228    #[test]
229    fn gen_tree() {
230        let initial = 0xFFFF0000;
231        let increment = 1;
232        let depth = 2;
233        // make sure we are generating a tree at all.
234        let mut rng = StepRng::new(initial, increment);
235        let dist = EntryDistribution::new(depth);
236        let tree: DirectoryEntry = rng.sample(dist);
237        // this skips the contents for the files, because they are huge, so we do our own manual
238        // comparison.
239        let fixture = get_fixture(initial, increment, depth);
240        assert_eq!(fixture, tree);
241        for i in 0..fixture.entries.len() {
242            match &fixture.entries[i] {
243                Entry::File(fixture_file_entry) => match &tree.entries[i] {
244                    Entry::File(tree_file_entry) => {
245                        assert_eq!(fixture_file_entry.name, tree_file_entry.name)
246                    }
247                    _ => panic!("expected a file in generated tree"),
248                },
249                _ => panic!("expected a file in fixture tree"),
250            }
251        }
252    }
253
254    #[test]
255    fn same_rng_same_tree() {
256        // this confirms an assumption about the properties of tree generation that we rely on for
257        // consistency checking.
258        let dist = EntryDistribution::new(5);
259
260        let tree1: DirectoryEntry = StepRng::new(1337, 1).sample(dist);
261        let tree2: DirectoryEntry = StepRng::new(1337, 1).sample(dist);
262
263        assert_eq!(tree1, tree2);
264    }
265
266    #[fuchsia::test]
267    async fn write_tree() {
268        let root = "/test-root";
269        let initial = 0xFFFF0000;
270        let increment = 1;
271        let depth = 2;
272
273        let mut rng = StepRng::new(initial, increment);
274        let dist = EntryDistribution::new(depth);
275        let tree: DirectoryEntry = rng.sample(dist);
276
277        let ramdisk = RamdiskClient::create(512, 1 << 16).await.expect("failed to make ramdisk");
278        let controller = ramdisk.connector().expect("invalid controller");
279        let mut minfs = fs_management::filesystem::Filesystem::from_boxed_config(
280            controller,
281            Box::new(Minfs::default()),
282        );
283
284        minfs.format().await.expect("failed to format minfs");
285        let mut minfs = minfs.serve().await.expect("failed to mount minfs");
286        minfs.bind_to_path(root).expect("failed to bind path");
287
288        tree.write_tree_at(minfs.root()).await.expect("failed to write tree");
289
290        let fixture = get_fixture(initial, increment, depth);
291        let path = std::path::PathBuf::from(format!("{}/{}", root, fixture.name));
292        assert!(path.is_dir(), "{}", path.display());
293        for (i, entry) in std::fs::read_dir(&path).expect("failed to read directory").enumerate() {
294            let entry = entry.expect("failed to read entry");
295            let file_type = entry.file_type().expect("failed to get file type");
296            assert!(file_type.is_file());
297            let file_name =
298                entry.file_name().into_string().expect("failed to convert file name to string");
299            let expected_name = match &fixture.entries[i] {
300                Entry::File(f) => f.name,
301                Entry::Directory(_) => panic!("expected a file in the fixture tree"),
302            };
303            assert_eq!(file_name, expected_name.to_string());
304        }
305
306        minfs.shutdown().await.expect("failed to unmount minfs");
307    }
308}