#![allow(clippy::question_mark)]
use crate::advisories::model;
use anyhow::Context as _;
use semver::VersionReq;
struct ArrayIter {
arr: &'static str,
inner: memchr::Memchr<'static>,
}
impl ArrayIter {
fn new(toml: &'static str, line: Line, liter: &mut std::iter::Peekable<LineIter>) -> Self {
let start = memchr::memchr(b'[', line.s.as_bytes()).expect("no array opener");
let arr = if let Some(end) = memchr::memchr(b']', line.s.as_bytes()) {
&line.s[start + 1..end]
} else {
let arr_end = 'end: {
for l in liter.by_ref() {
if let Some(end) = memchr::memchr(b']', l.s.as_bytes()) {
break 'end l.start + end;
}
}
panic!("unclosed '['");
};
&toml[line.start + start..arr_end]
};
Self {
arr,
inner: memchr::memchr_iter(b'"', arr.as_bytes()),
}
}
}
impl Iterator for ArrayIter {
type Item = &'static str;
#[inline]
fn next(&mut self) -> Option<Self::Item> {
let Some(start) = self.inner.next() else {
return None;
};
let Some(end) = self.inner.next() else {
return None;
};
Some(&self.arr[start + 1..end])
}
}
#[derive(Copy, Clone)]
struct Line {
s: &'static str,
start: usize,
}
impl Line {
#[inline]
fn skip(self) -> bool {
self.s.trim().is_empty() || self.s.starts_with('#')
}
#[inline]
fn pair(self) -> anyhow::Result<(&'static str, &'static str)> {
let split = memchr::memchr(b'=', self.s.as_bytes())
.with_context(|| format!("line `{}` did not follow expected format", self.s))?;
Ok((self.s[..split].trim(), self.s[split + 1..].trim()))
}
}
struct LineIter {
start: usize,
v: &'static str,
inner: memchr::Memchr<'static>,
}
impl LineIter {
#[inline]
fn new(v: &'static str) -> Self {
Self {
start: 0,
v,
inner: memchr::memchr_iter(b'\n', v.as_bytes()),
}
}
}
impl Iterator for LineIter {
type Item = Line;
#[inline]
fn next(&mut self) -> Option<Self::Item> {
let Some(end) = self.inner.next() else {
return None;
};
let s = &self.v[self.start..end];
let start = self.start;
self.start = end + 1;
Some(Line { s, start })
}
}
struct StringIter {
s: &'static str,
inner: memchr::Memchr<'static>,
}
impl StringIter {
#[inline]
fn new(s: &'static str) -> Self {
Self {
s,
inner: memchr::memchr_iter(b'"', s.as_bytes()),
}
}
}
impl Iterator for StringIter {
type Item = &'static str;
#[inline]
fn next(&mut self) -> Option<Self::Item> {
let Some(start) = self.inner.next() else {
return None;
};
let Some(end) = self.inner.next() else {
return None;
};
Some(&self.s[start + 1..end])
}
}
#[inline]
pub(super) fn parse(b: &'static [u8]) -> anyhow::Result<model::Advisory<'static>> {
let whole = std::str::from_utf8(b)?;
let tstart = whole.find("```toml").context("failed to find toml block")?;
let s = &whole[tstart + 7..];
let tend = s.find("```").context("failed to find end of toml block")?;
let rest = &s[tend + 4..];
let toml = &s[..tend];
let mut adv = parse_toml(toml)?;
let mut start = 0;
for end in memchr::Memchr::new(b'\n', rest.as_bytes()) {
let line = &rest[start..end];
start = end + 1;
if let Some(title) = line.strip_prefix("# ") {
adv.advisory.title = title.trim_end();
break;
}
}
adv.advisory.description = rest[start..].trim();
Ok(adv)
}
fn parse_toml(toml: &'static str) -> anyhow::Result<model::Advisory<'static>> {
let mut liter = LineIter::new(toml).peekable();
let mut md = model::Metadata {
id: "",
krate: "",
title: "",
description: "",
date: jiff::civil::Date::constant(0, 1, 1),
aliases: Default::default(),
related: Default::default(),
categories: Default::default(),
keywords: Default::default(),
cvss: None,
informational: None,
source: None,
references: Default::default(),
url: None,
withdrawn: None,
license: Default::default(),
expect_deleted: false,
};
let mut versions = None;
let mut affected = None;
let parse_advisory = |liter: &mut std::iter::Peekable<LineIter>,
md: &mut model::Metadata<'static>|
-> anyhow::Result<()> {
while let Some(line) = liter.next_if(|l| !l.s.starts_with('[')) {
if line.skip() {
continue;
}
let (field, value) = line.pair().with_context(|| format!("TOML {toml}"))?;
let string = || -> &'static str {
let Some(start) = memchr::memchr(b'"', value.as_bytes()) else {
panic!("expected opening '\"' in `{value}`");
};
let Some(end) = memchr::memchr(b'"', &value.as_bytes()[start + 1..]) else {
panic!("expected closing '\"' in `{value}`");
};
&value[start + 1..start + 1 + end]
};
match field {
"id" => md.id = string(),
"package" => md.krate = string(),
"aliases" => {
for alias in ArrayIter::new(toml, line, liter) {
md.aliases.push(alias);
}
}
"related" => {
for id in ArrayIter::new(toml, line, liter) {
md.related.push(id);
}
}
"cvss" => md.cvss = Some(string()),
"date" => md.date = string().parse().context("failed to parse `date`")?,
"url" => md.url = Some(string()),
"informational" => {
md.informational = Some(match string() {
"unmaintained" => model::Informational::Unmaintained,
"unsound" => model::Informational::Unsound,
"notice" => model::Informational::Notice,
other => model::Informational::Other(other),
});
}
"categories" => {
for alias in ArrayIter::new(toml, line, liter) {
md.categories.push(alias);
}
}
"keywords" => {
for kw in ArrayIter::new(toml, line, liter) {
md.keywords.push(kw);
}
}
"references" => {
for r in ArrayIter::new(toml, line, liter) {
md.references.push(r);
}
}
"withdrawn" => {
md.withdrawn = Some(string().parse().context("failed to parse `withdrawn`")?);
}
"license" => {
md.license = model::AdvisoryLicense(string());
}
"source" => {
md.source = Some(
crate::Source::from_metadata(value.to_owned(), None)
.with_context(|| "failed to parse `source` field '{value}'")?,
);
}
"expect-deleted" => {
md.expect_deleted = value == "true";
}
unknown => {
log::warn!("unknown advisory field '{unknown}'");
}
}
}
Ok(())
};
let parse_versions =
|liter: &mut std::iter::Peekable<LineIter>| -> anyhow::Result<model::Versions> {
let mut v = model::Versions {
patched: Default::default(),
unaffected: Default::default(),
};
while let Some(line) = liter.next_if(|l| !l.s.starts_with('[')) {
if line.skip() {
continue;
}
let (field, _value) = line.pair()?;
match field {
"patched" => {
for vr in ArrayIter::new(toml, line, liter) {
v.patched.push(vr.parse().with_context(|| {
format!("failed to parse patched version '{vr}'")
})?);
}
}
"unaffected" => {
for vr in ArrayIter::new(toml, line, liter) {
v.unaffected.push(vr.parse().with_context(|| {
format!("failed to parse unaffected version '{vr}'")
})?);
}
}
unknown => anyhow::bail!("unknown versions field '{unknown}'"),
}
}
Ok(v)
};
let parse_affected = |liter: &mut std::iter::Peekable<LineIter>,
first: &'static str|
-> anyhow::Result<Option<model::Affected<'static>>> {
let mut affected = model::Affected {
functions: Default::default(),
os: Default::default(),
arch: Default::default(),
};
let parse_function_table = |liter: &mut std::iter::Peekable<LineIter>,
funcs: &mut std::collections::BTreeMap<
&'static str,
Vec<VersionReq>,
>| {
while let Some(line) = liter.next_if(|l| !l.s.starts_with('[')) {
if line.skip() {
continue;
}
let Some((field, _value)) = line.s.split_once(" = ") else {
continue;
};
let key = field.trim_matches('"');
let mut val = Vec::new();
for vr in ArrayIter::new(toml, line, liter) {
match vr.parse() {
Ok(vr) => val.push(vr),
Err(error) => {
log::error!(
"failed to parse version requirement for function '{key}': {error}"
);
}
}
}
funcs.insert(key, val);
}
};
if first == "[affected]" {
while let Some(line) = liter.peek() {
if line.s.starts_with('[') {
if line.s == "[affected.functions]" {
liter.next();
parse_function_table(liter, &mut affected.functions);
}
break;
}
let line = liter.next().unwrap();
if line.skip() {
continue;
}
let (field, value) = line.pair()?;
match field {
"functions" => {
let Some(start) = memchr::memchr(b'{', value.as_bytes()) else {
continue;
};
let Some(end) = memchr::memrchr(b'}', value.as_bytes()) else {
continue;
};
let mut map = value[start + 1..end].trim();
while let Some((key, value)) = map.split_once(" = ") {
let key = key.trim_matches('"');
let mut val = Vec::new();
let vstart = memchr::memchr(b'[', value.as_bytes())
.expect("function did not have a valid version array start");
let vend = memchr::memchr(b']', value.as_bytes())
.expect("function did not have a valid version array end");
for vr in StringIter::new(&value[vstart..vend]) {
match vr.parse() {
Ok(vr) => val.push(vr),
Err(error) => {
log::error!(
"failed to parse version requirement for function '{key}': {error}"
);
}
}
}
affected.functions.insert(key, val);
let Some(start) = memchr::memchr(b'"', &value.as_bytes()[vend..])
else {
break;
};
map = &value[vend + start..];
}
}
"os" => {
for os in ArrayIter::new(toml, line, liter) {
affected
.os
.push(cfg_expr::targets::Os(std::borrow::Cow::Borrowed(os)));
}
}
"arch" => {
for arch in ArrayIter::new(toml, line, liter) {
affected
.arch
.push(cfg_expr::targets::Arch(std::borrow::Cow::Borrowed(arch)));
}
}
unknown => {
log::warn!("unknown `affected` field '{unknown}'");
}
}
}
} else if first == "[affected.functions]" {
parse_function_table(liter, &mut affected.functions);
}
if affected.functions.is_empty() && affected.os.is_empty() && affected.arch.is_empty() {
Ok(None)
} else {
Ok(Some(affected))
}
};
while let Some(line) = liter.next() {
match line.s {
"[advisory]" => {
parse_advisory(&mut liter, &mut md)?;
}
"[versions]" => {
versions = Some(parse_versions(&mut liter)?);
}
aff if line.s.starts_with("[affected") => {
affected = parse_affected(&mut liter, aff)?;
}
"```" => {
break;
}
"" => {}
unknown => {
log::warn!("unknown toml table '{unknown}'");
}
}
}
Ok(model::Advisory {
advisory: md,
affected,
versions: versions.unwrap_or(model::Versions {
patched: Vec::new(),
unaffected: Vec::new(),
}),
})
}
#[cfg(test)]
mod test {
#[test]
fn split_arrays() {
let toml = r#"[advisory]
id = "RUSTSEC-2020-0146"
package = "generic-array"
date = "2020-04-09"
url = "https://github.com/fizyk20/generic-array/issues/98"
categories = ["memory-corruption"]
keywords = ["soundness"]
aliases = ["CVE-2020-36465", "GHSA-3358-4f7f-p4j4"]
cvss = "CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:N/I:N/A:H"
[versions]
patched = [
">= 0.8.4, < 0.9.0",
">= 0.9.1, < 0.10.0",
">= 0.10.1, < 0.11.0",
">= 0.11.2, < 0.12.0",
">= 0.12.4, < 0.13.0",
">= 0.13.3",
]
unaffected = ["< 0.8.0"]"#;
super::parse_toml(toml).unwrap();
}
}