use std::io::Write;
use anyhow::Result;
use pelicode_detect::{Candidate, MatchKind, Report};
use serde_json::{Value, json};
const SCHEMA: &str = "https://json.schemastore.org/sarif-2.1.0.json";
const INFORMATION_URI: &str = "https://tamada.github.io/pelicode";
const HELP_URI: &str = "https://tamada.github.io/pelicode/usage/#scan";
const EXACT: &str = "exact-match";
const SIMILAR: &str = "similar-match";
const ADVICE: &str = "Review whether the code was copied, and whether the license of its source \
requires attribution or prevents the reuse.";
pub fn write(
out: &mut dyn Write,
reports: &[Report],
uri: impl Fn(&Report) -> String,
) -> Result<()> {
let results: Vec<Value> = reports.iter().map(|r| result(r, &uri(r))).collect();
let log = json!({
"$schema": SCHEMA,
"version": "2.1.0",
"runs": [{
"tool": {
"driver": {
"name": "pelicode",
"version": env!("CARGO_PKG_VERSION"),
"informationUri": INFORMATION_URI,
"rules": [rule(EXACT), rule(SIMILAR)],
}
},
"results": results,
}]
});
serde_json::to_writer_pretty(&mut *out, &log)?;
writeln!(out)?;
Ok(())
}
fn rule(id: &str) -> Value {
let (name, short, full) = match id {
EXACT => (
"ExactMatch",
"Function identical to published code after normalization",
"The function is identical to a function of a published package once comments, \
layout and, depending on the level, the names of identifiers and types are set aside.",
),
_ => (
"SimilarMatch",
"Function similar to published code",
"The function shares many fingerprints (hashes of token sequences) with a function \
of a published package.",
),
};
json!({
"id": id,
"name": name,
"shortDescription": { "text": short },
"fullDescription": { "text": full },
"help": {
"text": format!("{full} {ADVICE}"),
"markdown": format!("{full}\n\n{ADVICE} pelicode does not decide whether code was \
plagiarized; see [pelicode scan]({HELP_URI})."),
},
"helpUri": HELP_URI,
"defaultConfiguration": { "level": "note" },
"properties": { "tags": ["provenance", "license"] },
})
}
fn result(report: &Report, uri: &str) -> Value {
let best = &report.candidates[0];
let rule_id = match best.kind {
MatchKind::Exact(_) => EXACT,
MatchKind::Similar(_) => SIMILAR,
};
let mut message = format!("`{}` {}. {ADVICE}", report.name, describe(best));
if report.candidates.len() > 1 {
let others: Vec<String> = report.candidates[1..].iter().map(source).collect();
message.push_str(&format!(" Other candidates: {}.", others.join("; ")));
}
json!({
"ruleId": rule_id,
"level": "note",
"message": { "text": message },
"locations": [{
"physicalLocation": {
"artifactLocation": { "uri": uri },
"region": {
"startLine": report.start_line,
"endLine": report.end_line,
},
}
}],
"partialFingerprints": {
"pelicodeCandidate/v1": fingerprint(uri, report, best),
},
"properties": {
"function": report.name,
"tokens": report.token_count,
"candidates": report.candidates.iter().map(candidate).collect::<Vec<_>>(),
},
})
}
fn describe(c: &Candidate) -> String {
let relation = match c.kind {
MatchKind::Exact(level) => format!("is identical after {level} normalization to"),
MatchKind::Similar(_) => format!("is similar ({:.2}) to", c.similarity),
};
let mut text = format!("{relation} {}", source(c));
if let MatchKind::Similar(_) = c.kind {
text.push_str(&format!(
"; lines {} match lines {}",
ranges(&c.matched_lines),
ranges(&c.candidate_lines)
));
}
text
}
fn source(c: &Candidate) -> String {
let location = match c.occurrences.first() {
Some(o) => format!(
"{}:{}@{} {}:{}-{}",
o.ecosystem, o.package, o.version, o.path, c.function.start_line, c.function.end_line
),
None => "an unknown package".into(),
};
let license = c
.occurrences
.first()
.and_then(|o| o.license.as_deref())
.unwrap_or("license unknown");
format!("`{}` in {location} ({license})", c.function.name)
}
fn ranges(lines: &[(u32, u32)]) -> String {
lines
.iter()
.map(|&(s, e)| {
if s == e {
s.to_string()
} else {
format!("{s}-{e}")
}
})
.collect::<Vec<_>>()
.join(", ")
}
fn candidate(c: &Candidate) -> Value {
let o = c.occurrences.first();
json!({
"match": match c.kind { MatchKind::Exact(_) => "exact", MatchKind::Similar(_) => "similar" },
"level": match c.kind { MatchKind::Exact(l) | MatchKind::Similar(l) => l.to_string() },
"similarity": c.similarity,
"containment": c.containment,
"ecosystem": o.map(|o| o.ecosystem.as_str()),
"package": o.map(|o| o.package.as_str()),
"version": o.map(|o| o.version.as_str()),
"path": o.map(|o| o.path.as_str()),
"license": o.and_then(|o| o.license.as_deref()),
"repository": o.and_then(|o| o.repository_url.as_deref()),
"function": c.function.name,
"startLine": c.function.start_line,
"endLine": c.function.end_line,
"occurrences": c.occurrence_count,
})
}
fn fingerprint(uri: &str, report: &Report, best: &Candidate) -> String {
let source = best
.occurrences
.first()
.map(|o| format!("{}:{}:{}", o.ecosystem, o.package, o.path))
.unwrap_or_default();
let key = format!(
"{uri}\u{1f}{}\u{1f}{source}\u{1f}{}",
report.name, best.function.name
);
pelicode_core::extract::sha256_hex(key.as_bytes())[..32].to_string()
}