tree.v

6.1 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 {62			// A whole valid sequence prints as itself; any other byte is escaped alone.63			n := utf8_char_len(byte)64			if n > 1 && i + n <= bytes.len && utf8.validate_str(bytes[i..i + n].bytestr()) {65				escaped += bytes[i..i + n].bytestr()66				i += n67				continue68			}69			escaped += '\\x${byte.hex()}'70		} else {71			escaped += byte.ascii_str()72		}73		i++74	}75	return escaped76}7778// walk hashes every tracked file under `root` and returns them sorted by path.79// Symlinks are skipped, as Koh 2 skipped them, so migration stays lossless.80pub fn walk(store &storage.Store, root string, ig Ignore) ![]FileEntry {81	return walk_report(store, root, ig)!.files82}8384pub fn walk_report(store &storage.Store, root string, ig Ignore) !WalkReport {85	cache := store.load_cache()!86	mut entries := []FileEntry{}87	mut fresh := []storage.CacheRow{}88	mut unsafe_paths := []string{}89	walk_dir(root, '', ig.banning(store.stripped_paths()!), cache, mut entries, mut fresh,90		mut unsafe_paths)!91	// Skipped while another command writes, or inside a caller's transaction:92	// the next walk hashes those files again, which is all a missing row costs.93	store.save_cache_rows(fresh) or {}94	entries.sort(a.path < b.path)95	unsafe_paths.sort()96	return WalkReport{97		files:        entries98		unsafe_paths: unsafe_paths99	}100}101102// walk_raw hashes the same files without a store: no cache, nothing written.103// `migrate` uses it to check a working tree against a manifest before it has a104// database to check it with.105pub fn walk_raw(root string, ig Ignore) ![]FileEntry {106	mut entries := []FileEntry{}107	mut unused := []storage.CacheRow{}108	mut unsafe_paths := []string{}109	walk_dir(root, '', ig, map[string]storage.CacheRow{}, mut entries, mut unused, mut unsafe_paths)!110	entries.sort(a.path < b.path)111	return entries112}113114fn walk_dir(root string, rel string, ig Ignore, cache map[string]storage.CacheRow, mut out []FileEntry, mut fresh []storage.CacheRow, mut unsafe_paths []string) ! {115	dir := if rel == '' { root } else { os.join_path(root, rel) }116	for name in os.ls(dir)! {117		child := if rel == '' { name } else { '${rel}/${name}' }118		if !legal_path(child) {119			unsafe_paths << escape_path(child)120			continue121		}122		st := lstat(os.join_path(root, child)) or { continue }123		// The kind is known by now, so a pattern ending in `/` can be applied124		// to directories only.125		if ig.matches(child, st.kind == .dir) {126			continue127		}128		match st.kind {129			.dir {130				walk_dir(root, child, ig, cache, mut out, mut fresh, mut unsafe_paths)!131			}132			.file {133				if st.size > storage.max_file_size {134					return error('${child} is ${st.size} bytes; koh stores code and text, not files this large')135				}136				hash := hash_file(root, child, st, cache, mut fresh)!137				out << FileEntry{138					path:       child139					hash:       hash140					size:       st.size141					executable: is_executable(st.mode)142				}143			}144			else {}145		}146	}147}148149fn hash_file(root string, rel string, st Stat, cache map[string]storage.CacheRow, mut fresh []storage.CacheRow) !string {150	if row := cache[rel] {151		if row.size == st.size && row.mtime_ns == st.mtime_ns && row.ctime_ns == st.ctime_ns {152			return row.hash153		}154	}155	hash := storage.hash_hex(os.read_bytes(os.join_path(root, rel))!)156	if cacheable(st, time.now().unix_nano()) {157		fresh << storage.CacheRow{158			path:     rel159			size:     st.size160			mtime_ns: st.mtime_ns161			ctime_ns: st.ctime_ns162			hash:     hash163		}164	}165	return hash166}167168// cacheable reports whether a hash may be cached. Linux stamps files from a169// clock that ticks every few milliseconds, so a same-size rewrite in the tick170// of the hash would look unchanged; a file changed that recently is hashed171// again next time instead. `cp -p` and the like keep an old mtime while172// rewriting, so the newer of mtime and ctime decides.173fn cacheable(st Stat, now_ns i64) bool {174	changed := if st.ctime_ns > st.mtime_ns { st.ctime_ns } else { st.mtime_ns }175	return now_ns - changed >= racy_window_ns176}177178// store_objects writes the bytes of every entry the database does not have179// yet, so a save only pays for content it has never seen.180pub fn store_objects(store &storage.Store, root string, entries []FileEntry) ! {181	for entry in entries {182		if store.has_object(entry.hash)! {183			continue184		}185		data := os.read_bytes(os.join_path(root, entry.path))!186		stored := store.put_object(data)!187		if stored != entry.hash {188			return error('${entry.path} changed while saving')189		}190	}191}192193// same_tree compares two sorted entry lists by path, hash and executable bit.194pub fn same_tree(a []FileEntry, b []FileEntry) bool {195	if a.len != b.len {196		return false197	}198	for i in 0 .. a.len {199		if a[i].path != b[i].path || a[i].hash != b[i].hash || a[i].executable != b[i].executable {200			return false201		}202	}203	return true204}