An oversized CR3 candidate is skipped without ending the search, repeated child IFD references count once toward the pending cap, deflated chunks lay their rows out as the unpacker reads them (16-bit words, 32-bit word alignment), the tile grid's nominal size is checked against the image before anything is decoded, chunk grouping saturates, and the XMP flattened-key budget is document-wide. The JPEG-header test now parses a real header.
XMP flattening, sparse-focus EXIF decode (plus the maker-note and TIFF value
budgets), lossy-DNG chunk staging, panorama stitch frames, the reference ONNX
executor's tensors and the denoise product reader all now reject or cap what a
malformed file can make them allocate: each result is checked before the
memory behind it is reserved, every regression carrying a test that fails
without its bound.
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>
Conflicts:
- xtask/src/main.rs: upstream's runners (cargo test or nextest, no
examples, doctests apart, one wasm `cargo check --keep-going`) with
#2302's `ci --quick` on top: test_selected builds the same targets
(--lib --bins --tests), the wasm check takes the changed crates in one
call, the full test step is upstream's.
- apps/lightcraft/src/control_server.rs: upstream's `before` closure, with
the batch's no-delay sockets kept.
- docs/parity-checklist.md: LR-BEHAV-ACCESS from main (tests/it path),
LR-BEHAV-LOCALE / LR-BEHAV-LOCALIZE from the batch (Polish).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* CI: dev profile keeps line tables for workspace crates and no debug info for dependencies
* Control server tests: persistent_requests_and_slow_replies_outlive_the_idle_read_timeout no longer races the idle timeout
Each request (and the closing half-shutdown) reaches the socket before the
worker reads, so on a slow machine only the reply waits outlast the 100 ms
idle timeout, as the test intends. The idle test sends its input the same
way, and the no-frames test allows 5 s instead of 2 s to give up.
* CI: one integration-test binary per crate
Each crate's tests/*.rs become modules of tests/it/main.rs (51 test files
into 13 binaries, plus 6 that stay their own). Inner #![cfg] attributes
move to the mod lines. Files that change state for the whole process keep
a binary of their own: codecs jxl_tail_heap and raw sr2_heap (dhat global
allocator), gpu shutdown, shutdown_first and memory, ui-egui shutdown.
Docs, comments and the GPU probe name the new paths and targets
(--test it <module>::); the CI report names an it binary's skips after
the crate.
* CI: tests under nextest, doctests apart, one wasm check
xtask ci runs the tests with cargo nextest run --lib --bins --tests when
cargo-nextest is installed, else cargo test with the same targets and a
one-line notice; doctests run after them with cargo test --workspace --doc.
The heif and hardware GPU steps use the same runner. Examples stay out of
the test build (clippy --all-targets still checks them). Concurrent tests
under nextest follow the RUST_TEST_THREADS cap (NEXTEST_TEST_THREADS).
The wasm check is one cargo check --keep-going over the same crates, and
clippy runs with --keep-going.
* CI: tests run under cargo test by default, nextest with LIGHTCRAFT_NEXTEST=1 (per-process scene cache)
Found while testing crates/meta/src/exif.rs:483 (write_exif, the `r` helper; via lightcraft_tiff::writer::rational at crates/tiff/src/writer.rs:261)
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).
* 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.
Found while testing crates/meta/src/xmp.rs:458 (fmt_gps_coord)
Co-Authored-By: Claude Haiku 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Found while testing crates/meta/src/tags.rs:148 (rational)
Co-Authored-By: Claude Haiku 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Found while testing crates/meta/src/exif.rs:455 (write_exif)
Co-Authored-By: Claude Haiku 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Resolved against rb-base-36: Scanned keeps the base's rewrite flag next to magic and tag; Exif keeps the Panasonic lens fallback; locale catalogs merged (one duplicate key dropped).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Found while testing crates/meta/src/xmp.rs:364 (parse_gps_coord, used at xmp.rs:539-544)
Co-Authored-By: Claude Haiku 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Move nested conditions into match guards and simplify equivalent Boolean
expressions without changing validation or short-circuit behavior. Keep
the explicit permitted Testkit dev-dependency arm in the layer checker.
Co-Authored-By: OpenAI Codex <noreply@openai.com>
photo.allMetadata listed no lens for a RW2: Panasonic writes no Exif
LensModel, and the maker note that names the lens is in the Exif of
the embedded JpgFromRaw. List that maker note tag (LensType, 0x0051)
as a row of a Panasonic group, the same name photo.inspect shows, for
a RW2/RWL and for a JPEG with the note in its own Exif.
Checked on the CC0 sample DMC-G80 P1000403.RW2: Panasonic / Lens Type
= SIGMA 60mm F2.8 DN.
Panasonic writes no Exif LensModel. The lens name is in its maker note
(LensType, 0x0051), and a RW2 or RWL keeps that maker note only in the
Exif of its embedded JpgFromRaw (IFD0 0x002E), not in the raw's own Exif
IFD. When LensModel is absent, read it from the maker note, and for a
raw from the JpgFromRaw's Exif.
Checked on the CC0 samples DC-GX9 gx9_1_1.RW2 (LUMIX G 25/F1.7) and
DC-S5 P1047510.RW2 (LUMIX S 85/F1.8), also through the sparse header
plan an import uses.
Docs conflicts resolved on upstream's text; MRW edits reapplied with
coverage re-measured by docs/raw/coverage.py (243 verified, 946
unverified, 221 preview). Drops stale duplicate rows upstream's docs
carried in raw-parity.md and gaps.md.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Phones without a fix write a GPS IFD whose refs are NUL and whose rationals
are 0/0. dms() read the triplet through to_f64_vec, which turns 0/0 into
0.0, so the block became 0° N 0° E and exports wrote that location.
dms() now rejects a component that is present but not a number, and
read_gps() requires an N/S and E/W ref.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Photos imported before focus points were read got one only through Reload from Disk, which
hashes whole files on the UI thread. Now:
- lightcraft_meta::focus::read_header reads only what the decoder looks at: IFD0 (Make,
Model), the Exif IFD, the maker note's IFD and the focus tags' values (a JPEG: its segments
up to the Exif one), walked round by round and decoded with the whole-file code from a
buffer of the file's length holding just those ranges. Bounded rounds, bytes (2 MB) and
size (1 GB). One 32 KB read per α1 / A7S III ARW; on 109 real files (83 with a point)
it gives the whole-file read's point for every one.
- A background job (crates/engine/src/focus_scan.rs) reads photos with no point from a
supported body (by the stored camera; nothing else is opened), ARW or JPEG, one at a
time in path order, with a "Reading focus points" activity row (Cancel::Flag). The UI's
per-frame focus.pump gives it the face scan's pace (paused while dragging or typing) and
starts it once per library opening. Commits every 200 photos or 3 s as system ops (no
undo step, no sidecar write), only meta.focus, only on photos that still have none at the
same path. "Looked, none" goes to focus-scanned.bin beside the library, headed by the
decoder's tag so a decoder knowing more bodies starts afresh; unreadable files are retried
at the next opening.
- photo.readFocusPoints {ids?, wait?} (Photo > Read Focus Points) runs it on demand.
Measured cold on a 5400 rpm USB drive: 200 α1 / A7S III ARWs in 1.9 s (~100 photos/s,
32 KB each), so 58,796 raws take about 10 minutes instead of re-reading 2.25 TB.
Read the autofocus point from Sony maker notes (FocusLocation 0x2027, FocusFrameSize
0x2037, FocusMode 0x201b, AFAreaModeSetting 0x201c, AFTracking 0x2021 and the area mode
used from the enciphered 0x9402 block), for the ILCE-1 and ILCE-7SM3, whose layout was
checked against ExifTool's output on 333 real ARWs. Manual focus, centre-only Multi
reports and unchecked bodies give no point. The box is stored on the upright photo as
Meta.focus (omitted when unknown; no catalog format change), filled at import and by
Reload from Disk.
View > Show Focus Point (Alt+F) draws it in Detail, Compare and Survey through rotation
and crop; View > Zoom to Focus Point (on by default) makes Toggle Zoom centre on it; the
Info panel shows "AF-C · Wide · Animal Eye Tracking". photo.inspect returns meta.focus.
The \0MRM block container: PRD sensor size, storage and Bayer cell; WBG
as-shot gains in cell order; TTW TIFF for Exif and orientation. Black 0,
white from the saturation plateau. 4 CC0 samples pinned in RAW_SAMPLES;
DiMAGE 7, 7Hi, A1, A2, A200 and Dynax / Maxxum 7D verified. Also fixes
clippy::chunks_exact_to_as_chunks in pipeline/spots.rs.
Sources: public prose description of the MRW layout (Dalibor Jelinek's
MRW notes) and own black-box analysis of the CC0 samples.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>