1use chrono::{DateTime, SecondsFormat, Utc};
2use rand::Rng;
3use sha2::{Digest, Sha256};
4use std::fs::{File, Metadata};
5use std::io::{BufReader, Read};
6use std::path::Path;
7use std::time::SystemTime;
8
9pub fn system_time_to_datetime(time: SystemTime) -> Option<DateTime<Utc>> {
10 Some(DateTime::<Utc>::from(time))
11}
12
13pub fn system_time_to_rfc3339(time: SystemTime) -> Option<DateTime<Utc>> {
14 system_time_to_datetime(time)
15}
16
17fn hash_path_metadata_digest(path: &Path, metadata: &Metadata) -> [u8; 32] {
18 let mut hasher = Sha256::new();
19 hasher.update(path.to_string_lossy().as_bytes());
20 hasher.update(metadata.len().to_le_bytes());
21 if let Ok(modified) = metadata.modified()
22 && let Some(dt) = system_time_to_datetime(modified)
23 {
24 hasher.update(dt.to_rfc3339_opts(SecondsFormat::Micros, true).as_bytes());
25 }
26 hasher.finalize().into()
27}
28
29fn hash_path_digest(path: &Path) -> [u8; 32] {
30 let mut hasher = Sha256::new();
31 hasher.update(path.to_string_lossy().as_bytes());
32 hasher.finalize().into()
33}
34
35pub fn hash_path_metadata_full(path: &Path, metadata: &Metadata) -> String {
36 let digest = hash_path_metadata_digest(path, metadata);
37 let mut out = String::with_capacity(64);
38 for b in digest {
39 out.push_str(&format!("{:02x}", b));
40 }
41 out
42}
43
44const WORKBOOK_ID_TOKEN_LEN: usize = 10;
45
46fn encode_base32_u64_prefix(value: u64, len: usize) -> String {
47 let mut out = String::with_capacity(len);
48 for i in 0..len {
49 let shift = 64 - (i + 1) * 5;
50 let idx = ((value >> shift) & 31) as usize;
51 out.push(SHORT_ID_ALPHABET[idx] as char);
52 }
53 out
54}
55
56pub fn hash_path_metadata(path: &Path, metadata: &Metadata) -> String {
57 let digest = hash_path_metadata_digest(path, metadata);
58 workbook_id_from_digest(digest)
59}
60
61pub fn hash_path_identity(path: &Path) -> String {
62 let digest = hash_path_digest(path);
63 workbook_id_from_digest(digest)
64}
65
66fn workbook_id_from_digest(digest: [u8; 32]) -> String {
67 let mut bytes = [0u8; 8];
68 bytes.copy_from_slice(&digest[..8]);
69 let value = u64::from_be_bytes(bytes);
70
71 format!(
72 "wb-{}",
73 encode_base32_u64_prefix(value, WORKBOOK_ID_TOKEN_LEN)
74 )
75}
76
77pub fn hash_bytes_sha256_hex(bytes: &[u8]) -> String {
78 let mut hasher = Sha256::new();
79 hasher.update(bytes);
80 format!("{:x}", hasher.finalize())
81}
82
83pub fn hash_file_sha256_hex(path: &Path) -> std::io::Result<String> {
84 let file = File::open(path)?;
85 let mut reader = BufReader::new(file);
86 let mut hasher = Sha256::new();
87 let mut buffer = [0u8; 64 * 1024];
88
89 loop {
90 let read = reader.read(&mut buffer)?;
91 if read == 0 {
92 break;
93 }
94 hasher.update(&buffer[..read]);
95 }
96
97 Ok(format!("{:x}", hasher.finalize()))
98}
99
100pub fn column_number_to_name(column: u32) -> String {
101 let mut column = column;
102 let mut name = String::new();
103 while column > 0 {
104 let rem = ((column - 1) % 26) as u8;
105 name.insert(0, (b'A' + rem) as char);
106 column = (column - 1) / 26;
107 }
108 name
109}
110
111pub fn cell_address(column: u32, row: u32) -> String {
112 format!("{}{}", column_number_to_name(column), row)
113}
114
115pub fn make_short_workbook_id(_slug: &str, canonical_id: &str) -> String {
116 canonical_id
117 .strip_prefix("wb-")
118 .unwrap_or(canonical_id)
119 .to_string()
120}
121
122pub fn path_to_forward_slashes(path: &Path) -> String {
123 let raw = path.to_string_lossy();
124 if raw.contains('\\') {
125 raw.replace('\\', "/")
126 } else {
127 raw.into_owned()
128 }
129}
130
131const SHORT_ID_ALPHABET: &[u8] = b"23456789abcdefghijkmnpqrstuvwxyz";
132
133pub fn make_short_random_id(prefix: &str, len: usize) -> String {
134 let mut rng = rand::thread_rng();
135
136 let mut out = String::with_capacity(prefix.len() + if prefix.is_empty() { 0 } else { 1 } + len);
137 if !prefix.is_empty() {
138 out.push_str(prefix);
139 out.push('-');
140 }
141
142 for _ in 0..len {
143 let idx = rng.gen_range(0..SHORT_ID_ALPHABET.len());
144 out.push(SHORT_ID_ALPHABET[idx] as char);
145 }
146
147 out
148}