/* 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 compositing::compositor_thread::{EmbedderMsg, EmbedderProxy}; use ipc_channel::ipc::{self, IpcSender}; use mime_guess::guess_mime_type_opt; use net_traits::blob_url_store::{BlobBuf, BlobURLStoreError}; use net_traits::filemanager_thread::{FileManagerResult, FileManagerThreadMsg, FileOrigin, FilterPattern}; use net_traits::filemanager_thread::{FileManagerThreadError, ReadFileProgress, RelativePos, SelectedFile}; use servo_config::prefs::PREFS; use std::collections::HashMap; use std::fs::File; use std::io::{Read, Seek, SeekFrom}; use std::ops::Index; use std::path::{Path, PathBuf}; use std::sync::{Arc, RwLock}; use std::sync::atomic::{self, AtomicBool, AtomicUsize, Ordering}; use std::thread; use url::Url; use uuid::Uuid; /// FileManagerStore's entry struct FileStoreEntry { /// Origin of the entry's "creator" origin: FileOrigin, /// Backend implementation file_impl: FileImpl, /// Number of FileID holders that the ID is used to /// index this entry in `FileManagerStore`. /// Reference holders include a FileStoreEntry or /// a script-side File-based Blob refs: AtomicUsize, /// UUIDs only become valid blob URIs when explicitly requested /// by the user with createObjectURL. Validity can be revoked as well. /// (The UUID is the one that maps to this entry in `FileManagerStore`) is_valid_url: AtomicBool } #[derive(Clone)] struct FileMetaData { path: PathBuf, /// Modified time in UNIX Epoch format modified: u64, size: u64, } /// File backend implementation #[derive(Clone)] enum FileImpl { /// Metadata of on-disk file MetaDataOnly(FileMetaData), /// In-memory Blob buffer object Memory(BlobBuf), /// A reference to parent entry in `FileManagerStore`, /// representing a sliced version of the parent entry data Sliced(Uuid, RelativePos), } #[derive(Clone)] pub struct FileManager { embedder_proxy: EmbedderProxy, store: Arc, } impl FileManager { pub fn new(embedder_proxy: EmbedderProxy) -> FileManager { FileManager { embedder_proxy: embedder_proxy, store: Arc::new(FileManagerStore::new()), } } pub fn read_file(&self, sender: IpcSender>, id: Uuid, check_url_validity: bool, origin: FileOrigin) { let store = self.store.clone(); thread::Builder::new().name("read file".to_owned()).spawn(move || { if let Err(e) = store.try_read_file(&sender, id, check_url_validity, origin) { let _ = sender.send(Err(FileManagerThreadError::BlobURLStoreError(e))); } }).expect("Thread spawning failed"); } pub fn promote_memory(&self, blob_buf: BlobBuf, set_valid: bool, sender: IpcSender>, origin: FileOrigin) { let store = self.store.clone(); thread::Builder::new().name("transfer memory".to_owned()).spawn(move || { store.promote_memory(blob_buf, set_valid, sender, origin); }).expect("Thread spawning failed"); } /// Message handler pub fn handle(&self, msg: FileManagerThreadMsg) { match msg { FileManagerThreadMsg::SelectFile(filter, sender, origin, opt_test_path) => { let store = self.store.clone(); let embedder = self.embedder_proxy.clone(); thread::Builder::new().name("select file".to_owned()).spawn(move || { store.select_file(filter, sender, origin, opt_test_path, embedder); }).expect("Thread spawning failed"); } FileManagerThreadMsg::SelectFiles(filter, sender, origin, opt_test_paths) => { let store = self.store.clone(); let embedder = self.embedder_proxy.clone(); thread::Builder::new().name("select files".to_owned()).spawn(move || { store.select_files(filter, sender, origin, opt_test_paths, embedder); }).expect("Thread spawning failed"); } FileManagerThreadMsg::ReadFile(sender, id, check_url_validity, origin) => { self.read_file(sender, id, check_url_validity, origin); } FileManagerThreadMsg::PromoteMemory(blob_buf, set_valid, sender, origin) => { self.promote_memory(blob_buf, set_valid, sender, origin); } FileManagerThreadMsg::AddSlicedURLEntry(id, rel_pos, sender, origin) =>{ self.store.add_sliced_url_entry(id, rel_pos, sender, origin); } FileManagerThreadMsg::DecRef(id, origin, sender) => { let _ = sender.send(self.store.dec_ref(&id, &origin)); } FileManagerThreadMsg::RevokeBlobURL(id, origin, sender) => { let _ = sender.send(self.store.set_blob_url_validity(false, &id, &origin)); } FileManagerThreadMsg::ActivateBlobURL(id, sender, origin) => { let _ = sender.send(self.store.set_blob_url_validity(true, &id, &origin)); } } } } /// File manager's data store. It maintains a thread-safe mapping /// from FileID to FileStoreEntry which might have different backend implementation. /// Access to the content is encapsulated as methods of this struct. struct FileManagerStore { entries: RwLock>, } impl FileManagerStore { fn new() -> Self { FileManagerStore { entries: RwLock::new(HashMap::new()), } } /// 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 { let is_valid = entry.is_valid_url.load(Ordering::Acquire); if check_url_validity && !is_valid { 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: Uuid, rel_pos: RelativePos, sender: IpcSender>, origin_in: FileOrigin) { 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 // from a BlobImpl::Sliced is_valid_url: AtomicBool::new(true), }); // We assume that the returned id will be held by BlobImpl::File let _ = sender.send(Ok(new_id)); } Err(e) => { let _ = sender.send(Err(e)); } } } fn query_files_from_embedder(&self, patterns: Vec, multiple_files: bool, embedder_proxy: EmbedderProxy) -> Option> { let (ipc_sender, ipc_receiver) = ipc::channel().expect("Failed to create IPC channel!"); let msg = EmbedderMsg::SelectFiles(patterns, multiple_files, ipc_sender); embedder_proxy.send(msg); match ipc_receiver.recv() { Ok(result) => result, Err(e) => { warn!("Failed to receive files from embedder ({}).", e); None } } } fn select_file(&self, patterns: Vec, sender: IpcSender>, origin: FileOrigin, opt_test_path: Option, embedder_proxy: EmbedderProxy) { // 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.query_files_from_embedder(patterns, false, embedder_proxy).and_then(|mut x| x.pop()) }; match opt_s { Some(s) => { let selected_path = Path::new(&s); let result = self.create_entry(selected_path, &origin); let _ = sender.send(result); } None => { let _ = sender.send(Err(FileManagerThreadError::UserCancelled)); return; } } } fn select_files(&self, patterns: Vec, sender: IpcSender>>, origin: FileOrigin, opt_test_paths: Option>, embedder_proxy: EmbedderProxy) { // 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.query_files_from_embedder(patterns, true, embedder_proxy) }; 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) { Ok(triple) => replies.push(triple), Err(e) => { let _ = sender.send(Err(e)); return; } }; } let _ = sender.send(Ok(replies)); } None => { let _ = sender.send(Err(FileManagerThreadError::UserCancelled)); return; } } } fn create_entry(&self, file_path: &Path, origin: &str) -> Result { use net_traits::filemanager_thread::FileManagerThreadError::FileSystemError; let file = File::open(file_path).map_err(|e| FileSystemError(e.to_string()))?; let metadata = file.metadata().map_err(|e| FileSystemError(e.to_string()))?; let modified = metadata.modified().map_err(|e| FileSystemError(e.to_string()))?; let elapsed = modified.elapsed().map_err(|e| FileSystemError(e.to_string()))?; // Unix Epoch: https://doc.servo.org/std/time/constant.UNIX_EPOCH.html let modified_epoch = elapsed.as_secs() * 1000 + elapsed.subsec_nanos() as u64 / 1000000; let file_size = metadata.len(); let file_name = file_path.file_name().ok_or(FileSystemError("Invalid filepath".to_string()))?; let file_impl = FileImpl::MetaDataOnly(FileMetaData { path: file_path.to_path_buf(), modified: modified_epoch, size: file_size, }); let id = Uuid::new_v4(); 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), }); let filename_path = Path::new(file_name); let type_string = match guess_mime_type_opt(filename_path) { Some(x) => format!("{}", x), None => "".to_string(), }; Ok(SelectedFile { id: id, filename: filename_path.to_path_buf(), modified: modified_epoch, size: file_size, type_string: type_string, }) } fn get_blob_buf(&self, sender: &IpcSender>, id: &Uuid, origin_in: &FileOrigin, rel_pos: RelativePos, check_url_validity: bool) -> Result<(), BlobURLStoreError> { let file_impl = self.get_impl(id, origin_in, check_url_validity)?; match file_impl { FileImpl::Memory(buf) => { let range = rel_pos.to_abs_range(buf.size as usize); let buf = BlobBuf { filename: None, type_string: buf.type_string, size: range.len() as u64, bytes: buf.bytes.index(range).to_vec(), }; let _ = sender.send(Ok(ReadFileProgress::Meta(buf))); let _ = sender.send(Ok(ReadFileProgress::EOF)); Ok(()) } FileImpl::MetaDataOnly(metadata) => { /* XXX: Snapshot state check (optional) https://w3c.github.io/FileAPI/#snapshot-state. Concretely, here we create another file, and this file might not has the same underlying file state (meta-info plus content) as the time create_entry is called. */ let opt_filename = metadata.path.file_name() .and_then(|osstr| osstr.to_str()) .map(|s| s.to_string()); let mime = guess_mime_type_opt(metadata.path.clone()); let range = rel_pos.to_abs_range(metadata.size as usize); let mut file = File::open(&metadata.path) .map_err(|e| BlobURLStoreError::External(e.to_string()))?; let seeked_start = file.seek(SeekFrom::Start(range.start as u64)) .map_err(|e| BlobURLStoreError::External(e.to_string()))?; if seeked_start == (range.start as u64) { let type_string = match mime { Some(x) => format!("{}", x), None => "".to_string(), }; chunked_read(sender, &mut file, range.len(), opt_filename, type_string); Ok(()) } else { Err(BlobURLStoreError::InvalidEntry) } } FileImpl::Sliced(parent_id, inner_rel_pos) => { // Next time we don't need to check validity since // we have already done that for requesting URL if necessary self.get_blob_buf(sender, &parent_id, origin_in, rel_pos.slice_inner(&inner_rel_pos), false) } } } // Convenient wrapper over get_blob_buf fn try_read_file(&self, sender: &IpcSender>, id: Uuid, check_url_validity: bool, origin_in: FileOrigin) -> Result<(), BlobURLStoreError> { self.get_blob_buf(sender, &id, &origin_in, RelativePos::full_range(), check_url_validity) } fn dec_ref(&self, id: &Uuid, origin_in: &FileOrigin) -> Result<(), BlobURLStoreError> { let (do_remove, 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 { // not the last reference, no need to touch parent (false, None) } else { // last reference, and if it has a reference to parent id // dec_ref on parent later if necessary let is_valid = entry.is_valid_url.load(Ordering::Acquire); if let FileImpl::Sliced(ref parent_id, _) = entry.file_impl { (!is_valid, Some(parent_id.clone())) } else { (!is_valid, None) } } } else { return Err(BlobURLStoreError::InvalidOrigin); } } None => return Err(BlobURLStoreError::InvalidFileID), }; // Trigger removing if its last reference is gone and it is // not a part of a valid Blob URL if do_remove { atomic::fence(Ordering::Acquire); self.remove(id); if let Some(parent_id) = opt_parent_id { return self.dec_ref(&parent_id, origin_in); } } Ok(()) } fn promote_memory(&self, blob_buf: BlobBuf, set_valid: bool, 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(blob_buf), refs: AtomicUsize::new(1), is_valid_url: AtomicBool::new(set_valid), }); let _ = sender.send(Ok(id)); } Err(_) => { let _ = sender.send(Err(BlobURLStoreError::InvalidOrigin)); } } } fn set_blob_url_validity(&self, validity: bool, id: &Uuid, origin_in: &FileOrigin) -> Result<(), BlobURLStoreError> { let (do_remove, opt_parent_id, res) = match self.entries.read().unwrap().get(id) { Some(entry) => { if *entry.origin == *origin_in { entry.is_valid_url.store(validity, Ordering::Release); if !validity { // Check if it is the last possible reference // since refs only accounts for blob id holders // and store entry id holders let zero_refs = entry.refs.load(Ordering::Acquire) == 0; if let FileImpl::Sliced(ref parent_id, _) = entry.file_impl { (zero_refs, Some(parent_id.clone()), Ok(())) } else { (zero_refs, None, Ok(())) } } else { (false, None, Ok(())) } } else { (false, None, Err(BlobURLStoreError::InvalidOrigin)) } } None => (false, None, Err(BlobURLStoreError::InvalidFileID)) }; if do_remove { atomic::fence(Ordering::Acquire); self.remove(id); if let Some(parent_id) = opt_parent_id { return self.dec_ref(&parent_id, origin_in); } } res } } fn select_files_pref_enabled() -> bool { PREFS.get("dom.testing.htmlinputelement.select_files.enabled") .as_boolean().unwrap_or(false) } const CHUNK_SIZE: usize = 8192; fn chunked_read(sender: &IpcSender>, file: &mut File, size: usize, opt_filename: Option, type_string: String) { // First chunk let mut buf = vec![0; CHUNK_SIZE]; match file.read(&mut buf) { Ok(n) => { buf.truncate(n); let blob_buf = BlobBuf { filename: opt_filename, type_string: type_string, size: size as u64, bytes: buf, }; let _ = sender.send(Ok(ReadFileProgress::Meta(blob_buf))); } Err(e) => { let _ = sender.send(Err(FileManagerThreadError::FileSystemError(e.to_string()))); return; } } // Send the remaining chunks loop { let mut buf = vec![0; CHUNK_SIZE]; match file.read(&mut buf) { Ok(0) => { let _ = sender.send(Ok(ReadFileProgress::EOF)); return; } Ok(n) => { buf.truncate(n); let _ = sender.send(Ok(ReadFileProgress::Partial(buf))); } Err(e) => { let _ = sender.send(Err(FileManagerThreadError::FileSystemError(e.to_string()))); return; } } } }