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