fix(network): harden native proxy discovery and FFmpeg provider matching

- discover active macOS dynamic store proxies via scutil before networksetup

- treat missing registry keys and unavailable probe tools as clean absence instead of hard error

- support two-part versions and tag-exact asset patterns in BtbN FFmpeg updates

- normalize BtbN n-prefixed FFmpeg versions in sidecar status checks
This commit is contained in:
NimBold
2026-09-05 08:41:10 +03:30
parent d329a5ad91
commit bbfc6e9eb1
4 changed files with 348 additions and 63 deletions
+8 -3
View File
@@ -177,14 +177,14 @@ function escapeRegExp(value) {
}
async function latestBtbnFfmpegStableBuild(stableVersion) {
if (!/^\d+\.\d+\.\d+$/.test(stableVersion)) {
if (!/^\d+\.\d+(?:\.\d+)?$/.test(stableVersion)) {
throw new Error(`unsupported FFmpeg stable version: ${stableVersion}`);
}
const stableSeries = stableVersion.split('.').slice(0, 2).join('.');
const versionPattern = escapeRegExp(stableVersion);
const seriesPattern = escapeRegExp(stableSeries);
const assetPattern = new RegExp(
`^ffmpeg-n(${versionPattern}-\\d+-g[0-9a-f]+)-(win64|linux64)-gpl-${seriesPattern}\\.(?:zip|tar\\.xz)$`
`^ffmpeg-n(${versionPattern}(?:-\\d+-g[0-9a-f]+)?)-(win64|linux64)-gpl-${seriesPattern}\\.(?:zip|tar\\.xz)$`
);
const releases = await fetchJson('https://api.github.com/repos/BtbN/FFmpeg-Builds/releases?per_page=10');
if (!Array.isArray(releases)) throw new Error('BtbN releases response is not an array');
@@ -449,7 +449,12 @@ async function main() {
async () => {
const build = await latestMartinRiedlMacArm64Release();
const stableVersion = await ffmpegStablePromise;
if (!build?.version || !build.url || !build.sha256 || build.version !== stableVersion) {
if (
!build?.version ||
!build.url ||
!build.sha256 ||
compareVersions(build.version, stableVersion) !== 0
) {
throw new Error('Martin Riedl FFmpeg provider response has no complete matching macOS arm64 stable build');
}
return build;
+27
View File
@@ -179,6 +179,33 @@ test('selects a complete BtbN build for the current stable series', async () =>
assert.equal(result.hashes.linux, 'b'.repeat(64));
});
test('selects a complete BtbN build for a two-part stable version and tag-exact assets', async () => {
const digest = value => `sha256:${value.repeat(64)}`;
const release = {
tag_name: 'autobuild-test',
assets: [
{
name: 'ffmpeg-n9.0-win64-gpl-9.0.zip',
browser_download_url: 'https://example.test/windows-exact.zip',
digest: digest('e'),
},
{
name: 'ffmpeg-n9.0-linux64-gpl-9.0.tar.xz',
browser_download_url: 'https://example.test/linux-exact.tar.xz',
digest: digest('f'),
},
],
};
const result = await withMockFetch(
async () => new Response(JSON.stringify([release]), { status: 200 }),
() => latestBtbnFfmpegStableBuild('9.0'),
);
assert.equal(result.version, '9.0');
assert.equal(result.urls.windows, 'https://example.test/windows-exact.zip');
assert.equal(result.hashes.linux, 'f'.repeat(64));
});
test('rejects an incomplete BtbN stable target tuple', async () => {
const release = {
tag_name: 'autobuild-test',
+15
View File
@@ -4616,6 +4616,14 @@ fn parse_ffmpeg_version(output: &str) -> Option<String> {
without_url
.split('-')
.next()
.map(|version| {
if let Some(rest) = version.strip_prefix(['n', 'N']) {
if rest.chars().next().is_some_and(|c| c.is_ascii_digit()) {
return rest;
}
}
version
})
.filter(|version| !version.trim().is_empty())
.map(str::to_string)
}
@@ -17932,6 +17940,13 @@ mod tests {
assert_eq!(parse_ffmpeg_version(output), Some("9.0.1".to_string()));
}
#[test]
fn parses_btbn_ffmpeg_prefixed_version() {
let output = "ffmpeg version n9.0.1-11-ge47273f4d9 Copyright (c) 2000-2026 the FFmpeg developers";
assert_eq!(parse_ffmpeg_version(output), Some("9.0.1".to_string()));
}
#[test]
fn uses_fragment_progress_instead_of_temporary_hls_size_estimates() {
let line = format!(
+298 -60
View File
@@ -100,22 +100,7 @@ async fn native_system_proxy(runner: &dyn ProxyCommandRunner) -> Result<Option<S
#[cfg(target_os = "linux")]
async fn native_system_proxy(runner: &dyn ProxyCommandRunner) -> Result<Option<String>, String> {
let mode = runner
.stdout(
"gsettings",
&string_args(&["get", "org.gnome.system.proxy", "mode"]),
)
.await?;
if strip_gsettings_string(&mode) != "manual" {
return Ok(None);
}
for (service, scheme) in [("https", "http"), ("http", "http"), ("socks", "socks5")] {
if let Some(proxy) = linux_gsettings_proxy(runner, service, scheme).await {
return Ok(Some(proxy));
}
}
Ok(None)
linux_system_proxy(runner).await
}
#[cfg(not(any(target_os = "windows", target_os = "macos", target_os = "linux")))]
@@ -127,20 +112,28 @@ fn string_args(args: &[&str]) -> Vec<String> {
args.iter().map(|value| (*value).to_string()).collect()
}
#[cfg_attr(
all(not(target_os = "windows"), not(target_os = "linux"), not(target_os = "macos")),
allow(dead_code)
)]
fn is_probe_unavailable(error: &str) -> bool {
error.contains("is unavailable") || error.contains("exited unsuccessfully")
}
fn proxy_from_environment() -> Option<String> {
[
"HTTPS_PROXY",
"https_proxy",
"HTTP_PROXY",
"http_proxy",
"ALL_PROXY",
"all_proxy",
("HTTPS_PROXY", "http"),
("https_proxy", "http"),
("HTTP_PROXY", "http"),
("http_proxy", "http"),
("ALL_PROXY", "socks5"),
("all_proxy", "socks5"),
]
.into_iter()
.find_map(|name| {
.find_map(|(name, scheme)| {
std::env::var(name)
.ok()
.and_then(|value| normalize_proxy_address(&value, "http"))
.and_then(|value| normalize_proxy_address(&value, scheme))
})
}
@@ -180,7 +173,8 @@ fn proxy_from_host_port(host: &str, port: &str, scheme: &str) -> Option<String>
};
let port = port.trim().parse::<u16>().ok().filter(|port| *port != 0)?;
if host.is_empty()
|| host.contains(['/', '@', '?', '#'])
|| host.eq_ignore_ascii_case("(null)")
|| host.contains(['/', '@', '?', '#', '(', ')'])
|| host.chars().any(char::is_whitespace)
{
return None;
@@ -224,13 +218,64 @@ fn parse_windows_proxy_server(value: &str) -> Option<String> {
https.or(http).or(socks)
}
#[cfg(target_os = "macos")]
#[cfg_attr(not(target_os = "macos"), allow(dead_code))]
fn scutil_dict_value<'a>(output: &'a str, key: &str) -> Option<&'a str> {
for line in output.lines() {
let trimmed = line.trim();
if let Some((k, v)) = trimmed.split_once(':') {
if k.trim().eq_ignore_ascii_case(key) {
let val = v.trim();
if !val.is_empty() {
return Some(val);
}
}
}
}
None
}
#[cfg_attr(not(target_os = "macos"), allow(dead_code))]
fn parse_macos_scutil_proxy(output: &str) -> Option<String> {
for (enable_key, proxy_key, port_key, scheme) in [
("HTTPSEnable", "HTTPSProxy", "HTTPSPort", "http"),
("HTTPEnable", "HTTPProxy", "HTTPPort", "http"),
("SOCKSEnable", "SOCKSProxy", "SOCKSPort", "socks5"),
] {
if scutil_dict_value(output, enable_key) == Some("1") {
if let (Some(server), Some(port)) = (
scutil_dict_value(output, proxy_key),
scutil_dict_value(output, port_key),
) {
if let Some(proxy) = proxy_from_host_port(server, port, scheme) {
return Some(proxy);
}
}
}
}
None
}
#[cfg_attr(not(target_os = "macos"), allow(dead_code))]
async fn macos_system_proxy(runner: &dyn ProxyCommandRunner) -> Result<Option<String>, String> {
let services_output = runner
if let Ok(scutil_output) = runner.stdout("scutil", &string_args(&["--proxy"])).await {
if let Some(proxy) = parse_macos_scutil_proxy(&scutil_output) {
return Ok(Some(proxy));
}
}
let services_output = match runner
.stdout("networksetup", &string_args(&["-listallnetworkservices"]))
.await?;
.await
{
Ok(output) => output,
Err(error) => {
if is_probe_unavailable(&error) {
return Ok(None);
}
return Err(error);
}
};
let services = parse_macos_network_services(&services_output)?;
let mut successful_probe = false;
for (target, scheme) in [
("securewebproxy", "http"),
("webproxy", "http"),
@@ -239,18 +284,13 @@ async fn macos_system_proxy(runner: &dyn ProxyCommandRunner) -> Result<Option<St
for service in &services {
let args = vec![format!("-get{target}"), service.clone()];
if let Ok(output) = runner.stdout("networksetup", &args).await {
successful_probe = true;
if let Some(proxy) = parse_macos_networksetup_proxy(&output, scheme) {
return Ok(Some(proxy));
}
}
}
}
if !services.is_empty() && !successful_probe {
Err("failed to query enabled macOS proxy settings".to_string())
} else {
Ok(None)
}
Ok(None)
}
#[cfg_attr(not(target_os = "macos"), allow(dead_code))]
@@ -290,7 +330,36 @@ fn macos_networksetup_value<'a>(output: &'a str, key: &str) -> Option<&'a str> {
.filter(|value| !value.is_empty())
}
#[cfg(target_os = "linux")]
#[cfg_attr(not(target_os = "linux"), allow(dead_code))]
async fn linux_system_proxy(runner: &dyn ProxyCommandRunner) -> Result<Option<String>, String> {
let mode = match runner
.stdout(
"gsettings",
&string_args(&["get", "org.gnome.system.proxy", "mode"]),
)
.await
{
Ok(mode) => mode,
Err(error) => {
if is_probe_unavailable(&error) {
return Ok(None);
}
return Err(error);
}
};
if strip_gsettings_string(&mode) != "manual" {
return Ok(None);
}
for (service, scheme) in [("https", "http"), ("http", "http"), ("socks", "socks5")] {
if let Some(proxy) = linux_gsettings_proxy(runner, service, scheme).await {
return Ok(Some(proxy));
}
}
Ok(None)
}
#[cfg_attr(not(target_os = "linux"), allow(dead_code))]
async fn linux_gsettings_proxy(
runner: &dyn ProxyCommandRunner,
service: &str,
@@ -322,9 +391,9 @@ fn strip_gsettings_string(value: &str) -> String {
.to_string()
}
#[cfg(target_os = "windows")]
#[cfg_attr(not(target_os = "windows"), allow(dead_code))]
async fn windows_system_proxy(runner: &dyn ProxyCommandRunner) -> Result<Option<String>, String> {
let output = runner
let output = match runner
.stdout(
"reg",
&string_args(&[
@@ -334,7 +403,16 @@ async fn windows_system_proxy(runner: &dyn ProxyCommandRunner) -> Result<Option<
"ProxyEnable",
]),
)
.await?;
.await
{
Ok(output) => output,
Err(error) => {
if is_probe_unavailable(&error) {
return Ok(None);
}
return Err(error);
}
};
let enabled = registry_value(&output, "ProxyEnable")
.as_deref()
.is_some_and(windows_proxy_enabled);
@@ -342,7 +420,7 @@ async fn windows_system_proxy(runner: &dyn ProxyCommandRunner) -> Result<Option<
return Ok(None);
}
let output = runner
let output = match runner
.stdout(
"reg",
&string_args(&[
@@ -352,7 +430,16 @@ async fn windows_system_proxy(runner: &dyn ProxyCommandRunner) -> Result<Option<
"ProxyServer",
]),
)
.await?;
.await
{
Ok(output) => output,
Err(error) => {
if is_probe_unavailable(&error) {
return Ok(None);
}
return Err(error);
}
};
Ok(registry_value(&output, "ProxyServer").and_then(|value| parse_windows_proxy_server(&value)))
}
@@ -395,9 +482,9 @@ fn registry_value(output: &str, name: &str) -> Option<String> {
mod proxy_tests {
use super::{
bounded_native_system_proxy, normalize_proxy_address, parse_macos_network_services,
parse_macos_networksetup_proxy, parse_windows_proxy_server, proxy_from_host_port,
registry_value, strip_gsettings_string, windows_proxy_enabled, ProxyCommandRunner,
SystemProxyCommandRunner,
parse_macos_networksetup_proxy, parse_macos_scutil_proxy, parse_windows_proxy_server,
proxy_from_host_port, registry_value, strip_gsettings_string, windows_proxy_enabled,
ProxyCommandRunner, SystemProxyCommandRunner,
};
use std::time::Duration;
@@ -407,32 +494,57 @@ mod proxy_tests {
#[cfg(target_os = "macos")]
struct SlowProxyCommandRunner;
#[cfg(target_os = "macos")]
struct ScutilProxyCommandRunner;
#[cfg(target_os = "macos")]
#[async_trait::async_trait]
impl ProxyCommandRunner for MockProxyCommandRunner {
async fn stdout(&self, program: &str, args: &[String]) -> Result<String, String> {
assert_eq!(program, "networksetup");
match args
.iter()
.map(String::as_str)
.collect::<Vec<_>>()
.as_slice()
{
["-listallnetworkservices"] => Ok("Wi-Fi\nEthernet\n".to_string()),
["-getsecurewebproxy", "Wi-Fi"] => {
Ok("Enabled: No\nServer: ignored.example\nPort: 443\n".to_string())
}
["-getsecurewebproxy", "Ethernet"] => {
Ok("Enabled: Yes\nServer: secure.example\nPort: 8443\n".to_string())
}
["-getwebproxy", _] | ["-getsocksfirewallproxy", _] => {
panic!("lower-priority proxy was queried after HTTPS succeeded")
match program {
"scutil" => {
assert_eq!(args, &["--proxy"]);
Ok("<dictionary> {\n HTTPEnable : 0\n HTTPSEnable : 0\n}\n".to_string())
}
"networksetup" => match args
.iter()
.map(String::as_str)
.collect::<Vec<_>>()
.as_slice()
{
["-listallnetworkservices"] => Ok("Wi-Fi\nEthernet\n".to_string()),
["-getsecurewebproxy", "Wi-Fi"] => {
Ok("Enabled: No\nServer: ignored.example\nPort: 443\n".to_string())
}
["-getsecurewebproxy", "Ethernet"] => {
Ok("Enabled: Yes\nServer: secure.example\nPort: 8443\n".to_string())
}
["-getwebproxy", _] | ["-getsocksfirewallproxy", _] => {
panic!("lower-priority proxy was queried after HTTPS succeeded")
}
_ => Err("unexpected command".to_string()),
},
_ => Err("unexpected command".to_string()),
}
}
}
#[cfg(target_os = "macos")]
#[async_trait::async_trait]
impl ProxyCommandRunner for ScutilProxyCommandRunner {
async fn stdout(&self, program: &str, args: &[String]) -> Result<String, String> {
assert_eq!(program, "scutil");
assert_eq!(args, &["--proxy"]);
Ok(r#"<dictionary> {
HTTPSEnable : 1
HTTPSPort : 8443
HTTPSProxy : scutil.example
}
"#
.to_string())
}
}
#[cfg(target_os = "macos")]
#[async_trait::async_trait]
impl ProxyCommandRunner for SlowProxyCommandRunner {
@@ -594,6 +706,132 @@ HKEY_CURRENT_USER\Software\Microsoft\Windows\CurrentVersion\Internet Settings
assert!(!windows_proxy_enabled("0X0"));
assert!(windows_proxy_enabled("0X1"));
}
#[cfg(target_os = "macos")]
#[tokio::test]
async fn selects_active_scutil_proxy_before_networksetup() {
assert_eq!(
super::macos_system_proxy(&ScutilProxyCommandRunner)
.await
.unwrap()
.as_deref(),
Some("http://scutil.example:8443")
);
}
#[test]
fn parses_macos_scutil_proxy_outputs() {
let scutil_https = r#"<dictionary> {
HTTPEnable : 1
HTTPPort : 8080
HTTPProxy : 127.0.0.1
HTTPSEnable : 1
HTTPSPort : 8443
HTTPSProxy : secure.local
}
"#;
assert_eq!(
parse_macos_scutil_proxy(scutil_https).as_deref(),
Some("http://secure.local:8443")
);
let scutil_socks = r#"<dictionary> {
HTTPEnable : 0
HTTPSEnable : 0
SOCKSEnable : 1
SOCKSPort : 1080
SOCKSProxy : 127.0.0.1
}
"#;
assert_eq!(
parse_macos_scutil_proxy(scutil_socks).as_deref(),
Some("socks5://127.0.0.1:1080")
);
let scutil_ipv6 = r#"<dictionary> {
HTTPEnable : 1
HTTPPort : 8080
HTTPProxy : 2001:db8::1
}
"#;
assert_eq!(
parse_macos_scutil_proxy(scutil_ipv6).as_deref(),
Some("http://[2001:db8::1]:8080")
);
let scutil_disabled = r#"<dictionary> {
HTTPEnable : 0
HTTPSEnable : 0
SOCKSEnable : 0
}
"#;
assert_eq!(parse_macos_scutil_proxy(scutil_disabled), None);
}
struct MockWindowsMissingRegRunner;
#[async_trait::async_trait]
impl ProxyCommandRunner for MockWindowsMissingRegRunner {
async fn stdout(&self, _program: &str, _args: &[String]) -> Result<String, String> {
Err("reg exited unsuccessfully".to_string())
}
}
struct MockWindowsEnabledRegRunner;
#[async_trait::async_trait]
impl ProxyCommandRunner for MockWindowsEnabledRegRunner {
async fn stdout(&self, program: &str, args: &[String]) -> Result<String, String> {
assert_eq!(program, "reg");
if args.iter().any(|a| a == "ProxyEnable") {
Ok(" ProxyEnable REG_DWORD 0x1\n".to_string())
} else if args.iter().any(|a| a == "ProxyServer") {
Ok(" ProxyServer REG_SZ 127.0.0.1:8080\n".to_string())
} else {
Err("reg exited unsuccessfully".to_string())
}
}
}
#[tokio::test]
async fn windows_system_proxy_returns_none_when_value_is_missing() {
assert_eq!(
super::windows_system_proxy(&MockWindowsMissingRegRunner)
.await
.unwrap(),
None
);
}
#[tokio::test]
async fn windows_system_proxy_returns_configured_proxy() {
assert_eq!(
super::windows_system_proxy(&MockWindowsEnabledRegRunner)
.await
.unwrap()
.as_deref(),
Some("http://127.0.0.1:8080")
);
}
struct MockLinuxUnavailableGsettingsRunner;
#[async_trait::async_trait]
impl ProxyCommandRunner for MockLinuxUnavailableGsettingsRunner {
async fn stdout(&self, _program: &str, _args: &[String]) -> Result<String, String> {
Err("gsettings is unavailable: not found".to_string())
}
}
#[tokio::test]
async fn linux_system_proxy_returns_none_when_gsettings_is_unavailable() {
assert_eq!(
super::linux_system_proxy(&MockLinuxUnavailableGsettingsRunner)
.await
.unwrap(),
None
);
}
}
#[tauri::command]