84f2455205b5

match strip paths exactly, not with case-blind LIKE wildcards (3.0.26, codex review pending)

time
2026-09-23 22:41 UTC · 2 weeks ago
machine
koh2
agent
claude-opus-5-5
session
koh 3 reliability campaign
parent
4fdcae077240
4 files VERSION+1 −1src/sync/strip.v+4 −6src/sync/sync_test.v+26 −0v.mod+1 −1
VERSION+1 −1
@@ -1 +1 @@1-3.0.251+3.0.26

view file

src/sync/strip.v+4 −6
@@ -222,18 +222,16 @@222222 	return store.db.changes()223223 }224224 225-// match_clause is `covers` in SQL. LIKE's own wildcards are escaped by226-// bind_match, so a path containing `%` or `_` matches itself and nothing else.225+// match_clause is `covers` in SQL. LIKE would ignore ASCII case and treat `_`226+// and `%` as wildcards; a strip destroys, so the prefix is compared exactly.227227 fn match_clause(only_save string) string {228228 	scope := if only_save == '' { '' } else { ' AND save_id = ?3' }229-	return '(path = ?1 OR path LIKE ?2 ESCAPE \'\\\')${scope}'229+	return '(path = ?1 OR substr(path, 1, length(?2)) = ?2)${scope}'230230 }231231 232232 fn bind_match(stmt &storage.Stmt, path string, only_save string) ! {233-	folder := path.trim_right('/')234-	escaped := folder.replace('\\', '\\\\').replace('%', '\\%').replace('_', '\\_')235233 	stmt.bind_text(1, path)!236-	stmt.bind_text(2, '${escaped}/%')!234+	stmt.bind_text(2, '${path.trim_right('/')}/')!237235 	if only_save != '' {238236 		stmt.bind_text(3, only_save)!239237 	}

view file

src/sync/sync_test.v+26 −0
@@ -616,6 +616,32 @@616616 	assert events.strip_events(a)!.len == 1617617 }618618 619+// SQLite's LIKE ignores ASCII case, so a strip of `docs` must not reach into620+// `Docs/`, and `a_b` must not reach `aXb`: strip destroys, so it matches exactly.621+// The tree is built by hand: on macOS `Docs` and `docs` are one folder on disk,622+// but a save made on Linux can hold both.623+fn test_a_strip_matches_the_path_exactly() {624+	mut a := open_repo(machine('strip_exact')!)!625+	defer {626+		a.close()627+	}628+	hash := a.put_object('x\n'.bytes())!629+	mut files := []snapshot.FileEntry{}630+	for path in ['DOCS.txt', 'Docs/keep.txt', 'a_b/x.txt', 'aXb/y.txt', 'docs/secret.txt'] {631+		files << snapshot.FileEntry{632+			path: path633+			hash: hash634+		}635+	}636+	save := history.commit_files(a, '', files, history.SaveInput{637+		message: 'add all'638+	})!639+	assert strip_scope(a, 'docs', '')!.files == 1640+	strip_local(a, 'docs', '')!641+	strip_local(a, 'a_b', '')!642+	assert history.files_of(a, save.id)!.map(it.path) == ['DOCS.txt', 'Docs/keep.txt', 'aXb/y.txt']643+}644+619645 fn test_a_strip_reaches_the_remote_and_other_machines() {620646 	base := scratch('strip_sync')!621647 	defer {

view file

v.mod+1 −1
@@ -1,6 +1,6 @@11 Module {22 	name: 'koh3'3-	version: '3.0.25'3+	version: '3.0.26'44 	description: 'One SQLite file per repo, one line of history.'55 	license: 'MIT'66 }

view file