1use std::collections::BTreeMap;
9use std::io;
10use std::path::{Path, PathBuf};
11
12use kernel::ledger::chain_hash;
13use kernel::{EventRecord, GENESIS_PREV, Seq};
14
15use crate::alias::WriteTarget;
16use crate::error::{StorageError, io_err};
17use crate::jsonl::JsonlLedger;
18use crate::vfs::Vfs;
19
20use super::landing::{Bits, is_staging_name, land};
21
22use super::manifest::RESERVED;
23
24pub(crate) fn head_of(vfs: &dyn Vfs, ledger_dir: &Path) -> Result<String, StorageError> {
25 let mut prev = GENESIS_PREV;
26 let mut expected = Seq::FIRST;
27 for line in read_lines(vfs, ledger_dir)? {
28 let record = EventRecord::parse_line(&line).map_err(|err| StorageError::Bundle {
29 op: "verify",
30 detail: err.to_string(),
31 })?;
32 if record.seq() != expected || record.prev() != prev {
33 return Err(StorageError::Bundle {
34 op: "verify",
35 detail: format!(
36 "record {} does not continue the chain",
37 record.seq().value()
38 ),
39 });
40 }
41 prev = chain_hash(&line);
42 expected = expected.next().map_err(|err| StorageError::Bundle {
43 op: "verify",
44 detail: err.to_string(),
45 })?;
46 }
47 Ok(prev.to_string())
48}
49
50pub(crate) fn count_records(vfs: &dyn Vfs, ledger_dir: &Path) -> Result<u64, StorageError> {
51 let lines = read_lines(vfs, ledger_dir)?;
52 u64::try_from(lines.len()).map_err(|_| StorageError::Bundle {
53 op: "count",
54 detail: "more records than a count can hold".to_owned(),
55 })
56}
57
58pub(crate) fn read_lines(vfs: &dyn Vfs, ledger_dir: &Path) -> Result<Vec<Vec<u8>>, StorageError> {
59 let mut out = Vec::new();
60 for path in walk(vfs, ledger_dir)? {
61 if path.extension().and_then(|ext| ext.to_str()) != Some("jsonl") {
62 continue;
63 }
64 let bytes = vfs
65 .read(&path)
66 .map_err(io_err("read a bundle file", &path))?;
67 for line in bytes.split(|byte| *byte == b'\n') {
68 if !line.is_empty() {
69 out.push(line.to_vec());
70 }
71 }
72 }
73 Ok(out)
74}
75
76pub(crate) fn walk(vfs: &dyn Vfs, root: &Path) -> Result<Vec<PathBuf>, StorageError> {
88 let mut found = Vec::new();
89 let mut pending = vec![root.to_path_buf()];
90 while let Some(dir) = pending.pop() {
91 let Some(files) = present(vfs.list(&dir), &dir)? else {
92 continue;
93 };
94 for file in &files {
95 refuse_alias(file)?;
96 }
97 found.extend(files);
98 if let Some(dirs) = present(vfs.list_dirs(&dir), &dir)? {
99 for sub in &dirs {
100 refuse_alias(sub)?;
101 }
102 pending.extend(dirs);
103 }
104 }
105 found.sort();
106 Ok(found)
107}
108
109fn refuse_alias(path: &Path) -> Result<(), StorageError> {
111 match crate::alias::kind_at(path)? {
112 Some(kind) => Err(crate::alias::refused("walk a store or bundle", path, kind)),
113 None => Ok(()),
114 }
115}
116
117fn travels(relative: &Path) -> bool {
128 !relative.starts_with(RESERVED)
129 && !relative.file_name().is_some_and(is_staging_name)
130 && !relative.components().any(|component| {
131 component
132 .as_os_str()
133 .to_str()
134 .is_some_and(|segment| segment.eq_ignore_ascii_case(kernel::GIT_METADATA))
135 })
136}
137
138pub(crate) fn only_city_files(vfs: &dyn Vfs, root: &Path) -> Result<u64, StorageError> {
151 let mut repository = 0u64;
152 for path in walk(vfs, root)? {
153 let Ok(relative) = path.strip_prefix(root) else {
154 continue;
155 };
156 if relative
157 .strip_prefix(kernel::GIT_METADATA)
158 .is_ok_and(|inner| !inner.as_os_str().is_empty())
159 {
160 repository = repository.saturating_add(1);
161 continue;
162 }
163 if !travels(relative) {
164 return Err(StorageError::Bundle {
165 op: "restore",
166 detail: format!(
167 "{} is protected metadata or a staging file, which no bundle may hold",
168 path.display()
169 ),
170 });
171 }
172 }
173 Ok(repository)
174}
175
176fn present(
182 listing: io::Result<Vec<PathBuf>>,
183 dir: &Path,
184) -> Result<Option<Vec<PathBuf>>, StorageError> {
185 match listing {
186 Ok(paths) => Ok(Some(paths)),
187 Err(err) if err.kind() == io::ErrorKind::NotFound => Ok(None),
188 Err(err) => Err(io_err("walk a bundle directory", dir)(err)),
189 }
190}
191
192pub(crate) fn copy_tree(
196 vfs: &mut dyn Vfs,
197 root: &Path,
198 from: &Path,
199 to: &Path,
200) -> Result<u64, StorageError> {
201 let mut copied = 0u64;
202 let files = walk(vfs, from)?;
203 if files.is_empty() {
204 return Ok(0);
205 }
206 vfs.create_dir_all(to)
207 .map_err(io_err("make a bundle directory", to))?;
208 for path in files {
209 let Ok(relative) = path.strip_prefix(from) else {
210 continue;
211 };
212 let target = to.join(relative);
213 let cleared = WriteTarget::within("copy a bundle file", root, &target)?;
217 if let Some(parent) = target.parent() {
218 vfs.create_dir_all(parent)
219 .map_err(io_err("make a bundle directory", parent))?;
220 }
221 let bytes = vfs
222 .read(&path)
223 .map_err(io_err("read a bundle file", &path))?;
224 land(vfs, cleared, &bytes, Bits::Of(&path))?;
225 copied = copied.saturating_add(1);
226 }
227 Ok(copied)
228}
229
230pub(crate) fn copy_city_files(
233 vfs: &mut dyn Vfs,
234 root: &Path,
235 city_root: &Path,
236 to: &Path,
237) -> Result<u64, StorageError> {
238 let mut copied = 0u64;
239 let mut seen = BTreeMap::new();
240 for path in walk(vfs, city_root)? {
241 let Ok(relative) = path.strip_prefix(city_root) else {
242 continue;
243 };
244 if !travels(relative) {
245 continue;
246 }
247 seen.insert(relative.to_path_buf(), path);
248 }
249 if seen.is_empty() {
250 return Ok(0);
251 }
252 vfs.create_dir_all(to)
253 .map_err(io_err("make a bundle directory", to))?;
254 for (relative, path) in seen {
255 let target = to.join(&relative);
256 let cleared = WriteTarget::within("copy a city file", root, &target)?;
259 if let Some(parent) = target.parent() {
260 vfs.create_dir_all(parent)
261 .map_err(io_err("make a bundle directory", parent))?;
262 }
263 let bytes = vfs
264 .read(&path)
265 .map_err(io_err("read a bundle file", &path))?;
266 land(vfs, cleared, &bytes, Bits::Of(&path))?;
267 copied = copied.saturating_add(1);
268 }
269 Ok(copied)
270}
271
272pub(crate) fn count_files(vfs: &dyn Vfs, root: &Path) -> Result<u64, StorageError> {
273 let mut count = 0u64;
274 for path in walk(vfs, root)? {
275 let Ok(relative) = path.strip_prefix(root) else {
276 continue;
277 };
278 if !travels(relative) {
279 continue;
280 }
281 count = count.saturating_add(1);
282 }
283 Ok(count)
284}
285
286pub fn open_restored(city_root: &Path, now: kernel::TimeMs) -> Result<PathBuf, StorageError> {
292 let dir = kernel::layout::CityLayout::new(city_root).ledger();
293 let (_ledger, _report) = JsonlLedger::open(&dir, now)?;
294 Ok(dir)
295}
296
297#[cfg(test)]
298#[allow(
299 clippy::unwrap_used,
300 clippy::expect_used,
301 clippy::panic,
302 clippy::indexing_slicing,
303 reason = "test code"
304)]
305mod tests {
306 use super::super::export::Bundle;
307 use super::super::fixture::city_with;
308 use super::super::manifest::CITY;
309 use super::*;
310
311 #[test]
315 fn a_read_only_city_file_comes_back_read_only() {
316 let home = tempfile::tempdir().unwrap();
317 city_with(1, home.path());
318 let kept = home.path().join("kept.md");
319 std::fs::write(&kept, b"do not touch").unwrap();
320 let mut bits = std::fs::metadata(&kept).unwrap().permissions();
321 bits.set_readonly(true);
322 std::fs::set_permissions(&kept, bits).unwrap();
323
324 let carried = tempfile::tempdir().unwrap();
325 Bundle::export(home.path(), carried.path()).unwrap();
326 let elsewhere = tempfile::tempdir().unwrap();
327 Bundle::restore(carried.path(), elsewhere.path()).unwrap();
328 let read_only = |at: &Path| std::fs::metadata(at).unwrap().permissions().readonly();
329 assert_eq!(
330 (
331 read_only(&carried.path().join(CITY).join("kept.md")),
332 read_only(&elsewhere.path().join("kept.md")),
333 ),
334 (true, true)
335 );
336 }
337
338 #[test]
341 fn an_export_repeats_over_a_bundle_holding_a_read_only_file() {
342 let home = tempfile::tempdir().unwrap();
343 city_with(1, home.path());
344 let kept = home.path().join("kept.md");
345 std::fs::write(&kept, b"do not touch").unwrap();
346 let mut bits = std::fs::metadata(&kept).unwrap().permissions();
347 bits.set_readonly(true);
348 std::fs::set_permissions(&kept, bits).unwrap();
349
350 let carried = tempfile::tempdir().unwrap();
351 Bundle::export(home.path(), carried.path()).unwrap();
352 let again = Bundle::export(home.path(), carried.path())
353 .map(|_| ())
354 .map_err(|err| err.to_string());
355 assert_eq!(again, Ok(()));
356 }
357
358 #[test]
361 fn a_city_kept_under_a_link_exports_and_restores() {
362 let tmp = tempfile::tempdir().unwrap();
363 let real = tmp.path().join("real");
364 std::fs::create_dir_all(&real).unwrap();
365 let via = tmp.path().join("via");
366 if !crate::alias::tests::place_link(false, &real, &via) {
367 return;
368 }
369 let (city, carried, elsewhere) = (via.join("city"), via.join("bk"), via.join("back"));
370 std::fs::create_dir_all(&city).unwrap();
371 std::fs::create_dir_all(&elsewhere).unwrap();
372 city_with(1, &city);
373 let outcome = Bundle::export(&city, &carried)
374 .and_then(|_| Bundle::restore(&carried, &elsewhere))
375 .map(|_| ())
376 .map_err(|err| err.to_string());
377 assert_eq!(outcome, Ok(()));
378 }
379
380 #[test]
381 fn nothing_under_the_reserved_prefix_travels_as_a_city_file() {
382 let home = tempfile::tempdir().unwrap();
383 city_with(1, home.path());
384 let cache = home.path().join(RESERVED).join("index");
387 std::fs::create_dir_all(&cache).unwrap();
388 std::fs::write(cache.join("ledger.idx"), b"derived").unwrap();
389
390 let carried = tempfile::tempdir().unwrap();
391 Bundle::export(home.path(), carried.path()).unwrap();
392 assert!(!carried.path().join(CITY).join(RESERVED).exists());
393 assert!(!carried.path().join(CITY).join("index").exists());
394 }
395
396 #[test]
399 fn a_staging_file_a_crash_left_does_not_travel() {
400 let home = tempfile::tempdir().unwrap();
401 city_with(1, home.path());
402 std::fs::write(home.path().join("kept.md"), b"whole").unwrap();
403 std::fs::write(home.path().join(".kept.md.part"), b"half").unwrap();
404
405 let carried = tempfile::tempdir().unwrap();
406 Bundle::export(home.path(), carried.path()).unwrap();
407 let city = carried.path().join(CITY);
408 assert_eq!(
409 (
410 city.join("kept.md").exists(),
411 city.join(".kept.md.part").exists()
412 ),
413 (true, false)
414 );
415 }
416}