ltk_webkit/wpe/
downloads.rsuse std::ffi::{ c_char, CStr, CString, OsStr };
use std::os::unix::ffi::OsStrExt;
use std::path::{ Path, PathBuf };
use std::sync::{ Mutex, Once };
use glib_sys::*;
use gobject_sys::*;
use wpe_webkit_sys::*;
use crate::types::{ DownloadEvent, DownloadId };
struct Live
{
id: DownloadId,
download: usize,
reported: f64,
failed: bool,
}
struct Registry
{
next_id: DownloadId,
dir: Option<PathBuf>,
live: Vec<Live>,
events: Vec<DownloadEvent>,
}
static REGISTRY: Mutex<Registry> = Mutex::new( Registry { next_id: 1, dir: None, live: Vec::new(), events: Vec::new() } );
const PROGRESS_STEP: f64 = 0.01;
pub fn set_download_dir( dir: impl Into<PathBuf> )
{
if let Ok( mut r ) = REGISTRY.lock() { r.dir = Some( dir.into() ); }
}
pub fn poll_downloads() -> Vec<DownloadEvent>
{
let Ok( mut r ) = REGISTRY.lock() else { return Vec::new() };
let mut events = std::mem::take( &mut r.events );
for live in r.live.iter_mut()
{
let fraction = unsafe { webkit_download_get_estimated_progress( live.download as *mut WebKitDownload ) };
if fraction - live.reported >= PROGRESS_STEP
{
live.reported = fraction;
events.push( DownloadEvent::Progress { id: live.id, fraction } );
}
}
events
}
pub fn cancel_download( id: DownloadId )
{
let download = REGISTRY.lock().ok()
.and_then( |r| r.live.iter().find( |l| l.id == id ).map( |l| l.download ) );
if let Some( download ) = download
{
unsafe { webkit_download_cancel( download as *mut WebKitDownload ); }
}
}
pub( crate ) unsafe fn track( webview: *mut WebKitWebView )
{
static ONCE: Once = Once::new();
ONCE.call_once( ||
{
unsafe
{
let session = webkit_web_view_get_network_session( webview );
if session.is_null() { return; }
connect( session as *mut GObject, b"download-started\0", on_started as *const () );
}
} );
}
unsafe fn connect( instance: *mut GObject, signal: &[ u8 ], handler: *const () )
{
unsafe
{
let callback = std::mem::transmute::<*const (), unsafe extern "C" fn()>( handler );
g_signal_connect_data( instance, signal.as_ptr() as *const c_char, Some( callback ), std::ptr::null_mut(), None, 0 );
}
}
unsafe extern "C" fn on_started( _session: *mut WebKitNetworkSession, download: *mut WebKitDownload, _data: gpointer )
{
if download.is_null() { return; }
unsafe
{
g_object_ref( download as *mut GObject );
connect( download as *mut GObject, b"decide-destination\0", on_decide_destination as *const () );
connect( download as *mut GObject, b"failed\0", on_failed as *const () );
connect( download as *mut GObject, b"finished\0", on_finished as *const () );
}
if let Ok( mut r ) = REGISTRY.lock()
{
let id = r.next_id;
r.next_id += 1;
r.live.push( Live { id, download: download as usize, reported: 0.0, failed: false } );
}
}
unsafe extern "C" fn on_decide_destination( download: *mut WebKitDownload, suggested: *const c_char, _data: gpointer ) -> gboolean
{
let name = if suggested.is_null()
{
String::new()
} else {
unsafe { CStr::from_ptr( suggested ) }.to_string_lossy().into_owned()
};
let dir = REGISTRY.lock().ok().and_then( |r| r.dir.clone() ).unwrap_or_else( default_dir );
let _ = std::fs::create_dir_all( &dir );
let path = unique_path( &dir, &sanitize( &name ) );
let Ok( path_c ) = CString::new( path.to_string_lossy().as_bytes() ) else { return 0 };
let uri = unsafe
{
webkit_download_set_destination( download, path_c.as_ptr() );
let request = webkit_download_get_request( download );
if request.is_null() { String::new() } else { lossy( webkit_uri_request_get_uri( request ) ) }
};
if let Ok( mut r ) = REGISTRY.lock()
{
let id = r.live.iter().find( |l| l.download == download as usize ).map( |l| l.id );
if let Some( id ) = id
{
r.events.push( DownloadEvent::Started { id, uri, path } );
}
}
1
}
unsafe extern "C" fn on_failed( download: *mut WebKitDownload, _error: *mut GError, _data: gpointer )
{
let Ok( mut r ) = REGISTRY.lock() else { return };
let failed = r.live.iter_mut().find( |l| l.download == download as usize ).map( |l|
{
l.failed = true;
l.id
} );
if let Some( id ) = failed
{
r.events.push( DownloadEvent::Failed { id } );
}
}
unsafe extern "C" fn on_finished( download: *mut WebKitDownload, _data: gpointer )
{
let removed = REGISTRY.lock().ok().and_then( |mut r|
{
let pos = r.live.iter().position( |l| l.download == download as usize )?;
let live = r.live.remove( pos );
if !live.failed
{
r.events.push( DownloadEvent::Finished { id: live.id } );
}
Some( () )
} );
if removed.is_some()
{
unsafe { g_object_unref( download as *mut GObject ); }
}
}
unsafe fn lossy( s: *const c_char ) -> String
{
if s.is_null() { return String::new(); }
unsafe { CStr::from_ptr( s ) }.to_string_lossy().into_owned()
}
fn default_dir() -> PathBuf
{
if let Some( dir ) = std::env::var_os( "XDG_DOWNLOAD_DIR" ) { return PathBuf::from( dir ); }
let special = unsafe { g_get_user_special_dir( G_USER_DIRECTORY_DOWNLOAD ) };
if !special.is_null()
{
let bytes = unsafe { CStr::from_ptr( special ) }.to_bytes();
return PathBuf::from( OsStr::from_bytes( bytes ) );
}
let home = std::env::var_os( "HOME" ).map( PathBuf::from ).unwrap_or_else( || PathBuf::from( "/tmp" ) );
home.join( "Downloads" )
}
fn sanitize( name: &str ) -> String
{
let clean: String = name.chars().map( |c| if c == '/' || c == '\0' { '_' } else { c } ).collect();
let clean = clean.trim_start_matches( '.' ).trim();
if clean.is_empty() { "download".to_string() } else { clean.to_string() }
}
fn unique_path( dir: &Path, name: &str ) -> PathBuf
{
let first = dir.join( name );
if !first.exists() { return first; }
let ( stem, ext ) = match name.rsplit_once( '.' )
{
Some( ( stem, ext ) ) if !stem.is_empty() => ( stem, format!( ".{ext}" ) ),
_ => ( name, String::new() ),
};
( 1u32.. )
.map( |n| dir.join( format!( "{stem} ({n}){ext}" ) ) )
.find( |p| !p.exists() )
.unwrap_or( first )
}
#[ cfg( test ) ]
mod tests
{
use super::*;
#[ test ]
fn sanitize_strips_separators_and_leading_dots()
{
assert_eq!( sanitize( "../../etc/passwd" ), "_.._etc_passwd" );
assert_eq!( sanitize( ".bashrc" ), "bashrc" );
assert_eq!( sanitize( "" ), "download" );
}
#[ test ]
fn unique_path_keeps_a_free_name()
{
let dir = std::env::temp_dir().join( "ltk-webkit-unique-path-test" );
assert_eq!( unique_path( &dir, "a.txt" ), dir.join( "a.txt" ) );
}
}