semifold_core/
file_edit.rs1use camino::Utf8PathBuf;
2use serde::Serialize;
3use sha2::{Digest, Sha256};
4
5use crate::{PackageId, VersionSourceId};
6
7#[derive(Clone, Debug, Eq, Ord, PartialEq, PartialOrd, Serialize)]
8pub struct SharedVersionEdit {
9 pub source: VersionSourceId,
10 pub packages: Vec<PackageId>,
11}
12
13#[derive(Clone, Debug, Eq, Hash, Ord, PartialEq, PartialOrd, Serialize)]
15#[serde(transparent)]
16pub struct FileHash(String);
17
18impl FileHash {
19 #[must_use]
20 pub fn from_bytes(bytes: &[u8]) -> Self {
21 Self(
22 Sha256::digest(bytes)
23 .iter()
24 .map(|byte| format!("{byte:02x}"))
25 .collect(),
26 )
27 }
28
29 pub fn from_sha256(value: impl Into<String>) -> Result<Self, FileHashError> {
31 let value = value.into();
32 if value.len() == 64
33 && value
34 .bytes()
35 .all(|byte| byte.is_ascii_digit() || (b'a'..=b'f').contains(&byte))
36 {
37 Ok(Self(value))
38 } else {
39 Err(FileHashError::InvalidSha256 { value })
40 }
41 }
42
43 #[must_use]
44 pub fn as_str(&self) -> &str {
45 &self.0
46 }
47}
48
49#[derive(Clone, Debug, Eq, PartialEq, thiserror::Error)]
50pub enum FileHashError {
51 #[error("file SHA-256 must contain exactly 64 lowercase hexadecimal characters: {value}")]
52 InvalidSha256 { value: String },
53}
54
55#[derive(Clone, Debug, Eq, Ord, PartialEq, PartialOrd, Serialize)]
57#[serde(tag = "kind", rename_all = "snake_case")]
58pub enum EditSource {
59 PackageVersion {
60 package: PackageId,
61 },
62 DependencyVersion {
63 package: PackageId,
64 dependency: PackageId,
65 },
66 WorkspaceDependencies {
67 dependencies: Vec<PackageId>,
68 },
69 WorkspaceManifest {
70 shared_versions: Vec<SharedVersionEdit>,
71 dependencies: Vec<PackageId>,
72 },
73 Changelog {
74 package: PackageId,
75 },
76}
77
78#[derive(Clone, Debug, Eq, PartialEq, Serialize)]
80#[serde(tag = "kind", rename_all = "snake_case")]
81pub enum FileEditExpectation {
82 Existing { hash: FileHash },
83 Missing,
84}
85
86#[derive(Clone, Debug, Eq, PartialEq, Serialize)]
88pub struct FileEdit {
89 pub path: Utf8PathBuf,
90 pub expected: FileEditExpectation,
91 pub new_content: String,
92 pub source: EditSource,
93}
94
95#[cfg(test)]
96mod tests {
97 use super::{FileHash, FileHashError};
98
99 #[test]
100 fn hashes_source_bytes_with_sha256() {
101 assert_eq!(
102 FileHash::from_bytes(b"semifold").as_str(),
103 "acfa94237c0f2abcae06590ebe6bb12455e24f07a9608a2418d618b540aee4e0"
104 );
105 }
106
107 #[test]
108 fn restores_only_canonical_sha256_values() {
109 let value = "acfa94237c0f2abcae06590ebe6bb12455e24f07a9608a2418d618b540aee4e0";
110
111 assert_eq!(FileHash::from_sha256(value).unwrap().as_str(), value);
112 assert!(matches!(
113 FileHash::from_sha256(value.to_uppercase()),
114 Err(FileHashError::InvalidSha256 { .. })
115 ));
116 assert!(matches!(
117 FileHash::from_sha256("short"),
118 Err(FileHashError::InvalidSha256 { .. })
119 ));
120 }
121}