2021-02-28 14:06:05 +00:00
|
|
|
//! The various formats that [fif](crate) can output to.
|
|
|
|
|
2021-04-20 05:20:10 +00:00
|
|
|
use std::ffi::OsStr;
|
2021-03-25 18:46:07 +00:00
|
|
|
use std::io::{self, Write};
|
2021-02-14 13:58:46 +00:00
|
|
|
#[cfg(unix)]
|
|
|
|
use std::os::unix::ffi::OsStrExt;
|
2021-03-11 17:44:31 +00:00
|
|
|
use std::path::Path;
|
2021-02-14 14:25:32 +00:00
|
|
|
|
2021-04-26 10:19:58 +00:00
|
|
|
use cfg_if::cfg_if;
|
2021-09-22 15:33:10 +00:00
|
|
|
use itertools::{Either, Itertools};
|
2021-02-14 14:25:32 +00:00
|
|
|
use snailquote::escape;
|
|
|
|
|
2021-05-05 23:27:16 +00:00
|
|
|
use crate::findings::ScanError;
|
2021-06-14 07:54:11 +00:00
|
|
|
use crate::utils::clap_long_version;
|
2021-06-14 07:08:52 +00:00
|
|
|
use crate::Findings;
|
2021-09-22 15:33:10 +00:00
|
|
|
use crate::String;
|
2021-02-14 14:25:32 +00:00
|
|
|
|
2021-04-26 10:19:58 +00:00
|
|
|
/// A macro for creating an array of `Writable`s without needing to pepper your code with `into()`s.
|
|
|
|
/// # Usage
|
|
|
|
/// ```
|
2021-08-28 07:54:01 +00:00
|
|
|
/// use crate::fif::writables;
|
|
|
|
/// use crate::fif::formats::{Writable, smart_write};
|
|
|
|
/// let mut f = std::io::stdout();
|
|
|
|
///
|
2021-04-26 10:19:58 +00:00
|
|
|
/// // Instead of...
|
2021-08-28 07:54:01 +00:00
|
|
|
/// smart_write(&mut f, &["hello".into(), Writable::Newline]);
|
2021-04-26 10:19:58 +00:00
|
|
|
/// // ...just use:
|
2021-08-28 07:54:01 +00:00
|
|
|
/// smart_write(&mut f, writables!["hello", Newline]);
|
2021-04-26 10:19:58 +00:00
|
|
|
/// ```
|
2021-08-28 07:54:01 +00:00
|
|
|
|
2021-04-26 10:19:58 +00:00
|
|
|
#[macro_export]
|
|
|
|
macro_rules! writables {
|
|
|
|
[$($args:tt),+] => {
|
|
|
|
&[$(writables!(@do $args),)*]
|
|
|
|
};
|
|
|
|
|
|
|
|
(@do Newline) => {
|
|
|
|
$crate::formats::Writable::Newline
|
|
|
|
};
|
|
|
|
|
|
|
|
(@do $arg:expr) => {
|
|
|
|
$arg.into()
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2021-04-28 11:39:56 +00:00
|
|
|
#[macro_export]
|
|
|
|
/// Does the same thing as [writables], but adds a Newline to the end.
|
|
|
|
macro_rules! writablesln {
|
|
|
|
[$($args:tt),+] => {
|
|
|
|
&[$(writables!(@do $args),)* writables!(@do Newline)]
|
|
|
|
};
|
|
|
|
}
|
|
|
|
|
2021-02-28 14:06:05 +00:00
|
|
|
#[doc(hidden)]
|
2021-06-18 05:17:30 +00:00
|
|
|
type Entries<'a> = [Result<Findings, ScanError<'a>>];
|
2021-02-10 09:20:22 +00:00
|
|
|
|
2021-04-26 10:19:58 +00:00
|
|
|
#[derive(Debug, PartialEq)]
|
|
|
|
pub enum Writable<'a> {
|
2021-02-21 14:46:51 +00:00
|
|
|
String(&'a str),
|
2021-03-11 17:26:35 +00:00
|
|
|
Path(&'a Path),
|
2021-02-21 15:55:27 +00:00
|
|
|
Newline,
|
2021-02-21 14:46:51 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
// the lifetime of a lifetime
|
|
|
|
impl<'a> From<&'a str> for Writable<'a> {
|
2021-05-05 23:06:05 +00:00
|
|
|
fn from(s: &'a str) -> Writable<'a> { Writable::String(s) }
|
2021-02-21 14:46:51 +00:00
|
|
|
}
|
|
|
|
|
2021-03-11 17:26:35 +00:00
|
|
|
impl<'a> From<&'a Path> for Writable<'a> {
|
2021-05-05 23:06:05 +00:00
|
|
|
fn from(p: &'a Path) -> Writable<'a> { Writable::Path(p) }
|
2021-02-21 14:46:51 +00:00
|
|
|
}
|
|
|
|
|
2021-03-25 14:28:03 +00:00
|
|
|
impl<'a> From<&'a OsStr> for Writable<'a> {
|
2021-05-05 23:06:05 +00:00
|
|
|
fn from(p: &'a OsStr) -> Writable<'a> { Writable::Path(p.as_ref()) }
|
2021-03-25 14:28:03 +00:00
|
|
|
}
|
|
|
|
|
2021-09-22 15:33:10 +00:00
|
|
|
fn generated_by() -> String { format!("Generated by fif {}", clap_long_version()).into() }
|
2021-04-26 10:19:58 +00:00
|
|
|
|
2021-08-28 07:54:01 +00:00
|
|
|
pub fn smart_write<W: Write>(f: &mut W, writeables: &[Writable]) -> io::Result<()> {
|
2021-02-21 14:46:51 +00:00
|
|
|
// ehhhh
|
|
|
|
for writeable in writeables {
|
|
|
|
match writeable {
|
2021-04-26 10:19:58 +00:00
|
|
|
Writable::Newline => {
|
|
|
|
cfg_if! {
|
|
|
|
if #[cfg(windows)] {
|
2021-06-14 06:53:41 +00:00
|
|
|
write!(f, "\r\n")?;
|
2021-04-26 10:19:58 +00:00
|
|
|
} else {
|
2021-06-14 06:53:41 +00:00
|
|
|
writeln!(f,)?;
|
2021-04-26 10:19:58 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
2021-02-21 14:46:51 +00:00
|
|
|
Writable::String(s) => write!(f, "{}", s)?,
|
|
|
|
Writable::Path(path) => {
|
2021-05-05 22:57:42 +00:00
|
|
|
if let Some(path_str) = path.to_str() {
|
|
|
|
let escaped = escape(path_str);
|
|
|
|
if escaped.as_ref() == path_str {
|
2021-04-26 10:19:58 +00:00
|
|
|
// the escaped string is the same as the input - this will occur for inputs like "file.txt" which don't
|
|
|
|
// need to be escaped. however, it's Best Practice™ to escape such strings anyway, so we prefix/suffix the
|
|
|
|
// escaped string with single quotes.
|
2021-06-14 06:53:41 +00:00
|
|
|
write!(f, "'{}'", escaped)?;
|
2021-04-26 10:19:58 +00:00
|
|
|
} else {
|
2021-06-14 06:53:41 +00:00
|
|
|
write!(f, "{}", escaped)?;
|
2021-04-26 10:19:58 +00:00
|
|
|
}
|
2021-02-21 15:55:27 +00:00
|
|
|
} else {
|
2021-03-25 14:28:03 +00:00
|
|
|
write!(f, "'")?;
|
2021-04-26 10:19:58 +00:00
|
|
|
cfg_if! {
|
|
|
|
if #[cfg(windows)] {
|
|
|
|
// TODO: implement bonked strings for windows
|
|
|
|
// something like:
|
|
|
|
// f.write_all(&*path.as_os_str().encode_wide().collect::<Vec<u16>>())?;
|
|
|
|
write!(f, "{}", path.as_os_str().to_string_lossy())?;
|
|
|
|
} else {
|
|
|
|
f.write_all(&*path.as_os_str().as_bytes())?;
|
|
|
|
}
|
|
|
|
}
|
2021-06-14 06:53:41 +00:00
|
|
|
write!(f, "'")?;
|
2021-02-21 14:46:51 +00:00
|
|
|
}
|
|
|
|
}
|
2021-02-14 13:58:46 +00:00
|
|
|
}
|
|
|
|
}
|
2021-02-21 14:46:51 +00:00
|
|
|
Ok(())
|
2021-02-14 13:58:46 +00:00
|
|
|
}
|
|
|
|
|
2021-07-24 06:20:49 +00:00
|
|
|
pub trait FormatSteps {
|
2021-09-22 15:03:20 +00:00
|
|
|
fn rename<W: Write>(&self, _f: &mut W, _from: &Path, _to: &Path) -> io::Result<()>;
|
2021-09-22 15:21:15 +00:00
|
|
|
fn no_known_extension<W: Write>(&self, _f: &mut W, _path: &Path) -> io::Result<()>;
|
2021-09-22 15:03:20 +00:00
|
|
|
fn unreadable<W: Write>(&self, _f: &mut W, _path: &Path) -> io::Result<()>;
|
|
|
|
fn unknown_type<W: Write>(&self, _f: &mut W, _path: &Path) -> io::Result<()>;
|
|
|
|
fn header<W: Write>(&self, _f: &mut W, _entries: &Entries) -> io::Result<()>;
|
|
|
|
fn footer<W: Write>(&self, _f: &mut W, _entries: &Entries) -> io::Result<()>;
|
2021-07-24 06:20:49 +00:00
|
|
|
fn write_steps<W: Write>(&self, f: &mut W, entries: &Entries) -> io::Result<()> {
|
2021-04-28 11:27:06 +00:00
|
|
|
self.header(f, entries)?;
|
2021-02-14 14:25:32 +00:00
|
|
|
|
2021-04-20 08:52:49 +00:00
|
|
|
// output will be generated in the order:
|
|
|
|
// - files that couldn't be read
|
|
|
|
// - files with no known mime type
|
|
|
|
// - files with no known extension
|
|
|
|
// - files with a known extension
|
|
|
|
// files that already have a correct extension won't be represented in the output.
|
|
|
|
|
|
|
|
// sort errors so unreadable files appear before files with unknown mimetypes - ScanError impls Ord such that
|
|
|
|
// ScanError::File > ScanError::Mime
|
2021-08-25 05:26:41 +00:00
|
|
|
let errors = entries.iter().filter_map(|e| e.as_ref().err()).sorted_unstable();
|
2021-04-20 08:52:49 +00:00
|
|
|
// sort files so that files with no known extension come before those with known extensions - None > Some("jpg")
|
|
|
|
let findings = entries
|
|
|
|
.iter()
|
|
|
|
.filter_map(|e| e.as_ref().ok())
|
2021-08-25 05:26:41 +00:00
|
|
|
.sorted_unstable_by(|a, b| b.recommended_extension().cmp(&a.recommended_extension()).reverse());
|
2021-04-20 08:52:49 +00:00
|
|
|
|
|
|
|
for error in errors {
|
|
|
|
match error {
|
|
|
|
// failed to read the file
|
|
|
|
ScanError::File(path) => self.unreadable(f, path)?,
|
|
|
|
// file was read successfully, but we couldn't determine a mimetype
|
|
|
|
ScanError::Mime(path) => self.unknown_type(f, path)?,
|
|
|
|
}
|
|
|
|
}
|
2021-02-21 14:46:51 +00:00
|
|
|
|
2021-06-18 07:42:16 +00:00
|
|
|
if findings.len() != entries.len() {
|
|
|
|
// if these lengths aren't the same, there was at least one error
|
|
|
|
// add a blank line between the errors and commands
|
|
|
|
smart_write(f, writables![Newline])?;
|
|
|
|
}
|
|
|
|
|
2021-04-20 08:52:49 +00:00
|
|
|
for finding in findings {
|
|
|
|
if let Some(ext) = finding.recommended_extension() {
|
2021-06-18 05:17:30 +00:00
|
|
|
self.rename(f, finding.file.as_path(), &finding.file.with_extension(ext.as_str()))?;
|
2021-04-20 08:52:49 +00:00
|
|
|
} else {
|
2021-06-18 05:17:30 +00:00
|
|
|
self.no_known_extension(f, finding.file.as_path())?;
|
2021-02-10 09:20:22 +00:00
|
|
|
}
|
|
|
|
}
|
2021-02-14 14:25:32 +00:00
|
|
|
|
2021-04-28 11:27:06 +00:00
|
|
|
self.footer(f, entries)
|
2021-02-10 09:20:22 +00:00
|
|
|
}
|
2021-02-06 11:51:20 +00:00
|
|
|
}
|
|
|
|
|
2021-07-24 06:20:49 +00:00
|
|
|
pub trait Format {
|
|
|
|
fn write_all<W: Write>(&self, f: &mut W, entries: &Entries) -> io::Result<()>;
|
|
|
|
}
|
|
|
|
|
2021-02-28 14:06:05 +00:00
|
|
|
/// Bourne-Shell compatible script.
|
2021-07-24 06:20:49 +00:00
|
|
|
pub struct Shell;
|
2021-02-14 17:12:27 +00:00
|
|
|
|
2021-04-14 08:25:46 +00:00
|
|
|
impl Format for Shell {
|
2021-07-24 06:20:49 +00:00
|
|
|
fn write_all<W: Write>(&self, f: &mut W, entries: &Entries) -> io::Result<()> { self.write_steps(f, entries) }
|
|
|
|
}
|
2021-02-10 09:20:22 +00:00
|
|
|
|
2021-07-24 06:20:49 +00:00
|
|
|
impl FormatSteps for Shell {
|
2021-03-11 17:26:35 +00:00
|
|
|
fn rename<W: Write>(&self, f: &mut W, from: &Path, to: &Path) -> io::Result<()> {
|
2021-08-06 14:12:26 +00:00
|
|
|
smart_write(f, writablesln!("mv -v -i -- ", from, "\t", to))
|
2021-02-10 09:20:22 +00:00
|
|
|
}
|
|
|
|
|
2021-03-11 17:26:35 +00:00
|
|
|
fn no_known_extension<W: Write>(&self, f: &mut W, path: &Path) -> io::Result<()> {
|
2021-07-01 08:52:53 +00:00
|
|
|
smart_write(
|
|
|
|
f,
|
|
|
|
writablesln![
|
|
|
|
"cat <<- '???'",
|
|
|
|
Newline,
|
|
|
|
"No known extension for ",
|
|
|
|
path,
|
|
|
|
Newline,
|
|
|
|
"???"
|
|
|
|
],
|
|
|
|
)
|
2021-02-06 11:51:20 +00:00
|
|
|
}
|
|
|
|
|
2021-03-11 17:26:35 +00:00
|
|
|
fn unreadable<W: Write>(&self, f: &mut W, path: &Path) -> io::Result<()> {
|
2021-04-28 11:39:56 +00:00
|
|
|
smart_write(f, writablesln!["# Failed to read", path])
|
2021-02-06 11:51:20 +00:00
|
|
|
}
|
|
|
|
|
2021-03-11 17:26:35 +00:00
|
|
|
fn unknown_type<W: Write>(&self, f: &mut W, path: &Path) -> io::Result<()> {
|
2021-04-28 11:39:56 +00:00
|
|
|
smart_write(f, writablesln!["# Failed to detect mime type for ", path])
|
2021-02-06 11:51:20 +00:00
|
|
|
}
|
2021-02-14 14:25:32 +00:00
|
|
|
|
2021-04-28 11:27:06 +00:00
|
|
|
fn header<W: Write>(&self, f: &mut W, _: &Entries) -> io::Result<()> {
|
2021-04-26 10:19:58 +00:00
|
|
|
smart_write(
|
2021-02-18 09:48:38 +00:00
|
|
|
f,
|
2021-04-28 11:39:56 +00:00
|
|
|
writablesln!["#!/usr/bin/env sh", Newline, "# ", (generated_by().as_str())],
|
2021-04-28 11:27:06 +00:00
|
|
|
)?;
|
|
|
|
|
|
|
|
if let Ok(working_directory) = std::env::current_dir() {
|
2021-04-28 11:39:56 +00:00
|
|
|
smart_write(f, writablesln!["# Run from ", (working_directory.as_path())])?;
|
2021-04-28 11:27:06 +00:00
|
|
|
}
|
|
|
|
|
2021-04-28 11:39:56 +00:00
|
|
|
smart_write(f, writablesln![Newline, "set -e", Newline])
|
2021-02-14 14:25:32 +00:00
|
|
|
}
|
|
|
|
|
2021-04-28 11:27:06 +00:00
|
|
|
fn footer<W: Write>(&self, f: &mut W, _: &Entries) -> io::Result<()> {
|
2021-04-28 11:39:56 +00:00
|
|
|
smart_write(f, writablesln![Newline, "echo 'Done.'"])
|
2021-02-14 14:25:32 +00:00
|
|
|
}
|
2021-02-18 09:48:38 +00:00
|
|
|
}
|
2021-03-25 14:28:03 +00:00
|
|
|
|
2021-04-04 13:52:16 +00:00
|
|
|
// PowerShell is a noun, not a type
|
|
|
|
#[allow(clippy::doc_markdown)]
|
2021-03-25 14:28:03 +00:00
|
|
|
/// PowerShell script.
|
2021-07-24 06:20:49 +00:00
|
|
|
pub struct PowerShell;
|
2021-03-25 14:28:03 +00:00
|
|
|
|
|
|
|
impl Format for PowerShell {
|
2021-07-24 06:20:49 +00:00
|
|
|
fn write_all<W: Write>(&self, f: &mut W, entries: &Entries) -> io::Result<()> { self.write_steps(f, entries) }
|
|
|
|
}
|
2021-03-25 14:28:03 +00:00
|
|
|
|
2021-07-24 06:20:49 +00:00
|
|
|
impl FormatSteps for PowerShell {
|
2021-03-25 14:28:03 +00:00
|
|
|
fn rename<W: Write>(&self, f: &mut W, from: &Path, to: &Path) -> io::Result<()> {
|
|
|
|
// unfortunately there doesn't seem to be an equivalent of sh's `mv -i` -- passing the '-Confirm' flag will prompt
|
|
|
|
// the user to confirm every single rename, and using Move-Item -Force will always overwrite without prompting.
|
|
|
|
// there doesn't seem to be a way to rename the file, prompting only if the target already exists.
|
|
|
|
smart_write(
|
|
|
|
f,
|
2021-07-01 08:52:53 +00:00
|
|
|
writablesln![
|
|
|
|
"Rename-Item -Verbose -Path ",
|
|
|
|
from,
|
|
|
|
" -NewName ",
|
|
|
|
(to.file_name().unwrap())
|
|
|
|
],
|
2021-03-25 14:28:03 +00:00
|
|
|
)
|
|
|
|
}
|
|
|
|
|
|
|
|
fn no_known_extension<W: Write>(&self, f: &mut W, path: &Path) -> io::Result<()> {
|
|
|
|
smart_write(
|
|
|
|
f,
|
2021-06-13 09:24:21 +00:00
|
|
|
writablesln![
|
2021-04-26 10:19:58 +00:00
|
|
|
"Write-Output @'",
|
|
|
|
Newline,
|
|
|
|
"No known extension for ",
|
|
|
|
path,
|
|
|
|
Newline,
|
|
|
|
"'@"
|
2021-03-25 18:46:07 +00:00
|
|
|
],
|
2021-03-25 14:28:03 +00:00
|
|
|
)
|
|
|
|
}
|
|
|
|
|
|
|
|
fn unreadable<W: Write>(&self, f: &mut W, path: &Path) -> io::Result<()> {
|
|
|
|
smart_write(
|
|
|
|
f,
|
2021-06-13 09:24:21 +00:00
|
|
|
writablesln!["Write-Output @'", Newline, "Failed to read ", path, Newline, "'@"],
|
2021-03-25 14:28:03 +00:00
|
|
|
)
|
|
|
|
}
|
|
|
|
|
|
|
|
fn unknown_type<W: Write>(&self, f: &mut W, path: &Path) -> io::Result<()> {
|
2021-04-28 11:39:56 +00:00
|
|
|
smart_write(f, writablesln!["<# Failed to detect mime type for ", path, " #>"])
|
2021-03-25 14:28:03 +00:00
|
|
|
}
|
|
|
|
|
2021-04-28 11:27:06 +00:00
|
|
|
fn header<W: Write>(&self, f: &mut W, _: &Entries) -> io::Result<()> {
|
2021-04-26 10:19:58 +00:00
|
|
|
smart_write(
|
2021-03-25 14:28:03 +00:00
|
|
|
f,
|
2021-04-28 11:39:56 +00:00
|
|
|
writablesln!["#!/usr/bin/env pwsh", Newline, "<# ", (generated_by().as_str()), " #>"],
|
2021-04-28 11:27:06 +00:00
|
|
|
)?;
|
|
|
|
|
|
|
|
if let Ok(working_directory) = std::env::current_dir() {
|
2021-04-28 11:39:56 +00:00
|
|
|
smart_write(f, writablesln!["<# Run from ", (working_directory.as_path()), " #>"])?;
|
2021-04-28 11:27:06 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
smart_write(f, writables![Newline])
|
2021-03-25 14:28:03 +00:00
|
|
|
}
|
|
|
|
|
2021-04-28 11:27:06 +00:00
|
|
|
fn footer<W: Write>(&self, f: &mut W, _: &Entries) -> io::Result<()> {
|
2021-04-28 11:39:56 +00:00
|
|
|
smart_write(f, writablesln![Newline, "Write-Output 'Done!'"])
|
2021-03-25 14:28:03 +00:00
|
|
|
}
|
2021-03-25 18:46:07 +00:00
|
|
|
}
|
2021-05-05 22:57:42 +00:00
|
|
|
|
2021-06-07 05:21:47 +00:00
|
|
|
pub struct Text;
|
|
|
|
impl Format for Text {
|
2021-07-24 06:20:49 +00:00
|
|
|
fn write_all<W: Write>(&self, f: &mut W, entries: &Entries) -> io::Result<()> { self.write_steps(f, entries) }
|
|
|
|
}
|
|
|
|
|
|
|
|
impl FormatSteps for Text {
|
2021-06-07 05:21:47 +00:00
|
|
|
fn rename<W: Write>(&self, f: &mut W, from: &Path, to: &Path) -> io::Result<()> {
|
|
|
|
smart_write(f, writablesln![from, " should be renamed to ", to])
|
|
|
|
}
|
|
|
|
|
|
|
|
fn no_known_extension<W: Write>(&self, f: &mut W, path: &Path) -> io::Result<()> {
|
|
|
|
smart_write(f, writablesln!["No known extension for ", path])
|
|
|
|
}
|
|
|
|
|
|
|
|
fn unreadable<W: Write>(&self, f: &mut W, path: &Path) -> io::Result<()> {
|
|
|
|
smart_write(f, writablesln!["Encountered IO error while accessing ", path])
|
|
|
|
}
|
|
|
|
|
|
|
|
fn unknown_type<W: Write>(&self, f: &mut W, path: &Path) -> io::Result<()> {
|
|
|
|
smart_write(f, writablesln!["Couldn't determine type for ", path])
|
|
|
|
}
|
|
|
|
|
|
|
|
fn header<W: Write>(&self, f: &mut W, _entries: &Entries) -> io::Result<()> {
|
|
|
|
smart_write(f, writablesln![(generated_by().as_str()), Newline])
|
|
|
|
}
|
|
|
|
|
|
|
|
fn footer<W: Write>(&self, f: &mut W, entries: &Entries) -> io::Result<()> {
|
|
|
|
smart_write(
|
|
|
|
f,
|
|
|
|
writablesln![Newline, "Processed ", (entries.len().to_string().as_str()), " files"],
|
|
|
|
)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2021-05-05 22:57:42 +00:00
|
|
|
#[cfg(feature = "json")]
|
|
|
|
pub struct Json;
|
|
|
|
|
|
|
|
#[cfg(feature = "json")]
|
|
|
|
impl Format for Json {
|
|
|
|
fn write_all<W: Write>(&self, f: &mut W, entries: &Entries) -> io::Result<()> {
|
|
|
|
#[derive(serde::Serialize)]
|
|
|
|
struct SerdeEntries<'a> {
|
|
|
|
errors: &'a Vec<&'a ScanError<'a>>,
|
2021-06-18 05:17:30 +00:00
|
|
|
findings: &'a Vec<&'a Findings>,
|
2021-05-05 22:57:42 +00:00
|
|
|
}
|
|
|
|
|
2021-08-25 05:44:21 +00:00
|
|
|
let (errors, findings) = &entries.iter().partition_map(|entry| match entry {
|
|
|
|
Err(e) => Either::Left(e),
|
|
|
|
Ok(f) => Either::Right(f),
|
|
|
|
});
|
|
|
|
|
|
|
|
let result = serde_json::to_writer_pretty(f, &SerdeEntries { errors, findings });
|
2021-05-05 23:06:05 +00:00
|
|
|
|
2021-05-05 22:57:42 +00:00
|
|
|
if let Err(err) = result {
|
|
|
|
log::error!("Error while serialising: {}", err);
|
2021-05-05 23:06:05 +00:00
|
|
|
return Err(err.into());
|
2021-05-05 22:57:42 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
Ok(())
|
|
|
|
}
|
2021-05-05 23:06:05 +00:00
|
|
|
}
|