0
0
Fork 0
mirror of https://github.com/denoland/deno.git synced 2025-03-03 17:34:47 -05:00

refactor: add deno_permissions crate (#22236)

Issue https://github.com/denoland/deno/issues/22222


![image](https://github.com/denoland/deno/assets/34997667/2af8474b-b919-4519-98ce-9d29bc7829f2)

This PR moves `runtime/permissions` code to a upstream crate called
`deno_permissions`. The `deno_permissions::PermissionsContainer` is put
into the OpState and can be used instead of the current trait-based
permissions system.

For this PR, I've migrated `deno_fetch` to the new crate but kept the
rest of the trait-based system as a wrapper of `deno_permissions` crate.
Doing the migration all at once is error prone and hard to review.

Comparing incremental compile times for `ext/fetch` on Mac M1:

| profile | `cargo build --bin deno` | `cargo plonk build --bin deno` |
| --------- | ------------- | ------------------- |
| `debug`   | 20 s          | 0.8s                |
| `release` | 4 mins 12 s   | 1.4s                  |
This commit is contained in:
Divy Srivastava 2024-03-12 10:42:26 -07:00 committed by Nathan Whitaker
parent 530f073688
commit c8034cef73
No known key found for this signature in database
16 changed files with 381 additions and 210 deletions

17
Cargo.lock generated
View file

@ -1728,6 +1728,22 @@ dependencies = [
"thiserror",
]
[[package]]
name = "deno_permissions"
version = "0.1.0"
dependencies = [
"console_static_text",
"deno_core",
"deno_terminal",
"libc",
"log",
"once_cell",
"serde",
"termcolor",
"which 4.4.2",
"winapi",
]
[[package]]
name = "deno_runtime"
version = "0.149.0"
@ -1750,6 +1766,7 @@ dependencies = [
"deno_napi",
"deno_net",
"deno_node",
"deno_permissions",
"deno_terminal",
"deno_tls",
"deno_url",

View file

@ -28,6 +28,7 @@ members = [
"ext/websocket",
"ext/webstorage",
"runtime",
"runtime/permissions",
"tests",
"tests/ffi",
"tests/napi",
@ -48,6 +49,7 @@ deno_core = { version = "0.269.0", features = ["lazy_eval_snapshot"] }
deno_bench_util = { version = "0.135.0", path = "./bench_util" }
deno_lockfile = "0.19.0"
deno_media_type = { version = "0.1.1", features = ["module_specifier"] }
deno_permissions = { version = "0.1.0", path = "./runtime/permissions" }
deno_runtime = { version = "0.149.0", path = "./runtime" }
deno_terminal = "0.1.1"
napi_sym = { version = "0.71.0", path = "./cli/napi/sym" }

View file

@ -56,7 +56,7 @@ pub fn op_pledge_test_permissions(
let token = Uuid::new_v4();
let parent_permissions = state.borrow_mut::<PermissionsContainer>();
let worker_permissions = {
let mut parent_permissions = parent_permissions.0.lock();
let mut parent_permissions = parent_permissions.0 .0.lock();
let perms = create_child_permissions(&mut parent_permissions, args)?;
PermissionsContainer::new(perms)
};
@ -69,6 +69,7 @@ pub fn op_pledge_test_permissions(
state.put::<PermissionsHolder>(PermissionsHolder(token, parent_permissions));
// NOTE: This call overrides current permission set for the worker
state.put(worker_permissions.0.clone());
state.put::<PermissionsContainer>(worker_permissions);
Ok(token)
@ -85,6 +86,7 @@ pub fn op_restore_test_permissions(
}
let permissions = permissions_holder.1;
state.put(permissions.0.clone());
state.put::<PermissionsContainer>(permissions);
Ok(())
} else {

View file

@ -57,7 +57,7 @@ pub fn op_pledge_test_permissions(
let token = Uuid::new_v4();
let parent_permissions = state.borrow_mut::<PermissionsContainer>();
let worker_permissions = {
let mut parent_permissions = parent_permissions.0.lock();
let mut parent_permissions = parent_permissions.0 .0.lock();
let perms = create_child_permissions(&mut parent_permissions, args)?;
PermissionsContainer::new(perms)
};
@ -69,6 +69,7 @@ pub fn op_pledge_test_permissions(
state.put::<PermissionsHolder>(PermissionsHolder(token, parent_permissions));
// NOTE: This call overrides current permission set for the worker
state.put(worker_permissions.0.clone());
state.put::<PermissionsContainer>(worker_permissions);
Ok(token)
@ -85,6 +86,7 @@ pub fn op_restore_test_permissions(
}
let permissions = permissions_holder.1;
state.put(permissions.0.clone());
state.put::<PermissionsContainer>(permissions);
Ok(())
} else {

View file

@ -168,15 +168,6 @@ impl FetchHandler for DefaultFileFetchHandler {
}
}
pub trait FetchPermissions {
fn check_net_url(
&mut self,
_url: &Url,
api_name: &str,
) -> Result<(), AnyError>;
fn check_read(&mut self, _p: &Path, api_name: &str) -> Result<(), AnyError>;
}
pub fn get_declaration() -> PathBuf {
PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("lib.deno_fetch.d.ts")
}
@ -268,6 +259,15 @@ impl Drop for ResourceToBodyAdapter {
}
}
pub trait FetchPermissions {
fn check_net_url(
&mut self,
_url: &Url,
api_name: &str,
) -> Result<(), AnyError>;
fn check_read(&mut self, _p: &Path, api_name: &str) -> Result<(), AnyError>;
}
#[op2]
#[serde]
#[allow(clippy::too_many_arguments)]

View file

@ -90,6 +90,7 @@ deno_kv.workspace = true
deno_napi.workspace = true
deno_net.workspace = true
deno_node.workspace = true
deno_permissions.workspace = true
deno_terminal.workspace = true
deno_tls.workspace = true
deno_url.workspace = true

View file

@ -16,6 +16,8 @@ pub use deno_kv;
pub use deno_napi;
pub use deno_net;
pub use deno_node;
pub use deno_permissions;
pub use deno_terminal::colors;
pub use deno_tls;
pub use deno_url;
pub use deno_web;

View file

@ -1,8 +1,8 @@
// Copyright 2018-2024 the Deno authors. All rights reserved. MIT license.
use crate::permissions::parse_sys_kind;
use crate::permissions::PermissionState;
use crate::permissions::PermissionsContainer;
use ::deno_permissions::parse_sys_kind;
use ::deno_permissions::PermissionState;
use ::deno_permissions::PermissionsContainer;
use deno_core::error::custom_error;
use deno_core::error::uri_error;
use deno_core::error::AnyError;

View file

@ -153,7 +153,7 @@ fn op_create_worker(
let parent_permissions = state.borrow_mut::<PermissionsContainer>();
let worker_permissions = if let Some(child_permissions_arg) = args.permissions
{
let mut parent_permissions = parent_permissions.0.lock();
let mut parent_permissions = parent_permissions.0 .0.lock();
let perms =
create_child_permissions(&mut parent_permissions, child_permissions_arg)?;
PermissionsContainer::new(perms)

238
runtime/permissions.rs Normal file
View file

@ -0,0 +1,238 @@
// Copyright 2018-2024 the Deno authors. All rights reserved. MIT license.
use std::path::Path;
use deno_core::error::AnyError;
use deno_core::url::Url;
pub use deno_permissions::create_child_permissions;
pub use deno_permissions::parse_sys_kind;
pub use deno_permissions::set_prompt_callbacks;
pub use deno_permissions::ChildPermissionsArg;
pub use deno_permissions::Permissions;
pub use deno_permissions::PermissionsOptions;
// NOTE: Temporary permissions container to satisfy traits. We are migrating to the deno_permissions
// crate.
#[derive(Debug, Clone)]
pub struct PermissionsContainer(pub deno_permissions::PermissionsContainer);
impl PermissionsContainer {
pub fn new(permissions: deno_permissions::Permissions) -> Self {
Self(deno_permissions::PermissionsContainer::new(permissions))
}
pub fn allow_all() -> Self {
Self(deno_permissions::PermissionsContainer::allow_all())
}
}
impl std::ops::Deref for PermissionsContainer {
type Target = deno_permissions::PermissionsContainer;
fn deref(&self) -> &Self::Target {
&self.0
}
}
impl std::ops::DerefMut for PermissionsContainer {
fn deref_mut(&mut self) -> &mut Self::Target {
&mut self.0
}
}
impl deno_node::NodePermissions for PermissionsContainer {
#[inline(always)]
fn check_net_url(
&mut self,
url: &Url,
api_name: &str,
) -> Result<(), AnyError> {
self.0.check_net_url(url, api_name)
}
#[inline(always)]
fn check_read_with_api_name(
&self,
path: &Path,
api_name: Option<&str>,
) -> Result<(), AnyError> {
self.0.check_read_with_api_name(path, api_name)
}
#[inline(always)]
fn check_write_with_api_name(
&self,
path: &Path,
api_name: Option<&str>,
) -> Result<(), AnyError> {
self.0.check_write_with_api_name(path, api_name)
}
fn check_sys(&self, kind: &str, api_name: &str) -> Result<(), AnyError> {
self.0.check_sys(kind, api_name)
}
}
impl deno_fetch::FetchPermissions for PermissionsContainer {
#[inline(always)]
fn check_net_url(
&mut self,
url: &Url,
api_name: &str,
) -> Result<(), AnyError> {
self.0.check_net_url(url, api_name)
}
#[inline(always)]
fn check_read(
&mut self,
path: &Path,
api_name: &str,
) -> Result<(), AnyError> {
self.0.check_read(path, api_name)
}
}
impl deno_net::NetPermissions for PermissionsContainer {
#[inline(always)]
fn check_net<T: AsRef<str>>(
&mut self,
host: &(T, Option<u16>),
api_name: &str,
) -> Result<(), AnyError> {
self.0.check_net(host, api_name)
}
#[inline(always)]
fn check_read(
&mut self,
path: &Path,
api_name: &str,
) -> Result<(), AnyError> {
self.0.check_read(path, api_name)
}
#[inline(always)]
fn check_write(
&mut self,
path: &Path,
api_name: &str,
) -> Result<(), AnyError> {
self.0.check_write(path, api_name)
}
}
impl deno_web::TimersPermission for PermissionsContainer {
#[inline(always)]
fn allow_hrtime(&mut self) -> bool {
self.0.allow_hrtime()
}
}
impl deno_websocket::WebSocketPermissions for PermissionsContainer {
#[inline(always)]
fn check_net_url(
&mut self,
url: &Url,
api_name: &str,
) -> Result<(), AnyError> {
self.0.check_net_url(url, api_name)
}
}
impl deno_fs::FsPermissions for PermissionsContainer {
fn check_read(
&mut self,
path: &Path,
api_name: &str,
) -> Result<(), AnyError> {
self.0.check_read(path, api_name)
}
fn check_read_blind(
&mut self,
path: &Path,
display: &str,
api_name: &str,
) -> Result<(), AnyError> {
self.0.check_read_blind(path, display, api_name)
}
fn check_write(
&mut self,
path: &Path,
api_name: &str,
) -> Result<(), AnyError> {
self.0.check_write(path, api_name)
}
fn check_write_partial(
&mut self,
path: &Path,
api_name: &str,
) -> Result<(), AnyError> {
self.0.check_write_partial(path, api_name)
}
fn check_write_blind(
&mut self,
p: &Path,
display: &str,
api_name: &str,
) -> Result<(), AnyError> {
self.0.check_write_blind(p, display, api_name)
}
fn check_read_all(&mut self, api_name: &str) -> Result<(), AnyError> {
self.0.check_read_all(api_name)
}
fn check_write_all(&mut self, api_name: &str) -> Result<(), AnyError> {
self.0.check_write_all(api_name)
}
}
// NOTE(bartlomieju): for now, NAPI uses `--allow-ffi` flag, but that might
// change in the future.
impl deno_napi::NapiPermissions for PermissionsContainer {
#[inline(always)]
fn check(&mut self, path: Option<&Path>) -> Result<(), AnyError> {
self.0.check_ffi(path)
}
}
impl deno_ffi::FfiPermissions for PermissionsContainer {
#[inline(always)]
fn check_partial(&mut self, path: Option<&Path>) -> Result<(), AnyError> {
self.0.check_ffi_partial(path)
}
}
impl deno_kv::sqlite::SqliteDbHandlerPermissions for PermissionsContainer {
#[inline(always)]
fn check_read(&mut self, p: &Path, api_name: &str) -> Result<(), AnyError> {
self.0.check_read(p, api_name)
}
#[inline(always)]
fn check_write(&mut self, p: &Path, api_name: &str) -> Result<(), AnyError> {
self.0.check_write(p, api_name)
}
}
impl deno_kv::remote::RemoteDbHandlerPermissions for PermissionsContainer {
#[inline(always)]
fn check_env(&mut self, var: &str) -> Result<(), AnyError> {
self.0.check_env(var)
}
#[inline(always)]
fn check_net_url(
&mut self,
url: &Url,
api_name: &str,
) -> Result<(), AnyError> {
self.0.check_net_url(url, api_name)
}
}

View file

@ -0,0 +1,28 @@
# Copyright 2018-2024 the Deno authors. All rights reserved. MIT license.
[package]
name = "deno_permissions"
version = "0.1.0"
authors.workspace = true
edition.workspace = true
license.workspace = true
repository.workspace = true
description = "Provides the deno permissions implementation."
[lib]
name = "deno_permissions"
path = "lib.rs"
[dependencies]
console_static_text.workspace = true
deno_core.workspace = true
deno_terminal.workspace = true
libc.workspace = true
log.workspace = true
once_cell.workspace = true
serde.workspace = true
termcolor.workspace = true
which = "4.2.5"
[target.'cfg(windows)'.dependencies]
winapi = { workspace = true, features = ["commapi", "knownfolders", "mswsock", "objbase", "psapi", "shlobj", "tlhelp32", "winbase", "winerror", "winuser", "winsock2"] }

View file

View file

@ -1,10 +1,11 @@
// Copyright 2018-2024 the Deno authors. All rights reserved. MIT license.
use crate::fs_util::resolve_from_cwd;
use deno_core::anyhow::Context;
use deno_core::error::custom_error;
use deno_core::error::type_error;
use deno_core::error::uri_error;
use deno_core::error::AnyError;
use deno_core::normalize_path;
use deno_core::parking_lot::Mutex;
use deno_core::serde::de;
use deno_core::serde::Deserialize;
@ -15,7 +16,6 @@ use deno_core::url;
use deno_core::url::Url;
use deno_core::ModuleSpecifier;
use deno_terminal::colors;
use log;
use once_cell::sync::Lazy;
use std::borrow::Cow;
use std::collections::HashSet;
@ -28,7 +28,7 @@ use std::string::ToString;
use std::sync::Arc;
use which::which;
mod prompter;
pub mod prompter;
use prompter::permission_prompt;
use prompter::PromptResponse;
use prompter::PERMISSION_EMOJI;
@ -36,6 +36,18 @@ use prompter::PERMISSION_EMOJI;
pub use prompter::set_prompt_callbacks;
pub use prompter::PromptCallback;
#[inline]
fn resolve_from_cwd(path: &Path) -> Result<PathBuf, AnyError> {
if path.is_absolute() {
Ok(normalize_path(path))
} else {
#[allow(clippy::disallowed_methods)]
let cwd = std::env::current_dir()
.context("Failed to get current working directory")?;
Ok(normalize_path(cwd.join(path)))
}
}
static DEBUG_LOG_ENABLED: Lazy<bool> =
Lazy::new(|| log::log_enabled!(log::Level::Debug));
@ -1271,6 +1283,11 @@ impl PermissionsContainer {
Self(Arc::new(Mutex::new(perms)))
}
#[inline(always)]
pub fn allow_hrtime(&mut self) -> bool {
self.0.lock().hrtime.check().is_ok()
}
pub fn allow_all() -> Self {
Self::new(Permissions::allow_all())
}
@ -1292,6 +1309,15 @@ impl PermissionsContainer {
self.0.lock().read.check(path, Some(api_name))
}
#[inline(always)]
pub fn check_read_with_api_name(
&self,
path: &Path,
api_name: Option<&str>,
) -> Result<(), AnyError> {
self.0.lock().read.check(path, api_name)
}
#[inline(always)]
pub fn check_read_blind(
&mut self,
@ -1316,6 +1342,15 @@ impl PermissionsContainer {
self.0.lock().write.check(path, Some(api_name))
}
#[inline(always)]
pub fn check_write_with_api_name(
&self,
path: &Path,
api_name: Option<&str>,
) -> Result<(), AnyError> {
self.0.lock().write.check(path, api_name)
}
#[inline(always)]
pub fn check_write_all(&mut self, api_name: &str) -> Result<(), AnyError> {
self.0.lock().write.check_all(Some(api_name))
@ -1331,6 +1366,15 @@ impl PermissionsContainer {
self.0.lock().write.check_blind(path, display, api_name)
}
#[inline(always)]
pub fn check_write_partial(
&mut self,
path: &Path,
api_name: &str,
) -> Result<(), AnyError> {
self.0.lock().write.check_partial(path, Some(api_name))
}
#[inline(always)]
pub fn check_run(
&mut self,
@ -1346,11 +1390,7 @@ impl PermissionsContainer {
}
#[inline(always)]
pub fn check_sys(
&mut self,
kind: &str,
api_name: &str,
) -> Result<(), AnyError> {
pub fn check_sys(&self, kind: &str, api_name: &str) -> Result<(), AnyError> {
self.0.lock().sys.check(kind, Some(api_name))
}
@ -1363,11 +1403,9 @@ impl PermissionsContainer {
pub fn check_env_all(&mut self) -> Result<(), AnyError> {
self.0.lock().env.check_all()
}
}
impl deno_node::NodePermissions for PermissionsContainer {
#[inline(always)]
fn check_net_url(
pub fn check_net_url(
&mut self,
url: &Url,
api_name: &str,
@ -1376,31 +1414,7 @@ impl deno_node::NodePermissions for PermissionsContainer {
}
#[inline(always)]
fn check_read_with_api_name(
&self,
path: &Path,
api_name: Option<&str>,
) -> Result<(), AnyError> {
self.0.lock().read.check(path, api_name)
}
#[inline(always)]
fn check_write_with_api_name(
&self,
path: &Path,
api_name: Option<&str>,
) -> Result<(), AnyError> {
self.0.lock().write.check(path, api_name)
}
fn check_sys(&self, kind: &str, api_name: &str) -> Result<(), AnyError> {
self.0.lock().sys.check(kind, Some(api_name))
}
}
impl deno_net::NetPermissions for PermissionsContainer {
#[inline(always)]
fn check_net<T: AsRef<str>>(
pub fn check_net<T: AsRef<str>>(
&mut self,
host: &(T, Option<u16>),
api_name: &str,
@ -1409,155 +1423,16 @@ impl deno_net::NetPermissions for PermissionsContainer {
}
#[inline(always)]
fn check_read(
&mut self,
path: &Path,
api_name: &str,
) -> Result<(), AnyError> {
self.0.lock().read.check(path, Some(api_name))
}
#[inline(always)]
fn check_write(
&mut self,
path: &Path,
api_name: &str,
) -> Result<(), AnyError> {
self.0.lock().write.check(path, Some(api_name))
}
}
impl deno_fetch::FetchPermissions for PermissionsContainer {
#[inline(always)]
fn check_net_url(
&mut self,
url: &url::Url,
api_name: &str,
) -> Result<(), AnyError> {
self.0.lock().net.check_url(url, Some(api_name))
}
#[inline(always)]
fn check_read(
&mut self,
path: &Path,
api_name: &str,
) -> Result<(), AnyError> {
self.0.lock().read.check(path, Some(api_name))
}
}
impl deno_web::TimersPermission for PermissionsContainer {
#[inline(always)]
fn allow_hrtime(&mut self) -> bool {
self.0.lock().hrtime.check().is_ok()
}
}
impl deno_websocket::WebSocketPermissions for PermissionsContainer {
#[inline(always)]
fn check_net_url(
&mut self,
url: &url::Url,
api_name: &str,
) -> Result<(), AnyError> {
self.0.lock().net.check_url(url, Some(api_name))
}
}
impl deno_fs::FsPermissions for PermissionsContainer {
fn check_read(
&mut self,
path: &Path,
api_name: &str,
) -> Result<(), AnyError> {
self.0.lock().read.check(path, Some(api_name))
}
fn check_read_blind(
&mut self,
path: &Path,
display: &str,
api_name: &str,
) -> Result<(), AnyError> {
self.0.lock().read.check_blind(path, display, api_name)
}
fn check_write(
&mut self,
path: &Path,
api_name: &str,
) -> Result<(), AnyError> {
self.0.lock().write.check(path, Some(api_name))
}
fn check_write_partial(
&mut self,
path: &Path,
api_name: &str,
) -> Result<(), AnyError> {
self.0.lock().write.check_partial(path, Some(api_name))
}
fn check_write_blind(
&mut self,
p: &Path,
display: &str,
api_name: &str,
) -> Result<(), AnyError> {
self.0.lock().write.check_blind(p, display, api_name)
}
fn check_read_all(&mut self, api_name: &str) -> Result<(), AnyError> {
self.0.lock().read.check_all(Some(api_name))
}
fn check_write_all(&mut self, api_name: &str) -> Result<(), AnyError> {
self.0.lock().write.check_all(Some(api_name))
}
}
// NOTE(bartlomieju): for now, NAPI uses `--allow-ffi` flag, but that might
// change in the future.
impl deno_napi::NapiPermissions for PermissionsContainer {
#[inline(always)]
fn check(&mut self, path: Option<&Path>) -> Result<(), AnyError> {
pub fn check_ffi(&mut self, path: Option<&Path>) -> Result<(), AnyError> {
self.0.lock().ffi.check(path.unwrap(), None)
}
}
impl deno_ffi::FfiPermissions for PermissionsContainer {
#[inline(always)]
fn check_partial(&mut self, path: Option<&Path>) -> Result<(), AnyError> {
self.0.lock().ffi.check_partial(path)
}
}
impl deno_kv::sqlite::SqliteDbHandlerPermissions for PermissionsContainer {
#[inline(always)]
fn check_read(&mut self, p: &Path, api_name: &str) -> Result<(), AnyError> {
self.0.lock().read.check(p, Some(api_name))
}
#[inline(always)]
fn check_write(&mut self, p: &Path, api_name: &str) -> Result<(), AnyError> {
self.0.lock().write.check(p, Some(api_name))
}
}
impl deno_kv::remote::RemoteDbHandlerPermissions for PermissionsContainer {
#[inline(always)]
fn check_env(&mut self, var: &str) -> Result<(), AnyError> {
self.0.lock().env.check(var)
}
#[inline(always)]
fn check_net_url(
pub fn check_ffi_partial(
&mut self,
url: &url::Url,
api_name: &str,
path: Option<&Path>,
) -> Result<(), AnyError> {
self.0.lock().net.check_url(url, Some(api_name))
self.0.lock().ffi.check_partial(path)
}
}

View file

@ -19,6 +19,22 @@ use std::sync::Arc;
#[derive(Clone)]
struct Permissions;
impl deno_websocket::WebSocketPermissions for Permissions {
fn check_net_url(
&mut self,
_url: &deno_core::url::Url,
_api_name: &str,
) -> Result<(), deno_core::error::AnyError> {
unreachable!("snapshotting!")
}
}
impl deno_web::TimersPermission for Permissions {
fn allow_hrtime(&mut self) -> bool {
unreachable!("snapshotting!")
}
}
impl deno_fetch::FetchPermissions for Permissions {
fn check_net_url(
&mut self,
@ -37,22 +53,6 @@ impl deno_fetch::FetchPermissions for Permissions {
}
}
impl deno_websocket::WebSocketPermissions for Permissions {
fn check_net_url(
&mut self,
_url: &deno_core::url::Url,
_api_name: &str,
) -> Result<(), deno_core::error::AnyError> {
unreachable!("snapshotting!")
}
}
impl deno_web::TimersPermission for Permissions {
fn allow_hrtime(&mut self) -> bool {
unreachable!("snapshotting!")
}
}
impl deno_ffi::FfiPermissions for Permissions {
fn check_partial(
&mut self,

View file

@ -390,6 +390,7 @@ impl WebWorker {
enable_testing_features: bool,
},
state = |state, options| {
state.put(options.permissions.0.clone());
state.put::<PermissionsContainer>(options.permissions);
state.put(ops::TestingFeaturesEnabled(options.enable_testing_features));
},

View file

@ -315,6 +315,9 @@ impl MainWorker {
enable_testing_features: bool,
},
state = |state, options| {
// Save the permissions container and the wrapper.
state.put(options.permissions.0.clone());
// This is temporary until we migrate all exts/ to the deno_permissions crate.
state.put::<PermissionsContainer>(options.permissions);
state.put(ops::TestingFeaturesEnabled(options.enable_testing_features));
},