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