oxideav-mkv
Pure-Rust Matroska (MKV) and WebM container — demuxer + muxer built on the EBML primitives from RFC 8794. Zero C dependencies.
Part of the oxideav framework but usable standalone.
Installation
[]
= "0.1"
= "0.1"
= "0.1"
= "0.0"
Quick use
Register both containers ("matroska" and "webm") and let the probe
pick which DocType the file carries:
use ContainerRegistry;
let mut containers = new;
register;
let input: = Boxnew;
let mut dmx = containers.open_demuxer?;
for s in dmx.streams
loop
# Ok::
The demuxer returns raw Packet bytes — pair it with a decoder crate
(e.g. oxideav-opus,
oxideav-flac,
oxideav-vp9) or go through
the unified oxideav aggregator to wire decoding automatically.
What's implemented
Demuxer (demux::open)
- EBML header parse, DocType validation (
matroska/webm). - Segment walk:
Info,Tracks,Tags,Cues,Cluster. Known- and unknown-size Segment/Cluster both supported. - Clusters:
SimpleBlockandBlockGroup -> Block, all three lacing modes (Xiph, fixed, EBML-signed-delta). - Metadata lift: title, muxer, encoder, date (Matroska
DateUTC-> ISO-8601), TagsSimpleTagname/value pairs with target-scope resolution (Tags.Targets.TagTrackUID->tag:track:N:<name>,TagChapterUID->tag:chapter:N:<name>,TagAttachmentUID->tag:attachment:N:<name>,TagEditionUID->tag:edition:N:<name>; all-zero UIDs -> bare<name>global key; unresolved non-zero UIDs are dropped per RFC 9559 §5.1.8.1.1.x "MUST match"),Chapters(chapter:N:start_ms/:end_ms/:title, ns→ms), andAttachments(attachment:N:filename/:mime_type/:size_bytes; payload is skipped, only the index surfaces). - Typed
Tagaccessor:demux::open_typedreturns the concreteMkvDemuxer, whose.tags() -> &[Tag]exposes RFC 9559 §5.1.8.1 fields the flat metadata view drops —TargetType/TargetTypeValueinformational hints, multi-UIDTargetsmasters (oneTagcan scope to several tracks/chapters at once), per-SimpleTagTagLanguage/TagLanguageBCP47/TagDefault, and binaryTagBinarypayloads (e.g. embedded cover-art bytes). Tags with only dangling non-zero UIDs are filtered out per §5.1.8.1.1.3..§5.1.8.1.1.6; mixed Targets keep their resolvable UIDs. - Typed
Attachmentsaccessor (RFC 9559 §5.1.6):MkvDemuxer::attachments() -> &[Attachment]returns one [Attachment] perAttachedFileparsed from the Segment, in document order. Each entry carries the 1-basedindex(matching theattachment:N:*flat metadata keys and anytag:attachment:N:<name>Tag scope),filename(FileName, §5.1.6.2),mime_type(FileMimeType, §5.1.6.3),description(FileDescription, §5.1.6.1),uid(FileUID, §5.1.6.5), and the on-disk byte range (data_offset+data_size) of theFileDatapayload. The payload bytes are not read up front — a multi-megabyte embedded font stays on disk untilMkvDemuxer::attachment_data(index)is called, at which point exactlydata_sizebytes are read fromdata_offsetand returned; the demuxer's reader position is preserved across the fetch so calling it betweennext_packetcalls is safe. The flatmetadata()view also gains anattachment:N:descriptionkey when the source element was present. - Typed
Chaptersaccessor (RFC 9559 §5.1.7):MkvDemuxer::chapters() -> &[Edition]exposes the structured chapter tree the flatchapter:N:*metadata view collapses — everyEditionEntrykeeps itsEditionUID,EditionFlagDefaultandEditionFlagOrderedflags; everyChapterAtomkeeps itsChapterUID,ChapterStringUID(e.g. WebVTT cue id), full-precisionChapterTimeStart/ChapterTimeEndnanoseconds,ChapterFlagHidden,ChapterFlagEnabled(spec default1materialised astrue), Medium-Linking fieldsChapterSegmentUUID(raw 16 B) +ChapterSegmentEditionUID(zero suppressed per spec "range: not 0"),ChapterPhysicalEquiv(DVD/SIDE physical mapping per §20.4), all multilingualChapterDisplayrows (each withChapString,ChapLanguage+ChapLanguageBCP47,ChapCountry), theChapProcesssub-tree (RFC 9559 §5.1.7.1.4.14–19 —ChapProcessCodecID,ChapProcessPrivate, and zero or moreChapProcessCommandrows each withChapProcessTime+ rawChapProcessData; payloads surfaced verbatim, never executed), and any nested child atoms (the spec marksChapterAtomas recursive). Atoms are 1-indexed depth-first in document order — the same index the flatchapter:N:*keys andTagChapterUID-resolved tags use, now extended to nested chapters. Returns an empty slice when the file has noChapterselement. - Duration:
Segment\Info\Durationtranslated to microseconds. - Seek:
seek_to(stream, pts)uses the Cues index. Handles Cues at either end of the Segment, and walks an unknown-size final Cluster to find Cues that sit past it. CueRelativePositionhonoured on seek (RFC 9559 §5.1.5.1.2.3): when a Cues entry carries theCueRelativePositionelement,seek_toopens the target Cluster, captures itsTimestamp(RFC 9559 §5.1.3.1 — SHOULD be the first child), and then repositions the reader directly at the byte offset of the referencedSimpleBlock/BlockGroup(0being the first possible element position inside that Cluster). The next packet emitted is the cue's exact block, not the first block in the Cluster — finer seek granularity than the legacy "scan from cluster start" path, which is preserved as a fallback when the cue has noCueRelativePositionor the encoded position is out of range.- An unknown-size Cluster is terminated cleanly when a sibling Segment- child element follows it (no more "Cues silently eaten as payload").
- CRC-32 validation (RFC 8794 §11.3.1, RFC 9559 §6.2): when a Top-Level
master element (
Info,Tracks,Tags,Cues,Chapters,Attachments,SeekHead) or aClustercarries a leadingCRC-32child, the demuxer recomputes the IEEE CRC-32 (reflected poly0xEDB88320, init0xFFFFFFFF, final XOR, little-endian storage) over the rest of the element and records the result.MkvDemuxer::crc_status() -> &[CrcStatus]exposes each{element_id, stored, computed}triple with anis_valid()helper. Up-front masters are checked at open time in segment order; Cluster checks land lazily on the firstnext_packet/seek_tothat opens each Cluster (the element id on a Cluster status isids::CLUSTER), with a body-offset dedup so a back-then-forward seek revisiting the same Cluster never produces two statuses for it. A Cluster declared with the unknown-size VINT can't be CRC-checked (the spec requires a bounded body) and produces no status. Validation is informational — a mismatch does not abort the open (RFC 8794 §12: a reader MAY ignore the data); strict callers reject any non-valid status. Elements with noCRC-32child produce no status (omission is spec-legal). TrackOperationtyped decode (RFC 9559 §5.1.4.1.30): a virtual track assembled from other tracks.MkvDemuxer::track_operation(stream_index)(and the per-streamtrack_operations()slice) returns a typedTrackOperationfor anyTrackEntrycarrying the element,Nonefor an ordinary track.TrackCombinePlanes(§5.1.4.1.30.1) surfaces as aVec<TrackPlane>— each pairs a referenced track with itsTrackPlaneType(LeftEye/RightEye/Background, withOther(u64)preserving FCFS-registry values per §27.17) — andTrackJoinBlocks(§5.1.4.1.30.5) surfaces as aVec<TrackRef>. EveryTrackPlaneUID/TrackJoinUIDis resolved back to aTrackRefcarrying both the on-diskTrackUIDand the matching 0-indexed stream index (Nonefor a dangling reference, kept rather than dropped). ATrackPlanemissing its mandatoryTrackPlaneUIDand a zeroTrackJoinUID("not 0" per spec) are dropped.ContentEncodingstyped decode (RFC 9559 §5.1.4.1.31):MkvDemuxer::content_encodings(stream_index)(and the per-streamall_content_encodings()slice) returns the track's transformation chain — compression and/or encryption applied to frame data /CodecPrivatebefore the bytes hit Blocks — as typedContentEncodings,Nonefor an ordinary track. EachContentEncodingcarries itsContentEncodingOrder,ContentEncodingScopebit field (block()/private()/next()accessors), and aContentEncodingTransformenum:Compression(ContentCompAlgo→Zlib/Bzlib/Lzo1x/HeaderStripping/Other(u64), plus theContentCompSettingsstripped bytes) orEncryption(ContentEncAlgo→None/Des/TripleDes/Twofish/Blowfish/Aes/Other(u64), theContentEncKeyID, and the nestedContentEncAESSettings→AESSettingsCipherModeasCtr/Cbc/Other(u64)). The list is pre-sorted into decode order (highestContentEncodingOrderfirst, per §5.1.4.1.31.2). Element defaults are honoured (order 0, scope 0x1 Block, type 0 compression, comp-algo 0 zlib). The headers are surfaced; zlib/bzlib/lzo1x and encryption are never decompressed or decrypted (out of container scope).- Header-Stripping applied on read (RFC 9559 §5.1.4.1.31.6 algo 3,
§5.1.4.1.31.7): Header Stripping is the one
ContentEncodingtransform the container can reverse without a codec — theContentCompSettingsbytes were removed from the front of each frame on write, so the demuxer prepends them back to every de-laced frame, andnext_packetreturns the original (un-stripped) frame data. Block scope (§5.1.4.1.31.3 bit 0x1) is honoured per-frame (the prefix lands on each laced sub-frame, not the Block once); a chain of several Header-Stripping steps is combined in decode order. If the Block-scoped chain contains any step the container can't undo (zlib/bzlib/lzo1x compression or encryption), packets pass through encoded — the demuxer never partially strips. Private-scope (CodecPrivate-only) Header Stripping leaves frame data untouched. Videogeometry quartet typed decode (RFC 9559 §5.1.4.1.28.8..§5.1.4.1.28.14):MkvDemuxer::video_geometry(stream_index)(and the per-streamvideo_geometries()slice) folds thePixelCrop{Top,Bottom,Left,Right}hide-window plus theDisplayWidth/DisplayHeight/DisplayUnitrender-size triple into a single typedVideoGeometry.DisplayUnitsurfaces as theDisplayUnitenum (Pixels/Centimeters/Inches/DisplayAspectRatio/Unknown/Other(u64)for forward-compat with the §27.9 "Matroska Display Units" registry).display_width()/display_height()returnOption<u64>: the explicit element when the file carries it, otherwise the §5.1.4.1.28.12 / §5.1.4.1.28.13 derived default (PixelWidth - PixelCropLeft - PixelCropRight/PixelHeight - PixelCropTop - PixelCropBottom) — but only whenDisplayUnit == 0(pixels), since the spec explicitly states "If the DisplayUnit of the same TrackEntry is 0, then the default value for DisplayWidth is ...; else, there is no default value". For any otherDisplayUnitan absent element resolves toNone. The PixelCrop defaults (0, §5.1.4.1.28.8..11) and DisplayUnit default (0, §5.1.4.1.28.14) are always materialised. Non-video tracks (and video tracks with noVideomaster) returnNone; a derivation that would underflow (malformed file with crops larger than the encoded width or height on the same axis) returnsNoneon that axis rather than wrapping.Video > Colourtyped decode (RFC 9559 §5.1.4.1.28.16, including §5.1.4.1.28.17..§5.1.4.1.28.40 sub-elements and the SMPTE 2086 / CTA-861.3 HDRMasteringMetadata):MkvDemuxer::video_colour(stream_index)(and the per-streamvideo_colours()slice) folds theColourmaster's children into a single typedVideoColour. Each ofMatrixCoefficients,TransferCharacteristics,Primaries,ColourRange,ChromaSitingHorzandChromaSitingVertsurfaces as a typed enum; forward-compat values outside the registered tables pass through via anOther(u64)variant (§27 leaves registries open for future additions).BitsPerChannel,ChromaSubsampling{Horz,Vert},CbSubsampling{Horz,Vert},MaxCLL/MaxFALLsurface as the raw unsigned integer (Optional when the spec doesn't define a default). The nestedMasteringMetadata(§5.1.4.1.28.30..§5.1.4.1.28.40) surfaces asOption<&MasteringMetadata>with the sixPrimary{R,G,B}Chromaticity{X,Y}floats, the twoWhitePointChromaticity{X,Y}floats and theLuminance{Max,Min}cd/m² pair — each independently optional, since the spec does not require all-or-nothing. Spec defaults are materialised on the typed surface so an emptyColourmaster decodes as fully-typed unspecified (§5.1.4.1.28.17 / .26 / .27 default2; §5.1.4.1.28.23..25 default0). Non-video tracks (and video tracks with noColourchild) returnNone.Video > StereoModetyped decode (RFC 9559 §5.1.4.1.28.3):MkvDemuxer::video_stereo_mode(stream_index) -> Option<StereoMode>(and the per-streamvideo_stereo_modes()slice) returns the single-track stereo-3D packing —Mono/SideBySide{Left,Right}First/TopBottom{Left,Right}First/Checkboard{Left,Right}First/RowInterleaved{Left,Right}First/ColumnInterleaved{Left,Right}First/Anaglyph{CyanRed,GreenMagenta}/BothEyesLaced{Left,Right}First(the full §5.1.4.1.28.3 Table 5 set) plusOther(u64)for values registered after RFC 9559 (§27.7 leaves the registry open). The §5.1.4.1.28.3 default0(Mono) is materialised: aVideomaster with no explicitStereoModedecodes asSome(StereoMode::Mono), distinguishable fromNone(which means "noVideomaster at all"). Multi-track stereo (TrackOperation > TrackCombinePlanes, §5.1.4.1.30.1) is independent and surfaces throughtrack_operation; a single track MAY carry both. A convenienceStereoMode::is_stereo()returnstruefor any non-Monopacking.Video > Projectiontyped decode (RFC 9559 §5.1.4.1.28.41, including §5.1.4.1.28.42..§5.1.4.1.28.46):MkvDemuxer::video_projection(stream_index)(and the per-streamvideo_projections()slice) folds theProjectionmaster's children into a single typedProjection.ProjectionTypesurfaces as a typed enum (Rectangular/Equirectangular/Cubemap/Mesh/Other(u64)for values registered after RFC 9559 — §27.15 leaves the registry open).ProjectionPrivate(the verbatim ISOBMFF box body —equi/cbmp/mshp— that pairs with the projection type) surfaces verbatim asOption<&[u8]>and is never parsed or validated by the container; that's a renderer concern. The yaw / pitch / roll pose triple (degrees, ranges±180 / ±90 / ±180per §5.1.4.1.28.44..46) surfaces as threef64s with the spec default0.0materialised. An emptyProjectionmaster decodes as a fully-typed identity projection (rectangular + zero pose), distinguishable fromNone(which means "noProjectionmaster at all" — the common case for ordinary 2D video). The §5.1.4.1.28.46 worked example<Projection><ProjectionPoseRoll>90</ProjectionPoseRoll></Projection>(signalling a 90° counter-clockwise rotation) round-trips withprojection_type == Rectangular,pose_roll == 90.0, and the other pose components at their defaults. Convenience helpersProjectionType::is_spherical()andProjection::is_rotated()provide the headline yes/no answers. Non-video tracks (and video tracks with noProjectionchild) returnNone.Video > AlphaModetyped decode (RFC 9559 §5.1.4.1.28.4):MkvDemuxer::video_alpha_mode(stream_index) -> Option<AlphaMode>(and the per-streamvideo_alpha_modes()slice) folds the per-track WebM-alpha hint into a typed enum (None/Present/Other(u64)for values registered after RFC 9559 — §27.8 leaves the registry open). The §5.1.4.1.28.4 default0(None) is materialised: aVideomaster with no explicitAlphaModedecodes asSome(AlphaMode::None), distinguishable fromNone(which means "noVideomaster at all").AlphaMode::Present(value1) signals that the track'sBlockAdditionalelement withBlockAddID=1carries alpha-channel data per the codec mapping forCodecID(the WebM VP8/VP9 alpha extension is the canonical user). A convenienceAlphaMode::has_alpha()returnstrueexactly for thePresentvariant — values outside Table 6 are conservatively treated as "no alpha" because the spec leaves their semantics implementation-defined.Video > AspectRatioTypetyped decode (RFC 9559 Appendix A.24, reclaimed):MkvDemuxer::video_aspect_ratio_type(stream_index) -> Option<u64>(and the per-streamvideo_aspect_ratio_types()slice) surfaces the rawu64value rather than synthesising an enum — the reclaimed appendix says only "Specifies the possible modifications to the aspect ratio" and enumerates no values. ReturnsNonewhenever the file did not carry the element (the appendix specifies no default, so absence is not materialised).Video > UncompressedFourCCtyped decode (RFC 9559 §5.1.4.1.28.15):MkvDemuxer::video_uncompressed_fourcc(stream_index) -> Option<&UncompressedFourCC>(and the per-streamvideo_uncompressed_fourccs()slice) surfaces the 4-byte FourCC that identifies the uncompressed pixel layout. Spec-mandatory only whenCodecID == "V_UNCOMPRESSED"(Table 11); the typed surface carries the verbatim on-disk bytes viaas_bytes(), plus conveniencefourcc() -> Option<[u8; 4]>andas_str() -> Option<String>(UTF-8 lossy) accessors that returnNonewhenever the on-disk payload isn't exactly 4 bytes. A malformed non-4-byte payload is preserved verbatim rather than being dropped, so callers debugging a malformed file can still see what the writer emitted. Absence on any track is legal — the spec specifies no default — and returnsNone.Video > FlagInterlaced+FieldOrdertyped decode (RFC 9559 §5.1.4.1.28.1 + §5.1.4.1.28.2):MkvDemuxer::video_interlacing(stream_index)(and the per-streamvideo_interlacings()slice) folds both elements into a typedVideoInterlacing—flag()returns aFlagInterlacedenum (Undetermined/Interlaced/Progressive/Other(u64)) andfield_order()returnsSome(FieldOrder)(Progressive/Tff/Undetermined/Bff/TffInterleaved/BffInterleaved/Other(u64)) only when the track is actually interlaced. §5.1.4.1.28.2's "If FlagInterlaced is not set to 1, this element MUST be ignored" is honoured by the typed surface: a strayFieldOrderon a progressive / undetermined track silently resolves toNone. Spec defaults materialised — bareVideomaster with noFlagInterlacedchild decodes asUndetermined(default0); an interlaced track with no explicitFieldOrderdecodes asSome(FieldOrder::Undetermined)(default2). Non-video tracks (and video tracks with noVideomaster) returnNone.
Muxer (mux::open and mux::open_webm)
- EBML header + Segment (unknown size) for a streaming-friendly layout.
- Fixed-size
SeekHeadat the start of the Segment with Seek entries forInfo,Tracks, andCues- so players that pre-walk the SeekHead (mpv, Chromium) jump straight to Cues without scanning. The CuesSeekPositionis patched inwrite_trailer; if no packets were written, the Cues entry is rewritten as a Void filler. Info(1 msTimecodeScale),Tracks, rolling ~5 sClusters withSimpleBlockpayload.Cueselement emitted inwrite_trailer- index entries for every video keyframe and every audio cluster-start, so the resulting file is seekable without a second pass. Each entry carriesCueRelativePosition(RFC 9559 §5.1.5.1.2.3, recommended by §22.1) so seek-aware readers jump straight to the indexedSimpleBlockinside the Cluster instead of scanning from the cluster header.- Codec-specific fields:
CodecPrivatenormalisation for FLAC (fLaCmagic prepended), OpusCodecDelayderived from theOpusHeadpre-skip plus an 80 msSeekPreRollper the WebM spec. Chapters(RFC 9559 §5.1.7):MkvMuxer::add_chapter(start_ns, end_ns, title)queues a single English-languageChapterAtom;add_chapter_full(MkvChapter)takes a fully-specified record with multilingualChapterDisplayrows (ChapString+ChapLanguage- optional
ChapCountry). Chapters must be added beforewrite_header; the muxer emits a singleEditionEntrybetween Tracks and the first Cluster and patches the SeekHeadChaptersslot to point at it (slot is voided if no chapters were queued).
- optional
- WebM profile:
mux::open_webmpinsDocType="webm"and rejects any stream whose codec isn't VP8/VP9/AV1 video or Vorbis/Opus audio withError::Unsupported. - Opt-in block lacing on write (RFC 9559 §5.1.4.5.5, §10.3):
MkvMuxer::with_block_lacing(LacingMode::{Xiph,Ebml,FixedSize})beforewrite_headeraggregates same-track, same-keyframe-status consecutive frames (up to 8 per Block, never crossing a cluster boundary) into a single lacedSimpleBlock. Default staysLacingMode::None(one frame per Block,FlagLacing = 0) for byte-identical back-compat. When lacing is on, the muxer writesTrackEntry.FlagLacing = 1, sets the LACING bits in the SimpleBlock flags byte to the requested mode, and encodes the per-frame size header (Xiph 255-additive octets, EBML signed-VINT deltas, or no header for fixed-size). For fixed-size mode, a frame whose size differs from the buffered run flushes the lace and starts a new one. Demuxer side already handles all three modes — the new write path completes the round-trip in-tree.
Codec ID mapping (codec_id module)
Matroska CodecID string <-> oxideav CodecId. Both directions are
implemented for roundtrip:
- Audio:
A_FLAC,A_OPUS,A_VORBIS,A_PCM/INT/LIT,A_PCM/INT/BIG,A_PCM/FLOAT/IEEE,A_AAC(+MPEG4/LC/MPEG2/LCaliases),A_MPEG/L3,A_AC3,A_EAC3. - Video:
V_VP8,V_VP9,V_AV1,V_MPEG4/ISO/AVC,V_MPEGH/ISO/HEVC,V_FFV1,V_THEORA, plusV_MS/VFW/FOURCCwith BITMAPINFOHEADER fourcc extraction (e.g.FFV1). - Subtitle:
S_TEXT/UTF8(subrip),S_TEXT/SSA,S_TEXT/ASS,S_TEXT/WEBVTT,S_TEXT/USF,S_VOBSUB(DVD),S_HDMV/PGS/S_HDMV/TEXTST(Blu-ray),S_DVBSUB,S_KATE. Subtitle tracks surface withMediaType::Subtitle; their payload bytes pass through unchanged.
Unknown MKV codec IDs fall back to a pass-through mkv:<raw-id> form
so the demuxer never hides an unrecognised track.
Probes + registration
- Registers both
"matroska"and"webm"with the container registry. - Extensions:
.mkv,.mka,.mks->matroska;.webm->webm. - Probe scoring: DocType=webm scores 100 on
probe_webmand 0 onprobe_matroska(so.mkvnever masquerades aswebm). DocType= matroska scores 100 onprobe_matroskaand 0 onprobe_webm. Files with an ambiguous DocType fall through to the matroska entry.
What's NOT implemented
- CRC-32 validation covers Top-Level master elements parsed up front and
every
Clusterthe demuxer opens throughnext_packet/seek_to; the late best-effort Cues rescan (when Cues sit after the final Cluster) is not yet checksummed, and aClusterdeclared with the unknown-size VINT produces no status (RFC 8794 §11.3.1 needs a bounded body). CRC-32 is never written on the mux side. - Attachments are never written on the mux side — the demuxer surfaces
AttachedFileentries via the typedMkvDemuxer::attachmentsaccessor and on-demandMkvDemuxer::attachment_datapayload reader (see above), but the muxer has noadd_attachmentAPI yet. TrackOperationis decoded and surfaced (left/right-eye plane combining, block joining) but the demuxer does not yet apply it — virtual tracks are reported alongside their source tracks rather than being synthesised into a single combined output stream.TrackOperationis never written on the mux side.ContentEncodingsis decoded and surfaced (compression / encryption headers). The demuxer undoes a Block-scoped Header-Stripping chain (algo 3) on read — packets carry the original frame bytes — but the generic compression algorithms (zlib / bzlib / lzo1x) and encryption are not reversed: for those a caller that wants raw codec bytes must apply the reported encoding chain itself. zlib/bzlib/lzo1x decompression and decryption are out of container scope;ContentEncodingsis never written on the mux side.Videosub-element coverage is now complete on the demux side:PixelWidth/PixelHeight(§5.1.4.1.28.6 / §5.1.4.1.28.7) feed theStreamInfodimensions;FlagInterlaced/FieldOrder(§5.1.4.1.28.1 / §5.1.4.1.28.2) surface throughvideo_interlacing; thePixelCrop{Top,Bottom,Left,Right}+DisplayWidth/DisplayHeight/DisplayUnitquartet (§5.1.4.1.28.8..§5.1.4.1.28.14) surfaces throughvideo_geometry; the fullColourmaster (§5.1.4.1.28.16) — including HDR metadata (MaxCLL/MaxFALL/MasteringMetadata) — surfaces throughvideo_colour;StereoMode(§5.1.4.1.28.3) surfaces throughvideo_stereo_mode; theProjectionmaster (§5.1.4.1.28.41) — includingProjectionType, the verbatim ISOBMFF-mirroredProjectionPrivatepayload, and the yaw / pitch / roll pose triple — surfaces throughvideo_projection;AlphaMode(§5.1.4.1.28.4) surfaces throughvideo_alpha_mode; the reclaimed Appendix-AAspectRatioTypeelement surfaces throughvideo_aspect_ratio_type; andUncompressedFourCC(§5.1.4.1.28.15) surfaces throughvideo_uncompressed_fourcc. None of theVideosub-elements above the PixelWidth/PixelHeight pair are written on the mux side.
Robustness
tests/injection_robustness.rs pins sixteen attacker-shaped byte
patterns against the open / next_packet / seek_to / attachment_data
surface: a skip helper that previously cast u64 as i64 and could
seek the reader backwards on a forged Size field; demux-open
rejection of an empty input, an EBML-magic with a truncated header, an
oversize EBML-header Size, oversize DocType / CodecID / TagString
strings, and a Segment declared size that runs past EoF; cluster-time
handling of an oversize SimpleBlock, a Xiph-laced SimpleBlock whose
declared sub-frame sizes overrun the body, and a fixed-laced
SimpleBlock with n_frames = 5 over an empty payload; on-demand
attachment_data short-read on a forged 4 GiB FileData size and a
forged 2 GiB FileName; an out-of-range CueRelativePosition in
seek_to; and an inline fuzz-corpus replay of five malformed seed
shapes. All checks land as standard cargo test targets so a regression
on any one surfaces in CI without waiting for a fuzz cycle.
Fuzzing
A cargo-fuzz harness for the demuxer lives in fuzz/. It drives
demux::open, drains up to 256 packets via next_packet, and exercises
the seek_to cluster pre-open path — over arbitrary bytes — against the
contract that no call panics, aborts, integer-overflows (in a debug
build), or attempts an attacker-controlled allocation that exceeds what
the input can back. The seed corpus in fuzz/corpus/demux/ covers a
minimal valid Matroska file, a minimal valid WebM file, an EBML-header-
only stream, and two regression inputs (one for an EBML size-overflow,
one for a zero-frame-size fixed-lacing SimpleBlock).
Run locally with a nightly toolchain:
CI runs a 30-minute fuzz cycle daily via
.github/workflows/fuzz.yml (the OxideAV org-level reusable
crate-fuzz.yml).
License
MIT - see LICENSE.