tree.v

7.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// Counted hears how far a long step has come; `total` is 0 when it is not known.79pub type Counted = fn (done int, total int)8081// Hashing counts the files a walk has to read and hash (a cached one costs82// nothing), and tells `on` each time, when there is an `on`.83pub struct Hashing {84pub:85	on Counted = unsafe { nil }86mut:87	count int88}8990// walk hashes every tracked file under `root` and returns them sorted by path.91// Symlinks are skipped, as Koh 2 skipped them, so migration stays lossless.92pub fn walk(store &storage.Store, root string, ig Ignore) ![]FileEntry {93	mut quiet := Hashing{}94	return walk_report(store, root, ig, mut quiet)!.files95}9697pub fn walk_report(store &storage.Store, root string, ig Ignore, mut hashing Hashing) !WalkReport {98	cache := store.load_cache()!99	mut entries := []FileEntry{}100	mut fresh := []storage.CacheRow{}101	mut unsafe_paths := []string{}102	walk_dir(root, '', ig.banning(store.stripped_paths()!), cache, mut entries, mut fresh,103		mut unsafe_paths, mut hashing)!104	// Skipped while another command writes, or inside a caller's transaction:105	// the next walk hashes those files again, which is all a missing row costs.106	store.save_cache_rows(fresh) or {}107	entries.sort(a.path < b.path)108	unsafe_paths.sort()109	return WalkReport{110		files:        entries111		unsafe_paths: unsafe_paths112	}113}114115// walk_raw hashes the same files without a store: no cache, nothing written.116// `migrate` uses it to check a working tree against a manifest before it has a117// database to check it with.118pub fn walk_raw(root string, ig Ignore) ![]FileEntry {119	mut entries := []FileEntry{}120	mut unused := []storage.CacheRow{}121	mut unsafe_paths := []string{}122	mut quiet := Hashing{}123	walk_dir(root, '', ig, map[string]storage.CacheRow{}, mut entries, mut unused, mut unsafe_paths, mut124		quiet)!125	entries.sort(a.path < b.path)126	return entries127}128129// walk_dir lists one folder and recurses, adding hashed files to `out`. `cache` holds each path's last130// size and stamps, so an unchanged file is not read again; `fresh` gathers rows for files read now, for131// walk_report to store. A name breaking spec §5.5 is skipped and named in `unsafe_paths`, not an error:132// the walk goes on and status and save report it.133fn walk_dir(root string, rel string, ig Ignore, cache map[string]storage.CacheRow, mut out []FileEntry, mut fresh []storage.CacheRow, mut unsafe_paths []string, mut hashing Hashing) ! {134	dir := if rel == '' { root } else { os.join_path(root, rel) }135	for name in os.ls(dir)! {136		child := if rel == '' { name } else { '${rel}/${name}' }137		if !legal_path(child) {138			unsafe_paths << escape_path(child)139			continue140		}141		st := lstat(os.join_path(root, child)) or { continue }142		// The kind is known by now, so a pattern ending in `/` can be applied143		// to directories only.144		if ig.matches(child, st.kind == .dir) {145			continue146		}147		match st.kind {148			.dir {149				walk_dir(root, child, ig, cache, mut out, mut fresh, mut unsafe_paths, mut hashing)!150			}151			.file {152				if st.size > storage.max_file_size {153					return error('${child} is ${st.size} bytes; koh stores code and text, not files this large')154				}155				hash := hash_file(root, child, st, cache, mut fresh, mut hashing)!156				out << FileEntry{157					path:       child158					hash:       hash159					size:       st.size160					executable: is_executable(st.mode)161				}162			}163			else {}164		}165	}166}167168fn hash_file(root string, rel string, st Stat, cache map[string]storage.CacheRow, mut fresh []storage.CacheRow, mut hashing Hashing) !string {169	if row := cache[rel] {170		if row.size == st.size && row.mtime_ns == st.mtime_ns && row.ctime_ns == st.ctime_ns {171			return row.hash172		}173	}174	hashing.count++175	if hashing.on != unsafe { nil } {176		hashing.on(hashing.count, 0)177	}178	hash := storage.hash_hex(os.read_bytes(os.join_path(root, rel))!)179	if cacheable(st, time.now().unix_nano()) {180		fresh << storage.CacheRow{181			path:     rel182			size:     st.size183			mtime_ns: st.mtime_ns184			ctime_ns: st.ctime_ns185			hash:     hash186		}187	}188	return hash189}190191// cacheable reports whether a hash may be cached. Linux stamps files from a192// clock that ticks every few milliseconds, so a same-size rewrite in the tick193// of the hash would look unchanged; a file changed that recently is hashed194// again next time instead. `cp -p` and the like keep an old mtime while195// rewriting, so the newer of mtime and ctime decides.196fn cacheable(st Stat, now_ns i64) bool {197	changed := if st.ctime_ns > st.mtime_ns { st.ctime_ns } else { st.mtime_ns }198	return now_ns - changed >= racy_window_ns199}200201// store_objects writes the bytes of every entry the database does not have202// yet, so a save only pays for content it has never seen.203pub fn store_objects(store &storage.Store, root string, entries []FileEntry, progress Counted) ! {204	for i, entry in entries {205		if progress != unsafe { nil } {206			progress(i + 1, entries.len)207		}208		if store.has_object(entry.hash)! {209			continue210		}211		data := os.read_bytes(os.join_path(root, entry.path))!212		stored := store.put_object(data)!213		if stored != entry.hash {214			return error('${entry.path} changed while saving')215		}216	}217}218219// same_tree compares two sorted entry lists by path, hash and executable bit.220pub fn same_tree(a []FileEntry, b []FileEntry) bool {221	if a.len != b.len {222		return false223	}224	for i in 0 .. a.len {225		if a[i].path != b[i].path || a[i].hash != b[i].hash || a[i].executable != b[i].executable {226			return false227		}228	}229	return true230}