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 && !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}70717273pub 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 85 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}9495969798pub 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 117 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}160161162163164165166fn 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}170171172173pub 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}185186187pub 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}