/* This Source Code Form is subject to the terms of the Mozilla Public * License, v. 2.0. If a copy of the MPL was not distributed with this * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ use blob_loader::load_blob; use ipc_channel::ipc::{self, IpcReceiver, IpcSender}; use mime_classifier::MimeClassifier; use mime_guess::guess_mime_type_opt; use net_traits::blob_url_store::{BlobURLStoreEntry, BlobURLStoreError, parse_blob_url}; use net_traits::filemanager_thread::{FileManagerThreadMsg, FileManagerResult, FilterPattern, FileOrigin}; use net_traits::filemanager_thread::{SelectedFile, RelativePos, FileManagerThreadError, SelectedFileId}; use net_traits::{LoadConsumer, LoadData, NetworkError}; use resource_thread::send_error; use std::collections::HashMap; use std::fs::File; use std::io::Read; use std::ops::Index; use std::path::{Path, PathBuf}; use std::sync::atomic::{self, AtomicUsize, AtomicBool, Ordering}; use std::sync::{Arc, RwLock}; #[cfg(any(target_os = "macos", target_os = "linux"))] use tinyfiledialogs; use url::Url; use util::prefs::PREFS; use util::thread::spawn_named; use uuid::Uuid; pub trait FileManagerThreadFactory { fn new(&'static UI) -> Self; } pub trait UIProvider where Self: Sync { fn open_file_dialog(&self, path: &str, patterns: Vec) -> Option; fn open_file_dialog_multi(&self, path: &str, patterns: Vec) -> Option>; } pub struct TFDProvider; impl UIProvider for TFDProvider { #[cfg(any(target_os = "macos", target_os = "linux"))] fn open_file_dialog(&self, path: &str, patterns: Vec) -> Option { let mut filter = vec![]; for p in patterns { let s = "*.".to_string() + &p.0; filter.push(s) } let filter_ref = &(filter.iter().map(|s| s.as_str()).collect::>()[..]); let filter_opt = if filter.len() > 0 { Some((filter_ref, "")) } else { None }; tinyfiledialogs::open_file_dialog("Pick a file", path, filter_opt) } #[cfg(any(target_os = "macos", target_os = "linux"))] fn open_file_dialog_multi(&self, path: &str, patterns: Vec) -> Option> { let mut filter = vec![]; for p in patterns { let s = "*.".to_string() + &p.0; filter.push(s) } let filter_ref = &(filter.iter().map(|s| s.as_str()).collect::>()[..]); let filter_opt = if filter.len() > 0 { Some((filter_ref, "")) } else { None }; tinyfiledialogs::open_file_dialog_multi("Pick files", path, filter_opt) } #[cfg(not(any(target_os = "macos", target_os = "linux")))] fn open_file_dialog(&self, path: &str, patterns: Vec) -> Option { None } #[cfg(not(any(target_os = "macos", target_os = "linux")))] fn open_file_dialog_multi(&self, path: &str, patterns: Vec) -> Option> { None } } impl FileManagerThreadFactory for IpcSender { /// Create a FileManagerThread fn new(ui: &'static UI) -> IpcSender { let (chan, recv) = ipc::channel().unwrap(); spawn_named("FileManager".to_owned(), move || { FileManager::new(recv, ui).start(); }); chan } } struct FileStoreEntry { /// Origin of the entry's "creator" origin: FileOrigin, /// Backend implementation file_impl: FileImpl, /// Reference counting refs: AtomicUsize, /// UUID key's validity as Blob URL is_valid_url: AtomicBool } /// File backend implementation #[derive(Clone)] enum FileImpl { PathOnly(PathBuf), Memory(BlobURLStoreEntry), Sliced(Uuid, RelativePos), } struct FileManager { receiver: IpcReceiver, store: Arc>, classifier: Arc, } impl FileManager { fn new(recv: IpcReceiver, ui: &'static UI) -> FileManager { FileManager { receiver: recv, store: Arc::new(FileManagerStore::new(ui)), classifier: Arc::new(MimeClassifier::new()), } } /// Start the file manager event loop fn start(&mut self) { loop { let store = self.store.clone(); match self.receiver.recv().unwrap() { FileManagerThreadMsg::SelectFile(filter, sender, origin, opt_test_path) => { spawn_named("select file".to_owned(), move || { store.select_file(filter, sender, origin, opt_test_path); }); } FileManagerThreadMsg::SelectFiles(filter, sender, origin, opt_test_paths) => { spawn_named("select files".to_owned(), move || { store.select_files(filter, sender, origin, opt_test_paths); }) } FileManagerThreadMsg::ReadFile(sender, id, origin) => { spawn_named("read file".to_owned(), move || { match store.try_read_file(id, origin) { Ok(buffer) => { let _ = sender.send(Ok(buffer)); } Err(_) => { let _ = sender.send(Err(FileManagerThreadError::ReadFileError)); } } }) } FileManagerThreadMsg::PromoteMemory(entry, sender, origin) => { spawn_named("transfer memory".to_owned(), move || { store.promote_memory(entry, sender, origin); }) } FileManagerThreadMsg::AddSlicedURLEntry(id, rel_pos, sender, origin) =>{ spawn_named("add sliced URL entry".to_owned(), move || { store.add_sliced_url_entry(id, rel_pos, sender, origin); }) } FileManagerThreadMsg::LoadBlob(load_data, consumer) => { match parse_blob_url(&load_data.url.clone()) { None => { let e = format!("Invalid blob URL format {:?}", load_data.url); let format_err = NetworkError::Internal(e); send_error(load_data.url.clone(), format_err, consumer); } Some((id, _fragment)) => { // check_url_validity is true since content is requested by this URL self.process_request(load_data, consumer, RelativePos::full_range(), id, true); } } }, FileManagerThreadMsg::RevokeBlobURL(id, origin, sender) => { if let Ok(id) = Uuid::parse_str(&id.0) { spawn_named("revoke blob url".to_owned(), move || { // Since it is revocation, unset_url_validity is true let _ = sender.send(store.dec_ref(&id, &origin, true)); }) } else { let _ = sender.send(Err(BlobURLStoreError::InvalidFileID)); } } FileManagerThreadMsg::DecRef(id, origin, sender) => { if let Ok(id) = Uuid::parse_str(&id.0) { spawn_named("dec ref".to_owned(), move || { // Since it is simple DecRef (possibly caused by close/drop), // unset_url_validity is false let _ = sender.send(store.dec_ref(&id, &origin, false)); }) } else { let _ = sender.send(Err(BlobURLStoreError::InvalidFileID)); } } FileManagerThreadMsg::IncRef(id, origin) => { if let Ok(id) = Uuid::parse_str(&id.0) { spawn_named("inc ref".to_owned(), move || { let _ = store.inc_ref(&id, &origin); }) } } FileManagerThreadMsg::ActivateBlobURL(id, sender, origin) => { if let Ok(id) = Uuid::parse_str(&id.0) { spawn_named("activate blob url".to_owned(), move || { let _ = sender.send(store.activate_blob_url(&id, &origin)); }); } else { let _ = sender.send(Err(BlobURLStoreError::InvalidFileID)); } } FileManagerThreadMsg::Exit => break, }; } } fn process_request(&self, load_data: LoadData, consumer: LoadConsumer, rel_pos: RelativePos, id: Uuid, check_url_validity: bool) { let origin_in = load_data.url.origin().unicode_serialization(); match self.store.get_impl(&id, &origin_in, check_url_validity) { Ok(file_impl) => { match file_impl { FileImpl::Memory(buffered) => { let classifier = self.classifier.clone(); spawn_named("load blob".to_owned(), move || load_blob(load_data, consumer, classifier, None, rel_pos, buffered)); } FileImpl::PathOnly(filepath) => { let opt_filename = filepath.file_name() .and_then(|osstr| osstr.to_str()) .map(|s| s.to_string()); let mut bytes = vec![]; let mut handler = File::open(&filepath).unwrap(); let mime = guess_mime_type_opt(filepath); let size = handler.read_to_end(&mut bytes).unwrap(); let entry = BlobURLStoreEntry { type_string: match mime { Some(x) => format!("{}", x), None => "".to_string(), }, size: size as u64, bytes: bytes, }; let classifier = self.classifier.clone(); spawn_named("load blob".to_owned(), move || load_blob(load_data, consumer, classifier, opt_filename, rel_pos, entry)); }, FileImpl::Sliced(id, rel_pos) => { // Next time we don't need to check validity since // we have already done that for requesting URL self.process_request(load_data, consumer, rel_pos, id, false); } } } Err(e) => { send_error(load_data.url.clone(), NetworkError::Internal(format!("{:?}", e)), consumer); } } } } struct FileManagerStore { entries: RwLock>, ui: &'static UI, } impl FileManagerStore { fn new(ui: &'static UI) -> Self { FileManagerStore { entries: RwLock::new(HashMap::new()), ui: ui, } } /// Copy out the file backend implementation content fn get_impl(&self, id: &Uuid, origin_in: &FileOrigin, check_url_validity: bool) -> Result { match self.entries.read().unwrap().get(id) { Some(ref entry) => { if *origin_in != *entry.origin { Err(BlobURLStoreError::InvalidOrigin) } else { if check_url_validity && !entry.is_valid_url.load(Ordering::Acquire) { Err(BlobURLStoreError::InvalidFileID) } else { Ok(entry.file_impl.clone()) } } } None => Err(BlobURLStoreError::InvalidFileID), } } fn insert(&self, id: Uuid, entry: FileStoreEntry) { self.entries.write().unwrap().insert(id, entry); } fn remove(&self, id: &Uuid) { self.entries.write().unwrap().remove(id); } fn inc_ref(&self, id: &Uuid, origin_in: &FileOrigin) -> Result<(), BlobURLStoreError>{ match self.entries.read().unwrap().get(id) { Some(entry) => { if entry.origin == *origin_in { entry.refs.fetch_add(1, Ordering::Relaxed); Ok(()) } else { Err(BlobURLStoreError::InvalidOrigin) } } None => Err(BlobURLStoreError::InvalidFileID), } } fn add_sliced_url_entry(&self, parent_id: SelectedFileId, rel_pos: RelativePos, sender: IpcSender>, origin_in: FileOrigin) { if let Ok(parent_id) = Uuid::parse_str(&parent_id.0) { match self.inc_ref(&parent_id, &origin_in) { Ok(_) => { let new_id = Uuid::new_v4(); self.insert(new_id, FileStoreEntry { origin: origin_in, file_impl: FileImpl::Sliced(parent_id, rel_pos), refs: AtomicUsize::new(1), // Valid here since AddSlicedURLEntry implies URL creation is_valid_url: AtomicBool::new(true), }); let _ = sender.send(Ok(SelectedFileId(new_id.simple().to_string()))); } Err(e) => { let _ = sender.send(Err(e)); } } } else { let _ = sender.send(Err(BlobURLStoreError::InvalidFileID)); } } fn select_file(&self, patterns: Vec, sender: IpcSender>, origin: FileOrigin, opt_test_path: Option) { // Check if the select_files preference is enabled // to ensure process-level security against compromised script; // Then try applying opt_test_path directly for testing convenience let opt_s = if select_files_pref_enabled() { opt_test_path } else { self.ui.open_file_dialog("", patterns) }; match opt_s { Some(s) => { let selected_path = Path::new(&s); match self.create_entry(selected_path, &origin) { Some(triple) => { let _ = sender.send(Ok(triple)); } None => { let _ = sender.send(Err(FileManagerThreadError::InvalidSelection)); } }; } None => { let _ = sender.send(Err(FileManagerThreadError::UserCancelled)); return; } } } fn select_files(&self, patterns: Vec, sender: IpcSender>>, origin: FileOrigin, opt_test_paths: Option>) { // Check if the select_files preference is enabled // to ensure process-level security against compromised script; // Then try applying opt_test_paths directly for testing convenience let opt_v = if select_files_pref_enabled() { opt_test_paths } else { self.ui.open_file_dialog_multi("", patterns) }; match opt_v { Some(v) => { let mut selected_paths = vec![]; for s in &v { selected_paths.push(Path::new(s)); } let mut replies = vec![]; for path in selected_paths { match self.create_entry(path, &origin) { Some(triple) => replies.push(triple), None => { let _ = sender.send(Err(FileManagerThreadError::InvalidSelection)); } }; } let _ = sender.send(Ok(replies)); } None => { let _ = sender.send(Err(FileManagerThreadError::UserCancelled)); return; } } } fn create_entry(&self, file_path: &Path, origin: &str) -> Option { match File::open(file_path) { Ok(handler) => { let id = Uuid::new_v4(); let file_impl = FileImpl::PathOnly(file_path.to_path_buf()); self.insert(id, FileStoreEntry { origin: origin.to_string(), file_impl: file_impl, refs: AtomicUsize::new(1), // Invalid here since create_entry is called by file selection is_valid_url: AtomicBool::new(false), }); // Unix Epoch: https://doc.servo.org/std/time/constant.UNIX_EPOCH.html let epoch = handler.metadata().and_then(|metadata| metadata.modified()).map_err(|_| ()) .and_then(|systime| systime.elapsed().map_err(|_| ())) .and_then(|elapsed| { let secs = elapsed.as_secs(); let nsecs = elapsed.subsec_nanos(); let msecs = secs * 1000 + nsecs as u64 / 1000000; Ok(msecs) }); let filename = file_path.file_name(); match (epoch, filename) { (Ok(epoch), Some(filename)) => { let filename_path = Path::new(filename); let mime = guess_mime_type_opt(filename_path); Some(SelectedFile { id: SelectedFileId(id.simple().to_string()), filename: filename_path.to_path_buf(), modified: epoch, type_string: match mime { Some(x) => format!("{}", x), None => "".to_string(), }, }) } _ => None } }, Err(_) => None } } fn try_read_file(&self, id: SelectedFileId, origin_in: FileOrigin) -> Result, BlobURLStoreError> { let id = try!(Uuid::parse_str(&id.0).map_err(|_| BlobURLStoreError::InvalidFileID)); match self.get_impl(&id, &origin_in, false) { Ok(file_impl) => { match file_impl { FileImpl::PathOnly(filepath) => { let mut buffer = vec![]; let mut handler = try!(File::open(filepath) .map_err(|_| BlobURLStoreError::InvalidEntry)); try!(handler.read_to_end(&mut buffer) .map_err(|_| BlobURLStoreError::External)); Ok(buffer) }, FileImpl::Memory(buffered) => { Ok(buffered.bytes) }, FileImpl::Sliced(id, rel_pos) => { self.try_read_file(SelectedFileId(id.simple().to_string()), origin_in) .map(|bytes| bytes.index(rel_pos.to_abs_range(bytes.len())).to_vec()) } } }, Err(e) => Err(e), } } fn dec_ref(&self, id: &Uuid, origin_in: &FileOrigin, unset_url_validity: bool) -> Result<(), BlobURLStoreError> { let (is_last_ref, opt_parent_id) = match self.entries.read().unwrap().get(id) { Some(entry) => { if *entry.origin == *origin_in { let old_refs = entry.refs.fetch_sub(1, Ordering::Release); if old_refs > 1 { if unset_url_validity { entry.is_valid_url.store(false, Ordering::Release); } (false, None) } else { if let FileImpl::Sliced(ref parent_id, _) = entry.file_impl { // if it has a reference to parent id, dec_ref on parent later (true, Some(parent_id.clone())) } else { (true, None) } } } else { // Invalid origin return Err(BlobURLStoreError::InvalidOrigin); } } None => return Err(BlobURLStoreError::InvalidFileID), }; if is_last_ref { atomic::fence(Ordering::Acquire); self.remove(id); if let Some(parent_id) = opt_parent_id { // unset_url_validity for parent is false since we only need // to unset the initial requesting URL return self.dec_ref(&parent_id, origin_in, false); } } Ok(()) } fn promote_memory(&self, entry: BlobURLStoreEntry, sender: IpcSender>, origin: FileOrigin) { match Url::parse(&origin) { // parse to check sanity Ok(_) => { let id = Uuid::new_v4(); self.insert(id, FileStoreEntry { origin: origin.clone(), file_impl: FileImpl::Memory(entry), refs: AtomicUsize::new(1), // Valid here since PromoteMemory implies URL creation is_valid_url: AtomicBool::new(true), }); let _ = sender.send(Ok(SelectedFileId(id.simple().to_string()))); } Err(_) => { let _ = sender.send(Err(BlobURLStoreError::InvalidOrigin)); } } } fn activate_blob_url(&self, id: &Uuid, origin_in: &FileOrigin) -> Result<(), BlobURLStoreError> { match self.entries.read().unwrap().get(id) { Some(entry) => { if *entry.origin == *origin_in { entry.is_valid_url.store(true, Ordering::Release); Ok(()) } else { Err(BlobURLStoreError::InvalidOrigin) } } None => Err(BlobURLStoreError::InvalidFileID) } } } fn select_files_pref_enabled() -> bool { PREFS.get("dom.testing.htmlinputelement.select_files.enabled") .as_boolean().unwrap_or(false) }