Skip to content

SideloadedSubtitle: Resolve playlist URIs without decoding existing percent-escapes - #134

Open
devin-ai-integration[bot] wants to merge 7 commits into
mainfrom
improvement/sideloaded-subtitles-preserve-uri-encoding
Open

devin-ai-integration[bot] wants to merge 7 commits into
mainfrom
improvement/sideloaded-subtitles-preserve-uri-encoding

Conversation

@devin-ai-integration

@devin-ai-integration devin-ai-integration Bot commented Nov 6, 2026 •

Copy link
Copy Markdown
Contributor

Summary

Builds on #132 (contains its 3 commits). #132 fixes relative URIs with a query string (video.m3u8?hdntl=... no longer becomes video.m3u8%3Fhdntl=...), but PlaylistParser.getFullURL(from:) still decodes all percent-escapes and then re-encodes the string. That step can't tell an escaped character that is data apart from a delimiter, so some valid URIs are still rewritten wrongly.

getFullURL(from:) now uses the URI as written:

func getFullURL(from path: String) -> URL? {
    let uri = path.trimmingCharacters(in: .whitespacesAndNewlines)
    guard !uri.isEmpty else { return nil }

    if let url = URL(string: uri, relativeTo: self.manifestURL) {
        return url.absoluteURL
    }

    // Before iOS 17, URL(string:) rejects invalid characters such as raw spaces.
    // Escape only those and keep existing %XX escapes and URL delimiters as written.
    var allowed = CharacterSet.urlQueryAllowed
    allowed.insert(charactersIn: "%#[]")
    guard let escaped = uri.addingPercentEncoding(withAllowedCharacters: allowed) else { return nil }
    return URL(string: escaped, relativeTo: self.manifestURL)?.absoluteURL
}

The decode/re-encode step and the encodingInvalidCharacters check were added in #37 only because appendingPathComponent escapes its input again (%20 -> %2520). This change removes appendingPathComponent, so those workarounds are no longer needed. The isEmpty guard is required: VariantPlaylistParser passes blank lines to getFullURL, and URL(string: "", relativeTo:) would resolve them to the manifest URL itself.

Playlist URI (manifest .../vod/movie/index.m3u8) main #132 this PR
video%3Falt.m3u8 (escaped ? in the filename) .../video%3Falt.m3u8 .../video?alt.m3u8 .../video%3Falt.m3u8
seg1.ts?exp=2&sig=ab%2Fcd%2B%3D%3D ? becomes %3F, signature decoded signature decoded to ab/cd+== unchanged
video.m3u8?token=a%26b%2Bc%3Dd ? becomes %3F, value decoded value decoded to a&b+c=d unchanged
proxy/https%3A%2F%2Forigin.com%2Fvideo.m3u8%3Ftoken%3Dabc embedded URL decoded embedded URL decoded, ? becomes a query unchanged
MK-...-227%20Soy%20una%20tazaUHD.m3u8 (#37) %20 kept %20 kept %20 kept

Invalid characters (raw spaces): from iOS 17, URL(string:) escapes invalid characters itself. Before iOS 17 it returns nil, so VariantPlaylistParser would drop a segment line such as seg one.ts from the rewritten playlist. When the first parse fails, getFullURL escapes only the invalid characters (existing %XX escapes and URL delimiters are kept) and tries once more. Valid URIs never reach this fallback. It checks the parse result instead of the OS version, because the iOS 17 parsing also depends on the SDK the app is linked against.

Tests

20 new tests in PlaylistParserTests, alongside the 5 from #132. All use hard-coded expected strings:

  • Unit tests for getFullURL: %20, %25 and %2520 are kept as written; escaped %3F, %26, %2B, %3D and %2F are kept; already-encoded absolute URLs are not changed; root-relative and scheme-relative URIs; ../ does not inherit the manifest's query; surrounding whitespace is trimmed; a raw space is escaped to %20; empty and whitespace-only input returns nil.
  • Playlist tests against a local Swifter server:
    • master playlist variant URIs and URI= attributes;
    • a real 307 redirect, checking that the final response URL is used as the base;
    • variant playlist segments, EXT-X-KEY and EXT-X-MAP with EXT-X-BYTERANGE between EXTINF and the segment;
    • escaped base64 signatures in segment and key URIs;
    • a segment line with a raw space is kept and escaped.

THEOplayerConnectorSideloadedSubtitleTests: 36/36 pass on the iOS 16.4 and iOS 27.0 simulators (run locally). Before the two raw-space tests were added, the #132 code passed 29/34 of the same tests and main passed 18/34.

Tests in PlaylistParserTests are grouped with MARKs by the PR whose behavior they cover (#12, #17, #20, #37, #132, #134).

Added a CHANGELOG entry under Unreleased → Fixed → SideloadedSubtitle.

Link to Devin session: https://dolby.devinenterprise.com/sessions/e338c69f5ccb4f599ef26ccbc7bd4a23
Open in Devin Desktop: https://dolby.devinenterprise.com/desktop/session/e338c69f5ccb4f599ef26ccbc7bd4a23?variant=devin

@devin-ai-integration

Copy link
Copy Markdown
Contributor Author

I'll fix CI failures and address comments from users with write access. I'll skip comments containing "(aside)".

  • Disable automatic comment, CI, and merge conflict monitoring

olejnjak and others added 5 commits November 6, 2026 07:54
getFullURL built absolute URLs for relative URIs with appendingPathComponent. That method percent-encodes '?' into '%3F', so a variant URI such as 'video.m3u8?hdntl=...' became part of the path and the server returned HTTP 400. Resolve the URI against the manifest URL with URL(string:relativeTo:) instead. Fall back to the previous behavior when that fails.
Cover relative URIs with a query string in the same directory, in a subdirectory and in a parent directory, a relative URI without a query, and an absolute URI. A relative URI without its own query does not get the playlist's query, as RFC 3986 section 5.2.2 specifies.
Co-Authored-By: raffi.manissalian <raffi.manissalian@dolby.com>
Use URL(string:relativeTo:) on the trimmed URI as written instead of
removing and re-adding percent-encoding, so escaped characters such as
%3F, %26, %2B, %3D and %2F stay as data. Blank lines still return nil.
Add regression tests for encoded filenames, escaped query values,
redirects and playlist URI rewriting.

Co-Authored-By: raffi.manissalian <raffi.manissalian@dolby.com>
Co-Authored-By: raffi.manissalian <raffi.manissalian@dolby.com>
@devin-ai-integration
devin-ai-integration Bot force-pushed the improvement/sideloaded-subtitles-preserve-uri-encoding branch from 0931ed5 to a51dec6 Compare November 6, 2026 15:54
devin-ai-integration Bot and others added 2 commits November 9, 2026 05:25
…'t be parsed

Co-Authored-By: raffi.manissalian <raffi.manissalian@dolby.com>
Co-Authored-By: raffi.manissalian <raffi.manissalian@dolby.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant