<commit_before>/* 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 ipc_channel::ipc::{self, IpcReceiver, IpcSender};
use net_traits::storage_thread::{StorageThreadMsg, StorageType};
use resource_thread;
use std::borrow::ToOwned;
use std::collections::BTreeMap;
use std::collections::HashMap;
use url::Url;
use util::opts;
use util::thread::spawn_named;

const QUOTA_SIZE_LIMIT: usize = 5 * 1024 * 1024;

pub trait StorageThreadFactory {
    fn new() -> Self;
}

impl StorageThreadFactory for IpcSender<StorageThreadMsg> {
    /// Create a storage thread
    fn new() -> IpcSender<StorageThreadMsg> {
        let (chan, port) = ipc::channel().unwrap();
        spawn_named("StorageManager".to_owned(), move || {
            StorageManager::new(port).start();
        });
        chan
    }
}

struct StorageManager {
    port: IpcReceiver<StorageThreadMsg>,
    session_data: HashMap<String, (usize, BTreeMap<String, String>)>,
    local_data: HashMap<String, (usize, BTreeMap<String, String>)>,
}

impl StorageManager {
    fn new(port: IpcReceiver<StorageThreadMsg>) -> StorageManager {
        let mut local_data = HashMap::new();
        if let Some(ref config_dir) = opts::get().config_dir {
            resource_thread::read_json_from_file(&mut local_data, config_dir, "local_data.json");
        }
        StorageManager {
            port: port,
            session_data: HashMap::new(),
            local_data: local_data,
        }
    }
}

impl StorageManager {
    fn start(&mut self) {
        loop {
            match self.port.recv().unwrap() {
                StorageThreadMsg::Length(sender, url, storage_type) => {
                    self.length(sender, url, storage_type)
                }
                StorageThreadMsg::Key(sender, url, storage_type, index) => {
                    self.key(sender, url, storage_type, index)
                }
                StorageThreadMsg::Keys(sender, url, storage_type) => {
                    self.keys(sender, url, storage_type)
                }
                StorageThreadMsg::SetItem(sender, url, storage_type, name, value) => {
                    self.set_item(sender, url, storage_type, name, value)
                }
                StorageThreadMsg::GetItem(sender, url, storage_type, name) => {
                    self.request_item(sender, url, storage_type, name)
                }
                StorageThreadMsg::RemoveItem(sender, url, storage_type, name) => {
                    self.remove_item(sender, url, storage_type, name)
                }
                StorageThreadMsg::Clear(sender, url, storage_type) => {
                    self.clear(sender, url, storage_type)
                }
                StorageThreadMsg::Exit(sender) => {
                    if let Some(ref config_dir) = opts::get().config_dir {
                        resource_thread::write_json_to_file(&self.local_data, config_dir, "local_data.json");
                    }
                    let _ = sender.send(());
                    break
                }
            }
        }
    }

    fn select_data(&self, storage_type: StorageType)
                   -> &HashMap<String, (usize, BTreeMap<String, String>)> {
        match storage_type {
            StorageType::Session => &self.session_data,
            StorageType::Local => &self.local_data
        }
    }

    fn select_data_mut(&mut self, storage_type: StorageType)
                       -> &mut HashMap<String, (usize, BTreeMap<String, String>)> {
        match storage_type {
            StorageType::Session => &mut self.session_data,
            StorageType::Local => &mut self.local_data
        }
    }

    fn length(&self, sender: IpcSender<usize>, url: Url, storage_type: StorageType) {
        let origin = self.origin_as_string(url);
        let data = self.select_data(storage_type);
        sender.send(data.get(&origin).map_or(0, |&(_, ref entry)| entry.len())).unwrap();
    }

    fn key(&self,
           sender: IpcSender<Option<String>>,
           url: Url,
           storage_type: StorageType,
           index: u32) {
        let origin = self.origin_as_string(url);
        let data = self.select_data(storage_type);
        let key = data.get(&origin)
                      .and_then(|&(_, ref entry)| entry.keys().nth(index as usize))
                      .cloned();
        sender.send(key).unwrap();
    }

    fn keys(&self,
            sender: IpcSender<Vec<String>>,
            url: Url,
            storage_type: StorageType) {
        let origin = self.origin_as_string(url);
        let data = self.select_data(storage_type);
        let keys = data.get(&origin)
                       .map_or(vec![], |&(_, ref entry)| entry.keys().cloned().collect());

        sender.send(keys).unwrap();
    }

    /// Sends Ok(changed, Some(old_value)) in case there was a previous
    /// value with the same key name but with different value name
    /// otherwise sends Err(()) to indicate that the operation would result in
    /// exceeding the quota limit
    fn set_item(&mut self,
                sender: IpcSender<Result<(bool, Option<String>), ()>>,
                url: Url,
                storage_type: StorageType,
                name: String,
                value: String) {
        let origin = self.origin_as_string(url);

        let current_total_size = {
            let local_data = self.select_data(StorageType::Local);
            let session_data = self.select_data(StorageType::Session);
            let local_data_size = local_data.get(&origin).map_or(0, |&(total, _)| total);
            let session_data_size = session_data.get(&origin).map_or(0, |&(total, _)| total);
            local_data_size + session_data_size
        };

        let data = self.select_data_mut(storage_type);
        if !data.contains_key(&origin) {
            data.insert(origin.clone(), (0, BTreeMap::new()));
        }

        let message = data.get_mut(&origin).map(|&mut (ref mut total, ref mut entry)| {
            let mut new_total_size = current_total_size + value.as_bytes().len();
            if let Some(old_value) = entry.get(&name) {
                new_total_size -= old_value.as_bytes().len();
            } else {
                new_total_size += name.as_bytes().len();
            }

            if new_total_size > QUOTA_SIZE_LIMIT {
                return Err(());
            }

            let message = entry.insert(name.clone(), value.clone()).map_or(
                Ok((true, None)),
                |old| if old == value {
                    Ok((false, None))
                } else {
                    Ok((true, Some(old)))
                });
            *total = new_total_size;
            message
        }).unwrap();
        sender.send(message).unwrap();
    }

    fn request_item(&self,
                sender: IpcSender<Option<String>>,
                url: Url,
                storage_type: StorageType,
                name: String) {
        let origin = self.origin_as_string(url);
        let data = self.select_data(storage_type);
        sender.send(data.get(&origin)
                    .and_then(|&(_, ref entry)| entry.get(&name))
                    .map(String::clone)).unwrap();
    }

    /// Sends Some(old_value) in case there was a previous value with the key name, otherwise sends None
    fn remove_item(&mut self,
                   sender: IpcSender<Option<String>>,
                   url: Url,
                   storage_type: StorageType,
                   name: String) {
        let origin = self.origin_as_string(url);
        let data = self.select_data_mut(storage_type);
        let old_value = data.get_mut(&origin).and_then(|&mut (ref mut total, ref mut entry)| {
            entry.remove(&name).and_then(|old| {
                *total -= name.as_bytes().len() + old.as_bytes().len();
                Some(old)
            })
        });
        sender.send(old_value).unwrap();
    }

    fn clear(&mut self, sender: IpcSender<bool>, url: Url, storage_type: StorageType) {
        let origin = self.origin_as_string(url);
        let data = self.select_data_mut(storage_type);
        sender.send(data.get_mut(&origin)
                    .map_or(false, |&mut (ref mut total, ref mut entry)| {
                        if !entry.is_empty() {
                            entry.clear();
                            *total = 0;
                            true
                        } else {
                            false
                        }})).unwrap();
    }

    fn origin_as_string(&self, url: Url) -> String {
        url.origin().ascii_serialization()
    }
}
<commit_msg>Properly check storage size against QUOTA_SIZE_LIMIT (fixes #12247)<commit_after>/* 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 ipc_channel::ipc::{self, IpcReceiver, IpcSender};
use net_traits::storage_thread::{StorageThreadMsg, StorageType};
use resource_thread;
use std::borrow::ToOwned;
use std::collections::BTreeMap;
use std::collections::HashMap;
use url::Url;
use util::opts;
use util::thread::spawn_named;

const QUOTA_SIZE_LIMIT: usize = 5 * 1024 * 1024;

pub trait StorageThreadFactory {
    fn new() -> Self;
}

impl StorageThreadFactory for IpcSender<StorageThreadMsg> {
    /// Create a storage thread
    fn new() -> IpcSender<StorageThreadMsg> {
        let (chan, port) = ipc::channel().unwrap();
        spawn_named("StorageManager".to_owned(), move || {
            StorageManager::new(port).start();
        });
        chan
    }
}

struct StorageManager {
    port: IpcReceiver<StorageThreadMsg>,
    session_data: HashMap<String, (usize, BTreeMap<String, String>)>,
    local_data: HashMap<String, (usize, BTreeMap<String, String>)>,
}

impl StorageManager {
    fn new(port: IpcReceiver<StorageThreadMsg>) -> StorageManager {
        let mut local_data = HashMap::new();
        if let Some(ref config_dir) = opts::get().config_dir {
            resource_thread::read_json_from_file(&mut local_data, config_dir, "local_data.json");
        }
        StorageManager {
            port: port,
            session_data: HashMap::new(),
            local_data: local_data,
        }
    }
}

impl StorageManager {
    fn start(&mut self) {
        loop {
            match self.port.recv().unwrap() {
                StorageThreadMsg::Length(sender, url, storage_type) => {
                    self.length(sender, url, storage_type)
                }
                StorageThreadMsg::Key(sender, url, storage_type, index) => {
                    self.key(sender, url, storage_type, index)
                }
                StorageThreadMsg::Keys(sender, url, storage_type) => {
                    self.keys(sender, url, storage_type)
                }
                StorageThreadMsg::SetItem(sender, url, storage_type, name, value) => {
                    self.set_item(sender, url, storage_type, name, value)
                }
                StorageThreadMsg::GetItem(sender, url, storage_type, name) => {
                    self.request_item(sender, url, storage_type, name)
                }
                StorageThreadMsg::RemoveItem(sender, url, storage_type, name) => {
                    self.remove_item(sender, url, storage_type, name)
                }
                StorageThreadMsg::Clear(sender, url, storage_type) => {
                    self.clear(sender, url, storage_type)
                }
                StorageThreadMsg::Exit(sender) => {
                    if let Some(ref config_dir) = opts::get().config_dir {
                        resource_thread::write_json_to_file(&self.local_data, config_dir, "local_data.json");
                    }
                    let _ = sender.send(());
                    break
                }
            }
        }
    }

    fn select_data(&self, storage_type: StorageType)
                   -> &HashMap<String, (usize, BTreeMap<String, String>)> {
        match storage_type {
            StorageType::Session => &self.session_data,
            StorageType::Local => &self.local_data
        }
    }

    fn select_data_mut(&mut self, storage_type: StorageType)
                       -> &mut HashMap<String, (usize, BTreeMap<String, String>)> {
        match storage_type {
            StorageType::Session => &mut self.session_data,
            StorageType::Local => &mut self.local_data
        }
    }

    fn length(&self, sender: IpcSender<usize>, url: Url, storage_type: StorageType) {
        let origin = self.origin_as_string(url);
        let data = self.select_data(storage_type);
        sender.send(data.get(&origin).map_or(0, |&(_, ref entry)| entry.len())).unwrap();
    }

    fn key(&self,
           sender: IpcSender<Option<String>>,
           url: Url,
           storage_type: StorageType,
           index: u32) {
        let origin = self.origin_as_string(url);
        let data = self.select_data(storage_type);
        let key = data.get(&origin)
                      .and_then(|&(_, ref entry)| entry.keys().nth(index as usize))
                      .cloned();
        sender.send(key).unwrap();
    }

    fn keys(&self,
            sender: IpcSender<Vec<String>>,
            url: Url,
            storage_type: StorageType) {
        let origin = self.origin_as_string(url);
        let data = self.select_data(storage_type);
        let keys = data.get(&origin)
                       .map_or(vec![], |&(_, ref entry)| entry.keys().cloned().collect());

        sender.send(keys).unwrap();
    }

    /// Sends Ok(changed, Some(old_value)) in case there was a previous
    /// value with the same key name but with different value name
    /// otherwise sends Err(()) to indicate that the operation would result in
    /// exceeding the quota limit
    fn set_item(&mut self,
                sender: IpcSender<Result<(bool, Option<String>), ()>>,
                url: Url,
                storage_type: StorageType,
                name: String,
                value: String) {
        let origin = self.origin_as_string(url);

        let (this_storage_size, other_storage_size) = {
            let local_data = self.select_data(StorageType::Local);
            let session_data = self.select_data(StorageType::Session);
            let local_data_size = local_data.get(&origin).map_or(0, |&(total, _)| total);
            let session_data_size = session_data.get(&origin).map_or(0, |&(total, _)| total);
            match storage_type {
                StorageType::Local => (local_data_size, session_data_size),
                StorageType::Session => (session_data_size, local_data_size),
            }
        };

        let data = self.select_data_mut(storage_type);
        if !data.contains_key(&origin) {
            data.insert(origin.clone(), (0, BTreeMap::new()));
        }

        let message = data.get_mut(&origin).map(|&mut (ref mut total, ref mut entry)| {
            let mut new_total_size = this_storage_size + value.as_bytes().len();
            if let Some(old_value) = entry.get(&name) {
                new_total_size -= old_value.as_bytes().len();
            } else {
                new_total_size += name.as_bytes().len();
            }

            if (new_total_size + other_storage_size) > QUOTA_SIZE_LIMIT {
                return Err(());
            }

            let message = entry.insert(name.clone(), value.clone()).map_or(
                Ok((true, None)),
                |old| if old == value {
                    Ok((false, None))
                } else {
                    Ok((true, Some(old)))
                });
            *total = new_total_size;
            message
        }).unwrap();
        sender.send(message).unwrap();
    }

    fn request_item(&self,
                sender: IpcSender<Option<String>>,
                url: Url,
                storage_type: StorageType,
                name: String) {
        let origin = self.origin_as_string(url);
        let data = self.select_data(storage_type);
        sender.send(data.get(&origin)
                    .and_then(|&(_, ref entry)| entry.get(&name))
                    .map(String::clone)).unwrap();
    }

    /// Sends Some(old_value) in case there was a previous value with the key name, otherwise sends None
    fn remove_item(&mut self,
                   sender: IpcSender<Option<String>>,
                   url: Url,
                   storage_type: StorageType,
                   name: String) {
        let origin = self.origin_as_string(url);
        let data = self.select_data_mut(storage_type);
        let old_value = data.get_mut(&origin).and_then(|&mut (ref mut total, ref mut entry)| {
            entry.remove(&name).and_then(|old| {
                *total -= name.as_bytes().len() + old.as_bytes().len();
                Some(old)
            })
        });
        sender.send(old_value).unwrap();
    }

    fn clear(&mut self, sender: IpcSender<bool>, url: Url, storage_type: StorageType) {
        let origin = self.origin_as_string(url);
        let data = self.select_data_mut(storage_type);
        sender.send(data.get_mut(&origin)
                    .map_or(false, |&mut (ref mut total, ref mut entry)| {
                        if !entry.is_empty() {
                            entry.clear();
                            *total = 0;
                            true
                        } else {
                            false
                        }})).unwrap();
    }

    fn origin_as_string(&self, url: Url) -> String {
        url.origin().ascii_serialization()
    }
}
<|endoftext|>


<commit_before>#![ allow (unused_parens) ]

use futures;
use futures::BoxFuture;
use futures::Future;

use futures_cpupool::CpuPool;

use lru_cache::LruCache;

use minilzo;

use std::error::Error;
use std::io::Read;
use std::io::Write;
use std::fs;
use std::fs::File;
use std::ops::Deref;
use std::ops::DerefMut;
use std::path::Path;
use std::sync::Arc;
use std::sync::Mutex;

#[ derive (Clone) ]
pub struct StorageManager {
	data: Arc <StorageManagerData>,
	state: Arc <Mutex <StorageManagerState>>,
	cpu_pool: CpuPool,
}

#[ derive (Clone) ]
struct StorageManagerData {
	path: String,
}

enum MemoryCacheItem {
	Compressed (Arc <Vec <u8>>, usize),
	Uncompressed (Arc <Vec <u8>>),
}

struct StorageManagerState {
	uncompressed_memory_items: LruCache <String, Arc <Vec <u8>>>,
	compressed_memory_items: LruCache <String, MemoryCacheItem>,
	filesystem_items: LruCache <String, FilesystemItem>,
}

struct FilesystemItem {
	storage_manager: Arc <StorageManagerData>,
	key: String,
	compressed: bool,
	uncompressed_size: usize,
}

impl StorageManager {

	pub fn new <PathRef: AsRef <Path>> (
		path_ref: PathRef,
		cpu_pool: CpuPool,
		uncompressed_memory_cache_size: usize,
		compressed_memory_cache_size: usize,
		filesystem_cache_size: usize,
	) -> Result <StorageManager, String> {

		// get string from path

		let path =
			try! (

			path_ref.as_ref ().to_str (
			).ok_or_else (
				||

				format! (
					"Invalid path: {}",
					path_ref.as_ref ().to_string_lossy ())

			)

		).to_owned ();

		// try and create filesystem cache path

		try! (

			fs::create_dir_all (
				& path,
			).map_err (
				|error|

				format! (
					"Error creating filesystem cache path: {}: {}",
					& path,
					error.description ())

			)

		);

		// check we can access filesystem cache path

		let metadata =
			try! (

			fs::metadata (
				& path,
			).map_err (
				|_|

				format! (
					"Unable to access {}",
					& path)

			)

		);

		if ! metadata.is_dir () {

			return Err (
				format! (
					"Not a directory: {}",
					& path));

		}

		// TODO create a test file to check permissions

		Ok (StorageManager {

			data: Arc::new (
				StorageManagerData {

				path:
					path,

			}),

			state: Arc::new (
				Mutex::new (
					StorageManagerState {

				uncompressed_memory_items:
					LruCache::new (
						uncompressed_memory_cache_size),

				compressed_memory_items:
					LruCache::new (
						compressed_memory_cache_size),

				filesystem_items:
					LruCache::new (
						filesystem_cache_size),

			})),

			cpu_pool:
				cpu_pool,

		})

	}

	pub fn insert (
		& mut self,
		key: String,
		uncompressed_data: Arc <Vec <u8>>,
	) -> Result <(), String> {

		let entry_path =
			format! (
				"{}/{}",
				self.data.path,
				key);

		let mut self_state =
			self.state.lock ().unwrap ();

		// store in memory cache

		self_state.uncompressed_memory_items.insert (
			key.deref ().to_owned (),
			uncompressed_data.clone ());

		// try and compress the data

		let (compressed, stored_data) =
			match (

			minilzo::compress (
				& uncompressed_data)

		) {

			Ok (compressed_data) =>
				(true, Arc::new (compressed_data)),

			Err (minilzo::Error::NotCompressible) =>
				(false, uncompressed_data.clone ()),

			Err (error) =>
				return Err (
					format! (
						"Error during compression: {:?}",
						error)),

		};

		// store in compressed memory cache

		self_state.compressed_memory_items.insert (
			key.deref ().to_owned (),
			if compressed {

				MemoryCacheItem::Compressed (
					stored_data.clone (),
					uncompressed_data.len ())

			} else {

				MemoryCacheItem::Uncompressed (
					stored_data.clone ())

			}
		);

		// write out to the filesystem

		let mut output =
			try! (

			File::create (
				& entry_path,
			).map_err (
				|error|

				format! (
					"Unable to create {}: {}",
					& entry_path,
					error.description ())
			)

		);

		try! (

			output.write (
				& stored_data,
			).map_err (
				|error|

				format! (
					"Error writing to {}: {}",
					entry_path,
					error.description ())
			)

		);

		// create and store the filesystem item in the index

		let filesystem_item =
			FilesystemItem {

			storage_manager: self.data.clone (),
			key: key.deref ().to_owned (),
			compressed: compressed,
			uncompressed_size: uncompressed_data.len (),

		};

		self_state.filesystem_items.insert (
			key.deref ().to_owned (),
			filesystem_item);

		Ok (())

	}

	pub fn get (
		& self,
		key: & str,
	) -> Option <BoxFuture <Arc <Vec <u8>>, String>> {

		let mut self_state =
			self.state.lock ().unwrap ();

		// try in uncompressed memory cache

		match (

			self_state.uncompressed_memory_items.get_mut (
				key,
			)

		) {

			Some (item_data) =>
				return Some (
					futures::done (
						Ok (item_data.clone ())
					).boxed (),
				),

			None =>
				(),
		};

		// try in compressed memory cache

		match (

			self_state.compressed_memory_items.get_mut (
				key,
			)

		) {

			Some (
				& mut MemoryCacheItem::Compressed (
					ref compressed_data,
					uncompressed_size)
			) =>
				return Some ({

				futures::done (

					minilzo::decompress (
						& compressed_data,
						uncompressed_size,
					).map (
						|uncompressed_data|

						Arc::new (
							uncompressed_data)

					).map_err (
						|error|

						format! (
							"Decompression failed: {:?}",
							error)

					)

				).boxed ()

			}),

			Some (
				& mut MemoryCacheItem::Uncompressed (
					ref uncompressed_data)
			) =>
				return Some (
					futures::done (
						Ok (uncompressed_data.clone ())
					).boxed (),
				),

			None =>
				(),

		};

		// try in compressed filesystem cache

		Self::get_filesystem (
			self_state.deref_mut (),
			key)

	}

	fn get_filesystem (
		self_state: & mut StorageManagerState,
		key: & str,
	) -> Option <BoxFuture <Arc <Vec <u8>>, String>> {

		match (

			self_state.filesystem_items.get_mut (
				key,
			)

		) {

			Some (filesystem_item) => {

				let uncompressed_data =
					match (

					filesystem_item.get ()

				) {

					Ok (data) =>
						data,

					Err (error) =>
						return Some (
							futures::failed (
								error,
							).boxed ()
						),

				};

				self_state.uncompressed_memory_items.insert (
					key.to_owned (),
					uncompressed_data.clone ());

				Some (futures::done (
					Ok (uncompressed_data)
				).boxed ())

			},

			None =>
				None,

		}

	}

}

impl FilesystemItem {

	fn path (
		& self,
	) -> String {

		format! (
			"{}/{}",
			self.storage_manager.path,
			self.key)

	}

	fn get (
		& self,
	) -> Result <Arc <Vec <u8>>, String> {

		let mut file =
			try! (

			File::open (
				self.path (),
			).map_err (
				|error|

				format! (
					"Error loading storage item {}: {}",
					self.key,
					error.description ())

			)

		);

		let mut stored_data =
			Vec::new ();

		try! (

			file.read_to_end (
				& mut stored_data,
			).map_err (
				|error|

				format! (
					"Error loading storage item {}: {}",
					self.key,
					error.description ())

			)

		);

		if self.compressed {

			let uncompressed_data =
				Arc::new (
					try! (

				minilzo::decompress (
					& stored_data,
					self.uncompressed_size,
				).map_err (
					|error|

					format! (
						"Error decompressing stored data: {:?}",
						error)

				)

			));

			Ok (
				uncompressed_data
			)

		} else {

			Ok (

				Arc::new (
					stored_data)

			)

		}

	}

}

impl Drop for FilesystemItem {

	fn drop (
		& mut self,
	) {

		match (

			fs::remove_file (
				self.path (),
			)

		) {

			Ok (_) => (),

			Err (error) => {

				stderrln! (
					"Error removing storage item {}: {}",
					self.key,
					error.description ());

			}

		}

	}

}

// ex: noet ts=4 filetype=rust
<commit_msg>fix race conditions in storage manager<commit_after>#![ allow (unused_parens) ]

use futures;
use futures::BoxFuture;
use futures::Future;

use futures_cpupool::CpuPool;

use lru_cache::LruCache;

use minilzo;

use std::error::Error;
use std::io::Read;
use std::io::Write;
use std::fs;
use std::fs::File;
use std::ops::Deref;
use std::ops::DerefMut;
use std::path::Path;
use std::sync::Arc;
use std::sync::Mutex;

#[ derive (Clone) ]
pub struct StorageManager {
	data: Arc <StorageManagerData>,
	state: Arc <Mutex <StorageManagerState>>,
	cpu_pool: CpuPool,
}

#[ derive (Clone) ]
struct StorageManagerData {
	path: String,
}

enum MemoryCacheItem {
	Compressed (Arc <Vec <u8>>, usize),
	Uncompressed (Arc <Vec <u8>>),
}

struct StorageManagerState {
	uncompressed_memory_items: LruCache <String, Arc <Vec <u8>>>,
	compressed_memory_items: LruCache <String, MemoryCacheItem>,
	filesystem_items: LruCache <String, FilesystemItem>,
}

struct FilesystemItem {
	storage_manager: Arc <StorageManagerData>,
	key: String,
	compressed: bool,
	uncompressed_size: usize,
}

impl StorageManager {

	pub fn new <PathRef: AsRef <Path>> (
		path_ref: PathRef,
		cpu_pool: CpuPool,
		uncompressed_memory_cache_size: usize,
		compressed_memory_cache_size: usize,
		filesystem_cache_size: usize,
	) -> Result <StorageManager, String> {

		// get string from path

		let path =
			try! (

			path_ref.as_ref ().to_str (
			).ok_or_else (
				||

				format! (
					"Invalid path: {}",
					path_ref.as_ref ().to_string_lossy ())

			)

		).to_owned ();

		// try and create filesystem cache path

		try! (

			fs::create_dir_all (
				& path,
			).map_err (
				|error|

				format! (
					"Error creating filesystem cache path: {}: {}",
					& path,
					error.description ())

			)

		);

		// check we can access filesystem cache path

		let metadata =
			try! (

			fs::metadata (
				& path,
			).map_err (
				|_|

				format! (
					"Unable to access {}",
					& path)

			)

		);

		if ! metadata.is_dir () {

			return Err (
				format! (
					"Not a directory: {}",
					& path));

		}

		// TODO create a test file to check permissions

		Ok (StorageManager {

			data: Arc::new (
				StorageManagerData {

				path:
					path,

			}),

			state: Arc::new (
				Mutex::new (
					StorageManagerState {

				uncompressed_memory_items:
					LruCache::new (
						uncompressed_memory_cache_size),

				compressed_memory_items:
					LruCache::new (
						compressed_memory_cache_size),

				filesystem_items:
					LruCache::new (
						filesystem_cache_size),

			})),

			cpu_pool:
				cpu_pool,

		})

	}

	pub fn insert (
		& mut self,
		key: String,
		uncompressed_data: Arc <Vec <u8>>,
	) -> Result <(), String> {

		let entry_path =
			format! (
				"{}/{}",
				self.data.path,
				key);

		let mut self_state =
			self.state.lock ().unwrap ();

		// store in memory cache

		if ! self_state.uncompressed_memory_items.contains_key (
			& key,
		) {

			self_state.uncompressed_memory_items.insert (
				key.deref ().to_owned (),
				uncompressed_data.clone ());

		}

		// check if it is compressed

		let in_compressed_memory_cache =
			self_state.compressed_memory_items.contains_key (
				& key);

		let in_filesystem_cache =
			self_state.filesystem_items.contains_key (
				& key);

		if (
			! in_compressed_memory_cache
			|| ! in_filesystem_cache
		) {

			// try and compress the data

			let (compressed, stored_data) =
				match (

				minilzo::compress (
					& uncompressed_data)

			) {

				Ok (compressed_data) =>
					(true, Arc::new (compressed_data)),

				Err (minilzo::Error::NotCompressible) =>
					(false, uncompressed_data.clone ()),

				Err (error) =>
					return Err (
						format! (
							"Error during compression: {:?}",
							error)),

			};

			// store in compressed memory cache

			if ! in_compressed_memory_cache {

				self_state.compressed_memory_items.insert (
					key.deref ().to_owned (),
					if compressed {

						MemoryCacheItem::Compressed (
							stored_data.clone (),
							uncompressed_data.len ())

					} else {

						MemoryCacheItem::Uncompressed (
							stored_data.clone ())

					}
				);

			}

			// write out to the filesystem

			if ! in_filesystem_cache {

				let mut output =
					try! (

					File::create (
						& entry_path,
					).map_err (
						|error|

						format! (
							"Unable to create {}: {}",
							& entry_path,
							error.description ())
					)

				);

				try! (

					output.write (
						& stored_data,
					).map_err (
						|error|

						format! (
							"Error writing to {}: {}",
							entry_path,
							error.description ())
					)

				);

				// create and store the filesystem item in the index

				let filesystem_item =
					FilesystemItem {

					storage_manager: self.data.clone (),
					key: key.deref ().to_owned (),
					compressed: compressed,
					uncompressed_size: uncompressed_data.len (),

				};

				self_state.filesystem_items.insert (
					key.deref ().to_owned (),
					filesystem_item);

			}

		}

		// return

		Ok (())

	}

	pub fn get (
		& self,
		key: & str,
	) -> Option <BoxFuture <Arc <Vec <u8>>, String>> {

		let mut self_state =
			self.state.lock ().unwrap ();

		// try in uncompressed memory cache

		match (

			self_state.uncompressed_memory_items.get_mut (
				key,
			)

		) {

			Some (item_data) =>
				return Some (
					futures::done (
						Ok (item_data.clone ())
					).boxed (),
				),

			None =>
				(),
		};

		// try in compressed memory cache

		match (

			self_state.compressed_memory_items.get_mut (
				key,
			)

		) {

			Some (
				& mut MemoryCacheItem::Compressed (
					ref compressed_data,
					uncompressed_size)
			) =>
				return Some ({

				futures::done (

					minilzo::decompress (
						& compressed_data,
						uncompressed_size,
					).map (
						|uncompressed_data|

						Arc::new (
							uncompressed_data)

					).map_err (
						|error|

						format! (
							"Decompression failed: {:?}",
							error)

					)

				).boxed ()

			}),

			Some (
				& mut MemoryCacheItem::Uncompressed (
					ref uncompressed_data)
			) =>
				return Some (
					futures::done (
						Ok (uncompressed_data.clone ())
					).boxed (),
				),

			None =>
				(),

		};

		// try in compressed filesystem cache

		Self::get_filesystem (
			self_state.deref_mut (),
			key)

	}

	fn get_filesystem (
		self_state: & mut StorageManagerState,
		key: & str,
	) -> Option <BoxFuture <Arc <Vec <u8>>, String>> {

		match (

			self_state.filesystem_items.get_mut (
				key,
			)

		) {

			Some (filesystem_item) => {

				let uncompressed_data =
					match (

					filesystem_item.get ()

				) {

					Ok (data) =>
						data,

					Err (error) =>
						return Some (
							futures::failed (
								error,
							).boxed ()
						),

				};

				self_state.uncompressed_memory_items.insert (
					key.to_owned (),
					uncompressed_data.clone ());

				Some (futures::done (
					Ok (uncompressed_data)
				).boxed ())

			},

			None =>
				None,

		}

	}

}

impl FilesystemItem {

	fn path (
		& self,
	) -> String {

		format! (
			"{}/{}",
			self.storage_manager.path,
			self.key)

	}

	fn get (
		& self,
	) -> Result <Arc <Vec <u8>>, String> {

		let mut file =
			try! (

			File::open (
				self.path (),
			).map_err (
				|error|

				format! (
					"Error loading storage item {}: {}",
					self.key,
					error.description ())

			)

		);

		let mut stored_data =
			Vec::new ();

		try! (

			file.read_to_end (
				& mut stored_data,
			).map_err (
				|error|

				format! (
					"Error loading storage item {}: {}",
					self.key,
					error.description ())

			)

		);

		if self.compressed {

			let uncompressed_data =
				Arc::new (
					try! (

				minilzo::decompress (
					& stored_data,
					self.uncompressed_size,
				).map_err (
					|error|

					format! (
						"Error decompressing stored data: {:?}",
						error)

				)

			));

			Ok (
				uncompressed_data
			)

		} else {

			Ok (

				Arc::new (
					stored_data)

			)

		}

	}

}

impl Drop for FilesystemItem {

	fn drop (
		& mut self,
	) {

		match (

			fs::remove_file (
				self.path (),
			)

		) {

			Ok (_) => (),

			Err (error) => {

				stderrln! (
					"Error removing storage item {}: {}",
					self.key,
					error.description ());

			}

		}

	}

}

// ex: noet ts=4 filetype=rust
<|endoftext|>


<commit_before>// Copyright © 2017 Mozilla Foundation
//
// This program is made available under an ISC-style license.  See the
// accompanying file LICENSE for details

#![cfg_attr(feature = "cargo-clippy", allow(float_cmp))]

#[macro_use]
extern crate cubeb_backend;

use cubeb_backend::{ffi, Context, ContextOps, DeviceCollectionRef, DeviceId, DeviceRef,
                    DeviceType, Ops, Result, Stream, StreamOps, StreamParams, StreamParamsRef};
use std::ffi::CStr;
use std::os::raw::c_void;
use std::ptr;

pub const OPS: Ops = capi_new!(TestContext, TestStream);

struct TestContext {
    pub ops: *const Ops,
}

impl ContextOps for TestContext {
    fn init(_context_name: Option<&CStr>) -> Result<Context> {
        let ctx = Box::new(TestContext {
            ops: &OPS as *const _,
        });
        Ok(unsafe { Context::from_ptr(Box::into_raw(ctx) as *mut _) })
    }

    fn backend_id(&mut self) -> &'static CStr {
        unsafe { CStr::from_ptr(b"remote\0".as_ptr() as *const _) }
    }
    fn max_channel_count(&mut self) -> Result<u32> {
        Ok(0u32)
    }
    fn min_latency(&mut self, _params: StreamParams) -> Result<u32> {
        Ok(0u32)
    }
    fn preferred_sample_rate(&mut self) -> Result<u32> {
        Ok(0u32)
    }
    fn enumerate_devices(
        &mut self,
        _devtype: DeviceType,
        collection: &DeviceCollectionRef,
    ) -> Result<()> {
        let coll = unsafe { &mut *collection.as_ptr() };
        coll.device = 0xDEAD_BEEF as *mut _;
        coll.count = usize::max_value();
        Ok(())
    }
    fn device_collection_destroy(&mut self, collection: &mut DeviceCollectionRef) -> Result<()> {
        let coll = unsafe { &mut *collection.as_ptr() };
        assert_eq!(coll.device, 0xDEAD_BEEF as *mut _);
        assert_eq!(coll.count, usize::max_value());
        coll.device = ptr::null_mut();
        coll.count = 0;
        Ok(())
    }
    fn stream_init(
        &mut self,
        _stream_name: Option<&CStr>,
        _input_device: DeviceId,
        _input_stream_params: Option<&StreamParamsRef>,
        _output_device: DeviceId,
        _output_stream_params: Option<&StreamParamsRef>,
        _latency_frame: u32,
        _data_callback: ffi::cubeb_data_callback,
        _state_callback: ffi::cubeb_state_callback,
        _user_ptr: *mut c_void,
    ) -> Result<Stream> {
        Ok(unsafe { Stream::from_ptr(0xDEAD_BEEF as *mut _) })
    }
    fn register_device_collection_changed(
        &mut self,
        _dev_type: DeviceType,
        _collection_changed_callback: ffi::cubeb_device_collection_changed_callback,
        _user_ptr: *mut c_void,
    ) -> Result<()> {
        Ok(())
    }
}

struct TestStream {}

impl StreamOps for TestStream {
    fn start(&mut self) -> Result<()> {
        Ok(())
    }
    fn stop(&mut self) -> Result<()> {
        Ok(())
    }
    fn reset_default_device(&mut self) -> Result<()> {
        Ok(())
    }
    fn position(&mut self) -> Result<u64> {
        Ok(0u64)
    }
    fn latency(&mut self) -> Result<u32> {
        Ok(0u32)
    }
    fn set_volume(&mut self, volume: f32) -> Result<()> {
        assert_eq!(volume, 0.5);
        Ok(())
    }
    fn current_device(&mut self) -> Result<&DeviceRef> {
        Ok(unsafe { DeviceRef::from_ptr(0xDEAD_BEEF as *mut _) })
    }
    fn device_destroy(&mut self, device: &DeviceRef) -> Result<()> {
        assert_eq!(device.as_ptr(), 0xDEAD_BEEF as *mut _);
        Ok(())
    }
    fn register_device_changed_callback(
        &mut self,
        _: ffi::cubeb_device_changed_callback,
    ) -> Result<()> {
        Ok(())
    }
}

#[test]
fn test_ops_context_init() {
    let mut c: *mut ffi::cubeb = ptr::null_mut();
    assert_eq!(
        unsafe { OPS.init.unwrap()(&mut c, ptr::null()) },
        ffi::CUBEB_OK
    );
    unsafe { OPS.destroy.unwrap()(c) }
}

#[test]
fn test_ops_context_max_channel_count() {
    let c: *mut ffi::cubeb = ptr::null_mut();
    let mut max_channel_count = u32::max_value();
    assert_eq!(
        unsafe { OPS.get_max_channel_count.unwrap()(c, &mut max_channel_count) },
        ffi::CUBEB_OK
    );
    assert_eq!(max_channel_count, 0);
}

#[test]
fn test_ops_context_min_latency() {
    let c: *mut ffi::cubeb = ptr::null_mut();
    let params: ffi::cubeb_stream_params = unsafe { ::std::mem::zeroed() };
    let mut latency = u32::max_value();
    assert_eq!(
        unsafe { OPS.get_min_latency.unwrap()(c, params, &mut latency) },
        ffi::CUBEB_OK
    );
    assert_eq!(latency, 0);
}

#[test]
fn test_ops_context_preferred_sample_rate() {
    let c: *mut ffi::cubeb = ptr::null_mut();
    let mut rate = u32::max_value();
    assert_eq!(
        unsafe { OPS.get_preferred_sample_rate.unwrap()(c, &mut rate) },
        ffi::CUBEB_OK
    );
    assert_eq!(rate, 0);
}

#[test]
fn test_ops_context_enumerate_devices() {
    let c: *mut ffi::cubeb = ptr::null_mut();
    let mut coll = ffi::cubeb_device_collection {
        device: ptr::null_mut(),
        count: 0,
    };
    assert_eq!(
        unsafe { OPS.enumerate_devices.unwrap()(c, 0, &mut coll) },
        ffi::CUBEB_OK
    );
    assert_eq!(coll.device, 0xDEAD_BEEF as *mut _);
    assert_eq!(coll.count, usize::max_value())
}

#[test]
fn test_ops_context_device_collection_destroy() {
    let c: *mut ffi::cubeb = ptr::null_mut();
    let mut coll = ffi::cubeb_device_collection {
        device: 0xDEAD_BEEF as *mut _,
        count: usize::max_value(),
    };
    assert_eq!(
        unsafe { OPS.device_collection_destroy.unwrap()(c, &mut coll) },
        ffi::CUBEB_OK
    );
    assert_eq!(coll.device, ptr::null_mut());
    assert_eq!(coll.count, 0);
}

// stream_init: Some($crate::capi::capi_stream_init::<$ctx>),
// stream_destroy: Some($crate::capi::capi_stream_destroy::<$stm>),
// stream_start: Some($crate::capi::capi_stream_start::<$stm>),
// stream_stop: Some($crate::capi::capi_stream_stop::<$stm>),
// stream_get_position: Some($crate::capi::capi_stream_get_position::<$stm>),

#[test]
fn test_ops_stream_latency() {
    let s: *mut ffi::cubeb_stream = ptr::null_mut();
    let mut latency = u32::max_value();
    assert_eq!(
        unsafe { OPS.stream_get_latency.unwrap()(s, &mut latency) },
        ffi::CUBEB_OK
    );
    assert_eq!(latency, 0);
}

#[test]
fn test_ops_stream_set_volume() {
    let s: *mut ffi::cubeb_stream = ptr::null_mut();
    unsafe {
        OPS.stream_set_volume.unwrap()(s, 0.5);
    }
}

#[test]
fn test_ops_stream_current_device() {
    let s: *mut ffi::cubeb_stream = ptr::null_mut();
    let mut device: *mut ffi::cubeb_device = ptr::null_mut();
    assert_eq!(
        unsafe { OPS.stream_get_current_device.unwrap()(s, &mut device) },
        ffi::CUBEB_OK
    );
    assert_eq!(device, 0xDEAD_BEEF as *mut _);
}

#[test]
fn test_ops_stream_device_destroy() {
    let s: *mut ffi::cubeb_stream = ptr::null_mut();
    unsafe {
        OPS.stream_device_destroy.unwrap()(s, 0xDEAD_BEEF as *mut _);
    }
}
<commit_msg>Add `input_latency` to TestStream impl.<commit_after>// Copyright © 2017 Mozilla Foundation
//
// This program is made available under an ISC-style license.  See the
// accompanying file LICENSE for details

#![cfg_attr(feature = "cargo-clippy", allow(float_cmp))]

#[macro_use]
extern crate cubeb_backend;

use cubeb_backend::{ffi, Context, ContextOps, DeviceCollectionRef, DeviceId, DeviceRef,
                    DeviceType, Ops, Result, Stream, StreamOps, StreamParams, StreamParamsRef};
use std::ffi::CStr;
use std::os::raw::c_void;
use std::ptr;

pub const OPS: Ops = capi_new!(TestContext, TestStream);

struct TestContext {
    pub ops: *const Ops,
}

impl ContextOps for TestContext {
    fn init(_context_name: Option<&CStr>) -> Result<Context> {
        let ctx = Box::new(TestContext {
            ops: &OPS as *const _,
        });
        Ok(unsafe { Context::from_ptr(Box::into_raw(ctx) as *mut _) })
    }

    fn backend_id(&mut self) -> &'static CStr {
        unsafe { CStr::from_ptr(b"remote\0".as_ptr() as *const _) }
    }
    fn max_channel_count(&mut self) -> Result<u32> {
        Ok(0u32)
    }
    fn min_latency(&mut self, _params: StreamParams) -> Result<u32> {
        Ok(0u32)
    }
    fn preferred_sample_rate(&mut self) -> Result<u32> {
        Ok(0u32)
    }
    fn enumerate_devices(
        &mut self,
        _devtype: DeviceType,
        collection: &DeviceCollectionRef,
    ) -> Result<()> {
        let coll = unsafe { &mut *collection.as_ptr() };
        coll.device = 0xDEAD_BEEF as *mut _;
        coll.count = usize::max_value();
        Ok(())
    }
    fn device_collection_destroy(&mut self, collection: &mut DeviceCollectionRef) -> Result<()> {
        let coll = unsafe { &mut *collection.as_ptr() };
        assert_eq!(coll.device, 0xDEAD_BEEF as *mut _);
        assert_eq!(coll.count, usize::max_value());
        coll.device = ptr::null_mut();
        coll.count = 0;
        Ok(())
    }
    fn stream_init(
        &mut self,
        _stream_name: Option<&CStr>,
        _input_device: DeviceId,
        _input_stream_params: Option<&StreamParamsRef>,
        _output_device: DeviceId,
        _output_stream_params: Option<&StreamParamsRef>,
        _latency_frame: u32,
        _data_callback: ffi::cubeb_data_callback,
        _state_callback: ffi::cubeb_state_callback,
        _user_ptr: *mut c_void,
    ) -> Result<Stream> {
        Ok(unsafe { Stream::from_ptr(0xDEAD_BEEF as *mut _) })
    }
    fn register_device_collection_changed(
        &mut self,
        _dev_type: DeviceType,
        _collection_changed_callback: ffi::cubeb_device_collection_changed_callback,
        _user_ptr: *mut c_void,
    ) -> Result<()> {
        Ok(())
    }
}

struct TestStream {}

impl StreamOps for TestStream {
    fn start(&mut self) -> Result<()> {
        Ok(())
    }
    fn stop(&mut self) -> Result<()> {
        Ok(())
    }
    fn reset_default_device(&mut self) -> Result<()> {
        Ok(())
    }
    fn position(&mut self) -> Result<u64> {
        Ok(0u64)
    }
    fn latency(&mut self) -> Result<u32> {
        Ok(0u32)
    }
    fn input_latency(&mut self) -> Result<u32> {
        Ok(0u32)
    }
    fn set_volume(&mut self, volume: f32) -> Result<()> {
        assert_eq!(volume, 0.5);
        Ok(())
    }
    fn current_device(&mut self) -> Result<&DeviceRef> {
        Ok(unsafe { DeviceRef::from_ptr(0xDEAD_BEEF as *mut _) })
    }
    fn device_destroy(&mut self, device: &DeviceRef) -> Result<()> {
        assert_eq!(device.as_ptr(), 0xDEAD_BEEF as *mut _);
        Ok(())
    }
    fn register_device_changed_callback(
        &mut self,
        _: ffi::cubeb_device_changed_callback,
    ) -> Result<()> {
        Ok(())
    }
}

#[test]
fn test_ops_context_init() {
    let mut c: *mut ffi::cubeb = ptr::null_mut();
    assert_eq!(
        unsafe { OPS.init.unwrap()(&mut c, ptr::null()) },
        ffi::CUBEB_OK
    );
    unsafe { OPS.destroy.unwrap()(c) }
}

#[test]
fn test_ops_context_max_channel_count() {
    let c: *mut ffi::cubeb = ptr::null_mut();
    let mut max_channel_count = u32::max_value();
    assert_eq!(
        unsafe { OPS.get_max_channel_count.unwrap()(c, &mut max_channel_count) },
        ffi::CUBEB_OK
    );
    assert_eq!(max_channel_count, 0);
}

#[test]
fn test_ops_context_min_latency() {
    let c: *mut ffi::cubeb = ptr::null_mut();
    let params: ffi::cubeb_stream_params = unsafe { ::std::mem::zeroed() };
    let mut latency = u32::max_value();
    assert_eq!(
        unsafe { OPS.get_min_latency.unwrap()(c, params, &mut latency) },
        ffi::CUBEB_OK
    );
    assert_eq!(latency, 0);
}

#[test]
fn test_ops_context_preferred_sample_rate() {
    let c: *mut ffi::cubeb = ptr::null_mut();
    let mut rate = u32::max_value();
    assert_eq!(
        unsafe { OPS.get_preferred_sample_rate.unwrap()(c, &mut rate) },
        ffi::CUBEB_OK
    );
    assert_eq!(rate, 0);
}

#[test]
fn test_ops_context_enumerate_devices() {
    let c: *mut ffi::cubeb = ptr::null_mut();
    let mut coll = ffi::cubeb_device_collection {
        device: ptr::null_mut(),
        count: 0,
    };
    assert_eq!(
        unsafe { OPS.enumerate_devices.unwrap()(c, 0, &mut coll) },
        ffi::CUBEB_OK
    );
    assert_eq!(coll.device, 0xDEAD_BEEF as *mut _);
    assert_eq!(coll.count, usize::max_value())
}

#[test]
fn test_ops_context_device_collection_destroy() {
    let c: *mut ffi::cubeb = ptr::null_mut();
    let mut coll = ffi::cubeb_device_collection {
        device: 0xDEAD_BEEF as *mut _,
        count: usize::max_value(),
    };
    assert_eq!(
        unsafe { OPS.device_collection_destroy.unwrap()(c, &mut coll) },
        ffi::CUBEB_OK
    );
    assert_eq!(coll.device, ptr::null_mut());
    assert_eq!(coll.count, 0);
}

// stream_init: Some($crate::capi::capi_stream_init::<$ctx>),
// stream_destroy: Some($crate::capi::capi_stream_destroy::<$stm>),
// stream_start: Some($crate::capi::capi_stream_start::<$stm>),
// stream_stop: Some($crate::capi::capi_stream_stop::<$stm>),
// stream_get_position: Some($crate::capi::capi_stream_get_position::<$stm>),

#[test]
fn test_ops_stream_latency() {
    let s: *mut ffi::cubeb_stream = ptr::null_mut();
    let mut latency = u32::max_value();
    assert_eq!(
        unsafe { OPS.stream_get_latency.unwrap()(s, &mut latency) },
        ffi::CUBEB_OK
    );
    assert_eq!(latency, 0);
}

#[test]
fn test_ops_stream_set_volume() {
    let s: *mut ffi::cubeb_stream = ptr::null_mut();
    unsafe {
        OPS.stream_set_volume.unwrap()(s, 0.5);
    }
}

#[test]
fn test_ops_stream_current_device() {
    let s: *mut ffi::cubeb_stream = ptr::null_mut();
    let mut device: *mut ffi::cubeb_device = ptr::null_mut();
    assert_eq!(
        unsafe { OPS.stream_get_current_device.unwrap()(s, &mut device) },
        ffi::CUBEB_OK
    );
    assert_eq!(device, 0xDEAD_BEEF as *mut _);
}

#[test]
fn test_ops_stream_device_destroy() {
    let s: *mut ffi::cubeb_stream = ptr::null_mut();
    unsafe {
        OPS.stream_device_destroy.unwrap()(s, 0xDEAD_BEEF as *mut _);
    }
}
<|endoftext|>


<commit_before>use super::{
    attr, attr_id, attr_itemtype, attr_itemtype_string, attr_slot, concat, elem, elem_empty, json,
    Context, ToHtml,
};
use crate::methods::encode::txt::ToTxt;
use html_escape::encode_safe;
use std::{fs, path::PathBuf};
use stencila_schema::*;

/// Encode a vector of `InlineContent` as HTML
///
/// Note that, as for blocks, if the `slot` is an empty string, then no wrapping `<div>`
/// will be generated.
impl ToHtml for Vec<InlineContent> {
    fn to_html(&self, slot: &str, context: &Context) -> String {
        let inlines = self
            .iter()
            .map(|item| item.to_html("", context))
            .collect::<Vec<String>>()
            .concat();
        if slot.is_empty() {
            inlines
        } else {
            elem("span", &[attr_slot(slot)], &inlines)
        }
    }
}

impl ToHtml for InlineContent {
    fn to_html(&self, slot: &str, context: &Context) -> String {
        match self {
            InlineContent::AudioObject(node) => node.to_html(slot, context),
            InlineContent::Boolean(node) => node.to_html(slot, context),
            InlineContent::Cite(node) => node.to_html(slot, context),
            InlineContent::CiteGroup(node) => node.to_html(slot, context),
            InlineContent::CodeExpression(node) => node.to_html(slot, context),
            InlineContent::CodeFragment(node) => node.to_html(slot, context),
            InlineContent::Delete(node) => node.to_html(slot, context),
            InlineContent::Emphasis(node) => node.to_html(slot, context),
            InlineContent::ImageObject(node) => node.to_html(slot, context),
            InlineContent::Integer(node) => node.to_html(slot, context),
            InlineContent::Link(node) => node.to_html(slot, context),
            InlineContent::MathFragment(node) => node.to_html(slot, context),
            InlineContent::NontextualAnnotation(node) => node.to_html(slot, context),
            InlineContent::Note(node) => node.to_html(slot, context),
            InlineContent::Null(node) => node.to_html(slot, context),
            InlineContent::Number(node) => node.to_html(slot, context),
            InlineContent::Parameter(node) => node.to_html(slot, context),
            InlineContent::Quote(node) => node.to_html(slot, context),
            InlineContent::String(node) => node.to_html(slot, context),
            InlineContent::Strong(node) => node.to_html(slot, context),
            InlineContent::Subscript(node) => node.to_html(slot, context),
            InlineContent::Superscript(node) => node.to_html(slot, context),
            InlineContent::VideoObject(node) => node.to_html(slot, context),
        }
    }
}

macro_rules! mark_to_html {
    ($type:ident, $tag:literal) => {
        impl ToHtml for $type {
            fn to_html(&self, slot: &str, context: &Context) -> String {
                elem(
                    $tag,
                    &[attr_slot(slot), attr_itemtype(self), attr_id(&self.id)],
                    &self.content.to_html("", context),
                )
            }
        }
    };
}

mark_to_html!(Delete, "del");
mark_to_html!(Emphasis, "em");
mark_to_html!(NontextualAnnotation, "u");
mark_to_html!(Strong, "strong");
mark_to_html!(Subscript, "sub");
mark_to_html!(Superscript, "sup");

/// Convert a file:// URL to a data:// URI
///
/// Note that this function assumes that paths are absolute.
/// File URLs are usually resolves elsewhere e.g. in the `compile` method
/// before encoding to HTML.
fn file_uri_to_data_uri(url: &str) -> String {
    let path = if let Some(path) = url.strip_prefix("file://") {
        PathBuf::from(path)
    } else {
        return url.into();
    };

    // Read the file, convert it to a dataURI, and record it as a dependency
    match fs::read(&path) {
        Ok(bytes) => {
            let mime = match mime_guess::from_path(&path).first() {
                Some(mime) => mime.to_string(),
                None => "image/png".to_string(),
            };
            let data = base64::encode(bytes);
            ["data:", &mime, ";base64,", &data].concat()
        }
        Err(error) => {
            tracing::warn!("Unable to read media file {}: {}", path.display(), error);
            url.into()
        }
    }
}

fn content_url_to_src_attr(content_url: &str, context: &Context) -> String {
    let url = match context.bundle {
        true => file_uri_to_data_uri(content_url),
        false => content_url.to_string(),
    };
    attr("src", &url)
}

impl ToHtml for AudioObjectSimple {
    fn to_html(&self, slot: &str, context: &Context) -> String {
        elem(
            "audio",
            &[
                attr_slot(slot),
                attr_itemtype_string("AudioObject"),
                attr_id(&self.id),
                "controls".to_string(),
                content_url_to_src_attr(&self.content_url, context),
            ],
            "",
        )
    }
}

impl ToHtml for ImageObjectSimple {
    fn to_html(&self, slot: &str, context: &Context) -> String {
        elem_empty(
            "img",
            &[
                attr_slot(slot),
                attr_itemtype_string("ImageObject"),
                attr_id(&self.id),
                content_url_to_src_attr(&self.content_url, context),
            ],
        )
    }
}

impl ToHtml for VideoObjectSimple {
    fn to_html(&self, slot: &str, context: &Context) -> String {
        let src_attr = content_url_to_src_attr(&self.content_url, context);
        let type_attr = match &self.media_type {
            Some(media_type) => attr("type", media_type),
            None => "".to_string(),
        };
        elem(
            "video",
            &[
                attr_slot(slot),
                attr_itemtype_string("VideoObject"),
                attr_id(&self.id),
                "controls".to_string(),
            ],
            &elem("source", &[src_attr, type_attr], ""),
        )
    }
}

impl ToHtml for Cite {
    fn to_html(&self, slot: &str, context: &Context) -> String {
        let content = match &self.content {
            Some(nodes) => nodes.to_html("", context),
            None => {
                // Get the list of references from the root nodes
                let references = match context.root {
                    Node::Article(article) => article.references.clone(),
                    Node::CreativeWork(work) => work.references.clone(),
                    _ => {
                        tracing::warn!("Unhandled root document type");
                        None
                    }
                };
                if let Some(references) = references {
                    // Find the reference that matches the `target`
                    let reference = references
                        .iter()
                        .enumerate()
                        .find_map(|(index, reference)| match reference {
                            // A string reference so match against its index in the references list
                            CreativeWorkReferences::String(_) => {
                                if self.target == format!("ref{}", index + 1)
                                    || self.target == format!("ref{}", index + 1)
                                {
                                    Some((index, reference))
                                } else {
                                    None
                                }
                            }
                            // A `CreativeWork` reference so match against its `id`/
                            CreativeWorkReferences::CreativeWorkTypes(work) => match work {
                                CreativeWorkTypes::Article(Article { id, .. }) => {
                                    if let Some(id) = id.as_deref() {
                                        if self.target == *id {
                                            Some((index, reference))
                                        } else {
                                            None
                                        }
                                    } else {
                                        None
                                    }
                                }
                                _ => None,
                            },
                        });
                    // Create the content for the citation
                    match reference {
                        None => {
                            tracing::warn!("When encoding citation was unable to find reference '{}' in root document", self.target);
                            format!(r#"{}"#, self.target.clone())
                        }
                        Some((index, reference)) => {
                            // Always have a numeric citation
                            let mut content = format!(r#"{}"#, index + 1);
                            // If a `CreativeWorkType` then add authors and year
                            if let CreativeWorkReferences::CreativeWorkTypes(work) = reference {
                                let (authors, date) = match &work {
                                    CreativeWorkTypes::Article(article) => {
                                        (article.authors.clone(), article.date_published.clone())
                                    }
                                    CreativeWorkTypes::CreativeWork(work) => {
                                        (work.authors.clone(), work.date_published.clone())
                                    }
                                    _ => {
                                        tracing::warn!("Unhandled root document type");
                                        (None, None)
                                    }
                                };
                                if let Some(authors) = authors {
                                    let names: Vec<String> = authors
                                        .iter()
                                        .map(|author| match author {
                                            CreativeWorkAuthors::Person(person) => {
                                                match &person.family_names {
                                                    Some(family_names) => family_names.join(" "),
                                                    None => "Anonymous".to_string(),
                                                }
                                            }
                                            CreativeWorkAuthors::Organization(org) => {
                                                match &org.name {
                                                    Some(name) => *name.clone(),
                                                    None => "Anonymous".to_string(),
                                                }
                                            }
                                        })
                                        .collect();
                                    let names = if authors.len() == 1 {
                                        names.join("")
                                    } else if authors.len() == 2 {
                                        format!(r#"{} and {}"#, names[0], names[1])
                                    } else {
                                        format!(r#"{} et al"#, names[0])
                                    };
                                    content += &format!(r#"{}"#, names)
                                }
                                if let Some(date) = date {
                                    if date.value.len() >= 4 {
                                        let year = date.value[..4].to_string();
                                        content += &format!(r#"{}"#, year)
                                    }
                                }
                            }
                            content
                        }
                    }
                } else {
                    tracing::warn!("When encoding citation was unable to find references list in root document");
                    format!(r#"{}"#, self.target.clone())
                }
            }
        };
        elem(
            "cite",
            &[attr_slot(slot), attr_itemtype(self), attr_id(&self.id)],
            &elem("a", &[attr("href", &self.target)], &content),
        )
    }
}

impl ToHtml for CiteGroup {
    fn to_html(&self, slot: &str, context: &Context) -> String {
        elem(
            "span",
            &[attr_slot(slot), attr_itemtype(self), attr_id(&self.id)],
            &concat(&self.items, |cite| cite.to_html("", context)),
        )
    }
}

impl ToHtml for CodeExpression {
    fn to_html(&self, slot: &str, context: &Context) -> String {
        let output = match &self.output {
            Some(output) => elem("pre", &[], &output.to_html("", context)),
            None => "".to_string(),
        };
        elem(
            "stencila-code-expression",
            &[
                attr_slot(slot),
                attr_itemtype(self),
                attr_id(&self.id),
                attr("programming-language", &self.programming_language),
            ],
            &[
                elem("code", &[attr("slot", "text")], &encode_safe(&self.text)),
                elem("output", &[attr("slot", "output")], &output),
            ]
            .concat(),
        )
    }
}

impl ToHtml for CodeFragment {
    fn to_html(&self, slot: &str, _context: &Context) -> String {
        elem(
            "code",
            &[
                attr_slot(slot),
                attr_itemtype(self),
                attr_id(&self.id),
                match &self.programming_language {
                    Some(lang) => attr("class", &["language-", lang].concat()),
                    None => "".to_string(),
                },
            ],
            &encode_safe(&self.text),
        )
    }
}

impl ToHtml for Link {
    fn to_html(&self, slot: &str, context: &Context) -> String {
        elem(
            "a",
            &[
                attr_slot(slot),
                attr_itemtype(self),
                attr_id(&self.id),
                attr("href", &self.target),
            ],
            &self.content.to_html("", context),
        )
    }
}

impl ToHtml for MathFragment {
    fn to_html(&self, slot: &str, _context: &Context) -> String {
        elem(
            "code",
            &[attr_slot(slot), attr_itemtype(self), attr_id(&self.id)],
            &encode_safe(&self.text),
        )
    }
}

impl ToHtml for Note {
    fn to_html(&self, slot: &str, _context: &Context) -> String {
        elem(
            "code",
            &[
                attr_slot(slot),
                attr_itemtype(self),
                attr_id(&self.id),
                attr("class", "todo"),
            ],
            &json(self),
        )
    }
}

impl ToHtml for Parameter {
    fn to_html(&self, slot: &str, _context: &Context) -> String {
        let input_type = match self.validator.as_deref() {
            Some(ValidatorTypes::NumberValidator(..)) => "number",
            _ => "text",
        };
        let value_attr = match self.value.as_deref() {
            Some(node) => attr("value", &node.to_txt()),
            _ => "".to_string(),
        };
        elem_empty(
            "input",
            &[
                attr_slot(slot),
                attr_itemtype(self),
                attr_id(&self.id),
                attr("type", input_type),
                attr("name", &self.name),
                value_attr,
            ],
        )
    }
}

impl ToHtml for Quote {
    fn to_html(&self, slot: &str, context: &Context) -> String {
        elem(
            "q",
            &[attr_slot(slot), attr_itemtype(self), attr_id(&self.id)],
            &self.content.to_html("", context),
        )
    }
}
<commit_msg>fix(HTML Encoding): Fix `CodeExpression` output<commit_after>use super::{
    attr, attr_id, attr_itemtype, attr_itemtype_string, attr_slot, concat, elem, elem_empty, json,
    Context, ToHtml,
};
use crate::methods::encode::txt::ToTxt;
use html_escape::encode_safe;
use std::{fs, path::PathBuf};
use stencila_schema::*;

/// Encode a vector of `InlineContent` as HTML
///
/// Note that, as for blocks, if the `slot` is an empty string, then no wrapping `<div>`
/// will be generated.
impl ToHtml for Vec<InlineContent> {
    fn to_html(&self, slot: &str, context: &Context) -> String {
        let inlines = self
            .iter()
            .map(|item| item.to_html("", context))
            .collect::<Vec<String>>()
            .concat();
        if slot.is_empty() {
            inlines
        } else {
            elem("span", &[attr_slot(slot)], &inlines)
        }
    }
}

impl ToHtml for InlineContent {
    fn to_html(&self, slot: &str, context: &Context) -> String {
        match self {
            InlineContent::AudioObject(node) => node.to_html(slot, context),
            InlineContent::Boolean(node) => node.to_html(slot, context),
            InlineContent::Cite(node) => node.to_html(slot, context),
            InlineContent::CiteGroup(node) => node.to_html(slot, context),
            InlineContent::CodeExpression(node) => node.to_html(slot, context),
            InlineContent::CodeFragment(node) => node.to_html(slot, context),
            InlineContent::Delete(node) => node.to_html(slot, context),
            InlineContent::Emphasis(node) => node.to_html(slot, context),
            InlineContent::ImageObject(node) => node.to_html(slot, context),
            InlineContent::Integer(node) => node.to_html(slot, context),
            InlineContent::Link(node) => node.to_html(slot, context),
            InlineContent::MathFragment(node) => node.to_html(slot, context),
            InlineContent::NontextualAnnotation(node) => node.to_html(slot, context),
            InlineContent::Note(node) => node.to_html(slot, context),
            InlineContent::Null(node) => node.to_html(slot, context),
            InlineContent::Number(node) => node.to_html(slot, context),
            InlineContent::Parameter(node) => node.to_html(slot, context),
            InlineContent::Quote(node) => node.to_html(slot, context),
            InlineContent::String(node) => node.to_html(slot, context),
            InlineContent::Strong(node) => node.to_html(slot, context),
            InlineContent::Subscript(node) => node.to_html(slot, context),
            InlineContent::Superscript(node) => node.to_html(slot, context),
            InlineContent::VideoObject(node) => node.to_html(slot, context),
        }
    }
}

macro_rules! mark_to_html {
    ($type:ident, $tag:literal) => {
        impl ToHtml for $type {
            fn to_html(&self, slot: &str, context: &Context) -> String {
                elem(
                    $tag,
                    &[attr_slot(slot), attr_itemtype(self), attr_id(&self.id)],
                    &self.content.to_html("", context),
                )
            }
        }
    };
}

mark_to_html!(Delete, "del");
mark_to_html!(Emphasis, "em");
mark_to_html!(NontextualAnnotation, "u");
mark_to_html!(Strong, "strong");
mark_to_html!(Subscript, "sub");
mark_to_html!(Superscript, "sup");

/// Convert a file:// URL to a data:// URI
///
/// Note that this function assumes that paths are absolute.
/// File URLs are usually resolves elsewhere e.g. in the `compile` method
/// before encoding to HTML.
fn file_uri_to_data_uri(url: &str) -> String {
    let path = if let Some(path) = url.strip_prefix("file://") {
        PathBuf::from(path)
    } else {
        return url.into();
    };

    // Read the file, convert it to a dataURI, and record it as a dependency
    match fs::read(&path) {
        Ok(bytes) => {
            let mime = match mime_guess::from_path(&path).first() {
                Some(mime) => mime.to_string(),
                None => "image/png".to_string(),
            };
            let data = base64::encode(bytes);
            ["data:", &mime, ";base64,", &data].concat()
        }
        Err(error) => {
            tracing::warn!("Unable to read media file {}: {}", path.display(), error);
            url.into()
        }
    }
}

fn content_url_to_src_attr(content_url: &str, context: &Context) -> String {
    let url = match context.bundle {
        true => file_uri_to_data_uri(content_url),
        false => content_url.to_string(),
    };
    attr("src", &url)
}

impl ToHtml for AudioObjectSimple {
    fn to_html(&self, slot: &str, context: &Context) -> String {
        elem(
            "audio",
            &[
                attr_slot(slot),
                attr_itemtype_string("AudioObject"),
                attr_id(&self.id),
                "controls".to_string(),
                content_url_to_src_attr(&self.content_url, context),
            ],
            "",
        )
    }
}

impl ToHtml for ImageObjectSimple {
    fn to_html(&self, slot: &str, context: &Context) -> String {
        elem_empty(
            "img",
            &[
                attr_slot(slot),
                attr_itemtype_string("ImageObject"),
                attr_id(&self.id),
                content_url_to_src_attr(&self.content_url, context),
            ],
        )
    }
}

impl ToHtml for VideoObjectSimple {
    fn to_html(&self, slot: &str, context: &Context) -> String {
        let src_attr = content_url_to_src_attr(&self.content_url, context);
        let type_attr = match &self.media_type {
            Some(media_type) => attr("type", media_type),
            None => "".to_string(),
        };
        elem(
            "video",
            &[
                attr_slot(slot),
                attr_itemtype_string("VideoObject"),
                attr_id(&self.id),
                "controls".to_string(),
            ],
            &elem("source", &[src_attr, type_attr], ""),
        )
    }
}

impl ToHtml for Cite {
    fn to_html(&self, slot: &str, context: &Context) -> String {
        let content = match &self.content {
            Some(nodes) => nodes.to_html("", context),
            None => {
                // Get the list of references from the root nodes
                let references = match context.root {
                    Node::Article(article) => article.references.clone(),
                    Node::CreativeWork(work) => work.references.clone(),
                    _ => {
                        tracing::warn!("Unhandled root document type");
                        None
                    }
                };
                if let Some(references) = references {
                    // Find the reference that matches the `target`
                    let reference = references
                        .iter()
                        .enumerate()
                        .find_map(|(index, reference)| match reference {
                            // A string reference so match against its index in the references list
                            CreativeWorkReferences::String(_) => {
                                if self.target == format!("ref{}", index + 1)
                                    || self.target == format!("ref{}", index + 1)
                                {
                                    Some((index, reference))
                                } else {
                                    None
                                }
                            }
                            // A `CreativeWork` reference so match against its `id`/
                            CreativeWorkReferences::CreativeWorkTypes(work) => match work {
                                CreativeWorkTypes::Article(Article { id, .. }) => {
                                    if let Some(id) = id.as_deref() {
                                        if self.target == *id {
                                            Some((index, reference))
                                        } else {
                                            None
                                        }
                                    } else {
                                        None
                                    }
                                }
                                _ => None,
                            },
                        });
                    // Create the content for the citation
                    match reference {
                        None => {
                            tracing::warn!("When encoding citation was unable to find reference '{}' in root document", self.target);
                            format!(r#"{}"#, self.target.clone())
                        }
                        Some((index, reference)) => {
                            // Always have a numeric citation
                            let mut content = format!(r#"{}"#, index + 1);
                            // If a `CreativeWorkType` then add authors and year
                            if let CreativeWorkReferences::CreativeWorkTypes(work) = reference {
                                let (authors, date) = match &work {
                                    CreativeWorkTypes::Article(article) => {
                                        (article.authors.clone(), article.date_published.clone())
                                    }
                                    CreativeWorkTypes::CreativeWork(work) => {
                                        (work.authors.clone(), work.date_published.clone())
                                    }
                                    _ => {
                                        tracing::warn!("Unhandled root document type");
                                        (None, None)
                                    }
                                };
                                if let Some(authors) = authors {
                                    let names: Vec<String> = authors
                                        .iter()
                                        .map(|author| match author {
                                            CreativeWorkAuthors::Person(person) => {
                                                match &person.family_names {
                                                    Some(family_names) => family_names.join(" "),
                                                    None => "Anonymous".to_string(),
                                                }
                                            }
                                            CreativeWorkAuthors::Organization(org) => {
                                                match &org.name {
                                                    Some(name) => *name.clone(),
                                                    None => "Anonymous".to_string(),
                                                }
                                            }
                                        })
                                        .collect();
                                    let names = if authors.len() == 1 {
                                        names.join("")
                                    } else if authors.len() == 2 {
                                        format!(r#"{} and {}"#, names[0], names[1])
                                    } else {
                                        format!(r#"{} et al"#, names[0])
                                    };
                                    content += &format!(r#"{}"#, names)
                                }
                                if let Some(date) = date {
                                    if date.value.len() >= 4 {
                                        let year = date.value[..4].to_string();
                                        content += &format!(r#"{}"#, year)
                                    }
                                }
                            }
                            content
                        }
                    }
                } else {
                    tracing::warn!("When encoding citation was unable to find references list in root document");
                    format!(r#"{}"#, self.target.clone())
                }
            }
        };
        elem(
            "cite",
            &[attr_slot(slot), attr_itemtype(self), attr_id(&self.id)],
            &elem("a", &[attr("href", &self.target)], &content),
        )
    }
}

impl ToHtml for CiteGroup {
    fn to_html(&self, slot: &str, context: &Context) -> String {
        elem(
            "span",
            &[attr_slot(slot), attr_itemtype(self), attr_id(&self.id)],
            &concat(&self.items, |cite| cite.to_html("", context)),
        )
    }
}

impl ToHtml for CodeExpression {
    fn to_html(&self, slot: &str, context: &Context) -> String {
        let output = match &self.output {
            Some(output) => output.to_html("", context),
            None => "".to_string(),
        };
        elem(
            "stencila-code-expression",
            &[
                attr_slot(slot),
                attr_itemtype(self),
                attr_id(&self.id),
                attr("programming-language", &self.programming_language),
            ],
            &[
                elem("code", &[attr("slot", "text")], &encode_safe(&self.text)),
                elem("output", &[attr("slot", "output")], &output),
            ]
            .concat(),
        )
    }
}

impl ToHtml for CodeFragment {
    fn to_html(&self, slot: &str, _context: &Context) -> String {
        elem(
            "code",
            &[
                attr_slot(slot),
                attr_itemtype(self),
                attr_id(&self.id),
                match &self.programming_language {
                    Some(lang) => attr("class", &["language-", lang].concat()),
                    None => "".to_string(),
                },
            ],
            &encode_safe(&self.text),
        )
    }
}

impl ToHtml for Link {
    fn to_html(&self, slot: &str, context: &Context) -> String {
        elem(
            "a",
            &[
                attr_slot(slot),
                attr_itemtype(self),
                attr_id(&self.id),
                attr("href", &self.target),
            ],
            &self.content.to_html("", context),
        )
    }
}

impl ToHtml for MathFragment {
    fn to_html(&self, slot: &str, _context: &Context) -> String {
        elem(
            "code",
            &[attr_slot(slot), attr_itemtype(self), attr_id(&self.id)],
            &encode_safe(&self.text),
        )
    }
}

impl ToHtml for Note {
    fn to_html(&self, slot: &str, _context: &Context) -> String {
        elem(
            "code",
            &[
                attr_slot(slot),
                attr_itemtype(self),
                attr_id(&self.id),
                attr("class", "todo"),
            ],
            &json(self),
        )
    }
}

impl ToHtml for Parameter {
    fn to_html(&self, slot: &str, _context: &Context) -> String {
        let input_type = match self.validator.as_deref() {
            Some(ValidatorTypes::NumberValidator(..)) => "number",
            _ => "text",
        };
        let value_attr = match self.value.as_deref() {
            Some(node) => attr("value", &node.to_txt()),
            _ => "".to_string(),
        };
        elem_empty(
            "input",
            &[
                attr_slot(slot),
                attr_itemtype(self),
                attr_id(&self.id),
                attr("type", input_type),
                attr("name", &self.name),
                value_attr,
            ],
        )
    }
}

impl ToHtml for Quote {
    fn to_html(&self, slot: &str, context: &Context) -> String {
        elem(
            "q",
            &[attr_slot(slot), attr_itemtype(self), attr_id(&self.id)],
            &self.content.to_html("", context),
        )
    }
}
<|endoftext|>