javascript 0.1.13

A JavaScript engine implementation in Rust
Documentation
use std::collections::HashMap;
use std::fs::File;
use std::io::{BufRead, BufReader, Read, Seek, SeekFrom, Write};
use std::sync::{LazyLock, Mutex};

use crate::core::{Expr, JSObjectDataPtr, Value, evaluate_expr, get_own_property, new_js_object_data, obj_set_key_value};
use crate::error::JSError;
use crate::unicode::{utf8_to_utf16, utf16_to_utf8};

static FILE_STORE: LazyLock<Mutex<HashMap<u64, File>>> = LazyLock::new(|| Mutex::new(HashMap::new()));
static NEXT_FILE_ID: LazyLock<Mutex<u64>> = LazyLock::new(|| Mutex::new(1));

fn get_next_file_id() -> u64 {
    let mut id = NEXT_FILE_ID.lock().unwrap();
    let current = *id;
    *id += 1;
    current
}

/// Create a temporary file object
pub(crate) fn create_tmpfile() -> Result<Value, JSError> {
    // Create a real temporary file with a more random suffix
    use std::time::{SystemTime, UNIX_EPOCH};
    let timestamp = SystemTime::now().duration_since(UNIX_EPOCH).unwrap().as_nanos();
    let process_id = std::process::id();
    let thread_id = format!("{:?}", std::thread::current().id());
    let thread_hash = thread_id.chars().map(|c| c as u64).sum::<u64>();
    let temp_dir = std::env::temp_dir();
    let filename = temp_dir.join(format!("rust_js_tmp_{}_{}_{}.tmp", timestamp, process_id, thread_hash));
    match std::fs::OpenOptions::new()
        .read(true)
        .write(true)
        .create(true)
        .truncate(true)
        .open(&filename)
    {
        Ok(file) => {
            let file_id = get_next_file_id();
            FILE_STORE.lock().unwrap().insert(file_id, file);

            let tmp = new_js_object_data();
            obj_set_key_value(&tmp, &"__file_id".into(), Value::Number(file_id as f64))?;
            obj_set_key_value(&tmp, &"__eof".into(), Value::Boolean(false))?;
            // methods
            obj_set_key_value(&tmp, &"puts".into(), Value::Function("tmp.puts".to_string()))?;
            obj_set_key_value(&tmp, &"readAsString".into(), Value::Function("tmp.readAsString".to_string()))?;
            obj_set_key_value(&tmp, &"seek".into(), Value::Function("tmp.seek".to_string()))?;
            obj_set_key_value(&tmp, &"tell".into(), Value::Function("tmp.tell".to_string()))?;
            obj_set_key_value(&tmp, &"putByte".into(), Value::Function("tmp.putByte".to_string()))?;
            obj_set_key_value(&tmp, &"getByte".into(), Value::Function("tmp.getByte".to_string()))?;
            obj_set_key_value(&tmp, &"getline".into(), Value::Function("tmp.getline".to_string()))?;
            obj_set_key_value(&tmp, &"eof".into(), Value::Function("tmp.eof".to_string()))?;
            obj_set_key_value(&tmp, &"close".into(), Value::Function("tmp.close".to_string()))?;
            Ok(Value::Object(tmp))
        }
        Err(e) => Err(raise_eval_error!(format!("Failed to create temporary file: {e}"))),
    }
}

/// Handle file object method calls
pub(crate) fn handle_file_method(obj_map: &JSObjectDataPtr, method: &str, args: &[Expr], env: &JSObjectDataPtr) -> Result<Value, JSError> {
    // If this object is a file-like object (we use '__file_id' as marker)
    if get_own_property(obj_map, &"__file_id".into()).is_some() {
        let file_id_val = get_own_property(obj_map, &"__file_id".into())
            .ok_or(raise_eval_error!("Invalid file object"))?
            .borrow()
            .clone();
        let file_id = match file_id_val {
            Value::Number(n) => n as u64,
            _ => {
                return Err(raise_eval_error!("Invalid file object"));
            }
        };

        let mut file_store = FILE_STORE.lock().unwrap();
        let file = match file_store.get_mut(&file_id) {
            Some(f) => f,
            None => {
                return Err(raise_eval_error!("File not found"));
            }
        };

        match method {
            "puts" => {
                // write string arguments to file
                if args.is_empty() {
                    return Ok(Value::Undefined);
                }
                // build string to write
                let mut to_write = String::new();
                for a in args {
                    let av = evaluate_expr(env, a)?;
                    match av {
                        Value::String(sv) => to_write.push_str(&utf16_to_utf8(&sv)),
                        Value::Number(n) => to_write.push_str(&n.to_string()),
                        Value::Boolean(b) => to_write.push_str(&b.to_string()),
                        _ => {}
                    }
                }
                // write to file
                if file.write_all(to_write.as_bytes()).is_err() {
                    return Ok(Value::Number(-1.0));
                }
                if file.flush().is_err() {
                    return Ok(Value::Number(-1.0));
                }
                return Ok(Value::Undefined);
            }
            "readAsString" => {
                // flush any pending writes and seek to beginning and read entire file
                if file.flush().is_err() {
                    return Ok(Value::String(utf8_to_utf16("")));
                }
                if file.seek(SeekFrom::Start(0)).is_err() {
                    return Ok(Value::String(utf8_to_utf16("")));
                }
                let mut contents = String::new();
                if file.read_to_string(&mut contents).is_err() {
                    return Ok(Value::String(utf8_to_utf16("")));
                }
                return Ok(Value::String(utf8_to_utf16(&contents)));
            }
            "seek" => {
                // seek(offset, whence)
                if args.len() >= 2 {
                    let offv = evaluate_expr(env, &args[0])?;
                    let whv = evaluate_expr(env, &args[1])?;
                    let offset = match offv {
                        Value::Number(n) => n as i64,
                        _ => 0,
                    };
                    let whence = match whv {
                        Value::Number(n) => n as i32,
                        _ => 0,
                    };
                    let seek_from = match whence {
                        0 => SeekFrom::Start(offset as u64), // SEEK_SET
                        1 => SeekFrom::Current(offset),      // SEEK_CUR
                        2 => SeekFrom::End(offset),          // SEEK_END
                        _ => SeekFrom::Start(0),
                    };
                    match file.seek(seek_from) {
                        Ok(pos) => return Ok(Value::Number(pos as f64)),
                        Err(_) => return Ok(Value::Number(-1.0)),
                    }
                }
                return Ok(Value::Number(-1.0));
            }
            "tell" => match file.stream_position() {
                Ok(pos) => return Ok(Value::Number(pos as f64)),
                Err(_) => return Ok(Value::Number(-1.0)),
            },
            "putByte" => {
                if !args.is_empty() {
                    let bv = evaluate_expr(env, &args[0])?;
                    let byte = match bv {
                        Value::Number(n) => n as u8,
                        _ => 0,
                    };
                    // write byte to file
                    if file.write_all(&[byte]).is_err() {
                        return Ok(Value::Number(-1.0));
                    }
                    if file.flush().is_err() {
                        return Ok(Value::Number(-1.0));
                    }
                    return Ok(Value::Undefined);
                }
                return Ok(Value::Undefined);
            }
            "getByte" => {
                // read one byte from current position
                let mut buf = [0u8; 1];
                match file.read(&mut buf) {
                    Ok(1) => return Ok(Value::Number(buf[0] as f64)),
                    _ => return Ok(Value::Number(-1.0)),
                }
            }
            "getline" => {
                // read line from current position
                let mut reader = BufReader::new(&mut *file);
                let mut line = String::new();
                match reader.read_line(&mut line) {
                    Ok(0) => return Ok(Value::Undefined), // EOF
                    Ok(_) => {
                        // remove trailing newline if present
                        if line.ends_with('\n') {
                            line.pop();
                            if line.ends_with('\r') {
                                line.pop();
                            }
                        }
                        return Ok(Value::String(utf8_to_utf16(&line)));
                    }
                    Err(_) => return Ok(Value::Undefined),
                }
            }
            "eof" => {
                // check if we're at EOF
                let mut buf = [0u8; 1];
                match file.read(&mut buf) {
                    Ok(0) => return Ok(Value::Boolean(true)), // EOF
                    Ok(_) => {
                        // unread the byte by seeking back
                        file.seek(SeekFrom::Current(-1))?;
                        return Ok(Value::Boolean(false));
                    }
                    Err(_) => return Ok(Value::Boolean(true)),
                }
            }
            "close" => {
                // remove file from store (file will be closed when dropped)
                drop(file_store.remove(&file_id));
                return Ok(Value::Undefined);
            }
            _ => {}
        }
    }

    Err(raise_eval_error!(format!("File method {method} not implemented")))
}