123module snapshot45import encoding.utf86import os7import time8import src.storage91011const racy_window_ns = i64(3_000_000_000)121314pub struct FileEntry {15pub:16 path string17 hash string18 size i6419 executable bool20}2122pub struct WalkReport {23pub:24 files []FileEntry25 unsafe_paths []string26}272829pub 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}454647pub 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 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}777879pub type Counted = fn (done int, total int)80818283pub struct Hashing {84pub:85 on Counted = unsafe { nil }86mut:87 count int88}89909192pub 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 105 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}114115116117118pub 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}128129130131132133fn 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 143 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}190191192193194195196fn 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}200201202203pub 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}218219220pub 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}