* never-crash: deny the panic lints in every crate root; GPU asserts become fail paths (#2137) The never-crash policy was documented but not machine-checked for the two largest crates: engine and meta carried only #![forbid(unsafe_code)], so a future unwrap/expect/panic would pass cargo xtask ci. Both crate roots now also carry as does xtask (previously un-annotated); the guarded expects in meta/src/xmp.rs and xtask's parity.rs / root() now use the documented #[allow(clippy::expect_used)] + invariant form. crates/gpu: the three production asserts become fail(FailKind::Fatal, ..) + return, so a binding-count mismatch, an over-long readback, or a wrong-length curve fails the render over to the CPU instead of crashing (finish_and_read also early-returns on len == 0, which would have issued a zero-size copy_buffer_to_buffer that wgpu rejects). xtask: new roots check (in ci) verifies every workspace lib root and every binary main.rs carries both attributes; crates/sysmem is the documented exception for forbid(unsafe_code) only. A panic in placement or readback is a fail, not a crash. * audit fixes: roots covers binary and cdylib/rlib roots; render gates on failed() after finish_block (#2137) Two reviewers audited the first commit; both highs it found are fixed here: - roots scanner: the by_ref().any()/any() pair consumed the kind iterator hunting for "lib", so every bin-only root (both app mains, the sam3-worker bin, xtask itself) and the cdylib/rlib web root was silently unchecked — the check covered 29 of the 33 real roots. Kind detection now collects the kinds once and matches lib/rlib/dylib/cdylib/staticlib, with a root_paths regression test over bin, lib and cdylib/rlib targets; unreadable root files now fail with the I/O error instead of a fake policy violation. - render.rs: the caller gates on failed() right after finish_block, so the malformed-curve fail path can no longer reach the band loop's p[y0_at] index on the empty block. - finish_and_read: the len == 0 path now submits the encoder before returning zeros, so a failure staged in the same render stays detectable (the old zero-size copy failed loudly via the error scopes; a bare return would have hidden it). - docs: finish_block and finish_and_read state their failure contracts; USAGE and AGENTS.md name the new roots step in the ci enumeration. * xtask: check a library root at the source path that cargo metadata reports The roots check read src/lib.rs for every library target, so a package that sets its library path was checked on the wrong file. Libraries now use the reported source path, with the same default as binaries when none is given. * xtask: match the crate-root deny lints by whole name, ignoring a trailing comment A lint named only in a trailing comment, or a lint whose name contains another (clippy::panic inside clippy::panic_in_result_fn), satisfied the deny check. Lint names are now compared whole and a trailing comment is ignored. * xtask: read the crate-root attributes from the header, not a fixed 30 lines The roots check read only the first 30 lines of each crate root, so a root whose attributes came after a longer doc header was reported as missing them. The check now reads the inner attributes at the top of the file: it skips blank lines and comments, reads each attribute whole, and stops at the first item. A multi-line list is read in full and a commented-out attribute does not count. * xtask: reword two comments in the roots check Comments only: the lint-name match and the source-path default are described without naming a lint or a build tool. No code changes. * xtask: report a root attribute that overrides the required denies * xtask: accept a runtime manifest dir only when it has a parent * xtask: render the overridden deny attribute without literal braces The write! escaped the braces around the substituted attribute and lint, so the violation printed `#![{allow}({clippy::panic})]`; assert the rendered message in the allow-override test. * Tests: new test helpers meet the crate-root panic lints Main added test helpers after this branch; the deny at the crate root now applies to them. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * roots: crates/wincolor is the second crate allowed unsafe Main added it for the Windows colour APIs after this branch; its module docs already say so. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> --------- Co-authored-by: eliets2 <287273517+eliets2@users.noreply.github.com> Co-authored-by: FredApps <153173854+FredApps@users.noreply.github.com> Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
lightcraft-meta (L1)
Photo metadata: EXIF, XMP (read + write), IPTC-IIM, and the containers that carry them.
API
| Function | Purpose |
|---|---|
read_exif(&[u8]) -> Metadata |
TIFF-structured Exif block (whole TIFF/DNG/raw file, or JPEG APP1 payload with/without Exif\0\0). try_read_exif reports header errors. |
from_tiff(&Tiff) -> Metadata |
Same, from an already parsed stream (used by lightcraft-raw). |
embedded(&[u8]) -> Embedded |
Sniffs JPEG / PNG / WebP / TIFF / PSD / PSB and returns the Exif, XMP (+ extended XMP), ICC and IPTC blocks. PSD/PSB EXIF resource 1058 and XMP resource 1060 are read from the bounded image-resource section without decoding pixels. jpeg_segments, png_chunks, webp_chunks are the per-container walkers. |
parse_xmp(&str) -> Result<XmpData> |
Metadata + the opaque lc:settings JSON + every property (prefix:name, structs flattened as a:b/c:d). |
write_xmp(&Metadata, Option<&str>) -> String |
Complete <?xpacket?> packet; the second argument is LightCraft's full develop settings JSON. |
parse_iptc(&[u8]) -> Metadata |
IIM record 2: title (2:5), keywords (2:25), by-line (2:80), copyright (2:116), caption (2:120), date/time created (2:55/2:60). |
extract(&[u8]) -> Metadata |
Everything for a whole file. Precedence: EXIF → IPTC fills gaps → XMP overrides user fields (rating, label, title, caption, artist, copyright, keywords, GPS) and fills the rest. |
try_extract(&[u8]) -> Result<Metadata, ExtractError> |
Like extract, but reports malformed PSD/PSB headers, image-resource sections, or embedded EXIF/XMP structure (including duplicate EXIF/XMP resources) so callers can fail closed when location absence cannot be established. Other formats remain best-effort. |
Metadata carries camera (make, model, serial, software), lens (make, model, serial, specification), capture
(date-time with UTC offset and milliseconds, exposure time, f-number, ISO incl. extended ISO, focal length and
35 mm equivalent, flash, exposure bias, program, metering, white balance), orientation, GPS, dimensions and the
user fields (artist, copyright, title, caption, keywords, hierarchical keywords, rating −1…5, colour label).
EXIF capture-time candidates are tried in order: DateTimeOriginal, DateTimeDigitized, then IFD0 DateTime. A clock with more than three colon-separated components is rejected as a whole, allowing the next valid candidate or XMP fallback to supply the capture time. The original tag remains available in All Metadata. JPEG ICC data is exposed only when its APP2 chunks form a complete, consistent, uniquely numbered sequence; malformed or truncated ICC sequences leave unrelated readable metadata available and produce no profile.
XMP namespaces
Written: dc, xmp, photoshop, exif, exifEX, tiff, lr (hierarchicalSubject) and ours,
lc = http://ns.lightcraft.app/lc/1.0/ (lc:settings, an opaque JSON string). Reading canonicalises
prefixes by namespace URI, handles attribute and element forms, rdf:Seq/Bag/Alt (x-default first),
rdf:parseType="Resource" and nested-description structs, predefined and numeric entities. Nesting depth
and node count are limited.
An explicitly foreign namespace using a recognized prefix (for example lc or xmp) cannot supply
recognized properties. Its keys use Q{hex-encoded namespace URI}local in properties and values;
hex encoding keeps / reserved for struct paths. Other unknown prefixes keep their existing QName keys.
Merging preserves the original foreign XML rather than writing these opaque keys into the packet.
The parser and merge scanner each cap generated opaque names at 8 MiB per packet, returning TooComplex
if repeated use of a long namespace URI would exceed that budget.
Robustness
No panics on malformed input: proptests feed random bytes to every container walker and random text to the XMP parser; truncation tests cut a sample JPEG at every byte and exercise truncated PSD/PSB headers and image-resource records with hostile declared lengths. PSD/PSB XMP resources require valid UTF-8; trailing NUL padding is trimmed after validation.
ISO capture-time parsing requires a complete clock and consumes every clock and fractional-second character. Invalid
XMP capture-time values remain available in the raw property map but are not projected into Metadata; a later valid
capture-time property can still supply the metadata value. Invalid GPX point times are treated as untimed.
Sources
CIPA DC-008 (Exif 2.32), TIFF 6.0, Adobe DNG 1.7 (tag numbers), ISO 16684-1 / Adobe XMP Specification
Parts 1–3 (packet, RDF serialisation, standard schemas, JPEG extended XMP), IPTC-IIM 4.2 and the IPTC Photo
Metadata Standard, ITU-T T.81 (JPEG markers), ICC.1 (APP2 embedding), PNG Specification 3rd ed. (eXIf,
iTXt), the WebP RIFF container specification, Adobe Photoshop File Format Specification (image resource
blocks). XML parsing: quick-xml (MIT). No third-party metadata code was consulted.