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}77787980pub 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 92 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}101102103104105pub 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 124 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}167168169170171172173fn 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}177178179180pub 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}192193194pub 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}