Skip to main content

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::{Context as _, Result, bail};
8use flex_fuchsia_fuzzer::{self as fuzz, Result_ as FuzzResult};
9use serde_json::Value;
10use sha2::{Digest, Sha256};
11use std::fs;
12use std::path::{Path, PathBuf};
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!("{}{}", prefix, hex::encode(digest.finalize())));
66    path
67}
68
69/// Gets URLs for available fuzzers.
70///
71/// Reads from the filesystem and parses the build metadata in `tests_json` to produce a list of
72/// URLs for fuzzer packages.
73///
74/// Returns an error if `tests_json` cannot be read or parsed as valid JSON.
75pub fn get_fuzzer_urls<P: AsRef<Path>>(tests_json: P) -> Result<Vec<Url>> {
76    let tests_json = tests_json.as_ref();
77    let json_data = fs::read_to_string(tests_json)
78        .context(format!("failed to read '{}'", tests_json.to_string_lossy()))?;
79    parse_tests_json(json_data)
80        .context(format!("failed to parse '{}'", tests_json.to_string_lossy()))
81}
82
83fn parse_tests_json(json_data: String) -> Result<Vec<Url>> {
84    let deserialized = serde_json::from_str(&json_data).context("failed to deserialize")?;
85    let tests = match deserialized {
86        Value::Array(tests) => tests,
87        _ => bail!("root object is not array"),
88    };
89    let mut fuzzer_urls = Vec::new();
90    for test in tests {
91        let metadata = match test.get("test") {
92            Some(Value::Object(metadata)) => metadata,
93            Some(_) => bail!("found 'test' field that is not an object"),
94            None => continue,
95        };
96        let build_rule = match metadata.get("build_rule") {
97            Some(Value::String(build_rule)) => build_rule,
98            Some(_) => bail!("found 'build_rule' field that is not a string"),
99            None => continue,
100        };
101        if build_rule != "fuchsia_fuzzer_package" {
102            continue;
103        }
104        let package_url = match metadata.get("package_url") {
105            Some(Value::String(package_url)) => package_url,
106            Some(_) => bail!("found 'package_url' field that is not a string"),
107            None => continue,
108        };
109        let url = Url::parse(package_url).context("failed to parse URL")?;
110        fuzzer_urls.push(url);
111    }
112    Ok(fuzzer_urls)
113}
114
115#[cfg(test)]
116mod tests {
117    use super::get_fuzzer_urls;
118    use anyhow::Result;
119    use fuchsia_fuzzctl_test::Test;
120    use serde_json::json;
121
122    #[fuchsia::test]
123    async fn test_get_fuzzer_urls() -> Result<()> {
124        let test = Test::try_new()?;
125        let build_dir = test.create_dir("out/default")?;
126        let tests_json = build_dir.join("tests.json");
127
128        // Missing tests.json
129        let actual = format!("{:?}", get_fuzzer_urls(&tests_json));
130        let expected = format!("failed to read '{}/tests.json'", build_dir.to_string_lossy());
131        assert!(actual.contains(&expected));
132
133        // tests.json is not JSON.
134        test.write_tests_json(&build_dir, "hello world!\n")?;
135        let actual = format!("{:?}", get_fuzzer_urls(&tests_json));
136        assert!(actual.contains("expected value"));
137
138        // tests.json is not an array.
139        let json_data = json!({
140            "foo": 1
141        });
142        test.write_tests_json(&build_dir, json_data.to_string())?;
143        let actual = format!("{:?}", get_fuzzer_urls(&tests_json));
144        assert!(actual.contains("root object is not array"));
145
146        // tests.json contains empty array
147        let json_data = json!([]);
148        test.write_tests_json(&build_dir, json_data.to_string())?;
149        let fuzzers = get_fuzzer_urls(&tests_json)?;
150        assert!(fuzzers.is_empty());
151
152        // Various malformed tests.jsons
153        let json_data = json!([
154            {
155                "test": 1
156            }
157        ]);
158        test.write_tests_json(&build_dir, json_data.to_string())?;
159        let actual = format!("{:?}", get_fuzzer_urls(&tests_json));
160        assert!(actual.contains("found 'test' field that is not an object"));
161
162        let json_data = json!([
163            {
164                "test": {
165                    "build_rule": 1
166                }
167            }
168        ]);
169        test.write_tests_json(&build_dir, json_data.to_string())?;
170        let actual = format!("{:?}", get_fuzzer_urls(&tests_json));
171        assert!(actual.contains("found 'build_rule' field that is not a string"));
172
173        let json_data = json!([
174            {
175                "test": {
176                    "build_rule": "fuchsia_fuzzer_package",
177                    "package_url": 1
178                }
179            }
180        ]);
181        test.write_tests_json(&build_dir, json_data.to_string())?;
182        let actual = format!("{:?}", get_fuzzer_urls(&tests_json));
183        assert!(actual.contains("found 'package_url' field that is not a string"));
184
185        let json_data = json!([
186            {
187                "test": {
188                    "build_rule": "fuchsia_fuzzer_package",
189                    "package_url": "not a valid URL"
190                }
191            }
192        ]);
193        test.write_tests_json(&build_dir, json_data.to_string())?;
194        let actual = format!("{:?}", get_fuzzer_urls(&tests_json));
195        assert!(actual.contains("failed to parse URL"));
196
197        // tests.json contains fuzzers mixed with other tests.
198        let json_data = json!([
199            {
200                "test": {
201                    "name": "host-test"
202                }
203            },
204            {
205                "test": {
206                    "build_rule": "fuchsia_fuzzer_package",
207                    "package_url": "fuchsia-pkg://fuchsia.com/fake#meta/foo-fuzzer.cm"
208                }
209            },
210            {
211                "test": {
212                    "build_rule": "fuchsia_test_package",
213                    "package_url": "fuchsia-pkg://fuchsia.com/fake#meta/unittests.cm"
214                }
215            },
216            {
217                "test": {
218                    "build_rule": "fuchsia_fuzzer_package",
219                    "package_url": "fuchsia-pkg://fuchsia.com/fake#meta/bar-fuzzer.cm"
220                }
221            }
222        ]);
223        test.write_tests_json(&build_dir, json_data.to_string())?;
224        let urls = get_fuzzer_urls(&tests_json)?;
225        assert_eq!(urls[0].as_str(), "fuchsia-pkg://fuchsia.com/fake#meta/foo-fuzzer.cm");
226        assert_eq!(urls[1].as_str(), "fuchsia-pkg://fuchsia.com/fake#meta/bar-fuzzer.cm");
227        Ok(())
228    }
229}