atagen

macltok

public
Star0
Watch0

Commit 6db49c0ecf71

tarball

atagen <boss@atagen.co>2026-09-27 01:42 UTC

6 files changed+268-89
session: make record writes safe and let IndexedDB deletes report blocked
Modified file: Cargo.lock+4 additions
@@ -1828,6 +1828,7 @@ dependencies = [
18281828 "getrandom 0.4.3",
18291829 "http",
18301830 "imbl",
1831+ "js-sys",
18311832 "matrix-sdk",
18321833 "matrix-sdk-crypto",
18331834 "matrix-sdk-store-encryption",
@@ -1845,6 +1846,9 @@ dependencies = [
18451846 "tracing",
18461847 "url",
18471848 "uuid",
1849+ "wasm-bindgen",
1850+ "wasm-bindgen-futures",
1851+ "web-sys",
18481852 "wiremock",
18491853 ]
18501854
Modified file: Cargo.toml+14 additions
@@ -16,6 +16,10 @@ indexeddb = [
1616 "dep:url",
1717 "dep:matrix-sdk-store-encryption",
1818 "dep:indexed_db_futures",
19+ "dep:js-sys",
20+ "dep:wasm-bindgen",
21+ "dep:wasm-bindgen-futures",
22+ "dep:web-sys",
1923 ]
2024
2125 [dependencies]
@@ -40,11 +44,21 @@ getrandom = { version = "0.2", features = ["js"] }
4044 getrandom3 = { package = "getrandom", version = "0.3", features = ["wasm_js"] }
4145 getrandom4 = { package = "getrandom", version = "0.4", features = ["wasm_js"] }
4246 indexed_db_futures = { package = "matrix_indexed_db_futures", version = "0.7.0", optional = true, features = ["typed-arrays"] }
47+js-sys = { version = "0.3.105", optional = true }
4348 matrix-sdk = { version = "0.18.0", default-features = false, features = ["js"] }
4449 matrix-sdk-ui = { version = "0.18.0", default-features = false, features = ["js"] }
4550 # the sdk's own wasm reqwest leaves out stream, which the bounded media download needs
4651 reqwest = { version = "0.13.1", default-features = false, features = ["stream"] }
4752 uuid = { version = "1", features = ["js"] }
53+wasm-bindgen = { version = "0.2.128", optional = true }
54+wasm-bindgen-futures = { version = "0.4.78", optional = true }
55+web-sys = { version = "0.3.105", optional = true, features = [
56+ "IdbFactory",
57+ "IdbOpenDbRequest",
58+ "IdbRequest",
59+ "Window",
60+ "WorkerGlobalScope",
61+] }
4862
4963 [dev-dependencies]
5064 matrix-sdk = { version = "0.18.0", default-features = false, features = ["testing"] }
Modified file: README.md+3 additions
@@ -128,6 +128,9 @@ what's different in a browser
128128 restarts itself with backoff, that part is ours
129129 - two tabs or workers on one store each need `StoreConfig::with_lock_holder`
130130 with a name of their own, or they both think they hold the crypto store
131+- `discard` while another tab still has the store open answers
132+ `StoreDeletePending`. the delete lands once that tab lets go, and reopening
133+ the store waits behind it
131134
132135 ## Dependency policy
133136
Modified file: src/account.rs+5 additions−1 deletions
@@ -245,7 +245,7 @@ impl StoreConfig {
245245 #[cfg(feature = "indexeddb")]
246246 StoreLocation::IndexedDb(name) => crate::session_store::delete_indexeddb_store(name)
247247 .await
248- .map_err(|error| account_error("deleting the Matrix store", error)),
248+ .map_err(|error| error.into_account_error("deleting the Matrix store")),
249249 }
250250 }
251251
@@ -348,6 +348,10 @@ pub enum AccountError {
348348 UnsupportedAuthentication,
349349 #[error("the OAuth callback was already submitted")]
350350 OAuthCallbackAlreadySubmitted,
351+ /// an IndexedDB delete is waiting on the store's other connections. it goes through
352+ /// once they close, and opening the store again waits behind it
353+ #[error("the store is still open elsewhere, it will be deleted once that closes")]
354+ StoreDeletePending,
351355 #[error(transparent)]
352356 Engine(#[from] EngineError),
353357 }
Modified file: src/session_store.rs+236 additions−85 deletions
@@ -3,7 +3,6 @@ use std::path::{Path, PathBuf};
33
44 use matrix_sdk::{BoxFuture, SendOutsideWasm, SyncOutsideWasm};
55 use matrix_sdk_store_encryption::StoreCipher;
6-use tokio::sync::Mutex;
76
87 use crate::{AccountError, SessionRecord, StoreConfig, account::account_error};
98
@@ -12,6 +11,11 @@ type RecordLocation = PathBuf;
1211 #[cfg(feature = "indexeddb")]
1312 type RecordLocation = String;
1413
14+#[cfg(not(target_family = "wasm"))]
15+type Lock<T> = std::sync::Mutex<T>;
16+#[cfg(target_family = "wasm")]
17+type Lock<T> = tokio::sync::Mutex<T>;
18+
1519 /// one login's record and the matrix store it belongs to. the engine rotates refresh tokens
1620 /// and each works once, so a `save` has to land before the next refresh or the login is gone
1721 pub trait SessionStore: SendOutsideWasm + SyncOutsideWasm {
@@ -43,7 +47,7 @@ pub trait SessionStore: SendOutsideWasm + SyncOutsideWasm {
4347 pub struct EncryptedSessionStore {
4448 record: RecordLocation,
4549 store: StoreConfig,
46- sealer: Mutex<Option<Sealer>>,
50+ sealer: Lock<Option<Sealer>>,
4751 }
4852
4953 struct Sealer {
@@ -70,50 +74,126 @@ impl EncryptedSessionStore {
7074 Self {
7175 record,
7276 store,
73- sealer: Mutex::new(None),
77+ sealer: Lock::new(None),
7478 }
7579 }
7680
77- async fn load_record(&self) -> Result<Option<SessionRecord>, AccountError> {
78- let mut sealer = self.sealer.lock().await;
79- self.read(&mut sealer).await
81+ fn open(
82+ &self,
83+ sealer: &mut Option<Sealer>,
84+ sealed: Option<Vec<u8>>,
85+ ) -> Result<Option<SessionRecord>, AccountError> {
86+ let Some(sealed) = sealed else {
87+ return Ok(None);
88+ };
89+ let (opened, record) = unseal(self.store.passphrase(), &sealed)?;
90+ *sealer = Some(opened);
91+ Ok(Some(record))
8092 }
8193
82- async fn save_record(&self, record: &SessionRecord) -> Result<(), AccountError> {
83- let mut sealer = self.sealer.lock().await;
84- self.write(&mut sealer, record).await
94+ fn seal(
95+ &self,
96+ sealer: &mut Option<Sealer>,
97+ record: &SessionRecord,
98+ ) -> Result<Vec<u8>, AccountError> {
99+ let sealer = match sealer {
100+ Some(sealer) => sealer,
101+ empty => empty.insert(Sealer::new(self.store.passphrase())?),
102+ };
103+ sealer.seal(record)
85104 }
105+}
86106
87- async fn soft_log_out_record(&self) -> Result<(), AccountError> {
88- let mut sealer = self.sealer.lock().await;
89- match self.read(&mut sealer).await? {
90- Some(record) => self.write(&mut sealer, &record.soft_logged_out()).await,
107+#[cfg(not(target_family = "wasm"))]
108+impl EncryptedSessionStore {
109+ fn lock(&self) -> std::sync::MutexGuard<'_, Option<Sealer>> {
110+ self.sealer
111+ .lock()
112+ .unwrap_or_else(std::sync::PoisonError::into_inner)
113+ }
114+
115+ fn read(&self, sealer: &mut Option<Sealer>) -> Result<Option<SessionRecord>, AccountError> {
116+ let sealed = backend::read(&self.record)
117+ .map_err(|error| account_error("reading the session record", error))?;
118+ self.open(sealer, sealed)
119+ }
120+
121+ fn write(
122+ &self,
123+ sealer: &mut Option<Sealer>,
124+ record: &SessionRecord,
125+ ) -> Result<(), AccountError> {
126+ let sealed = self.seal(sealer, record)?;
127+ backend::write(&self.record, &sealed)
128+ .map_err(|error| account_error("writing the session record", error))
129+ }
130+
131+ fn load_record(&self) -> Result<Option<SessionRecord>, AccountError> {
132+ self.read(&mut self.lock())
133+ }
134+
135+ fn save_record(&self, record: &SessionRecord) -> Result<(), AccountError> {
136+ self.write(&mut self.lock(), record)
137+ }
138+
139+ fn soft_log_out_record(&self) -> Result<(), AccountError> {
140+ let mut sealer = self.lock();
141+ match self.read(&mut sealer)? {
142+ Some(record) => self.write(&mut sealer, &record.soft_logged_out()),
91143 None => Ok(()),
92144 }
93145 }
94146
95- async fn discard_all(&self) -> Result<(), AccountError> {
96- let mut sealer = self.sealer.lock().await;
147+ fn forget_record(&self) -> Result<(), AccountError> {
148+ let mut sealer = self.lock();
97149 backend::remove(&self.record)
98- .await
99150 .map_err(|error| account_error("removing the session record", error))?;
100151 *sealer = None;
101- self.store.delete().await
152+ Ok(())
153+ }
154+}
155+
156+#[cfg(not(target_family = "wasm"))]
157+impl SessionStore for EncryptedSessionStore {
158+ fn store(&self) -> &StoreConfig {
159+ &self.store
102160 }
103161
162+ fn load(&self) -> BoxFuture<'_, Result<Option<SessionRecord>, AccountError>> {
163+ Box::pin(async move { self.load_record() })
164+ }
165+
166+ fn save<'a>(&'a self, record: &'a SessionRecord) -> BoxFuture<'a, Result<(), AccountError>> {
167+ Box::pin(async move { self.save_record(record) })
168+ }
169+
170+ /// blocks the calling worker for the write and its fsyncs, about a millisecond
171+ fn save_blocking(&self, record: &SessionRecord) -> Option<Result<(), AccountError>> {
172+ Some(self.save_record(record))
173+ }
174+
175+ fn soft_log_out(&self) -> BoxFuture<'_, Result<(), AccountError>> {
176+ Box::pin(async move { self.soft_log_out_record() })
177+ }
178+
179+ fn discard(&self) -> BoxFuture<'_, Result<(), AccountError>> {
180+ Box::pin(async move {
181+ self.forget_record()?;
182+ self.store.delete().await
183+ })
184+ }
185+}
186+
187+#[cfg(target_family = "wasm")]
188+impl EncryptedSessionStore {
104189 async fn read(
105190 &self,
106191 sealer: &mut Option<Sealer>,
107192 ) -> Result<Option<SessionRecord>, AccountError> {
108- let Some(sealed) = backend::read(&self.record)
193+ let sealed = backend::read(&self.record)
109194 .await
110- .map_err(|error| account_error("reading the session record", error))?
111- else {
112- return Ok(None);
113- };
114- let (opened, record) = unseal(self.store.passphrase(), &sealed)?;
115- *sealer = Some(opened);
116- Ok(Some(record))
195+ .map_err(|error| account_error("reading the session record", error))?;
196+ self.open(sealer, sealed)
117197 }
118198
119199 async fn write(
@@ -127,30 +207,33 @@ impl EncryptedSessionStore {
127207 .map_err(|error| account_error("writing the session record", error))
128208 }
129209
130- fn seal(
131- &self,
132- sealer: &mut Option<Sealer>,
133- record: &SessionRecord,
134- ) -> Result<Vec<u8>, AccountError> {
135- let sealer = match sealer {
136- Some(sealer) => sealer,
137- empty => empty.insert(Sealer::new(self.store.passphrase())?),
138- };
139- sealer.seal(record)
210+ async fn load_record(&self) -> Result<Option<SessionRecord>, AccountError> {
211+ self.read(&mut *self.sealer.lock().await).await
140212 }
141213
142- /// the lock is only ever held around one small write, so losing the race to it just
143- /// hands this refresh to the async save
144- #[cfg(feature = "sqlite")]
145- fn save_now(&self, record: &SessionRecord) -> Option<Result<(), AccountError>> {
146- let mut sealer = self.sealer.try_lock().ok()?;
147- Some(self.seal(&mut sealer, record).and_then(|sealed| {
148- backend::write_blocking(&self.record, &sealed)
149- .map_err(|error| account_error("writing the session record", error))
150- }))
214+ async fn save_record(&self, record: &SessionRecord) -> Result<(), AccountError> {
215+ self.write(&mut *self.sealer.lock().await, record).await
216+ }
217+
218+ async fn soft_log_out_record(&self) -> Result<(), AccountError> {
219+ let mut sealer = self.sealer.lock().await;
220+ match self.read(&mut sealer).await? {
221+ Some(record) => self.write(&mut sealer, &record.soft_logged_out()).await,
222+ None => Ok(()),
223+ }
224+ }
225+
226+ async fn discard_all(&self) -> Result<(), AccountError> {
227+ let mut sealer = self.sealer.lock().await;
228+ backend::remove(&self.record)
229+ .await
230+ .map_err(|error| error.into_account_error("removing the session record"))?;
231+ *sealer = None;
232+ self.store.delete().await
151233 }
152234 }
153235
236+#[cfg(target_family = "wasm")]
154237 impl SessionStore for EncryptedSessionStore {
155238 fn store(&self) -> &StoreConfig {
156239 &self.store
@@ -164,11 +247,6 @@ impl SessionStore for EncryptedSessionStore {
164247 Box::pin(self.save_record(record))
165248 }
166249
167- #[cfg(feature = "sqlite")]
168- fn save_blocking(&self, record: &SessionRecord) -> Option<Result<(), AccountError>> {
169- self.save_now(record)
170- }
171-
172250 fn soft_log_out(&self) -> BoxFuture<'_, Result<(), AccountError>> {
173251 Box::pin(self.soft_log_out_record())
174252 }
@@ -229,12 +307,16 @@ fn unseal(passphrase: &str, sealed: &[u8]) -> Result<(Sealer, SessionRecord), Ac
229307 #[cfg(feature = "sqlite")]
230308 mod backend {
231309 use std::{
310+ ffi::OsString,
232311 fs::{self, File, OpenOptions},
233312 io::{self, ErrorKind, Write},
234313 path::{Path, PathBuf},
314+ sync::atomic::{AtomicU64, Ordering},
235315 };
236316
237- pub(super) async fn read(path: &Path) -> io::Result<Option<Vec<u8>>> {
317+ static WRITES: AtomicU64 = AtomicU64::new(0);
318+
319+ pub(super) fn read(path: &Path) -> io::Result<Option<Vec<u8>>> {
238320 match fs::read(path) {
239321 Ok(sealed) => Ok(Some(sealed)),
240322 Err(error) if error.kind() == ErrorKind::NotFound => Ok(None),
@@ -242,59 +324,68 @@ mod backend {
242324 }
243325 }
244326
245- pub(super) async fn write(path: &Path, sealed: &[u8]) -> io::Result<()> {
246- write_blocking(path, sealed)
247- }
248-
249- pub(super) fn write_blocking(path: &Path, sealed: &[u8]) -> io::Result<()> {
250- let mut temporary = path.as_os_str().to_owned();
251- temporary.push(".tmp");
252- let temporary = PathBuf::from(temporary);
327+ pub(super) fn write(path: &Path, sealed: &[u8]) -> io::Result<()> {
253328 let parent = match path.parent() {
254329 Some(parent) if !parent.as_os_str().is_empty() => parent,
255330 _ => Path::new("."),
256331 };
257332 fs::create_dir_all(parent)?;
258- let written = replace(path, &temporary, parent, sealed);
333+ let (temporary, mut file) = temporary_file(path)?;
334+ let written = replace(path, &temporary, &mut file, parent, sealed);
259335 if written.is_err() {
260336 let _ = fs::remove_file(&temporary);
261337 }
262338 written
263339 }
264340
265- fn replace(path: &Path, temporary: &Path, parent: &Path, sealed: &[u8]) -> io::Result<()> {
266- let mut file = private_file(temporary)?;
341+ fn temporary_file(path: &Path) -> io::Result<(PathBuf, File)> {
342+ loop {
343+ let mut name = OsString::from(path.as_os_str());
344+ let write = WRITES.fetch_add(1, Ordering::Relaxed);
345+ name.push(format!(".{}.{write}.tmp", std::process::id()));
346+ let temporary = PathBuf::from(name);
347+ match private_file(&temporary) {
348+ Ok(file) => return Ok((temporary, file)),
349+ Err(error) if error.kind() == ErrorKind::AlreadyExists => {}
350+ Err(error) => return Err(error),
351+ }
352+ }
353+ }
354+
355+ fn replace(
356+ path: &Path,
357+ temporary: &Path,
358+ file: &mut File,
359+ parent: &Path,
360+ sealed: &[u8],
361+ ) -> io::Result<()> {
267362 file.write_all(sealed)?;
268363 file.sync_all()?;
269364 fs::rename(temporary, path)?;
270365 sync_directory(parent)
271366 }
272367
273- pub(super) async fn remove(path: &Path) -> io::Result<()> {
368+ pub(super) fn remove(path: &Path) -> io::Result<()> {
274369 match fs::remove_file(path) {
275370 Err(error) if error.kind() != ErrorKind::NotFound => Err(error),
276371 _ => Ok(()),
277372 }
278373 }
279374
375+ /// `create_new` is `O_EXCL`, which posix says never follows a symlink, even a dangling one
280376 #[cfg(unix)]
281377 fn private_file(path: &Path) -> io::Result<File> {
282378 use std::os::unix::fs::OpenOptionsExt;
283379 OpenOptions::new()
284380 .write(true)
285- .create(true)
286- .truncate(true)
381+ .create_new(true)
287382 .mode(0o600)
288383 .open(path)
289384 }
290385
291386 #[cfg(not(unix))]
292387 fn private_file(path: &Path) -> io::Result<File> {
293- OpenOptions::new()
294- .write(true)
295- .create(true)
296- .truncate(true)
297- .open(path)
388+ OpenOptions::new().write(true).create_new(true).open(path)
298389 }
299390
300391 /// a rename isn't durable until its directory is synced
@@ -334,20 +425,36 @@ mod backend {
334425 .await
335426 }
336427
428+ /// the crate's `Database` never closes itself on drop, and an open one blocks deleting it
337429 pub(super) async fn read(name: &str) -> Result<Option<Vec<u8>>, OpenDbError> {
338430 let database = open(name).await?;
339- let sealed: Option<Uint8Array> = database
431+ let sealed = get(&database).await;
432+ database.close();
433+ Ok(sealed?.map(Into::into))
434+ }
435+
436+ pub(super) async fn write(name: &str, sealed: &[u8]) -> Result<(), OpenDbError> {
437+ let database = open(name).await?;
438+ let written = put(&database, sealed).await;
439+ database.close();
440+ Ok(written?)
441+ }
442+
443+ pub(super) async fn remove(name: &str) -> Result<(), super::DeleteError> {
444+ super::delete_database(name).await
445+ }
446+
447+ async fn get(database: &Database) -> Result<Option<Uint8Array>, Error> {
448+ database
340449 .transaction(RECORDS)
341450 .with_mode(TransactionMode::Readonly)
342451 .build()?
343452 .object_store(RECORDS)?
344453 .get(KEY)
345- .await?;
346- Ok(sealed.map(Into::into))
454+ .await
347455 }
348456
349- pub(super) async fn write(name: &str, sealed: &[u8]) -> Result<(), OpenDbError> {
350- let database = open(name).await?;
457+ async fn put(database: &Database, sealed: &[u8]) -> Result<(), Error> {
351458 let transaction = database
352459 .transaction(RECORDS)
353460 .with_mode(TransactionMode::Readwrite)
@@ -357,11 +464,7 @@ mod backend {
357464 .put(Uint8ArraySlice::new(sealed))
358465 .with_key(KEY)
359466 .build()?;
360- Ok(transaction.commit().await?)
361- }
362-
363- pub(super) async fn remove(name: &str) -> Result<(), OpenDbError> {
364- Ok(Database::delete_by_name(name)?.await?)
467+ transaction.commit().await
365468 }
366469 }
367470
@@ -391,11 +494,7 @@ pub(crate) fn delete_sqlite_store(path: &Path) -> std::io::Result<()> {
391494 }
392495
393496 #[cfg(feature = "indexeddb")]
394-pub(crate) async fn delete_indexeddb_store(
395- name: &str,
396-) -> Result<(), indexed_db_futures::error::OpenDbError> {
397- use indexed_db_futures::database::Database;
398-
497+pub(crate) async fn delete_indexeddb_store(name: &str) -> Result<(), DeleteError> {
399498 // the databases matrix-sdk-indexeddb 0.18 opens under one store name
400499 for suffix in [
401500 "",
@@ -405,7 +504,59 @@ pub(crate) async fn delete_indexeddb_store(
405504 "::event_cache",
406505 "::media",
407506 ] {
408- Database::delete_by_name(&format!("{name}{suffix}"))?.await?;
507+ delete_database(&format!("{name}{suffix}")).await?;
409508 }
410509 Ok(())
411510 }
511+
512+#[cfg(feature = "indexeddb")]
513+#[derive(Debug, thiserror::Error)]
514+pub(crate) enum DeleteError {
515+ #[error("the database is still open somewhere, it goes once that closes")]
516+ Blocked,
517+ #[error("this global scope has no IndexedDB")]
518+ Unavailable,
519+ #[error("deleting the database failed, {0}")]
520+ Failed(String),
521+}
522+
523+#[cfg(feature = "indexeddb")]
524+impl DeleteError {
525+ pub(crate) fn into_account_error(self, context: &'static str) -> AccountError {
526+ match self {
527+ Self::Blocked => AccountError::StoreDeletePending,
528+ error => account_error(context, error),
529+ }
530+ }
531+}
532+
533+/// matrix_indexed_db_futures 0.7 ignores `blocked` on a delete, which then waits forever
534+#[cfg(feature = "indexeddb")]
535+async fn delete_database(name: &str) -> Result<(), DeleteError> {
536+ use wasm_bindgen::{JsCast, JsValue};
537+
538+ const BLOCKED: &str = "blocked";
539+ let global = js_sys::global();
540+ let factory = if let Some(window) = global.dyn_ref::<web_sys::Window>() {
541+ window.indexed_db()
542+ } else if let Some(worker) = global.dyn_ref::<web_sys::WorkerGlobalScope>() {
543+ worker.indexed_db()
544+ } else {
545+ return Err(DeleteError::Unavailable);
546+ };
547+ let factory = factory.ok().flatten().ok_or(DeleteError::Unavailable)?;
548+ let request = factory
549+ .delete_database(name)
550+ .map_err(|error| DeleteError::Failed(format!("{error:?}")))?;
551+ let settled = js_sys::Promise::new(&mut |resolve, reject| {
552+ request.set_onsuccess(Some(&resolve));
553+ request.set_onerror(Some(&reject));
554+ let blocked = reject.bind1(&JsValue::NULL, &JsValue::from_str(BLOCKED));
555+ request.set_onblocked(Some(blocked.unchecked_ref()));
556+ });
557+ match wasm_bindgen_futures::JsFuture::from(settled).await {
558+ Ok(_) => Ok(()),
559+ Err(reason) if reason.as_string().as_deref() == Some(BLOCKED) => Err(DeleteError::Blocked),
560+ Err(_) => Err(DeleteError::Failed(format!("{:?}", request.error()))),
561+ }
562+}
Modified file: tests/account.rs+6 additions−3 deletions
@@ -130,7 +130,7 @@ async fn a_torn_or_failed_save_leaves_the_old_record() {
130130 StoreConfig::sqlite(root.join("store"), "test store passphrase"),
131131 )
132132 };
133- let temporary = root.join("session.tmp");
133+ let temporary = root.join("session.1.0.tmp");
134134 sessions()
135135 .save(&record("https://old.example"))
136136 .await
@@ -140,8 +140,10 @@ async fn a_torn_or_failed_save_leaves_the_old_record() {
140140 let loaded = sessions().load().await.unwrap().unwrap();
141141 assert_eq!(loaded.homeserver(), "https://old.example");
142142
143- std::fs::remove_file(&temporary).unwrap();
144- std::fs::create_dir(&temporary).unwrap();
143+ let writable = std::fs::metadata(&root).unwrap().permissions();
144+ let mut read_only = writable.clone();
145+ read_only.set_readonly(true);
146+ std::fs::set_permissions(&root, read_only).unwrap();
145147 assert!(
146148 sessions()
147149 .save(&record("https://new.example"))
@@ -151,6 +153,7 @@ async fn a_torn_or_failed_save_leaves_the_old_record() {
151153 let loaded = sessions().load().await.unwrap().unwrap();
152154 assert_eq!(loaded.homeserver(), "https://old.example");
153155
156+ std::fs::set_permissions(&root, writable).unwrap();
154157 std::fs::remove_dir_all(root).unwrap();
155158 }