Commit Graph
276 Commits
Author SHA1 Message Date
aiongg d151df0149 style: cargo fmt the moved test modules 2026-10-26 18:30:36 +00:00
aiongg c751fd84e9 refactor(lightcraft-catalog): move rules.rs tests to crates/catalog/src/rules/tests.rs 2026-10-26 18:16:07 +00:00
aiongg c619470bbb refactor(lightcraft-catalog): move keywords.rs tests to crates/catalog/src/keywords/tests.rs 2026-10-26 18:16:06 +00:00
skylartaylor c057ccc23f Migrate LightCraft to egui 0.37 and reconcile dependency and font locks
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.
2026-10-25 02:54:42 +01:00
Brandon Thomas 7ed3f0f046 Merge PR #1122
# Conflicts:
#	crates/catalog/src/tests_local.rs
2026-10-24 10:20:13 -04:00
Brandon Thomas 749928088a Merge PR #2699 2026-10-24 10:20:13 -04:00
Brandon Thomas 621c4dcb6e Merge PR #2761 2026-10-24 10:20:13 -04:00
Brandon Thomas 747caeeb58 Merge PR #2788 2026-10-24 10:20:12 -04:00
Marco Arnulfo afbd931561 LR-FILT-LABEL: the label chip and filter bar show what label and labels let through together, not their union 2026-10-24 14:51:44 +02:00
NetterSilverandClaude Haiku 5.5 098a1a2e72 Forget Local records directly in drive and share roots
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>
2026-10-24 09:51:09 +00:00
skylartaylor aa870ffbe1 egui-037: reconcile current main and preserve valid catalog chains 2026-10-24 09:04:49 +02:00
Nathaniel Ledford c6acf9e642 LR-VIEW-PHOTOGRID: say in the docs that a failed rollback advances the revision
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.
2026-10-24 00:20:43 -04:00
Nathaniel Ledford bd485adc89 LR-VIEW-PHOTOGRID: a batch that cannot be rolled back whole makes a revision
The catalog changed, so caches keyed by its revision must not keep serving what they held for the one before.
2026-10-23 23:37:40 -04:00
francoisgnandClaude Opus 5.5 538b261067 Merge upstream/main (#2763) into round3a-integration
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-24 05:50:04 +03:00
Dylan M. TaylorandCodebuff 4ae3089420 security: bounded keyword, repair and journal-recovery work (CodeRabbit) (#2763)
* 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>
2026-10-23 22:45:21 -04:00
Nathaniel Ledford 8781f5cb5c LR-VIEW-PHOTOGRID: an op that fails leaves no touched photos behind in the catalog
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.
2026-10-23 21:51:53 -04:00
francoisgnandClaude Opus 5.5 144d900c21 Merge upstream/main (integration batch 43, #2729, #2737, #2412) into round3a-integration
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>
2026-10-24 02:26:48 +03:00
Brandon Thomas a344e277f0 Merge PR #2728 2026-10-23 17:21:16 -04:00
Brandon Thomas 433ce2375d Merge PR #2109 2026-10-23 17:21:16 -04:00
NetterSilverandClaude Haiku 5.5 c7cff2d9a7 Fix smart album gte/lte rules missing equal f-number and focal length
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>
2026-10-23 21:24:32 +02:00
Nathaniel Ledford 5d24e34ed5 LRC-LIB-LIBFOLDERS: a folder located below itself leaves the records already at its destination in place 2026-10-22 15:32:44 -04:00
francoisgn 1938bc3845 Merge PR #2422: PERF-SMART: count every smart album once per pass, not once
# Conflicts:
#	docs/whats-new.md
2026-10-22 22:31:03 +03:00
francoisgn 77f313a0f2 Merge PR #2506: Edit Capture Time leaves a capture time it can't fully read
# Conflicts:
#	crates/ui-egui/src/lib.rs
#	docs/parity-checklist.md
#	docs/whats-new.md
2026-10-22 22:29:19 +03:00
Nathaniel Ledford a9be2e14f3 LRC-LIB-LIBFOLDERS: catalog queries and streamed file hashing for locating a folder (#1112)
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).
2026-10-22 14:48:20 -04:00
Matej Vrzala ffb57d1a9b Library lock: Don't write through a link at the owner note's name
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
2026-10-22 18:39:33 +02:00
Nathaniel LedfordandClaude Opus 5.5 6b8cb9826c PERF-SMART: count every smart album once per pass, not once per album that tests it
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>
2026-10-22 01:54:43 -04:00
aiongg 727e66babe Merge track r2-t03 into next
# Conflicts:
#	docs/gaps.md
#	docs/parity-checklist.md
#	docs/ui-parity.md
#	docs/whats-new.md
2026-10-22 02:53:00 +00:00
aiongg 3300e6dad8 Merge track r2-t01 into next
# Conflicts:
#	docs/gaps.md
#	docs/parity-checklist.md
#	docs/raw/coverage.tsv
#	docs/whats-new.md
2026-10-22 02:49:52 +00:00
aiongg 70e9338085 Merge PR #1040: LR-IMP-FORMATS: decode Minolta MRW
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).
2026-10-22 02:42:05 +00:00
aiongg 91744336d0 Merge PR #2006: LR-EDIT-COLOR-TEMP: Kelvin white balance for the Nikon Z5II from built-in presets
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.
2026-10-22 02:41:12 +00:00
aiongg 61ad2ff853 Merge PR #2054: LR-VIEW-PHOTOGRID: the grid reads only the photos an import batch or an edit touched
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
2026-10-22 02:40:53 +00:00
aiongg 792b2517e5 Merge PR #1468: Issue #1449: Search Photos finds named people, and quotes keep a name together (steps 1 and 2) 2026-10-22 02:40:15 +00:00
aiongg 4ba72fa0eb Merge PR #2420: PERF-SMART: File Path rules compare in place instead of building strings per photo
docs/whats-new.md: both entries, newest first.
2026-10-22 02:38:00 +00:00
Clyde C BrownandClaude Opus 5.5 2292a91dd8 Merge origin/main into fix/capture-time-strict-shift
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-21 19:35:53 -06:00
Clyde C BrownandClaude Opus 5.5 3aa26d8273 #2505: Edit Capture Time leaves a capture time it can't fully read as it was
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>
2026-10-21 19:20:28 -06:00
Dylan M. Taylor bfb23db2ca Use chrono instead of hand-rolled UTC conversion (#2457)
* 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.
2026-10-21 19:36:36 -04:00
Nathaniel LedfordandClaude Opus 5.5 84dbd1b15a PERF-SMART: File Path rules compare in place instead of building strings per photo
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>
2026-10-21 14:07:51 -04:00
Nathaniel LedfordandClaude d184b3ef50 LR-VIEW-PHOTOGRID: the grid reads only the photos an import batch or an edit touched
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>
2026-10-21 13:17:39 -04:00
aiongg 5443225c3c Merge track r1-i18nci into next 2026-10-21 14:54:41 +00:00
aiongg 6e893fdaa3 Merge PR #1967: CI: catalog's staging-link test module compiles only on Unix (fixes clippy -D warnings on Windows) 2026-10-21 14:52:38 +00:00
aiongg 7760d71655 Merge PR #2044: Reject impossible hour/minute/second in normalize_iso 2026-10-21 14:49:01 +00:00
Andrew FrahnandClaude Sonnet 5.5 d8f1b30bcc Kelvin presets: match multiword makes in the catalog's joined camera lookup
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>
2026-10-20 10:04:25 +11:00
Andrew FrahnandClaude Sonnet 5.5 5e1bac1264 Merge upstream/main into pr/longtail-raws
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-20 09:58:17 +11:00
Arekatla Nishanth ChowdaryandClaude Opus 5.5 3a6e7d0b9b Merge main into feat/1449-people-search; people and quotes go through text_matches (#2099)
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>
2026-10-19 22:18:49 +05:30
NetterSilverandClaude Haiku 5.5 eb009d1bd6 fix: give Find Similar an explicit empty-result filter state and clamp similarity
Co-Authored-By: Claude Haiku 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-18 15:59:53 +00:00
Brandon Thomas 592cb335cb Merge PR #1935: raw: alias Canon EOS R6 Mark II to EOS R6m2 2026-10-18 09:18:21 -04:00
Brandon Thomas d6a9a2e8f9 Merge PR #1845: Match f:/aperture:/focal: search tokens at f32 precision 2026-10-18 09:18:19 -04:00
Brandon Thomas 5c85bc3ee4 Merge PR #2149: Refuse catalog snapshots with an out-of-range seq 2026-10-18 09:18:14 -04:00
NetterSilverandClaude Haiku 5.5 601ddce6ef Match f:/aperture:/focal: search tokens at f32 precision
Found while testing crates/catalog/src/query.rs:130

Co-Authored-By: Claude Haiku 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 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>
2026-10-18 13:49:17 +02:00
NetterSilverandClaude Haiku 5.5 6e97193b00 Reject impossible hour/minute/second in normalize_iso
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>
2026-10-18 11:36:44 +00:00