File: report.rs

package info (click to toggle)
rust-ingredients 0.2.2-2
  • links: PTS, VCS
  • area: main
  • in suites: forky, sid
  • size: 348 kB
  • sloc: makefile: 10
file content (353 lines) | stat: -rw-r--r-- 13,677 bytes parent folder | download | duplicates (2)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
use std::borrow::Cow;
use std::fmt::{self, Display, Formatter};

use serde::Serialize;
use serde_json::Value;

use crate::severity::Severity;

/// List of differences between a published crate and the associated state of
/// the upstream version control system
#[derive(Debug, Default)]
pub struct Report {
    items: Vec<ReportItem>,
}

impl Report {
    pub(crate) const fn from_items(items: Vec<ReportItem>) -> Self {
        Report { items }
    }

    /// List of differences
    #[must_use]
    pub fn items(&self) -> &[ReportItem] {
        &self.items
    }

    /// Get list of differences in machine-readable JSON format
    ///
    /// # Panics
    ///
    /// This function panics if there are internal errors related to serializing
    /// data in JSON format.
    #[must_use]
    pub fn to_json(&self) -> String {
        let items: Vec<JsonReportItem> = self
            .items
            .iter()
            .map(|i| JsonReportItem {
                severity: i.severity().to_string(),
                kind: i.kind(),
                data: i.data(),
            })
            .collect();

        // if this fails, something is seriously wrong - just panic
        #[expect(clippy::expect_used)]
        serde_json::to_string_pretty(&items).expect("Failed to serialize report as JSON.")
    }
}

impl Display for Report {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        for item in self.items() {
            if f.alternate() {
                write!(f, "{item:#}")?;
            } else {
                write!(f, "{item}")?;
            }
        }
        Ok(())
    }
}

/// A specific difference between a published crate and the associated state of the upstream version
/// control system
#[derive(Clone, Debug, PartialEq)]
#[non_exhaustive]
// keep in sync with the Python enum
pub enum ReportItem {
    /// The crate metadata does not contain a repository URL.
    MissingRepositoryUrl,
    /// The crate metadata specifies an invalid repository URL.
    InvalidRepoUrl {
        /// Repository URL
        repo: String,
    },
    /// The git ref from `.cargo_vcs_info.json` could not be checked out.
    InvalidGitRef {
        /// Repository URL
        repo: String,
        /// Repository ref
        rev: String,
    },
    /// The `.cargo_vcs_info.json` file is missing from the published crate.
    MissingVcsInfo,
    /// The `"path_in_vcs"` property is missing from `.cargo_vcs_info.json`.
    NoPathInVcsInfo,
    /// The `"path_in_vcs"` property is missing from `.cargo_vcs_info.json` and the crate cannot be
    /// found inside the repository.
    NotFoundInRepo {
        /// Repository URL
        repo: String,
        /// Crate name
        name: String,
    },
    /// The crate was published from a "dirty" repository according to `.cargo_vcs_info.json`.
    DirtyRepository,
    /// Crate metadata does not match metadata from VCS contents.
    MetadataMismatch {
        /// Field in Cargo.toml that does not match
        field: Cow<'static, str>,
        /// Value in the published crate
        krate: Option<String>,
        /// Value from VCS contents
        urepo: Option<String>,
    },
    /// The crate contains a broken symbolic link.
    BrokenSymlinkInCrate {
        /// Path of the symbolic link
        path: String,
    },
    /// The repository contains a broken symbolic link.
    BrokenSymlinkInRepo {
        /// Path of the symbolic link
        path: String,
    },
    /// The crate contains a symbolic link that points outside the source directory.
    InvalidSymlinkInCrate {
        /// Path of the symbolic link
        path: String,
    },
    /// The repository contains a symbolic link that points outside the source directory.
    InvalidSymlinkInRepo {
        /// Path of the symbolic link
        path: String,
    },
    /// The crate contains a file that is not present in the VCS.
    MissingFile {
        /// Path of the file
        path: String,
    },
    /// The crate contains a file that does not match file contents from the VCS.
    ContentMismatch {
        /// Path of the file
        path: String,
        /// Diff between the file in the published crate and the file in the VCS
        ///
        /// If this is `None` then the file is a binary file and / or not valid UTF-8.
        diff: Option<String>,
    },
    /// The crate contains a file that has different line endings than the file in the VCS.
    LineEndings {
        /// Path of the file
        path: String,
    },
    /// The crate contains a file that has different mode / permissions than the file in the VCS.
    Permissions {
        /// Path of the file
        path: String,
        /// Mode of the file in the published crate
        krate: String,
        /// Mode of the file in the VCS
        urepo: String,
    },
}

impl ReportItem {
    pub(crate) fn metadata_mismatch<F: Into<Cow<'static, str>>>(
        field: F,
        krate: Option<String>,
        urepo: Option<String>,
    ) -> Self {
        ReportItem::MetadataMismatch {
            field: field.into(),
            krate,
            urepo,
        }
    }
}

#[derive(Serialize)]
struct JsonReportItem {
    severity: String,
    kind: &'static str,
    data: Value,
}

impl ReportItem {
    /// Severity associated with this report item.
    #[must_use]
    pub const fn severity(&self) -> Severity {
        #[expect(clippy::match_same_arms)]
        match self {
            Self::MissingRepositoryUrl => Severity::Fatal,
            Self::InvalidRepoUrl { .. } => Severity::Fatal,
            Self::InvalidGitRef { .. } => Severity::Fatal,
            Self::MissingVcsInfo => Severity::Fatal,
            Self::NoPathInVcsInfo => Severity::Warning,
            Self::NotFoundInRepo { .. } => Severity::Fatal,
            Self::DirtyRepository => Severity::Warning,
            Self::MetadataMismatch { .. } => Severity::Error,
            Self::BrokenSymlinkInCrate { .. } => Severity::Warning,
            Self::BrokenSymlinkInRepo { .. } => Severity::Warning,
            Self::InvalidSymlinkInCrate { .. } => Severity::Error,
            Self::InvalidSymlinkInRepo { .. } => Severity::Error,
            Self::MissingFile { .. } => Severity::Error,
            Self::ContentMismatch { .. } => Severity::Error,
            Self::LineEndings { .. } => Severity::Warning,
            Self::Permissions { .. } => Severity::Warning,
        }
    }

    /// String representation / ID of the report item kind.
    #[must_use]
    // keep in sync with the Python enum
    pub const fn kind(&self) -> &'static str {
        match self {
            Self::MissingRepositoryUrl => "MissingRepositoryUrl",
            Self::InvalidRepoUrl { .. } => "InvalidRepoUrl",
            Self::InvalidGitRef { .. } => "InvalidGitRef",
            Self::MissingVcsInfo => "MissingVcsInfo",
            Self::NoPathInVcsInfo => "NoPathInVcsInfo",
            Self::NotFoundInRepo { .. } => "NotFoundInRepository",
            Self::DirtyRepository => "DirtyRepository",
            Self::MetadataMismatch { .. } => "MetadataMismatch",
            Self::BrokenSymlinkInCrate { .. } => "BrokenSymlinkInCrate",
            Self::BrokenSymlinkInRepo { .. } => "BrokenSymlinkInRepo",
            Self::InvalidSymlinkInCrate { .. } => "InvalidSymlinkInCrate",
            Self::InvalidSymlinkInRepo { .. } => "InvalidSymlinkInRepo",
            Self::MissingFile { .. } => "MissingFile",
            Self::ContentMismatch { .. } => "ContentMismatch",
            Self::LineEndings { .. } => "LineEndings",
            Self::Permissions { .. } => "Permissions",
        }
    }

    /// Data associated with this report item.
    #[must_use]
    pub fn data(&self) -> Value {
        let mut data = serde_json::Map::new();

        #[expect(clippy::match_same_arms)]
        match self {
            Self::MissingRepositoryUrl => {},
            Self::InvalidRepoUrl { repo } => {
                data.insert(String::from("url"), Value::from(repo.clone()));
            },
            Self::InvalidGitRef { repo, rev } => {
                data.insert(String::from("url"), Value::from(repo.clone()));
                data.insert(String::from("ref"), Value::from(rev.clone()));
            },
            Self::MissingVcsInfo => {},
            Self::NoPathInVcsInfo => {},
            Self::NotFoundInRepo { repo, name } => {
                data.insert(String::from("url"), Value::from(repo.clone()));
                data.insert(String::from("name"), Value::from(name.clone()));
            },
            Self::DirtyRepository => {},
            Self::MetadataMismatch { field, krate, urepo } => {
                data.insert(String::from("field"), Value::from(String::from(field.as_ref())));
                data.insert(String::from("crate"), Value::from(krate.clone()));
                data.insert(String::from("urepo"), Value::from(urepo.clone()));
            },
            Self::BrokenSymlinkInCrate { path } => {
                data.insert(String::from("path"), Value::from(path.clone()));
            },
            Self::BrokenSymlinkInRepo { path } => {
                data.insert(String::from("path"), Value::from(path.clone()));
            },
            Self::InvalidSymlinkInCrate { path } => {
                data.insert(String::from("path"), Value::from(path.clone()));
            },
            Self::InvalidSymlinkInRepo { path } => {
                data.insert(String::from("path"), Value::from(path.clone()));
            },
            Self::MissingFile { path } => {
                data.insert(String::from("path"), Value::from(path.clone()));
            },
            Self::ContentMismatch { path, diff } => {
                data.insert(String::from("path"), Value::from(path.clone()));
                data.insert(String::from("diff"), Value::from(diff.clone()));
            },
            Self::LineEndings { path } => {
                data.insert(String::from("path"), Value::from(path.clone()));
            },
            Self::Permissions { path, krate, urepo } => {
                data.insert(String::from("path"), Value::from(path.clone()));
                data.insert(String::from("mode-in-crate"), Value::from(krate.clone()));
                data.insert(String::from("mode-in-repo"), Value::from(urepo.clone()));
            },
        }

        Value::Object(data)
    }

    /// Human-readable message associated with this report item.
    #[must_use]
    pub fn message(&self) -> Cow<'static, str> {
        match self {
            Self::MissingRepositoryUrl => Into::into("missing repository URL in crate metadata"),
            Self::InvalidRepoUrl { repo } => format!("invalid repository URL: '{repo}'").into(),
            Self::InvalidGitRef { repo, rev } => format!("invalid git ref '{rev}' for repository at '{repo}'").into(),
            Self::MissingVcsInfo => Into::into("missing '.cargo_vcs_info.json' in published crate"),
            Self::NoPathInVcsInfo => Into::into("no path specified in '.cargo_vcs_info.json'"),
            Self::NotFoundInRepo { repo, name } => {
                format!("crate '{name}' cannot be found in repository at '{repo}'").into()
            },
            Self::DirtyRepository => Into::into("crate was published from a \"dirty\" repository"),
            Self::MetadataMismatch { field, krate, urepo } => {
                let kmd = krate.as_ref().map_or("(none)", String::as_str);
                let umd = urepo.as_ref().map_or("(none)", String::as_str);
                format!("metadata mismatch: '{field}' differs between crate ({kmd}) and repository ({umd})").into()
            },
            Self::BrokenSymlinkInCrate { path } => format!("broken symbolic link in crate at path '{path}'").into(),
            Self::BrokenSymlinkInRepo { path } => format!("broken symbolic link in repository at path '{path}'").into(),
            Self::InvalidSymlinkInCrate { path } => format!("invalid symbolic link in crate at path '{path}'").into(),
            Self::InvalidSymlinkInRepo { path } => {
                format!("invalid symbolic link in repository at path '{path}'").into()
            },
            Self::MissingFile { path } => {
                format!("file present in crate missing from repository at path '{path}'").into()
            },
            Self::ContentMismatch { path, .. } => {
                format!("contents of file at path '{path}' differ between crate and repository").into()
            },
            Self::LineEndings { path } => {
                format!("contents of file at path '{path}' use different line endings (CRLF / LF)").into()
            },
            Self::Permissions { path, krate, urepo } => {
                format!("file at path '{path}' has different modes in crate ({krate}) and repository ({urepo})").into()
            },
        }
    }

    /// Additional message content from this report item.
    #[must_use]
    pub fn extra(&self) -> Option<String> {
        if let Self::ContentMismatch { diff, .. } = self
            && let Some(diff) = diff
        {
            Some(diff.clone())
        } else {
            None
        }
    }
}

impl Display for ReportItem {
    fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
        if f.alternate() {
            writeln!(f, "{}: {}", self.severity(), self.message())?;
            if let Some(extra) = self.extra() {
                for line in extra.lines() {
                    writeln!(f, "  {line}")?;
                }
                writeln!(f)?;
            }
            Ok(())
        } else {
            writeln!(f, "{}: {}", self.severity(), self.message())
        }
    }
}