1use std::any::Any;
8use std::collections::VecDeque;
9use std::future::Future;
10use std::io::{self, Read, Seek, SeekFrom, Write};
11#[cfg(unix)]
12use std::os::unix::{
13 ffi::OsStrExt,
14 fs::{DirBuilderExt, MetadataExt, OpenOptionsExt, PermissionsExt},
15};
16#[cfg(target_os = "windows")]
17use std::os::windows::prelude::*;
18use std::path::{Path, PathBuf};
19use std::pin::Pin;
20use std::sync::atomic::{AtomicU32, Ordering};
21use std::sync::Arc;
22use std::task::{Context, Poll};
23use std::time::{Duration, SystemTime, UNIX_EPOCH};
24
25use bytes::{Buf, Bytes, BytesMut};
26use futures_util::{future, future::BoxFuture, FutureExt, Stream};
27use pin_utils::pin_mut;
28use tokio::task;
29
30use libc;
31use reflink_copy::reflink_or_copy;
32
33use crate::davpath::DavPath;
34use crate::fs::*;
35use crate::localfs_macos::DUCacheBuilder;
36
37const RUNTIME_TYPE_BASIC: u32 = 1;
38const RUNTIME_TYPE_THREADPOOL: u32 = 2;
39static RUNTIME_TYPE: AtomicU32 = AtomicU32::new(0);
40
41#[derive(Clone, Copy)]
42#[repr(u32)]
43enum RuntimeType {
44 Basic = RUNTIME_TYPE_BASIC,
45 ThreadPool = RUNTIME_TYPE_THREADPOOL,
46}
47
48impl RuntimeType {
49 #[inline]
50 fn get() -> RuntimeType {
51 match RUNTIME_TYPE.load(Ordering::Relaxed) {
52 RUNTIME_TYPE_BASIC => RuntimeType::Basic,
53 RUNTIME_TYPE_THREADPOOL => RuntimeType::ThreadPool,
54 _ => {
55 let dbg = format!("{:?}", tokio::runtime::Handle::current());
56 let rt = if dbg.contains("ThreadPool") {
57 RuntimeType::ThreadPool
58 } else {
59 RuntimeType::Basic
60 };
61 RUNTIME_TYPE.store(rt as u32, Ordering::SeqCst);
62 rt
63 }
64 }
65 }
66}
67
68#[inline]
73async fn blocking<F, R>(func: F) -> R
74where
75 F: FnOnce() -> R,
76 F: Send + 'static,
77 R: Send + 'static,
78{
79 match RuntimeType::get() {
80 RuntimeType::Basic => task::spawn_blocking(func).await.unwrap(),
81 RuntimeType::ThreadPool => task::block_in_place(func),
82 }
83}
84
85#[derive(Debug, Clone)]
86struct LocalFsMetaData(std::fs::Metadata);
87
88#[derive(Clone)]
90pub struct LocalFs {
91 pub(crate) inner: Arc<LocalFsInner>,
92}
93
94pub(crate) struct LocalFsInner {
96 pub basedir: PathBuf,
97 #[allow(dead_code)]
98 pub public: bool,
99 pub case_insensitive: bool,
100 pub macos: bool,
101 pub is_file: bool,
102 pub fs_access_guard: Option<Box<dyn Fn() -> Box<dyn Any> + Send + Sync + 'static>>,
103}
104
105#[derive(Debug)]
106struct LocalFsFile(Option<std::fs::File>);
107
108struct LocalFsReadDir {
109 fs: LocalFs,
110 do_meta: ReadDirMeta,
111 buffer: VecDeque<io::Result<LocalFsDirEntry>>,
112 dir_cache: Option<DUCacheBuilder>,
113 iterator: Option<std::fs::ReadDir>,
114 fut: Option<BoxFuture<'static, ReadDirBatch>>,
115}
116
117enum Meta {
120 Data(io::Result<std::fs::Metadata>),
121 Fs(LocalFs),
122}
123
124struct LocalFsDirEntry {
126 meta: Meta,
127 entry: std::fs::DirEntry,
128}
129
130impl LocalFs {
131 pub fn new<P: AsRef<Path>>(
140 base: P,
141 public: bool,
142 case_insensitive: bool,
143 macos: bool,
144 ) -> Box<LocalFs> {
145 let inner = LocalFsInner {
146 basedir: base.as_ref().to_path_buf(),
147 public,
148 macos,
149 case_insensitive,
150 is_file: false,
151 fs_access_guard: None,
152 };
153 Box::new({
154 LocalFs {
155 inner: Arc::new(inner),
156 }
157 })
158 }
159
160 pub fn new_file<P: AsRef<Path>>(file: P, public: bool) -> Box<LocalFs> {
165 let inner = LocalFsInner {
166 basedir: file.as_ref().to_path_buf(),
167 public,
168 macos: false,
169 case_insensitive: false,
170 is_file: true,
171 fs_access_guard: None,
172 };
173 Box::new({
174 LocalFs {
175 inner: Arc::new(inner),
176 }
177 })
178 }
179
180 #[doc(hidden)]
182 pub fn new_with_fs_access_guard<P: AsRef<Path>>(
183 base: P,
184 public: bool,
185 case_insensitive: bool,
186 macos: bool,
187 fs_access_guard: Option<Box<dyn Fn() -> Box<dyn Any> + Send + Sync + 'static>>,
188 ) -> Box<LocalFs> {
189 let inner = LocalFsInner {
190 basedir: base.as_ref().to_path_buf(),
191 public,
192 macos,
193 case_insensitive,
194 is_file: false,
195 fs_access_guard,
196 };
197 Box::new({
198 LocalFs {
199 inner: Arc::new(inner),
200 }
201 })
202 }
203
204 fn fspath_dbg(&self, path: &DavPath) -> PathBuf {
205 let mut pathbuf = self.inner.basedir.clone();
206 if !self.inner.is_file {
207 pathbuf.push(path.as_rel_ospath());
208 }
209 pathbuf
210 }
211
212 fn fspath(&self, path: &DavPath) -> PathBuf {
213 if self.inner.case_insensitive {
214 crate::localfs_windows::resolve(&self.inner.basedir, path)
215 } else {
216 let mut pathbuf = self.inner.basedir.clone();
217 if !self.inner.is_file {
218 pathbuf.push(path.as_rel_ospath());
219 }
220 pathbuf
221 }
222 }
223
224 #[doc(hidden)]
226 pub async fn blocking<F, R>(&self, func: F) -> R
227 where
228 F: FnOnce() -> R + Send + 'static,
229 R: Send + 'static,
230 {
231 let this = self.clone();
232 blocking(move || {
233 let _guard = this.inner.fs_access_guard.as_ref().map(|f| f());
234 func()
235 })
236 .await
237 }
238}
239
240impl DavFileSystem for LocalFs {
243 fn metadata<'a>(&'a self, davpath: &'a DavPath) -> FsFuture<'a, Box<dyn DavMetaData>> {
244 async move {
245 if let Some(meta) = self.is_virtual(davpath) {
246 return Ok(meta);
247 }
248 let path = self.fspath(davpath);
249 if self.is_notfound(&path) {
250 return Err(FsError::NotFound);
251 }
252 self.blocking(move || match std::fs::metadata(path) {
253 Ok(meta) => Ok(Box::new(LocalFsMetaData(meta)) as Box<dyn DavMetaData>),
254 Err(e) => Err(e.into()),
255 })
256 .await
257 }
258 .boxed()
259 }
260
261 fn symlink_metadata<'a>(&'a self, davpath: &'a DavPath) -> FsFuture<'a, Box<dyn DavMetaData>> {
262 async move {
263 if let Some(meta) = self.is_virtual(davpath) {
264 return Ok(meta);
265 }
266 let path = self.fspath(davpath);
267 if self.is_notfound(&path) {
268 return Err(FsError::NotFound);
269 }
270 self.blocking(move || match std::fs::symlink_metadata(path) {
271 Ok(meta) => Ok(Box::new(LocalFsMetaData(meta)) as Box<dyn DavMetaData>),
272 Err(e) => Err(e.into()),
273 })
274 .await
275 }
276 .boxed()
277 }
278
279 fn read_dir<'a>(
282 &'a self,
283 davpath: &'a DavPath,
284 meta: ReadDirMeta,
285 ) -> FsFuture<'a, FsStream<Box<dyn DavDirEntry>>> {
286 async move {
287 trace!("FS: read_dir {:?}", self.fspath_dbg(davpath));
288 let path = self.fspath(davpath);
289 let path2 = path.clone();
290 let iter = self.blocking(move || std::fs::read_dir(&path)).await;
291 match iter {
292 Ok(iterator) => {
293 let strm = LocalFsReadDir {
294 fs: self.clone(),
295 do_meta: meta,
296 buffer: VecDeque::new(),
297 dir_cache: self.dir_cache_builder(path2),
298 iterator: Some(iterator),
299 fut: None,
300 };
301 Ok(Box::pin(strm) as FsStream<Box<dyn DavDirEntry>>)
302 }
303 Err(e) => Err(e.into()),
304 }
305 }
306 .boxed()
307 }
308
309 fn open<'a>(
310 &'a self,
311 path: &'a DavPath,
312 options: OpenOptions,
313 ) -> FsFuture<'a, Box<dyn DavFile>> {
314 async move {
315 trace!("FS: open {:?}", self.fspath_dbg(path));
316 if self.is_forbidden(path) {
317 return Err(FsError::Forbidden);
318 }
319 #[cfg(unix)]
320 let mode = if self.inner.public { 0o644 } else { 0o600 };
321 let path = self.fspath(path);
322 self.blocking(move || {
323 #[cfg(unix)]
324 let res = std::fs::OpenOptions::new()
325 .read(options.read)
326 .write(options.write)
327 .append(options.append)
328 .truncate(options.truncate)
329 .create(options.create)
330 .create_new(options.create_new)
331 .mode(mode)
332 .open(path);
333 #[cfg(windows)]
334 let res = std::fs::OpenOptions::new()
335 .read(options.read)
336 .write(options.write)
337 .append(options.append)
338 .truncate(options.truncate)
339 .create(options.create)
340 .create_new(options.create_new)
341 .open(path);
342 match res {
343 Ok(file) => Ok(Box::new(LocalFsFile(Some(file))) as Box<dyn DavFile>),
344 Err(e) => Err(e.into()),
345 }
346 })
347 .await
348 }
349 .boxed()
350 }
351
352 fn create_dir<'a>(&'a self, path: &'a DavPath) -> FsFuture<'a, ()> {
353 async move {
354 trace!("FS: create_dir {:?}", self.fspath_dbg(path));
355 if self.is_forbidden(path) {
356 return Err(FsError::Forbidden);
357 }
358 #[cfg(unix)]
359 let mode = if self.inner.public { 0o755 } else { 0o700 };
360 let path = self.fspath(path);
361 self.blocking(move || {
362 #[cfg(unix)]
363 {
364 std::fs::DirBuilder::new()
365 .mode(mode)
366 .create(path)
367 .map_err(|e| e.into())
368 }
369 #[cfg(windows)]
370 {
371 std::fs::DirBuilder::new()
372 .create(path)
373 .map_err(|e| e.into())
374 }
375 })
376 .await
377 }
378 .boxed()
379 }
380
381 fn remove_dir<'a>(&'a self, path: &'a DavPath) -> FsFuture<'a, ()> {
382 async move {
383 trace!("FS: remove_dir {:?}", self.fspath_dbg(path));
384 let path = self.fspath(path);
385 self.blocking(move || std::fs::remove_dir(path).map_err(|e| e.into()))
386 .await
387 }
388 .boxed()
389 }
390
391 fn remove_file<'a>(&'a self, path: &'a DavPath) -> FsFuture<'a, ()> {
392 async move {
393 trace!("FS: remove_file {:?}", self.fspath_dbg(path));
394 if self.is_forbidden(path) {
395 return Err(FsError::Forbidden);
396 }
397 let path = self.fspath(path);
398 self.blocking(move || std::fs::remove_file(path).map_err(|e| e.into()))
399 .await
400 }
401 .boxed()
402 }
403
404 fn rename<'a>(&'a self, from: &'a DavPath, to: &'a DavPath) -> FsFuture<'a, ()> {
405 async move {
406 trace!(
407 "FS: rename {:?} {:?}",
408 self.fspath_dbg(from),
409 self.fspath_dbg(to)
410 );
411 if self.is_forbidden(from) || self.is_forbidden(to) {
412 return Err(FsError::Forbidden);
413 }
414 let frompath = self.fspath(from);
415 let topath = self.fspath(to);
416 self.blocking(move || {
417 match std::fs::rename(&frompath, &topath) {
418 Ok(v) => Ok(v),
419 Err(e) => {
420 if e.raw_os_error() == Some(libc::ENOTDIR) && frompath.is_dir() {
424 let _ = std::fs::remove_file(&topath);
426 std::fs::rename(frompath, topath).map_err(|e| e.into())
427 } else {
428 Err(e.into())
429 }
430 }
431 }
432 })
433 .await
434 }
435 .boxed()
436 }
437
438 fn copy<'a>(&'a self, from: &'a DavPath, to: &'a DavPath) -> FsFuture<'a, ()> {
439 async move {
440 trace!(
441 "FS: copy {:?} {:?}",
442 self.fspath_dbg(from),
443 self.fspath_dbg(to)
444 );
445 if self.is_forbidden(from) || self.is_forbidden(to) {
446 return Err(FsError::Forbidden);
447 }
448 let path_from = self.fspath(from);
449 let path_to = self.fspath(to);
450
451 match self
452 .blocking(move || reflink_or_copy(path_from, path_to))
453 .await
454 {
455 Ok(_) => Ok(()),
456 Err(e) => {
457 debug!(
458 "copy({:?}, {:?}) failed: {}",
459 self.fspath_dbg(from),
460 self.fspath_dbg(to),
461 e
462 );
463 Err(e.into())
464 }
465 }
466 }
467 .boxed()
468 }
469}
470
471struct ReadDirBatch {
473 iterator: Option<std::fs::ReadDir>,
474 buffer: VecDeque<io::Result<LocalFsDirEntry>>,
475}
476
477fn read_batch(
480 iterator: Option<std::fs::ReadDir>,
481 fs: LocalFs,
482 do_meta: ReadDirMeta,
483) -> ReadDirBatch {
484 let mut buffer = VecDeque::new();
485 let mut iterator = match iterator {
486 Some(i) => i,
487 None => {
488 return ReadDirBatch {
489 buffer,
490 iterator: None,
491 }
492 }
493 };
494 let _guard = match do_meta {
495 ReadDirMeta::None => None,
496 _ => fs.inner.fs_access_guard.as_ref().map(|f| f()),
497 };
498 for _ in 0..256 {
499 match iterator.next() {
500 Some(Ok(entry)) => {
501 let meta = match do_meta {
502 ReadDirMeta::Data => Meta::Data(std::fs::metadata(entry.path())),
503 ReadDirMeta::DataSymlink => Meta::Data(entry.metadata()),
504 ReadDirMeta::None => Meta::Fs(fs.clone()),
505 };
506 let d = LocalFsDirEntry { meta, entry };
507 buffer.push_back(Ok(d))
508 }
509 Some(Err(e)) => {
510 buffer.push_back(Err(e));
511 break;
512 }
513 None => break,
514 }
515 }
516 ReadDirBatch {
517 buffer,
518 iterator: Some(iterator),
519 }
520}
521
522impl LocalFsReadDir {
523 fn read_batch(&mut self) -> BoxFuture<'static, ReadDirBatch> {
528 let iterator = self.iterator.take();
529 let fs = self.fs.clone();
530 let do_meta = self.do_meta;
531
532 let fut: BoxFuture<ReadDirBatch> =
533 blocking(move || read_batch(iterator, fs, do_meta)).boxed();
534 fut
535 }
536}
537
538impl Stream for LocalFsReadDir {
540 type Item = FsResult<Box<dyn DavDirEntry>>;
541
542 fn poll_next(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Option<Self::Item>> {
543 let this = Pin::into_inner(self);
544
545 if this.buffer.is_empty() {
547 if this.fut.is_none() {
549 if this.iterator.is_none() {
550 return Poll::Ready(None);
551 }
552 this.fut = Some(this.read_batch());
553 }
554
555 let fut = this.fut.as_mut().unwrap();
557 pin_mut!(fut);
558 match Pin::new(&mut fut).poll(cx) {
559 Poll::Ready(batch) => {
560 this.fut.take();
561 if let Some(ref mut nb) = this.dir_cache {
562 batch.buffer.iter().for_each(|e| {
563 if let Ok(ref e) = e {
564 nb.add(e.entry.file_name());
565 }
566 });
567 }
568 this.buffer = batch.buffer;
569 this.iterator = batch.iterator;
570 }
571 Poll::Pending => return Poll::Pending,
572 }
573 }
574
575 match this.buffer.pop_front() {
577 Some(Ok(item)) => Poll::Ready(Some(Ok(Box::new(item)))),
578 Some(Err(err)) => {
579 this.iterator.take();
581 if let Some(ref mut nb) = this.dir_cache {
583 nb.finish();
584 }
585 Poll::Ready(Some(Err(err.into())))
587 }
588 None => {
589 this.iterator.take();
591 if let Some(ref mut nb) = this.dir_cache {
593 nb.finish();
594 }
595 Poll::Ready(None)
597 }
598 }
599 }
600}
601
602enum Is {
603 File,
604 Dir,
605 Symlink,
606}
607
608impl LocalFsDirEntry {
609 async fn is_a(&self, is: Is) -> FsResult<bool> {
610 match self.meta {
611 Meta::Data(Ok(ref meta)) => Ok(match is {
612 Is::File => meta.file_type().is_file(),
613 Is::Dir => meta.file_type().is_dir(),
614 Is::Symlink => meta.file_type().is_symlink(),
615 }),
616 Meta::Data(Err(ref e)) => Err(e.into()),
617 Meta::Fs(ref fs) => {
618 let fullpath = self.entry.path();
619 let ft = fs
620 .blocking(move || std::fs::metadata(&fullpath))
621 .await?
622 .file_type();
623 Ok(match is {
624 Is::File => ft.is_file(),
625 Is::Dir => ft.is_dir(),
626 Is::Symlink => ft.is_symlink(),
627 })
628 }
629 }
630 }
631}
632
633impl DavDirEntry for LocalFsDirEntry {
634 fn metadata(&self) -> FsFuture<Box<dyn DavMetaData>> {
635 match self.meta {
636 Meta::Data(ref meta) => {
637 let m = match meta {
638 Ok(meta) => Ok(Box::new(LocalFsMetaData(meta.clone())) as Box<dyn DavMetaData>),
639 Err(e) => Err(e.into()),
640 };
641 Box::pin(future::ready(m))
642 }
643 Meta::Fs(ref fs) => {
644 let fullpath = self.entry.path();
645 fs.blocking(move || match std::fs::metadata(&fullpath) {
646 Ok(meta) => Ok(Box::new(LocalFsMetaData(meta)) as Box<dyn DavMetaData>),
647 Err(e) => Err(e.into()),
648 })
649 .boxed()
650 }
651 }
652 }
653
654 #[cfg(unix)]
655 fn name(&self) -> Vec<u8> {
656 self.entry.file_name().as_bytes().to_vec()
657 }
658
659 #[cfg(windows)]
660 fn name(&self) -> Vec<u8> {
661 self.entry.file_name().to_str().unwrap().as_bytes().to_vec()
662 }
663
664 fn is_dir(&self) -> FsFuture<bool> {
665 Box::pin(self.is_a(Is::Dir))
666 }
667
668 fn is_file(&self) -> FsFuture<bool> {
669 Box::pin(self.is_a(Is::File))
670 }
671
672 fn is_symlink(&self) -> FsFuture<bool> {
673 Box::pin(self.is_a(Is::Symlink))
674 }
675}
676
677impl DavFile for LocalFsFile {
678 fn metadata(&mut self) -> FsFuture<Box<dyn DavMetaData>> {
679 async move {
680 let file = self.0.take().unwrap();
681 let (meta, file) = blocking(move || (file.metadata(), file)).await;
682 self.0 = Some(file);
683 Ok(Box::new(LocalFsMetaData(meta?)) as Box<dyn DavMetaData>)
684 }
685 .boxed()
686 }
687
688 fn write_bytes(&mut self, buf: Bytes) -> FsFuture<()> {
689 async move {
690 let mut file = self.0.take().unwrap();
691 let (res, file) = blocking(move || (file.write_all(&buf), file)).await;
692 self.0 = Some(file);
693 res.map_err(|e| e.into())
694 }
695 .boxed()
696 }
697
698 fn write_buf(&mut self, mut buf: Box<dyn Buf + Send>) -> FsFuture<()> {
699 async move {
700 let mut file = self.0.take().unwrap();
701 let (res, file) = blocking(move || {
702 while buf.remaining() > 0 {
703 let n = match file.write(buf.chunk()) {
704 Ok(n) => n,
705 Err(e) => return (Err(e), file),
706 };
707 buf.advance(n);
708 }
709 (Ok(()), file)
710 })
711 .await;
712 self.0 = Some(file);
713 res.map_err(|e| e.into())
714 }
715 .boxed()
716 }
717
718 fn read_bytes(&mut self, count: usize) -> FsFuture<Bytes> {
719 async move {
720 let mut file = self.0.take().unwrap();
721 let (res, file) = blocking(move || {
722 let mut buf = BytesMut::with_capacity(count);
723 let res = unsafe {
724 buf.set_len(count);
725 file.read(&mut buf).map(|n| {
726 buf.set_len(n);
727 buf.freeze()
728 })
729 };
730 (res, file)
731 })
732 .await;
733 self.0 = Some(file);
734 res.map_err(|e| e.into())
735 }
736 .boxed()
737 }
738
739 fn seek(&mut self, pos: SeekFrom) -> FsFuture<u64> {
740 async move {
741 let mut file = self.0.take().unwrap();
742 let (res, file) = blocking(move || (file.seek(pos), file)).await;
743 self.0 = Some(file);
744 res.map_err(|e| e.into())
745 }
746 .boxed()
747 }
748
749 fn flush(&mut self) -> FsFuture<()> {
750 async move {
751 let mut file = self.0.take().unwrap();
752 let (res, file) = blocking(move || (file.flush(), file)).await;
753 self.0 = Some(file);
754 res.map_err(|e| e.into())
755 }
756 .boxed()
757 }
758}
759
760impl DavMetaData for LocalFsMetaData {
761 fn len(&self) -> u64 {
762 self.0.len()
763 }
764 fn created(&self) -> FsResult<SystemTime> {
765 self.0.created().map_err(|e| e.into())
766 }
767 fn modified(&self) -> FsResult<SystemTime> {
768 self.0.modified().map_err(|e| e.into())
769 }
770 fn accessed(&self) -> FsResult<SystemTime> {
771 self.0.accessed().map_err(|e| e.into())
772 }
773
774 #[cfg(unix)]
775 fn status_changed(&self) -> FsResult<SystemTime> {
776 Ok(UNIX_EPOCH + Duration::new(self.0.ctime() as u64, 0))
777 }
778
779 #[cfg(windows)]
780 fn status_changed(&self) -> FsResult<SystemTime> {
781 Ok(UNIX_EPOCH + Duration::from_nanos(self.0.creation_time() - 116444736000000000))
782 }
783
784 fn is_dir(&self) -> bool {
785 self.0.is_dir()
786 }
787 fn is_file(&self) -> bool {
788 self.0.is_file()
789 }
790 fn is_symlink(&self) -> bool {
791 self.0.file_type().is_symlink()
792 }
793
794 #[cfg(unix)]
795 fn executable(&self) -> FsResult<bool> {
796 if self.0.is_file() {
797 return Ok((self.0.permissions().mode() & 0o100) > 0);
798 }
799 Err(FsError::NotImplemented)
800 }
801
802 #[cfg(windows)]
803 fn executable(&self) -> FsResult<bool> {
804 Err(FsError::NotImplemented)
806 }
807
808 #[cfg(unix)]
810 fn etag(&self) -> Option<String> {
811 let modified = self.0.modified().ok()?;
812 let t = modified.duration_since(UNIX_EPOCH).ok()?;
813 let t = t.as_secs() * 1000000 + t.subsec_nanos() as u64 / 1000;
814 if self.is_file() {
815 Some(format!("{:x}-{:x}-{:x}", self.0.ino(), self.0.len(), t))
816 } else {
817 Some(format!("{:x}-{:x}", self.0.ino(), t))
818 }
819 }
820
821 #[cfg(windows)]
823 fn etag(&self) -> Option<String> {
824 let modified = self.0.modified().ok()?;
825 let t = modified.duration_since(UNIX_EPOCH).ok()?;
826 let t = t.as_secs() * 1000000 + t.subsec_nanos() as u64 / 1000;
827 if self.is_file() {
828 Some(format!("{:x}-{:x}", self.0.len(), t))
829 } else {
830 Some(format!("{:x}", t))
831 }
832 }
833}