Commit Graph
9 Commits
Author SHA1 Message Date
Dylan M. Taylor 251ef87c89 ci: use StepSecurity's maintained actions (#853)
Each third-party action that StepSecurity publishes a secure drop-in
replacement for now uses that replacement, pinned to a commit with its
version as a comment. Versions follow what we used: the newest
StepSecurity release in the same major, or the closest major they
publish. dtolnay/rust-toolchain refs that named a toolchain (stable,
nightly, 1.76) become the replacement's `toolchain` input.
2026-11-11 14:57:04 -05:00
Dylan M. Taylor ac5f998274 ci: run Harden-Runner on every job (#811)
Adds step-security/harden-runner (audit, pinned to v2.22.4 like
craft-common-actions) as the first step of each job that did not have it,
so every workflow in the org's public repos reports its egress to
StepSecurity. Audit, not block: block-mode allow lists come from each
job's observed traffic.
2026-11-11 12:10:08 -05:00
FredAppsandFredApps 02ef0c597a ci: save the dependency cache from main only, and cancel superseded PR runs (#772)
* ci: save the dependency cache from main only, and cancel superseded PR runs

* ci: give runs outside pull requests their own concurrency group

---------

Co-authored-by: FredApps <FredApps@users.noreply.github.com>
2026-11-10 15:06:39 +01:00
Aleksander HelgakerandClaude Opus 5.5 6865897f14 craft-media: add Ogg demuxer (#440)
* craft-media: Ogg demuxer

FilmCraft's `filmcraft-ogg` as `craft_media::ogg`, behind the opt-in
`ogg` feature: pages (CRC-32 checked, damage skipped by searching for
the next `OggS`), logical streams, packets across pages, and the Ogg
Opus, Vorbis and FLAC mappings. `OpusTiming` gives each Opus packet's
start, the pre-skip and the final page's end trimming.
`Stream::codec_params()` returns `CodecParams` like the other
containers; for FLAC each stream keeps its own header count, and an
unstated count is bounded (it ends at the first audio frame, or at
32 MiB with a warning). `ogg::ByteSource` is `craft_media::ByteSource`.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* craft-media: Ogg review fixes: bounded recovery, lost pages, chained links, caps

From the review of the Ogg demuxer (storytold/craft-libs#440):

- The damage scan charges the positions it examines, not a whole 1 MiB read,
  reads 64 KiB at a time, and says when the 64 MiB budget runs out. A file
  with many damaged pages lost its tail without a warning.
- A page whose lacing claims more bytes than the file has left is skipped
  and the scan resumes, instead of ending the parse.
- A gap in a stream's page sequence numbers drops the packet the lost page
  continued; its two ends were joined into one corrupted packet.
- A beginning-of-stream page for a serial already seen starts a new stream,
  so a chained file whose links reuse a serial no longer hands the second
  link's headers to the decoder as audio.
- The packets kept from a file are capped by its size (as Matroska caps
  frames), so zero lacing values cannot make the packet lists many times the
  file. FLAC metadata under an unstated header count must be a block of the
  length its header states, and at most 1024 are kept.
- A packet over 4 GiB is dropped instead of overflowing its size.
- OpusTiming saturates, so hostile granule positions cannot overflow, and a
  clip whose only audio page is also the last is trimmed at the end
  (RFC 7845 §4.5) instead of at the start.
- Speex streams read extra_headers (offset 68) for their header count.
  codec_params gives Theora its Xiph-laced headers and Speex its header,
  rate and channels; the FLAC extradata flags exactly the last block.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* craft-media: Ogg damage recovery charges every page it reads and hashes

From CodeRabbit's review of 2308071: a page that fails its CRC was read and
hashed in full (up to about 64 KB) before the parser stepped one byte on,
and the damage scan read 64 KiB per call however near the next page was.
Neither was charged to the recovery budget, so a file packed with fake page
headers could cost gigabytes of reading and hashing.

- A failed page's bytes are charged to the recovery budget; opening stops
  with a warning when it is spent.
- find_capture starts with a 256-byte read and doubles it up to 64 KiB.
- Warnings about damaged pages are capped at 1024, with a closing count of
  the rest.
- Filing a completed packet as header or audio moves into Stream::add_packet,
  keeping open under the cognitive-complexity limit.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* craft-media: Ogg recovery budget in proportion to the file; stray BOS; header caps

- Damage recovery charges every byte it reads again (a high-water mark), against a budget of
  twice the file's length plus 1 MiB, instead of a fixed 64 MiB that cut off large damaged
  files. A page's body is read without reading its header again, so a page is not charged twice.
- A beginning-of-stream page that carries on the page sequence of a stream with no
  end-of-stream page yet is a stray flag: a warning, and the stream is not split. A link that
  starts again before its end-of-stream page warns.
- Header packet counts a file states (FLAC, Speex) are capped like the unstated FLAC count.
- Reserved FLAC metadata block types (7-126) are metadata blocks.
- Theora codec_params carries the picture size; the extradata table lists Theora and Speex.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* craft-media: Ogg header reads checked before they are made; scan span on 32-bit

- A header packet past the size cap is checked by its stated size and not read, and a stream's
  first packet is read once (only its first 80 bytes when it is too large to be a header).
- MAX_HEADER_BYTES is 64 MiB, room for two of the largest FLAC metadata blocks with their block
  headers and the rest of the headers.
- find_capture clamps the scan span in u64 before casting, so a span of 2^32 bytes cannot become
  0 on 32-bit targets.
- The Recovery doc says which reads it charges, and what header packet reads add.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-03 09:06:14 -05:00
Aleksander HelgakerandClaude Opus 5.5 006b50d2ae craft-media: shared codec identifiers, one ByteSource, the testkit (1/10) (#439)
* craft-media: shared codec identifiers, one ByteSource, the testkit

The types a codec crate needs to describe a file and configure a
decoder, ahead of craft-audio:

- `Container`, `CodecId`, `PcmFormat` and `CodecParams` (`id.rs`),
  always built, with optional serde.
- `codec_params()` on `isobmff::Track` and `matroska::Track`, both
  returning `CodecParams` in the same form; `extradata` follows
  Matroska's `CodecPrivate`.
- One `ByteSource` trait (`source.rs`), with `ReadSeekSource`;
  `isobmff::ByteSource` and `matroska::ByteSource` are that trait.
- craft-media-testkit (`publish = false`): the ffmpeg oracle helpers
  from craft-media's tests, so the codec crates share one copy.

The existing tests prove the refactor; the new types have their own.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* craft-media: APV extradata is the bare record; reseek after a poisoned lock

An MP4 apvC track gave CodecParams.extradata with the 4-byte FullBox header
in front, where Matroska's V_APV CodecPrivate is the bare
APVDecoderConfigurationRecord. ApvConfig::record_bytes serializes the record
alone and extradata uses it.

ReadSeekSource kept its tracked position after recovering a poisoned lock or
a failed seek, so a reader moved by a panic was read from the wrong offset
with no error. Both now force a seek on the next read.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* craft-media: MP4 codec params per sample description

Track::codec_params always described the first stsd entry, while each sample
selects its own through description_index. It now describes the first
sample's entry; codec_params_for gives the parameters of any entry, and
has_mixed_descriptions says whether one set covers every sample.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* craft-media: test file-backed ByteSources; a File read past i64::MAX is an EOF

A File and a ReadSeekSource over a file now have the buffer's test: the same
bytes at the same offsets, and UnexpectedEof past the end. The test found
that a File read at an offset past i64::MAX failed with the OS's
InvalidInput instead of the UnexpectedEof the trait promises; it now
reports the end of the source like the other implementations.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* craft-media: Matroska codec params match MP4 for ALAC, DNx and Opus

Matroska's codec_params gave Unknown for A_ALAC and for DNxHD/DNxHR stored
as V_MS/VFW/FOURCC (AVdn, AVdh), where MP4 gives Alac and Dnxhd; ALAC's
extradata is the CodecPrivate, the ALACSpecificConfig MP4's alac box holds.
Opus now reports 48 kHz from Matroska as from MP4, whatever rate the muxer
declared.

ReadSeekSource clears the poison when it recovers the lock, so one
panicked read costs one recovery instead of a seek on every later read.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* craft-media: MP4 reports the MPEG audio layer the first frame names

An MP4 track with MPEG audio reported CodecId::Mp3 whatever the layer, since
mp4a + 0x69/0x6B names none (ffmpeg stores MP2 that way), while Matroska
reports Mp1/Mp2/Mp3 from its CodecID. open() now reads the frame header of
each such entry's first sample and codec_params() reports the layer it
names; without a valid header, QuickTime's .mp2 gives Mp2 and anything else
Mp3 as before. CodecConfig::Mp3 is unchanged.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-02 14:18:36 +01:00
dependabot[bot] 96f59c81cc ci(deps): bump the actions group across 1 directory with 2 updates
Bumps the actions group with 2 updates in the / directory: [actions/checkout](https://github.com/actions/checkout) and [actions/setup-python](https://github.com/actions/setup-python).


Updates `actions/checkout` from 4 to 7
- [Release notes](https://github.com/actions/checkout/releases)
- [Changelog](https://github.com/actions/checkout/blob/main/CHANGELOG.md)
- [Commits](https://github.com/actions/checkout/compare/v4...v7)

Updates `actions/setup-python` from 5 to 7
- [Release notes](https://github.com/actions/setup-python/releases)
- [Commits](https://github.com/actions/setup-python/compare/v5...v7)

---
updated-dependencies:
- dependency-name: actions/checkout
  dependency-version: '7'
  dependency-type: direct:production
  update-type: version-update:semver-major
  dependency-group: actions
- dependency-name: actions/setup-python
  dependency-version: '7'
  dependency-type: direct:production
  update-type: version-update:semver-major
  dependency-group: actions
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-10-24 06:00:20 +00:00
aiongg 7474826f6f media: craft-media review fixes (frame budget, PCM chunks, spec names) (#196)
* media: look up the track of each traf in a hash map

* media: bound Matroska indexing by a file-wide frame budget

Every indexed frame claims its stored bytes and at least one, so fixed-size
laces of empty frames cannot describe more frames than the file has bytes.
The frame count across tracks is capped at 2^26, the TrackEntry count at
2^16, and blocks find their track through a hash map.

* media: keep the spec spellings of Colour, ColourSpace and colourPrimaries

* media: PCM chunks stay bounded in MOV and MP4

The writer ends a PCM chunk at 1 MiB or half a second, and returns an
error past 2^32 - 1 samples in a track instead of clamping the stsz count.
The reader splits a PCM chunk above 16 MiB into several samples, so files
from writers without chunk limits read whole past 4 GiB. A sparse output
lets the tests write past 4 GiB, which also exercises the writer's co64
path.

* media: clamp max_lace_frames to 256 and round BlockDuration

* media: level-1 Matroska elements read at most the file's length in all

SeekHead entries pointing at nested elements made each one read the rest
of the file.

* media: set_sample_entry rejects a codec change

* media: the mux oracle compares stream counts before streams

* ci: media checkout does not persist credentials

* media: README lists the renames from FilmCraft and the output that differs on valid input

* media: rustfmt

* media: the stream count comparison leaves out timecode streams

* media: PCM chunks end at 1 MiB only, as ffmpeg's do

ffmpeg's PCM chunks run past half a second, so a duration limit split
remuxed chunks the source kept whole.

* media: frames and samples indexed scale with the file's length

Matroska indexes at most one frame per 8 bytes of file and ISO BMFF at most
one sample per 4 bytes, beyond the first 2^20 of each, so the sample tables
stay within about ten times the file's size. Empty fixed-size laces and
one-byte constant-size samples could make them 40 to 76 times its size.

* media: clamp the frame and sample allowances
2026-10-23 15:42:51 -04:00
Dylan M. Taylor 1b9cf1973a ci: pin GitHub Actions to full commit SHA (SonarCloud githubactions:S7637) (#228)
Pin third-party actions to full-length commit SHAs (with version comments)
instead of mutable tags/branches to prevent supply-chain tampering via
retagged releases. Resolves SonarCloud rule githubactions:S7637.
2026-10-21 20:14:01 -04:00
aiongg 07047b7221 Add craft-media: shared MP4/MOV, Matroska and bitstream code from FilmCraft (#182)
* media: add craft-media with bitstream and isobmff from FilmCraft

Moves filmcraft-bitstream and filmcraft-isobmff (storytold/filmcraft@6c3948a)
into one crate with a feature per module. The public API matches FilmCraft's so
its crates can become re-exports. Tests, oracle tests and fuzzing come along;
the ffmpeg discovery helpers replace filmcraft-testkit.

Out-of-contract inputs no longer panic: bit widths above 32, a zero NAL length
prefix width, and PCM frame sizes that overflow u32. Adds
BitReader::read_ue_plus from DeckCraft's copy.

* media: public ISO BMFF box reader (isobmff::boxes)

Make the demuxer's box iterator and big-endian reader public as
isobmff::boxes::{BoxIter, BoxRef, BoxHeader, Reader}, for walkers of
boxes the demuxer does not model (CR3 uuid boxes, HEIF meta items,
JPEG 2000). BoxIter::within keeps offsets relative to the outer slice,
and allow_truncated yields a cut last box. The fuzz test walks every
box of each mutated file.

* media: matroska feature from FilmCraft's filmcraft-matroska

Port storytold/filmcraft@6c3948a crates/matroska as craft_media::matroska
behind the opt-in feature matroska, with the same public API so FilmCraft
can make filmcraft-matroska a re-export. Its unit and ffprobe oracle tests
come with it, on craft-media's test helpers.

From DeckCraft's copy: cluster and block timestamps no longer overflow;
they saturate at i64::MAX. MuxingApp and the default WritingApp name
craft-media. New tests: block additions and mutation fuzzing. The oracle
skips the duration of a track's last packet when ffmpeg 8 shortened it.

* media: MOV PCM uses lpcm v2 above 16.16 rates and for unsigned 16-bit; signed 8-bit as twos

Version 0 sound descriptions store the rate as 16.16 fixed point, so 88.2 kHz
and higher were written as 0 Hz. 8-bit signed PCM was labelled raw (unsigned)
and 16-bit unsigned as twos/sowt (signed). The reader also took raw under a
sound handler for uncompressed video.

* media: stop the top-level box scan at a box that runs past the end of the file

A 64-bit box size near u64::MAX saturated the scan offset, and the next
offset + 8 overflowed.

* media: one sample budget for the whole ISO BMFF file

The sample cap and the file-length bound on samples without table entries
(constant-size stsz, trun without per-sample fields) applied per track or
per trun, so repeating a small trak or trun multiplied the allocation.
Both are now shared by all tracks and fragments.

* media: set_sample_entry rejects entries that change the track's media type or PCM frame size

The handler, stsz constant size and PCM timescale check are fixed at add_track;
a replacement that changed them produced a corrupt file. A bad track index
returns NoSuchTrack like the other writer methods.

* media: saturate the sample end in MkvFile::duration_ns

Cluster timestamps saturate at i64::MAX, so pts + duration could overflow
for files without Info/Duration.

* media: checked add for the Matroska cue cluster position

CueClusterPosition comes from the file; an add past u64::MAX panicked in
debug builds and wrapped in release. Such a cue now falls back to the index.

* media: cap the Matroska TimestampScale at 2^32 and saturate tick conversions

A scale near 2^64 overflowed u128/i128 in the per-lace split and in
Track::ticks_to_ns. A BlockDuration near u64::MAX also overflowed the tick
difference at the default scale. Larger scales are ignored like a zero scale,
and tick and nanosecond values outside i64 saturate.

* media: Matroska fixture generation no longer truncates the shared subtitle inputs

Parallel tests generating subs.mkv or webvtt.webm could read in.srt or in.vtt
while another thread rewrote it, and cache a fixture with missing packets.

* media: rustfmt parse_moof signature

* ci: add craft-media workflow covering feature sets, WASM and ffmpeg oracles

The lint job runs fmt, clippy for each feature set (none, bitstream,
isobmff, matroska, all) and a wasm32 check. The test job runs the
all-features tests with Ubuntu's ffmpeg and CRAFT_MEDIA_REQUIRE_ORACLES=1,
installing ffmpeg while the tests compile.

* media: Matroska lacing keeps timestamp gaps

A laced frame's timestamp is implied by the block's and the track's
DefaultDuration, so lacing collapsed gaps in the input. A frame now joins
a lace only when that implied tick equals its own, and tracks without a
DefaultDuration do not lace. The laced remux oracle compares timestamps.

* media: required oracles fail on fixture generator and ffprobe failures

Every path that gave up on an oracle check (missing tool, failed fixture
generator, failed ffprobe run) goes through one helper that prints
SKIPPED, or fails the test with CRAFT_MEDIA_REQUIRE_ORACLES=1. Fixture
generators of both containers share run_generator, which takes the
required flag explicitly so tests can exercise it without changing the
process environment.

* media: use US spelling

* media: a failed ffprobe probe skips the optional oracle check

* media: the Matroska oracle compares the last subtitle cue's duration
2026-10-20 09:07:12 -06:00