tripley-native-core 0.1.2

Core Tripley Native xRPC services for desktop and WebView containers
Documentation
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())
}