use super::*;
pub(crate) async fn fs_call(
state: NativeState,
ctx: RpcCallContext,
method: u32,
payload: Value,
) -> Result<Value, RuntimeError> {
match method {
1 => {
let path = path_arg(&payload, 0)?;
state.policy.allow_fs_path("read_file", &path)?;
let bytes = tokio::fs::read(path).await.map_err(io_error)?;
Ok(array(vec![Value::Binary(bytes)]))
}
2 => {
let path = path_arg(&payload, 0)?;
let bytes = bytes_arg(&payload, 1)?;
state.policy.allow_fs_path("write_file", &path)?;
tokio::fs::write(path, bytes).await.map_err(io_error)?;
Ok(empty())
}
3 => {
let path = path_arg(&payload, 0)?;
let bytes = bytes_arg(&payload, 1)?;
state.policy.allow_fs_path("append_file", &path)?;
let mut file = tokio::fs::OpenOptions::new()
.create(true)
.append(true)
.open(path)
.await
.map_err(io_error)?;
file.write_all(&bytes).await.map_err(io_error)?;
Ok(empty())
}
4 => {
let path = path_arg(&payload, 0)?;
state.policy.allow_fs_path("mkdir", &path)?;
if bool_arg(&payload, 1).unwrap_or(false) {
tokio::fs::create_dir_all(path).await.map_err(io_error)?;
} else {
tokio::fs::create_dir(path).await.map_err(io_error)?;
}
Ok(empty())
}
5 => {
let path = path_arg(&payload, 0)?;
state.policy.allow_fs_path("read_dir", &path)?;
let mut read_dir = tokio::fs::read_dir(path).await.map_err(io_error)?;
let mut entries = Vec::new();
while let Some(entry) = read_dir.next_entry().await.map_err(io_error)? {
let meta = entry.metadata().await.map_err(io_error)?;
entries.push(array(vec![
string(entry.file_name().to_string_lossy()),
string(entry.path().to_string_lossy()),
Value::Boolean(meta.is_file()),
Value::Boolean(meta.is_dir()),
]));
}
Ok(Value::Array(entries))
}
6 => {
let path = path_arg(&payload, 0)?;
state.policy.allow_fs_path("stat", &path)?;
let meta = tokio::fs::metadata(path).await.map_err(io_error)?;
let modified = meta
.modified()
.ok()
.and_then(|time| time.duration_since(UNIX_EPOCH).ok())
.map(|duration| Value::from(duration.as_millis() as i64))
.unwrap_or(Value::Nil);
Ok(array(vec![
Value::from(meta.len()),
Value::Boolean(meta.is_file()),
Value::Boolean(meta.is_dir()),
Value::Boolean(meta.permissions().readonly()),
modified,
]))
}
7 => {
let path = path_arg(&payload, 0)?;
state.policy.allow_fs_path("exists", &path)?;
Ok(Value::Boolean(tokio::fs::metadata(path).await.is_ok()))
}
8 => {
let path = path_arg(&payload, 0)?;
state.policy.allow_fs_path("remove", &path)?;
let recursive = bool_arg(&payload, 1).unwrap_or(false);
let force = bool_arg(&payload, 2).unwrap_or(false);
let result = if recursive {
tokio::fs::remove_dir_all(path).await
} else {
tokio::fs::remove_file(path).await
};
if let Err(error) = result {
if !(force && error.kind() == std::io::ErrorKind::NotFound) {
return Err(io_error(error));
}
}
Ok(empty())
}
9 => {
let from = path_arg(&payload, 0)?;
let to = path_arg(&payload, 1)?;
state.policy.allow_fs_path("rename_from", &from)?;
state.policy.allow_fs_path("rename_to", &to)?;
tokio::fs::rename(from, to).await.map_err(io_error)?;
Ok(empty())
}
10 => {
let from = path_arg(&payload, 0)?;
let to = path_arg(&payload, 1)?;
state.policy.allow_fs_path("copy_from", &from)?;
state.policy.allow_fs_path("copy_to", &to)?;
tokio::fs::copy(from, to).await.map_err(io_error)?;
Ok(empty())
}
11 => open_file_handle(state, ctx, payload).await,
12 => read_file_handle(state, payload).await,
13 => write_file_handle(state, payload).await,
14 => flush_file_handle(state, payload).await,
15 => seek_file_handle(state, payload).await,
16 => set_len_file_handle(state, payload).await,
17 => close_file_handle(state, payload).await,
_ => Err(method_not_found(method)),
}
}
async fn open_file_handle(
state: NativeState,
ctx: RpcCallContext,
payload: Value,
) -> Result<Value, RuntimeError> {
let path = path_arg(&payload, 0)?;
let options = array_arg(&payload, 1)?;
let read = bool_field(options, 0).unwrap_or(false);
let write = bool_field(options, 1).unwrap_or(false);
let append = bool_field(options, 2).unwrap_or(false);
let create = bool_field(options, 3).unwrap_or(false);
let create_new = bool_field(options, 4).unwrap_or(false);
let truncate = bool_field(options, 5).unwrap_or(false);
if !read && !write && !append {
return Err(decode_error(
"open_file requires at least one of read, write, or append",
));
}
if read {
state.policy.allow_fs_path("open_file_read", &path)?;
}
if write || append || create || create_new || truncate {
state.policy.allow_fs_path("open_file_write", &path)?;
}
let file = tokio::fs::OpenOptions::new()
.read(read)
.write(write)
.append(append)
.create(create)
.create_new(create_new)
.truncate(truncate)
.open(path)
.await
.map_err(io_error)?;
let id = resource_id("file");
state.open_files.lock().await.insert(
id.clone(),
OpenFileResource {
owner_connection_id: ctx.connection_id(),
file: Arc::new(AsyncMutex::new(file)),
},
);
Ok(array(vec![string(id)]))
}
async fn read_file_handle(state: NativeState, payload: Value) -> Result<Value, RuntimeError> {
let id = string_arg(&payload, 0)?;
let length = u64_arg(&payload, 1)?;
let length =
usize::try_from(length).map_err(|_| decode_error("file_read length is too large"))?;
let file = open_file(&state, &id).await?;
let mut file = file.lock().await;
let mut buf = vec![0_u8; length];
let read = file.read(&mut buf).await.map_err(io_error)?;
buf.truncate(read);
Ok(array(vec![Value::Binary(buf)]))
}
async fn write_file_handle(state: NativeState, payload: Value) -> Result<Value, RuntimeError> {
let id = string_arg(&payload, 0)?;
let bytes = bytes_arg(&payload, 1)?;
let file = open_file(&state, &id).await?;
file.lock()
.await
.write_all(&bytes)
.await
.map_err(io_error)?;
Ok(empty())
}
async fn flush_file_handle(state: NativeState, payload: Value) -> Result<Value, RuntimeError> {
let id = string_arg(&payload, 0)?;
let file = open_file(&state, &id).await?;
file.lock().await.flush().await.map_err(io_error)?;
Ok(empty())
}
async fn seek_file_handle(state: NativeState, payload: Value) -> Result<Value, RuntimeError> {
let id = string_arg(&payload, 0)?;
let position = i64_arg(&payload, 1)?;
let whence = string_arg(&payload, 2)?;
let seek_from = match whence.as_str() {
"start" => SeekFrom::Start(
u64::try_from(position)
.map_err(|_| decode_error("file_seek start position must be non-negative"))?,
),
"current" => SeekFrom::Current(position),
"end" => SeekFrom::End(position),
_ => {
return Err(decode_error(
"file_seek whence must be start, current, or end",
));
}
};
let file = open_file(&state, &id).await?;
let position = file.lock().await.seek(seek_from).await.map_err(io_error)?;
Ok(array(vec![Value::from(position)]))
}
async fn set_len_file_handle(state: NativeState, payload: Value) -> Result<Value, RuntimeError> {
let id = string_arg(&payload, 0)?;
let len = u64_arg(&payload, 1)?;
let file = open_file(&state, &id).await?;
file.lock().await.set_len(len).await.map_err(io_error)?;
Ok(empty())
}
async fn close_file_handle(state: NativeState, payload: Value) -> Result<Value, RuntimeError> {
let id = string_arg(&payload, 0)?;
state.open_files.lock().await.remove(&id);
Ok(empty())
}