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
This commit is contained in:
2026-07-31 17:57:01 -07:00
parent e64ff1a0ee
commit f5e98dfa9d
19 changed files with 567 additions and 81 deletions
+11
View File
@@ -57,6 +57,17 @@ test('the escape length counts the full header string including separators', ()
assert.ok(options.includes(`%${fields.length}%${fields}`));
});
test('the escape length is counted in utf-8 bytes, not js string units', () => {
// An extension may put a non-ASCII value in a header; mpv reads %n% as a
// byte count, so counting string units would truncate the value.
const options = buildLoadfileOptions({
stream: stream({ headers: { 'X-Title': '日本語' } }),
});
const fields = 'X-Title: 日本語';
assert.ok(options.includes(`%${Buffer.byteLength(fields, 'utf8')}%${fields}`));
assert.ok(!options.includes(`%${fields.length}%`));
});
test('no header option is emitted when the stream carries no headers', () => {
const options = buildLoadfileOptions({ stream: stream() });
assert.ok(!options.includes('http-header-fields'));