Skip to main content

cljrs_stdlib/
io.rs

1//! Native implementations for `clojure.rust.io`.
2
3use std::any::Any;
4use std::io::{BufRead, BufReader, BufWriter, Cursor, Read, Write};
5use std::sync::{
6    Arc, Mutex,
7    atomic::{AtomicBool, Ordering},
8};
9
10use cljrs_value::resource::Resource;
11use cljrs_value::{Arity, ResourceHandle, Value, ValueError, ValueResult};
12
13use crate::register_fns;
14
15// ── Concrete resource types ──────────────────────────────────────────────────
16
17// Only owned host data enters a worker. Managed errors are built after the wait.
18fn file_work<T: Send + 'static>(
19    job: impl FnOnce() -> Result<T, String> + Send + 'static,
20) -> ValueResult<T> {
21    cljrs_value::parking::blocking(job).map_err(ValueError::Other)
22}
23
24/// A buffered file reader with parking-aware operations.
25#[derive(Debug)]
26pub struct IoReader {
27    inner: Arc<Mutex<Option<BufReader<std::fs::File>>>>,
28    closed: Arc<AtomicBool>,
29}
30
31impl IoReader {
32    pub fn open(path: &str) -> ValueResult<Self> {
33        let path = path.to_owned();
34        let file = file_work(move || {
35            std::fs::File::open(&path).map_err(|e| format!("cannot open {path}: {e}"))
36        })?;
37        Ok(Self {
38            inner: Arc::new(Mutex::new(Some(BufReader::new(file)))),
39            closed: Arc::new(AtomicBool::new(false)),
40        })
41    }
42
43    pub fn read_line(&self) -> ValueResult<Option<String>> {
44        let inner = self.inner.clone();
45        file_work(move || {
46            let mut guard = inner.lock().unwrap();
47            let reader = guard.as_mut().ok_or("reader is closed")?;
48            let mut line = String::new();
49            let n = reader
50                .read_line(&mut line)
51                .map_err(|e| format!("read error: {e}"))?;
52            Ok((n != 0).then_some(line))
53        })
54    }
55
56    pub fn read_all(&self) -> ValueResult<String> {
57        let inner = self.inner.clone();
58        file_work(move || {
59            let mut guard = inner.lock().unwrap();
60            let reader = guard.as_mut().ok_or("reader is closed")?;
61            let mut buf = String::new();
62            reader
63                .read_to_string(&mut buf)
64                .map_err(|e| format!("read error: {e}"))?;
65            Ok(buf)
66        })
67    }
68}
69
70impl Resource for IoReader {
71    fn close(&self) -> ValueResult<()> {
72        let inner = self.inner.clone();
73        let closed = self.closed.clone();
74        file_work(move || {
75            *inner.lock().unwrap() = None;
76            closed.store(true, Ordering::Release);
77            Ok(())
78        })
79    }
80    fn is_closed(&self) -> bool {
81        self.closed.load(Ordering::Acquire)
82    }
83    fn resource_type(&self) -> &'static str {
84        "reader"
85    }
86    fn as_any(&self) -> &dyn Any {
87        self
88    }
89}
90
91/// A buffered file writer with parking-aware operations.
92#[derive(Debug)]
93pub struct IoWriter {
94    inner: Arc<Mutex<Option<BufWriter<std::fs::File>>>>,
95    closed: Arc<AtomicBool>,
96}
97
98impl IoWriter {
99    pub fn open(path: &str, append: bool) -> ValueResult<Self> {
100        let path = path.to_owned();
101        let file = file_work(move || {
102            std::fs::OpenOptions::new()
103                .write(true)
104                .create(true)
105                .truncate(!append)
106                .append(append)
107                .open(&path)
108                .map_err(|e| format!("cannot open {path}: {e}"))
109        })?;
110        Ok(Self {
111            inner: Arc::new(Mutex::new(Some(BufWriter::new(file)))),
112            closed: Arc::new(AtomicBool::new(false)),
113        })
114    }
115    pub fn write_str(&self, s: &str) -> ValueResult<()> {
116        let inner = self.inner.clone();
117        let text = s.to_owned();
118        file_work(move || {
119            let mut guard = inner.lock().unwrap();
120            let writer = guard.as_mut().ok_or("writer is closed")?;
121            writer
122                .write_all(text.as_bytes())
123                .map_err(|e| format!("write error: {e}"))
124        })
125    }
126    pub fn flush(&self) -> ValueResult<()> {
127        let inner = self.inner.clone();
128        file_work(move || {
129            if let Some(writer) = inner.lock().unwrap().as_mut() {
130                writer.flush().map_err(|e| format!("flush error: {e}"))?;
131            }
132            Ok(())
133        })
134    }
135}
136impl Resource for IoWriter {
137    fn close(&self) -> ValueResult<()> {
138        let inner = self.inner.clone();
139        let closed = self.closed.clone();
140        file_work(move || {
141            let mut guard = inner.lock().unwrap();
142            let mut writer = guard.take();
143            if let Some(writer) = writer.as_mut() {
144                let _ = writer.flush();
145            }
146            drop(writer);
147            closed.store(true, Ordering::Release);
148            Ok(())
149        })
150    }
151    fn is_closed(&self) -> bool {
152        self.closed.load(Ordering::Acquire)
153    }
154    fn resource_type(&self) -> &'static str {
155        "writer"
156    }
157    fn as_any(&self) -> &dyn Any {
158        self
159    }
160}
161
162/// A reader backed by a String (for EDN read-string, etc.).
163#[derive(Debug)]
164pub struct StringReader {
165    inner: Mutex<Option<Cursor<String>>>,
166}
167
168impl StringReader {
169    pub fn new(s: String) -> Self {
170        Self {
171            inner: Mutex::new(Some(Cursor::new(s))),
172        }
173    }
174
175    pub fn read_line(&self) -> ValueResult<Option<String>> {
176        let mut guard = self.inner.lock().unwrap();
177        let cursor = guard
178            .as_mut()
179            .ok_or_else(|| ValueError::Other("reader is closed".into()))?;
180        let mut line = String::new();
181        let n = BufRead::read_line(cursor, &mut line)
182            .map_err(|e| ValueError::Other(format!("read error: {e}")))?;
183        if n == 0 { Ok(None) } else { Ok(Some(line)) }
184    }
185
186    pub fn read_all(&self) -> ValueResult<String> {
187        let mut guard = self.inner.lock().unwrap();
188        let cursor = guard
189            .as_mut()
190            .ok_or_else(|| ValueError::Other("reader is closed".into()))?;
191        let mut buf = String::new();
192        cursor
193            .read_to_string(&mut buf)
194            .map_err(|e| ValueError::Other(format!("read error: {e}")))?;
195        Ok(buf)
196    }
197}
198
199impl Resource for StringReader {
200    fn close(&self) -> ValueResult<()> {
201        *self.inner.lock().unwrap() = None;
202        Ok(())
203    }
204
205    fn is_closed(&self) -> bool {
206        self.inner.lock().unwrap().is_none()
207    }
208
209    fn resource_type(&self) -> &'static str {
210        "string-reader"
211    }
212
213    fn as_any(&self) -> &dyn Any {
214        self
215    }
216}
217
218// ── Native builtins ──────────────────────────────────────────────────────────
219
220pub fn register(globals: &Arc<cljrs_runtime::env::env::GlobalEnv>, ns: &str) {
221    register_fns!(
222        globals,
223        ns,
224        [
225            ("reader", Arity::Fixed(1), builtin_reader),
226            ("writer", Arity::Variadic { min: 1 }, builtin_writer),
227            ("string-reader", Arity::Fixed(1), builtin_string_reader),
228            ("close", Arity::Fixed(1), builtin_close),
229            ("read-line", Arity::Fixed(1), builtin_read_line),
230            ("write", Arity::Fixed(2), builtin_write),
231            ("flush", Arity::Fixed(1), builtin_flush),
232            ("reader?", Arity::Fixed(1), builtin_reader_q),
233            ("writer?", Arity::Fixed(1), builtin_writer_q),
234            ("file", Arity::Fixed(1), builtin_file),
235            (
236                "delete-file",
237                Arity::Variadic { min: 1 },
238                builtin_delete_file
239            ),
240            ("make-parents", Arity::Fixed(1), builtin_make_parents),
241        ]
242    );
243}
244
245fn builtin_reader(args: &[Value]) -> ValueResult<Value> {
246    let path = match &args[0] {
247        Value::Str(s) => s.get().clone(),
248        v => {
249            return Err(ValueError::WrongType {
250                expected: "string",
251                got: v.type_name().to_string(),
252            });
253        }
254    };
255    let reader = IoReader::open(&path)?;
256    Ok(Value::Resource(ResourceHandle::new(reader)))
257}
258
259fn builtin_writer(args: &[Value]) -> ValueResult<Value> {
260    let path = match &args[0] {
261        Value::Str(s) => s.get().clone(),
262        v => {
263            return Err(ValueError::WrongType {
264                expected: "string",
265                got: v.type_name().to_string(),
266            });
267        }
268    };
269    // Check for :append option
270    let append = if args.len() >= 3 {
271        matches!(
272            (&args[1], &args[2]),
273            (Value::Keyword(k), Value::Bool(true)) if k.get().name.as_ref() == "append"
274        )
275    } else {
276        false
277    };
278    let writer = IoWriter::open(&path, append)?;
279    Ok(Value::Resource(ResourceHandle::new(writer)))
280}
281
282fn builtin_string_reader(args: &[Value]) -> ValueResult<Value> {
283    let s = match &args[0] {
284        Value::Str(s) => s.get().clone(),
285        v => {
286            return Err(ValueError::WrongType {
287                expected: "string",
288                got: v.type_name().to_string(),
289            });
290        }
291    };
292    Ok(Value::Resource(ResourceHandle::new(StringReader::new(s))))
293}
294
295fn builtin_close(args: &[Value]) -> ValueResult<Value> {
296    match &args[0] {
297        Value::Resource(r) => {
298            r.close()?;
299            Ok(Value::Nil)
300        }
301        v => Err(ValueError::WrongType {
302            expected: "resource",
303            got: v.type_name().to_string(),
304        }),
305    }
306}
307
308fn builtin_read_line(args: &[Value]) -> ValueResult<Value> {
309    match &args[0] {
310        Value::Resource(r) => {
311            if let Some(reader) = r.downcast::<IoReader>() {
312                match reader.read_line()? {
313                    Some(line) => Ok(Value::string(line)),
314                    None => Ok(Value::Nil),
315                }
316            } else if let Some(reader) = r.downcast::<StringReader>() {
317                match reader.read_line()? {
318                    Some(line) => Ok(Value::string(line)),
319                    None => Ok(Value::Nil),
320                }
321            } else {
322                Err(ValueError::Other("not a readable resource".into()))
323            }
324        }
325        v => Err(ValueError::WrongType {
326            expected: "reader",
327            got: v.type_name().to_string(),
328        }),
329    }
330}
331
332fn builtin_write(args: &[Value]) -> ValueResult<Value> {
333    match &args[0] {
334        Value::Resource(r) => {
335            let s = match &args[1] {
336                Value::Str(s) => s.get().clone(),
337                v => format!("{v}"),
338            };
339            if let Some(writer) = r.downcast::<IoWriter>() {
340                writer.write_str(&s)?;
341                Ok(Value::Nil)
342            } else {
343                Err(ValueError::Other("not a writable resource".into()))
344            }
345        }
346        v => Err(ValueError::WrongType {
347            expected: "writer",
348            got: v.type_name().to_string(),
349        }),
350    }
351}
352
353fn builtin_flush(args: &[Value]) -> ValueResult<Value> {
354    match &args[0] {
355        Value::Resource(r) => {
356            if let Some(writer) = r.downcast::<IoWriter>() {
357                writer.flush()?;
358                Ok(Value::Nil)
359            } else {
360                Err(ValueError::Other("not a writable resource".into()))
361            }
362        }
363        v => Err(ValueError::WrongType {
364            expected: "writer",
365            got: v.type_name().to_string(),
366        }),
367    }
368}
369
370fn builtin_reader_q(args: &[Value]) -> ValueResult<Value> {
371    Ok(Value::Bool(matches!(&args[0], Value::Resource(r) if
372        r.downcast::<IoReader>().is_some() || r.downcast::<StringReader>().is_some()
373    )))
374}
375
376fn builtin_writer_q(args: &[Value]) -> ValueResult<Value> {
377    Ok(Value::Bool(matches!(
378        &args[0],
379        Value::Resource(r) if r.downcast::<IoWriter>().is_some()
380    )))
381}
382
383fn builtin_file(args: &[Value]) -> ValueResult<Value> {
384    match &args[0] {
385        Value::Str(s) => Ok(Value::string(s.get().clone())),
386        v => Err(ValueError::WrongType {
387            expected: "string",
388            got: v.type_name().to_string(),
389        }),
390    }
391}
392
393fn builtin_delete_file(args: &[Value]) -> ValueResult<Value> {
394    let path = match &args[0] {
395        Value::Str(s) => s.get().clone(),
396        v => {
397            return Err(ValueError::WrongType {
398                expected: "string",
399                got: v.type_name().to_string(),
400            });
401        }
402    };
403    let silently = args.len() >= 2 && args[1] != Value::Nil && args[1] != Value::Bool(false);
404    match std::fs::remove_file(&path) {
405        Ok(()) => Ok(Value::Bool(true)),
406        Err(_) if silently => Ok(Value::Bool(false)),
407        Err(e) => Err(ValueError::Other(format!("cannot delete {path}: {e}"))),
408    }
409}
410
411fn builtin_make_parents(args: &[Value]) -> ValueResult<Value> {
412    let path = match &args[0] {
413        Value::Str(s) => s.get().clone(),
414        v => {
415            return Err(ValueError::WrongType {
416                expected: "string",
417                got: v.type_name().to_string(),
418            });
419        }
420    };
421    if let Some(parent) = std::path::Path::new(&path).parent() {
422        std::fs::create_dir_all(parent)
423            .map_err(|e| ValueError::Other(format!("cannot create dirs: {e}")))?;
424    }
425    Ok(Value::Bool(true))
426}
427
428#[cfg(all(test, unix))]
429mod close_tests {
430    use super::*;
431
432    #[test]
433    fn close_ignores_a_final_flush_error_and_is_idempotent() {
434        use std::os::fd::OwnedFd;
435        let (stream, peer) = std::os::unix::net::UnixStream::pair().unwrap();
436        drop(peer);
437        let file = std::fs::File::from(OwnedFd::from(stream));
438        let writer = IoWriter {
439            inner: Arc::new(Mutex::new(Some(BufWriter::new(file)))),
440            closed: Arc::new(AtomicBool::new(false)),
441        };
442        writer.write_str("buffered body").unwrap();
443        assert!(writer.flush().is_err());
444        assert!(!writer.is_closed());
445        writer.close().unwrap();
446        assert!(writer.is_closed());
447        writer.close().unwrap();
448    }
449}