Skip to content

Commit

Permalink
Implement Checksum and Digest mechanisms (#783)
Browse files Browse the repository at this point in the history
**Stack**:
- #793
- #790
- #792
- #783⚠️ *Part of a stack created by [spr](https://github.com/ejoffe/spr). Do
not merge manually using the UI - doing so may have unexpected results.*
  • Loading branch information
mkaput authored Oct 16, 2023
1 parent 658fb89 commit e5b673a
Show file tree
Hide file tree
Showing 6 changed files with 178 additions and 2 deletions.
1 change: 1 addition & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -76,6 +76,7 @@ serde-untagged = "0.1"
serde-value = "0.7"
serde_json = "1"
serde_test = "1"
sha2 = "0.10"
similar-asserts = { version = "1", features = ["serde"] }
smallvec = "1"
smol_str = { version = "0.2", features = ["serde"] }
Expand Down
1 change: 1 addition & 0 deletions scarb/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,7 @@ serde-untagged.workspace = true
serde-value.workspace = true
serde.workspace = true
serde_json.workspace = true
sha2.workspace = true
smallvec.workspace = true
smol_str.workspace = true
tar.workspace = true
Expand Down
171 changes: 171 additions & 0 deletions scarb/src/core/checksum.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,171 @@
use std::fmt;
use std::fmt::Write;
use std::str;
use std::str::FromStr;

use anyhow::{bail, ensure, Context, Result};
use data_encoding::{Encoding, HEXLOWER_PERMISSIVE};
use serde::{Deserialize, Serialize};
use sha2::Digest as _;

#[derive(Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
#[serde(try_from = "String", into = "String")]
pub struct Checksum([u8; 32]);

impl Checksum {
const HASH_FUNC_TYPE: &'_ str = "sha256";
const ENCODING: Encoding = HEXLOWER_PERMISSIVE;

pub fn parse(s: &str) -> Result<Self> {
fn inner(s: &str) -> Result<Checksum> {
let Some((hash_func_type, hash)) = s.split_once(':') else {
bail!("checksum is missing hash function type prefix");
};

ensure!(
hash_func_type == Checksum::HASH_FUNC_TYPE,
"unsupported hash function type: {hash_func_type}",
);

let mut buffer = [0u8; 32];
let expected_len = buffer.len();

let decode_len = Checksum::ENCODING.decode_len(hash.len())?;
ensure!(
decode_len == expected_len,
"invalid checksum length {decode_len}, should be {expected_len}"
);

let len = Checksum::ENCODING
.decode_mut(hash.as_bytes(), &mut buffer)
.map_err(|e| e.error)?;
ensure!(
len == expected_len,
"invalid checksum length {len}, should be {expected_len}"
);

Ok(Checksum(buffer))
}

inner(s).with_context(|| format!("failed to parse checksum: {s}"))
}

/// Create a [`Digest`] instance which will use the same algorithm as this checksum.
pub fn digest(&self) -> Digest {
Digest::recommended()
}
}

impl FromStr for Checksum {
type Err = anyhow::Error;

fn from_str(s: &str) -> Result<Self, Self::Err> {
Checksum::parse(s)
}
}

impl<'a> TryFrom<&'a str> for Checksum {
type Error = anyhow::Error;

fn try_from(s: &str) -> Result<Self, Self::Error> {
s.parse()
}
}

impl TryFrom<String> for Checksum {
type Error = anyhow::Error;

fn try_from(s: String) -> Result<Self, Self::Error> {
s.parse()
}
}

impl From<Checksum> for String {
fn from(c: Checksum) -> Self {
c.to_string()
}
}

impl fmt::Display for Checksum {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.write_str(Checksum::HASH_FUNC_TYPE)?;
f.write_char(':')?;

let mut buffer = [0u8; 64];
Checksum::ENCODING.encode_mut(&self.0, &mut buffer);
// SAFETY: We just generated this hexadecimal string.
let string = unsafe { str::from_utf8_unchecked(&buffer) };
f.write_str(string)?;

Ok(())
}
}

impl fmt::Debug for Checksum {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "Checksum({self})")
}
}

pub struct Digest(sha2::Sha256);

impl Digest {
/// Get recommended digest algorithm.
pub fn recommended() -> Self {
Self(sha2::Sha256::new())
}

pub fn update(&mut self, bytes: &[u8]) -> &mut Self {
self.0.update(bytes);
self
}

pub fn finish(&mut self) -> Checksum {
Checksum(self.0.finalize_reset().into())
}
}

#[cfg(test)]
mod tests {
use super::{Checksum, Digest};

#[test]
fn checksum_parse_display() {
let s = "sha256:0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef";
let checksum = Checksum::parse(s).unwrap();
assert_eq!(
checksum,
Checksum([
0x01, 0x23, 0x45, 0x67, 0x89, 0xab, 0xcd, 0xef, 0x01, 0x23, 0x45, 0x67, 0x89, 0xab,
0xcd, 0xef, 0x01, 0x23, 0x45, 0x67, 0x89, 0xab, 0xcd, 0xef, 0x01, 0x23, 0x45, 0x67,
0x89, 0xab, 0xcd, 0xef
])
);
assert_eq!(checksum.to_string(), s);
}

#[test]
fn checksum_serde() {
let json = r#""sha256:0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef""#;
let checksum = serde_json::from_str::<Checksum>(json).unwrap();
assert_eq!(
checksum,
Checksum([
0x01, 0x23, 0x45, 0x67, 0x89, 0xab, 0xcd, 0xef, 0x01, 0x23, 0x45, 0x67, 0x89, 0xab,
0xcd, 0xef, 0x01, 0x23, 0x45, 0x67, 0x89, 0xab, 0xcd, 0xef, 0x01, 0x23, 0x45, 0x67,
0x89, 0xab, 0xcd, 0xef
])
);
assert_eq!(serde_json::to_string(&checksum).unwrap(), json);
}

#[test]
fn digest() {
let input = b"Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod.";
let expected = "sha256:b62fc4b9bfbd9310a47d2e595d2c8f468354266be0827aeea9b465d9984908de"
.parse()
.unwrap();
let actual = Digest::recommended().update(input).finish();
assert_eq!(actual, expected);
}
}
2 changes: 2 additions & 0 deletions scarb/src/core/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
//!
//! For read operations and workspace mutations, see [`crate::ops`] module.
pub use checksum::*;
pub use config::Config;
pub use dirs::AppDirs;
pub use manifest::*;
Expand All @@ -10,6 +11,7 @@ pub use resolver::Resolve;
pub use source::{GitReference, SourceId, SourceIdInner, SourceKind};
pub use workspace::{Utf8PathWorkspaceExt, Workspace};

mod checksum;
pub(crate) mod config;
mod dirs;
pub mod errors;
Expand Down
4 changes: 2 additions & 2 deletions scarb/src/core/registry/index/record.rs
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
use semver::{Version, VersionReq};
use serde::{Deserialize, Serialize};

use crate::core::PackageName;
use crate::core::{Checksum, PackageName};

pub type IndexRecords = Vec<IndexRecord>;

Expand All @@ -12,7 +12,7 @@ pub struct IndexRecord {
#[serde(rename = "deps")]
pub dependencies: IndexDependencies,
#[serde(rename = "cksum")]
pub checksum: String,
pub checksum: Checksum,
#[serde(default = "default_false", skip_serializing_if = "is_false")]
pub no_core: bool,
}
Expand Down

0 comments on commit e5b673a

Please sign in to comment.