1use serde::Deserialize;
6
7#[derive(Debug, Deserialize)]
9pub struct PhpConfig {
10 #[serde(default)]
12 pub enabled: bool,
13
14 #[serde(default = "default_document_root")]
17 pub document_root: String,
18
19 pub workers: Option<usize>,
22
23 #[serde(default)]
33 pub ini: std::collections::HashMap<String, String>,
34
35 #[serde(default = "default_extensions")]
37 pub extensions: Vec<String>,
38
39 #[serde(default = "default_index")]
41 pub index: String,
42
43 #[serde(default = "default_max_body")]
45 pub max_body_bytes: usize,
46
47 #[serde(default = "default_max_execution_time")]
49 pub max_execution_time: u32,
50
51 #[serde(default = "default_true_val")]
56 pub cache_responses: bool,
57
58 #[serde(default = "default_max_requests")]
61 pub max_requests: u64,
62
63 pub worker_script: Option<String>,
79}
80
81impl Default for PhpConfig {
82 fn default() -> Self {
83 Self {
84 enabled: false,
85 document_root: default_document_root(),
86 workers: None,
87 ini: std::collections::HashMap::new(),
88 extensions: default_extensions(),
89 index: default_index(),
90 max_body_bytes: default_max_body(),
91 max_execution_time: default_max_execution_time(),
92 max_requests: default_max_requests(),
93 worker_script: None,
94 cache_responses: true,
95 }
96 }
97}
98
99fn default_true_val() -> bool {
100 true
101}
102
103impl PhpConfig {
104 pub fn effective_workers(&self) -> usize {
110 let requested = self.workers.unwrap_or_else(|| {
111 std::thread::available_parallelism()
112 .map(|n| n.get())
113 .unwrap_or(4)
114 });
115
116 #[cfg(not(php_zts))]
117 {
118 if requested > 1 {
119 tracing::warn!(
120 requested = requested,
121 "PHP is NTS (non-thread-safe) — clamping to 1 worker. \
122 Rebuild PHP with --enable-zts for multi-threaded mode."
123 );
124 }
125 #[allow(clippy::needless_return)]
126 return 1;
127 }
128
129 #[cfg(php_zts)]
130 requested
131 }
132
133 pub fn ini_entries_string(&self) -> String {
137 let mut entries = String::new();
138
139 let defaults = [
146 ("max_execution_time", self.max_execution_time.to_string()),
147 ("zend.max_allowed_stack_size", "-1".into()),
150 ("opcache.enable", "1".into()),
151 ("opcache.enable_cli", "1".into()),
152 ("opcache.validate_timestamps", "0".into()),
153 ("opcache.memory_consumption", "128".into()),
154 ("opcache.max_accelerated_files", "10000".into()),
155 ("opcache.interned_strings_buffer", "16".into()),
156 ("realpath_cache_size", "4096K".into()),
157 ("realpath_cache_ttl", "600".into()),
158 ("open_basedir", format!("{}:/tmp", self.document_root)),
162 ("enable_dl", "0".into()),
164 ("disable_functions", [
168 "exec", "system", "passthru", "shell_exec",
169 "proc_open", "popen", "proc_close", "proc_terminate",
170 "proc_get_status", "proc_nice",
171 "pcntl_exec", "pcntl_fork", "pcntl_signal", "pcntl_waitpid",
172 "pcntl_wexitstatus", "pcntl_alarm",
173 "dl",
174 "putenv", "apache_setenv",
176 "show_source", "phpinfo",
177 ].join(",").into()),
178 ("expose_php", "Off".into()),
180 ("display_errors", "Off".into()),
181 ("log_errors", "On".into()),
182 ];
183
184 for (key, value) in &defaults {
185 if !self.ini.contains_key(*key) {
186 if key.contains('\n')
187 || key.contains('\r')
188 || key.contains('\0')
189 || value.contains('\n')
190 || value.contains('\r')
191 || value.contains('\0')
192 {
193 eprintln!(
194 "[SECURITY] Skipping INI entry with unsafe characters: {}",
195 key
196 );
197 continue;
198 }
199 entries.push_str(&format!("{}={}\n", key, value));
200 }
201 }
202
203 const SECURITY_CRITICAL_KEYS: &[&str] = &[
206 "disable_functions",
207 "open_basedir",
208 "enable_dl",
209 "allow_url_include",
210 "allow_url_fopen",
211 ];
212
213 for (key, value) in &self.ini {
215 if key.contains('\n')
216 || key.contains('\r')
217 || key.contains('\0')
218 || value.contains('\n')
219 || value.contains('\r')
220 || value.contains('\0')
221 {
222 eprintln!(
223 "[SECURITY] Skipping INI entry with unsafe characters: {}",
224 key
225 );
226 continue;
227 }
228 if SECURITY_CRITICAL_KEYS.iter().any(|&k| k == key.as_str()) {
229 eprintln!(
230 "[SECURITY WARNING] PHP INI override for '{}' — this weakens the \
231 default sandbox. Ensure this is intentional. Value: '{}'",
232 key, value
233 );
234 tracing::warn!(
235 key = key.as_str(),
236 value = value.as_str(),
237 "PHP security-critical INI override — default sandbox weakened"
238 );
239 }
240 entries.push_str(&format!("{}={}\n", key, value));
241 }
242
243 entries
244 }
245
246 pub fn is_worker_mode(&self) -> bool {
251 if self.worker_script.is_none() {
252 return false;
253 }
254
255 #[cfg(php_zts)]
256 {
257 true
258 }
259
260 #[cfg(not(php_zts))]
261 {
262 tracing::warn!(
263 "Worker mode requires ZTS PHP (--enable-zts). \
264 Falling back to classic mode."
265 );
266 false
267 }
268 }
269
270 pub fn is_php_request(&self, path: &str) -> bool {
272 for ext in &self.extensions {
274 if path.ends_with(ext.as_str()) {
275 return true;
276 }
277 }
278 if path.ends_with('/') || !path.contains('.') {
280 return true; }
282 false
283 }
284
285 pub fn resolve_script(&self, request_path: &str) -> Option<String> {
289 let doc_root = std::path::Path::new(&self.document_root);
290
291 let relative = request_path.trim_start_matches('/');
293 if relative.contains("..") || relative.contains('\0') {
294 return None;
295 }
296
297 let candidate = doc_root.join(relative);
300 if candidate.is_file() {
301 let canon_root = std::fs::canonicalize(doc_root).ok()?;
303 let canon_file = std::fs::canonicalize(&candidate).ok()?;
304 if !canon_file.starts_with(&canon_root) {
305 tracing::warn!(
306 path = %request_path,
307 resolved = %canon_file.display(),
308 root = %canon_root.display(),
309 "Path traversal blocked (symlink escape)"
310 );
311 return None;
312 }
313 return canon_file.to_str().map(|s| s.to_string());
314 }
315
316 let index_candidate = doc_root.join(relative).join(&self.index);
318
319 if index_candidate.is_file() {
320 return index_candidate.to_str().map(|s| s.to_string());
321 }
322
323 let router = doc_root.join(&self.index);
325 if router.is_file() {
326 return router.to_str().map(|s| s.to_string());
327 }
328
329 None
330 }
331}
332
333fn default_document_root() -> String {
334 "./public".into()
335}
336
337fn default_extensions() -> Vec<String> {
338 vec![".php".into()]
339}
340
341fn default_index() -> String {
342 "index.php".into()
343}
344
345fn default_max_body() -> usize {
346 8 * 1024 * 1024
347}
348
349fn default_max_execution_time() -> u32 {
350 30
351}
352
353fn default_max_requests() -> u64 {
354 10_000
355}
356
357#[cfg(test)]
358mod tests {
359 use super::*;
360
361 #[test]
364 fn default_config() {
365 let config = PhpConfig::default();
366 assert!(!config.enabled);
367 assert_eq!(config.document_root, "./public");
368 assert_eq!(config.extensions, vec![".php"]);
369 assert_eq!(config.index, "index.php");
370 assert_eq!(config.max_body_bytes, 8 * 1024 * 1024);
371 assert_eq!(config.max_execution_time, 30);
372 assert_eq!(config.max_requests, 10_000);
373 assert!(config.workers.is_none());
374 assert!(config.ini.is_empty());
375 }
376
377 #[test]
380 fn ini_entries_string() {
381 let mut config = PhpConfig::default();
382 config.ini.insert("memory_limit".into(), "256M".into());
383 config.ini.insert("display_errors".into(), "Off".into());
384 let ini = config.ini_entries_string();
385 assert!(ini.contains("max_execution_time=30"));
386 assert!(ini.contains("memory_limit=256M"));
387 assert!(ini.contains("display_errors=Off"));
388 }
389
390 #[test]
391 fn ini_entries_includes_defaults_when_no_overrides() {
392 let config = PhpConfig::default();
393 let ini = config.ini_entries_string();
394 assert!(ini.contains("max_execution_time=30"));
395 assert!(ini.contains("opcache.enable=1"));
396 assert!(ini.contains("opcache.validate_timestamps=0"));
397 assert!(ini.contains("realpath_cache_size=4096K"));
398 assert!(!ini.contains("opcache.jit="));
400 }
401
402 #[test]
403 fn ini_entries_custom_execution_time() {
404 let mut config = PhpConfig::default();
405 config.max_execution_time = 120;
406 let ini = config.ini_entries_string();
407 assert!(ini.starts_with("max_execution_time=120\n"));
408 }
409
410 #[test]
413 fn is_php_request_extensions() {
414 let config = PhpConfig::default();
415 assert!(config.is_php_request("/index.php"));
416 assert!(config.is_php_request("/api/users.php"));
417 assert!(config.is_php_request("/")); assert!(config.is_php_request("/api/users")); assert!(!config.is_php_request("/style.css"));
420 assert!(!config.is_php_request("/script.js"));
421 assert!(!config.is_php_request("/image.png"));
422 }
423
424 #[test]
425 fn is_php_request_custom_extensions() {
426 let mut config = PhpConfig::default();
427 config.extensions = vec![".php".into(), ".phtml".into(), ".php7".into()];
428 assert!(config.is_php_request("/template.phtml"));
429 assert!(config.is_php_request("/legacy.php7"));
430 assert!(config.is_php_request("/index.php"));
431 assert!(!config.is_php_request("/style.css"));
432 }
433
434 #[test]
435 fn is_php_request_trailing_slash() {
436 let config = PhpConfig::default();
437 assert!(config.is_php_request("/admin/"));
438 assert!(config.is_php_request("/"));
439 assert!(config.is_php_request("/api/v2/"));
440 }
441
442 #[test]
443 fn is_php_request_no_extension_paths() {
444 let config = PhpConfig::default();
445 assert!(config.is_php_request("/users"));
447 assert!(config.is_php_request("/api/v2/products"));
448 assert!(config.is_php_request("/dashboard"));
449 }
450
451 #[test]
452 fn is_php_request_static_files_rejected() {
453 let config = PhpConfig::default();
454 assert!(!config.is_php_request("/favicon.ico"));
455 assert!(!config.is_php_request("/robots.txt"));
456 assert!(!config.is_php_request("/sitemap.xml"));
457 assert!(!config.is_php_request("/assets/app.js"));
458 assert!(!config.is_php_request("/css/main.css"));
459 assert!(!config.is_php_request("/images/logo.png"));
460 assert!(!config.is_php_request("/fonts/inter.woff2"));
461 }
462
463 #[test]
466 fn resolve_script_direct_file() {
467 let tmp = std::env::temp_dir().join("bext-php-test-resolve");
468 let _ = std::fs::remove_dir_all(&tmp);
469 std::fs::create_dir_all(&tmp).unwrap();
470 std::fs::write(tmp.join("info.php"), "<?php phpinfo();").unwrap();
471
472 let mut config = PhpConfig::default();
473 config.document_root = tmp.to_str().unwrap().to_string();
474
475 let resolved = config.resolve_script("/info.php");
476 assert!(resolved.is_some());
477 assert!(resolved.unwrap().ends_with("info.php"));
478
479 let _ = std::fs::remove_dir_all(&tmp);
480 }
481
482 #[test]
483 fn resolve_script_index_file() {
484 let tmp = std::env::temp_dir().join("bext-php-test-index");
485 let _ = std::fs::remove_dir_all(&tmp);
486 std::fs::create_dir_all(&tmp).unwrap();
487 std::fs::write(tmp.join("index.php"), "<?php echo 'home';").unwrap();
488
489 let mut config = PhpConfig::default();
490 config.document_root = tmp.to_str().unwrap().to_string();
491
492 let resolved = config.resolve_script("/");
494 assert!(resolved.is_some());
495 assert!(resolved.unwrap().ends_with("index.php"));
496
497 let _ = std::fs::remove_dir_all(&tmp);
498 }
499
500 #[test]
501 fn resolve_script_front_controller() {
502 let tmp = std::env::temp_dir().join("bext-php-test-router");
503 let _ = std::fs::remove_dir_all(&tmp);
504 std::fs::create_dir_all(&tmp).unwrap();
505 std::fs::write(tmp.join("index.php"), "<?php /* router */").unwrap();
506
507 let mut config = PhpConfig::default();
508 config.document_root = tmp.to_str().unwrap().to_string();
509
510 let resolved = config.resolve_script("/api/users/42");
512 assert!(resolved.is_some());
513 assert!(resolved.unwrap().ends_with("index.php"));
514
515 let _ = std::fs::remove_dir_all(&tmp);
516 }
517
518 #[test]
519 fn resolve_script_subdirectory() {
520 let tmp = std::env::temp_dir().join("bext-php-test-subdir");
521 let _ = std::fs::remove_dir_all(&tmp);
522 std::fs::create_dir_all(tmp.join("admin")).unwrap();
523 std::fs::write(tmp.join("admin/dashboard.php"), "<?php").unwrap();
524
525 let mut config = PhpConfig::default();
526 config.document_root = tmp.to_str().unwrap().to_string();
527
528 let resolved = config.resolve_script("/admin/dashboard.php");
529 assert!(resolved.is_some());
530 assert!(resolved.unwrap().contains("admin/dashboard.php"));
531
532 let _ = std::fs::remove_dir_all(&tmp);
533 }
534
535 #[test]
536 fn resolve_script_missing_doc_root() {
537 let config = PhpConfig {
538 document_root: "/nonexistent/path/xyz".into(),
539 ..PhpConfig::default()
540 };
541 assert!(config.resolve_script("/index.php").is_none());
542 }
543
544 #[test]
545 fn resolve_script_path_traversal_blocked() {
546 let config = PhpConfig::default();
547 assert!(config.resolve_script("/../../../etc/passwd").is_none());
548 assert!(config
549 .resolve_script("/admin/../../../etc/shadow")
550 .is_none());
551 assert!(config.resolve_script("/..").is_none());
552 }
553
554 #[test]
555 fn resolve_script_custom_index() {
556 let tmp = std::env::temp_dir().join("bext-php-test-custom-idx");
557 let _ = std::fs::remove_dir_all(&tmp);
558 std::fs::create_dir_all(&tmp).unwrap();
559 std::fs::write(tmp.join("app.php"), "<?php").unwrap();
560
561 let config = PhpConfig {
562 document_root: tmp.to_str().unwrap().to_string(),
563 index: "app.php".into(),
564 ..PhpConfig::default()
565 };
566
567 let resolved = config.resolve_script("/");
568 assert!(resolved.is_some());
569 assert!(resolved.unwrap().ends_with("app.php"));
570
571 let _ = std::fs::remove_dir_all(&tmp);
572 }
573
574 #[test]
577 fn effective_workers_default() {
578 let config = PhpConfig::default();
579 assert!(config.effective_workers() > 0);
580 }
581
582 #[test]
583 fn effective_workers_override() {
584 let mut config = PhpConfig::default();
585 config.workers = Some(8);
586 #[cfg(php_zts)]
587 assert_eq!(config.effective_workers(), 8);
588 #[cfg(not(php_zts))]
589 assert_eq!(config.effective_workers(), 1);
590 }
591
592 #[test]
595 fn toml_round_trip() {
596 let toml_str = r#"
597 enabled = true
598 document_root = "/var/www/html"
599 workers = 4
600 index = "app.php"
601 max_execution_time = 60
602 max_requests = 5000
603 extensions = [".php", ".phtml"]
604
605 [ini]
606 memory_limit = "512M"
607 "opcache.enable" = "1"
608 "opcache.jit" = "1255"
609 "#;
610 let config: PhpConfig = toml::from_str(toml_str).unwrap();
611 assert!(config.enabled);
612 assert_eq!(config.document_root, "/var/www/html");
613 assert_eq!(config.workers, Some(4));
614 assert_eq!(config.index, "app.php");
615 assert_eq!(config.max_execution_time, 60);
616 assert_eq!(config.max_requests, 5000);
617 assert_eq!(config.ini.get("memory_limit").unwrap(), "512M");
618 assert_eq!(config.ini.get("opcache.enable").unwrap(), "1");
619 assert_eq!(config.extensions, vec![".php", ".phtml"]);
620 }
621
622 #[test]
623 fn toml_minimal() {
624 let toml_str = r#"enabled = true"#;
625 let config: PhpConfig = toml::from_str(toml_str).unwrap();
626 assert!(config.enabled);
627 assert_eq!(config.document_root, "./public");
628 assert_eq!(config.extensions, vec![".php"]);
629 }
630
631 #[test]
632 fn toml_empty() {
633 let config: PhpConfig = toml::from_str("").unwrap();
634 assert!(!config.enabled);
635 }
636
637 #[test]
638 fn toml_max_body_override() {
639 let toml_str = r#"max_body_bytes = 1048576"#;
640 let config: PhpConfig = toml::from_str(toml_str).unwrap();
641 assert_eq!(config.max_body_bytes, 1_048_576); }
643
644 #[test]
645 fn toml_nested_in_server_config() {
646 let toml_str = r#"
647 [php]
648 enabled = true
649 document_root = "/srv/app/public"
650 workers = 2
651
652 [php.ini]
653 "session.save_handler" = "files"
654 "#;
655
656 #[derive(serde::Deserialize)]
657 struct Wrapper {
658 php: PhpConfig,
659 }
660
661 let wrapper: Wrapper = toml::from_str(toml_str).unwrap();
662 assert!(wrapper.php.enabled);
663 assert_eq!(wrapper.php.document_root, "/srv/app/public");
664 assert_eq!(wrapper.php.workers, Some(2));
665 assert_eq!(
666 wrapper.php.ini.get("session.save_handler").unwrap(),
667 "files"
668 );
669 }
670
671 #[test]
674 fn is_worker_mode_without_script() {
675 let config = PhpConfig::default();
676 assert!(!config.is_worker_mode());
677 }
678
679 #[test]
680 fn is_worker_mode_with_script() {
681 let config = PhpConfig {
682 worker_script: Some("./worker.php".into()),
683 ..PhpConfig::default()
684 };
685 #[cfg(php_zts)]
687 assert!(config.is_worker_mode());
688 #[cfg(not(php_zts))]
689 assert!(!config.is_worker_mode());
690 }
691
692 #[test]
695 fn cache_responses_default_true() {
696 let config = PhpConfig::default();
697 assert!(config.cache_responses);
698 }
699
700 #[test]
701 fn cache_responses_disable() {
702 let toml_str = r#"cache_responses = false"#;
703 let config: PhpConfig = toml::from_str(toml_str).unwrap();
704 assert!(!config.cache_responses);
705 }
706
707 #[test]
710 fn toml_worker_script() {
711 let toml_str = r#"
712 enabled = true
713 worker_script = "./bootstrap/worker.php"
714 workers = 4
715 "#;
716 let config: PhpConfig = toml::from_str(toml_str).unwrap();
717 assert_eq!(
718 config.worker_script.as_deref(),
719 Some("./bootstrap/worker.php")
720 );
721 assert_eq!(config.workers, Some(4));
722 }
723
724 #[test]
725 fn toml_full_with_all_fields() {
726 let toml_str = r#"
727 enabled = true
728 document_root = "/var/www"
729 workers = 8
730 max_execution_time = 120
731 max_requests = 50000
732 max_body_bytes = 16777216
733 cache_responses = false
734 worker_script = "/opt/app/worker.php"
735 extensions = [".php", ".phtml"]
736 index = "app.php"
737
738 [ini]
739 memory_limit = "1G"
740 "#;
741 let config: PhpConfig = toml::from_str(toml_str).unwrap();
742 assert!(config.enabled);
743 assert_eq!(config.document_root, "/var/www");
744 assert_eq!(config.workers, Some(8));
745 assert_eq!(config.max_execution_time, 120);
746 assert_eq!(config.max_requests, 50000);
747 assert_eq!(config.max_body_bytes, 16_777_216);
748 assert!(!config.cache_responses);
749 assert_eq!(config.worker_script.as_deref(), Some("/opt/app/worker.php"));
750 assert_eq!(config.extensions, vec![".php", ".phtml"]);
751 assert_eq!(config.index, "app.php");
752 assert_eq!(config.ini.get("memory_limit").unwrap(), "1G");
753 }
754
755 #[test]
758 fn ini_user_overrides_default() {
759 let mut config = PhpConfig::default();
760 config.ini.insert("opcache.enable".into(), "0".into());
762 let ini = config.ini_entries_string();
763 assert!(ini.contains("opcache.enable=0"));
765 let count = ini.matches("opcache.enable=").count();
767 assert_eq!(count, 1);
768 }
769
770 #[test]
773 fn is_php_request_empty_path() {
774 let config = PhpConfig::default();
775 assert!(config.is_php_request(""));
777 }
778
779 #[test]
780 fn is_php_request_double_extension() {
781 let config = PhpConfig::default();
782 assert!(config.is_php_request("/file.backup.php"));
783 assert!(!config.is_php_request("/file.php.bak"));
784 }
785
786 #[test]
787 fn is_php_request_with_query_component() {
788 let config = PhpConfig::default();
789 assert!(config.is_php_request("/index.php"));
792 assert!(!config.is_php_request("/index.php?foo=bar"));
796 }
797
798 #[test]
801 fn resolve_script_empty_path() {
802 let tmp = std::env::temp_dir().join("bext-php-test-empty");
803 let _ = std::fs::remove_dir_all(&tmp);
804 std::fs::create_dir_all(&tmp).unwrap();
805 std::fs::write(tmp.join("index.php"), "<?php").unwrap();
806
807 let config = PhpConfig {
808 document_root: tmp.to_str().unwrap().to_string(),
809 ..PhpConfig::default()
810 };
811
812 let resolved = config.resolve_script("");
814 assert!(resolved.is_some());
815 assert!(resolved.unwrap().ends_with("index.php"));
816
817 let _ = std::fs::remove_dir_all(&tmp);
818 }
819
820 #[test]
821 fn resolve_script_dot_dot_in_component() {
822 let config = PhpConfig::default();
823 assert!(config.resolve_script("/foo/../bar.php").is_none());
824 assert!(config.resolve_script("/../index.php").is_none());
825 assert!(config.resolve_script("/a/b/../../c").is_none());
826 }
827
828 #[test]
831 fn resolve_script_null_byte_blocked() {
832 let config = PhpConfig::default();
833 assert!(config.resolve_script("/index.php\0.jpg").is_none());
834 assert!(config.resolve_script("/\0/etc/passwd").is_none());
835 }
836
837 #[test]
838 fn resolve_script_symlink_escape_blocked() {
839 let tmp = std::env::temp_dir().join("bext-php-test-symlink");
840 let _ = std::fs::remove_dir_all(&tmp);
841 std::fs::create_dir_all(&tmp).unwrap();
842 std::fs::write(tmp.join("legit.php"), "<?php").unwrap();
843
844 #[cfg(unix)]
846 {
847 let _ = std::os::unix::fs::symlink("/etc/hostname", tmp.join("escape.php"));
848
849 let config = PhpConfig {
850 document_root: tmp.to_str().unwrap().to_string(),
851 ..PhpConfig::default()
852 };
853
854 assert!(config.resolve_script("/legit.php").is_some());
856
857 assert!(config.resolve_script("/escape.php").is_none());
859 }
860
861 let _ = std::fs::remove_dir_all(&tmp);
862 }
863
864 #[test]
865 fn resolve_script_encoded_traversal_blocked() {
866 let config = PhpConfig::default();
867 assert!(config.resolve_script("/..%2f..%2fetc/passwd").is_none());
869 assert!(config.resolve_script("/....//....//etc/passwd").is_none());
871 }
872
873 #[test]
874 fn is_php_request_bext_internal_skipped() {
875 let config = PhpConfig::default();
878 assert!(config.is_php_request("/__bext/jsc-render"));
880 assert!(config.is_php_request("/__bext/php-call"));
881 }
883
884 #[test]
885 fn ini_no_injection() {
886 let mut config = PhpConfig::default();
887 config.ini.insert("safe_key".into(), "safe_value".into());
889 config
890 .ini
891 .insert("inject\nmalicious".into(), "value".into());
892 let ini = config.ini_entries_string();
893 assert!(ini.contains("safe_key=safe_value"));
896 }
897}