diff --git a/src/agent/Cargo.lock b/src/agent/Cargo.lock index 63f9c493f9..80362e7bd0 100644 --- a/src/agent/Cargo.lock +++ b/src/agent/Cargo.lock @@ -2127,6 +2127,19 @@ dependencies = [ "winapi", ] +[[package]] +name = "onefuzz-file-format" +version = "0.1.0" +dependencies = [ + "anyhow", + "coverage", + "debuggable-module", + "pretty_assertions", + "quick-xml", + "serde", + "serde_json", +] + [[package]] name = "onefuzz-task" version = "0.2.0" diff --git a/src/agent/Cargo.toml b/src/agent/Cargo.toml index 2421c248e6..e592cc2308 100644 --- a/src/agent/Cargo.toml +++ b/src/agent/Cargo.toml @@ -10,6 +10,7 @@ members = [ "onefuzz", "onefuzz-task", "onefuzz-agent", + "onefuzz-file-format", "onefuzz-telemetry", "reqwest-retry", "srcview", diff --git a/src/agent/onefuzz-file-format/Cargo.toml b/src/agent/onefuzz-file-format/Cargo.toml new file mode 100644 index 0000000000..efb4397420 --- /dev/null +++ b/src/agent/onefuzz-file-format/Cargo.toml @@ -0,0 +1,16 @@ +[package] +name = "onefuzz-file-format" +version = "0.1.0" +edition = "2021" +license = "MIT" + +[dependencies] +anyhow = "1.0" +coverage = { path = "../coverage" } +debuggable-module = { path = "../debuggable-module" } +quick-xml = "0.26.0" +serde = { version = "1.0", features = ["derive"] } +serde_json = { version = "1.0", features = ["preserve_order"] } + +[dev-dependencies] +pretty_assertions = "1.3" diff --git a/src/agent/onefuzz-file-format/examples/cobertura.rs b/src/agent/onefuzz-file-format/examples/cobertura.rs new file mode 100644 index 0000000000..3ae9d62fa3 --- /dev/null +++ b/src/agent/onefuzz-file-format/examples/cobertura.rs @@ -0,0 +1,44 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +use anyhow::Result; + +use coverage::source::{Count, FileCoverage, Line, SourceCoverage}; +use debuggable_module::path::FilePath; +use onefuzz_file_format::coverage::cobertura::CoberturaCoverage; + +fn main() -> Result<()> { + let modoff = vec![ + (r"/missing/lib.c", vec![1, 2, 3, 5, 8]), + ( + r"test-data/fuzz.c", + vec![ + 7, 8, 10, 13, 16, 17, 21, 22, 23, 27, 28, 29, 30, 32, 33, 37, 39, 42, 44, + ], + ), + (r"test-data\fuzz.h", vec![3, 4, 5]), + (r"test-data\lib\explode.h", vec![1, 2, 3]), + ]; + + let mut coverage = SourceCoverage::default(); + + for (path, lines) in modoff { + let file_path = FilePath::new(path)?; + + let mut file = FileCoverage::default(); + + for line in lines { + let count = if line % 3 == 0 { 1 } else { 0 }; + file.lines.insert(Line::new(line)?, Count(count)); + } + + coverage.files.insert(file_path, file); + } + + let cobertura = CoberturaCoverage::from(coverage); + + let text = cobertura.to_string()?; + println!("{text}"); + + Ok(()) +} diff --git a/src/agent/onefuzz-file-format/src/coverage.rs b/src/agent/onefuzz-file-format/src/coverage.rs new file mode 100644 index 0000000000..f83f2dc192 --- /dev/null +++ b/src/agent/onefuzz-file-format/src/coverage.rs @@ -0,0 +1,6 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +pub mod binary; +pub mod cobertura; +pub mod source; diff --git a/src/agent/onefuzz-file-format/src/coverage/binary.rs b/src/agent/onefuzz-file-format/src/coverage/binary.rs new file mode 100644 index 0000000000..f18af080ac --- /dev/null +++ b/src/agent/onefuzz-file-format/src/coverage/binary.rs @@ -0,0 +1,31 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +use anyhow::Result; +use coverage::binary::BinaryCoverage; + +pub mod v0; +pub mod v1; + +#[derive(Serialize, Deserialize)] +#[serde(tag = "version")] +pub enum BinaryCoverageJson { + #[serde(rename = "0")] + V0(v0::BinaryCoverageJson), + + #[serde(rename = "1")] + V1(v1::BinaryCoverageJson), +} + +impl TryFrom for BinaryCoverage { + type Error = anyhow::Error; + + fn try_from(json: BinaryCoverageJson) -> Result { + use BinaryCoverageJson::*; + + match json { + V0(v0) => v0.try_into(), + V1(v1) => v1.try_into(), + } + } +} diff --git a/src/agent/onefuzz-file-format/src/coverage/binary/v0.rs b/src/agent/onefuzz-file-format/src/coverage/binary/v0.rs new file mode 100644 index 0000000000..4cafeb50ba --- /dev/null +++ b/src/agent/onefuzz-file-format/src/coverage/binary/v0.rs @@ -0,0 +1,80 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +use anyhow::Result; +use coverage::binary::{BinaryCoverage, Count, ModuleBinaryCoverage}; +use debuggable_module::path::FilePath; +use debuggable_module::Offset; +use serde::{Deserialize, Serialize}; + +#[derive(Deserialize, Serialize)] +#[serde(transparent)] +pub struct BinaryCoverageJson { + #[serde(flatten)] + pub modules: Vec, +} + +#[derive(Deserialize, Serialize)] +pub struct BinaryModuleCoverageJson { + pub module: String, + pub blocks: Vec, +} + +#[derive(Deserialize, Serialize)] +pub struct BinaryBlockCoverageJson { + pub offset: u32, + pub count: u32, +} + +impl TryFrom for BinaryCoverageJson { + type Error = anyhow::Error; + + fn try_from(binary: BinaryCoverage) -> Result { + let mut modules = Vec::new(); + + for (module, offsets) in binary.modules { + let mut blocks = Vec::new(); + + for (offset, count) in offsets.as_ref() { + let offset = u32::try_from(offset.0)?; + let count = count.0; + let block = BinaryBlockCoverageJson { offset, count }; + blocks.push(block); + } + + let module = module.as_str().to_owned(); + let module = BinaryModuleCoverageJson { module, blocks }; + + modules.push(module); + } + + Ok(Self { modules }) + } +} + +impl TryFrom for BinaryCoverage { + type Error = anyhow::Error; + + fn try_from(json: BinaryCoverageJson) -> Result { + let mut process = BinaryCoverage::default(); + + for coverage_json in json.modules { + let mut coverage = ModuleBinaryCoverage::default(); + + for block in coverage_json.blocks { + let offset = Offset(u64::from(block.offset)); + let count = Count(block.count); + coverage.offsets.insert(offset, count); + } + + let path = FilePath::new(coverage_json.module)?; + + process.modules.insert(path, coverage); + } + + Ok(process) + } +} + +#[cfg(test)] +mod tests; diff --git a/src/agent/onefuzz-file-format/src/coverage/binary/v0/tests.rs b/src/agent/onefuzz-file-format/src/coverage/binary/v0/tests.rs new file mode 100644 index 0000000000..1ec4641cb5 --- /dev/null +++ b/src/agent/onefuzz-file-format/src/coverage/binary/v0/tests.rs @@ -0,0 +1,73 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +use anyhow::Result; +use pretty_assertions::assert_eq; +use serde_json::json; + +use super::*; + +const MAIN_EXE: &str = "/setup/main.exe"; +const SOME_DLL: &str = "/setup/lib/some.dll"; + +const EXPECTED: &str = r#" +[ + { + "module": "/setup/main.exe", + "blocks": [ + { + "offset": 1, + "count": 0 + }, + { + "offset": 300, + "count": 1 + }, + { + "offset": 5000, + "count": 0 + } + ] + }, + { + "module": "/setup/lib/some.dll", + "blocks": [ + { + "offset": 123, + "count": 0 + }, + { + "offset": 456, + "count": 10 + } + ] + } +] +"#; + +#[test] +fn test_serialize_deseralize() -> Result<()> { + let value = json!([ + { + "module": MAIN_EXE, + "blocks": [ + { "offset": 1, "count": 0 }, + { "offset": 300, "count": 1 }, + { "offset": 5000, "count": 0 }, + ], + }, + { + "module": SOME_DLL, + "blocks": [ + { "offset": 123, "count": 0 }, + { "offset": 456, "count": 10 }, + ], + }, + ]); + let coverage: BinaryCoverageJson = serde_json::from_value(value)?; + + let text = serde_json::to_string_pretty(&coverage)?; + assert_eq!(text.trim(), EXPECTED.trim()); + + Ok(()) +} diff --git a/src/agent/onefuzz-file-format/src/coverage/binary/v1.rs b/src/agent/onefuzz-file-format/src/coverage/binary/v1.rs new file mode 100644 index 0000000000..d1b97d96a1 --- /dev/null +++ b/src/agent/onefuzz-file-format/src/coverage/binary/v1.rs @@ -0,0 +1,58 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +use std::collections::BTreeMap; + +use anyhow::Result; +use coverage::binary::{BinaryCoverage, Count, ModuleBinaryCoverage}; +use debuggable_module::path::FilePath; +use debuggable_module::Offset; + +use crate::hex::Hex; + +#[derive(Deserialize, Serialize)] +pub struct BinaryCoverageJson { + #[serde(flatten)] + pub modules: BTreeMap>, +} + +impl From for BinaryCoverageJson { + fn from(binary: BinaryCoverage) -> Self { + let mut modules = BTreeMap::new(); + + for (module, offsets) in &binary.modules { + let mut map: BTreeMap = BTreeMap::new(); + + for (offset, count) in offsets.as_ref() { + map.insert(Hex(offset.0), count.0); + } + + let path = module.as_str().to_owned(); + modules.insert(path, map); + } + + Self { modules } + } +} + +impl TryFrom for BinaryCoverage { + type Error = anyhow::Error; + + fn try_from(json: BinaryCoverageJson) -> Result { + let mut process = BinaryCoverage::default(); + + for (module, offsets) in json.modules { + let mut coverage = ModuleBinaryCoverage::default(); + + for (hex, count) in offsets { + let offset = Offset(hex.0); + coverage.offsets.insert(offset, Count(count)); + } + + let path = FilePath::new(module)?; + process.modules.insert(path, coverage); + } + + Ok(process) + } +} diff --git a/src/agent/onefuzz-file-format/src/coverage/cobertura.rs b/src/agent/onefuzz-file-format/src/coverage/cobertura.rs new file mode 100644 index 0000000000..6842c50c5b --- /dev/null +++ b/src/agent/onefuzz-file-format/src/coverage/cobertura.rs @@ -0,0 +1,492 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. +#![allow(clippy::field_reassign_with_default)] + +use std::collections::{BTreeMap, BTreeSet}; +use std::io::{Cursor, Write}; + +use coverage::source::SourceCoverage; +use debuggable_module::path::FilePath; +use quick_xml::{Result, Writer}; + +impl CoberturaCoverage { + pub fn to_string(&self) -> anyhow::Result { + let mut data = Vec::new(); + let cursor = Cursor::new(&mut data); + + let mut writer = Writer::new_with_indent(cursor, b' ', 2); + + self.write_xml(&mut writer)?; + + let text = String::from_utf8(data)?; + Ok(text) + } +} + +trait WriteXml { + fn write_xml(&self, writer: &mut Writer) -> Result<()>; +} + +// Only write optional fields if present. +impl WriteXml for Option +where + T: WriteXml, +{ + fn write_xml(&self, writer: &mut Writer) -> Result<()> { + if let Some(value) = self { + value.write_xml(writer)?; + } + + Ok(()) + } +} + +impl WriteXml for Vec +where + T: WriteXml, +{ + fn write_xml(&self, writer: &mut Writer) -> Result<()> { + for value in self { + value.write_xml(writer)?; + } + + Ok(()) + } +} + +macro_rules! float { + ($val: expr) => { + format!("{:.02}", $val).as_str() + }; +} + +macro_rules! uint { + ($val: expr) => { + format!("{}", $val).as_str() + }; +} + +macro_rules! boolean { + ($val: expr) => { + format!("{}", $val).as_str() + }; +} + +macro_rules! string { + ($val: expr) => { + &*quick_xml::escape::escape(&$val) + }; +} + +// +// +// +// +// +// +// +// +// +// +#[derive(Clone, Debug, Default)] +pub struct CoberturaCoverage { + pub sources: Option, + pub packages: Packages, + + pub line_rate: f64, + pub branch_rate: f64, + pub lines_covered: u64, + pub lines_valid: u64, + pub branches_covered: u64, + pub branches_valid: u64, + pub complexity: u64, + pub version: String, + pub timestamp: u64, +} + +impl WriteXml for CoberturaCoverage { + fn write_xml(&self, writer: &mut Writer) -> Result<()> { + writer + .create_element("coverage") + .with_attributes([ + ("line-rate", float!(self.line_rate)), + ("branch-rate", float!(self.branch_rate)), + ("lines-covered", uint!(self.lines_covered)), + ("lines-valid", uint!(self.lines_valid)), + ("branches-covered", uint!(self.branches_covered)), + ("branches-valid", uint!(self.branches_valid)), + ("complexity", uint!(self.complexity)), + ("version", string!(self.version)), + ("timestamp", uint!(self.timestamp)), + ]) + .write_inner_content(|w| { + self.sources.write_xml(w)?; + self.packages.write_xml(w)?; + + Ok(()) + })?; + + Ok(()) + } +} + +// +#[derive(Clone, Debug, Default)] +pub struct Sources { + pub sources: Vec, +} + +impl WriteXml for Sources { + fn write_xml(&self, writer: &mut Writer) -> Result<()> { + writer + .create_element("sources") + .write_inner_content(|w| self.sources.write_xml(w))?; + + Ok(()) + } +} + +// +#[derive(Clone, Debug, Default)] +pub struct Source { + pub path: String, +} + +impl WriteXml for Source { + fn write_xml(&self, writer: &mut Writer) -> Result<()> { + writer + .create_element("source") + .with_attributes([("path", string!(self.path))]) + .write_empty()?; + + Ok(()) + } +} + +// +#[derive(Clone, Debug, Default)] +pub struct Packages { + pub packages: Vec, +} + +impl WriteXml for Packages { + fn write_xml(&self, writer: &mut Writer) -> Result<()> { + writer + .create_element("packages") + .write_inner_content(|w| self.packages.write_xml(w))?; + + Ok(()) + } +} + +// +// +// +// +// +#[derive(Clone, Debug, Default)] +pub struct Package { + pub classes: Classes, + + pub name: String, + pub line_rate: f64, + pub branch_rate: f64, + pub complexity: u64, +} + +impl WriteXml for Package { + fn write_xml(&self, writer: &mut Writer) -> Result<()> { + writer + .create_element("package") + .with_attributes([ + ("name", string!(self.name)), + ("line-rate", float!(self.line_rate)), + ("branch-rate", float!(self.branch_rate)), + ("complexity", uint!(self.complexity)), + ]) + .write_inner_content(|w| self.classes.write_xml(w))?; + + Ok(()) + } +} + +// +#[derive(Clone, Debug, Default)] +pub struct Classes { + pub classes: Vec, +} + +impl WriteXml for Classes { + fn write_xml(&self, writer: &mut Writer) -> Result<()> { + writer + .create_element("classes") + .write_inner_content(|w| self.classes.write_xml(w))?; + + Ok(()) + } +} + +// +// +// +// +// +// +#[derive(Clone, Debug, Default)] +pub struct Class { + pub methods: Methods, + pub lines: Lines, + + pub name: String, + pub filename: String, + pub line_rate: f64, + pub branch_rate: f64, + pub complexity: u64, +} + +impl WriteXml for Class { + fn write_xml(&self, writer: &mut Writer) -> Result<()> { + writer + .create_element("class") + .with_attributes([ + ("name", string!(self.name)), + ("filename", string!(self.filename)), + ("line-rate", float!(self.line_rate)), + ("branch-rate", float!(self.branch_rate)), + ("complexity", uint!(self.complexity)), + ]) + .write_inner_content(|w| { + self.methods.write_xml(w)?; + self.lines.write_xml(w)?; + Ok(()) + })?; + + Ok(()) + } +} + +// +#[derive(Clone, Debug, Default)] +pub struct Methods { + pub methods: Vec, +} + +impl WriteXml for Methods { + fn write_xml(&self, writer: &mut Writer) -> Result<()> { + writer + .create_element("methods") + .write_inner_content(|w| self.methods.write_xml(w))?; + + Ok(()) + } +} + +// +// +// +// +// +#[derive(Clone, Debug, Default)] +pub struct Method { + pub lines: Lines, + + pub name: String, + pub signature: String, + pub line_rate: f64, + pub branch_rate: f64, +} + +impl WriteXml for Method { + fn write_xml(&self, writer: &mut Writer) -> Result<()> { + writer + .create_element("method") + .with_attributes([ + ("name", string!(self.name)), + ("signature", string!(self.signature)), + ("line-rate", float!(self.line_rate)), + ("branch-rate", float!(self.branch_rate)), + ]) + .write_inner_content(|w| self.lines.write_xml(w))?; + + Ok(()) + } +} + +// +#[derive(Clone, Debug, Default)] +pub struct Lines { + pub lines: Vec, +} + +impl WriteXml for Lines { + fn write_xml(&self, writer: &mut Writer) -> Result<()> { + writer + .create_element("lines") + .write_inner_content(|w| self.lines.write_xml(w))?; + + Ok(()) + } +} + +// +// +// +// +// +#[derive(Clone, Debug, Default)] +pub struct Line { + pub conditions: Conditions, + + pub number: u64, + pub hits: u64, + pub branch: Option, + pub condition_coverage: Option, +} + +impl WriteXml for Line { + fn write_xml(&self, writer: &mut Writer) -> Result<()> { + let condition_coverage = if let Some(s) = &self.condition_coverage { + s.as_str() + } else { + "100%" + }; + + writer + .create_element("line") + .with_attributes([ + ("number", uint!(self.number)), + ("hits", uint!(self.hits)), + ("branch", boolean!(self.branch.unwrap_or_default())), + ("condition-coverage", condition_coverage), + ]) + .write_inner_content(|w| self.conditions.write_xml(w))?; + + Ok(()) + } +} + +// +#[derive(Clone, Debug, Default)] +pub struct Conditions { + pub conditions: Vec, +} + +impl WriteXml for Conditions { + fn write_xml(&self, writer: &mut Writer) -> Result<()> { + writer + .create_element("conditions") + .write_inner_content(|w| self.conditions.write_xml(w))?; + + Ok(()) + } +} + +// +// +// +// +#[derive(Clone, Debug, Default)] +pub struct Condition { + pub number: u64, + pub r#type: u64, + pub coverage: u64, +} + +impl WriteXml for Condition { + fn write_xml(&self, writer: &mut Writer) -> Result<()> { + writer + .create_element("condition") + .with_attributes([ + ("number", uint!(self.number)), + ("type", uint!(self.r#type)), + ("coverage", uint!(self.coverage)), + ]) + .write_empty()?; + + Ok(()) + } +} + +// Dir -> Set +type FileMap<'a> = BTreeMap<&'a str, BTreeSet<&'a FilePath>>; + +impl From for CoberturaCoverage { + fn from(source: SourceCoverage) -> Self { + // The Cobertura data model is organized around `classes` and `methods` contained + // in `packages`. Our source coverage has no language-level assumptions. + // + // To obtain legible HTML reports using ReportGenerator, will we use `` + // elements to group files by their parent directory. Each measured source file + // will be represented a ``. The and the measured source file's lines will + // become `` elements of the (synthetic) class. + // + // Note: ReportGenerator automatically computes and rolls up aggregated coverage + // stats. We do _not_ need to manually compute any `line-rate` attributes. The + // presence of these attributes is required by the Cobertura schema, but even if + // they are set to 0 (as in our `Default` impls), ReportGenerator ignores them. + + // Source files grouped by directory. + let mut file_map = FileMap::default(); + + for file_path in source.files.keys() { + let dir = file_path.directory(); + let files = file_map.entry(dir).or_default(); + files.insert(file_path); + } + + // Iterate through the grouped files, accumulating `` elements. + let mut packages = vec![]; + let mut sources = vec![]; + + for (directory, files) in file_map { + // Make a `` to represent the directory. + // + // We will add a `` for each contained file. + let mut package = Package::default(); + package.name = directory.to_owned(); + + let mut classes = vec![]; + + for file_path in files { + // Add the file to the `` manifest element. + let src = Source { + path: file_path.to_string(), + }; + sources.push(src); + + let mut lines = vec![]; + + // Can't panic, by construction. + let file_coverage = &source.files[file_path]; + + for (line, count) in &file_coverage.lines { + let number = u64::from(line.number()); + let hits = u64::from(count.0); + + let mut line = Line::default(); + line.number = number; + line.hits = hits; + + lines.push(line); + } + + let mut class = Class::default(); + class.name = file_path.file_name().to_owned(); + class.filename = file_path.to_string(); + class.lines = Lines { lines }; + + classes.push(class); + } + + package.classes = Classes { classes }; + + packages.push(package); + } + + let mut xml = CoberturaCoverage::default(); + xml.sources = Some(Sources { sources }); + xml.packages = Packages { packages }; + + xml + } +} diff --git a/src/agent/onefuzz-file-format/src/coverage/source.rs b/src/agent/onefuzz-file-format/src/coverage/source.rs new file mode 100644 index 0000000000..6a74402862 --- /dev/null +++ b/src/agent/onefuzz-file-format/src/coverage/source.rs @@ -0,0 +1,31 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +use anyhow::Result; +use coverage::source::SourceCoverage; + +pub mod v0; +pub mod v1; + +#[derive(Serialize, Deserialize)] +#[serde(tag = "version")] +pub enum SourceCoverageJson { + #[serde(rename = "0")] + V0(v0::SourceCoverageJson), + + #[serde(rename = "1")] + V1(v1::SourceCoverageJson), +} + +impl TryFrom for SourceCoverage { + type Error = anyhow::Error; + + fn try_from(json: SourceCoverageJson) -> Result { + use SourceCoverageJson::*; + + match json { + V0(v0) => v0.try_into(), + V1(v1) => v1.try_into(), + } + } +} diff --git a/src/agent/onefuzz-file-format/src/coverage/source/v0.rs b/src/agent/onefuzz-file-format/src/coverage/source/v0.rs new file mode 100644 index 0000000000..26de83596d --- /dev/null +++ b/src/agent/onefuzz-file-format/src/coverage/source/v0.rs @@ -0,0 +1,59 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +use anyhow::Result; +use coverage::source::{Count, FileCoverage, Line, SourceCoverage}; +use debuggable_module::path::FilePath; +use serde::{Deserialize, Serialize}; + +#[derive(Clone, Debug, Default, Deserialize, PartialEq, Eq, Serialize)] +#[serde(transparent)] +pub struct SourceCoverageJson { + pub files: Vec, +} + +#[derive(Clone, Debug, Default, Deserialize, PartialEq, Eq, Serialize)] +pub struct SourceFile { + /// UTF-8 encoding of the path to the source file. + pub file: String, + + pub locations: Vec, +} + +#[derive(Clone, Debug, Default, Deserialize, PartialEq, Eq, Serialize)] +pub struct Location { + /// Line number of entry in `file` (1-indexed). + pub line: u32, + + /// Optional column offset (0-indexed). + /// + /// When column offsets are present, they should be interpreted as the start + /// of a span bounded by the next in-line column offset (or end-of-line). + pub column: Option, + + /// Execution count at location. + pub count: u32, +} + +impl TryFrom for SourceCoverage { + type Error = anyhow::Error; + + fn try_from(json: SourceCoverageJson) -> Result { + let mut source = SourceCoverage::default(); + + for file in json.files { + let file_path = FilePath::new(&file.file)?; + let mut file_coverage = FileCoverage::default(); + + for location in file.locations { + let line = Line::new(location.line)?; + let count = Count(location.count); + file_coverage.lines.insert(line, count); + } + + source.files.insert(file_path, file_coverage); + } + + Ok(source) + } +} diff --git a/src/agent/onefuzz-file-format/src/coverage/source/v1.rs b/src/agent/onefuzz-file-format/src/coverage/source/v1.rs new file mode 100644 index 0000000000..4a110795c0 --- /dev/null +++ b/src/agent/onefuzz-file-format/src/coverage/source/v1.rs @@ -0,0 +1,43 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +use std::collections::BTreeMap; + +use anyhow::Result; +use coverage::source::{Count, FileCoverage, Line, SourceCoverage}; +use debuggable_module::path::FilePath; +use serde::{Deserialize, Serialize}; + +pub type SourceFile = String; +pub type LineNumber = u32; +pub type HitCount = u32; + +#[derive(Deserialize, Serialize)] +pub struct SourceCoverageJson { + #[serde(flatten)] + pub modules: BTreeMap>, +} + +impl TryFrom for SourceCoverage { + type Error = anyhow::Error; + + fn try_from(json: SourceCoverageJson) -> Result { + let mut source = SourceCoverage::default(); + + for (file_path, lines) in json.modules { + let file_path = FilePath::new(file_path)?; + + let mut file = FileCoverage::default(); + + for (line, count) in lines { + let line = Line::new(line)?; + let count = Count(count); + file.lines.insert(line, count); + } + + source.files.insert(file_path, file); + } + + Ok(source) + } +} diff --git a/src/agent/onefuzz-file-format/src/hex.rs b/src/agent/onefuzz-file-format/src/hex.rs new file mode 100644 index 0000000000..ec909275cd --- /dev/null +++ b/src/agent/onefuzz-file-format/src/hex.rs @@ -0,0 +1,23 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +use serde::{Deserialize, Deserializer, Serialize, Serializer}; + +#[derive(Clone, Copy, Debug, Deserialize, Eq, Ord, PartialEq, PartialOrd, Serialize)] +pub struct Hex(#[serde(with = "self")] pub u64); + +pub fn serialize(val: &u64, serializer: S) -> Result +where + S: Serializer, +{ + let s = format!("{:x}", val); + serializer.serialize_str(&s) +} + +pub fn deserialize<'de, D>(deserializer: D) -> Result +where + D: Deserializer<'de>, +{ + let s = String::deserialize(deserializer)?; + u64::from_str_radix(&s, 16).map_err(serde::de::Error::custom) +} diff --git a/src/agent/onefuzz-file-format/src/lib.rs b/src/agent/onefuzz-file-format/src/lib.rs new file mode 100644 index 0000000000..3e51884f90 --- /dev/null +++ b/src/agent/onefuzz-file-format/src/lib.rs @@ -0,0 +1,8 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +#[macro_use] +extern crate serde; + +pub mod coverage; +mod hex; diff --git a/src/agent/onefuzz-file-format/test-data/fuzz.c b/src/agent/onefuzz-file-format/test-data/fuzz.c new file mode 100644 index 0000000000..271a5263ec --- /dev/null +++ b/src/agent/onefuzz-file-format/test-data/fuzz.c @@ -0,0 +1,45 @@ +#include +#include +#include + +#include "fuzz.h" + +int LLVMFuzzerTestOneInput(uint8_t *data, size_t len) { + if (len < 4) { return 0; } + + int hit = 0; + + // Multiple statements per line. + if (data[0] == 'b') { hit++; } + + // One statement per line. + if (data[1] == 'a') { + hit++; + } + + // Access separate from comparison. + char c = data[2]; + if (c == 'd') { + hit++; + } + + // Switch. + switch (data[3]) { + case '!': { + hit++; + break; + } + default: { + // Do nothing. + } + } + + if (len > 4 && data[4] == '!') { + // Also used in `check_hit_count()`. + explode(); + } + + check_hit_count(hit); + + return 0; +} diff --git a/src/agent/onefuzz-file-format/test-data/fuzz.h b/src/agent/onefuzz-file-format/test-data/fuzz.h new file mode 100644 index 0000000000..dfed7e2701 --- /dev/null +++ b/src/agent/onefuzz-file-format/test-data/fuzz.h @@ -0,0 +1,7 @@ +#include "lib/explode.h" + +void check_hit_count(int hit) { + if (hit > 3) { + explode(); + } +} diff --git a/src/agent/onefuzz-file-format/test-data/lib/explode.h b/src/agent/onefuzz-file-format/test-data/lib/explode.h new file mode 100644 index 0000000000..1aaddbe9cc --- /dev/null +++ b/src/agent/onefuzz-file-format/test-data/lib/explode.h @@ -0,0 +1,4 @@ +void explode() { + int *ptr = (int *) 0xdead; + *ptr = 0x123; +} diff --git a/src/agent/onefuzz-file-format/test-script/generate-report.ps1 b/src/agent/onefuzz-file-format/test-script/generate-report.ps1 new file mode 100644 index 0000000000..c5eed238c0 --- /dev/null +++ b/src/agent/onefuzz-file-format/test-script/generate-report.ps1 @@ -0,0 +1,7 @@ +cargo run --example cobertura > coverage.xml + +# To install: +# +# dotnet tool install --global dotnet-reportgenerator-globaltool --version 4.6.1 +# +reportgenerator.exe -sourcedirs:test-data -reports:coverage.xml -targetdir:reports -reporttypes:HtmlInline_AzurePipelines diff --git a/src/agent/onefuzz-file-format/test-script/generate-report.sh b/src/agent/onefuzz-file-format/test-script/generate-report.sh new file mode 100644 index 0000000000..2057c444d0 --- /dev/null +++ b/src/agent/onefuzz-file-format/test-script/generate-report.sh @@ -0,0 +1,10 @@ +#!/usr/bin/env bash +set -ex -o pipefail + +cargo run --example cobertura > coverage.xml + +# To install: +# +# dotnet tool install --global dotnet-reportgenerator-globaltool --version 4.6.1 +# +reportgenerator -sourcedirs:test-data -reports:coverage.xml -targetdir:reports -reporttypes:HtmlInline_AzurePipelines