1use std::fmt;
4use std::io::Read;
5use std::path::{Path, PathBuf};
6
7use serde::{Deserialize, Serialize};
8use sha2::{Digest, Sha256};
9
10#[derive(Clone, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
12pub struct BundlePath(String);
13
14impl BundlePath {
15 pub fn new(value: impl Into<String>) -> Result<Self, BundlePathError> {
17 let value = value.into();
18 if value.is_empty() {
19 return Err(BundlePathError::Empty);
20 }
21 if value.starts_with('/') {
22 return Err(BundlePathError::Absolute(value));
23 }
24 if value.contains('\\') {
25 return Err(BundlePathError::NotNormalized(value));
26 }
27 if value
28 .split('/')
29 .any(|component| component.is_empty() || component == "." || component == "..")
30 {
31 return Err(BundlePathError::NotNormalized(value));
32 }
33 Ok(Self(value))
34 }
35
36 #[must_use]
38 pub fn as_str(&self) -> &str {
39 &self.0
40 }
41
42 pub(crate) fn starts_with_directory(&self, directory: &str) -> bool {
43 self.0
44 .strip_prefix(directory)
45 .is_some_and(|rest| rest.starts_with('/') && rest.len() > 1)
46 }
47
48 pub(crate) fn filesystem_path(&self, root: &Path) -> PathBuf {
49 root.join(self.0.split('/').collect::<PathBuf>())
50 }
51}
52
53impl fmt::Display for BundlePath {
54 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
55 formatter.write_str(self.as_str())
56 }
57}
58
59impl Serialize for BundlePath {
60 fn serialize<S: serde::Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
61 serializer.serialize_str(self.as_str())
62 }
63}
64
65impl<'de> Deserialize<'de> for BundlePath {
66 fn deserialize<D: serde::Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
67 Self::new(String::deserialize(deserializer)?).map_err(serde::de::Error::custom)
68 }
69}
70
71#[derive(Clone, Copy, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
73pub struct Sha256Digest(pub(crate) [u8; 32]);
74
75impl Sha256Digest {
76 #[must_use]
78 pub fn of(bytes: &[u8]) -> Self {
79 Self(Sha256::digest(bytes).into())
80 }
81
82 pub fn from_reader(mut reader: impl Read) -> std::io::Result<Self> {
84 let mut hasher = Sha256::new();
85 let mut buffer = [0_u8; 64 * 1024];
86 loop {
87 let read = reader.read(&mut buffer)?;
88 if read == 0 {
89 break;
90 }
91 hasher.update(&buffer[..read]);
92 }
93 Ok(Self(hasher.finalize().into()))
94 }
95
96 pub fn parse(value: &str) -> Result<Self, DigestError> {
98 if value.len() != 64
99 || !value
100 .bytes()
101 .all(|byte| byte.is_ascii_hexdigit() && !byte.is_ascii_uppercase())
102 {
103 return Err(DigestError(value.to_string()));
104 }
105 let mut bytes = [0; 32];
106 for (index, pair) in value.as_bytes().chunks_exact(2).enumerate() {
107 bytes[index] = (hex(pair[0])? << 4) | hex(pair[1])?;
108 }
109 Ok(Self(bytes))
110 }
111
112 #[must_use]
114 pub fn as_hex(self) -> String {
115 let mut output = String::with_capacity(64);
116 for byte in self.0 {
117 output.push(hex_digit(byte >> 4));
118 output.push(hex_digit(byte & 0x0f));
119 }
120 output
121 }
122}
123
124impl fmt::Display for Sha256Digest {
125 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
126 formatter.write_str(&self.as_hex())
127 }
128}
129
130impl Serialize for Sha256Digest {
131 fn serialize<S: serde::Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
132 serializer.serialize_str(&self.as_hex())
133 }
134}
135
136impl<'de> Deserialize<'de> for Sha256Digest {
137 fn deserialize<D: serde::Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
138 Self::parse(&String::deserialize(deserializer)?).map_err(serde::de::Error::custom)
139 }
140}
141
142fn hex(value: u8) -> Result<u8, DigestError> {
143 match value {
144 b'0'..=b'9' => Ok(value - b'0'),
145 b'a'..=b'f' => Ok(value - b'a' + 10),
146 _ => Err(DigestError(String::from("non-hex digest"))),
147 }
148}
149
150const fn hex_digit(value: u8) -> char {
151 match value {
152 0..=9 => (b'0' + value) as char,
153 _ => (b'a' + value - 10) as char,
154 }
155}
156
157#[derive(Clone, Debug, Eq, PartialEq, thiserror::Error)]
159#[error("digest must be 64 lowercase hexadecimal characters, got '{0}'")]
160pub struct DigestError(String);
161
162#[derive(Clone, Debug, Eq, PartialEq, thiserror::Error)]
164pub enum BundlePathError {
165 #[error("bundle path is empty")]
166 Empty,
167 #[error("bundle path is absolute: '{0}'")]
168 Absolute(String),
169 #[error("bundle path is not normalized: '{0}'")]
170 NotNormalized(String),
171}