1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
// Copyright 2022 The Fuchsia Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

//! Utilities for working with the `fuchsia.mem` FIDL library.

use fidl_fuchsia_io as fio;
use fidl_fuchsia_mem as fmem;
use fuchsia_zircon_status as zxs;
use std::borrow::Cow;

/// Open `path` from given `parent` directory, returning an [`fmem::Data`] of the contents.
///
/// Prioritizes returning an [`fmem::Data::Buffer`] if it can be done by reusing a VMO handle
/// from the directory's server.
pub async fn open_file_data(
    parent: &fio::DirectoryProxy,
    path: &str,
) -> Result<fmem::Data, FileError> {
    let file =
        fuchsia_fs::directory::open_file_no_describe(parent, path, fio::OpenFlags::RIGHT_READABLE)?;
    match file
        .get_backing_memory(fio::VmoFlags::READ)
        .await
        .map_err(|e| {
            // Don't swallow the root cause of the error without a trace. It may
            // be impossible to correlate resulting error to its root cause
            // otherwise.
            tracing::debug!("error for path={}: {}:", path, e);
            FileError::GetBufferError(e)
        })?
        .map_err(zxs::Status::from_raw)
    {
        Ok(vmo) => {
            let size = vmo.get_content_size().expect("failed to get VMO size");
            Ok(fmem::Data::Buffer(fmem::Buffer { vmo, size }))
        }
        Err(e) => {
            let _: zxs::Status = e;
            // we still didn't get a VMO handle, fallback to reads over the channel
            let bytes = fuchsia_fs::file::read(&file).await?;
            Ok(fmem::Data::Bytes(bytes))
        }
    }
}

/// Errors that can occur when operating on `DirectoryProxy`s and `FileProxy`s.
#[derive(Debug, thiserror::Error)]
pub enum FileError {
    #[error("Failed to open a File.")]
    OpenError(#[from] fuchsia_fs::node::OpenError),

    #[error("Couldn't read a file")]
    ReadError(#[from] fuchsia_fs::file::ReadError),

    #[error("FIDL call to retrieve a file's buffer failed")]
    GetBufferError(#[source] fidl::Error),
}

/// Retrieve the bytes in `data`, returning a reference if it's a `Data::Bytes` and a copy of
/// the bytes read from the VMO if it's a `Data::Buffer`.
pub fn bytes_from_data<'d>(data: &'d fmem::Data) -> Result<Cow<'d, [u8]>, DataError> {
    Ok(match data {
        fmem::Data::Buffer(buf) => {
            let size = buf.size as usize;
            let mut raw_bytes = Vec::with_capacity(size);
            raw_bytes.resize(size, 0);
            buf.vmo.read(&mut raw_bytes, 0).map_err(DataError::VmoReadError)?;
            Cow::Owned(raw_bytes)
        }
        fmem::Data::Bytes(b) => Cow::Borrowed(b),
        fmem::DataUnknown!() => return Err(DataError::UnrecognizedDataVariant),
    })
}

/// Errors that can occur when operating on `fuchsia.mem.Data` values.
#[derive(Debug, thiserror::Error, PartialEq, Eq)]
pub enum DataError {
    #[error("Couldn't read from VMO")]
    VmoReadError(#[source] zxs::Status),

    #[error("Encountered an unrecognized variant of fuchsia.mem.Data")]
    UnrecognizedDataVariant,
}

#[cfg(test)]
mod tests {
    use super::*;
    use async_trait::async_trait;
    use fidl::endpoints::{create_proxy, ServerEnd};
    use fuchsia_zircon_status::Status;
    use futures::StreamExt;
    use std::sync::Arc;
    use vfs::{
        directory::{
            entry::{DirectoryEntry, EntryInfo, OpenRequest},
            entry_container::Directory,
        },
        execution_scope::ExecutionScope,
        file::vmo::read_only,
        file::{FileLike, FileOptions},
        object_request::Representation,
        pseudo_directory, ObjectRequestRef,
    };

    #[fuchsia::test]
    async fn bytes_from_read_only() {
        let fs = pseudo_directory! {
            // `read_only` is a vmo file, returns the buffer in OnOpen
            "foo" => read_only("hello, world!"),
        };
        let directory = serve_vfs_dir(fs);

        let data = open_file_data(&directory, "foo").await.unwrap();
        match bytes_from_data(&data).unwrap() {
            Cow::Owned(b) => assert_eq!(b, b"hello, world!"),
            _ => panic!("must produce an owned value from reading contents of fmem::Data::Buffer"),
        }
    }

    /// Test that we get a VMO when the server supports `File/GetBackingMemory`.
    #[fuchsia::test]
    async fn bytes_from_vmo_from_get_buffer() {
        let vmo_data = b"hello, world!";
        let fs = pseudo_directory! {
            "foo" => read_only(vmo_data),
        };
        let directory = serve_vfs_dir(fs);

        let data = open_file_data(&directory, "foo").await.unwrap();
        match bytes_from_data(&data).unwrap() {
            Cow::Owned(b) => assert_eq!(b, vmo_data),
            _ => panic!("must produce an owned value from reading contents of fmem::Data::Buffer"),
        }
    }

    /// Test that we correctly fall back to reading through FIDL calls in a channel if the server
    /// doesn't support returning a VMO.
    #[fuchsia::test]
    async fn bytes_from_channel_fallback() {
        // This test File does not handle `File/GetBackingMemory` request, but will return
        // b"hello, world!" on File/Read`.
        struct NonVMOTestFile;

        impl DirectoryEntry for NonVMOTestFile {
            fn entry_info(&self) -> EntryInfo {
                EntryInfo::new(fio::INO_UNKNOWN, fio::DirentType::File)
            }

            fn open_entry(self: Arc<Self>, request: OpenRequest<'_>) -> Result<(), Status> {
                request.open_file(self)
            }
        }

        #[async_trait]
        impl vfs::node::Node for NonVMOTestFile {
            async fn get_attrs(&self) -> Result<fio::NodeAttributes, Status> {
                Err(Status::NOT_SUPPORTED)
            }

            async fn get_attributes(
                &self,
                _requested_attributes: fio::NodeAttributesQuery,
            ) -> Result<fio::NodeAttributes2, Status> {
                Err(Status::NOT_SUPPORTED)
            }
        }

        impl FileLike for NonVMOTestFile {
            fn open(
                self: Arc<Self>,
                scope: ExecutionScope,
                _options: FileOptions,
                object_request: ObjectRequestRef<'_>,
            ) -> Result<(), Status> {
                struct Connection;
                impl Representation for Connection {
                    type Protocol = fio::FileMarker;

                    async fn get_representation(
                        &self,
                        _requested_attributes: fio::NodeAttributesQuery,
                    ) -> Result<fio::Representation, Status> {
                        unreachable!()
                    }

                    async fn node_info(&self) -> Result<fio::NodeInfoDeprecated, Status> {
                        unreachable!()
                    }
                }
                let connection = Connection;
                let object_request = object_request.take();
                scope.spawn(async move {
                    if let Ok(mut file_requests) =
                        object_request.into_request_stream(&connection).await
                    {
                        let mut have_sent_bytes = false;
                        while let Some(Ok(request)) = file_requests.next().await {
                            match request {
                                fio::FileRequest::GetBackingMemory { flags: _, responder } => {
                                    responder.send(Err(Status::NOT_SUPPORTED.into_raw())).unwrap()
                                }
                                fio::FileRequest::Read { count: _, responder } => {
                                    let to_send: &[u8] = if !have_sent_bytes {
                                        have_sent_bytes = true;
                                        b"hello, world!"
                                    } else {
                                        &[]
                                    };
                                    responder.send(Ok(to_send)).unwrap();
                                }
                                unexpected => unimplemented!("{:#?}", unexpected),
                            }
                        }
                    }
                });
                Ok(())
            }
        }

        let fs = pseudo_directory! {
            "foo" => Arc::new(NonVMOTestFile),
        };
        let directory = serve_vfs_dir(fs);

        let data = open_file_data(&directory, "foo").await.unwrap();
        let data = bytes_from_data(&data).unwrap();
        assert_eq!(
            data,
            Cow::Borrowed(b"hello, world!"),
            "must produce a borrowed value from fmem::Data::Bytes"
        );
    }

    fn serve_vfs_dir(root: Arc<impl Directory>) -> fio::DirectoryProxy {
        let fs_scope = ExecutionScope::new();
        let (client, server) = create_proxy::<fio::DirectoryMarker>().unwrap();
        root.open(
            fs_scope.clone(),
            fio::OpenFlags::RIGHT_READABLE,
            vfs::path::Path::dot(),
            ServerEnd::new(server.into_channel()),
        );
        client
    }
}