/* This Source Code Form is subject to the terms of the Mozilla Public *License,v.2.0.IfacopyoftheMPLwasnotdistributedwiththis
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
use byteorder::{BigEndian, ReadBytesExt, WriteBytesExt}; use firefox_on_glean::metrics::data_storage; use malloc_size_of::{MallocSizeOf, MallocSizeOfOps}; use moz_task::{create_background_task_queue, RunnableBuilder}; use nserror::{
nsresult, NS_ERROR_FAILURE, NS_ERROR_ILLEGAL_INPUT, NS_ERROR_INVALID_ARG,
NS_ERROR_NOT_AVAILABLE, NS_OK,
}; use nsstring::{nsACString, nsAString, nsCStr, nsCString, nsString}; use thin_vec::ThinVec; use xpcom::interfaces::{
nsIDataStorage, nsIDataStorageItem, nsIFile, nsIHandleReportCallback, nsIMemoryReporter,
nsIMemoryReporterManager, nsIObserverService, nsIProperties, nsISerialEventTarget, nsISupports,
}; use xpcom::{xpcom_method, RefPtr, XpCom};
use std::collections::HashMap; use std::ffi::CStr; use std::fs::{File, OpenOptions}; use std::io::{BufRead, BufReader, ErrorKind, Read, Seek, SeekFrom, Write}; use std::os::raw::{c_char, c_void}; use std::path::PathBuf; use std::sync::{Condvar, Mutex}; use std::time::{Duration, SystemTime, UNIX_EPOCH};
/// Helper type for turning the nsIDataStorage::DataType "enum" into a rust /// enum. #[derive(Copy, Clone, Eq, PartialEq)] enum DataType {
Persistent,
Private,
}
impl From<u8> for DataType { fn from(value: u8) -> Self { match value {
nsIDataStorage::Persistent => DataType::Persistent,
nsIDataStorage::Private => DataType::Private,
_ => panic!("invalid nsIDataStorage::DataType"),
}
}
}
impl From<DataType> for u8 { fn from(value: DataType) -> Self { match value {
DataType::Persistent => nsIDataStorage::Persistent,
DataType::Private => nsIDataStorage::Private,
}
}
}
/// Returns the current day in days since the unix epoch, to a maximum of /// u16::MAX days. fn now_in_days() -> u16 { const SECONDS_PER_DAY: u64 = 60 * 60 * 24; let now = SystemTime::now()
.duration_since(UNIX_EPOCH)
.unwrap_or(Duration::ZERO);
(now.as_secs() / SECONDS_PER_DAY)
.try_into()
.unwrap_or(u16::MAX)
}
/// An entry in some DataStorageTable. #[derive(Clone, MallocSizeOf)] struct Entry { /// The number of unique days this Entry has been accessed on.
score: u16, /// The number of days since the unix epoch this Entry was last accessed.
last_accessed: u16, /// The key.
key: Vec<u8>, /// The value.
value: Vec<u8>, /// The slot index of this Entry.
slot_index: usize,
}
impl Entry { /// Constructs an Entry given a line of text from the old DataStorage format. fn from_old_line(line: &str, slot_index: usize, value_length: usize) -> Result<Self, nsresult> { // the old format is <key>\t<score>\t<last accessed>\t<value> let parts: Vec<&str> = line.split('\t').collect(); if parts.len() != 4 { return Err(NS_ERROR_ILLEGAL_INPUT);
} let score = parts[1]
.parse::<u16>()
.map_err(|_| NS_ERROR_ILLEGAL_INPUT)?; let last_accessed = parts[2]
.parse::<u16>()
.map_err(|_| NS_ERROR_ILLEGAL_INPUT)?; let key = Vec::from(parts[0]); if key.len() > KEY_LENGTH { return Err(NS_ERROR_ILLEGAL_INPUT);
} let value = Vec::from(parts[3]); if value.len() > value_length { return Err(NS_ERROR_ILLEGAL_INPUT);
}
Ok(Entry {
score,
last_accessed,
key,
value,
slot_index,
})
}
/// Constructs an Entry given the parsed parts from the current format. fn from_slot(
score: u16,
last_accessed: u16,
key: Vec<u8>,
value: Vec<u8>,
slot_index: usize,
) -> Self {
Entry {
score,
last_accessed,
key,
value,
slot_index,
}
}
/// Constructs a new Entry given a key, value, and index. fn new(key: Vec<u8>, value: Vec<u8>, slot_index: usize) -> Self {
Entry {
score: 1,
last_accessed: now_in_days(),
key,
value,
slot_index,
}
}
/// Constructs a new, empty `Entry` with the given index. Useful for clearing /// slots on disk. fn new_empty(slot_index: usize) -> Self {
Entry {
score: 0,
last_accessed: 0,
key: Vec::new(),
value: Vec::new(),
slot_index,
}
}
/// Returns whether or not this is an empty `Entry` (an empty `Entry` has /// been created with `Entry::new_empty()` or cleared with /// `Entry::clear()`, has 0 `score` and `last_accessed`, and has an empty /// `key` and `value`. fn is_empty(&self) -> bool { self.score == 0 && self.last_accessed == 0 && self.key.is_empty() && self.value.is_empty()
}
/// If this Entry was last accessed on a day different from today, /// increments its score (as well as its last accessed day). /// Returns `true` if the score did in fact change, and `false` otherwise. fn update_score(&mutself) -> bool { let now_in_days = now_in_days(); ifself.last_accessed != now_in_days { self.last_accessed = now_in_days; self.score += 1; true
} else { false
}
}
/// Clear the data stored in this Entry. Useful for clearing a single slot /// on disk. fn clear(&mutself) { // Note: it's important that this preserves slot_index - the writer // needs it to know where to write out the zeroed Entry
*self = Self::new_empty(self.slot_index);
}
}
/// Strips all trailing 0 bytes from the end of the given vec. /// Useful for converting 0-padded keys and values to their original, non-padded /// state. fn strip_zeroes(vec: &mut Vec<u8>) { letmut length = vec.len(); while length > 0 && vec[length - 1] == 0 {
length -= 1;
}
vec.truncate(length);
}
/// Given a slice of entries, returns a Vec<Entry> consisting of each Entry /// with score equal to the minimum score among all entries. fn get_entries_with_minimum_score(entries: &[Entry]) -> Vec<&Entry> { letmut min_score = u16::MAX; letmut min_score_entries = Vec::new(); for entry in entries.iter() { if entry.score < min_score {
min_score = entry.score;
min_score_entries.clear();
} if entry.score == min_score {
min_score_entries.push(entry);
}
}
min_score_entries
}
/// Helper type to map between an entry key and the slot it is stored on. type DataStorageTable = HashMap<Vec<u8>, usize>;
/// The main structure of this implementation. Keeps track of persistent /// and private entries. #[derive(MallocSizeOf)] struct DataStorageInner { /// The key to slot index mapping table for persistent data.
persistent_table: DataStorageTable, /// The persistent entries that are stored on disk.
persistent_slots: Vec<Entry>, /// The key to slot index mapping table for private, temporary data.
private_table: DataStorageTable, /// The private, temporary entries that are not stored on disk. /// This data is cleared upon observing "last-pb-context-exited", and is /// forgotten when the program shuts down.
private_slots: Vec<Entry>, /// The name of the table (e.g. "SiteSecurityServiceState").
name: String, /// The maximum permitted length of values.
value_length: usize, /// A PathBuf holding the location of the profile directory, if available.
maybe_profile_path: Option<PathBuf>, /// A serial event target to post tasks to, to write out changed persistent /// data in the background. #[ignore_malloc_size_of = "not implemented for nsISerialEventTarget"]
write_queue: Option<RefPtr<nsISerialEventTarget>>,
}
/// Initializes the DataStorageInner. If the profile directory is not /// present, does nothing. If the backing file is available, processes it. /// Otherwise, if the old backing file is available, migrates it to the /// current format. fn initialize(&mutself) -> Result<(), nsresult> { let Some(profile_path) = self.maybe_profile_path.as_ref() else { return Ok(());
}; letmut backing_path = profile_path.clone();
backing_path.push(format!("{}.bin", &self.name)); letmut old_backing_path = profile_path.clone();
old_backing_path.push(format!("{}.txt", &self.name)); if backing_path.exists() { self.read(backing_path)
} elseif old_backing_path.exists() { self.read_old_format(old_backing_path)
} else {
Ok(())
}
}
/// Reads the backing file, processing each slot. fn read(&mutself, path: PathBuf) -> Result<(), nsresult> { let f = OpenOptions::new()
.read(true)
.write(true)
.create(true)
.open(path)
.map_err(|_| NS_ERROR_FAILURE)?; letmut backing_file = BufReader::new(f); letmut slots = Vec::new(); // First read each entry into the persistent slots list. while slots.len() < MAX_SLOTS { iflet Some(entry) = self.process_slot(&mut backing_file, slots.len())? {
slots.push(entry);
} else { break;
}
} self.persistent_slots = slots; // Then build the key -> slot index lookup table. self.persistent_table = self
.persistent_slots
.iter()
.filter(|slot| !slot.is_empty())
.map(|slot| (slot.key.clone(), slot.slot_index))
.collect(); let num_entries = self.persistent_table.len() as i64; matchself.name.as_str() { "AlternateServices" => data_storage::alternate_services.set(num_entries), "ClientAuthRememberList" => data_storage::client_auth_remember_list.set(num_entries), "SiteSecurityServiceState" => {
data_storage::site_security_service_state.set(num_entries)
}
_ => panic!("unknown nsIDataStorageManager::DataStorage"),
}
Ok(())
}
/// Processes a slot (via a reader) by reading its metadata, key, and /// value. If the checksum fails or if the score or last accessed fields /// are 0, this is an empty slot. Otherwise, un-0-pads the key and value, /// creates a new Entry, and puts it in the persistent table. fn process_slot<R: Read>(
&mutself,
reader: &mut R,
slot_index: usize,
) -> Result<Option<Entry>, nsresult> { // Format is [checksum][score][last accessed][key][value], where // checksum is 2 bytes big-endian, score and last accessed are 2 bytes // big-endian, key is KEY_LENGTH bytes (currently 256), and value is // self.value_length bytes (1024 for most instances, but 24 for // SiteSecurityServiceState - see DataStorageManager::Get). letmut checksum = match reader.read_u16::<BigEndian>() {
Ok(checksum) => checksum, // The file may be shorter than expected due to unoccupied slots. // Every slot after the last read slot is unoccupied.
Err(e) if e.kind() == ErrorKind::UnexpectedEof => return Ok(None),
Err(_) => return Err(NS_ERROR_FAILURE),
}; let score = reader
.read_u16::<BigEndian>()
.map_err(|_| NS_ERROR_FAILURE)?;
checksum ^= score; let last_accessed = reader
.read_u16::<BigEndian>()
.map_err(|_| NS_ERROR_FAILURE)?;
checksum ^= last_accessed;
// If this slot is incomplete, corrupted, or empty, treat it as empty. if checksum != 0 || score == 0 || last_accessed == 0 { // This slot is empty. return Ok(Some(Entry::new_empty(slot_index)));
}
/// Migrates from the old format to the current format. fn read_old_format(&mutself, path: PathBuf) -> Result<(), nsresult> { let file = File::open(path).map_err(|_| NS_ERROR_FAILURE)?; let reader = BufReader::new(file); // First read each line in the old file into the persistent slots list. // The old format was limited to 1024 lines, so only expect that many. for line in reader.lines().flatten().take(1024) { match Entry::from_old_line(&line, self.persistent_slots.len(), self.value_length) {
Ok(entry) => { ifself.persistent_slots.len() >= MAX_SLOTS {
warn!("too many lines in old DataStorage format"); break;
} if !entry.is_empty() { self.persistent_slots.push(entry);
} else {
warn!("empty entry in old DataStorage format?");
}
}
Err(_) => {
warn!("failed to migrate a line from old DataStorage format");
}
}
} // Then build the key -> slot index lookup table. self.persistent_table = self
.persistent_slots
.iter()
.filter(|slot| !slot.is_empty())
.map(|slot| (slot.key.clone(), slot.slot_index))
.collect(); // Finally, write out the migrated data to the new backing file. self.async_write_entries(self.persistent_slots.clone())?; let num_entries = self.persistent_table.len() as i64; matchself.name.as_str() { "AlternateServices" => data_storage::alternate_services.set(num_entries), "ClientAuthRememberList" => data_storage::client_auth_remember_list.set(num_entries), "SiteSecurityServiceState" => {
data_storage::site_security_service_state.set(num_entries)
}
_ => panic!("unknown nsIDataStorageManager::DataStorage"),
}
Ok(())
}
/// Given an `Entry` and `DataType`, this function updates the internal /// list of slots and the mapping from keys to slot indices. If the slot /// assigned to the `Entry` is already occupied, the existing `Entry` is /// evicted. /// After updating internal state, if the type of this entry is persistent, /// this function dispatches an event to asynchronously write the data out. fn put_internal(&mutself, entry: Entry, type_: DataType) -> Result<(), nsresult> { let (table, slots) = self.get_table_and_slots_for_type_mut(type_); if entry.slot_index < slots.len() { let entry_to_evict = &slots[entry.slot_index]; if !entry_to_evict.is_empty() {
table.remove(&entry_to_evict.key);
}
} let _ = table.insert(entry.key.clone(), entry.slot_index); if entry.slot_index < slots.len() {
slots[entry.slot_index] = entry.clone();
} elseif entry.slot_index == slots.len() {
slots.push(entry.clone());
} else {
panic!( "put_internal should not have been given an Entry with slot_index > slots.len()"
);
} if type_ == DataType::Persistent { self.async_write_entry(entry)?;
}
Ok(())
}
/// Returns the total length of each slot on disk. fn slot_length(&self) -> usize { // Checksum is 2 bytes, and score and last accessed are 2 bytes each. 2 + 2 + 2 + KEY_LENGTH + self.value_length
}
/// Gets the next free slot index, or determines a slot to evict (but /// doesn't actually perform the eviction - the caller must do that). fn get_free_slot_or_slot_to_evict(&self, type_: DataType) -> usize { let (_, slots) = self.get_table_and_slots_for_type(type_); let maybe_unoccupied_slot = slots
.iter()
.enumerate()
.find(|(_, maybe_empty_entry)| maybe_empty_entry.is_empty()); iflet Some((unoccupied_slot, _)) = maybe_unoccupied_slot { return unoccupied_slot;
} // If `slots` isn't full, the next free slot index is one more than the // current last index. if slots.len() < MAX_SLOTS { return slots.len();
} // If there isn't an unoccupied slot, evict the entry with the lowest score. let min_score_entries = get_entries_with_minimum_score(&slots); // `min_score_entry` is the oldest Entry with the minimum score. // There must be at least one such Entry, so unwrap it or abort. let min_score_entry = min_score_entries
.iter()
.min_by_key(|e| e.last_accessed)
.unwrap();
min_score_entry.slot_index
}
/// Helper function to get a handle on the slot list and key to slot index /// mapping for the given `DataType`. fn get_table_and_slots_for_type(&self, type_: DataType) -> (&DataStorageTable, &[Entry]) { match type_ {
DataType::Persistent => (&self.persistent_table, &self.persistent_slots),
DataType::Private => (&self.private_table, &self.private_slots),
}
}
/// Helper function to get a mutable handle on the slot list and key to /// slot index mapping for the given `DataType`. fn get_table_and_slots_for_type_mut(
&mutself,
type_: DataType,
) -> (&mut DataStorageTable, &mut Vec<Entry>) { match type_ {
DataType::Persistent => (&mutself.persistent_table, &='color:red'>mutself.persistent_slots),
DataType::Private => (&mutself.private_table, &mutself.private_slots),
}
}
/// Helper function to look up an `Entry` by its key and type. fn get_entry(&mutself, key: &[u8], type_: DataType) -> Option<&tyle='color:red'>mut Entry> { let (table, slots) = self.get_table_and_slots_for_type_mut(type_); let slot_index = table.get(key)?;
Some(&mut slots[*slot_index])
}
/// Gets a value by key, if available. Updates the Entry's score when appropriate. fn get(&mutself, key: &[u8], type_: DataType) -> Result<Vec<u8>, nsresult> { let Some(entry) = self.get_entry(key, type_) else { return Err(NS_ERROR_NOT_AVAILABLE);
}; let value = entry.value.clone(); if entry.update_score() && type_ == DataType::Persistent { let entry = entry.clone(); self.async_write_entry(entry)?;
}
Ok(value)
}
/// Inserts or updates a value by key. Updates the Entry's score if applicable. fn put(&mutself, key: Vec<u8>, value: Vec<u8>, type_: DataType) -> Result<(), nsresult> { if key.len() > KEY_LENGTH || value.len() > self.value_length { return Err(NS_ERROR_INVALID_ARG);
} iflet Some(existing_entry) = self.get_entry(&key, type_) { let data_changed = existing_entry.value != value; if data_changed {
existing_entry.value = value;
} if (existing_entry.update_score() || data_changed) && type_ == DataType::Persistent { let entry = existing_entry.clone(); self.async_write_entry(entry)?;
}
Ok(())
} else { let slot_index = self.get_free_slot_or_slot_to_evict(type_); let entry = Entry::new(key.clone(), value, slot_index); self.put_internal(entry, type_)
}
}
/// Removes an Entry by key, if it is present. fn remove(&mutself, key: &Vec<u8>, type_: DataType) -> Result<(), nsresult> { let (table, slots) = self.get_table_and_slots_for_type_mut(type_); let Some(slot_index) = table.remove(key) else { return Ok(());
}; let entry = &mut slots[slot_index];
entry.clear(); if type_ == DataType::Persistent { let entry = entry.clone(); self.async_write_entry(entry)?;
}
Ok(())
}
/// Clears all tables and the backing persistent file. fn clear(&mutself) -> Result<(), nsresult> { self.persistent_table.clear(); self.private_table.clear(); self.persistent_slots.clear(); self.private_slots.clear(); let Some(profile_path) = self.maybe_profile_path.clone() else { return Ok(());
}; let Some(write_queue) = self.write_queue.clone() else { return Ok(());
}; let name = self.name.clone();
RunnableBuilder::new("data_storage::remove_backing_files", move || { let old_backing_path = profile_path.join(format!("{name}.txt")); let _ = std::fs::remove_file(old_backing_path); let backing_path = profile_path.join(format!("{name}.bin")); let _ = std::fs::remove_file(backing_path);
})
.may_block(true)
.dispatch(write_queue.coerce())
}
/// Clears only data in the private table. fn clear_private_data(&mutself) { self.private_table.clear(); self.private_slots.clear();
}
/// Asynchronously writes the given entry on the background serial event /// target. fn async_write_entry(&self, entry: Entry) -> Result<(), nsresult> { self.async_write_entries(vec![entry])
}
/// Asynchronously writes the given entries on the background serial event /// target. fn async_write_entries(&self, entries: Vec<Entry>) -> Result<(), nsresult> { let Some(mut backing_path) = self.maybe_profile_path.clone() else { return Ok(());
}; let Some(write_queue) = self.write_queue.clone() else { return Ok(());
};
backing_path.push(format!("{}.bin", &self.name)); let value_length = self.value_length; let slot_length = self.slot_length();
RunnableBuilder::new("data_storage::write_entries", move || { let _ = write_entries(entries, backing_path, value_length, slot_length);
})
.may_block(true)
.dispatch(write_queue.coerce())
}
/// Drop the write queue to prevent further writes. fn drop_write_queue(&mutself) { let _ = self.write_queue.take();
}
/// Takes a callback that is run for each entry in each table. fn for_each<F>(&self, mut f: F) where
F: FnMut(&Entry, DataType),
{ for entry inself
.persistent_slots
.iter()
.filter(|entry| !entry.is_empty())
{
f(entry, DataType::Persistent);
} for entry inself.private_slots.iter().filter(|entry| !entry.is_empty()) {
f(entry, DataType::Private);
}
}
/// Collects the memory used by this DataStorageInner. fn collect_reports(
&self,
ops: &mut MallocSizeOfOps,
callback: &nsIHandleReportCallback,
data: Option<&nsISupports>,
) -> Result<(), nsresult> { let size = self.size_of(ops); let data = match data {
Some(data) => data as *const nsISupports,
None => std::ptr::null() as *const nsISupports,
}; unsafe {
callback
.Callback(
&nsCStr::new() as &nsACString,
&nsCString::from(format!("explicit/data-storage/{}", self.name)) as &nsACString,
nsIMemoryReporter::KIND_HEAP,
nsIMemoryReporter::UNITS_BYTES,
size as i64,
&nsCStr::from("Memory used by PSM data storage cache") as &nsACString,
data,
)
.to_result()
}
}
}
fn initialize(&self) -> Result<(), nsresult> { letmut storage = self.data.lock().unwrap(); // If this fails, the implementation is "ready", but it probably won't // store any data persistently. This is expected in cases where there // is no profile directory. let _ = storage.initialize(); self.indicate_ready()
}
Die Informationen auf dieser Webseite wurden
nach bestem Wissen sorgfältig zusammengestellt. Es wird jedoch weder Vollständigkeit, noch Richtigkeit,
noch Qualität der bereit gestellten Informationen zugesichert.
Bemerkung:
Die farbliche Syntaxdarstellung und die Messung sind noch experimentell.