1use super::{Advisory, Category, parts};
7use crate::advisory::license::License;
8use crate::fs;
9use std::str::FromStr;
10use std::{fmt, path::Path};
11
12#[derive(Debug)]
14pub struct Linter {
15 advisory: Advisory,
17
18 errors: Vec<Error>,
20}
21
22impl Linter {
23 pub fn lint_file<P: AsRef<Path>>(path: P) -> Result<Self, crate::Error> {
25 let path = path.as_ref();
26
27 match path.extension().and_then(|ext| ext.to_str()) {
28 Some("md") => (),
29 other => fail!(
30 crate::ErrorKind::Parse,
31 "invalid advisory file extension: {}",
32 other.unwrap_or("(missing)")
33 ),
34 }
35
36 let advisory_data = fs::read_to_string(path).map_err(|e| {
37 crate::Error::with_source(
38 crate::ErrorKind::Io,
39 format!("couldn't open {}", path.display()),
40 e,
41 )
42 })?;
43
44 Self::lint_string(&advisory_data)
45 }
46
47 pub fn lint_string(s: &str) -> Result<Self, crate::Error> {
49 let advisory = s.parse::<Advisory>()?;
51
52 let advisory_parts = parts::Parts::parse(s)?;
54 let front_matter = advisory_parts
55 .front_matter
56 .parse::<toml::Table>()
57 .map_err(crate::Error::from_toml)?;
58
59 let mut linter = Self {
60 advisory,
61 errors: vec![],
62 };
63
64 linter.lint_advisory(&front_matter);
65 Ok(linter)
66 }
67
68 pub fn advisory(&self) -> &Advisory {
70 &self.advisory
71 }
72
73 pub fn errors(&self) -> &[Error] {
75 self.errors.as_slice()
76 }
77
78 fn lint_advisory(&mut self, advisory: &toml::Table) {
80 for (key, value) in advisory {
81 match key.as_str() {
82 "advisory" => self.lint_metadata(value),
83 "versions" => self.lint_versions(value),
84 "affected" => self.lint_affected(value),
85 _ => self.errors.push(Error {
86 kind: ErrorKind::key(key),
87 section: None,
88 message: None,
89 }),
90 }
91 }
92 }
93
94 fn lint_metadata(&mut self, metadata: &toml::Value) {
96 let mut year = None;
97
98 if let Some(table) = metadata.as_table() {
99 for (key, value) in table {
100 match key.as_str() {
101 "id" => {
102 if self.advisory.metadata.id.is_other() {
103 self.errors.push(Error {
104 kind: ErrorKind::value("id", value.to_string()),
105 section: Some("advisory"),
106 message: Some("unknown advisory ID type"),
107 });
108 } else if let Some(y1) = self.advisory.metadata.id.year() {
109 if !self.advisory.metadata.id.is_cve() {
111 if let Some(y2) = year {
112 if y1 != y2 {
113 self.errors.push(Error {
114 kind: ErrorKind::value("id", value.to_string()),
115 section: Some("advisory"),
116 message: Some(
117 "year in advisory ID does not match date",
118 ),
119 });
120 }
121 } else {
122 year = Some(y1);
123 }
124 }
125 }
126 }
127 "categories" => {
128 for category in &self.advisory.metadata.categories {
129 if let Category::Other(other) = category {
130 self.errors.push(Error {
131 kind: ErrorKind::value("category", other.to_string()),
132 section: Some("advisory"),
133 message: Some("unknown category"),
134 });
135 }
136 }
137 }
138 "collection" => self.errors.push(Error {
139 kind: ErrorKind::Malformed,
140 section: Some("advisory"),
141 message: Some("collection shouldn't be explicit; inferred by location"),
142 }),
143 "informational" => {
144 let informational = self
145 .advisory
146 .metadata
147 .informational
148 .as_ref()
149 .expect("parsed informational");
150
151 if informational.is_other() {
152 self.errors.push(Error {
153 kind: ErrorKind::value("informational", informational.as_str()),
154 section: Some("advisory"),
155 message: Some("unknown informational advisory type"),
156 });
157 }
158 }
159 "url" => {
160 if let Some(url) = value.as_str() {
161 if !url.starts_with("https://") {
162 self.errors.push(Error {
163 kind: ErrorKind::value("url", value.to_string()),
164 section: Some("advisory"),
165 message: Some("URL must start with https://"),
166 });
167 }
168 }
169 }
170 "date" => {
171 let y1 = self.advisory.metadata.date.year();
172
173 if let Some(y2) = year {
174 if y1 != y2 {
175 self.errors.push(Error {
176 kind: ErrorKind::value("date", value.to_string()),
177 section: Some("advisory"),
178 message: Some("year in advisory ID does not match date"),
179 });
180 }
181 } else {
182 year = Some(y1);
183 }
184 }
185 "yanked" => {
186 if self.advisory.metadata.withdrawn.is_none() {
187 self.errors.push(Error {
188 kind: ErrorKind::Malformed,
189 section: Some("metadata"),
190 message: Some(
191 "Field `yanked` is deprecated, use `withdrawn` field instead",
192 ),
193 });
194 }
195 }
196 "license" => {
197 if let Some(l) = value.as_str() {
198 let unknown_license =
200 matches!(License::from_str(l).unwrap(), License::Other(_));
201 if unknown_license {
202 self.errors.push(Error {
203 kind: ErrorKind::value("license", l.to_string()),
204 section: Some("advisory"),
205 message: Some("Unknown license"),
206 });
207 }
208 }
209 }
210 "aliases" | "cvss" | "keywords" | "package" | "references" | "related"
211 | "title" | "withdrawn" | "description" | "expect-deleted" => (),
212 _ => self.errors.push(Error {
213 kind: ErrorKind::key(key),
214 section: Some("advisory"),
215 message: None,
216 }),
217 }
218 }
219 } else {
220 self.errors.push(Error {
221 kind: ErrorKind::Malformed,
222 section: Some("advisory"),
223 message: Some("expected table"),
224 });
225 }
226 }
227
228 fn lint_versions(&mut self, versions: &toml::Value) {
230 if let Some(table) = versions.as_table() {
231 for (key, _) in table {
232 match key.as_str() {
233 "patched" | "unaffected" => (),
234 _ => self.errors.push(Error {
235 kind: ErrorKind::key(key),
236 section: Some("versions"),
237 message: None,
238 }),
239 }
240 }
241 }
242 }
243
244 fn lint_affected(&mut self, affected: &toml::Value) {
246 if let Some(table) = affected.as_table() {
247 for (key, _) in table {
248 match key.as_str() {
249 "functions" => {
250 for function in self.advisory.affected.as_ref().unwrap().functions.keys() {
251 let crate_name =
254 self.advisory.metadata.package.as_str().replace('-', "_");
255 if function.segments()[0].as_str() != crate_name {
256 self.errors.push(Error {
257 kind: ErrorKind::value("functions", function.to_string()),
258 section: Some("affected"),
259 message: Some("function path must start with crate name"),
260 });
261 }
262 }
263 }
264 "arch" | "os" => (),
265 _ => self.errors.push(Error {
266 kind: ErrorKind::key(key),
267 section: Some("affected"),
268 message: None,
269 }),
270 }
271 }
272 }
273 }
274}
275
276#[derive(Clone, Debug, Eq, PartialEq)]
278pub struct Error {
279 kind: ErrorKind,
281
282 section: Option<&'static str>,
284
285 message: Option<&'static str>,
287}
288
289impl Error {
290 pub fn kind(&self) -> &ErrorKind {
292 &self.kind
293 }
294
295 pub fn section(&self) -> Option<&str> {
297 self.section.as_ref().map(AsRef::as_ref)
298 }
299
300 pub fn message(&self) -> Option<&str> {
302 self.message.as_ref().map(AsRef::as_ref)
303 }
304}
305
306impl fmt::Display for Error {
307 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
308 write!(f, "{}", &self.kind)?;
309
310 if let Some(section) = &self.section {
311 write!(f, " in [{section}]")?;
312 } else {
313 write!(f, " in toplevel")?;
314 }
315
316 if let Some(msg) = &self.message {
317 write!(f, ": {msg}")?
318 }
319
320 Ok(())
321 }
322}
323
324#[derive(Clone, Debug, Eq, PartialEq)]
326#[non_exhaustive]
327pub enum ErrorKind {
328 Malformed,
330
331 InvalidKey {
333 name: String,
335 },
336
337 InvalidValue {
339 name: String,
341
342 value: String,
344 },
345}
346
347impl ErrorKind {
348 pub fn key(name: &str) -> Self {
350 ErrorKind::InvalidKey {
351 name: name.to_owned(),
352 }
353 }
354
355 pub fn value(name: &str, value: impl Into<String>) -> Self {
357 ErrorKind::InvalidValue {
358 name: name.to_owned(),
359 value: value.into(),
360 }
361 }
362}
363
364impl fmt::Display for ErrorKind {
365 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
366 match self {
367 ErrorKind::Malformed => write!(f, "malformed content"),
368 ErrorKind::InvalidKey { name } => write!(f, "invalid key `{name}`"),
369 ErrorKind::InvalidValue { name, value } => {
370 write!(f, "invalid value `{value}` for key `{name}`")
371 }
372 }
373 }
374}