fix: lock sync's first inspection, resolve symlinks without their target

Fourth Codex (gpt-6.1-sol, xhigh) pass over 5e0831d, each item verified
before fixing:

- sync inspects the local database under the shared lock, so another
  sync cannot swap it under that connection.
- symlinks are resolved by reading the link (db::resolve_db_path), not
  canonicalize, so a symlinked path finds the same lock and install
  target even while the real file is missing (fresh install, mid-swap)
  or the link dangles.
- readers go ahead unlocked when no lock file can be created; nothing
  can replace the database there. (A read-only data directory still
  fails in SQLite's WAL setup, as in 0.2.4.)
- XLSX shared strings are capped at 2,000,000 entries.

Verified live: an install waits for a reader's shared lock; a symlinked
db_path waits on the real path's lock and stays a symlink. CLI goldens
unchanged; live A/B vs 0.2.4: 16 passed, discover hits the IDX 403 on
both builds (exit 4 vs 1, intended).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
hermes 2026-10-14 22:49:52 +00:00
commit c65982f407
4 changed files with 111 additions and 25 deletions

View file

@ -127,7 +127,10 @@ fn parse_shared_strings(xml: &str) -> Result<Vec<String>, IdxError> {
b"rPh" => in_phonetic = true,
_ => {}
},
Ok(Event::Empty(e)) if e.local_name().as_ref() == b"si" => strings.push(String::new()),
Ok(Event::Empty(e)) if e.local_name().as_ref() == b"si" => {
check_shared_string_count(strings.len())?;
strings.push(String::new())
}
Ok(Event::Text(text)) if in_text && !in_phonetic => {
if let Some(value) = current.as_mut() {
let unescaped = text
@ -137,7 +140,10 @@ fn parse_shared_strings(xml: &str) -> Result<Vec<String>, IdxError> {
}
}
Ok(Event::End(e)) => match e.local_name().as_ref() {
b"si" => strings.push(current.take().unwrap_or_default()),
b"si" => {
check_shared_string_count(strings.len())?;
strings.push(current.take().unwrap_or_default())
}
b"t" => in_text = false,
b"rPh" => in_phonetic = false,
_ => {}
@ -184,6 +190,18 @@ fn text_cap_error() -> IdxError {
)
}
const MAX_SHARED_STRINGS: usize = 2_000_000;
fn check_shared_string_count(count: usize) -> Result<(), IdxError> {
if count >= MAX_SHARED_STRINGS {
return Err(xml_error(
"shared strings",
format!("more than {MAX_SHARED_STRINGS} entries"),
));
}
Ok(())
}
fn check_row_count(rows: usize) -> Result<(), IdxError> {
if rows >= MAX_SHEET_ROWS {
return Err(xml_error(