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