1use 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
15fn 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#[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#[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#[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
218pub 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 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}