1pub mod policy;
16
17pub use policy::{AllowDecision, BuildPolicy, BuildPolicyError, pattern_matches};
18
19use aube_manifest::PackageJson;
20use std::collections::hash_map::DefaultHasher;
21use std::hash::{Hash, Hasher};
22use std::path::{Path, PathBuf};
23
24#[derive(Debug, Clone, Default)]
26pub struct ScriptSettings {
27 pub node_options: Option<String>,
28 pub script_shell: Option<PathBuf>,
29 pub unsafe_perm: Option<bool>,
30 pub shell_emulator: bool,
31}
32
33#[derive(Debug, Clone)]
35pub struct ScriptJail {
36 pub package_dir: PathBuf,
37 pub env: Vec<String>,
38 pub read_paths: Vec<PathBuf>,
39 pub write_paths: Vec<PathBuf>,
40 pub network: bool,
41}
42
43impl ScriptJail {
44 pub fn new(package_dir: impl Into<PathBuf>) -> Self {
45 Self {
46 package_dir: package_dir.into(),
47 env: Vec::new(),
48 read_paths: Vec::new(),
49 write_paths: Vec::new(),
50 network: false,
51 }
52 }
53
54 pub fn with_env(mut self, env: impl IntoIterator<Item = String>) -> Self {
55 self.env = env.into_iter().collect();
56 self
57 }
58
59 pub fn with_read_paths(mut self, paths: impl IntoIterator<Item = PathBuf>) -> Self {
60 self.read_paths = paths.into_iter().collect();
61 self
62 }
63
64 pub fn with_write_paths(mut self, paths: impl IntoIterator<Item = PathBuf>) -> Self {
65 self.write_paths = paths.into_iter().collect();
66 self
67 }
68
69 pub fn with_network(mut self, network: bool) -> Self {
70 self.network = network;
71 self
72 }
73}
74
75pub struct ScriptJailHomeCleanup {
76 path: PathBuf,
77}
78
79impl ScriptJailHomeCleanup {
80 pub fn new(jail: &ScriptJail) -> Self {
81 Self {
82 path: jail_home(&jail.package_dir),
83 }
84 }
85}
86
87impl Drop for ScriptJailHomeCleanup {
88 fn drop(&mut self) {
89 if self.path.exists()
90 && let Err(err) = std::fs::remove_dir_all(&self.path)
91 {
92 tracing::debug!("failed to clean jail HOME {}: {err}", self.path.display());
93 }
94 }
95}
96
97static SCRIPT_SETTINGS: std::sync::OnceLock<std::sync::RwLock<ScriptSettings>> =
98 std::sync::OnceLock::new();
99
100fn script_settings_lock() -> &'static std::sync::RwLock<ScriptSettings> {
101 SCRIPT_SETTINGS.get_or_init(|| std::sync::RwLock::new(ScriptSettings::default()))
102}
103
104pub fn set_script_settings(settings: ScriptSettings) {
108 match script_settings_lock().write() {
109 Ok(mut guard) => *guard = settings,
110 Err(poisoned) => *poisoned.into_inner() = settings,
111 }
112}
113
114fn script_settings() -> ScriptSettings {
115 match script_settings_lock().read() {
116 Ok(guard) => guard.clone(),
117 Err(poisoned) => poisoned.into_inner().clone(),
118 }
119}
120
121pub fn prepend_path(bin_dir: &Path) -> std::ffi::OsString {
124 let path = std::env::var_os("PATH").unwrap_or_default();
125 let mut entries = vec![bin_dir.to_path_buf()];
126 entries.extend(std::env::split_paths(&path));
127 std::env::join_paths(entries).unwrap_or(path)
128}
129
130pub fn spawn_shell(script_cmd: &str) -> tokio::process::Command {
149 let settings = script_settings();
150 spawn_shell_with_settings(script_cmd, &settings)
151}
152
153fn spawn_shell_with_settings(
154 script_cmd: &str,
155 settings: &ScriptSettings,
156) -> tokio::process::Command {
157 #[cfg(unix)]
158 {
159 let mut cmd = tokio::process::Command::new(
160 settings
161 .script_shell
162 .as_deref()
163 .unwrap_or_else(|| Path::new("sh")),
164 );
165 cmd.arg("-c").arg(script_cmd);
166 apply_script_settings_env(&mut cmd, settings);
167 cmd
168 }
169 #[cfg(windows)]
170 {
171 let mut cmd = tokio::process::Command::new(
172 settings
173 .script_shell
174 .as_deref()
175 .unwrap_or_else(|| Path::new("cmd.exe")),
176 );
177 if settings.script_shell.is_some() {
178 cmd.arg("-c").arg(script_cmd);
179 } else {
180 cmd.raw_arg("/d /s /c \"").raw_arg(script_cmd).raw_arg("\"");
185 }
186 apply_script_settings_env(&mut cmd, &settings);
187 cmd
188 }
189}
190
191#[cfg(target_os = "macos")]
192fn sbpl_escape(s: &str) -> String {
193 s.replace('\\', "\\\\").replace('"', "\\\"")
194}
195
196#[cfg(target_os = "macos")]
197fn push_write_rule(rules: &mut Vec<String>, path: &Path) {
198 let path = sbpl_escape(&path.to_string_lossy());
199 let rule = format!("(allow file-write* (subpath \"{path}\"))");
200 if !rules.iter().any(|existing| existing == &rule) {
201 rules.push(rule);
202 }
203}
204
205#[cfg(target_os = "macos")]
206fn jail_profile(jail: &ScriptJail, home: &Path) -> String {
207 let mut rules = vec![
208 "(version 1)".to_string(),
209 "(allow default)".to_string(),
210 "(allow network* (local unix))".to_string(),
211 "(deny file-write*)".to_string(),
212 ];
213 if !jail.network {
214 rules.insert(2, "(deny network*)".to_string());
215 }
216
217 for path in [
218 Path::new("/tmp"),
219 Path::new("/private/tmp"),
220 Path::new("/dev"),
221 ] {
222 push_write_rule(&mut rules, path);
223 }
224 for path in [&jail.package_dir, home] {
225 push_write_rule(&mut rules, path);
226 }
227 for path in &jail.write_paths {
228 push_write_rule(&mut rules, path);
229 }
230 for path in [&jail.package_dir, home] {
231 if let Ok(canonical) = path.canonicalize() {
232 push_write_rule(&mut rules, &canonical);
233 }
234 }
235 for path in &jail.write_paths {
236 if let Ok(canonical) = path.canonicalize() {
237 push_write_rule(&mut rules, &canonical);
238 }
239 }
240 rules.join("\n")
241}
242
243#[cfg(target_os = "macos")]
244fn spawn_jailed_shell(
245 script_cmd: &str,
246 settings: &ScriptSettings,
247 jail: &ScriptJail,
248 home: &Path,
249) -> tokio::process::Command {
250 let shell = settings
251 .script_shell
252 .as_deref()
253 .unwrap_or_else(|| Path::new("sh"));
254 let profile = jail_profile(jail, home);
255 let mut cmd = tokio::process::Command::new("sandbox-exec");
256 cmd.arg("-p")
257 .arg(profile)
258 .arg("--")
259 .arg(shell)
260 .arg("-c")
261 .arg(script_cmd);
262 apply_script_settings_env(&mut cmd, settings);
263 cmd
264}
265
266#[cfg(not(target_os = "macos"))]
267fn spawn_jailed_shell(
268 script_cmd: &str,
269 settings: &ScriptSettings,
270 _jail: &ScriptJail,
271 _home: &Path,
272) -> tokio::process::Command {
273 spawn_shell_with_settings(script_cmd, settings)
274}
275
276pub fn shell_quote_arg(arg: &str) -> String {
299 #[cfg(unix)]
300 {
301 let mut out = String::with_capacity(arg.len() + 2);
302 out.push('\'');
303 for ch in arg.chars() {
304 if ch == '\'' {
305 out.push_str("'\\''");
306 } else {
307 out.push(ch);
308 }
309 }
310 out.push('\'');
311 out
312 }
313 #[cfg(windows)]
314 {
315 let mut out = String::with_capacity(arg.len() + 2);
316 out.push('"');
317 let mut backslashes: usize = 0;
318 for ch in arg.chars() {
319 match ch {
320 '\\' => backslashes += 1,
321 '"' => {
322 for _ in 0..backslashes * 2 + 1 {
323 out.push('\\');
324 }
325 out.push('"');
326 backslashes = 0;
327 }
328 '%' => {
339 for _ in 0..backslashes {
340 out.push('\\');
341 }
342 backslashes = 0;
343 out.push_str("%%");
344 }
345 _ => {
346 for _ in 0..backslashes {
347 out.push('\\');
348 }
349 backslashes = 0;
350 out.push(ch);
351 }
352 }
353 }
354 for _ in 0..backslashes * 2 {
355 out.push('\\');
356 }
357 out.push('"');
358 out
359 }
360}
361
362pub fn exit_code_from_status(status: std::process::ExitStatus) -> i32 {
374 if let Some(code) = status.code() {
375 return code;
376 }
377 #[cfg(unix)]
378 {
379 use std::os::unix::process::ExitStatusExt;
380 if let Some(sig) = status.signal() {
381 return 128 + sig;
382 }
383 }
384 1
385}
386
387fn apply_script_settings_env(cmd: &mut tokio::process::Command, settings: &ScriptSettings) {
388 cmd.env_remove("AUBE_AUTH_TOKEN");
395 if let Some(node_options) = settings.node_options.as_deref() {
396 cmd.env("NODE_OPTIONS", node_options);
397 }
398 if let Some(unsafe_perm) = settings.unsafe_perm {
399 cmd.env(
400 "npm_config_unsafe_perm",
401 if unsafe_perm { "true" } else { "false" },
402 );
403 }
404 if settings.shell_emulator {
405 cmd.env("npm_config_shell_emulator", "true");
406 }
407}
408
409fn safe_jail_env_key(key: &str) -> bool {
410 const EXACT: &[&str] = &[
411 "PATH",
412 "HOME",
413 "TERM",
414 "LANG",
415 "LC_ALL",
416 "INIT_CWD",
417 "npm_lifecycle_event",
418 "npm_package_name",
419 "npm_package_version",
420 ];
421 if EXACT.contains(&key) {
422 return true;
423 }
424 let lower = key.to_ascii_lowercase();
425 if lower.contains("token")
426 || lower.contains("auth")
427 || lower.contains("password")
428 || lower.contains("credential")
429 || lower.contains("secret")
430 {
431 return false;
432 }
433 key.starts_with("npm_config_")
434}
435
436fn inherit_jail_env_key(key: &str, extra_env: &[String]) -> bool {
437 (safe_jail_env_key(key) || extra_env.iter().any(|env| env == key))
438 && !matches!(
439 key,
440 "PATH" | "HOME" | "npm_lifecycle_event" | "npm_package_name" | "npm_package_version"
441 )
442}
443
444fn jail_home(package_dir: &Path) -> PathBuf {
445 let mut hasher = DefaultHasher::new();
446 package_dir.hash(&mut hasher);
447 let hash = hasher.finish();
448 let name = package_dir
449 .file_name()
450 .and_then(|s| s.to_str())
451 .unwrap_or("package")
452 .chars()
453 .map(|c| {
454 if c.is_ascii_alphanumeric() || matches!(c, '.' | '-' | '_') {
455 c
456 } else {
457 '_'
458 }
459 })
460 .collect::<String>();
461 std::env::temp_dir()
462 .join("aube-jail")
463 .join(std::process::id().to_string())
464 .join(format!("{name}-{hash:016x}"))
465}
466
467fn apply_jail_env(
468 cmd: &mut tokio::process::Command,
469 path_env: &std::ffi::OsStr,
470 home: &Path,
471 project_root: &Path,
472 manifest: &PackageJson,
473 script_name: &str,
474 extra_env: &[String],
475) {
476 cmd.env_clear();
477 cmd.env("PATH", path_env)
478 .env("HOME", home)
479 .env("npm_lifecycle_event", script_name);
480 if std::env::var_os("INIT_CWD").is_none() {
481 cmd.env("INIT_CWD", project_root);
482 }
483 if let Some(ref name) = manifest.name {
484 cmd.env("npm_package_name", name);
485 }
486 if let Some(ref version) = manifest.version {
487 cmd.env("npm_package_version", version);
488 }
489 for (key, val) in std::env::vars_os() {
490 let Some(key_str) = key.to_str() else {
491 continue;
492 };
493 if inherit_jail_env_key(key_str, extra_env) {
494 cmd.env(key, val);
495 }
496 }
497}
498
499#[derive(Debug, Clone, Copy, PartialEq, Eq)]
503pub enum LifecycleHook {
504 PreInstall,
505 Install,
506 PostInstall,
507 Prepare,
508}
509
510impl LifecycleHook {
511 pub fn script_name(self) -> &'static str {
512 match self {
513 Self::PreInstall => "preinstall",
514 Self::Install => "install",
515 Self::PostInstall => "postinstall",
516 Self::Prepare => "prepare",
517 }
518 }
519}
520
521pub const DEP_LIFECYCLE_HOOKS: [LifecycleHook; 3] = [
525 LifecycleHook::PreInstall,
526 LifecycleHook::Install,
527 LifecycleHook::PostInstall,
528];
529
530#[cfg(unix)]
538static SAVED_STDERR_FD: std::sync::atomic::AtomicI32 = std::sync::atomic::AtomicI32::new(-1);
539
540#[cfg(unix)]
545pub fn set_saved_stderr_fd(fd: std::os::fd::RawFd) {
546 SAVED_STDERR_FD.store(fd, std::sync::atomic::Ordering::SeqCst);
547}
548
549#[cfg(not(unix))]
554pub fn set_saved_stderr_fd(_fd: i32) {}
555
556#[cfg(unix)]
561pub fn child_stderr() -> std::process::Stdio {
562 let fd = SAVED_STDERR_FD.load(std::sync::atomic::Ordering::SeqCst);
563 if fd < 0 {
564 return std::process::Stdio::inherit();
565 }
566 let borrowed = unsafe { std::os::fd::BorrowedFd::borrow_raw(fd) };
571 match borrowed.try_clone_to_owned() {
572 Ok(owned) => std::process::Stdio::from(owned),
573 Err(_) => std::process::Stdio::inherit(),
574 }
575}
576
577#[cfg(not(unix))]
578pub fn child_stderr() -> std::process::Stdio {
579 std::process::Stdio::inherit()
580}
581
582#[allow(clippy::too_many_arguments)]
599pub async fn run_script(
600 script_dir: &Path,
601 project_root: &Path,
602 modules_dir_name: &str,
603 manifest: &PackageJson,
604 script_name: &str,
605 script_cmd: &str,
606 extra_bin_dirs: &[&Path],
607 jail: Option<&ScriptJail>,
608) -> Result<(), Error> {
609 let project_bin = project_root.join(modules_dir_name).join(".bin");
617 let path = std::env::var_os("PATH").unwrap_or_default();
618 let mut entries: Vec<PathBuf> = Vec::with_capacity(extra_bin_dirs.len() + 1);
619 for dir in extra_bin_dirs {
620 entries.push(dir.to_path_buf());
621 }
622 entries.push(project_bin);
623 entries.extend(std::env::split_paths(&path));
624 let new_path = std::env::join_paths(entries).unwrap_or(path);
625
626 let settings = script_settings();
627 let jail_home = jail.map(|j| jail_home(&j.package_dir));
628 if let Some(home) = &jail_home {
629 std::fs::create_dir_all(home)
630 .map_err(|e| Error::Spawn(script_name.to_string(), e.to_string()))?;
631 }
632 let mut cmd = match (jail, jail_home.as_deref()) {
633 (Some(jail), Some(home)) => spawn_jailed_shell(script_cmd, &settings, jail, home),
634 _ => spawn_shell_with_settings(script_cmd, &settings),
635 };
636 cmd.current_dir(script_dir)
637 .stderr(child_stderr())
638 .env("PATH", &new_path)
639 .env("npm_lifecycle_event", script_name);
640
641 if std::env::var_os("INIT_CWD").is_none() {
648 cmd.env("INIT_CWD", project_root);
649 }
650
651 if let Some(ref name) = manifest.name {
652 cmd.env("npm_package_name", name);
653 }
654 if let Some(ref version) = manifest.version {
655 cmd.env("npm_package_version", version);
656 }
657 if let (Some(jail), Some(home)) = (jail, jail_home.as_deref()) {
658 apply_jail_env(
659 &mut cmd,
660 &new_path,
661 home,
662 project_root,
663 manifest,
664 script_name,
665 &jail.env,
666 );
667 apply_script_settings_env(&mut cmd, &settings);
668 }
669
670 tracing::debug!("lifecycle: {script_name} → {script_cmd}");
671 let status = cmd
672 .status()
673 .await
674 .map_err(|e| Error::Spawn(script_name.to_string(), e.to_string()))?;
675
676 if !status.success() {
677 return Err(Error::NonZeroExit {
678 script: script_name.to_string(),
679 code: status.code(),
680 });
681 }
682
683 Ok(())
684}
685
686pub async fn run_root_hook(
692 project_dir: &Path,
693 modules_dir_name: &str,
694 manifest: &PackageJson,
695 hook: LifecycleHook,
696) -> Result<bool, Error> {
697 run_root_script_by_name(project_dir, modules_dir_name, manifest, hook.script_name()).await
698}
699
700pub async fn run_root_script_by_name(
707 project_dir: &Path,
708 modules_dir_name: &str,
709 manifest: &PackageJson,
710 name: &str,
711) -> Result<bool, Error> {
712 let Some(script_cmd) = manifest.scripts.get(name) else {
713 return Ok(false);
714 };
715 run_script(
716 project_dir,
717 project_dir,
718 modules_dir_name,
719 manifest,
720 name,
721 script_cmd,
722 &[],
723 None,
724 )
725 .await?;
726 Ok(true)
727}
728
729pub fn implicit_install_script(
742 manifest: &PackageJson,
743 has_binding_gyp: bool,
744) -> Option<&'static str> {
745 if !has_binding_gyp {
746 return None;
747 }
748 if manifest
749 .scripts
750 .contains_key(LifecycleHook::Install.script_name())
751 || manifest
752 .scripts
753 .contains_key(LifecycleHook::PreInstall.script_name())
754 {
755 return None;
756 }
757 Some("node-gyp rebuild")
758}
759
760pub fn default_install_script(package_dir: &Path, manifest: &PackageJson) -> Option<&'static str> {
764 implicit_install_script(manifest, package_dir.join("binding.gyp").is_file())
765}
766
767pub fn has_dep_lifecycle_work(package_dir: &Path, manifest: &PackageJson) -> bool {
772 if DEP_LIFECYCLE_HOOKS
773 .iter()
774 .any(|h| manifest.scripts.contains_key(h.script_name()))
775 {
776 return true;
777 }
778 default_install_script(package_dir, manifest).is_some()
779}
780
781#[allow(clippy::too_many_arguments)]
811pub async fn run_dep_hook(
812 package_dir: &Path,
813 dep_modules_dir: &Path,
814 project_root: &Path,
815 modules_dir_name: &str,
816 manifest: &PackageJson,
817 hook: LifecycleHook,
818 tool_bin_dirs: &[&Path],
819 jail: Option<&ScriptJail>,
820) -> Result<bool, Error> {
821 let name = hook.script_name();
822 let script_cmd: &str = match manifest.scripts.get(name) {
823 Some(s) => s.as_str(),
824 None => match hook {
825 LifecycleHook::Install => match default_install_script(package_dir, manifest) {
826 Some(s) => s,
827 None => return Ok(false),
828 },
829 _ => return Ok(false),
830 },
831 };
832 let dep_bin_dir = dep_modules_dir.join(".bin");
833 let mut bin_dirs: Vec<&Path> = Vec::with_capacity(tool_bin_dirs.len() + 1);
834 bin_dirs.push(&dep_bin_dir);
835 bin_dirs.extend(tool_bin_dirs.iter().copied());
836 run_script(
837 package_dir,
838 project_root,
839 modules_dir_name,
840 manifest,
841 name,
842 script_cmd,
843 &bin_dirs,
844 jail,
845 )
846 .await?;
847 Ok(true)
848}
849
850#[derive(Debug, thiserror::Error)]
851pub enum Error {
852 #[error("failed to spawn script {0}: {1}")]
853 Spawn(String, String),
854 #[error("script `{script}` exited with code {code:?}")]
855 NonZeroExit { script: String, code: Option<i32> },
856}
857
858#[cfg(test)]
859mod jail_tests {
860 use super::*;
861
862 #[test]
863 fn jail_home_uses_full_package_path() {
864 let a = jail_home(Path::new("/tmp/project/node_modules/@scope-a/native"));
865 let b = jail_home(Path::new("/tmp/project/node_modules/@scope-b/native"));
866
867 assert_ne!(a, b);
868 assert!(
869 a.file_name()
870 .unwrap()
871 .to_string_lossy()
872 .starts_with("native-")
873 );
874 assert!(
875 b.file_name()
876 .unwrap()
877 .to_string_lossy()
878 .starts_with("native-")
879 );
880 }
881
882 #[test]
883 fn jail_home_cleanup_removes_temp_home() {
884 let package_dir = std::env::temp_dir()
885 .join("aube-jail-cleanup-test")
886 .join(std::process::id().to_string())
887 .join("node_modules")
888 .join("native");
889 let jail = ScriptJail::new(&package_dir);
890 let home = jail_home(&package_dir);
891 std::fs::create_dir_all(home.join(".cache")).unwrap();
892 std::fs::write(home.join(".cache").join("marker"), "x").unwrap();
893
894 {
895 let _cleanup = ScriptJailHomeCleanup::new(&jail);
896 }
897
898 assert!(!home.exists());
899 }
900
901 #[test]
902 fn parent_env_cannot_override_explicit_jail_metadata() {
903 for key in [
904 "PATH",
905 "HOME",
906 "npm_lifecycle_event",
907 "npm_package_name",
908 "npm_package_version",
909 ] {
910 assert!(!inherit_jail_env_key(key, &[]));
911 }
912 assert!(inherit_jail_env_key("INIT_CWD", &[]));
913 assert!(inherit_jail_env_key("npm_config_arch", &[]));
914 assert!(!inherit_jail_env_key("npm_config__authToken", &[]));
915 assert!(inherit_jail_env_key(
916 "SHARP_DIST_BASE_URL",
917 &["SHARP_DIST_BASE_URL".to_string()]
918 ));
919 }
920
921 #[test]
922 fn jail_env_preserves_script_settings_after_clear() {
923 let mut cmd = tokio::process::Command::new("node");
924 let manifest = PackageJson {
925 name: Some("pkg".to_string()),
926 version: Some("1.2.3".to_string()),
927 ..Default::default()
928 };
929 let settings = ScriptSettings {
930 node_options: Some("--conditions=aube".to_string()),
931 unsafe_perm: Some(false),
932 shell_emulator: true,
933 ..Default::default()
934 };
935
936 apply_jail_env(
937 &mut cmd,
938 std::ffi::OsStr::new("/bin"),
939 Path::new("/tmp/aube-jail/home"),
940 Path::new("/tmp/project"),
941 &manifest,
942 "postinstall",
943 &[],
944 );
945 apply_script_settings_env(&mut cmd, &settings);
946
947 let envs = cmd.as_std().get_envs().collect::<Vec<_>>();
948 let env = |name: &str| {
949 envs.iter()
950 .find(|(key, _)| *key == std::ffi::OsStr::new(name))
951 .and_then(|(_, val)| *val)
952 .and_then(|val| val.to_str())
953 };
954
955 assert_eq!(env("NODE_OPTIONS"), Some("--conditions=aube"));
956 assert_eq!(env("npm_config_unsafe_perm"), Some("false"));
957 assert_eq!(env("npm_config_shell_emulator"), Some("true"));
958 assert_eq!(env("npm_lifecycle_event"), Some("postinstall"));
959 assert_eq!(env("npm_package_name"), Some("pkg"));
960 assert_eq!(env("npm_package_version"), Some("1.2.3"));
961 }
962}
963
964#[cfg(all(test, windows))]
965mod windows_quote_tests {
966 use super::shell_quote_arg;
967
968 #[test]
969 fn windows_path_backslash_not_doubled() {
970 let q = shell_quote_arg(r"C:\Users\me\file.txt");
971 assert_eq!(q, "\"C:\\Users\\me\\file.txt\"");
972 }
973
974 #[test]
975 fn windows_trailing_backslash_doubled_before_close_quote() {
976 let q = shell_quote_arg(r"C:\path\");
977 assert_eq!(q, "\"C:\\path\\\\\"");
978 }
979
980 #[test]
981 fn windows_quote_in_arg_escapes_with_backslash() {
982 assert_eq!(shell_quote_arg(r#"a"b"#), "\"a\\\"b\"");
983 assert_eq!(shell_quote_arg(r#"a\"b"#), "\"a\\\\\\\"b\"");
984 assert_eq!(shell_quote_arg(r#"a\\"b"#), "\"a\\\\\\\\\\\"b\"");
985 }
986}