package_directory/
meta_as_dir.rs

1// Copyright 2021 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 crate::root_dir::RootDir;
6use crate::usize_to_u64_safe;
7use fidl::endpoints::ServerEnd;
8use fidl_fuchsia_io as fio;
9use std::sync::Arc;
10use vfs::common::send_on_open_with_error;
11use vfs::directory::entry::EntryInfo;
12use vfs::directory::immutable::connection::ImmutableConnection;
13use vfs::directory::traversal_position::TraversalPosition;
14use vfs::execution_scope::ExecutionScope;
15use vfs::{ObjectRequestRef, ToObjectRequest as _, immutable_attributes};
16
17pub(crate) struct MetaAsDir<S: crate::NonMetaStorage> {
18    root_dir: Arc<RootDir<S>>,
19}
20
21impl<S: crate::NonMetaStorage> MetaAsDir<S> {
22    pub(crate) fn new(root_dir: Arc<RootDir<S>>) -> Arc<Self> {
23        Arc::new(MetaAsDir { root_dir })
24    }
25}
26
27impl<S: crate::NonMetaStorage> vfs::directory::entry::GetEntryInfo for MetaAsDir<S> {
28    fn entry_info(&self) -> EntryInfo {
29        EntryInfo::new(fio::INO_UNKNOWN, fio::DirentType::Directory)
30    }
31}
32
33impl<S: crate::NonMetaStorage> vfs::node::Node for MetaAsDir<S> {
34    async fn get_attributes(
35        &self,
36        requested_attributes: fio::NodeAttributesQuery,
37    ) -> Result<fio::NodeAttributes2, zx::Status> {
38        Ok(immutable_attributes!(
39            requested_attributes,
40            Immutable {
41                protocols: fio::NodeProtocolKinds::DIRECTORY,
42                abilities: crate::DIRECTORY_ABILITIES,
43                content_size: usize_to_u64_safe(self.root_dir.meta_files.len()),
44                storage_size: usize_to_u64_safe(self.root_dir.meta_files.len()),
45                id: 1,
46            }
47        ))
48    }
49}
50
51impl<S: crate::NonMetaStorage> vfs::directory::entry_container::Directory for MetaAsDir<S> {
52    fn deprecated_open(
53        self: Arc<Self>,
54        scope: ExecutionScope,
55        flags: fio::OpenFlags,
56        path: vfs::Path,
57        server_end: ServerEnd<fio::NodeMarker>,
58    ) {
59        let flags = flags & !(fio::OpenFlags::POSIX_WRITABLE | fio::OpenFlags::POSIX_EXECUTABLE);
60        let describe = flags.contains(fio::OpenFlags::DESCRIBE);
61        // Disallow creating a writable or executable connection to this node or any children. We
62        // also disallow file flags which do not apply. Note that the latter is not required for
63        // Open3, as we require writable rights for the latter flags already.
64        if flags.intersects(
65            fio::OpenFlags::RIGHT_WRITABLE
66                | fio::OpenFlags::RIGHT_EXECUTABLE
67                | fio::OpenFlags::TRUNCATE,
68        ) {
69            let () = send_on_open_with_error(describe, server_end, zx::Status::NOT_SUPPORTED);
70            return;
71        }
72        // The VFS should disallow file creation since we cannot serve a mutable connection.
73        assert!(!flags.intersects(fio::OpenFlags::CREATE | fio::OpenFlags::CREATE_IF_ABSENT));
74
75        if path.is_empty() {
76            flags.to_object_request(server_end).handle(|object_request| {
77                // NOTE: Some older CTF tests still rely on being able to use the APPEND flag in
78                // some cases, so we cannot check this flag above. Appending is still not possible.
79                // As we plan to remove this method entirely, we can just leave this for now.
80                if flags.intersects(fio::OpenFlags::APPEND) {
81                    return Err(zx::Status::NOT_SUPPORTED);
82                }
83
84                // Only MetaAsDir can be obtained from Open calls to MetaAsDir. To obtain the "meta"
85                // file, the Open call must be made on RootDir. This is consistent with pkgfs
86                // behavior and is needed so that Clone'ing MetaAsDir results in MetaAsDir, because
87                // VFS handles Clone by calling Open with a path of ".", a mode of 0, and mostly
88                // unmodified flags and that combination of arguments would normally result in the
89                // file being used.
90                object_request
91                    .take()
92                    .create_connection_sync::<ImmutableConnection<_>, _>(scope, self, flags);
93                Ok(())
94            });
95            return;
96        }
97
98        // <path as vfs::path::Path>::as_str() is an object relative path expression [1], except
99        // that it may:
100        //   1. have a trailing "/"
101        //   2. be exactly "."
102        //   3. be longer than 4,095 bytes
103        // The .is_empty() check above rules out "." and the following line removes the possible
104        // trailing "/".
105        // [1] https://fuchsia.dev/fuchsia-src/concepts/process/namespaces?hl=en#object_relative_path_expressions
106        let file_path =
107            format!("meta/{}", path.as_ref().strip_suffix('/').unwrap_or_else(|| path.as_ref()));
108
109        match self.root_dir.get_meta_file(&file_path) {
110            Ok(Some(meta_file)) => {
111                flags.to_object_request(server_end).handle(|object_request| {
112                    vfs::file::serve(meta_file, scope, &flags, object_request)
113                });
114                return;
115            }
116            Ok(None) => {}
117            Err(status) => {
118                let () = send_on_open_with_error(describe, server_end, status);
119                return;
120            }
121        }
122
123        if let Some(subdir) = self.root_dir.get_meta_subdir(file_path + "/") {
124            let () = subdir.deprecated_open(scope, flags, vfs::Path::dot(), server_end);
125            return;
126        }
127
128        let () = send_on_open_with_error(describe, server_end, zx::Status::NOT_FOUND);
129    }
130
131    fn open(
132        self: Arc<Self>,
133        scope: ExecutionScope,
134        path: vfs::Path,
135        flags: fio::Flags,
136        object_request: ObjectRequestRef<'_>,
137    ) -> Result<(), zx::Status> {
138        if !flags.difference(crate::ALLOWED_FLAGS).is_empty() {
139            return Err(zx::Status::NOT_SUPPORTED);
140        }
141        // Disallow creating an executable connection to this node or any children.
142        if flags.contains(fio::Flags::PERM_EXECUTE) {
143            return Err(zx::Status::NOT_SUPPORTED);
144        }
145
146        // Handle case where the request is for this directory itself (e.g. ".").
147        if path.is_empty() {
148            // Only MetaAsDir can be obtained from Open calls to MetaAsDir. To obtain the "meta"
149            // file, the Open call must be made on RootDir. This is consistent with pkgfs behavior
150            // and is needed so that Clone'ing MetaAsDir results in MetaAsDir, because VFS handles
151            // Clone by calling Open with a path of ".", a mode of 0, and mostly unmodified flags
152            // and that combination of arguments would normally result in the file being used.
153            //
154            // `ImmutableConnection` will check flags contain only directory-allowed flags.
155            object_request
156                .take()
157                .create_connection_sync::<ImmutableConnection<_>, _>(scope, self, flags);
158            return Ok(());
159        }
160
161        // `path` is relative, and may include a trailing slash.
162        let file_path =
163            format!("meta/{}", path.as_ref().strip_suffix('/').unwrap_or_else(|| path.as_ref()));
164
165        if let Some(file) = self.root_dir.get_meta_file(&file_path)? {
166            if path.is_dir() {
167                return Err(zx::Status::NOT_DIR);
168            }
169            return vfs::file::serve(file, scope, &flags, object_request);
170        }
171
172        if let Some(subdir) = self.root_dir.get_meta_subdir(file_path + "/") {
173            return subdir.open(scope, vfs::Path::dot(), flags, object_request);
174        }
175
176        Err(zx::Status::NOT_FOUND)
177    }
178
179    async fn read_dirents<'a>(
180        &'a self,
181        pos: &'a TraversalPosition,
182        sink: Box<dyn vfs::directory::dirents_sink::Sink + 'static>,
183    ) -> Result<
184        (TraversalPosition, Box<dyn vfs::directory::dirents_sink::Sealed + 'static>),
185        zx::Status,
186    > {
187        vfs::directory::read_dirents::read_dirents(
188            &crate::get_dir_children(self.root_dir.meta_files.keys().map(|s| s.as_str()), "meta/"),
189            pos,
190            sink,
191        )
192    }
193
194    fn register_watcher(
195        self: Arc<Self>,
196        _: ExecutionScope,
197        _: fio::WatchMask,
198        _: vfs::directory::entry_container::DirectoryWatcher,
199    ) -> Result<(), zx::Status> {
200        Err(zx::Status::NOT_SUPPORTED)
201    }
202
203    // `register_watcher` is unsupported so no need to do anything here.
204    fn unregister_watcher(self: Arc<Self>, _: usize) {}
205}
206
207#[cfg(test)]
208mod tests {
209    use super::*;
210    use assert_matches::assert_matches;
211    use fuchsia_fs::directory::{DirEntry, DirentKind};
212    use fuchsia_pkg_testing::PackageBuilder;
213    use fuchsia_pkg_testing::blobfs::Fake as FakeBlobfs;
214    use futures::TryStreamExt as _;
215
216    struct TestEnv {
217        _blobfs_fake: FakeBlobfs,
218    }
219
220    impl TestEnv {
221        async fn new() -> (Self, fio::DirectoryProxy) {
222            let pkg = PackageBuilder::new("pkg")
223                .add_resource_at("meta/dir/file", &b"contents"[..])
224                .build()
225                .await
226                .unwrap();
227            let (metafar_blob, _) = pkg.contents();
228            let (blobfs_fake, blobfs_client) = FakeBlobfs::new();
229            blobfs_fake.add_blob(metafar_blob.merkle, metafar_blob.contents);
230            let root_dir = RootDir::new(blobfs_client, metafar_blob.merkle).await.unwrap();
231            let meta_as_dir = MetaAsDir::new(root_dir);
232            (Self { _blobfs_fake: blobfs_fake }, vfs::directory::serve_read_only(meta_as_dir))
233        }
234    }
235
236    /// Ensure connections to a [`MetaAsDir`] cannot be created as mutable (i.e. with
237    /// [`fio::PERM_WRITABLE`]) or executable ([`fio::PERM_EXECUTABLE`]). This ensures that the VFS
238    /// will disallow any attempts to create a new file/directory, modify the attributes of any
239    /// nodes, or open any files as writable/executable.
240    #[fuchsia_async::run_singlethreaded(test)]
241    async fn meta_as_dir_cannot_be_served_as_mutable() {
242        let pkg = PackageBuilder::new("pkg")
243            .add_resource_at("meta/dir/file", &b"contents"[..])
244            .build()
245            .await
246            .unwrap();
247        let (metafar_blob, _) = pkg.contents();
248        let (blobfs_fake, blobfs_client) = FakeBlobfs::new();
249        blobfs_fake.add_blob(metafar_blob.merkle, metafar_blob.contents);
250        let meta_as_dir =
251            MetaAsDir::new(RootDir::new(blobfs_client, metafar_blob.merkle).await.unwrap());
252        for flags in [fio::PERM_WRITABLE, fio::PERM_EXECUTABLE] {
253            let proxy = vfs::directory::serve(meta_as_dir.clone(), flags);
254            assert_matches!(
255                proxy.take_event_stream().try_next().await,
256                Err(fidl::Error::ClientChannelClosed { status: zx::Status::NOT_SUPPORTED, .. })
257            );
258        }
259    }
260
261    #[fuchsia_async::run_singlethreaded(test)]
262    async fn meta_as_dir_readdir() {
263        let (_env, meta_as_dir) = TestEnv::new().await;
264        assert_eq!(
265            fuchsia_fs::directory::readdir_inclusive(&meta_as_dir).await.unwrap(),
266            vec![
267                DirEntry { name: ".".to_string(), kind: DirentKind::Directory },
268                DirEntry { name: "contents".to_string(), kind: DirentKind::File },
269                DirEntry { name: "dir".to_string(), kind: DirentKind::Directory },
270                DirEntry { name: "fuchsia.abi".to_string(), kind: DirentKind::Directory },
271                DirEntry { name: "package".to_string(), kind: DirentKind::File }
272            ]
273        );
274    }
275
276    #[fuchsia_async::run_singlethreaded(test)]
277    async fn meta_as_dir_get_attributes() {
278        let (_env, meta_as_dir) = TestEnv::new().await;
279        let (mutable_attributes, immutable_attributes) =
280            meta_as_dir.get_attributes(fio::NodeAttributesQuery::all()).await.unwrap().unwrap();
281        assert_eq!(
282            fio::NodeAttributes2 { mutable_attributes, immutable_attributes },
283            immutable_attributes!(
284                fio::NodeAttributesQuery::all(),
285                Immutable {
286                    protocols: fio::NodeProtocolKinds::DIRECTORY,
287                    abilities: crate::DIRECTORY_ABILITIES,
288                    content_size: 4,
289                    storage_size: 4,
290                    id: 1,
291                }
292            )
293        );
294    }
295
296    #[fuchsia_async::run_singlethreaded(test)]
297    async fn meta_as_dir_watch_not_supported() {
298        let (_env, meta_as_dir) = TestEnv::new().await;
299        let (_client, server) = fidl::endpoints::create_endpoints();
300        let status = zx::Status::from_raw(
301            meta_as_dir.watch(fio::WatchMask::empty(), 0, server).await.unwrap(),
302        );
303        assert_eq!(status, zx::Status::NOT_SUPPORTED);
304    }
305
306    #[fuchsia_async::run_singlethreaded(test)]
307    async fn meta_as_dir_open_file() {
308        let (_env, meta_as_dir) = TestEnv::new().await;
309        let proxy = fuchsia_fs::directory::open_file(&meta_as_dir, "dir/file", fio::PERM_READABLE)
310            .await
311            .unwrap();
312        assert_eq!(fuchsia_fs::file::read(&proxy).await.unwrap(), b"contents".to_vec());
313    }
314
315    #[fuchsia_async::run_singlethreaded(test)]
316    async fn meta_as_dir_open_directory() {
317        let (_env, meta_as_dir) = TestEnv::new().await;
318        for path in ["dir", "dir/"] {
319            let proxy =
320                fuchsia_fs::directory::open_directory(&meta_as_dir, path, fio::PERM_READABLE)
321                    .await
322                    .unwrap();
323            assert_eq!(
324                fuchsia_fs::directory::readdir(&proxy).await.unwrap(),
325                vec![DirEntry { name: "file".to_string(), kind: DirentKind::File }]
326            );
327        }
328    }
329
330    #[fuchsia_async::run_singlethreaded(test)]
331    async fn meta_as_dir_deprecated_open_file() {
332        let (_env, meta_as_dir) = TestEnv::new().await;
333        let (proxy, server_end) = fidl::endpoints::create_proxy::<fio::FileMarker>();
334        meta_as_dir
335            .deprecated_open(
336                fio::OpenFlags::RIGHT_READABLE,
337                Default::default(),
338                "dir/file",
339                server_end.into_channel().into(),
340            )
341            .unwrap();
342        assert_eq!(fuchsia_fs::file::read(&proxy).await.unwrap(), b"contents".to_vec());
343    }
344
345    #[fuchsia_async::run_singlethreaded(test)]
346    async fn meta_as_dir_deprecated_open_directory() {
347        let (_env, meta_as_dir) = TestEnv::new().await;
348        for path in ["dir", "dir/"] {
349            let (proxy, server_end) = fidl::endpoints::create_proxy::<fio::DirectoryMarker>();
350            meta_as_dir
351                .deprecated_open(
352                    fio::OpenFlags::RIGHT_READABLE,
353                    Default::default(),
354                    path,
355                    server_end.into_channel().into(),
356                )
357                .unwrap();
358
359            assert_eq!(
360                fuchsia_fs::directory::readdir(&proxy).await.unwrap(),
361                vec![DirEntry { name: "file".to_string(), kind: DirentKind::File }]
362            );
363        }
364    }
365}