sl4f_lib/file/
facade.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
5use anyhow::{format_err, Error};
6use base64::engine::general_purpose::STANDARD as BASE64_STANDARD;
7use base64::engine::Engine as _;
8use serde_json::{to_value, Value};
9use std::path::Path;
10use std::{fs, io};
11
12use super::types::*;
13
14/// Facade providing access to session testing interfaces.
15#[derive(Debug)]
16pub struct FileFacade;
17
18impl FileFacade {
19    pub fn new() -> Self {
20        Self
21    }
22
23    /// Deletes the given path, which must be a file. Returns OK(NotFound) if the file does not
24    /// exist.
25    pub async fn delete_file(&self, args: Value) -> Result<DeleteFileResult, Error> {
26        let path = args.get("path").ok_or_else(|| format_err!("DeleteFile failed, no path"))?;
27        let path =
28            path.as_str().ok_or_else(|| format_err!("DeleteFile failed, path not string"))?;
29
30        match fs::remove_file(path) {
31            Ok(()) => Ok(DeleteFileResult::Success),
32            Err(e) if e.kind() == io::ErrorKind::NotFound => Ok(DeleteFileResult::NotFound),
33            Err(e) => Err(e.into()),
34        }
35    }
36
37    /// Creates a new directory. Returns OK(AlreadyExists) if the directory already exists.
38    pub async fn make_dir(&self, args: Value) -> Result<MakeDirResult, Error> {
39        let path = args.get("path").ok_or_else(|| format_err!("MakeDir failed, no path"))?;
40        let path = path.as_str().ok_or_else(|| format_err!("MakeDir failed, path not string"))?;
41        let path = Path::new(path);
42
43        let recurse = args["recurse"].as_bool().unwrap_or(false);
44
45        if path.is_dir() {
46            return Ok(MakeDirResult::AlreadyExists);
47        }
48
49        if recurse {
50            fs::create_dir_all(path)?;
51        } else {
52            fs::create_dir(path)?;
53        }
54        Ok(MakeDirResult::Success)
55    }
56
57    /// Given a source file, fetches its contents.
58    pub async fn read_file(&self, args: Value) -> Result<Value, Error> {
59        let path = args.get("path").ok_or_else(|| format_err!("ReadFile failed, no path"))?;
60        let path = path.as_str().ok_or_else(|| format_err!("ReadFile failed, path not string"))?;
61
62        let contents = fs::read(path)?;
63        let encoded_contents = BASE64_STANDARD.encode(&contents);
64
65        Ok(to_value(encoded_contents)?)
66    }
67
68    /// Given data and the destination, it creates a new file and
69    /// puts it in the corresponding path (given by the destination).
70    pub async fn write_file(&self, args: Value) -> Result<WriteFileResult, Error> {
71        let data = args.get("data").ok_or_else(|| format_err!("WriteFile failed, no data"))?;
72        let data = data.as_str().ok_or_else(|| format_err!("WriteFile failed, data not string"))?;
73
74        let contents = BASE64_STANDARD.decode(data)?;
75
76        let destination = args
77            .get("dst")
78            .ok_or_else(|| format_err!("WriteFile failed, no destination path given"))?;
79        let destination = destination
80            .as_str()
81            .ok_or_else(|| format_err!("WriteFile failed, destination not string"))?;
82
83        fs::write(destination, &contents)?;
84        Ok(WriteFileResult::Success)
85    }
86
87    /// Returns metadata for the given path. Returns Ok(NotFound) if the path does not exist.
88    pub async fn stat(&self, args: Value) -> Result<StatResult, Error> {
89        let path = args.get("path").ok_or_else(|| format_err!("Stat failed, no path"))?;
90        let path = path.as_str().ok_or_else(|| format_err!("Stat failed, path not string"))?;
91
92        let metadata = match fs::metadata(path) {
93            Err(e) if e.kind() == io::ErrorKind::NotFound => return Ok(StatResult::NotFound),
94            res => res,
95        }?;
96
97        Ok(StatResult::Success(Metadata {
98            kind: if metadata.is_dir() {
99                NodeKind::Directory
100            } else if metadata.is_file() {
101                NodeKind::File
102            } else {
103                NodeKind::Other
104            },
105            size: metadata.len(),
106        }))
107    }
108}
109
110#[cfg(test)]
111mod tests {
112    use super::*;
113    use assert_matches::assert_matches;
114    use serde_json::json;
115
116    #[fuchsia_async::run_singlethreaded(test)]
117    async fn delete_file_ok() {
118        let temp = tempfile::tempdir().unwrap();
119        let path = temp.path().join("test.txt");
120        fs::write(&path, "hello world!".as_bytes()).unwrap();
121        assert!(path.exists());
122
123        assert_matches!(
124            FileFacade.delete_file(json!({ "path": path })).await,
125            Ok(DeleteFileResult::Success)
126        );
127        assert!(!path.exists());
128
129        assert_matches!(
130            FileFacade.delete_file(json!({ "path": path })).await,
131            Ok(DeleteFileResult::NotFound)
132        );
133    }
134
135    #[fuchsia_async::run_singlethreaded(test)]
136    async fn make_dir_ok() {
137        let temp = tempfile::tempdir().unwrap();
138        let path = temp.path().join("a");
139        assert!(!path.exists());
140
141        assert_matches!(
142            FileFacade.make_dir(json!({ "path": path })).await,
143            Ok(MakeDirResult::Success)
144        );
145        assert!(path.is_dir());
146
147        assert_matches!(
148            FileFacade.make_dir(json!({ "path": path })).await,
149            Ok(MakeDirResult::AlreadyExists)
150        );
151    }
152
153    #[fuchsia_async::run_singlethreaded(test)]
154    async fn make_dir_recurse_ok() {
155        let temp = tempfile::tempdir().unwrap();
156        let path = temp.path().join("a/b/c");
157        assert!(!path.exists());
158
159        assert_matches!(FileFacade.make_dir(json!({ "path": path })).await, Err(_));
160        assert!(!path.exists());
161
162        assert_matches!(
163            FileFacade.make_dir(json!({ "path": path, "recurse": true })).await,
164            Ok(MakeDirResult::Success)
165        );
166        assert!(path.is_dir());
167    }
168
169    #[fuchsia_async::run_singlethreaded(test)]
170    async fn read_file_ok() {
171        const FILE_CONTENTS: &str = "hello world!";
172        const FILE_CONTENTS_AS_BASE64: &str = "aGVsbG8gd29ybGQh";
173
174        let temp = tempfile::tempdir().unwrap();
175        let path = temp.path().join("test.txt");
176        fs::write(&path, FILE_CONTENTS.as_bytes()).unwrap();
177
178        assert_matches!(
179            FileFacade.read_file(json!({ "path": path })).await,
180            Ok(value) if value == json!(FILE_CONTENTS_AS_BASE64)
181        );
182    }
183
184    #[fuchsia_async::run_singlethreaded(test)]
185    async fn write_file_ok() {
186        const FILE_CONTENTS: &str = "hello world!";
187        const FILE_CONTENTS_AS_BASE64: &str = "aGVsbG8gd29ybGQh";
188
189        let temp = tempfile::tempdir().unwrap();
190        let path = temp.path().join("test.txt");
191
192        assert_matches!(
193            FileFacade.write_file(json!({ "data": FILE_CONTENTS_AS_BASE64, "dst": path })).await,
194            Ok(WriteFileResult::Success)
195        );
196
197        assert_eq!(fs::read_to_string(&path).unwrap(), FILE_CONTENTS);
198    }
199
200    #[fuchsia_async::run_singlethreaded(test)]
201    async fn write_file_unwritable_path() {
202        const FILE_CONTENTS_AS_BASE64: &str = "aGVsbG8gd29ybGQh";
203
204        assert_matches!(
205            FileFacade
206                .write_file(json!({ "data": FILE_CONTENTS_AS_BASE64, "dst": "/pkg/is/readonly" }))
207                .await,
208            Err(_)
209        );
210    }
211
212    #[fuchsia_async::run_singlethreaded(test)]
213    async fn stat_file() {
214        let temp = tempfile::tempdir().unwrap();
215        let path = temp.path().join("test.txt");
216        fs::write(&path, "hello world!".as_bytes()).unwrap();
217
218        assert_matches!(
219            FileFacade.stat(json!({ "path": path })).await,
220            Ok(StatResult::Success(Metadata { kind: NodeKind::File, size: 12 }))
221        );
222    }
223
224    #[fuchsia_async::run_singlethreaded(test)]
225    async fn stat_dir() {
226        assert_matches!(
227            FileFacade.stat(json!({ "path": "/pkg" })).await,
228            Ok(StatResult::Success(Metadata { kind: NodeKind::Directory, size: 0 }))
229        );
230    }
231
232    #[fuchsia_async::run_singlethreaded(test)]
233    async fn stat_not_found() {
234        assert_matches!(
235            FileFacade.stat(json!({ "path": "/the/ultimate/question" })).await,
236            Ok(StatResult::NotFound)
237        );
238    }
239}