tree.v

5.9 KB · raw

1//! Walking the working tree: ignore rules, hashing, and the file cache.23module snapshot45import encoding.utf86import os7import time8import src.storage910// FAT filesystems keep mtime in 2-second steps; three covers them too.11const racy_window_ns = i64(3_000_000_000)1213// One tracked file in one tree.14pub struct FileEntry {15pub:16	path       string17	hash       string18	size       i6419	executable bool20}2122pub struct WalkReport {23pub:24	files        []FileEntry25	unsafe_paths []string26}2728// legal_path is the one rule for every path Koh saves, receives or restores.29pub fn legal_path(path string) bool {30	if path == '' || path.starts_with('/') || path.contains('\\') || !utf8.validate_str(path) {31		return false32	}33	if path.split('/').any(it in ['', '.', '..']) {34		return false35	}36	bytes := path.bytes()37	for i, byte in bytes {38		if byte < 0x20 || byte == 0x7f || (byte == 0xc2 && i + 1 < bytes.len39			&& bytes[i + 1] >= 0x80 && bytes[i + 1] <= 0x9f) {40			return false41		}42	}43	return true44}4546// escape_path makes unsafe names printable without letting controls affect the terminal.47pub fn escape_path(path string) string {48	mut escaped := ''49	bytes := path.bytes()50	mut i := 051	for i < bytes.len {52		byte := bytes[i]53		if byte == `\\` {54			escaped += '\\\\'55		} else if byte < 0x20 || byte == 0x7f {56			escaped += '\\x${byte.hex()}'57		} else if byte == 0xc2 && i + 1 < bytes.len && bytes[i + 1] >= 0x8058			&& bytes[i + 1] <= 0x9f {59			escaped += '\\u00${bytes[i + 1].hex()}'60			i++61		} else if byte >= 0x80 && !utf8.validate_str(bytes[i..].bytestr()) {62			escaped += '\\x${byte.hex()}'63		} else {64			escaped += byte.ascii_str()65		}66		i++67	}68	return escaped69}7071// walk hashes every tracked file under `root` and returns them sorted by path.72// Symlinks are skipped, as Koh 2 skipped them, so migration stays lossless.73pub fn walk(store &storage.Store, root string, ig Ignore) ![]FileEntry {74	return walk_report(store, root, ig)!.files75}7677pub fn walk_report(store &storage.Store, root string, ig Ignore) !WalkReport {78	cache := store.load_cache()!79	mut entries := []FileEntry{}80	mut fresh := []storage.CacheRow{}81	mut unsafe_paths := []string{}82	walk_dir(root, '', ig.banning(store.stripped_paths()!), cache, mut entries, mut fresh,83		mut unsafe_paths)!84	// Skipped while another command writes, or inside a caller's transaction:85	// the next walk hashes those files again, which is all a missing row costs.86	store.save_cache_rows(fresh) or {}87	entries.sort(a.path < b.path)88	unsafe_paths.sort()89	return WalkReport{90		files:        entries91		unsafe_paths: unsafe_paths92	}93}9495// walk_raw hashes the same files without a store: no cache, nothing written.96// `migrate` uses it to check a working tree against a manifest before it has a97// database to check it with.98pub fn walk_raw(root string, ig Ignore) ![]FileEntry {99	mut entries := []FileEntry{}100	mut unused := []storage.CacheRow{}101	mut unsafe_paths := []string{}102	walk_dir(root, '', ig, map[string]storage.CacheRow{}, mut entries, mut unused, mut unsafe_paths)!103	entries.sort(a.path < b.path)104	return entries105}106107fn walk_dir(root string, rel string, ig Ignore, cache map[string]storage.CacheRow, mut out []FileEntry, mut fresh []storage.CacheRow, mut unsafe_paths []string) ! {108	dir := if rel == '' { root } else { os.join_path(root, rel) }109	for name in os.ls(dir)! {110		child := if rel == '' { name } else { '${rel}/${name}' }111		if !legal_path(child) {112			unsafe_paths << escape_path(child)113			continue114		}115		st := lstat(os.join_path(root, child)) or { continue }116		// The kind is known by now, so a pattern ending in `/` can be applied117		// to directories only.118		if ig.matches(child, st.kind == .dir) {119			continue120		}121		match st.kind {122			.dir {123				walk_dir(root, child, ig, cache, mut out, mut fresh, mut unsafe_paths)!124			}125			.file {126				if st.size > storage.max_file_size {127					return error('${child} is ${st.size} bytes; koh stores code and text, not files this large')128				}129				hash := hash_file(root, child, st, cache, mut fresh)!130				out << FileEntry{131					path:       child132					hash:       hash133					size:       st.size134					executable: is_executable(st.mode)135				}136			}137			else {}138		}139	}140}141142fn hash_file(root string, rel string, st Stat, cache map[string]storage.CacheRow, mut fresh []storage.CacheRow) !string {143	if row := cache[rel] {144		if row.size == st.size && row.mtime_ns == st.mtime_ns && row.ctime_ns == st.ctime_ns {145			return row.hash146		}147	}148	hash := storage.hash_hex(os.read_bytes(os.join_path(root, rel))!)149	if cacheable(st, time.now().unix_nano()) {150		fresh << storage.CacheRow{151			path:     rel152			size:     st.size153			mtime_ns: st.mtime_ns154			ctime_ns: st.ctime_ns155			hash:     hash156		}157	}158	return hash159}160161// cacheable reports whether a hash may be cached. Linux stamps files from a162// clock that ticks every few milliseconds, so a same-size rewrite in the tick163// of the hash would look unchanged; a file changed that recently is hashed164// again next time instead. `cp -p` and the like keep an old mtime while165// rewriting, so the newer of mtime and ctime decides.166fn cacheable(st Stat, now_ns i64) bool {167	changed := if st.ctime_ns > st.mtime_ns { st.ctime_ns } else { st.mtime_ns }168	return now_ns - changed >= racy_window_ns169}170171// store_objects writes the bytes of every entry the database does not have172// yet, so a save only pays for content it has never seen.173pub fn store_objects(store &storage.Store, root string, entries []FileEntry) ! {174	for entry in entries {175		if store.has_object(entry.hash)! {176			continue177		}178		data := os.read_bytes(os.join_path(root, entry.path))!179		stored := store.put_object(data)!180		if stored != entry.hash {181			return error('${entry.path} changed while saving')182		}183	}184}185186// same_tree compares two sorted entry lists by path, hash and executable bit.187pub fn same_tree(a []FileEntry, b []FileEntry) bool {188	if a.len != b.len {189		return false190	}191	for i in 0 .. a.len {192		if a[i].path != b[i].path || a[i].hash != b[i].hash || a[i].executable != b[i].executable {193			return false194		}195	}196	return true197}