M3U parser: names with commas get truncated, BOM breaks the first entry, no format validation #10

Closed
opened 2026-08-26 01:54:10 +02:00 by benjamin · 1 comment
Owner

Problem

Three separate defects in M3uParser:

1. Commas in display names truncate the name. The name is taken with substringAfterLast(','), which finds the last comma in the line rather than the field separator after the duration. #EXTINF:-1,Comedy Central, HD yields HD — everything before the last comma is silently lost. Any channel named "News, Sport" or similar is affected.

2. A UTF-8 BOM corrupts the first entry. trim() strips whitespace, and U+FEFF is not whitespace; the reader does not strip it either. The first line becomes "#EXTM3U", so startsWith("#") is false and the header falls through into the "bare URL" branch, injecting a junk channel named #EXTM3U. If the BOM precedes an #EXTINF instead, that channel's metadata is swallowed and its URL becomes its own name.

3. Any text parses as a "valid" playlist. There is no #EXTM3U check and no structural validation — every non-# line becomes a channel, and the caller only asserts the result is non-empty. Point the app at a 404 HTML page and you get six channels named <!DOCTYPE html>, <html>, <body> … cached to disk as the real channel list, with no error shown. Same for an HLS media playlist, where every segment URL becomes a "channel".

Failure scenario

Someone pastes a slightly wrong URL under "Erweitert". Instead of "das sieht nicht nach einer Senderliste aus", the app cheerfully replaces the working list with HTML fragments and persists them.

Suggested fix

  • Split the #EXTINF line at the first comma after the duration field, not the last comma in the line
  • Strip a leading BOM before parsing
  • Require an #EXTM3U header (or at least one #EXTINF) and reject the payload otherwise, with a plain-language message

Found by multi-agent code review; the parser is unchanged from the reviewed version.

## Problem Three separate defects in `M3uParser`: **1. Commas in display names truncate the name.** The name is taken with `substringAfterLast(',')`, which finds the *last* comma in the line rather than the field separator after the duration. `#EXTINF:-1,Comedy Central, HD` yields `HD` — everything before the last comma is silently lost. Any channel named "News, Sport" or similar is affected. **2. A UTF-8 BOM corrupts the first entry.** `trim()` strips whitespace, and U+FEFF is not whitespace; the reader does not strip it either. The first line becomes `"#EXTM3U"`, so `startsWith("#")` is false and the header falls through into the "bare URL" branch, injecting a junk channel named `#EXTM3U`. If the BOM precedes an `#EXTINF` instead, that channel's metadata is swallowed and its URL becomes its own name. **3. Any text parses as a "valid" playlist.** There is no `#EXTM3U` check and no structural validation — every non-`#` line becomes a channel, and the caller only asserts the result is non-empty. Point the app at a 404 HTML page and you get six channels named `<!DOCTYPE html>`, `<html>`, `<body>` … cached to disk as the real channel list, with no error shown. Same for an HLS media playlist, where every segment URL becomes a "channel". ## Failure scenario Someone pastes a slightly wrong URL under "Erweitert". Instead of "das sieht nicht nach einer Senderliste aus", the app cheerfully replaces the working list with HTML fragments and persists them. ## Suggested fix - Split the `#EXTINF` line at the first comma after the duration field, not the last comma in the line - Strip a leading BOM before parsing - Require an `#EXTM3U` header (or at least one `#EXTINF`) and reject the payload otherwise, with a plain-language message Found by multi-agent code review; the parser is unchanged from the reviewed version.
Author
Owner

Fixed in dd612fc, shipped in v0.9.0 — with unit tests, since the comma logic is easy to get subtly wrong.

  • Names with commas survive: the display name is taken after the first unquoted comma following the duration field, so #EXTINF:-1 group-title="News, Sport",Das Erste HD yields Das Erste HD and the group News, Sport. Commas inside quoted attributes are ignored by the splitter.
  • BOM is stripped from the payload and from every line, so a BOM-prefixed #EXTM3U is recognised instead of becoming a junk channel.
  • Format validation: looksLikePlaylist() scans the first 50 lines for #EXTM3U or #EXTINF; without either, parse() throws NotAPlaylistException and the caller surfaces the existing playlist error instead of caching HTML fragments as channels.
  • A bare URL after #EXTGRP now inherits that group instead of dropping it.

Tests live in tests/unit/M3uParserTest.kt (Gradle points its unit-test source set there, reports go to tests/runs/, per the repo's test layout). 8 cases, all green:

[ok] keeps commas inside display names
[ok] commas inside attributes do not split the name
[ok] strips a UTF-8 BOM instead of inventing a channel
[ok] rejects HTML instead of turning it into channels
[ok] rejects an empty payload
[ok] accepts a playlist whose header is missing but has entries
[ok] falls back to the url when the name is empty
[ok] carries EXTGRP over to a bare url
Fixed in dd612fc, shipped in v0.9.0 — with unit tests, since the comma logic is easy to get subtly wrong. - **Names with commas** survive: the display name is taken after the *first unquoted* comma following the duration field, so `#EXTINF:-1 group-title="News, Sport",Das Erste HD` yields `Das Erste HD` and the group `News, Sport`. Commas inside quoted attributes are ignored by the splitter. - **BOM** is stripped from the payload and from every line, so a BOM-prefixed `#EXTM3U` is recognised instead of becoming a junk channel. - **Format validation:** `looksLikePlaylist()` scans the first 50 lines for `#EXTM3U` or `#EXTINF`; without either, `parse()` throws `NotAPlaylistException` and the caller surfaces the existing playlist error instead of caching HTML fragments as channels. - A bare URL after `#EXTGRP` now inherits that group instead of dropping it. Tests live in `tests/unit/M3uParserTest.kt` (Gradle points its unit-test source set there, reports go to `tests/runs/`, per the repo's test layout). 8 cases, all green: ``` [ok] keeps commas inside display names [ok] commas inside attributes do not split the name [ok] strips a UTF-8 BOM instead of inventing a channel [ok] rejects HTML instead of turning it into channels [ok] rejects an empty payload [ok] accepts a playlist whose header is missing but has entries [ok] falls back to the url when the name is empty [ok] carries EXTGRP over to a bare url ```
Sign in to join this conversation.
1 Participants
Notifications
Due Date
No due date set.
Dependencies

No dependencies set.

Reference: be-nj/castarr#10