From 8397813243b849d9afaed8faae16df39ff5aaae5 Mon Sep 17 00:00:00 2001 From: gvb26 Date: Thu, 4 Sep 2025 17:59:29 -0400 Subject: [PATCH 1/2] Squashed commits to the valid verified email to pass CLA --- .../HLSPlaylistStructure.swift | 42 +++++++++++++++---- mambaTests/HLSMediaSpanTests.swift | 42 +++++++++++++++++++ 2 files changed, 77 insertions(+), 7 deletions(-) diff --git a/mambaSharedFramework/HLS Models/Playlist Structure/HLSPlaylistStructure.swift b/mambaSharedFramework/HLS Models/Playlist Structure/HLSPlaylistStructure.swift index b433e5f..1f61eb5 100644 --- a/mambaSharedFramework/HLS Models/Playlist Structure/HLSPlaylistStructure.swift +++ b/mambaSharedFramework/HLS Models/Playlist Structure/HLSPlaylistStructure.swift @@ -244,9 +244,16 @@ final class HLSPlaylistStructure: HLSPlaylistStructureInterface { do { let result = try HLSPlaylistStructureConstructor.generateMediaGroups(fromTags: _tags) - let mediaSpans = try HLSPlaylistStructureConstructor.generateMediaSpans(fromTags: _tags, - header: result.header, - mediaSegmentGroups: result.mediaSegmentGroups) + let mediaSpans: [TagSpan] + do { + mediaSpans = try HLSPlaylistStructureConstructor.generateMediaSpans( + fromTags: _tags, + header: result.header, + mediaSegmentGroups: result.mediaSegmentGroups + ) + } catch { + mediaSpans = [] + } self._header = result.header self._mediaSegmentGroups = result.mediaSegmentGroups @@ -508,6 +515,11 @@ fileprivate struct HLSPlaylistStructureConstructor { header: TagGroup?, mediaSegmentGroups: [MediaSegmentTagGroup]) throws -> [TagSpan] { + // If the playlist contains no segments then there are no spans + if mediaSegmentGroups.isEmpty { + return [] + } + var mediaSpans = [TagSpan]() // handle our only known spannable tag, `EXT-X-KEY` @@ -546,7 +558,13 @@ fileprivate struct HLSPlaylistStructureConstructor { if let startKeyIndex = startKeyIndex, let startKeyTag = startKeyTag { // we are closing out our last key - mediaSpans.append(TagSpan(parentTag: startKeyTag, tagMediaSpan: startKeyIndex...currentIndex - 1)) + let spanEnd = currentIndex - 1 + if startKeyIndex <= spanEnd { + mediaSpans.append(TagSpan(parentTag: startKeyTag, tagMediaSpan: startKeyIndex...spanEnd)) + } else { + assertionFailure("Invalid media span range: \(startKeyIndex)...\(spanEnd)") + throw ParseError.invalidMediaSpanRange(start: startKeyIndex, end: spanEnd) + } } startKeyIndex = currentIndex @@ -559,16 +577,26 @@ fileprivate struct HLSPlaylistStructureConstructor { // close out our last tag if let startKeyIndex = startKeyIndex, let startKeyTag = startKeyTag { - mediaSpans.append(TagSpan(parentTag: startKeyTag, tagMediaSpan: startKeyIndex...(currentIndex - 1))) + let spanEnd = currentIndex - 1 + if startKeyIndex <= spanEnd { + mediaSpans.append(TagSpan(parentTag: startKeyTag, tagMediaSpan: startKeyIndex...spanEnd)) + } else { + assertionFailure("Invalid final media span range: \(startKeyIndex)...\(spanEnd)") + throw ParseError.invalidMediaSpanRange(start: startKeyIndex, end: spanEnd) + } + } + + // instead of assert, warn softly if key counts mismatch (footer keys or malformed playlists) + if keyCount != keyTags.count { + print("Warning: generateMediaSpans counted \(keyCount) EXT-X-KEY tags, but found \(keyTags.count). Possibly due to footer-only key tags.") } - - assert(keyCount == keyTags.count, "we missed a key tag") return mediaSpans } private enum ParseError: Error { case foundMediaSegmentWithoutDuration(inMediaSequence: MediaSequence) + case invalidMediaSpanRange(start: Int, end: Int) } } diff --git a/mambaTests/HLSMediaSpanTests.swift b/mambaTests/HLSMediaSpanTests.swift index 3de33f9..521c4dd 100644 --- a/mambaTests/HLSMediaSpanTests.swift +++ b/mambaTests/HLSMediaSpanTests.swift @@ -114,4 +114,46 @@ class HLSMediaSpanTests: XCTestCase { let hlsString = hlsArray.joined() runTest(hlsString: hlsString, expectedSpans: [0...1, 2...4, 5...8]) } + + // This validates the early return logic in generateMediaSpans() for empty mediaSegmentGroups. + func testNoMediaSegmentsScenario() { + let hlsArray = [ + "#EXTM3U\n", + "#EXT-X-TARGETDURATION:6\n", + "#EXT-X-VERSION:3\n", + "#EXT-X-MEDIA-SEQUENCE:0\n", + "#EXT-X-PLAYLIST-TYPE:VOD\n", + "#EXT-X-KEY:METHOD=NONE\n", + "#EXT-X-MAP:URI=\"test.mp4\",BYTERANGE=\"610@0\"\n" + ] + let hlsString = hlsArray.joined() + runTest(hlsString: hlsString, expectedSpans: []) + } + + // Covers an edge case crash where an EXT-X-KEY appears in the header, but the playlist has no media segments. + func testKeyInHeaderWithNoMediaSegmentsDoesNotCrash() { + let hlsArray = [ + "#EXTM3U\n", + "#EXT-X-VERSION:3\n", + "#EXT-X-KEY:METHOD=AES-128,URI=\"enc.key\"\n", + "#EXT-X-ENDLIST\n" + ] + let hlsString = hlsArray.joined() + runTest(hlsString: hlsString, expectedSpans: []) + } + + // Validates that an EXT-X-KEY tag appearing after the last media segment (in the footer) does not crash generateMediaSpans() or create invalid spans. This seems to occur with DAI + func testFooterOnlyKeyDoesNotCrashOrAppend() { + let hlsArray = [ + "#EXTM3U\n", + "#EXT-X-VERSION:3\n", + "#EXT-X-TARGETDURATION:6\n", + "#EXTINF:6.0,\n", + "segment1.ts\n", + "#EXT-X-KEY:METHOD=AES-128,URI=\"footer.key\"\n", + "#EXT-X-ENDLIST\n" + ] + let hlsString = hlsArray.joined() + runTest(hlsString: hlsString, expectedSpans: []) + } } From 70efb9f0362455bf04e698a7680074a0ea0ec4d1 Mon Sep 17 00:00:00 2001 From: Gaurang Bham Date: Fri, 5 Sep 2025 11:18:44 -0400 Subject: [PATCH 2/2] Addressed Ray's feedback --- .../HLSPlaylistStructure.swift | 19 ++++----- mambaTests/HLSMediaSpanTests.swift | 42 +++++++++++-------- 2 files changed, 31 insertions(+), 30 deletions(-) diff --git a/mambaSharedFramework/HLS Models/Playlist Structure/HLSPlaylistStructure.swift b/mambaSharedFramework/HLS Models/Playlist Structure/HLSPlaylistStructure.swift index 1f61eb5..0bdc0c8 100644 --- a/mambaSharedFramework/HLS Models/Playlist Structure/HLSPlaylistStructure.swift +++ b/mambaSharedFramework/HLS Models/Playlist Structure/HLSPlaylistStructure.swift @@ -244,16 +244,11 @@ final class HLSPlaylistStructure: HLSPlaylistStructureInterface { do { let result = try HLSPlaylistStructureConstructor.generateMediaGroups(fromTags: _tags) - let mediaSpans: [TagSpan] - do { - mediaSpans = try HLSPlaylistStructureConstructor.generateMediaSpans( - fromTags: _tags, - header: result.header, - mediaSegmentGroups: result.mediaSegmentGroups - ) - } catch { - mediaSpans = [] - } + let mediaSpans = (try? HLSPlaylistStructureConstructor.generateMediaSpans( + fromTags: _tags, + header: result.header, + mediaSegmentGroups: result.mediaSegmentGroups + )) ?? [] self._header = result.header self._mediaSegmentGroups = result.mediaSegmentGroups @@ -586,9 +581,9 @@ fileprivate struct HLSPlaylistStructureConstructor { } } - // instead of assert, warn softly if key counts mismatch (footer keys or malformed playlists) + // assert if key counts mismatch (footer keys or malformed playlists) if keyCount != keyTags.count { - print("Warning: generateMediaSpans counted \(keyCount) EXT-X-KEY tags, but found \(keyTags.count). Possibly due to footer-only key tags.") + assert(keyCount == keyTags.count, "Warning: generateMediaSpans counted \(keyCount) EXT-X-KEY tags, but found \(keyTags.count). Possibly due to footer-only key tags.") } return mediaSpans diff --git a/mambaTests/HLSMediaSpanTests.swift b/mambaTests/HLSMediaSpanTests.swift index 521c4dd..593733c 100644 --- a/mambaTests/HLSMediaSpanTests.swift +++ b/mambaTests/HLSMediaSpanTests.swift @@ -118,13 +118,15 @@ class HLSMediaSpanTests: XCTestCase { // This validates the early return logic in generateMediaSpans() for empty mediaSegmentGroups. func testNoMediaSegmentsScenario() { let hlsArray = [ - "#EXTM3U\n", - "#EXT-X-TARGETDURATION:6\n", - "#EXT-X-VERSION:3\n", - "#EXT-X-MEDIA-SEQUENCE:0\n", - "#EXT-X-PLAYLIST-TYPE:VOD\n", - "#EXT-X-KEY:METHOD=NONE\n", - "#EXT-X-MAP:URI=\"test.mp4\",BYTERANGE=\"610@0\"\n" + """ + #EXTM3U\n, + #EXT-X-TARGETDURATION:6\n, + #EXT-X-VERSION:3\n, + #EXT-X-MEDIA-SEQUENCE:0\n, + #EXT-X-PLAYLIST-TYPE:VOD\n, + #EXT-X-KEY:METHOD=NONE\n, + #EXT-X-MAP:URI=\"test.mp4\",BYTERANGE=\"610@0\"\n + """ ] let hlsString = hlsArray.joined() runTest(hlsString: hlsString, expectedSpans: []) @@ -133,10 +135,12 @@ class HLSMediaSpanTests: XCTestCase { // Covers an edge case crash where an EXT-X-KEY appears in the header, but the playlist has no media segments. func testKeyInHeaderWithNoMediaSegmentsDoesNotCrash() { let hlsArray = [ - "#EXTM3U\n", - "#EXT-X-VERSION:3\n", - "#EXT-X-KEY:METHOD=AES-128,URI=\"enc.key\"\n", - "#EXT-X-ENDLIST\n" + """ + #EXTM3U\n, + #EXT-X-VERSION:3\n, + #EXT-X-KEY:METHOD=AES-128,URI=\"enc.key\"\n, + #EXT-X-ENDLIST\n + """ ] let hlsString = hlsArray.joined() runTest(hlsString: hlsString, expectedSpans: []) @@ -145,13 +149,15 @@ class HLSMediaSpanTests: XCTestCase { // Validates that an EXT-X-KEY tag appearing after the last media segment (in the footer) does not crash generateMediaSpans() or create invalid spans. This seems to occur with DAI func testFooterOnlyKeyDoesNotCrashOrAppend() { let hlsArray = [ - "#EXTM3U\n", - "#EXT-X-VERSION:3\n", - "#EXT-X-TARGETDURATION:6\n", - "#EXTINF:6.0,\n", - "segment1.ts\n", - "#EXT-X-KEY:METHOD=AES-128,URI=\"footer.key\"\n", - "#EXT-X-ENDLIST\n" + """ + #EXTM3U\n, + #EXT-X-VERSION:3\n, + #EXT-X-TARGETDURATION:6\n, + #EXTINF:6.0,\n, + segment1.ts\n, + #EXT-X-KEY:METHOD=AES-128,URI=\"footer.key\"\n, + #EXT-X-ENDLIST\n + """ ] let hlsString = hlsArray.joined() runTest(hlsString: hlsString, expectedSpans: [])