Preserve current main, adapt frame/input and IME handling, and keep library
check results stable across layout passes. Include the matching Nix font lock
and reviewed attribution changes.
Found while testing crates/catalog/src/local.rs:62 (folder_of), used by forget_local_plan at :192 and :204
Co-Authored-By: Claude Haiku 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
The revision field and Catalog::apply said an error changes nothing and that the revision counts applied ops; a batch whose rollback fails changes the catalog and advances it.
* security: harden dev-tool scripts and the testkit skip-log test (CodeRabbit)
- tools/install-sam3.sh: the HF bearer token went through curl's argv, where
`ps` exposes it to other local accounts (CWE-214); it now travels through
a private 0600 temp config file removed on exit.
- packaging/macos/import-cert.sh: the decoded .p12 was written to a
predictable path in a shared TMPDIR, where a precreated attacker-readable
file would receive it (CWE-377); both cert and keychain are created inside
a mktemp -d 0700 directory now.
- docs/showcase/run.py: the reply reader copied whole responses with no size
cap (obvious quadratic-copy and unbounded-memory DoS). It accumulates in a
bytearray now and closes the connection past 128 MiB.
- tools/spectral_camera_table.py: alias/make/model strings and comments from
the rawtoaces-data checkout moved directly into Rust source; one newline in
a comment became Rust code. External text is now escaped as Rust literal
content and comments are normalized to one printable line.
- crates/testkit: appended_lines_accumulate reused a pip-predictable temp
dir by creating it with create_dir_all and carried on after failing to
remove its log path, so the append would follow an attacker-planted
symlink (CWE-59); it now fails the test if the directory exists instead of
testing through it, and removes any existing log path only after clearly
checking it, always failing the test instead of proceeding when removal fails.
Findings #10080/#10078 PR (rows 2, 6, 8, 9, 15 of the
storytold-lightcraft-agent-findings report).
🤖 Generated with Codebuff
Co-Authored-By: Codebuff <noreply@codebuff.com>
* security: bounded catalog keyword, repair, journal recovery work (CodeRabbit)
- keywords: from_file searched every hierarchical level for each flat keyword
and `same` lower-cased two strings per comparison — one lowered level set
instead (CWE-407, row 97).
- keywords: parse_keyword_list scanned each keyword's growing synonym list per
line — a lowered-synonym set per keyword instead (CWE-407, row 100).
- keywords: keyword_tree stored every level's prefix twice, so a photo XMP
keyword nested deeper than a list file accepts made stored bytes quadratic
in the level count; the tree caps at MAX_LEVELS, and keyword_list_text names
whatever lies deeper from the photos' and list's own spellings, so the file
still reads back (CWE-400, row 103).
- catalog: repair validated a folder chain once per album — memoized by chain
now, so a valid chain in a flat snapshot costs one walk overall (CWE-407,
row 101).
- store: journal append and truncate follow a planted catalog.log symlink;
both refuse any non-regular file at the name before writing through it
(CWE-59, row 107), like the staging writes since #1502.
- journal: resync re-validated the whole remaining suffix UTF-8 for every
candidate record start; a whole line is validated once (covering every
candidate in it), and an invalid line's re-checks stop at a 64 MiB budget,
with the tail then staying a torn one the damaged path keeps and reports
(CWE-407, row 109).
New tests: from_file case-insensitive level match, synonyms kept once, a
4096-level keyword cut at MAX_LEVELS, a link at catalog.log refused, and
resync over a fragment of 1024 record starts.
Findings rows 97, 100, 101, 103, 107, 109 of the
storytold-lightcraft-agent-findings report.
🤖 Generated with Codebuff
Co-Authored-By: Codebuff <noreply@codebuff.com>
---------
Co-authored-by: Codebuff <noreply@codebuff.com>
Catalog::apply noted the photos an op touches before checking they exist, and a batch that rolled back kept what its first ops and the rollback had noted. touched_since and aspects_since then named photos nothing changed, ids that do not exist among them, and a failed album op made earlier revisions unknown. apply now drops the notes made for the revision a failed op would have made and puts back the broad-change mark. A batch whose rollback itself fails keeps its notes and makes earlier revisions unknown, since the catalog did change.
Conflicts:
- apps/lightcraft/src/native_menu.rs: upstream's item swapping (macOS keys
given up for real, Help menu not swapped, Settings / Quit tracked) with
KEY-GRID's keymap-aware rows. `Item.label` stays the text the row shows
(native_item_presentation), so `Item::native_label` reuses it instead of
formatting it again; Settings / Quit store theirs through
display_item_label, as upstream's native_label did. No early return after
a rebuild (upstream).
- crates/ui-egui/src/i18n.rs: upstream's tests (#2729: one test per check
across languages, gaps reported, never a failure) plus the batch's two new
ones (dropped-photos notice, Polish counts); production code as merged.
- grid.rs, keyword_list.rs, masking.rs: the translation tests #2729 removed
stay removed; the batch's new keyword and segmenter tests stay.
- cmd/library.rs, ui-egui/src/lib.rs: both test modules, each with its
#[cfg(test)].
- xtask/src/main.rs: #2302's test_selected and upstream's translation_gaps.
- docs: tracker / gap rows take both sides' edits, headers list both sides'
changes, history rows of both sides newest-first. What's New: one section
per date, newest first (the batch had 10-22 above 10-23, and second 10-21
and 10-19 sections); no line lost.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Found while testing crates/catalog/src/rules.rs:673 (num_op), reached via Rule::matches at :956-957
Co-Authored-By: Claude Haiku 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Two catalog additions and one internal change, with nothing calling the new ones yet:
- Catalog::folder_records_located: folder_records_follow for a folder located where it
already is on disk (records of library folders at the destination stay).
- Catalog::local_records_of: the Local records of some files, split into those that can
give way and those that hold something of the person's.
- Find Missing Photos hashes a file in 64 KiB pieces instead of reading it whole (the same
hash).
LibraryLock::acquire wrote catalog.lock.owner with std::fs::write,
which follows a symlink at that name. A library folder someone else
prepared (a shared drive, an unpacked archive, a synced folder) could
carry catalog.lock.owner -> ~/.bashrc or any other file the user can
write, and opening the library replaced that file's content with the
owner JSON. The catalog's staging files had the same problem and were
fixed in #1502; the owner note was missed.
The note is now written through store::create_staging (made
pub(crate)): whatever is at the name is removed (a link itself, never
its target) and the file is created with create_new. Drop already
removed the note with remove_file, which never follows a link.
Test (Unix): a link at catalog.lock.owner pointing at a file outside
the library leaves that file's bytes unchanged through acquire and
drop, and the note lands in the library as a regular file naming this
process.
Reported-by: BS.ehlas.cz
A full count (AlbumTally::of, albums.list) keeps each loop-free smart album's photos for the pass and
answers "album is #N" from them, so an album tested by 51 others is worked out once instead of 52 times
per photo. Albums in or reaching a loop, or in a chain too deep for the depth limit, are evaluated by
their rules as before.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
docs/parity-checklist.md: header keeps the newer date; LR-IMP-FORMATS row keeps both sides' changes.
docs/raw-parity.md: header change note names both sides' changes.
docs/raw/coverage.tsv: merged cleanly; same rows as both sides, no duplicates (regenerated on the combined branch).
crates/raw/src/kelvin_presets.rs: covered_models keeps the Z5II asserts (#2006) and the Canon cases (#2009).
docs/parity-checklist.md: LR-EDIT-COLOR-TEMP row keeps both sides' preset clauses.
crates/ui-egui/src/tests_grid.rs: both sides' tests
docs/whats-new.md: both entries under one 2026-10-16 heading
docs/parity-checklist.md: header and revision rows from both sides
shift_iso writes back only a time it fully reads (a date alone, or HH:MM
with optional seconds and fraction, an optional zone, nothing after);
iso_seconds stays lenient for sorting and rules. photo.setCaptureTime
leaves such photos as they were and counts them as `unreadable`, and a
set that would move other photos by an unreadable time is refused; alone,
the photo gets the typed time. The dialog's preview says the photo's time
can't be read, OK says what was left, any error keeps the dialog open
(button and ui.dialog.confirm), and Set mode doesn't start from an
unreadable time.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* Use chrono for UTC/calendar conversion instead of hand-rolled copies
Replaces the hand-rolled Howard Hinnant civil/days-from-civil copies and the
leap-year/days-in-month table with chrono as a workspace dependency:
- apps/lightcraft/src/logging.rs: timestamp (log line time)
- crates/catalog/src/dates.rs: civil + weekday
- crates/catalog/src/stacks.rs: iso_seconds (date -> seconds)
- crates/engine/src/cmd/face_recognize.rs: capture_secs (native only)
- crates/meta/src/datetime.rs: DateTime::unix_seconds and the valid() day check
Output formats and semantics are unchanged (pre-epoch clamp, day rollover that
normalize_iso relies on, leap-second tolerance, EXIF/XMP parsing). chrono uses
only std, so wasm stays safe. AGENTS.md/CLAUDE.md now say time goes through chrono.
Follow-up to storytold/cadcraft#1785
* Never panic on a clock outside chrono's range; keep 5-digit years unprefixed
chrono's From<SystemTime> unwraps, so a clock past year 262143 panicked
inside the logger. timestamp now uses the fallible DateTime::from_timestamp
and reads such a clock as the epoch, keeping the old pre-epoch clamp.
timestamp and catalog's civil write years past 9999 with {:04} like the old
{y:04} did, because %Y would add a '+'. Tests cover the out-of-range and
5-digit-year cases.
* Don't build an unrepresentable SystemTime in the far-future test
UNIX_EPOCH + Duration::from_secs(u64::MAX / 2) overflows Windows'
SystemTime and panicked before timestamp() ran, failing the Windows
test job. Build it with checked_add and only assert where it exists.
* time: preserve old out-of-range date conversion (no epoch fallback)
`DateTime::from_timestamp(...).unwrap_or_default()` silently reset any instant
past chrono's year range to 1970-01-01, changing output the old hand-rolled
Howard-Hinnant code converted exactly (e.g. 10_000_000_000_000 s was
318857-05-20 17:46:40, not the epoch). Restore the old conversion domain with a
cycle-based helper that routes the calendar through chrono (the calendar
repeats every 400 years = 146_097 days, so month/day/time come from chrono and
only the year is carried in i128, keeping every i64 exact).
- apps/lightcraft/src/logging.rs: `timestamp` via `utc_parts`; keep the
pre-1970 epoch clamp; u64 secs past i64::MAX saturate, not epoch.
- crates/catalog/src/dates.rs: `civil` cycle-based (the callers stay inside
0000-9999, but a far clock no longer resets to the epoch).
Pinned old outputs in tests:
logging u64::MAX/2 s -> 292277026596-12-04T15:30:07.000Z (was epoch),
10_000_000_000_000 s -> 318857-05-20T17:46:40.000Z
civil 10_000_000_000_000 -> 318857-05-20T17:46:40,
-10_000_000_000_000 -> -314918-08-13T06:13:20, i64 extremes
* Don't build a year-318857 SystemTime unchecked in the logging test
Windows' SystemTime only reaches about year 30828, so
UNIX_EPOCH + Duration::from_secs(10_000_000_000_000) panicked before
timestamp() ran and failed the Windows test job. Build it with
checked_add and assert where the platform can hold it. The year-10000
case fits everywhere and stays unconditional.
* Name the 400-year range extension as the one exception to the time rule
The rule said never to hand-roll calendar math, but the range fix
(shifting by whole 400-year Gregorian cycles so dates past chrono's year
range keep their old output) is cycle arithmetic, so the rule and the code
disagreed. The rule now names that exception: the library still computes
month, day and time, and only whole cycles are carried in the year.
* Say the library computes month and day; time-of-day is plain arithmetic
Narrows the range-extension wording: the date library computes the
calendar fields, and splitting seconds into hours, minutes and seconds
is ordinary arithmetic, which is what the code does.
An ASCII path and rule text are compared byte by byte, folding case and
backslashes, with no allocation; other text takes the old code unchanged.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Every catalog change gave the grid a new list of photos, and it read every
photo of it again: each one's date group (a lookup, a date parse and a
string) and its cell's shape. At 180,000 photos that was a 100 ms frame for
every batch an import added.
- catalog: Catalog::date_runs is split into date_group (one photo's own
group) and date_runs_of (the stack rule and the runs), which the grid
uses too.
- ui: GridCache keeps what it read of each photo of the list, position by
position. A new list that differs by the photos the catalog says an edit
touched is read again only for those; anything else (another view,
order or grouping, a stack) is read whole. The square grid reads no
shapes. A 16-photo batch at 181,749 photos: 101 ms -> 7 ms.
Co-Authored-By: Claude <noreply@anthropic.com>
covers_camera tried only the first-space split, so NIKON CORPORATION NIKON Z5_2
stayed on the relative scale in the catalog and editor. Try every split, as
spectral::find_camera does.
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Review on #1468: main's #2099 split out text_matches() and lets unknown field
prefixes fall back to plain text. On top of it, text_matches() now also matches
Photo::people(); the free-text token is unquoted before text_matches(); an
unknown field's quoted value is unquoted in the fallback (`10:"30 am"` is the
text `10:30 am`); and the field split keeps the !t.starts_with('"') guard.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Found while testing crates/catalog/src/dates.rs:58 (normalize_iso), used by crates/engine/src/cmd/manage.rs:201
Co-Authored-By: Claude Haiku 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Haiku 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>