1use crate::{
2 Error, Level, ParseError,
3 proof::{
4 self, OverrideItem, OverrideItemDraft,
5 content::{OriginalReference, ValidationError, ValidationResult},
6 },
7 serde_content_serialize, serde_draft_serialize,
8};
9use crev_common::{self, is_equal_default, is_set_empty, is_vec_empty};
10use derive_builder::Builder;
11use proof::{CommonOps, Content};
12use semver::Version;
13use serde::{Deserialize, Serialize};
14use std::{
15 collections::HashSet,
16 default::Default,
17 fmt::{self, Debug},
18 ops,
19};
20use typed_builder::TypedBuilder;
21
22const CURRENT_PACKAGE_REVIEW_PROOF_SERIALIZATION_VERSION: i64 = -1;
23
24fn cur_version() -> i64 {
25 CURRENT_PACKAGE_REVIEW_PROOF_SERIALIZATION_VERSION
26}
27
28#[derive(Clone, Builder, Debug, Serialize, Deserialize, Default, PartialEq, Eq)]
30pub struct Flags {
31 #[serde(default = "Default::default", skip_serializing_if = "is_equal_default")]
32 pub unmaintained: bool,
33}
34
35impl ops::Add<Flags> for Flags {
36 type Output = Self;
37 fn add(self, other: Flags) -> Self {
38 Self {
39 unmaintained: self.unmaintained || other.unmaintained,
40 }
41 }
42}
43
44impl From<FlagsDraft> for Flags {
45 fn from(flags: FlagsDraft) -> Self {
46 Self {
47 unmaintained: flags.unmaintained,
48 }
49 }
50}
51#[derive(Clone, Builder, Debug, Serialize, Deserialize, Default, PartialEq, Eq)]
53pub struct FlagsDraft {
54 #[serde(default = "Default::default")]
55 unmaintained: bool,
56}
57
58impl From<Flags> for FlagsDraft {
59 fn from(flags: Flags) -> Self {
60 Self {
61 unmaintained: flags.unmaintained,
62 }
63 }
64}
65
66#[derive(Clone, Builder, Debug, Serialize, Deserialize)]
68pub struct Package {
70 #[serde(flatten)]
71 pub common: proof::Common,
72
73 #[serde(rename = "package")]
74 pub package: proof::PackageInfo,
75
76 #[serde(skip_serializing_if = "Option::is_none", default = "Default::default")]
77 #[serde(rename = "package-diff-base")]
78 #[builder(default = "Default::default()")]
79 pub diff_base: Option<proof::PackageInfo>,
80
81 #[builder(default = "Default::default()")]
82 #[serde(default = "Default::default", skip_serializing_if = "is_equal_default")]
83 review: super::Review,
84
85 #[builder(default = "Default::default()")]
86 #[serde(skip_serializing_if = "is_vec_empty", default = "Default::default")]
87 pub issues: Vec<Issue>,
88
89 #[builder(default = "Default::default()")]
90 #[serde(skip_serializing_if = "is_vec_empty", default = "Default::default")]
91 pub advisories: Vec<Advisory>,
92
93 #[serde(default = "Default::default", skip_serializing_if = "is_equal_default")]
94 #[builder(default = "Default::default()")]
95 pub flags: Flags,
96
97 #[builder(default = "Default::default()")]
98 #[serde(skip_serializing_if = "is_set_empty", default = "Default::default")]
99 pub alternatives: HashSet<proof::PackageId>,
100
101 #[serde(skip_serializing_if = "String::is_empty", default = "Default::default")]
102 #[builder(default = "Default::default()")]
103 pub comment: String,
104
105 #[builder(default = "Default::default()")]
106 #[serde(
107 default = "Default::default",
108 skip_serializing_if = "Vec::is_empty",
109 rename = "override"
110 )]
111 pub override_: Vec<OverrideItem>,
112
113 #[serde(skip_serializing_if = "Option::is_none", default, rename = "llm-agent")]
115 #[builder(default)]
116 pub llm_agent: Option<super::LlmAgentInfo>,
117}
118
119impl PackageBuilder {
120 pub fn from<VALUE: Into<crate::PublicId>>(&mut self, value: VALUE) -> &mut Self {
121 if let Some(ref mut common) = self.common {
122 common.from = value.into();
123 } else {
124 self.common = Some(proof::Common {
125 kind: Some(Package::KIND.into()),
126 version: cur_version(),
127 date: crev_common::now(),
128 from: value.into(),
129 original: None,
130 });
131 }
132 self
133 }
134}
135
136impl fmt::Display for Package {
137 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
138 self.serialize_to(f).map_err(|_| fmt::Error)
139 }
140}
141
142impl proof::WithReview for Package {
143 fn review(&self) -> &super::Review {
144 &self.review
145 }
146}
147
148impl proof::CommonOps for Package {
149 fn common(&self) -> &proof::Common {
150 &self.common
151 }
152
153 fn kind(&self) -> &str {
154 self.common.kind.as_deref().unwrap_or(Self::KIND)
156 }
157}
158
159impl Package {
160 pub fn touch_date(&mut self) {
161 self.common.date = crev_common::now();
162 }
163
164 pub fn change_from(&mut self, id: crate::PublicId) {
165 self.common.from = id;
166 }
167
168 pub fn set_original_reference(&mut self, orig_reference: OriginalReference) {
169 self.common.original = Some(orig_reference);
170 }
171
172 pub fn ensure_kind_is_backfilled(&mut self) {
173 if self.common.kind.is_none() {
174 self.common.kind = Some(self.kind().to_string());
176 }
177 }
178}
179
180#[derive(Clone, Debug, Serialize, Deserialize)]
182pub struct Draft {
183 #[serde(default = "Default::default")]
184 review: super::Review,
185
186 #[serde(default = "Default::default", skip_serializing_if = "is_vec_empty")]
187 pub advisories: Vec<Advisory>,
188
189 #[serde(default = "Default::default", skip_serializing_if = "is_vec_empty")]
190 pub issues: Vec<Issue>,
191
192 #[serde(default = "Default::default", skip_serializing_if = "String::is_empty")]
193 comment: String,
194 #[serde(default = "Default::default")]
195 pub flags: FlagsDraft,
196
197 #[serde(default = "Default::default", skip_serializing_if = "is_set_empty")]
198 pub alternatives: HashSet<proof::PackageId>,
199
200 #[serde(
201 default = "Default::default",
202 skip_serializing_if = "Vec::is_empty",
203 rename = "override"
204 )]
205 pub override_: Vec<OverrideItemDraft>,
206}
207
208impl Draft {
209 pub fn parse(s: &str) -> std::result::Result<Self, ParseError> {
210 serde_yaml::from_str(s).map_err(ParseError::Draft)
211 }
212}
213
214impl From<Package> for Draft {
215 fn from(package: Package) -> Self {
216 Draft {
217 review: package.review,
218 advisories: package.advisories,
219 issues: package.issues,
220 comment: package.comment,
221 alternatives: if package.alternatives.is_empty() {
222 vec![proof::PackageId {
225 source: package.package.id.id.source,
226 name: String::new(),
227 }]
228 .into_iter()
229 .collect()
230 } else {
231 package.alternatives
232 },
233 flags: package.flags.into(),
234 override_: package.override_.into_iter().map(Into::into).collect(),
235 }
236 }
237}
238
239impl proof::Content for Package {
240 fn validate_data(&self) -> ValidationResult<()> {
241 self.ensure_kind_is(Self::KIND)?;
242
243 for alternative in &self.alternatives {
244 if alternative.source.is_empty() {
245 return Err(ValidationError::AlternativeSourceCanNotBeEmpty);
246 }
247 if alternative.name.is_empty() {
248 return Err(ValidationError::AlternativeNameCanNotBeEmpty);
249 }
250 }
251 for issue in &self.issues {
252 if issue.id.is_empty() {
253 return Err(ValidationError::IssuesWithAnEmptyIDFieldAreNotAllowed);
254 }
255 }
256
257 for advisory in &self.advisories {
258 if advisory.ids.is_empty() {
259 return Err(ValidationError::AdvisoriesWithNoIDSAreNotAllowed);
260 }
261
262 for id in &advisory.ids {
263 if id.is_empty() {
264 return Err(ValidationError::AdvisoriesWithAnEmptyIDFieldAreNotAllowed);
265 }
266 }
267 }
268 Ok(())
269 }
270
271 fn serialize_to(&self, fmt: &mut dyn std::fmt::Write) -> fmt::Result {
272 serde_content_serialize!(self, fmt);
273 Ok(())
274 }
275}
276
277impl proof::ContentWithDraft for Package {
278 fn to_draft(&self) -> proof::Draft {
279 proof::Draft {
280 title: format!(
281 "Package Review of {} {}",
282 self.package.id.id.name, self.package.id.version
283 ),
284 body: Draft::from(self.clone()).to_string(),
285 }
286 }
287
288 fn apply_draft(&self, s: &str) -> Result<Self, Error> {
289 let draft = Draft::parse(s)?;
290
291 let mut package = self.clone();
292 package.review = draft.review;
293 package.comment = draft.comment;
294 package.advisories = draft.advisories;
295 package.issues = draft.issues;
296 package.alternatives = draft
297 .alternatives
298 .into_iter()
299 .filter(|a| !a.name.is_empty())
300 .collect();
301 package.flags = draft.flags.into();
302 package.override_ = draft.override_.into_iter().map(Into::into).collect();
303
304 package.validate_data()?;
305 Ok(package)
306 }
307}
308
309impl Package {
310 pub const KIND: &'static str = "package review";
311
312 #[must_use]
313 pub fn is_advisory_for(&self, version: &Version) -> bool {
314 for advisory in &self.advisories {
315 if advisory.is_for_version_when_reported_in_version(version, &self.package.id.version) {
316 return true;
317 }
318 }
319 false
320 }
321
322 #[must_use]
327 pub fn review(&self) -> Option<&super::Review> {
328 if self.review.is_none() {
329 None
330 } else {
331 Some(&self.review)
332 }
333 }
334
335 #[must_use]
340 pub fn review_possibly_none(&self) -> &super::Review {
341 &self.review
342 }
343
344 pub fn review_possibly_none_mut(&mut self) -> &mut super::Review {
345 &mut self.review
346 }
347}
348
349impl fmt::Display for Draft {
350 fn fmt(&self, fmt: &mut fmt::Formatter<'_>) -> fmt::Result {
351 serde_draft_serialize!(self, fmt);
352 Ok(())
353 }
354}
355
356#[derive(Clone, Copy, Debug, Serialize, Deserialize, PartialEq, Eq, PartialOrd, Ord, Default)]
357#[serde(rename_all = "kebab-case")]
358pub enum VersionRange {
359 Minor,
360 Major,
361 #[default]
362 All,
363}
364
365#[derive(Debug, Clone)]
366pub struct VersionRangeParseError(());
367
368impl fmt::Display for VersionRangeParseError {
369 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
370 write!(f, "Could not parse an incorrect advisory range value")
371 }
372}
373
374impl std::str::FromStr for VersionRange {
375 type Err = VersionRangeParseError;
376
377 fn from_str(s: &str) -> std::result::Result<Self, Self::Err> {
378 Ok(match s {
379 "all" => VersionRange::All,
380 "major" => VersionRange::Major,
381 "minor" => VersionRange::Minor,
382 _ => return Err(VersionRangeParseError(())),
383 })
384 }
385}
386
387impl VersionRange {
388 fn all() -> Self {
389 VersionRange::All
390 }
391
392 #[allow(clippy::trivially_copy_pass_by_ref)]
393 fn is_all_ref(&self) -> bool {
394 VersionRange::All == *self
395 }
396}
397
398#[derive(Clone, TypedBuilder, Debug, Serialize, Deserialize)]
404#[serde(rename_all = "kebab-case")]
405#[derive(Default)]
406pub struct Advisory {
407 pub ids: Vec<String>,
408
409 #[builder(default)]
410 pub severity: Level,
411
412 #[builder(default)]
413 #[serde(
414 default = "VersionRange::all",
415 skip_serializing_if = "VersionRange::is_all_ref"
416 )]
417 pub range: VersionRange,
418
419 #[builder(default)]
420 #[serde(default = "Default::default")]
421 pub comment: String,
422}
423
424impl From<VersionRange> for Advisory {
425 fn from(r: VersionRange) -> Self {
426 Advisory {
427 range: r,
428 ..Default::default()
429 }
430 }
431}
432
433impl Advisory {
434 #[must_use]
435 pub fn is_for_version_when_reported_in_version(
436 &self,
437 for_version: &Version,
438 in_pkg_version: &Version,
439 ) -> bool {
440 if for_version < in_pkg_version {
441 match self.range {
442 VersionRange::All => return true,
443 VersionRange::Major => {
444 if in_pkg_version.major == for_version.major {
445 return true;
446 }
447 }
448 VersionRange::Minor => {
449 if in_pkg_version.major == for_version.major
450 && in_pkg_version.minor == for_version.minor
451 {
452 return true;
453 }
454 }
455 }
456 }
457 false
458 }
459}
460
461#[derive(Clone, TypedBuilder, Debug, Serialize, Deserialize)]
470#[serde(rename_all = "kebab-case")]
471pub struct Issue {
472 pub id: String,
473 #[builder(default)]
474 pub severity: Level,
475
476 #[builder(default)]
477 #[serde(
478 default = "VersionRange::all",
479 skip_serializing_if = "VersionRange::is_all_ref"
480 )]
481 pub range: VersionRange,
482
483 #[builder(default)]
484 #[serde(default = "Default::default")]
485 pub comment: String,
486}
487
488impl Issue {
489 #[must_use]
490 pub fn new(id: String) -> Self {
491 Self {
492 id,
493 range: Default::default(),
494 severity: Default::default(),
495 comment: Default::default(),
496 }
497 }
498 #[must_use]
499 pub fn new_with_severity(id: String, severity: Level) -> Self {
500 Self {
501 id,
502 range: Default::default(),
503 severity,
504 comment: Default::default(),
505 }
506 }
507 #[must_use]
508 pub fn is_for_version_when_reported_in_version(
509 &self,
510 for_version: &Version,
511 in_pkg_version: &Version,
512 ) -> bool {
513 if for_version >= in_pkg_version {
514 match self.range {
515 VersionRange::All => return true,
516 VersionRange::Major => {
517 if in_pkg_version.major == for_version.major {
518 return true;
519 }
520 }
521 VersionRange::Minor => {
522 if in_pkg_version.major == for_version.major
523 && in_pkg_version.minor == for_version.minor
524 {
525 return true;
526 }
527 }
528 }
529 }
530 false
531 }
532}