d5d9ef7e460f
report unsafe paths in doctor, drop them in migrate by the shared rule, and keep flagged names in offer --builds --json (3.2.2)
- time
- 2026-09-28 15:21 UTC · 2 weeks ago
- machine
- Junes-MacBook-Pro.local
- agent
- claude-opus-5-5
- session
- harden shared path handling
- parent
- a8e3208a9603
↓ koh-d5d9ef7e.tar.gzatlas at this save
files changed3 files
src/cli/cmd_migrate.v+3 −0src/migrate/migrate.v+29 −8src/migrate/migrate_test.v+37 −0src/cli/cmd_migrate.v+3 −0
@@ -34,11 +34,13 @@3434 head: report.head3535 ignore_moved: report.ignore_carried3636 build_file: report.build_renamed37+ unsafe_paths: report.unsafe_paths3738 }))3839 return 03940 }4041 ok('migrated ${fmt.plural(report.saves, 'save')} from Koh 2 (lane ${report.lane})')4142 note('${report.checkpoints} checkpoints · ${report.flags} flags · ${report.objects} objects')43+ warn_unsafe_paths(report.unsafe_paths)4244 note('the Koh 2 data is untouched in ${migrate.archive_dir}/ — move it back to go back')4345 if report.ignore_carried {4446 note('carried the ignore rules to ${snapshot.ignore_file}')@@ -66,4 +68,5 @@6668 head string6769 ignore_moved bool6870 build_file bool71+ unsafe_paths []string6972 }src/migrate/migrate.v+29 −8
@@ -50,7 +50,8 @@5050 is_checkpoint bool5151 flagged bool5252 flag_reason ?string53- files []ManifestFile53+mut:54+ files []ManifestFile5455 }5556 5657 pub struct Report {@@ -66,6 +67,7 @@6667 ignore_carried bool6768 log_md_was_tracked bool6869 build_renamed bool70+ unsafe_paths []string6971 }7072 7173 // run converts the repo at `root`. `lane` picks which line to import: `dev`@@ -89,14 +91,16 @@8991 chosen = other_lane(manifests) or { return err }9092 lane_head(root, manifests, chosen)!9193 }92- line := ancestry(manifests, head)!93- check_clean_tree(root, head)!94+ mut line := ancestry(manifests, head)!95+ unsafe_paths := drop_unsafe_paths(mut line)96+ check_clean_tree(root, line[line.len - 1])!9497 check_build_files(root)!9598 9699 mut report := Report{97- root: root98- lane: chosen99- head: head.id100+ root: root101+ lane: chosen102+ head: head.id103+ unsafe_paths: unsafe_paths100104 }101105 // Import into a draft file first: nothing is touched until it verifies.102106 draft_path := os.join_path(root, draft_file)@@ -280,8 +284,8 @@280284 // disk_hash hashes a regular file inside the repo. Koh 2 never followed links,281285 // nor does Koh 3, so a link on the way to it means the saved file is missing.282286 fn disk_hash(root string, path string) ?string {283- // Only a plain path inside the repo; a damaged manifest cannot point out.284- if path.starts_with('/') || path.split('/').any(it in ['', '.', '..']) {287+ // Only a legal path inside the repo; a damaged manifest cannot point out.288+ if !snapshot.legal_path(path) {285289 return none286290 }287291 full := os.join_path(root, path)@@ -301,6 +305,23 @@301305 parts := path.split('/')302306 return parts[0] == '.kohconfig' || parts[0].starts_with('.kohignore.')303307 || parts.any(it.ends_with('.bundle'))308+}309+310+// drop_unsafe_paths removes, by the shared path rule, every file Koh 3 would311+// refuse to restore, and names each one once for the report.312+fn drop_unsafe_paths(mut line []Manifest) []string {313+ mut dropped := map[string]bool{}314+ for mut manifest in line {315+ for file in manifest.files {316+ if !snapshot.legal_path(file.path) {317+ dropped[snapshot.escape_path(file.path)] = true318+ }319+ }320+ manifest.files = manifest.files.filter(snapshot.legal_path(it.path))321+ }322+ mut names := dropped.keys()323+ names.sort()324+ return names304325 }305326 306327 fn import_and_verify(store &storage.Store, root string, line []Manifest, mut report Report) ! {src/migrate/migrate_test.v+37 −0
@@ -277,3 +277,40 @@277277 assert disk_hash(root, path) == none, path278278 }279279 }280+281+fn test_disk_hash_uses_the_shared_path_rule() {282+ root := os.join_path(os.temp_dir(), 'koh3_migrate_rule_${os.getpid()}')283+ os.mkdir_all(root)!284+ defer {285+ os.rmdir_all(root) or {}286+ }287+ for name in ['back\\slash', 'Icon\r'] {288+ // Joined by hand: os.join_path turns a backslash into a separator.289+ os.write_file('${root}/${name}', 'odd\n')!290+ assert disk_hash(root, name) == none, name291+ }292+}293+294+fn test_unsafe_manifest_paths_are_dropped_and_named_once() {295+ mut line := [296+ Manifest{297+ id: 'a'298+ files: [ManifestFile{299+ path: 'ok.txt'300+ }, ManifestFile{301+ path: 'Icon\r'302+ }]303+ },304+ Manifest{305+ id: 'b'306+ files: [ManifestFile{307+ path: 'Icon\r'308+ }, ManifestFile{309+ path: '../x'310+ }]311+ },312+ ]313+ assert drop_unsafe_paths(mut line) == ['../x', 'Icon\\x0d']314+ assert line[0].files.map(it.path) == ['ok.txt']315+ assert line[1].files.len == 0316+}