1use std::{convert::TryFrom, fmt, hash::Hash};
2
3use hex::FromHex;
4use rand::Rng;
5use sha2::{Digest, Sha256};
6
7pub mod config;
8pub mod errors;
9#[allow(clippy::all)]
10pub mod generated;
11pub mod path;
12pub mod range;
13pub mod region;
14pub mod strand;
15pub mod traits;
16
17pub use config::Workspace;
18pub use generated::gen_core_capnp;
19pub use path::PathBlock;
20#[cfg(feature = "python-bindings")]
21use pyo3::pyclass;
22pub use strand::Strand;
23
24pub static NO_CHROMOSOME_INDEX: i64 = -1;
25pub static PRESERVE_EDIT_SITE_CHROMOSOME_INDEX: i64 = -2;
26pub static INDETERMINATE_CHROMOSOME_INDEX: i64 = -3;
27
28pub const PATH_START_NODE_ID: HashId = HashId([
30 0x84, 0xd6, 0xad, 0xbd, 0x53, 0x95, 0x28, 0x19, 0x33, 0xfe, 0x41, 0xe8, 0x77, 0xd3, 0xa7, 0xf0,
31 0x2a, 0x3b, 0x19, 0x90, 0xa6, 0x5b, 0xe1, 0x90, 0x1b, 0x2c, 0x91, 0xfc, 0x68, 0x5e, 0x08, 0x3b,
32]);
33pub const PATH_END_NODE_ID: HashId = HashId([
34 0x1c, 0x7d, 0xfc, 0x64, 0x97, 0x7b, 0x08, 0x38, 0xaf, 0x07, 0x62, 0xd7, 0x33, 0x3d, 0xcb, 0x64,
35 0xc1, 0x75, 0xb1, 0x5e, 0x65, 0xa7, 0x00, 0x99, 0xec, 0x38, 0xf4, 0x6b, 0xf1, 0xa1, 0x5e, 0xa3,
36]);
37
38pub fn is_terminal(node_id: HashId) -> bool {
39 is_start_node(node_id) || is_end_node(node_id)
40}
41
42pub fn is_start_node(node_id: HashId) -> bool {
43 node_id == PATH_START_NODE_ID
44}
45
46pub fn is_end_node(node_id: HashId) -> bool {
47 node_id == PATH_END_NODE_ID
48}
49
50#[cfg_attr(feature = "python-bindings", pyclass)]
51#[derive(Clone, Copy, Default, PartialEq, Eq, Hash, PartialOrd, Ord)]
52pub struct HashId(pub [u8; 32]);
53
54impl serde::Serialize for HashId {
55 fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
56 where
57 S: serde::Serializer,
58 {
59 let hex_str = hex::encode(self.0);
60 serializer.serialize_str(&hex_str)
61 }
62}
63
64impl<'de> serde::Deserialize<'de> for HashId {
65 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
66 where
67 D: serde::Deserializer<'de>,
68 {
69 struct HashIdVisitor;
70
71 impl<'de> serde::de::Visitor<'de> for HashIdVisitor {
72 type Value = HashId;
73
74 fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result {
75 formatter.write_str("a 64-character hex string")
76 }
77
78 fn visit_str<E>(self, v: &str) -> Result<HashId, E>
79 where
80 E: serde::de::Error,
81 {
82 let bytes = hex::decode(v).map_err(E::custom)?;
83 if bytes.len() != 32 {
84 return Err(E::custom("expected 32 bytes"));
85 }
86 let mut arr = [0u8; 32];
87 arr.copy_from_slice(&bytes);
88 Ok(HashId(arr))
89 }
90 }
91
92 deserializer.deserialize_str(HashIdVisitor)
93 }
94}
95
96impl fmt::Display for HashId {
97 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
98 for byte in &self.0 {
99 write!(f, "{byte:02x}")?;
100 }
101 Ok(())
102 }
103}
104
105impl TryFrom<String> for HashId {
106 type Error = hex::FromHexError;
107
108 fn try_from(s: String) -> Result<Self, Self::Error> {
109 let bytes = <[u8; 32]>::from_hex(&s)?;
110 Ok(HashId(bytes))
111 }
112}
113
114impl fmt::Debug for HashId {
115 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
116 write!(f, "HashId({})", hex::encode(self.0))
117 }
118}
119
120impl TryFrom<&[u8]> for HashId {
121 type Error = &'static str;
122
123 fn try_from(slice: &[u8]) -> Result<Self, Self::Error> {
124 if slice.len() != 32 {
125 return Err("slice must be exactly 32 bytes long");
126 }
127
128 let mut array = [0u8; 32];
129 array.copy_from_slice(slice);
130 Ok(HashId(array))
131 }
132}
133
134impl TryFrom<&str> for HashId {
135 type Error = &'static str;
136
137 fn try_from(s: &str) -> Result<Self, Self::Error> {
138 let bytes = hex::decode(s).expect("invalid hex string");
139 Ok(HashId(bytes.try_into().expect("not 32 bytes")))
140 }
141}
142
143impl PartialEq<[u8; 32]> for HashId {
144 fn eq(&self, other: &[u8; 32]) -> bool {
145 &self.0 == other
146 }
147}
148
149impl PartialEq<HashId> for [u8; 32] {
150 fn eq(&self, other: &HashId) -> bool {
151 self == &other.0
152 }
153}
154
155use rusqlite::types::{FromSql, FromSqlError, FromSqlResult, ToSql, ToSqlOutput, ValueRef};
156
157impl FromSql for HashId {
158 fn column_result(value: ValueRef<'_>) -> FromSqlResult<Self> {
159 match value {
160 ValueRef::Blob(b) => {
161 if b.len() == 32 {
162 let mut arr = [0u8; 32];
163 arr.copy_from_slice(b);
164 Ok(HashId(arr))
165 } else {
166 Err(FromSqlError::Other(Box::new(std::io::Error::new(
167 std::io::ErrorKind::InvalidData,
168 format!("expected 32-byte blob, got {}", b.len()),
169 ))))
170 }
171 }
172 _ => Err(FromSqlError::InvalidType),
173 }
174 }
175}
176
177impl ToSql for HashId {
178 fn to_sql(&self) -> rusqlite::Result<ToSqlOutput<'_>> {
179 Ok(ToSqlOutput::from(self.0.as_ref())) }
181}
182
183use rusqlite::types::Value;
184
185impl From<HashId> for Value {
186 fn from(h: HashId) -> Self {
187 Value::Blob(h.0.to_vec())
188 }
189}
190
191impl From<uuid::Uuid> for HashId {
192 fn from(uuid: uuid::Uuid) -> Self {
193 let mut buf = [0u8; 32];
194 buf[..16].copy_from_slice(uuid.as_bytes());
195 Self(buf)
196 }
197}
198
199impl HashId {
200 pub fn pad_str<T: ToString>(input: T) -> Self {
201 let s = input.to_string();
202 let hex = format!("{s:0>64}"); let bytes = hex::decode(hex).expect("invalid hex string");
204 HashId(bytes.try_into().expect("not 32 bytes"))
205 }
206
207 pub fn convert_str(s: &str) -> Self {
208 HashId(calculate_hash(s))
209 }
210
211 pub fn random_str() -> Self {
212 let mut rng = rand::rng();
213 let mut random_bytes = [0u8; 32];
214 rng.fill_bytes(&mut random_bytes);
215 Self(random_bytes)
216 }
217
218 pub fn uuid7() -> Self {
219 uuid::Uuid::now_v7().into()
220 }
221
222 pub fn extract_digits(&self) -> i64 {
224 let hex = format!("{self}");
225 let digits: String = hex
226 .chars()
227 .map(|c| {
228 if c.is_ascii_digit() {
229 c
230 } else {
231 (c.to_ascii_lowercase() as u8 - 49 + 1) as char
234 }
235 })
236 .take(15) .collect();
238
239 digits.parse().unwrap_or(0)
240 }
241
242 pub fn starts_with(&self, prefix: &str) -> bool {
243 if prefix.len() > 64 || prefix.is_empty() {
244 return false;
245 }
246
247 let end_byte = prefix.len() / 2 + prefix.len() % 2;
248 let encoded = hex::encode(&self.0[..end_byte]);
249 encoded.starts_with(prefix)
250 }
251}
252
253#[derive(Copy, Clone, Debug, Eq, PartialEq, Hash, Ord, PartialOrd)]
254pub struct NodeIntervalBlock {
255 pub node_id: HashId,
256 pub start: i64,
257 pub end: i64,
258 pub sequence_start: i64,
259 pub sequence_end: i64,
260 pub strand: Strand,
261}
262
263pub fn calculate_hash(t: &str) -> [u8; 32] {
264 let mut hasher = Sha256::new();
265 hasher.update(t);
266 let result = hasher.finalize();
267 result.into()
268}
269
270#[cfg(test)]
271mod tests {
272 use super::*;
273
274 #[test]
275 fn it_hashes() {
276 let result: HashId = "a82639b6f8c3a6e536d8cc562c3b86ff4b012c84ab230c1e5be649aa9ad26d21"
277 .try_into()
278 .unwrap();
279 assert_eq!(calculate_hash("a test"), result);
280 }
281
282 #[cfg(test)]
283 mod hashid {
284 use super::*;
285
286 #[test]
287 fn test_starts_with() {
288 let hash: HashId = "a82639b6f8c3a6e536d8cc562c3b86ff4b012c84ab230c1e5be649aa9ad26d21"
289 .try_into()
290 .unwrap();
291 assert!(hash.starts_with("a826"));
292 assert!(
293 hash.starts_with(
294 "a82639b6f8c3a6e536d8cc562c3b86ff4b012c84ab230c1e5be649aa9ad26d21"
295 )
296 );
297 assert!(!hash.starts_with("b826"));
298 assert!(
299 !hash.starts_with(
300 "a82639b6f8c3a6e536d8cc562c3b86ff4b012c84ab230c1e5be649aa9ad26d210"
301 )
302 );
303 assert!(!hash.starts_with(""));
304 assert!(
305 !hash.starts_with(
306 "a82639b6f8c3a6e536d8cc562c3b86ff4b012c84ab230c1e5be649aa9ad26d219"
307 )
308 );
309 }
310 }
311}