CNA-BUG-008: Load<Song> and Load<Video> resolve a same-named .cnj file and return a media object that points at JSON

CNA snapshot 009d40f5  ·  Known Issues › Current bugs  ·  source links pinned to 009d40f5

✓

Evidence basis: source-verified at the pinned commit; executed for this entry (the Evidence section names exactly what was run); tests exist (not executed for this page). Claims on this page were checked by reading the CNA source at commit 009d40f5; unless a sentence says otherwise, nothing here was built or executed. Nothing on this page was executed unless the Evidence section says so.

ResolveAssetPath tries name.cnj before the Song and Video readers' media extensions, and neither reader handles .cnj, so a same-named .cnj silently becomes the Song or Video file.

Identifier
CNA-BUG-008
Category
Bug
Subsystem
Content & XNB/CNB/CNJ
Status
Open
Verified against
CNA 009d40f5 (009d40f5dd085c4e674d3479675fac84b12b3e0a)
Severity
Medium (a triage suggestion, not a project priority)
Evidence basis
Reproduced: executed for this entry (the Evidence section names exactly what was run)
Tests touching this area
Yes: see Current tests
Affected contract
ContentManager::Load for Media::Song and Media::Video (the loose-file tier)

Expected behaviour

The .cnj-first rule (a sidecar has the final say over a native file of the same name) is meant for readers that understand a .cnj envelope. Load<Song> and Load<Video> should either skip a .cnj candidate and reach the media file, or refuse it with a ContentLoadException naming the .cnj. They must never hand back a Song or Video whose file is a JSON document: XNA's contract for Load<T> is a usable asset or a ContentLoadException.

Actual behaviour at TARGET

After the compiled tiers miss, ResolveAssetPath (ContentManager.hpp) returns the literal path if it exists, then name.cnj if it exists, and only then tries the reader's GetExtensions(). SongTypeReader::Read and VideoTypeReader::Read (ContentManager.cpp) have no .cnj branch: they construct Media::Song(path, stem) and Media::Video(path, device) from whatever path they are given. Song::Song (Song.cpp) only checks that the file exists. Video::Video (Video.cpp) checks existence, requires a video decoder, and when VideoDecoder::Open fails on the JSON it silently leaves width, height and duration at zero.

So with Content/theme.cnj and Content/theme.ogg side by side, Load<Song>("theme") succeeds and returns a Song whose handle is theme.cnj; the failure appears only at MediaPlayer::Play, far from its cause. Load<Video> returns a 0×0, zero-length Video in a build with FFmpeg (in a decoder-free build it fails anyway, with a wrapped NotSupportedException). Texture2D, TextureCube and SoundEffect are not affected: their readers branch on a .cnj path.

Source locations

Evidence

Checked by reading at 009d40f5; the Song half was also executed (see below), the Video half was not. No Song or Video .cnj envelope type exists in the content module (the only Song and Video formats are the .xnb readers and the .cnb media codec), so no valid .cnj can be meant for these two readers. An earlier review described this defect as narrowed because the .xnb and .cnb tiers take precedence; they do, but they were never part of it: the compiled tiers run first, so the misresolution is confined to the loose tier. What MediaPlayer::Play reports for a JSON file depends on the audio backend's decoder and was not checked.

Independent re-verification: Reproduced for Song by the audit at 009d40f5; the Video half was checked by reading only. The TARGET ContentManager and Song sources were compiled with g++ -O0 (headless renderer macro, headless platform installed) into a probe with Content/theme.ogg (empty) and Content/theme.cnj (a small JSON envelope) side by side: Load<Song>("theme") returned a Song whose handle ended in theme.cnj without throwing, while Load<Song>("theme.ogg") returned the .ogg. The sharp-runtime types came from a sibling checkout that TARGET does not pin. No Song or Video .cnj envelope type exists in the content module, and cnj.md, which states that the resolver tries .cnj for every registered type, lists no Song or Video row in its per-reader sourceFile matrix. What MediaPlayer::Play reports for a JSON file depends on the audio backend's decoder and was not checked.

Focused reproduction

Illustrative; the Song half was executed by the audit (see Evidence).

// Content/theme.ogg is a real song; Content/theme.cnj is any .cnj document,
// for example a game-defined data record loaded elsewhere with RegisterCnjLoader.
ContentManager content(&services, "Content");
Media::Song song = content.Load<Media::Song>("theme");  // succeeds
// song.getHandle() ends in "theme.cnj", not "theme.ogg"
Media::MediaPlayer::Play(&song);                         // fails here instead

Current tests

CnjResolverOrderTests.cpp pins the .cnj-first rule, but only for Texture2D, whose reader understands .cnj. ContentManagerSongXnbTests.cpp and ContentManagerVideoXnbTests.cpp cover the .xnb tier. No test puts a .cnj beside a media file.

Regression test

A CnjResolverOrderTests case that writes theme.cnj (any valid envelope) and a short theme.wav, then requires Load<Song>("theme").getHandle() to end in .wav, or the load to throw ContentLoadException, whichever the fix chooses. Song construction needs no audio device, so the test runs headless. The Video twin needs an FFmpeg build and would check getFileNameProperty().

Blast radius

Only Load<Song> and Load<Video> on the loose tier, and only when a same-named .cnj exists in the same directory. Compiled .xnb/.cnb songs and videos, and every other built-in type, are unaffected.

Workaround

Give .cnj files names that no song or video shares, or load media with an explicit extension (Load<Song>("theme.ogg")), which the literal-path step resolves first.

The same subject is explained at several altitudes. These are the neighbouring pages at each one.

Known issues
Bug index