Skip to main content

spreadsheet_kit/
utils.rs

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}