mirror of
https://github.com/4gray/iptvnator.git
synced 2026-10-10 18:36:15 -08:00
fix(playlist): stop URL spans at inline delimiters, IPv6-safe port check, linear span scan
- '|' ends a URL span: it is the inline field delimiter of one-line handouts
('Server: http://host|User: alice'), and letting the span run through it
masked the next label and dropped the account entirely.
- The explicit-port check reads parsed.port instead of matching colons in the
authority, so an IPv6 endpoint no longer mistakes its address for a port
and now receives the separately labeled one.
- The out-of-span label search walks the ordered spans with a forward cursor
instead of rescanning them per match; detection runs on every keystroke and
the rescan was quadratic on pastes full of host-shaped query keys.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01JE1De2dfyRZMYh4H2iGXb4
This commit is contained in:
3 files changed
+54
-11
No files matched your search
@@ -315,6 +315,38 @@ describe('detectProviderImportCandidates', () => {
|
||||
expect(xtream[0].serverUrl).toBe('http://panel.example.io:8080');
|
||||
});
|
||||
|
||||
it('reads an inline one-line handout with pipe delimiters', () => {
|
||||
// No space after the pipe: the URL must not swallow the next
|
||||
// label, or the account silently produces no candidate at all.
|
||||
const candidates = detectProviderImportCandidates(
|
||||
'Server: http://panel.example.org|User: alice|Pass: s3cret'
|
||||
);
|
||||
|
||||
const xtream = only(candidates, 'xtream');
|
||||
expect(xtream).toHaveLength(1);
|
||||
expect(xtream[0]).toMatchObject({
|
||||
serverUrl: 'http://panel.example.org',
|
||||
username: 'alice',
|
||||
password: 's3cret',
|
||||
});
|
||||
});
|
||||
|
||||
it('applies a labeled port to an IPv6 endpoint', () => {
|
||||
// The address is full of colons; none of them is a port.
|
||||
const candidates = detectProviderImportCandidates(
|
||||
[
|
||||
'Server: http://[2001:db8::1]/player_api.php',
|
||||
'Port: 8080',
|
||||
'User: alice',
|
||||
'Pass: s3cret',
|
||||
].join('\n')
|
||||
);
|
||||
|
||||
const xtream = only(candidates, 'xtream');
|
||||
expect(xtream).toHaveLength(1);
|
||||
expect(xtream[0].serverUrl).toBe('http://[2001:db8::1]:8080');
|
||||
});
|
||||
|
||||
it('ignores host-shaped query keys inside unrelated links', () => {
|
||||
// `?url=guide` is a query key, not a label — the real "Server:"
|
||||
// line below it must still be the one the credentials attach to.
|
||||
|
||||
@@ -321,7 +321,9 @@ function completeWithLabeledPort(
|
||||
url: DetectedUrl,
|
||||
port: string | undefined
|
||||
): string {
|
||||
if (!port || /^https?:\/\/[^/]*:\d+/i.test(url.raw)) {
|
||||
// `parsed.port` rather than a colon in the authority: an IPv6 literal
|
||||
// (`http://[2001:db8::1]/…`) is full of colons that are address, not port.
|
||||
if (!port || url.parsed.port !== '') {
|
||||
return url.raw;
|
||||
}
|
||||
const { protocol, hostname, pathname, search } = url.parsed;
|
||||
|
||||
@@ -31,7 +31,11 @@ export interface LabeledFields {
|
||||
signature2?: string;
|
||||
}
|
||||
|
||||
const URL_PATTERN = /https?:\/\/[^\s<>"'`]+/gi;
|
||||
// `|` ends a URL: it is not a legal URL character, but it IS the inline field
|
||||
// delimiter of one-line handouts ("Server: http://host|User: alice"). Letting
|
||||
// the span run through it would mask the very next label and lose that
|
||||
// account entirely.
|
||||
const URL_PATTERN = /https?:\/\/[^\s<>"'`|]+/gi;
|
||||
const TRAILING_PUNCTUATION = /[),.;:!?\]»›]+$/;
|
||||
const M3U_PATH_PATTERN = /\.m3u8?$/i;
|
||||
const XTREAM_API_PATH_PATTERN = /\/(?:get|player_api|panel_api)\.php$/i;
|
||||
@@ -478,14 +482,14 @@ function maskSpans(text: string, spans: Array<[number, number]>): string {
|
||||
return chars.join('');
|
||||
}
|
||||
|
||||
function isInsideSpan(
|
||||
spans: Array<[number, number]>,
|
||||
index: number
|
||||
): boolean {
|
||||
return spans.some(([start, end]) => index > start && index < end);
|
||||
}
|
||||
|
||||
/** First match of `pattern` whose label does not start inside a URL span. */
|
||||
/**
|
||||
* First match of `pattern` whose label does not start inside a URL span.
|
||||
*
|
||||
* Both sequences are produced left to right and the spans never overlap, so
|
||||
* a single forward cursor is enough — rescanning the span list per match
|
||||
* would be quadratic on a paste full of host-shaped query keys, and this
|
||||
* runs synchronously on every keystroke.
|
||||
*/
|
||||
function firstMatchOutsideSpans(
|
||||
pattern: RegExp,
|
||||
text: string,
|
||||
@@ -496,9 +500,14 @@ function firstMatchOutsideSpans(
|
||||
? pattern
|
||||
: new RegExp(pattern.source, `${pattern.flags}g`)
|
||||
);
|
||||
let cursor = 0;
|
||||
let match: RegExpExecArray | null;
|
||||
while ((match = matcher.exec(text)) !== null) {
|
||||
if (!isInsideSpan(spans, match.index)) {
|
||||
while (cursor < spans.length && spans[cursor][1] <= match.index) {
|
||||
cursor += 1;
|
||||
}
|
||||
const span = spans[cursor];
|
||||
if (!span || !(match.index > span[0] && match.index < span[1])) {
|
||||
return match;
|
||||
}
|
||||
if (match[0] === '') {
|
||||
|
||||
Reference in new issue
Block a user