From 661d1b80220c292c2ef8e45e70c929636aaac588 Mon Sep 17 00:00:00 2001 From: Kai Lueke Date: Wed, 3 Jan 2024 14:32:28 +0100 Subject: [PATCH 1/4] Check Omaha sha1 hash if available Old Nebraska servers were missing the newly introduced sha256 attribute and might only serve paylads with the regular sha1 attribute set. The generic payload is also just using the regular sha1 attribute because at that time a different extension was used which holds the sha256 checksum for the generic payload. Support the regular Omaha sha1 hash for use with old Nebraska servers. This makes it also easy to test with the generic payload. --- Cargo.lock | 18 +++++++++-- examples/download_test.rs | 2 +- examples/full_test.rs | 4 +-- omaha/Cargo.toml | 3 ++ omaha/src/hash_types.rs | 43 +++++++++++++++++++++++-- src/bin/download_sysext.rs | 65 +++++++++++++++++++++----------------- src/download.rs | 14 ++++---- src/lib.rs | 2 +- test/crau_verify.rs | 2 +- 9 files changed, 109 insertions(+), 44 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 25e5fde..91ac5c7 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -762,7 +762,10 @@ version = "0.1.0" dependencies = [ "anyhow", "ct-codecs", + "digest", "hard-xml", + "sha1", + "sha2", "url", "uuid", ] @@ -1142,11 +1145,22 @@ dependencies = [ "serde", ] +[[package]] +name = "sha1" +version = "0.10.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e3bf829a2d51ab4a5ddf1352d8470c140cadc8301b2ae1789db023f01cedd6ba" +dependencies = [ + "cfg-if", + "cpufeatures", + "digest", +] + [[package]] name = "sha2" -version = "0.10.7" +version = "0.10.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "479fb9d862239e610720565ca91403019f2f00410f1864c5aa7479b950a76ed8" +checksum = "793db75ad2bcafc3ffa7c68b215fee268f537982cd901d132f89c6343f3a3dc8" dependencies = [ "cfg-if", "cpufeatures", diff --git a/examples/download_test.rs b/examples/download_test.rs index ba2f34b..c8dabd1 100644 --- a/examples/download_test.rs +++ b/examples/download_test.rs @@ -16,7 +16,7 @@ fn main() -> Result<(), Box> { let res = download_and_hash(&client, url, &path, false)?; tempdir.close()?; - println!("hash: {}", res.hash); + println!("hash: {}", res.hash_sha256); Ok(()) } diff --git a/examples/full_test.rs b/examples/full_test.rs index 2992a6a..24920f4 100644 --- a/examples/full_test.rs +++ b/examples/full_test.rs @@ -84,8 +84,8 @@ fn main() -> Result<(), Box> { tempdir.close()?; println!("\texpected sha256: {}", expected_sha256); - println!("\tcalculated sha256: {}", res.hash); - println!("\tsha256 match? {}", expected_sha256 == res.hash); + println!("\tcalculated sha256: {}", res.hash_sha256); + println!("\tsha256 match? {}", expected_sha256 == res.hash_sha256); } Ok(()) diff --git a/omaha/Cargo.toml b/omaha/Cargo.toml index 16d7316..ef24690 100644 --- a/omaha/Cargo.toml +++ b/omaha/Cargo.toml @@ -10,6 +10,9 @@ uuid = "1.2" ct-codecs = "1" url = "2" anyhow = "1.0.75" +sha2 = "0.10.8" +sha1 = "0.10.6" +digest = "0.10.7" [dependencies.hard-xml] path = "../vendor/hard-xml" diff --git a/omaha/src/hash_types.rs b/omaha/src/hash_types.rs index 20683b6..64cf7bf 100644 --- a/omaha/src/hash_types.rs +++ b/omaha/src/hash_types.rs @@ -1,6 +1,8 @@ use std::fmt; use std::str; +use sha2::Digest; + use anyhow::{Error as CodecError, anyhow}; #[rustfmt::skip] @@ -22,24 +24,61 @@ pub trait HashAlgo { const HASH_NAME: &'static str; type Output: AsRef<[u8]> + AsMut<[u8]> + Default + Sized + Eq; + + fn hasher() -> impl digest::DynDigest; + fn from_boxed(s: Box<[u8]>) -> Self::Output; } impl HashAlgo for Sha1 { const HASH_NAME: &'static str = "Sha1"; type Output = [u8; 20]; + + fn hasher() -> impl digest::DynDigest { + sha1::Sha1::new() + } + + fn from_boxed(s: Box<[u8]>) -> Self::Output { + let mut v = s.into_vec(); + v.resize(Self::Output::default().len(), 0); + let boxed_array: Box = match v.into_boxed_slice().try_into() { + Ok(a) => a, + Err(e) => { + println!("Unexpected length {}", e.len()); + Box::new(Self::Output::default()) + } + }; + *boxed_array + } } impl HashAlgo for Sha256 { const HASH_NAME: &'static str = "Sha256"; type Output = [u8; 32]; + + fn hasher() -> impl digest::DynDigest { + sha2::Sha256::new() + } + + fn from_boxed(s: Box<[u8]>) -> Self::Output { + let mut v = s.into_vec(); + v.resize(Self::Output::default().len(), 0); + let boxed_array: Box = match v.into_boxed_slice().try_into() { + Ok(a) => a, + Err(e) => { + println!("Unexpected length {}", e.len()); + Box::new(Self::Output::default()) + } + }; + *boxed_array + } } #[derive(PartialEq, Eq, Clone)] pub struct Hash(T::Output); impl Hash { - pub fn from_bytes(digest: T::Output) -> Self { - Self(digest) + pub fn from_bytes(digest: Box<[u8]>) -> Self { + Self(T::from_boxed(digest)) } } diff --git a/src/bin/download_sysext.rs b/src/bin/download_sysext.rs index 62ebf9b..55d480c 100644 --- a/src/bin/download_sysext.rs +++ b/src/bin/download_sysext.rs @@ -21,7 +21,7 @@ use reqwest::redirect::Policy; use url::Url; use update_format_crau::delta_update; -use ue_rs::hash_on_disk_sha256; +use ue_rs::hash_on_disk; #[derive(Debug)] enum PackageStatus { @@ -38,7 +38,8 @@ enum PackageStatus { struct Package<'a> { url: Url, name: Cow<'a, str>, - hash: omaha::Hash, + hash_sha256: Option>, + hash_sha1: Option>, size: omaha::FileSize, status: PackageStatus, } @@ -48,8 +49,8 @@ impl<'a> Package<'a> { // Return Sha256 hash of data in the given path. // If maxlen is None, a simple read to the end of the file. // If maxlen is Some, read only until the given length. - fn hash_on_disk(&mut self, path: &Path, maxlen: Option) -> Result> { - hash_on_disk_sha256(path, maxlen) + fn hash_on_disk(&mut self, path: &Path, maxlen: Option) -> Result> { + hash_on_disk::(path, maxlen) } #[rustfmt::skip] @@ -80,10 +81,13 @@ impl<'a> Package<'a> { if size_on_disk == expected_size { info!("{}: download complete, checking hash...", path.display()); - let hash = self.hash_on_disk(&path, None).context({ + let hash_sha256 = self.hash_on_disk::(&path, None).context({ format!("failed to hash_on_disk, path ({:?})", path.display()) })?; - if self.verify_checksum(hash) { + let hash_sha1 = self.hash_on_disk::(&path, None).context({ + format!("failed to hash_on_disk, path ({:?})", path.display()) + })?; + if self.verify_checksum(hash_sha256, hash_sha1) { info!("{}: good hash, will continue without re-download", path.display()); } else { info!("{}: bad hash, will re-download", path.display()); @@ -114,16 +118,19 @@ impl<'a> Package<'a> { } }; - self.verify_checksum(res.hash); + self.verify_checksum(res.hash_sha256, res.hash_sha1); Ok(()) } - fn verify_checksum(&mut self, calculated: omaha::Hash) -> bool { - debug!(" expected sha256: {}", self.hash); - debug!(" calculated sha256: {}", calculated); - debug!(" sha256 match? {}", self.hash == calculated); + fn verify_checksum(&mut self, calculated_sha256: omaha::Hash, calculated_sha1: omaha::Hash) -> bool { + debug!(" expected sha256: {:?}", self.hash_sha256); + debug!(" calculated sha256: {}", calculated_sha256); + debug!(" sha256 match? {}", self.hash_sha256 == Some(calculated_sha256.clone())); + debug!(" expected sha1: {:?}", self.hash_sha1); + debug!(" calculated sha1: {}", calculated_sha1); + debug!(" sha1 match? {}", self.hash_sha1 == Some(calculated_sha1.clone())); - if self.hash != calculated { + if self.hash_sha256.is_some() && self.hash_sha256 != Some(calculated_sha256.clone()) || self.hash_sha1.is_some() && self.hash_sha1 != Some(calculated_sha1.clone()) { self.status = PackageStatus::BadChecksum; false } else { @@ -150,7 +157,7 @@ impl<'a> Package<'a> { // Get length of header and data, including header and manifest. let header_data_length = delta_update::get_header_data_length(&header, &delta_archive_manifest).context("failed to get header data length")?; - let hdhash = self.hash_on_disk(from_path, Some(header_data_length)).context(format!("failed to hash_on_disk path ({:?}) failed", from_path.display()))?; + let hdhash = self.hash_on_disk::(from_path, Some(header_data_length)).context(format!("failed to hash_on_disk path ({:?}) failed", from_path.display()))?; let hdhashvec: Vec = hdhash.clone().into(); // Extract data blobs into a file, datablobspath. @@ -162,7 +169,7 @@ impl<'a> Package<'a> { None => bail!("unable to get new_partition_info hash"), }; - let datahash = self.hash_on_disk(datablobspath.as_path(), None).context(format!("failed to hash_on_disk path ({:?})", datablobspath.display()))?; + let datahash = self.hash_on_disk::(datablobspath.as_path(), None).context(format!("failed to hash_on_disk path ({:?})", datablobspath.display()))?; if datahash != omaha::Hash::from_bytes(pinfo_hash.as_slice()[..].try_into().unwrap_or_default()) { bail!( "mismatch of data hash ({:?}) with new_partition_info hash ({:?})", @@ -207,30 +214,29 @@ fn get_pkgs_to_download<'a>(resp: &'a omaha::Response, glob_set: &GlobSet) } let hash_sha256 = pkg.hash_sha256.as_ref(); + let hash_sha1 = pkg.hash.as_ref(); // TODO: multiple URLs per package // not sure if nebraska sends us more than one right now but i suppose this is // for mirrors? - let url = app.update_check.urls.get(0) - .map(|u| u.join(&pkg.name)); + let Some(Ok(url)) = app.update_check.urls.get(0) + .map(|u| u.join(&pkg.name)) else { + warn!("can't get url for package `{}`, skipping", pkg.name); + continue; + }; - match (url, hash_sha256) { - (Some(Ok(url)), Some(hash)) => { + if hash_sha256.is_none() && hash_sha1.is_none() { + warn!("package `{}` doesn't have a valid SHA256 or SHA1 hash, skipping", pkg.name); + continue; + } to_download.push(Package { url, name: Cow::Borrowed(&pkg.name), - hash: hash.clone(), + hash_sha256: hash_sha256.cloned(), + hash_sha1: hash_sha1.cloned(), size: pkg.size, status: PackageStatus::ToDownload - }) - } - - (Some(Ok(_)), None) => { - warn!("package `{}` doesn't have a valid SHA256 hash, skipping", pkg.name); - } - - _ => (), - } + }); } } @@ -247,7 +253,8 @@ where Ok(Package { name: Cow::Borrowed(path.file_name().unwrap_or(OsStr::new("fakepackage")).to_str().unwrap_or("fakepackage")), - hash: r.hash, + hash_sha256: Some(r.hash_sha256), + hash_sha1: Some(r.hash_sha1), size: FileSize::from_bytes(r.data.metadata().context(format!("failed to get metadata, path ({:?})", path.display()))?.len() as usize), url: input_url.into(), status: PackageStatus::Unverified, diff --git a/src/download.rs b/src/download.rs index 27d3a9d..14b6cb8 100644 --- a/src/download.rs +++ b/src/download.rs @@ -8,18 +8,19 @@ use url::Url; use reqwest::StatusCode; use reqwest::blocking::Client; -use sha2::{Sha256, Digest}; +use sha2::digest::DynDigest; const MAX_DOWNLOAD_RETRY: u32 = 20; pub struct DownloadResult { - pub hash: omaha::Hash, + pub hash_sha256: omaha::Hash, + pub hash_sha1: omaha::Hash, pub data: File, } -pub fn hash_on_disk_sha256(path: &Path, maxlen: Option) -> Result> { +pub fn hash_on_disk(path: &Path, maxlen: Option) -> Result> { let file = File::open(path).context(format!("failed to open path({:?})", path.display()))?; - let mut hasher = Sha256::new(); + let mut hasher = T::hasher(); let filelen = file.metadata().context(format!("failed to get metadata of {:?}", path.display()))?.len() as usize; @@ -55,7 +56,7 @@ pub fn hash_on_disk_sha256(path: &Path, maxlen: Option) -> Result(client: &Client, url: U, path: &Path, print_progress: bool) -> Result @@ -95,7 +96,8 @@ where res.copy_to(&mut file)?; Ok(DownloadResult { - hash: hash_on_disk_sha256(path, None)?, + hash_sha256: hash_on_disk::(path, None)?, + hash_sha1: hash_on_disk::(path, None)?, data: file, }) } diff --git a/src/lib.rs b/src/lib.rs index 8b05895..1ef93f9 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1,7 +1,7 @@ mod download; pub use download::DownloadResult; pub use download::download_and_hash; -pub use download::hash_on_disk_sha256; +pub use download::hash_on_disk; mod util; pub use util::retry_loop; diff --git a/test/crau_verify.rs b/test/crau_verify.rs index bd86474..5c53655 100644 --- a/test/crau_verify.rs +++ b/test/crau_verify.rs @@ -44,7 +44,7 @@ fn main() -> Result<(), Box> { // Get length of header and data, including header and manifest. let header_data_length = delta_update::get_header_data_length(&header, &delta_archive_manifest).context("failed to get header data length")?; - let hdhash = ue_rs::hash_on_disk_sha256(headerdatapath.as_path(), Some(header_data_length))?; + let hdhash = ue_rs::hash_on_disk::(headerdatapath.as_path(), Some(header_data_length))?; let hdhashvec: Vec = hdhash.clone().into(); // Get length of header and data From 887994b752e2a1bf50a8e849bf936380abf20cee Mon Sep 17 00:00:00 2001 From: Kai Lueke Date: Wed, 3 Jan 2024 15:36:04 +0100 Subject: [PATCH 2/4] Verify checksum after download, with retry The self.verify_checksum(...) call's return value wasn't checked in the package download call. Even if we do it there we should rather move it into the retry loop and make it explicit whether we expect certain checksums or not. Check the checksum after the download, and also retry when it mismatches. --- examples/download_test.rs | 2 +- examples/full_test.rs | 2 +- src/bin/download_sysext.rs | 13 +++++++--- src/download.rs | 52 +++++++++++++++++++++++++++++++++----- 4 files changed, 58 insertions(+), 11 deletions(-) diff --git a/examples/download_test.rs b/examples/download_test.rs index c8dabd1..367808a 100644 --- a/examples/download_test.rs +++ b/examples/download_test.rs @@ -13,7 +13,7 @@ fn main() -> Result<(), Box> { let tempdir = tempfile::tempdir()?; let path = tempdir.path().join("tmpfile"); - let res = download_and_hash(&client, url, &path, false)?; + let res = download_and_hash(&client, url, &path, None, None, false)?; tempdir.close()?; println!("hash: {}", res.hash_sha256); diff --git a/examples/full_test.rs b/examples/full_test.rs index 24920f4..319ae4a 100644 --- a/examples/full_test.rs +++ b/examples/full_test.rs @@ -80,7 +80,7 @@ fn main() -> Result<(), Box> { let tempdir = tempfile::tempdir()?; let path = tempdir.path().join("tmpfile"); - let res = ue_rs::download_and_hash(&client, url.clone(), &path, false).context(format!("download_and_hash({url:?}) failed"))?; + let res = ue_rs::download_and_hash(&client, url.clone(), &path, Some(expected_sha256.clone()), None, false).context(format!("download_and_hash({url:?}) failed"))?; tempdir.close()?; println!("\texpected sha256: {}", expected_sha256); diff --git a/src/bin/download_sysext.rs b/src/bin/download_sysext.rs index 55d480c..6bb6727 100644 --- a/src/bin/download_sysext.rs +++ b/src/bin/download_sysext.rs @@ -109,7 +109,14 @@ impl<'a> Package<'a> { info!("downloading {}...", self.url); let path = into_dir.join(&*self.name); - let res = match ue_rs::download_and_hash(client, self.url.clone(), &path, print_progress) { + match ue_rs::download_and_hash( + client, + self.url.clone(), + &path, + self.hash_sha256.clone(), + self.hash_sha1.clone(), + print_progress, + ) { Ok(ok) => ok, Err(err) => { error!("Downloading failed with error {}", err); @@ -118,7 +125,7 @@ impl<'a> Package<'a> { } }; - self.verify_checksum(res.hash_sha256, res.hash_sha1); + self.status = PackageStatus::Unverified; Ok(()) } @@ -249,7 +256,7 @@ where U: reqwest::IntoUrl + From + std::clone::Clone + std::fmt::Debug, Url: From, { - let r = ue_rs::download_and_hash(client, input_url.clone(), path, print_progress).context(format!("unable to download data(url {:?})", input_url))?; + let r = ue_rs::download_and_hash(client, input_url.clone(), path, None, None, print_progress).context(format!("unable to download data(url {:?})", input_url))?; Ok(Package { name: Cow::Borrowed(path.file_name().unwrap_or(OsStr::new("fakepackage")).to_str().unwrap_or("fakepackage")), diff --git a/src/download.rs b/src/download.rs index 14b6cb8..7b2fdac 100644 --- a/src/download.rs +++ b/src/download.rs @@ -2,7 +2,7 @@ use anyhow::{Context, Result, bail}; use std::io::{BufReader, Read}; use std::fs::File; use std::path::Path; -use log::info; +use log::{info, debug}; use url::Url; use reqwest::StatusCode; @@ -59,7 +59,14 @@ pub fn hash_on_disk(path: &Path, maxlen: Option) -> R Ok(omaha::Hash::from_bytes(Box::new(hasher).finalize())) } -fn do_download_and_hash(client: &Client, url: U, path: &Path, print_progress: bool) -> Result +fn do_download_and_hash( + client: &Client, + url: U, + path: &Path, + expected_sha256: Option>, + expected_sha1: Option>, + print_progress: bool, +) -> Result where U: reqwest::IntoUrl + Clone, Url: From, @@ -95,20 +102,53 @@ where let mut file = File::create(path).context(format!("failed to create path ({:?})", path.display()))?; res.copy_to(&mut file)?; + let calculated_sha256 = hash_on_disk::(path, None)?; + let calculated_sha1 = hash_on_disk::(path, None)?; + + debug!(" expected sha256: {:?}", expected_sha256); + debug!(" calculated sha256: {}", calculated_sha256); + debug!(" sha256 match? {}", expected_sha256 == Some(calculated_sha256.clone())); + debug!(" expected sha1: {:?}", expected_sha1); + debug!(" calculated sha1: {}", calculated_sha1); + debug!(" sha1 match? {}", expected_sha1 == Some(calculated_sha1.clone())); + + if expected_sha256.is_some() && expected_sha256 != Some(calculated_sha256.clone()) { + bail!("Checksum mismatch for sha256"); + } + if expected_sha1.is_some() && expected_sha1 != Some(calculated_sha1.clone()) { + bail!("Checksum mismatch for sha1"); + } + Ok(DownloadResult { - hash_sha256: hash_on_disk::(path, None)?, - hash_sha1: hash_on_disk::(path, None)?, + hash_sha256: calculated_sha256, + hash_sha1: calculated_sha1, data: file, }) } -pub fn download_and_hash(client: &Client, url: U, path: &Path, print_progress: bool) -> Result +pub fn download_and_hash( + client: &Client, + url: U, + path: &Path, + expected_sha256: Option>, + expected_sha1: Option>, + print_progress: bool, +) -> Result where U: reqwest::IntoUrl + Clone, Url: From, { crate::retry_loop( - || do_download_and_hash(client, url.clone(), path, print_progress), + || { + do_download_and_hash( + client, + url.clone(), + path, + expected_sha256.clone(), + expected_sha1.clone(), + print_progress, + ) + }, MAX_DOWNLOAD_RETRY, ) } From 4a5a55fa771dbf31f4416521caf1b2eb1877604e Mon Sep 17 00:00:00 2001 From: Kai Lueke Date: Fri, 5 Jan 2024 14:50:57 +0100 Subject: [PATCH 3/4] Fix clippy warnings for non-vendored crates, disable on vendored crates --- omaha/src/hash_types.rs | 2 ++ src/bin/download_sysext.rs | 4 ++-- src/request.rs | 2 +- vendor/hard-xml-derive/src/lib.rs | 2 ++ vendor/hard-xml/src/lib.rs | 1 + 5 files changed, 8 insertions(+), 3 deletions(-) diff --git a/omaha/src/hash_types.rs b/omaha/src/hash_types.rs index 64cf7bf..05026c3 100644 --- a/omaha/src/hash_types.rs +++ b/omaha/src/hash_types.rs @@ -44,6 +44,7 @@ impl HashAlgo for Sha1 { Ok(a) => a, Err(e) => { println!("Unexpected length {}", e.len()); + #[allow(clippy::box_default)] Box::new(Self::Output::default()) } }; @@ -66,6 +67,7 @@ impl HashAlgo for Sha256 { Ok(a) => a, Err(e) => { println!("Unexpected length {}", e.len()); + #[allow(clippy::box_default)] Box::new(Self::Output::default()) } }; diff --git a/src/bin/download_sysext.rs b/src/bin/download_sysext.rs index 6bb6727..592faaa 100644 --- a/src/bin/download_sysext.rs +++ b/src/bin/download_sysext.rs @@ -177,7 +177,7 @@ impl<'a> Package<'a> { }; let datahash = self.hash_on_disk::(datablobspath.as_path(), None).context(format!("failed to hash_on_disk path ({:?})", datablobspath.display()))?; - if datahash != omaha::Hash::from_bytes(pinfo_hash.as_slice()[..].try_into().unwrap_or_default()) { + if datahash != omaha::Hash::from_bytes(pinfo_hash.as_slice()[..].into()) { bail!( "mismatch of data hash ({:?}) with new_partition_info hash ({:?})", datahash, @@ -226,7 +226,7 @@ fn get_pkgs_to_download<'a>(resp: &'a omaha::Response, glob_set: &GlobSet) // TODO: multiple URLs per package // not sure if nebraska sends us more than one right now but i suppose this is // for mirrors? - let Some(Ok(url)) = app.update_check.urls.get(0) + let Some(Ok(url)) = app.update_check.urls.first() .map(|u| u.join(&pkg.name)) else { warn!("can't get url for package `{}`, skipping", pkg.name); continue; diff --git a/src/request.rs b/src/request.rs index 4adcf40..ccba9ff 100644 --- a/src/request.rs +++ b/src/request.rs @@ -28,7 +28,7 @@ pub struct Parameters<'a> { pub machine_id: Cow<'a, str>, } -pub fn perform<'a>(client: &reqwest::blocking::Client, parameters: Parameters<'a>) -> Result { +pub fn perform(client: &reqwest::blocking::Client, parameters: Parameters<'_>) -> Result { let req_body = { let r = omaha::Request { protocol_version: Cow::Borrowed(PROTOCOL_VERSION), diff --git a/vendor/hard-xml-derive/src/lib.rs b/vendor/hard-xml-derive/src/lib.rs index 6d869db..fc478f5 100644 --- a/vendor/hard-xml-derive/src/lib.rs +++ b/vendor/hard-xml-derive/src/lib.rs @@ -1,5 +1,7 @@ #![recursion_limit = "256"] +#![allow(clippy::all)] + extern crate proc_macro; mod attrs; diff --git a/vendor/hard-xml/src/lib.rs b/vendor/hard-xml/src/lib.rs index c193176..bdda8b1 100644 --- a/vendor/hard-xml/src/lib.rs +++ b/vendor/hard-xml/src/lib.rs @@ -333,6 +333,7 @@ //! Root { attr: true } //! ); //! ``` +#![allow(clippy::all)] #[cfg(feature = "log")] mod log; From e9a158ba42e80d7dc110aea83179ebf66ce0ec67 Mon Sep 17 00:00:00 2001 From: Kai Lueke Date: Fri, 5 Jan 2024 14:55:38 +0100 Subject: [PATCH 4/4] .github: Run clippy on CI --- .github/workflows/ci.yaml | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index ecfe428..36c9e88 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -34,3 +34,8 @@ jobs: with: command: test args: --workspace + - name: Run clippy + uses: actions-rs/cargo@v1 + with: + command: clippy + args: --workspace