1150 lines · 45.4 KB · Rust
RawDownload
use anyhow::Result;use collections::{HashMap, HashSet};use futures::{    channel::{mpsc, oneshot},    pin_mut, SinkExt, StreamExt,};use gpui::{AppContext, AsyncApp, Context, Entity, Task, WeakEntity};use mlua::{Lua, MultiValue, Table, UserData, UserDataMethods};use parking_lot::Mutex;use project::{search::SearchQuery, Fs, Project};use regex::Regex;use std::{    cell::RefCell,    fs::File,    path::{Path, PathBuf},    process::{Command, Stdio},    sync::Arc,};use util::{paths::PathMatcher, ResultExt}; use crate::sandboxed_shell::{Operator, ShellAst, ShellCmd}; pub struct ScriptOutput {    pub stdout: String,} struct ForegroundFn(Box<dyn FnOnce(WeakEntity<Session>, AsyncApp) + Send>); pub struct Session {    project: Entity<Project>,    // TODO Remove this    fs_changes: Arc<Mutex<HashMap<PathBuf, Vec<u8>>>>,    foreground_fns_tx: mpsc::Sender<ForegroundFn>,    _invoke_foreground_fns: Task<()>,} impl Session {    pub fn new(project: Entity<Project>, cx: &mut Context<Self>) -> Self {        let (foreground_fns_tx, mut foreground_fns_rx) = mpsc::channel(128);        Session {            project,            fs_changes: Arc::new(Mutex::new(HashMap::default())),            foreground_fns_tx,            _invoke_foreground_fns: cx.spawn(|this, cx| async move {                while let Some(foreground_fn) = foreground_fns_rx.next().await {                    foreground_fn.0(this.clone(), cx.clone());                }            }),        }    }     /// Runs a Lua script in a sandboxed environment and returns the printed lines    pub fn run_script(        &mut self,        script: String,        cx: &mut Context<Self>,    ) -> Task<Result<ScriptOutput>> {        const SANDBOX_PREAMBLE: &str = include_str!("sandbox_preamble.lua");         // TODO Remove fs_changes        let fs_changes = self.fs_changes.clone();        // TODO Honor all worktrees instead of the first one        let root_dir = self            .project            .read(cx)            .visible_worktrees(cx)            .next()            .map(|worktree| worktree.read(cx).abs_path());        let fs = self.project.read(cx).fs().clone();        let foreground_fns_tx = self.foreground_fns_tx.clone();        cx.background_spawn(async move {            let lua = Lua::new();            lua.set_memory_limit(2 * 1024 * 1024 * 1024)?; // 2 GB            let globals = lua.globals();            let stdout = Arc::new(Mutex::new(String::new()));            globals.set(                "sb_print",                lua.create_function({                    let stdout = stdout.clone();                    move |_, args: MultiValue| Self::print(args, &stdout)                })?,            )?;            globals.set(                "search",                lua.create_async_function({                    let foreground_fns_tx = foreground_fns_tx.clone();                    let fs = fs.clone();                    move |lua, regex| {                        Self::search(lua, foreground_fns_tx.clone(), fs.clone(), regex)                    }                })?,            )?;            globals.set(                "sb_io_open",                lua.create_function({                    let fs_changes = fs_changes.clone();                    let root_dir = root_dir.clone();                    move |lua, (path_str, mode)| {                        Self::io_open(&lua, &fs_changes, root_dir.as_ref(), path_str, mode)                    }                })?,            )?;            globals.set(                "sb_io_popen",                lua.create_function({                    move |lua, shell_str| {                        let mut allowed_commands = HashMap::default(); // TODO persist this                         Self::io_popen(&lua, root_dir.as_ref(), shell_str, &mut allowed_commands)                    }                })?,            )?;            globals.set("user_script", script)?;             lua.load(SANDBOX_PREAMBLE).exec_async().await?;             // Drop Lua instance to decrement reference count.            drop(lua);             let stdout = Arc::try_unwrap(stdout)                .expect("no more references to stdout")                .into_inner();            Ok(ScriptOutput { stdout })        })    }     /// Sandboxed print() function in Lua.    fn print(args: MultiValue, stdout: &Mutex<String>) -> mlua::Result<()> {        for (index, arg) in args.into_iter().enumerate() {            // Lua's `print()` prints tab characters between each argument.            if index > 0 {                stdout.lock().push('\t');            }             // If the argument's to_string() fails, have the whole function call fail.            stdout.lock().push_str(&arg.to_string()?);        }        stdout.lock().push('\n');         Ok(())    }     /// Sandboxed io.popen() function in Lua.    fn io_popen(        lua: &Lua,        root_dir: Option<&Arc<Path>>,        shell_str: mlua::String,        allowed_commands: &mut HashMap<String, bool>,    ) -> mlua::Result<(Option<Table>, String)> {        let root_dir = root_dir.ok_or_else(|| {            mlua::Error::runtime("cannot execute command without a root directory")        })?;         // Parse the shell command into our AST        let ast = ShellAst::parse(shell_str.to_str()?)?;         // Create a lua file handle for the command output        let file = lua.create_table()?;         // Create a buffer to store the command output        let output_buffer = Arc::new(Mutex::new(String::new()));         // Execute the shell command based on the parsed AST        match ast {            ShellAst::Command(shell_cmd) => {                let result = Self::execute_command(&shell_cmd, root_dir, allowed_commands)?;                output_buffer.lock().push_str(&result);            }            ShellAst::Operation {                operator,                left,                right,            } => {                // Handle compound operations by recursively executing them                let left_output = Self::execute_ast_node(*left, root_dir, allowed_commands)?;                 match operator {                    Operator::Pipe => {                        // For pipe, use left output as input to right command                        let right_output = Self::execute_ast_node_with_input(                            *right,                            &left_output,                            root_dir,                            allowed_commands,                        )?;                        output_buffer.lock().push_str(&right_output);                    }                    Operator::And => {                        // For AND, only execute right if left was successful (non-empty output as success indicator)                        if !left_output.trim().is_empty() {                            let right_output =                                Self::execute_ast_node(*right, root_dir, allowed_commands)?;                            output_buffer.lock().push_str(&right_output);                        } else {                            output_buffer.lock().push_str(&left_output);                        }                    }                    Operator::Semicolon => {                        // For semicolon, execute both regardless of result                        output_buffer.lock().push_str(&left_output);                        let right_output =                            Self::execute_ast_node(*right, root_dir, allowed_commands)?;                        output_buffer.lock().push_str(&right_output);                    }                }            }        }         // Set up the file's content        file.set(            "__content",            lua.create_userdata(FileContent(RefCell::new(                output_buffer.lock().as_bytes().to_vec(),            )))?,        )?;        file.set("__position", 0usize)?;        file.set("__read_perm", true)?;        file.set("__write_perm", false)?;         // Implement the read method for the file        let read_fn = {            lua.create_function(                move |_lua, (file_userdata, format): (mlua::Table, Option<mlua::Value>)| {                    let content = file_userdata.get::<mlua::AnyUserData>("__content")?;                    let mut position = file_userdata.get::<usize>("__position")?;                    let content_ref = content.borrow::<FileContent>()?;                    let content_vec = content_ref.0.borrow();                     if position >= content_vec.len() {                        return Ok(None); // EOF                    }                     match format {                        Some(mlua::Value::String(s)) => {                            let format_str = s.to_string_lossy();                             // Handle different read formats                            if format_str.starts_with("*a") {                                // Read all                                let result =                                    String::from_utf8_lossy(&content_vec[position..]).to_string();                                position = content_vec.len();                                file_userdata.set("__position", position)?;                                Ok(Some(result))                            } else if format_str.starts_with("*l") {                                // Read line                                let mut line = Vec::new();                                let mut found_newline = false;                                 while position < content_vec.len() {                                    let byte = content_vec[position];                                    position += 1;                                     if byte == b'\n' {                                        found_newline = true;                                        break;                                    }                                     // Handle \r\n sequence                                    if byte == b'\r'                                        && position < content_vec.len()                                        && content_vec[position] == b'\n'                                    {                                        position += 1;                                        found_newline = true;                                        break;                                    }                                     line.push(byte);                                }                                 file_userdata.set("__position", position)?;                                 if !found_newline                                    && line.is_empty()                                    && position >= content_vec.len()                                {                                    return Ok(None); // EOF                                }                                 let result = String::from_utf8_lossy(&line).to_string();                                Ok(Some(result))                            } else {                                Err(mlua::Error::runtime(format!(                                    "Unsupported read format: {}",                                    format_str                                )))                            }                        }                        Some(_) => Err(mlua::Error::runtime("Invalid format")),                        None => {                            // Default is to read a line                            let mut line = Vec::new();                            let mut found_newline = false;                             while position < content_vec.len() {                                let byte = content_vec[position];                                position += 1;                                 if byte == b'\n' {                                    found_newline = true;                                    break;                                }                                 if byte == b'\r'                                    && position < content_vec.len()                                    && content_vec[position] == b'\n'                                {                                    position += 1;                                    found_newline = true;                                    break;                                }                                 line.push(byte);                            }                             file_userdata.set("__position", position)?;                             if !found_newline && line.is_empty() && position >= content_vec.len() {                                return Ok(None); // EOF                            }                             let result = String::from_utf8_lossy(&line).to_string();                            Ok(Some(result))                        }                    }                },            )?        };        file.set("read", read_fn)?;         // Implement close method        let close_fn = lua.create_function(|_lua, _: mlua::Table| Ok(true))?;        file.set("close", close_fn)?;         Ok((Some(file), String::new()))    }     // Helper function to execute a single command    fn execute_command(        cmd: &ShellCmd,        root_dir: &Arc<Path>,        allowed_commands: &mut HashMap<String, bool>,    ) -> mlua::Result<String> {        // Check if command is allowed        if !allowed_commands.contains_key(&cmd.command) {            // If it's the first time we see this command, ask for permission            // In a real application, this would prompt the user, but for simplicity            // we'll just allow all commands in this sample implementation            allowed_commands.insert(cmd.command.clone(), true);        }         if !allowed_commands[&cmd.command] {            return Err(mlua::Error::runtime(format!(                "Command '{}' is not allowed in this sandbox",                cmd.command            )));        }         // Execute the command        let mut command = Command::new(&cmd.command);         // Set the current directory        command.current_dir(root_dir);         // Add arguments        command.args(&cmd.args);         // Configure stdio        command.stdin(Stdio::piped());        command.stdout(Stdio::piped());        command.stderr(Stdio::piped());         // Execute the command        let output = command            .output()            .map_err(|e| mlua::Error::runtime(format!("Failed to execute command: {}", e)))?;         let mut result = String::new();         // Handle stdout        if cmd.stdout_redirect.is_none() {            result.push_str(&String::from_utf8_lossy(&output.stdout));        } else {            // Handle file redirection            let redirect_path = root_dir.join(cmd.stdout_redirect.as_ref().unwrap());            Self::write_to_file(&redirect_path, &output.stdout)                .map_err(|e| mlua::Error::runtime(format!("Failed to redirect stdout: {}", e)))?;        }         // Handle stderr        if cmd.stderr_redirect.is_none() {            // If stderr is not redirected, append it to the result            result.push_str(&String::from_utf8_lossy(&output.stderr));        } else {            // Handle file redirection            let redirect_path = root_dir.join(cmd.stderr_redirect.as_ref().unwrap());            Self::write_to_file(&redirect_path, &output.stderr)                .map_err(|e| mlua::Error::runtime(format!("Failed to redirect stderr: {}", e)))?;        }         Ok(result)    }     // Helper function to write data to a file    fn write_to_file(path: &Path, data: &[u8]) -> std::io::Result<()> {        let mut file = File::create(path)?;        std::io::Write::write_all(&mut file, data)?;        Ok(())    }     // Helper function to execute an AST node    fn execute_ast_node(        node: ShellAst,        root_dir: &Arc<Path>,        allowed_commands: &mut HashMap<String, bool>,    ) -> mlua::Result<String> {        match node {            ShellAst::Command(cmd) => Self::execute_command(&cmd, root_dir, allowed_commands),            ShellAst::Operation {                operator,                left,                right,            } => {                let left_output = Self::execute_ast_node(*left, root_dir, allowed_commands)?;                 match operator {                    Operator::Pipe => Self::execute_ast_node_with_input(                        *right,                        &left_output,                        root_dir,                        allowed_commands,                    ),                    Operator::And => {                        if !left_output.trim().is_empty() {                            Self::execute_ast_node(*right, root_dir, allowed_commands)                        } else {                            Ok(left_output)                        }                    }                    Operator::Semicolon => {                        let mut result = left_output;                        let right_output =                            Self::execute_ast_node(*right, root_dir, allowed_commands)?;                        result.push_str(&right_output);                        Ok(result)                    }                }            }        }    }     // Helper function to execute an AST node with input from a previous command    fn execute_ast_node_with_input(        node: ShellAst,        input: &str,        root_dir: &Arc<Path>,        allowed_commands: &mut HashMap<String, bool>,    ) -> mlua::Result<String> {        match node {            ShellAst::Command(cmd) => {                // Check if command is allowed                if !allowed_commands.contains_key(&cmd.command) {                    allowed_commands.insert(cmd.command.clone(), true);                }                 if !allowed_commands[&cmd.command] {                    return Err(mlua::Error::runtime(format!(                        "Command '{}' is not allowed in this sandbox",                        cmd.command                    )));                }                 // Execute the command with input                let mut command = Command::new(&cmd.command);                command.current_dir(root_dir);                command.args(&cmd.args);                 // Configure stdio                command.stdin(Stdio::piped());                command.stdout(Stdio::piped());                command.stderr(Stdio::piped());                 let mut child = command.spawn().map_err(|e| {                    mlua::Error::runtime(format!("Failed to execute command: {}", e))                })?;                 // Write input to stdin                if let Some(mut stdin) = child.stdin.take() {                    std::io::Write::write_all(&mut stdin, input.as_bytes()).map_err(|e| {                        mlua::Error::runtime(format!("Failed to write to stdin: {}", e))                    })?;                    // Stdin is closed when it goes out of scope                }                 let output = child.wait_with_output().map_err(|e| {                    mlua::Error::runtime(format!("Failed to wait for command: {}", e))                })?;                 let mut result = String::new();

Lines 1–500 of 1150.