fuchsia_fuzzctl/
util.rs

1// Copyright 2022 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// This file contains a few helper routines that don't fit nicely anywhere else.
6
7use anyhow::{bail, Context as _, Result};
8use fidl_fuchsia_fuzzer::{self as fuzz, Result_ as FuzzResult};
9use serde_json::Value;
10use sha2::{Digest, Sha256};
11use std::path::{Path, PathBuf};
12use std::{env, fs};
13use url::Url;
14
15/// Creates a directory under the given `parent` directory, if it does not already exist.
16pub fn create_dir_at<P: AsRef<Path>, S: AsRef<str>>(parent: P, dirname: S) -> Result<PathBuf> {
17    let mut pathbuf = PathBuf::from(parent.as_ref());
18    pathbuf.push(dirname.as_ref());
19    fs::create_dir_all(&pathbuf)
20        .with_context(|| format!("failed to create directory: '{}'", pathbuf.to_string_lossy()))?;
21    Ok(pathbuf)
22}
23
24/// Returns the path under `output_dir` where a fuzzer could store artifacts.
25pub fn create_artifact_dir<P: AsRef<Path>>(output_dir: P) -> Result<PathBuf> {
26    create_dir_at(output_dir, "artifacts")
27}
28
29/// Returns the path under `output_dir` where a fuzzer could store a corpus of the given
30/// |corpus_type|.
31pub fn create_corpus_dir<P: AsRef<Path>>(
32    output_dir: P,
33    corpus_type: fuzz::Corpus,
34) -> Result<PathBuf> {
35    match corpus_type {
36        fuzz::Corpus::Seed => create_dir_at(output_dir, "seed-corpus"),
37        fuzz::Corpus::Live => create_dir_at(output_dir, "corpus"),
38        other => unreachable!("unsupported type: {:?}", other),
39    }
40}
41
42/// Generates the path for a file based on its contents.
43///
44/// Returns a `PathBuf` for a file in the `out_dir` that is named by the concatenating the `prefix`,
45/// if provided, with the hex encoded SHA-256 digest of the `data`. This naming scheme is used both
46/// for inputs retrieved from a fuzzer corpus and for artifacts produced by the fuzzer.
47///
48pub fn digest_path<P: AsRef<Path>>(out_dir: P, result: Option<FuzzResult>, data: &[u8]) -> PathBuf {
49    let mut path = PathBuf::from(out_dir.as_ref());
50    let prefix = match result {
51        None | Some(FuzzResult::NoErrors) => String::default(),
52        Some(FuzzResult::BadMalloc) => format!("alloc-"),
53        Some(FuzzResult::Crash) => format!("crash-"),
54        Some(FuzzResult::Death) => format!("death-"),
55        Some(FuzzResult::Exit) => format!("exit-"),
56        Some(FuzzResult::Leak) => format!("leak-"),
57        Some(FuzzResult::Oom) => format!("oom-"),
58        Some(FuzzResult::Timeout) => format!("timeout-"),
59        Some(FuzzResult::Cleansed) => format!("cleansed-"),
60        Some(FuzzResult::Minimized) => format!("minimized-"),
61        _ => unreachable!(),
62    };
63    let mut digest = Sha256::new();
64    digest.update(&data);
65    path.push(format!("{}{:x}", prefix, digest.finalize()));
66    path
67}
68
69/// Gets URLs for available fuzzers.
70///
71/// Reads from the filesystem and parses the build metadata to produce a list of URLs for fuzzer
72/// packages. If `tests_json` is `None`, this will look for the FUCHSIA_DIR environment variable to
73/// locate tests.json within the build directory.
74///
75/// Returns an error if tests.json cannot be found, read, or parsed as valid JSON.
76pub fn get_fuzzer_urls(tests_json: &Option<String>) -> Result<Vec<Url>> {
77    let tests_json = match tests_json {
78        Some(tests_json) => Ok(PathBuf::from(tests_json)),
79        None => test_json_path(None).context("tests.json was not provided and could not be found"),
80    }?;
81    let json_data = fs::read_to_string(&tests_json)
82        .context(format!("failed to read '{}'", tests_json.to_string_lossy()))?;
83    parse_tests_json(json_data)
84        .context(format!("failed to parse '{}'", tests_json.to_string_lossy()))
85}
86
87fn test_json_path(fuchsia_dir: Option<&Path>) -> Result<PathBuf> {
88    let fuchsia_dir = match fuchsia_dir {
89        Some(fuchsia_dir) => Ok(fuchsia_dir.to_string_lossy().to_string()),
90        None => env::var("FUCHSIA_DIR").context("FUCHSIA_DIR is not set"),
91    }?;
92    let mut fx_build_dir = PathBuf::from(&fuchsia_dir);
93    fx_build_dir.push(".fx-build-dir");
94    let mut fx_build_dir = fs::read_to_string(&fx_build_dir)
95        .with_context(|| format!("failed to read '{}'", fx_build_dir.to_string_lossy()))?;
96
97    fx_build_dir.retain(|c| !c.is_whitespace());
98    let mut tests_json = PathBuf::from(&fuchsia_dir);
99    tests_json.push(&fx_build_dir);
100    tests_json.push("tests.json");
101    Ok(tests_json)
102}
103
104fn parse_tests_json(json_data: String) -> Result<Vec<Url>> {
105    let deserialized = serde_json::from_str(&json_data).context("failed to deserialize")?;
106    let tests = match deserialized {
107        Value::Array(tests) => tests,
108        _ => bail!("root object is not array"),
109    };
110    let mut fuzzer_urls = Vec::new();
111    for test in tests {
112        let metadata = match test.get("test") {
113            Some(Value::Object(metadata)) => metadata,
114            Some(_) => bail!("found 'test' field that is not an object"),
115            None => continue,
116        };
117        let build_rule = match metadata.get("build_rule") {
118            Some(Value::String(build_rule)) => build_rule,
119            Some(_) => bail!("found 'build_rule' field that is not a string"),
120            None => continue,
121        };
122        if build_rule != "fuchsia_fuzzer_package" {
123            continue;
124        }
125        let package_url = match metadata.get("package_url") {
126            Some(Value::String(package_url)) => package_url,
127            Some(_) => bail!("found 'package_url' field that is not a string"),
128            None => continue,
129        };
130        let url = Url::parse(package_url).context("failed to parse URL")?;
131        fuzzer_urls.push(url);
132    }
133    Ok(fuzzer_urls)
134}
135
136#[cfg(test)]
137mod tests {
138    use super::{get_fuzzer_urls, test_json_path};
139    use anyhow::Result;
140    use fuchsia_fuzzctl_test::Test;
141    use serde_json::json;
142
143    #[fuchsia::test]
144    async fn test_test_json_path() -> Result<()> {
145        let test = Test::try_new()?;
146        let build_dir = test.create_dir("out/default")?;
147
148        // Missing .fx-build-dir
149        let fuchsia_dir = test.root_dir();
150        let actual = format!("{:?}", test_json_path(Some(&fuchsia_dir)));
151        let expected = format!("failed to read '{}/.fx-build-dir'", fuchsia_dir.to_string_lossy());
152        assert!(actual.contains(&expected));
153
154        // Valid
155        test.write_fx_build_dir(&build_dir)?;
156        test_json_path(Some(&fuchsia_dir))?;
157        Ok(())
158    }
159
160    #[fuchsia::test]
161    async fn test_get_fuzzer_urls() -> Result<()> {
162        let test = Test::try_new()?;
163        let build_dir = test.create_dir("out/default")?;
164        test.write_fx_build_dir(&build_dir)?;
165        let fuchsia_dir = test.root_dir();
166        let tests_json = test_json_path(Some(fuchsia_dir))?;
167        let tests_json = Some(tests_json.to_string_lossy().to_string());
168
169        // Missing tests.json
170        let actual = format!("{:?}", get_fuzzer_urls(&tests_json));
171        let expected = format!("failed to read '{}/tests.json'", build_dir.to_string_lossy());
172        assert!(actual.contains(&expected));
173
174        // tests.json is not JSON.
175        test.write_tests_json(&build_dir, "hello world!\n")?;
176        let actual = format!("{:?}", get_fuzzer_urls(&tests_json));
177        assert!(actual.contains("expected value"));
178
179        // tests.json is not an array.
180        let json_data = json!({
181            "foo": 1
182        });
183        test.write_tests_json(&build_dir, json_data.to_string())?;
184        let actual = format!("{:?}", get_fuzzer_urls(&tests_json));
185        assert!(actual.contains("root object is not array"));
186
187        // tests.json contains empty array
188        let json_data = json!([]);
189        test.write_tests_json(&build_dir, json_data.to_string())?;
190        let fuzzers = get_fuzzer_urls(&tests_json)?;
191        assert!(fuzzers.is_empty());
192
193        // Various malformed tests.jsons
194        let json_data = json!([
195            {
196                "test": 1
197            }
198        ]);
199        test.write_tests_json(&build_dir, json_data.to_string())?;
200        let actual = format!("{:?}", get_fuzzer_urls(&tests_json));
201        assert!(actual.contains("found 'test' field that is not an object"));
202
203        let json_data = json!([
204            {
205                "test": {
206                    "build_rule": 1
207                }
208            }
209        ]);
210        test.write_tests_json(&build_dir, json_data.to_string())?;
211        let actual = format!("{:?}", get_fuzzer_urls(&tests_json));
212        assert!(actual.contains("found 'build_rule' field that is not a string"));
213
214        let json_data = json!([
215            {
216                "test": {
217                    "build_rule": "fuchsia_fuzzer_package",
218                    "package_url": 1
219                }
220            }
221        ]);
222        test.write_tests_json(&build_dir, json_data.to_string())?;
223        let actual = format!("{:?}", get_fuzzer_urls(&tests_json));
224        assert!(actual.contains("found 'package_url' field that is not a string"));
225
226        let json_data = json!([
227            {
228                "test": {
229                    "build_rule": "fuchsia_fuzzer_package",
230                    "package_url": "not a valid URL"
231                }
232            }
233        ]);
234        test.write_tests_json(&build_dir, json_data.to_string())?;
235        let actual = format!("{:?}", get_fuzzer_urls(&tests_json));
236        assert!(actual.contains("failed to parse URL"));
237
238        // tests.json contains fuzzers mixed with other tests.
239        let json_data = json!([
240            {
241                "test": {
242                    "name": "host-test"
243                }
244            },
245            {
246                "test": {
247                    "build_rule": "fuchsia_fuzzer_package",
248                    "package_url": "fuchsia-pkg://fuchsia.com/fake#meta/foo-fuzzer.cm"
249                }
250            },
251            {
252                "test": {
253                    "build_rule": "fuchsia_test_package",
254                    "package_url": "fuchsia-pkg://fuchsia.com/fake#meta/unittests.cm"
255                }
256            },
257            {
258                "test": {
259                    "build_rule": "fuchsia_fuzzer_package",
260                    "package_url": "fuchsia-pkg://fuchsia.com/fake#meta/bar-fuzzer.cm"
261                }
262            }
263        ]);
264        test.write_tests_json(&build_dir, json_data.to_string())?;
265        let urls = get_fuzzer_urls(&tests_json)?;
266        assert_eq!(urls[0].as_str(), "fuchsia-pkg://fuchsia.com/fake#meta/foo-fuzzer.cm");
267        assert_eq!(urls[1].as_str(), "fuchsia-pkg://fuchsia.com/fake#meta/bar-fuzzer.cm");
268        Ok(())
269    }
270}