diff --git a/tests/test_sample.py b/tests/test_sample.py index 368048b2..50a2e97c 100644 --- a/tests/test_sample.py +++ b/tests/test_sample.py @@ -345,6 +345,12 @@ def test_parse_sidx_refuses_what_it_cannot_cut() -> None: assert parse_sidx(sidx_box(count=3)[:-4], 0) == (0, []) +def test_parse_sidx_refuses_a_box_that_counts_more_references_than_it_holds() -> None: + box = bytearray(sidx_box(count=3)) + box[30:32] = (5).to_bytes(2, "big") + assert parse_sidx(bytes(box) + bytes(100), 0) == (0, []) + + def test_a_single_file_dash_track_is_cut_by_its_segment_index(monkeypatch: pytest.MonkeyPatch) -> None: box = sidx_box() requested: list[str] = [] diff --git a/unshackle/core/manifests/dash.py b/unshackle/core/manifests/dash.py index 320e1a5f..f3efc2f5 100644 --- a/unshackle/core/manifests/dash.py +++ b/unshackle/core/manifests/dash.py @@ -6,7 +6,6 @@ import logging import math import re import shutil -import sys from concurrent.futures import Future from contextlib import ExitStack, closing from copy import deepcopy @@ -17,6 +16,7 @@ from urllib.parse import urljoin, urlparse from uuid import UUID from zlib import crc32 +import click import requests from langcodes import Language, tag_is_valid from lxml.etree import Element, ElementTree, XMLSyntaxError @@ -497,14 +497,13 @@ class DASH: if config.sample and segments: break + # raise, never sys.exit: this runs in a download thread, where SystemExit escapes the failure handler if not segments: - log.error("Could not find a way to get segments from this MPD manifest.") log.debug(track.url) - sys.exit(1) + raise click.ClickException("Could not find a way to get segments from this MPD manifest.") if config.sample and not segment_durations and not isinstance(track, Subtitle): - log.error("--sample cannot cut this track: the manifest gives it as one file with no segment index.") - sys.exit(1) + raise click.ClickException("--sample cannot cut this track: the server sent no segment index for it.") # TODO: Should we floor/ceil/round, or is int() ok? track.data["dash"]["timescale"] = int(segment_timescale) diff --git a/unshackle/core/manifests/hls.py b/unshackle/core/manifests/hls.py index 21974ea1..873f8cdf 100644 --- a/unshackle/core/manifests/hls.py +++ b/unshackle/core/manifests/hls.py @@ -8,7 +8,6 @@ import os import re import shutil import subprocess -import sys import time from contextlib import ExitStack, closing from itertools import accumulate @@ -18,6 +17,7 @@ from urllib.parse import urljoin, urlparse from uuid import UUID from zlib import crc32 +import click import m3u8 import requests from langcodes import Language, tag_is_valid @@ -644,14 +644,12 @@ class HLS: if not isinstance(response, (requests.Response, RnetResponse)): raise TypeError(f"Expected response to be a requests.Response or rnet.Response, not {type(response)}") if not response.ok: - log.error(f"Failed to request the invariant M3U8 playlist: {response.status_code}") - sys.exit(1) + raise click.ClickException(f"Failed to request the invariant M3U8 playlist: {response.status_code}") master = m3u8.loads(utf8_text(response), uri=track.url) if not master.segments: - log.error("Track's HLS playlist has no segments, expecting an invariant M3U8 playlist.") - sys.exit(1) + raise click.ClickException("Track's HLS playlist has no segments, expecting an invariant M3U8 playlist.") unwanted_segments = [ segment for segment in master.segments if callable(track.OnSegmentFilter) and track.OnSegmentFilter(segment) diff --git a/unshackle/core/tracks/track.py b/unshackle/core/tracks/track.py index 6e14cd32..136433f3 100644 --- a/unshackle/core/tracks/track.py +++ b/unshackle/core/tracks/track.py @@ -360,11 +360,15 @@ def parse_sidx(data: bytes, offset: int) -> tuple[int, list[tuple[int, int, int] Returns the timescale and one (start time, first byte, last byte) for each subsegment. The list is empty when the box is not a ``sidx`` or when it points at more indexes and not at media. """ + size, header = int.from_bytes(data[:4], "big"), 32 if data[8:9] == b"\x00" else 40 + # pymp4 loops forever on a box that counts more references than its size holds + if data[4:8] != b"sidx" or size < header + 12 * int.from_bytes(data[header - 2 : header], "big"): + return 0, [] try: box = Box.parse(data) except ConstructError: return 0, [] - if box.type != b"sidx" or any(reference.reference_type != "MEDIA" for reference in box.references): + if any(reference.reference_type != "MEDIA" for reference in box.references): return 0, [] start, byte = box.earliest_presentation_time, offset + box.end + box.first_offset subsegments: list[tuple[int, int, int]] = []