distributed_lock_file/
name.rs1use sha2::{Digest, Sha512};
4use std::path::{Path, PathBuf};
5
6use distributed_lock_core::error::{LockError, LockResult};
7
8const MIN_FILE_NAME_LENGTH: usize = 12;
10
11const PORTABLE_FILE_NAME_LENGTH: usize = 64;
13
14const HASH_LENGTH_IN_CHARS: usize = 32;
16
17const BASE32_ALPHABET: &[u8] = b"ABCDEFGHIJKLMNOPQRSTUVWXYZ234567";
19
20pub fn get_lock_file_name(directory: &Path, name: &str) -> LockResult<PathBuf> {
29 if name.is_empty() {
30 return Err(LockError::InvalidName(
31 "lock name cannot be empty".to_string(),
32 ));
33 }
34
35 let directory_path = directory
36 .canonicalize()
37 .or_else(|_| std::fs::create_dir_all(directory).map(|_| directory.to_path_buf()))
38 .map_err(|e| LockError::InvalidName(format!("failed to create directory: {e}")))?;
39
40 let directory_path_str = directory_path.to_string_lossy();
41 let directory_path_with_separator = if directory_path_str.ends_with('/') {
42 directory_path_str.to_string()
43 } else {
44 format!("{directory_path_str}/")
45 };
46
47 let base_name = convert_to_valid_base_name(name);
48 let name_hash = compute_hash(name.as_bytes());
49 const EXTENSION: &str = ".lock";
50
51 let base_name_prefix_len = PORTABLE_FILE_NAME_LENGTH
53 .saturating_sub(name_hash.len())
54 .saturating_sub(EXTENSION.len())
55 .min(base_name.len());
56
57 let portable_lock_file_name = format!(
58 "{}{}{}{}",
59 directory_path_with_separator,
60 &base_name[..base_name_prefix_len],
61 name_hash,
62 EXTENSION
63 );
64
65 if !is_too_long(&portable_lock_file_name) {
66 return Ok(PathBuf::from(portable_lock_file_name));
67 }
68
69 let hash_only_file_name = format!("{directory_path_with_separator}{name_hash}");
71 if !is_too_long(&hash_only_file_name) {
72 return Ok(PathBuf::from(hash_only_file_name));
73 }
74
75 let minimum_length_file_name = format!(
77 "{}{}",
78 directory_path_with_separator,
79 &name_hash[..name_hash.len().min(MIN_FILE_NAME_LENGTH)]
80 );
81 if !is_too_long(&minimum_length_file_name) {
82 return Ok(PathBuf::from(minimum_length_file_name));
83 }
84
85 Err(LockError::InvalidName(format!(
86 "unable to construct lock file name: directory path too long (length = {})",
87 directory_path_with_separator.len()
88 )))
89}
90
91fn is_too_long(path: &str) -> bool {
92 Path::new(path).canonicalize().is_err() && path.len() > 255 }
94
95fn convert_to_valid_base_name(name: &str) -> String {
96 const REPLACEMENT_CHAR: char = '_';
97
98 let mut result = String::with_capacity(name.len());
99 let mut needs_replacement = false;
100
101 for ch in name.chars() {
102 if ch.is_alphanumeric() || ch == REPLACEMENT_CHAR {
103 result.push(ch);
104 } else {
105 if !needs_replacement {
106 needs_replacement = true;
107 }
108 result.push(REPLACEMENT_CHAR);
109 }
110 }
111
112 result
113}
114
115fn compute_hash(bytes: &[u8]) -> String {
116 let mut hasher = Sha512::new();
117 hasher.update(bytes);
118 let hash_bytes = hasher.finalize();
119
120 let truncated: Vec<u8> = hash_bytes[..20].to_vec();
122
123 let mut chars = Vec::with_capacity(HASH_LENGTH_IN_CHARS);
125 let mut bit_buffer = 0u32;
126 let mut bits_remaining = 0u32;
127
128 for byte in truncated {
129 bit_buffer |= (byte as u32) << bits_remaining;
130 bits_remaining += 8;
131
132 while bits_remaining >= 5 {
133 let index = (bit_buffer & 0x1f) as usize;
134 chars.push(BASE32_ALPHABET[index] as char);
135 bit_buffer >>= 5;
136 bits_remaining -= 5;
137 }
138 }
139
140 if bits_remaining > 0 {
142 let index = (bit_buffer & 0x1f) as usize;
143 chars.push(BASE32_ALPHABET[index] as char);
144 }
145
146 while chars.len() < HASH_LENGTH_IN_CHARS {
148 chars.push('A'); }
150
151 chars.into_iter().take(HASH_LENGTH_IN_CHARS).collect()
152}
153
154#[cfg(test)]
155mod tests {
156 use super::*;
157 use tempfile::tempdir;
158
159 #[test]
160 fn test_valid_name() {
161 let dir = tempdir().unwrap();
162 let result = get_lock_file_name(dir.path(), "my-lock");
163 assert!(result.is_ok());
164 let path = result.unwrap();
165 let path_str = path.to_string_lossy();
167 assert!(path_str.contains("my-lock") || path_str.contains("my_lock"));
168 assert!(path_str.ends_with(".lock"));
169 }
170
171 #[test]
172 fn test_invalid_chars() {
173 let dir = tempdir().unwrap();
174 let result = get_lock_file_name(dir.path(), "foo/bar");
175 assert!(result.is_ok());
176 let path = result.unwrap();
177 assert!(path.to_string_lossy().contains("foo_bar"));
178 }
179}