summaryrefslogtreecommitdiffstats
path: root/security/manager/ssl/data_storage
diff options
context:
space:
mode:
authorDaniel Baumann <daniel.baumann@progress-linux.org>2024-04-19 00:47:55 +0000
committerDaniel Baumann <daniel.baumann@progress-linux.org>2024-04-19 00:47:55 +0000
commit26a029d407be480d791972afb5975cf62c9360a6 (patch)
treef435a8308119effd964b339f76abb83a57c29483 /security/manager/ssl/data_storage
parentInitial commit. (diff)
downloadfirefox-26a029d407be480d791972afb5975cf62c9360a6.tar.xz
firefox-26a029d407be480d791972afb5975cf62c9360a6.zip
Adding upstream version 124.0.1.upstream/124.0.1
Signed-off-by: Daniel Baumann <daniel.baumann@progress-linux.org>
Diffstat (limited to 'security/manager/ssl/data_storage')
-rw-r--r--security/manager/ssl/data_storage/Cargo.toml18
-rw-r--r--security/manager/ssl/data_storage/src/lib.rs1017
2 files changed, 1035 insertions, 0 deletions
diff --git a/security/manager/ssl/data_storage/Cargo.toml b/security/manager/ssl/data_storage/Cargo.toml
new file mode 100644
index 0000000000..a90b44c69d
--- /dev/null
+++ b/security/manager/ssl/data_storage/Cargo.toml
@@ -0,0 +1,18 @@
+[package]
+name = "data_storage"
+version = "0.0.1"
+edition = "2021"
+license = "MPL-2.0"
+
+[dependencies]
+byteorder = "1"
+cstr = "0.2"
+firefox-on-glean = { path = "../../../../toolkit/components/glean/api" }
+log = "0.4"
+malloc_size_of_derive = { path = "../../../../xpcom/rust/malloc_size_of_derive" }
+moz_task = { path = "../../../../xpcom/rust/moz_task" }
+nserror = { path = "../../../../xpcom/rust/nserror" }
+nsstring = { path = "../../../../xpcom/rust/nsstring" }
+thin-vec = { version = "0.2.1", features = ["gecko-ffi"] }
+wr_malloc_size_of = { path = "../../../../gfx/wr/wr_malloc_size_of" }
+xpcom = { path = "../../../../xpcom/rust/xpcom" }
diff --git a/security/manager/ssl/data_storage/src/lib.rs b/security/manager/ssl/data_storage/src/lib.rs
new file mode 100644
index 0000000000..8cbbf05563
--- /dev/null
+++ b/security/manager/ssl/data_storage/src/lib.rs
@@ -0,0 +1,1017 @@
+/* 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/. */
+
+extern crate byteorder;
+#[macro_use]
+extern crate cstr;
+extern crate firefox_on_glean;
+#[macro_use]
+extern crate log;
+#[macro_use]
+extern crate malloc_size_of_derive;
+extern crate moz_task;
+extern crate nserror;
+extern crate thin_vec;
+extern crate wr_malloc_size_of;
+#[macro_use]
+extern crate xpcom;
+
+use wr_malloc_size_of as malloc_size_of;
+
+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(&mut self) -> bool {
+ let now_in_days = now_in_days();
+ if self.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(&mut self) {
+ // 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>) {
+ let mut 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> {
+ let mut min_score = u16::MAX;
+ let mut 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
+}
+
+const MAX_SLOTS: usize = 2048;
+const KEY_LENGTH: usize = 256;
+
+/// 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>>,
+}
+
+impl DataStorageInner {
+ fn new(
+ name: String,
+ value_length: usize,
+ maybe_profile_path: Option<PathBuf>,
+ ) -> Result<Self, nsresult> {
+ Ok(DataStorageInner {
+ persistent_table: DataStorageTable::new(),
+ persistent_slots: Vec::new(),
+ private_table: DataStorageTable::new(),
+ private_slots: Vec::new(),
+ name,
+ value_length,
+ maybe_profile_path,
+ write_queue: Some(create_background_task_queue(cstr!("data_storage"))?),
+ })
+ }
+
+ /// 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(&mut self) -> Result<(), nsresult> {
+ let Some(profile_path) = self.maybe_profile_path.as_ref() else {
+ return Ok(());
+ };
+ let mut backing_path = profile_path.clone();
+ backing_path.push(format!("{}.bin", &self.name));
+ let mut old_backing_path = profile_path.clone();
+ old_backing_path.push(format!("{}.txt", &self.name));
+ if backing_path.exists() {
+ self.read(backing_path)
+ } else if old_backing_path.exists() {
+ self.read_old_format(old_backing_path)
+ } else {
+ Ok(())
+ }
+ }
+
+ /// Reads the backing file, processing each slot.
+ fn read(&mut self, path: PathBuf) -> Result<(), nsresult> {
+ let f = OpenOptions::new()
+ .read(true)
+ .write(true)
+ .create(true)
+ .open(path)
+ .map_err(|_| NS_ERROR_FAILURE)?;
+ let mut backing_file = BufReader::new(f);
+ let mut slots = Vec::new();
+ // First read each entry into the persistent slots list.
+ while slots.len() < MAX_SLOTS {
+ if let 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;
+ match self.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>(
+ &mut self,
+ 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).
+ let mut 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;
+
+ let mut key = vec![0u8; KEY_LENGTH];
+ reader.read_exact(&mut key).map_err(|_| NS_ERROR_FAILURE)?;
+ for mut chunk in key.chunks(2) {
+ checksum ^= chunk
+ .read_u16::<BigEndian>()
+ .map_err(|_| NS_ERROR_FAILURE)?;
+ }
+ strip_zeroes(&mut key);
+ let mut value = vec![0u8; self.value_length];
+ reader
+ .read_exact(&mut value)
+ .map_err(|_| NS_ERROR_FAILURE)?;
+ for mut chunk in value.chunks(2) {
+ checksum ^= chunk
+ .read_u16::<BigEndian>()
+ .map_err(|_| NS_ERROR_FAILURE)?;
+ }
+ strip_zeroes(&mut value);
+
+ // 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)));
+ }
+
+ Ok(Some(Entry::from_slot(
+ score,
+ last_accessed,
+ key,
+ value,
+ slot_index,
+ )))
+ }
+
+ /// Migrates from the old format to the current format.
+ fn read_old_format(&mut self, 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) => {
+ if self.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;
+ match self.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(&mut self, 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();
+ } else if 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());
+ if let 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(
+ &mut self,
+ type_: DataType,
+ ) -> (&mut DataStorageTable, &mut Vec<Entry>) {
+ match type_ {
+ DataType::Persistent => (&mut self.persistent_table, &mut self.persistent_slots),
+ DataType::Private => (&mut self.private_table, &mut self.private_slots),
+ }
+ }
+
+ /// Helper function to look up an `Entry` by its key and type.
+ fn get_entry(&mut self, key: &[u8], type_: DataType) -> Option<&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(&mut self, 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(&mut self, 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);
+ }
+ if let 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(&mut self, 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(&mut self) -> 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(&mut self) {
+ 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(&mut self) {
+ 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 in &self.persistent_slots {
+ f(entry, DataType::Persistent);
+ }
+ for entry in &self.private_slots {
+ 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()
+ }
+ }
+}
+
+#[xpcom(implement(nsIDataStorageItem), atomic)]
+struct DataStorageItem {
+ key: nsCString,
+ value: nsCString,
+ type_: u8,
+}
+
+impl DataStorageItem {
+ xpcom_method!(get_key => GetKey() -> nsACString);
+ fn get_key(&self) -> Result<nsCString, nsresult> {
+ Ok(self.key.clone())
+ }
+
+ xpcom_method!(get_value => GetValue() -> nsACString);
+ fn get_value(&self) -> Result<nsCString, nsresult> {
+ Ok(self.value.clone())
+ }
+
+ xpcom_method!(get_type => GetType() -> u8);
+ fn get_type(&self) -> Result<u8, nsresult> {
+ Ok(self.type_)
+ }
+}
+
+type VoidPtrToSizeFn = unsafe extern "C" fn(ptr: *const c_void) -> usize;
+
+/// Helper struct that coordinates xpcom access to the DataStorageInner that
+/// actually holds the data.
+#[xpcom(implement(nsIDataStorage, nsIMemoryReporter, nsIObserver), atomic)]
+struct DataStorage {
+ ready: (Mutex<bool>, Condvar),
+ data: Mutex<DataStorageInner>,
+ size_of_op: VoidPtrToSizeFn,
+ enclosing_size_of_op: VoidPtrToSizeFn,
+}
+
+impl DataStorage {
+ xpcom_method!(get => Get(key: *const nsACString, type_: u8) -> nsACString);
+ fn get(&self, key: &nsACString, type_: u8) -> Result<nsCString, nsresult> {
+ self.wait_for_ready()?;
+ let mut storage = self.data.lock().unwrap();
+ storage
+ .get(&Vec::from(key.as_ref()), type_.into())
+ .map(|data| nsCString::from(data))
+ }
+
+ xpcom_method!(put => Put(key: *const nsACString, value: *const nsACString, type_: u8));
+ fn put(&self, key: &nsACString, value: &nsACString, type_: u8) -> Result<(), nsresult> {
+ self.wait_for_ready()?;
+ let mut storage = self.data.lock().unwrap();
+ storage.put(
+ Vec::from(key.as_ref()),
+ Vec::from(value.as_ref()),
+ type_.into(),
+ )
+ }
+
+ xpcom_method!(remove => Remove(key: *const nsACString, type_: u8));
+ fn remove(&self, key: &nsACString, type_: u8) -> Result<(), nsresult> {
+ self.wait_for_ready()?;
+ let mut storage = self.data.lock().unwrap();
+ storage.remove(&Vec::from(key.as_ref()), type_.into())?;
+ Ok(())
+ }
+
+ xpcom_method!(clear => Clear());
+ fn clear(&self) -> Result<(), nsresult> {
+ self.wait_for_ready()?;
+ let mut storage = self.data.lock().unwrap();
+ storage.clear()?;
+ Ok(())
+ }
+
+ xpcom_method!(is_ready => IsReady() -> bool);
+ fn is_ready(&self) -> Result<bool, nsresult> {
+ let ready = self.ready.0.lock().unwrap();
+ Ok(*ready)
+ }
+
+ xpcom_method!(get_all => GetAll() -> ThinVec<Option<RefPtr<nsIDataStorageItem>>>);
+ fn get_all(&self) -> Result<ThinVec<Option<RefPtr<nsIDataStorageItem>>>, nsresult> {
+ self.wait_for_ready()?;
+ let storage = self.data.lock().unwrap();
+ let mut items = ThinVec::new();
+ let add_item = |entry: &Entry, data_type: DataType| {
+ let item = DataStorageItem::allocate(InitDataStorageItem {
+ key: entry.key.clone().into(),
+ value: entry.value.clone().into(),
+ type_: data_type.into(),
+ });
+ items.push(Some(RefPtr::new(item.coerce())));
+ };
+ storage.for_each(add_item);
+ Ok(items)
+ }
+
+ fn indicate_ready(&self) -> Result<(), nsresult> {
+ let (ready_mutex, condvar) = &self.ready;
+ let mut ready = ready_mutex.lock().unwrap();
+ *ready = true;
+ condvar.notify_all();
+ Ok(())
+ }
+
+ fn wait_for_ready(&self) -> Result<(), nsresult> {
+ let (ready_mutex, condvar) = &self.ready;
+ let mut ready = ready_mutex.lock().unwrap();
+ while !*ready {
+ ready = condvar.wait(ready).unwrap();
+ }
+ Ok(())
+ }
+
+ fn initialize(&self) -> Result<(), nsresult> {
+ let mut 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()
+ }
+
+ xpcom_method!(collect_reports => CollectReports(callback: *const nsIHandleReportCallback, data: *const nsISupports, anonymize: bool));
+ fn collect_reports(
+ &self,
+ callback: &nsIHandleReportCallback,
+ data: Option<&nsISupports>,
+ _anonymize: bool,
+ ) -> Result<(), nsresult> {
+ let storage = self.data.lock().unwrap();
+ let mut ops = MallocSizeOfOps::new(self.size_of_op, Some(self.enclosing_size_of_op));
+ storage.collect_reports(&mut ops, callback, data)
+ }
+
+ xpcom_method!(observe => Observe(_subject: *const nsISupports, topic: *const c_char, _data: *const u16));
+ unsafe fn observe(
+ &self,
+ _subject: Option<&nsISupports>,
+ topic: *const c_char,
+ _data: *const u16,
+ ) -> Result<(), nsresult> {
+ let mut storage = self.data.lock().unwrap();
+ let topic = CStr::from_ptr(topic);
+ // Observe shutdown - prevent any further writes.
+ // The backing file is in the profile directory, so stop writing when
+ // that goes away.
+ // "xpcom-shutdown-threads" is a backstop for situations where the
+ // "profile-before-change" notification is not emitted.
+ if topic == cstr!("profile-before-change") || topic == cstr!("xpcom-shutdown-threads") {
+ storage.drop_write_queue();
+ } else if topic == cstr!("last-pb-context-exited") {
+ storage.clear_private_data();
+ }
+ Ok(())
+ }
+}
+
+/// Given some entries, the path of the backing file, and metadata about Entry
+/// length, writes an Entry to the backing file in the appropriate slot.
+/// Creates the backing file if it does not exist.
+fn write_entries(
+ entries: Vec<Entry>,
+ backing_path: PathBuf,
+ value_length: usize,
+ slot_length: usize,
+) -> Result<(), std::io::Error> {
+ let mut backing_file = OpenOptions::new()
+ .write(true)
+ .create(true)
+ .open(backing_path)?;
+ let Some(max_slot_index) = entries.iter().map(|entry| entry.slot_index).max() else {
+ return Ok(()); // can only happen if entries is empty
+ };
+ let necessary_len = ((max_slot_index + 1) * slot_length) as u64;
+ if backing_file.metadata()?.len() < necessary_len {
+ backing_file.set_len(necessary_len)?;
+ }
+ let mut buf = vec![0u8; slot_length];
+ for entry in entries {
+ let mut buf_writer = buf.as_mut_slice();
+ buf_writer.write_u16::<BigEndian>(0)?; // set checksum to 0 for now
+ let mut checksum = entry.score;
+ buf_writer.write_u16::<BigEndian>(entry.score)?;
+ checksum ^= entry.last_accessed;
+ buf_writer.write_u16::<BigEndian>(entry.last_accessed)?;
+ for mut chunk in entry.key.chunks(2) {
+ if chunk.len() == 1 {
+ checksum ^= (chunk[0] as u16) << 8;
+ } else {
+ checksum ^= chunk.read_u16::<BigEndian>()?;
+ }
+ }
+ if entry.key.len() > KEY_LENGTH {
+ continue;
+ }
+ buf_writer.write_all(&entry.key)?;
+ let (key_remainder, mut buf_writer) = buf_writer.split_at_mut(KEY_LENGTH - entry.key.len());
+ key_remainder.fill(0);
+ for mut chunk in entry.value.chunks(2) {
+ if chunk.len() == 1 {
+ checksum ^= (chunk[0] as u16) << 8;
+ } else {
+ checksum ^= chunk.read_u16::<BigEndian>()?;
+ }
+ }
+ if entry.value.len() > value_length {
+ continue;
+ }
+ buf_writer.write_all(&entry.value)?;
+ buf_writer.fill(0);
+
+ backing_file.seek(SeekFrom::Start((entry.slot_index * slot_length) as u64))?;
+ backing_file.write_all(&buf)?;
+ backing_file.flush()?;
+ backing_file.seek(SeekFrom::Start((entry.slot_index * slot_length) as u64))?;
+ backing_file.write_u16::<BigEndian>(checksum)?;
+ }
+ Ok(())
+}
+
+/// Uses the xpcom directory service to try to obtain the profile directory.
+fn get_profile_path() -> Result<PathBuf, nsresult> {
+ let directory_service: RefPtr<nsIProperties> =
+ xpcom::components::Directory::service().map_err(|_| NS_ERROR_FAILURE)?;
+ let mut profile_dir = xpcom::GetterAddrefs::<nsIFile>::new();
+ unsafe {
+ directory_service
+ .Get(
+ cstr!("ProfD").as_ptr(),
+ &nsIFile::IID,
+ profile_dir.void_ptr(),
+ )
+ .to_result()?;
+ }
+ let profile_dir = profile_dir.refptr().ok_or(NS_ERROR_FAILURE)?;
+ let mut profile_path = nsString::new();
+ unsafe {
+ (*profile_dir).GetPath(&mut *profile_path).to_result()?;
+ }
+ let profile_path = String::from_utf16(profile_path.as_ref()).map_err(|_| NS_ERROR_FAILURE)?;
+ Ok(PathBuf::from(profile_path))
+}
+
+fn make_data_storage_internal(
+ basename: &str,
+ value_length: usize,
+ size_of_op: VoidPtrToSizeFn,
+ enclosing_size_of_op: VoidPtrToSizeFn,
+) -> Result<RefPtr<nsIDataStorage>, nsresult> {
+ let maybe_profile_path = get_profile_path().ok();
+ let data_storage = DataStorage::allocate(InitDataStorage {
+ ready: (Mutex::new(false), Condvar::new()),
+ data: Mutex::new(DataStorageInner::new(
+ basename.to_string(),
+ value_length,
+ maybe_profile_path,
+ )?),
+ size_of_op,
+ enclosing_size_of_op,
+ });
+ // Initialize the DataStorage on a background thread.
+ let data_storage_for_background_initialization = data_storage.clone();
+ RunnableBuilder::new("data_storage::initialize", move || {
+ let _ = data_storage_for_background_initialization.initialize();
+ })
+ .may_block(true)
+ .dispatch_background_task()?;
+
+ // Observe shutdown and when the last private browsing context exits.
+ if let Ok(observer_service) = xpcom::components::Observer::service::<nsIObserverService>() {
+ unsafe {
+ observer_service
+ .AddObserver(
+ data_storage.coerce(),
+ cstr!("profile-before-change").as_ptr(),
+ false,
+ )
+ .to_result()?;
+ observer_service
+ .AddObserver(
+ data_storage.coerce(),
+ cstr!("xpcom-shutdown-threads").as_ptr(),
+ false,
+ )
+ .to_result()?;
+ observer_service
+ .AddObserver(
+ data_storage.coerce(),
+ cstr!("last-pb-context-exited").as_ptr(),
+ false,
+ )
+ .to_result()?;
+ }
+ }
+
+ // Register the DataStorage as a memory reporter.
+ if let Some(memory_reporter_manager) = xpcom::get_service::<nsIMemoryReporterManager>(cstr!(
+ "@mozilla.org/memory-reporter-manager;1"
+ )) {
+ unsafe {
+ memory_reporter_manager
+ .RegisterStrongReporter(data_storage.coerce())
+ .to_result()?;
+ }
+ }
+
+ Ok(RefPtr::new(data_storage.coerce()))
+}
+
+#[no_mangle]
+pub unsafe extern "C" fn make_data_storage(
+ basename: *const nsAString,
+ value_length: usize,
+ size_of_op: VoidPtrToSizeFn,
+ enclosing_size_of_op: VoidPtrToSizeFn,
+ result: *mut *const xpcom::interfaces::nsIDataStorage,
+) -> nsresult {
+ if basename.is_null() || result.is_null() {
+ return NS_ERROR_INVALID_ARG;
+ }
+ let basename = &*basename;
+ let basename = basename.to_string();
+ match make_data_storage_internal(&basename, value_length, size_of_op, enclosing_size_of_op) {
+ Ok(val) => val.forget(&mut *result),
+ Err(e) => return e,
+ }
+ NS_OK
+}