//! Service Worker registration persistence. //! //! Each origin gets one file under `~/.we/service-workers/`. The binary format //! is owned by `we_js::service_worker::RegistrationStore`. use std::fs; use std::path::PathBuf; use we_js::service_worker::RegistrationStore; use we_url::Origin; #[derive(Debug, Clone, PartialEq, Eq)] pub enum StoragePartition { Persistent, Private(String), } pub struct ServiceWorkerManager { storage_dir: PathBuf, } impl Default for ServiceWorkerManager { fn default() -> Self { Self::new() } } impl ServiceWorkerManager { pub fn new() -> Self { let home = std::env::var("HOME").unwrap_or_else(|_| "/tmp".to_string()); Self::with_partition( PathBuf::from(home).join(".we"), StoragePartition::Persistent, ) } pub fn with_partition(base_dir: PathBuf, partition: StoragePartition) -> Self { let storage_dir = match partition { StoragePartition::Persistent => base_dir.join("service-workers"), StoragePartition::Private(session_id) => base_dir .join("service-workers-private") .join(safe_path_component(&session_id)), }; Self { storage_dir } } pub fn with_dir(dir: PathBuf) -> Self { Self { storage_dir: dir } } fn file_path(&self, origin: &Origin) -> Option { match origin { Origin::Opaque => None, Origin::Tuple(..) => { let serialized = origin.serialize(); Some(self.storage_dir.join(safe_path_component(&serialized))) } } } pub fn load(&self, origin: &Origin) -> RegistrationStore { let path = match self.file_path(origin) { Some(p) => p, None => return RegistrationStore::new(), }; match fs::read(path) { Ok(data) => RegistrationStore::deserialize(&data).unwrap_or_default(), Err(_) => RegistrationStore::new(), } } pub fn save(&self, origin: &Origin, store: &RegistrationStore) { let path = match self.file_path(origin) { Some(p) => p, None => return, }; if let Some(parent) = path.parent() { let _ = fs::create_dir_all(parent); } let tmp_path = path.with_extension("tmp"); if fs::write(&tmp_path, store.serialize()).is_ok() { let _ = fs::rename(tmp_path, path); } } pub fn clear_origin(&self, origin: &Origin) -> bool { let path = match self.file_path(origin) { Some(p) => p, None => return false, }; match fs::remove_file(path) { Ok(()) => true, Err(e) if e.kind() == std::io::ErrorKind::NotFound => false, Err(_) => false, } } pub fn clear_all(&self) -> bool { match fs::remove_dir_all(&self.storage_dir) { Ok(()) => true, Err(e) if e.kind() == std::io::ErrorKind::NotFound => false, Err(_) => false, } } } fn safe_path_component(input: &str) -> String { input .chars() .map(|c| match c { '/' | ':' | '?' | '#' | '\\' | '*' | '"' | '<' | '>' | '|' => '_', c => c, }) .collect() } #[cfg(test)] mod tests { use super::*; use we_url::Url; #[test] fn roundtrip_persistence() { let dir = std::env::temp_dir().join("we_service_worker_roundtrip"); let _ = fs::remove_dir_all(&dir); let mgr = ServiceWorkerManager::with_dir(dir.clone()); let base = Url::parse("https://example.com/app/page.html").unwrap(); let origin = base.origin(); let mut store = RegistrationStore::new(); store .register(&base, "sw.js", None, b"script-body".to_vec()) .unwrap(); mgr.save(&origin, &store); let loaded = mgr.load(&origin); assert_eq!(loaded.records().len(), 1); assert_eq!(loaded.records()[0].scope_url, "https://example.com/app/"); assert_eq!(loaded.records()[0].script_bytes, b"script-body"); let _ = fs::remove_dir_all(&dir); } #[test] fn opaque_origin_gets_no_storage() { let dir = std::env::temp_dir().join("we_service_worker_opaque"); let _ = fs::remove_dir_all(&dir); let mgr = ServiceWorkerManager::with_dir(dir.clone()); let store = RegistrationStore::new(); mgr.save(&Origin::Opaque, &store); assert!(mgr.load(&Origin::Opaque).records().is_empty()); let _ = fs::remove_dir_all(&dir); } #[test] fn private_partition_is_separate_from_persistent_storage() { let dir = std::env::temp_dir().join("we_service_worker_private_partition"); let _ = fs::remove_dir_all(&dir); let persistent = ServiceWorkerManager::with_partition(dir.clone(), StoragePartition::Persistent); let private = ServiceWorkerManager::with_partition( dir.clone(), StoragePartition::Private("session:one".to_string()), ); let base = Url::parse("https://example.com/app/page.html").unwrap(); let origin = base.origin(); let mut store = RegistrationStore::new(); store .register(&base, "sw.js", None, b"persistent".to_vec()) .unwrap(); persistent.save(&origin, &store); assert!(private.load(&origin).records().is_empty()); let mut private_store = RegistrationStore::new(); private_store .register(&base, "sw.js", None, b"private".to_vec()) .unwrap(); private.save(&origin, &private_store); assert_eq!( persistent.load(&origin).records()[0].script_bytes, b"persistent" ); assert_eq!(private.load(&origin).records()[0].script_bytes, b"private"); let _ = fs::remove_dir_all(&dir); } #[test] fn clear_origin_deletes_persisted_registration() { let dir = std::env::temp_dir().join("we_service_worker_clear_origin"); let _ = fs::remove_dir_all(&dir); let mgr = ServiceWorkerManager::with_dir(dir.clone()); let base = Url::parse("https://example.com/app/page.html").unwrap(); let origin = base.origin(); let mut store = RegistrationStore::new(); store .register(&base, "sw.js", None, b"script-body".to_vec()) .unwrap(); mgr.save(&origin, &store); assert!(mgr.clear_origin(&origin)); assert!(mgr.load(&origin).records().is_empty()); assert!(!mgr.clear_origin(&origin)); let _ = fs::remove_dir_all(&dir); } #[test] fn load_recovers_from_partially_written_metadata() { let dir = std::env::temp_dir().join("we_service_worker_partial_metadata"); let _ = fs::remove_dir_all(&dir); let mgr = ServiceWorkerManager::with_dir(dir.clone()); let base = Url::parse("https://example.com/app/page.html").unwrap(); let origin = base.origin(); let path = mgr.file_path(&origin).unwrap(); fs::create_dir_all(path.parent().unwrap()).unwrap(); fs::write(&path, b"WESW2\0\x01").unwrap(); assert!(mgr.load(&origin).records().is_empty()); let _ = fs::remove_dir_all(&dir); } }