diff --git a/crates/oakcodec/src/proxymanager.rs b/crates/oakcodec/src/proxymanager.rs index 226d351b7..a919938e1 100644 --- a/crates/oakcodec/src/proxymanager.rs +++ b/crates/oakcodec/src/proxymanager.rs @@ -320,13 +320,11 @@ impl ProxyManager { } } - // Fall back to searching the system PATH. + // Fall back to searching the system PATH (split per platform: + // `;` on Windows, `:` elsewhere — see [`split_path_env`]). if let Ok(path_env) = std::env::var("PATH") { - for dir in path_env.split(':') { - if dir.is_empty() { - continue; - } - let candidate = Path::new(dir).join("ffmpeg"); + for dir in split_path_env(&path_env) { + let candidate = Path::new(&dir).join(ffmpeg_exe_name()); if is_executable_file(&candidate) { return absolute(&candidate.to_string_lossy()); } @@ -338,13 +336,32 @@ impl ProxyManager { let mut candidates: Vec = Vec::new(); let app_path = application_path(); if !app_path.is_empty() { - candidates.push(format!("{}/ffmpeg", app_path)); + candidates.push(format!("{}/{}", app_path, ffmpeg_exe_name())); } candidates.push("/opt/homebrew/bin/ffmpeg".to_string()); candidates.push("/usr/local/bin/ffmpeg".to_string()); candidates.push("/usr/bin/ffmpeg".to_string()); candidates.push("/usr/local/bin/ffmpeg".to_string()); + // Windows-specific install locations (GUI-launched apps can also + // start with a minimal PATH there). + #[cfg(target_os = "windows")] + { + // The official Windows installer defaults to + // %LOCALAPPDATA%\Programs\ffmpeg\bin. + if let Ok(local_app_data) = std::env::var("LOCALAPPDATA") { + candidates.push(format!( + "{}/Programs/ffmpeg/bin/{}", + local_app_data, + ffmpeg_exe_name() + )); + } + // Oak's user configuration directory. + if let Ok(config_dir) = FileFunctions::new().get_configuration_location() { + candidates.push(format!("{}/{}", config_dir, ffmpeg_exe_name())); + } + } + for c in candidates { if is_executable_file(Path::new(&c)) { return absolute(&c); @@ -403,12 +420,41 @@ fn application_path() -> String { .unwrap_or_default() } -/// True when `p` is a regular file with at least one execute bit set. +/// Split a PATH-style variable into its non-empty directory entries. The +/// separator is platform-specific (`;` on Windows, `:` elsewhere); +/// `std::env::split_paths` already applies that rule natively. +fn split_path_env(raw: &str) -> Vec { + std::env::split_paths(raw) + .map(|p| p.to_string_lossy().into_owned()) + .filter(|p| !p.is_empty()) + .collect() +} + +/// The executable file name to look for: `ffmpeg.exe` on Windows, +/// `ffmpeg` everywhere else. +fn ffmpeg_exe_name() -> &'static str { + if cfg!(windows) { + "ffmpeg.exe" + } else { + "ffmpeg" + } +} + +/// True when `p` is a regular file that can be run as a program. Unix +/// checks for at least one execute bit; Windows has no permission bits, so +/// any regular file counts. fn is_executable_file(p: &Path) -> bool { - use std::os::unix::fs::PermissionsExt; - match std::fs::metadata(p) { - Ok(md) if md.is_file() => md.permissions().mode() & 0o111 != 0, - _ => false, + #[cfg(unix)] + { + use std::os::unix::fs::PermissionsExt; + match std::fs::metadata(p) { + Ok(md) if md.is_file() => md.permissions().mode() & 0o111 != 0, + _ => false, + } + } + #[cfg(windows)] + { + matches!(std::fs::metadata(p), Ok(md) if md.is_file()) } } @@ -669,6 +715,33 @@ mod tests_extra { assert!(std::path::Path::new(&found).exists(), "found: {found}"); } + #[test] + fn split_path_env_uses_platform_separator() { + // The separator is platform-dependent, so the expected input is + // gated: `:` on Unix, `;` on Windows. + #[cfg(unix)] + { + assert_eq!(split_path_env("a:b:c"), ["a", "b", "c"]); + // Empty entries are skipped. + assert_eq!(split_path_env("a::b:"), ["a", "b"]); + assert_eq!(split_path_env("solo"), ["solo"]); + } + #[cfg(windows)] + { + assert_eq!(split_path_env("a;b;c"), ["a", "b", "c"]); + assert_eq!(split_path_env("a;;b;"), ["a", "b"]); + assert_eq!(split_path_env("solo"), ["solo"]); + } + } + + #[test] + fn ffmpeg_exe_name_is_platform_specific() { + #[cfg(unix)] + assert_eq!(ffmpeg_exe_name(), "ffmpeg"); + #[cfg(windows)] + assert_eq!(ffmpeg_exe_name(), "ffmpeg.exe"); + } + #[test] fn get_proxy_state_empty_and_working() { // Empty filename -> Missing. diff --git a/crates/oakplugin/src/host.rs b/crates/oakplugin/src/host.rs index 4439779aa..3254d53de 100644 --- a/crates/oakplugin/src/host.rs +++ b/crates/oakplugin/src/host.rs @@ -71,12 +71,17 @@ const RTLD_LOCAL: c_int = 0x4; #[cfg(target_os = "linux")] const RTLD_LOCAL: c_int = 0x0; +// Windows has no dlopen/dlsym/dlclose: the POSIX FFI below is compiled +// out and replaced by LoadLibraryExW/GetProcAddress/FreeLibrary (see the +// `win32` module further down), keeping the same function signatures. +#[cfg(not(target_os = "windows"))] extern "C" { fn dlopen(filename: *const c_char, flag: c_int) -> *mut c_void; fn dlsym(handle: *mut c_void, symbol: *const c_char) -> *mut c_void; fn dlclose(handle: *mut c_void) -> c_int; } +#[cfg(not(target_os = "windows"))] /// 动态加载共享库;失败返回 None。 fn dl_open(path: &Path) -> Option<*mut c_void> { let c = CString::new(path.to_str()?).ok()?; @@ -88,6 +93,7 @@ fn dl_open(path: &Path) -> Option<*mut c_void> { } } +#[cfg(not(target_os = "windows"))] /// 查符号;失败返回 None。 fn dl_sym(handle: *mut c_void, name: &str) -> Option<*mut c_void> { let c = CString::new(name).ok()?; @@ -99,6 +105,7 @@ fn dl_sym(handle: *mut c_void, name: &str) -> Option<*mut c_void> { } } +#[cfg(not(target_os = "windows"))] /// 从 `dlsym` 结果取函数指针(libloading 同款转换;调用方保证符号 /// 类型正确)。 /// @@ -109,6 +116,85 @@ unsafe fn dlsym_fn(handle: *mut c_void, name: &str) -> Option { Some(unsafe { std::mem::transmute_copy(&p) }) } +// ---- Windows dynamic loading (kernel32) ---- +// +// LoadLibraryExW / GetProcAddress / FreeLibrary in place of the POSIX +// dlopen family. The Windows handle is an HMODULE, which is pointer-sized +// and stored in the same `*mut c_void` slot, so the public signatures +// (`dl_open` / `dl_sym` / `dlsym_fn` / `dlclose`) are unchanged. + +/// Windows dynamic loading (`LoadLibraryExW` / `GetProcAddress` / +/// `FreeLibrary`, kernel32). The handle is an `HMODULE` kept as +/// `*mut c_void` for signature parity with the POSIX path. +#[cfg(target_os = "windows")] +mod win32 { + use super::*; + use std::os::windows::ffi::OsStrExt; + + /// `LOAD_WITH_ALTERED_SEARCH_PATH` (winbase.h): resolve the loaded + /// module's dependent DLLs from the module's own directory first, so a + /// plugin bundle's sibling DLLs are found next to the binary. + const LOAD_WITH_ALTERED_SEARCH_PATH: u32 = 0x0000_0008; + + #[link(name = "kernel32")] + unsafe extern "C" { + fn LoadLibraryExW( + lp_file_name: *const u16, + h_file: *mut c_void, + dw_flags: u32, + ) -> *mut c_void; + fn GetProcAddress(h_module: *mut c_void, lp_proc_name: *const c_char) -> *mut c_void; + fn FreeLibrary(h_module: *mut c_void) -> c_int; + } + + /// Dynamically load a shared library from an arbitrary (possibly + /// non-UTF-8) path; `None` on failure. The path is converted to a + /// NUL-terminated UTF-16 string for `LoadLibraryExW`. + pub(super) fn dl_open(path: &Path) -> Option<*mut c_void> { + let wide: Vec = path.as_os_str().encode_wide().chain(Some(0)).collect(); + let h = unsafe { + LoadLibraryExW(wide.as_ptr(), std::ptr::null_mut(), LOAD_WITH_ALTERED_SEARCH_PATH) + }; + if h.is_null() { + None + } else { + Some(h) + } + } + + /// Look up an exported symbol by name; `None` on failure. The symbol + /// name is a narrow (ANSI) string — `OfxGetNumberOfPlugins` / + /// `OfxGetPlugin`, both plain ASCII. + pub(super) fn dl_sym(handle: *mut c_void, name: &str) -> Option<*mut c_void> { + let c = CString::new(name).ok()?; + let p = unsafe { GetProcAddress(handle, c.as_ptr()) }; + if p.is_null() { + None + } else { + Some(p) + } + } + + /// Function pointer from a `GetProcAddress` result (same transmute as + /// the POSIX `dlsym_fn`; the caller guarantees the symbol type). + /// + /// # Safety + /// The symbol must actually be that function type. + pub(super) unsafe fn dlsym_fn(handle: *mut c_void, name: &str) -> Option { + let p = dl_sym(handle, name)?; + Some(unsafe { std::mem::transmute_copy(&p) }) + } + + /// Unload a library (`FreeLibrary`; reference-counted like POSIX + /// `dlclose`). + pub(super) unsafe fn dlclose(handle: *mut c_void) -> c_int { + unsafe { FreeLibrary(handle) } + } +} + +#[cfg(target_os = "windows")] +use win32::{dl_open, dl_sym, dlsym_fn, dlclose}; + // ---- OFX 宿主侧结构(ofxCore.h,字段序与 SDK 逐字一致)---- /// `OfxHost`(ofxCore.h:44)。