Files
SubMiner/src/anime-bridge/headers.ts
T
sudacode f5e98dfa9d fix(anime): harden the extension bridge against untrusted repos and hangs
Addresses CodeRabbit review feedback on the Anime Browser:

- reject repository package/apk names that are not plain identifiers, and
  verify the install target resolves inside the extensions directory
- count mpv's %n% option escape in UTF-8 bytes, and escape backslashes in
  header values so a trailing one cannot eat the list separator
- key the bridge extension-id cache by APK content, so an in-place upgrade
  re-uploads instead of running the previous build
- bound every bridge, release-listing, and download request with a timeout
- enforce the APK size limit while streaming rather than after buffering
- read APK bytes on demand instead of holding a base64 copy per extension
  for the lifetime of the browser
- serialize preference mutations and write the file atomically
- handle the sidecar spawn error event, and wait for the child to exit in
  stop() before returning
- report a failed Anime Browser bootstrap instead of showing the starting
  banner forever
- keep the preferences panel's save confirmation and in-flight multi-select
  edits by re-rendering only on a structural schema change
2026-07-31 17:57:01 -07:00

57 lines
2.1 KiB
TypeScript

import type { BridgeVideo, OkHttpHeaders, ResolvedStream } from './types';
/**
* Flatten OkHttp's alternating `[name, value, name, value]` array into a map.
* A trailing name with no value is dropped rather than mapped to undefined.
*/
export function parseOkHttpHeaders(headers: OkHttpHeaders | undefined): Record<string, string> {
const flat = headers?.['namesAndValues$okhttp'];
if (!Array.isArray(flat)) return {};
const parsed: Record<string, string> = {};
for (let i = 0; i + 1 < flat.length; i += 2) {
const name = flat[i];
const value = flat[i + 1];
if (typeof name === 'string' && typeof value === 'string') parsed[name] = value;
}
return parsed;
}
/**
* Render headers as mpv's `--http-header-fields` string list. mpv splits
* entries on commas, so commas inside a value must be escaped — and the
* backslash that does the escaping has to be escaped first, or a value ending
* in `\` would neutralise the separator and swallow the next header.
*/
export function toMpvHeaderFields(headers: Record<string, string>): string {
return Object.entries(headers)
.map(([name, value]) => `${name}: ${value.replace(/\\/g, '\\\\').replace(/,/g, '\\,')}`)
.join(',');
}
function normalizeTracks(
tracks: Array<{ url?: string; lang?: string }> | undefined,
): Array<{ url: string; lang: string }> {
if (!Array.isArray(tracks)) return [];
return tracks
.filter((track): track is { url: string; lang?: string } => typeof track.url === 'string')
.map((track) => ({ url: track.url, lang: track.lang ?? '' }));
}
/**
* Normalize a bridge video into a playable stream. Returns null when the
* extension produced no `videoUrl`, which happens for entries it failed to
* resolve.
*/
export function resolveStream(video: BridgeVideo): ResolvedStream | null {
if (typeof video.videoUrl !== 'string' || video.videoUrl.length === 0) return null;
return {
url: video.videoUrl,
quality: video.quality ?? '',
headers: parseOkHttpHeaders(video.headers),
subtitles: normalizeTracks(video.subtitleTracks),
audios: normalizeTracks(video.audioTracks),
};
}