Skip to main content

ccalc_engine/
io.rs

1use std::collections::HashMap;
2use std::fs::{File, OpenOptions};
3use std::io::{BufRead, BufReader, Write};
4
5/// A file handle opened via `fopen`.
6enum FileHandle {
7    Read(BufReader<File>),
8    Write(File),
9}
10
11/// File descriptor table for the REPL session.
12///
13/// Lives in the binary layer and is passed into `eval_with_io`.
14/// File descriptors 1 (stdout) and 2 (stderr) are virtual — not stored here,
15/// handled by `write_to_fd` directly. User-opened files start at fd 3.
16pub struct IoContext {
17    handles: HashMap<i32, FileHandle>,
18    next_fd: i32,
19}
20
21impl Default for IoContext {
22    fn default() -> Self {
23        Self::new()
24    }
25}
26
27impl IoContext {
28    /// Creates an empty I/O context with no open file handles.
29    pub fn new() -> Self {
30        Self {
31            handles: HashMap::new(),
32            next_fd: 3,
33        }
34    }
35
36    /// Opens a file and returns a new file descriptor, or -1 on failure.
37    /// Supported modes: `"r"`, `"w"`, `"a"`, `"r+"`.
38    pub fn fopen(&mut self, path: &str, mode: &str) -> i32 {
39        let handle = match mode {
40            "r" => File::open(path).map(|f| FileHandle::Read(BufReader::new(f))),
41            "w" => File::create(path).map(FileHandle::Write),
42            "a" => OpenOptions::new()
43                .append(true)
44                .create(true)
45                .open(path)
46                .map(FileHandle::Write),
47            "r+" => OpenOptions::new()
48                .read(true)
49                .write(true)
50                .open(path)
51                .map(FileHandle::Write),
52            _ => return -1,
53        };
54        match handle {
55            Ok(h) => {
56                let fd = self.next_fd;
57                self.handles.insert(fd, h);
58                self.next_fd += 1;
59                fd
60            }
61            Err(_) => -1,
62        }
63    }
64
65    /// Closes a file descriptor. Returns 0 on success, -1 if fd is unknown.
66    pub fn fclose(&mut self, fd: i32) -> i32 {
67        if self.handles.remove(&fd).is_some() {
68            0
69        } else {
70            -1
71        }
72    }
73
74    /// Closes all open file handles.
75    pub fn fclose_all(&mut self) {
76        self.handles.clear();
77    }
78
79    /// Reads one line from fd, stripping the trailing newline (`fgetl` semantics).
80    /// Returns `None` at EOF or on error.
81    pub fn fgetl(&mut self, fd: i32) -> Option<String> {
82        match self.handles.get_mut(&fd)? {
83            FileHandle::Read(reader) => {
84                let mut line = String::new();
85                match reader.read_line(&mut line) {
86                    Ok(0) => None,
87                    Ok(_) => {
88                        if line.ends_with('\n') {
89                            line.pop();
90                        }
91                        if line.ends_with('\r') {
92                            line.pop();
93                        }
94                        Some(line)
95                    }
96                    Err(_) => None,
97                }
98            }
99            FileHandle::Write(_) => None,
100        }
101    }
102
103    /// Reads one line from fd, keeping the trailing newline (`fgets` semantics).
104    /// Returns `None` at EOF or on error.
105    pub fn fgets(&mut self, fd: i32) -> Option<String> {
106        match self.handles.get_mut(&fd)? {
107            FileHandle::Read(reader) => {
108                let mut line = String::new();
109                match reader.read_line(&mut line) {
110                    Ok(0) => None,
111                    Ok(_) => Some(line),
112                    Err(_) => None,
113                }
114            }
115            FileHandle::Write(_) => None,
116        }
117    }
118
119    /// Writes a string to a file descriptor.
120    /// fd 1 = stdout, fd 2 = stderr; all others must be in the handle table.
121    pub fn write_to_fd(&mut self, fd: i32, s: &str) -> Result<(), String> {
122        match fd {
123            1 => {
124                print!("{s}");
125                std::io::stdout().flush().ok();
126                Ok(())
127            }
128            2 => {
129                eprint!("{s}");
130                std::io::stderr().flush().ok();
131                Ok(())
132            }
133            _ => match self.handles.get_mut(&fd) {
134                Some(FileHandle::Write(f)) => f
135                    .write_all(s.as_bytes())
136                    .map_err(|e| format!("fprintf: write error: {e}")),
137                Some(FileHandle::Read(_)) => {
138                    Err(format!("fprintf: fd {fd} is not open for writing"))
139                }
140                None => Err(format!("fprintf: invalid file descriptor {fd}")),
141            },
142        }
143    }
144}