velesdb_memory/migration/
filesystem.rs1use super::query_error;
2use sha2::{Digest, Sha256};
3use std::ffi::OsStr;
4use std::fs::File;
5use std::io::{BufReader, Read};
6use std::path::{Component, Path, PathBuf};
7
8const FINGERPRINT_DOMAIN: &[u8] = b"velesdb-migration-source-tree-v2\0";
9
10#[derive(Debug, Clone, Copy)]
11enum EntryKind {
12 Directory,
13 File { len: u64 },
14}
15
16#[derive(Debug)]
17struct TreeEntry {
18 relative_path: PathBuf,
19 kind: EntryKind,
20}
21
22pub fn fingerprint(root: &Path) -> Result<String, crate::MemoryError> {
33 let entries = tree_entries(root)?;
34 let mut hash = Sha256::new();
35 hash.update(FINGERPRINT_DOMAIN);
36 hash.update(
37 u64::try_from(entries.len())
38 .unwrap_or(u64::MAX)
39 .to_le_bytes(),
40 );
41
42 for entry in entries {
43 match entry.kind {
44 EntryKind::Directory => hash.update([b'd']),
45 EntryKind::File { len } => {
46 hash.update([b'f']);
47 hash.update(len.to_le_bytes());
48 }
49 }
50 hash_relative_path(&mut hash, &entry.relative_path);
51 if let EntryKind::File { len } = entry.kind {
52 hash_file(root, &entry.relative_path, len, &mut hash)?;
53 }
54 }
55
56 Ok(format!("sha256-tree-v2:{}", encode_hex(&hash.finalize())))
57}
58
59pub fn bytes_on_disk(root: &Path) -> Result<u64, crate::MemoryError> {
64 tree_entries(root)?
65 .into_iter()
66 .try_fold(0u64, |total, entry| {
67 let len = match entry.kind {
68 EntryKind::Directory => 0,
69 EntryKind::File { len } => len,
70 };
71 total.checked_add(len).ok_or_else(|| {
72 query_error(format!(
73 "the byte count under {} exceeds u64",
74 root.display()
75 ))
76 })
77 })
78}
79
80fn tree_entries(root: &Path) -> Result<Vec<TreeEntry>, crate::MemoryError> {
81 let metadata = std::fs::symlink_metadata(root)
82 .map_err(|err| query_error(format!("cannot inspect source {}: {err}", root.display())))?;
83 if !metadata.is_dir() || metadata.file_type().is_symlink() {
84 return Err(query_error(format!(
85 "migration source {} must be a real directory, not a symlink or special file",
86 root.display()
87 )));
88 }
89
90 let mut entries = Vec::new();
91 collect_entries(root, root, &mut entries)?;
92 entries.sort_unstable_by(|left, right| left.relative_path.cmp(&right.relative_path));
93 Ok(entries)
94}
95
96fn collect_entries(
97 root: &Path,
98 directory: &Path,
99 entries: &mut Vec<TreeEntry>,
100) -> Result<(), crate::MemoryError> {
101 let read = std::fs::read_dir(directory)
102 .map_err(|err| query_error(format!("cannot read {}: {err}", directory.display())))?;
103 for entry in read {
104 let entry = entry.map_err(|err| query_error(format!("cannot read an entry: {err}")))?;
105 collect_entry(root, &entry.path(), entries)?;
106 }
107 Ok(())
108}
109
110fn collect_entry(
111 root: &Path,
112 path: &Path,
113 entries: &mut Vec<TreeEntry>,
114) -> Result<(), crate::MemoryError> {
115 let metadata = std::fs::symlink_metadata(path)
116 .map_err(|err| query_error(format!("cannot inspect {}: {err}", path.display())))?;
117 let relative_path = path
118 .strip_prefix(root)
119 .map_err(|err| query_error(format!("cannot relativize {}: {err}", path.display())))?
120 .to_path_buf();
121 if metadata.file_type().is_symlink() {
122 return Err(query_error(format!(
123 "migration source contains symlink {}; refusing to follow data outside the tree",
124 path.display()
125 )));
126 }
127 if metadata.is_dir() {
128 entries.push(TreeEntry {
129 relative_path,
130 kind: EntryKind::Directory,
131 });
132 return collect_entries(root, path, entries);
133 }
134 if metadata.is_file() {
135 entries.push(TreeEntry {
136 relative_path,
137 kind: EntryKind::File {
138 len: metadata.len(),
139 },
140 });
141 return Ok(());
142 }
143 Err(query_error(format!(
144 "migration source contains special file {}; only directories and regular files are supported",
145 path.display()
146 )))
147}
148
149fn hash_relative_path(hash: &mut Sha256, path: &Path) {
150 let components: Vec<_> = path
151 .components()
152 .filter_map(|component| match component {
153 Component::Normal(part) => Some(part),
154 _ => None,
155 })
156 .collect();
157 hash.update(
158 u64::try_from(components.len())
159 .unwrap_or(u64::MAX)
160 .to_le_bytes(),
161 );
162 for component in components {
163 update_os_str(hash, component);
164 }
165}
166
167fn hash_file(
168 root: &Path,
169 relative_path: &Path,
170 expected_len: u64,
171 hash: &mut Sha256,
172) -> Result<(), crate::MemoryError> {
173 let path = root.join(relative_path);
174 let file = open_expected_file(&path, expected_len)?;
175 let bytes_read = hash_reader(&path, file, hash)?;
176 if bytes_read == expected_len {
177 return Ok(());
178 }
179 Err(query_error(format!(
180 "source changed while fingerprinting {}: expected {expected_len} bytes, read {bytes_read}",
181 path.display()
182 )))
183}
184
185fn open_expected_file(path: &Path, expected_len: u64) -> Result<File, crate::MemoryError> {
186 let file = File::open(path)
187 .map_err(|err| query_error(format!("cannot open {}: {err}", path.display())))?;
188 let before = file
189 .metadata()
190 .map_err(|err| query_error(format!("cannot inspect {}: {err}", path.display())))?;
191 if before.is_file() && before.len() == expected_len {
192 return Ok(file);
193 }
194 Err(query_error(format!(
195 "source changed while fingerprinting {}",
196 path.display()
197 )))
198}
199
200fn hash_reader(path: &Path, file: File, hash: &mut Sha256) -> Result<u64, crate::MemoryError> {
201 let mut reader = BufReader::new(file);
202 let mut buffer = vec![0u8; 64 * 1024].into_boxed_slice();
203 let mut bytes_read = 0u64;
204 loop {
205 let read = reader
206 .read(&mut buffer)
207 .map_err(|err| query_error(format!("cannot read {}: {err}", path.display())))?;
208 if read == 0 {
209 break;
210 }
211 hash.update(&buffer[..read]);
212 bytes_read = bytes_read
213 .checked_add(u64::try_from(read).unwrap_or(u64::MAX))
214 .ok_or_else(|| query_error(format!("file {} exceeds u64", path.display())))?;
215 }
216 Ok(bytes_read)
217}
218
219#[cfg(unix)]
220fn update_os_str(hash: &mut Sha256, value: &OsStr) {
221 use std::os::unix::ffi::OsStrExt;
222 let bytes = value.as_bytes();
223 hash.update(u64::try_from(bytes.len()).unwrap_or(u64::MAX).to_le_bytes());
224 hash.update(bytes);
225}
226
227#[cfg(windows)]
228fn update_os_str(hash: &mut Sha256, value: &OsStr) {
229 use std::os::windows::ffi::OsStrExt;
230
231 hash.update(
232 u64::try_from(value.encode_wide().count())
233 .unwrap_or(u64::MAX)
234 .to_le_bytes(),
235 );
236 for unit in value.encode_wide() {
237 hash.update(unit.to_le_bytes());
238 }
239}
240
241#[cfg(not(any(unix, windows)))]
242fn update_os_str(hash: &mut Sha256, value: &OsStr) {
243 let bytes = value.to_string_lossy();
244 hash.update(u64::try_from(bytes.len()).unwrap_or(u64::MAX).to_le_bytes());
245 hash.update(bytes.as_bytes());
246}
247
248pub(super) fn encode_hex(bytes: &[u8]) -> String {
249 const HEX: &[u8; 16] = b"0123456789abcdef";
250 let mut encoded = String::with_capacity(bytes.len() * 2);
251 for byte in bytes {
252 encoded.push(char::from(HEX[usize::from(byte >> 4)]));
253 encoded.push(char::from(HEX[usize::from(byte & 0x0f)]));
254 }
255 encoded
256}
257
258#[cfg(all(test, windows))]
259#[path = "filesystem_tests.rs"]
260mod windows_tests;