1use std::cell::RefCell;
21use std::collections::HashMap;
22use std::io::{self, SeekFrom};
23use std::rc::Rc;
24
25use lua_types::{LuaError, LuaFileHandle, LuaType, LuaValue};
26use lua_vm::state::{InputHook, OutputHook};
27use crate::state_stub::{LuaState, LuaStateStubExt as _};
28
29thread_local! {
30 static LSTREAM_REGISTRY: RefCell<HashMap<usize, Rc<RefCell<LStream>>>>
37 = RefCell::new(HashMap::new());
38}
39
40fn register_lstream(ud_id: usize, lstream: LStream) -> Rc<RefCell<LStream>> {
41 let cell = Rc::new(RefCell::new(lstream));
42 LSTREAM_REGISTRY.with(|reg| {
43 reg.borrow_mut().insert(ud_id, cell.clone());
44 });
45 cell
46}
47
48fn lookup_lstream(ud_id: usize) -> Option<Rc<RefCell<LStream>>> {
49 LSTREAM_REGISTRY.with(|reg| reg.borrow().get(&ud_id).cloned())
50}
51
52pub const LUA_FILE_HANDLE: &[u8] = b"FILE*";
56
57const IO_INPUT_KEY: &[u8] = b"_IO_input";
59
60const IO_OUTPUT_KEY: &[u8] = b"_IO_output";
62
63const IO_PREFIX_LEN: usize = 4;
65
66const MAX_ARG_LINE: usize = 250;
68
69const L_MAX_LEN_NUM: usize = 200;
71
72const EOF_SENTINEL: i32 = -1;
74
75const LUAL_BUFFER_SIZE: usize = 8192;
77
78pub trait LuaFileOps: LuaFileHandle {
87 fn set_buf_mode(&mut self, mode: BufMode, size: usize) -> io::Result<()>;
89}
90
91#[derive(Debug, Clone, Copy, PartialEq, Eq)]
95pub enum SeekWhence {
96 Set,
97 Cur,
98 End,
99}
100
101#[derive(Debug, Clone, Copy, PartialEq, Eq)]
103pub enum BufMode {
104 No,
105 Full,
106 Line,
107}
108
109pub enum StdFileKind {
111 Stdin,
112 Stdout,
113 Stderr,
114}
115
116pub struct LStream {
129 pub file: Option<Box<dyn LuaFileHandle>>,
133 pub close_fn: Option<fn(&mut LuaState) -> Result<usize, LuaError>>,
135}
136
137impl LStream {
138 pub fn is_closed(&self) -> bool {
140 self.close_fn.is_none()
141 }
142}
143
144struct StdStreamHandle {
150 kind: StdFileKind,
151 input_hook: Option<InputHook>,
152 output_hook: Option<OutputHook>,
153 unread: Option<u8>,
154}
155
156impl LuaFileHandle for StdStreamHandle {
157 fn read_byte(&mut self) -> i32 {
158 if let Some(byte) = self.unread.take() {
159 return byte as i32;
160 }
161 match self.kind {
162 StdFileKind::Stdin => {
163 if let Some(read_fn) = self.input_hook {
164 let mut buf = [0u8; 1];
165 return match read_fn(&mut buf) {
166 Ok(1) => buf[0] as i32,
167 _ => EOF_SENTINEL,
168 };
169 }
170
171 #[cfg(all(target_arch = "wasm32", target_os = "unknown"))]
172 {
173 EOF_SENTINEL
174 }
175
176 #[cfg(not(all(target_arch = "wasm32", target_os = "unknown")))]
177 {
178 use std::io::Read;
179 let mut buf = [0u8; 1];
180 match std::io::stdin().read(&mut buf) {
181 Ok(1) => buf[0] as i32,
182 _ => EOF_SENTINEL,
183 }
184 }
185 }
186 _ => EOF_SENTINEL,
187 }
188 }
189 fn unread_byte(&mut self, byte: i32) {
190 if (0..=u8::MAX as i32).contains(&byte) {
191 self.unread = Some(byte as u8);
192 }
193 }
194 fn write_bytes(&mut self, data: &[u8]) -> io::Result<usize> {
195 if let Some(write_fn) = self.output_hook {
196 write_fn(data)?;
197 return Ok(data.len());
198 }
199
200 #[cfg(all(target_arch = "wasm32", target_os = "unknown"))]
201 {
202 let _ = data;
203 return Err(io::Error::new(
204 io::ErrorKind::Unsupported,
205 "standard output not available in this host",
206 ));
207 }
208
209 #[cfg(not(all(target_arch = "wasm32", target_os = "unknown")))]
210 {
211 use std::io::Write;
212 match self.kind {
213 StdFileKind::Stderr => {
214 std::io::stderr().write_all(data)?;
215 Ok(data.len())
216 }
217 _ => {
218 std::io::stdout().write_all(data)?;
219 Ok(data.len())
220 }
221 }
222 }
223 }
224 fn flush(&mut self) -> io::Result<()> {
225 if self.output_hook.is_some() {
226 return Ok(());
227 }
228
229 #[cfg(all(target_arch = "wasm32", target_os = "unknown"))]
230 {
231 return Err(io::Error::new(
232 io::ErrorKind::Unsupported,
233 "standard output not available in this host",
234 ));
235 }
236
237 #[cfg(not(all(target_arch = "wasm32", target_os = "unknown")))]
238 {
239 use std::io::Write;
240 match self.kind {
241 StdFileKind::Stderr => std::io::stderr().flush(),
242 _ => std::io::stdout().flush(),
243 }
244 }
245 }
246 fn seek(&mut self, _pos: SeekFrom) -> io::Result<u64> {
247 Err(io::Error::new(io::ErrorKind::Unsupported, "stdio seek"))
248 }
249 fn tell(&mut self) -> io::Result<u64> {
250 Err(io::Error::new(io::ErrorKind::Unsupported, "stdio tell"))
251 }
252 fn clear_error(&mut self) {}
253 fn has_error(&self) -> bool { false }
254}
255
256impl LuaFileOps for StdStreamHandle {
257 fn set_buf_mode(&mut self, _mode: BufMode, _size: usize) -> io::Result<()> { Ok(()) }
258}
259
260impl StdStreamHandle {
261 fn new(
262 kind: StdFileKind,
263 input_hook: Option<InputHook>,
264 output_hook: Option<OutputHook>,
265 ) -> Self {
266 StdStreamHandle {
267 kind,
268 input_hook,
269 output_hook,
270 unread: None,
271 }
272 }
273}
274
275struct ReadNumState {
277 current: i32,
279 count: usize,
281 buf: [u8; L_MAX_LEN_NUM + 1],
283}
284
285impl ReadNumState {
286 fn new(first_byte: i32) -> Self {
287 ReadNumState {
288 current: first_byte,
289 count: 0,
290 buf: [0u8; L_MAX_LEN_NUM + 1],
291 }
292 }
293
294 fn advance(&mut self, file: &mut dyn LuaFileHandle) -> bool {
297 if self.count >= L_MAX_LEN_NUM {
298 self.buf[0] = 0;
299 return false;
300 }
301 self.buf[self.count] = self.current as u8;
302 self.count += 1;
303 self.current = file.read_byte();
304 true
305 }
306
307 fn try2(&mut self, file: &mut dyn LuaFileHandle, set: [u8; 2]) -> bool {
309 if self.current == set[0] as i32 || self.current == set[1] as i32 {
310 self.advance(file)
311 } else {
312 false
313 }
314 }
315
316 fn read_digits(&mut self, file: &mut dyn LuaFileHandle, hex: bool) -> usize {
318 let mut count = 0usize;
319 loop {
320 let is_digit = if hex {
321 (self.current as u8).is_ascii_hexdigit()
322 } else {
323 (self.current as u8).is_ascii_digit()
324 };
325 if !is_digit || self.current == EOF_SENTINEL {
326 break;
327 }
328 if !self.advance(file) {
329 break;
330 }
331 count += 1;
332 }
333 count
334 }
335
336 fn as_bytes(&self) -> &[u8] {
338 &self.buf[..self.count]
339 }
340}
341
342pub const IO_LIB: &[(&[u8], fn(&mut LuaState) -> Result<usize, LuaError>)] = &[
346 (b"close", io_close),
347 (b"flush", io_flush),
348 (b"input", io_input),
349 (b"lines", io_lines),
350 (b"open", io_open),
351 (b"output", io_output),
352 (b"popen", io_popen),
353 (b"read", io_read),
354 (b"tmpfile", io_tmpfile),
355 (b"type", io_type),
356 (b"write", io_write),
357];
358
359pub const FILE_METHODS: &[(&[u8], fn(&mut LuaState) -> Result<usize, LuaError>)] = &[
361 (b"read", f_read),
362 (b"write", f_write),
363 (b"lines", f_lines),
364 (b"flush", f_flush),
365 (b"seek", f_seek),
366 (b"close", f_close),
367 (b"setvbuf", f_setvbuf),
368];
369
370pub const FILE_METAMETHODS: &[(&[u8], fn(&mut LuaState) -> Result<usize, LuaError>)] = &[
372 (b"__gc", f_gc),
373 (b"__close", f_gc),
374 (b"__tostring", f_tostring),
375];
376
377fn check_mode(mode: &[u8]) -> bool {
383 if mode.is_empty() {
384 return false;
385 }
386 let mut idx = 0usize;
387 if !matches!(mode[idx], b'r' | b'w' | b'a') {
388 return false;
389 }
390 idx += 1;
391 if idx < mode.len() && mode[idx] == b'+' {
392 idx += 1;
393 }
394 mode[idx..].iter().all(|&b| b == b'b')
395}
396
397fn check_mode_popen(mode: &[u8]) -> bool {
399 matches!(mode, b"r" | b"w")
400}
401
402fn file_result(
406 state: &mut LuaState,
407 success: bool,
408 fname: Option<&[u8]>,
409 os_err: io::Error,
410) -> Result<usize, LuaError> {
411 if success {
412 state.push(LuaValue::Bool(true));
413 return Ok(1);
414 }
415 state.push(LuaValue::Bool(false));
416 let msg = os_err.to_string();
417 match fname {
418 Some(name) => {
419 let mut s = Vec::with_capacity(name.len() + 2 + msg.len());
420 s.extend_from_slice(name);
421 s.extend_from_slice(b": ");
422 s.extend_from_slice(msg.as_bytes());
423 state.push_string(&s)?;
424 }
425 None => {
426 state.push_string(msg.as_bytes())?;
427 }
428 }
429 let errno_code = os_err.raw_os_error().unwrap_or(0) as i64;
430 state.push(LuaValue::Int(errno_code));
431 Ok(3)
432}
433
434fn exec_result(state: &mut LuaState, stat: i32) -> Result<usize, LuaError> {
441 if stat == 0 {
442 state.push(LuaValue::Bool(true));
443 Ok(1)
444 } else {
445 state.push(LuaValue::Bool(false));
446 state.push_string(b"exit")?;
448 state.push(LuaValue::Int(stat as i64));
449 Ok(3)
450 }
451}
452
453fn get_lstream(state: &mut LuaState) -> Result<Rc<RefCell<LStream>>, LuaError> {
460 let ud = state.check_arg_userdata(1, LUA_FILE_HANDLE)?;
461 lookup_lstream(ud.identity()).ok_or_else(|| {
462 LuaError::runtime(format_args!("invalid file handle"))
463 })
464}
465
466fn lstream_from_upvalue(
473 state: &mut LuaState,
474 idx: i32,
475) -> Result<Rc<RefCell<LStream>>, LuaError> {
476 let v = state.value_at(crate::state_stub::upvalue_index(idx));
477 let ud_id = match v {
478 LuaValue::UserData(ud) => ud.identity(),
479 _ => {
480 return Err(LuaError::runtime(format_args!(
481 "invalid file handle in upvalue {}",
482 idx
483 )));
484 }
485 };
486 lookup_lstream(ud_id).ok_or_else(|| {
487 LuaError::runtime(format_args!("invalid file handle in upvalue {}", idx))
488 })
489}
490
491fn tofile(state: &mut LuaState) -> Result<Rc<RefCell<LStream>>, LuaError> {
493 let p_rc = get_lstream(state)?;
494 {
495 let p = p_rc.borrow();
496 if p.is_closed() {
497 return Err(LuaError::runtime(format_args!(
498 "attempt to use a closed file"
499 )));
500 }
501 debug_assert!(p.file.is_some());
502 }
503 Ok(p_rc)
504}
505
506fn new_pre_file(state: &mut LuaState) -> Result<Rc<RefCell<LStream>>, LuaError> {
513 let ud = state.new_userdata_typed(LUA_FILE_HANDLE, std::mem::size_of::<LStream>(), 0)?;
514 state.set_metatable_by_name(LUA_FILE_HANDLE)?;
515 let cell = register_lstream(ud.identity(), LStream { file: None, close_fn: None });
516 Ok(cell)
517}
518
519fn new_file(state: &mut LuaState) -> Result<Rc<RefCell<LStream>>, LuaError> {
521 let cell = new_pre_file(state)?;
522 cell.borrow_mut().close_fn = Some(io_fclose);
523 Ok(cell)
524}
525
526fn opencheck(state: &mut LuaState, fname: &[u8], mode: &[u8]) -> Result<(), LuaError> {
531 let hook = state.global().file_open_hook;
532 let fh = match hook {
533 Some(open_fn) => open_fn(fname, mode).map_err(|e| {
534 LuaError::runtime(format_args!(
535 "cannot open file '{}' ({})",
536 fname.escape_ascii(),
537 match &e {
538 LuaError::Runtime(LuaValue::Str(s)) => {
539 String::from_utf8_lossy(s.as_bytes()).into_owned()
540 }
541 other => format!("{:?}", other),
542 }
543 ))
544 })?,
545 None => {
546 return Err(LuaError::runtime(format_args!(
547 "cannot open file '{}' (no filesystem hook registered)",
548 fname.escape_ascii()
549 )));
550 }
551 };
552 let cell = new_file(state)?;
553 cell.borrow_mut().file = Some(fh);
554 Ok(())
555}
556
557fn io_fclose(state: &mut LuaState) -> Result<usize, LuaError> {
563 let p_rc = get_lstream(state)?;
564 let _closed = p_rc.borrow_mut().file.take();
566 state.push(LuaValue::Bool(true));
567 Ok(1)
568}
569
570fn io_pclose(state: &mut LuaState) -> Result<usize, LuaError> {
574 let p_rc = get_lstream(state)?;
575 let _closed = p_rc.borrow_mut().file.take();
576 exec_result(state, 0)
578}
579
580fn io_noclose(state: &mut LuaState) -> Result<usize, LuaError> {
582 let p_rc = get_lstream(state)?;
583 p_rc.borrow_mut().close_fn = Some(io_noclose); state.push(LuaValue::Bool(false));
585 state.push_string(b"cannot close standard file")?;
586 Ok(2)
587}
588
589fn aux_close(state: &mut LuaState) -> Result<usize, LuaError> {
591 let p_rc = get_lstream(state)?;
592 let cf = p_rc.borrow_mut().close_fn.take().ok_or_else(|| {
593 LuaError::runtime(format_args!("attempt to close an already-closed file"))
594 })?;
595 cf(state)
596}
597
598pub fn io_type(state: &mut LuaState) -> Result<usize, LuaError> {
602 state.check_arg_any(1)?;
603 let maybe_userdata = state.test_arg_userdata(1, LUA_FILE_HANDLE);
604 match maybe_userdata {
605 None => {
606 state.push(LuaValue::Bool(false));
607 }
608 Some(ud) => {
609 let is_closed = match lookup_lstream(ud.identity()) {
610 Some(rc) => rc.borrow().is_closed(),
611 None => true, };
613 if is_closed {
614 state.push_string(b"closed file")?;
615 } else {
616 state.push_string(b"file")?;
617 }
618 }
619 }
620 Ok(1)
621}
622
623fn f_tostring(state: &mut LuaState) -> Result<usize, LuaError> {
627 let p_rc = get_lstream(state)?;
628 let closed = p_rc.borrow().is_closed();
629 if closed {
630 state.push_string(b"file (closed)")?;
631 } else {
632 state.push_string(b"file (0x?)")?;
634 }
635 Ok(1)
636}
637
638fn f_close(state: &mut LuaState) -> Result<usize, LuaError> {
642 let _ = tofile(state)?; aux_close(state)
644}
645
646pub fn io_close(state: &mut LuaState) -> Result<usize, LuaError> {
648 if state.type_at(1) == LuaType::None {
652 state.registry_get(IO_OUTPUT_KEY)?;
653 }
654 f_close(state)
655}
656
657fn f_gc(state: &mut LuaState) -> Result<usize, LuaError> {
659 let p_rc = get_lstream(state)?;
660 let needs_close = {
661 let p = p_rc.borrow();
662 !p.is_closed() && p.file.is_some()
663 };
664 if needs_close {
665 let _ = aux_close(state);
667 }
668 Ok(0)
669}
670
671pub fn io_open(state: &mut LuaState) -> Result<usize, LuaError> {
678 let filename: Vec<u8> = state.check_arg_string(1)?;
679 let mode: Vec<u8> = state.opt_arg_string(2, b"r")?;
680 if !check_mode(&mode) {
681 return Err(LuaError::arg_error(2, "invalid mode"));
682 }
683 let hook = state.global().file_open_hook;
684 match hook {
685 Some(open_fn) => match open_fn(&filename, &mode) {
686 Ok(fh) => {
687 let cell = new_file(state)?;
688 cell.borrow_mut().file = Some(fh);
689 Ok(1)
690 }
691 Err(e) => {
692 let os_err = io::Error::new(
693 io::ErrorKind::Other,
694 match &e {
695 LuaError::Runtime(LuaValue::Str(s)) => {
696 String::from_utf8_lossy(s.as_bytes()).into_owned()
697 }
698 other => format!("{:?}", other),
699 },
700 );
701 file_result(state, false, Some(&filename), os_err)
702 }
703 },
704 None => {
705 let os_err = io::Error::new(
706 io::ErrorKind::Unsupported,
707 "no filesystem hook registered",
708 );
709 file_result(state, false, Some(&filename), os_err)
710 }
711 }
712}
713
714pub fn io_popen(state: &mut LuaState) -> Result<usize, LuaError> {
723 let filename: Vec<u8> = state.check_arg_string(1)?;
724 let mode: Vec<u8> = state.opt_arg_string(2, b"r")?;
725 if !check_mode_popen(&mode) {
726 return Err(LuaError::arg_error(2, "invalid mode"));
727 }
728 let hook = state.global().popen_hook;
729 match hook {
730 Some(spawn_fn) => match spawn_fn(&filename, &mode) {
731 Ok(fh) => {
732 let cell = new_pre_file(state)?;
733 let mut p = cell.borrow_mut();
734 p.file = Some(fh);
735 p.close_fn = Some(io_pclose);
736 drop(p);
737 Ok(1)
738 }
739 Err(e) => {
740 let os_err = io::Error::new(
741 io::ErrorKind::Other,
742 match &e {
743 LuaError::Runtime(LuaValue::Str(s)) => {
744 String::from_utf8_lossy(s.as_bytes()).into_owned()
745 }
746 other => format!("{:?}", other),
747 },
748 );
749 file_result(state, false, Some(&filename), os_err)
750 }
751 },
752 None => {
753 let os_err = io::Error::new(
754 io::ErrorKind::Unsupported,
755 "popen not enabled in this build",
756 );
757 file_result(state, false, Some(&filename), os_err)
758 }
759 }
760}
761
762fn native_temp_name() -> io::Result<Vec<u8>> {
763 #[cfg(all(target_arch = "wasm32", target_os = "unknown"))]
764 {
765 return Err(io::Error::new(
766 io::ErrorKind::Unsupported,
767 "temporary files not available in this host",
768 ));
769 }
770
771 #[cfg(not(all(target_arch = "wasm32", target_os = "unknown")))]
772 {
773 let mut path = std::env::temp_dir().to_string_lossy().as_bytes().to_vec();
774 if path.last().copied() != Some(b'/') && path.last().copied() != Some(b'\\') {
775 path.push(b'/');
776 }
777 let unique = format!(
778 "lua_tmpfile_{}_{}",
779 std::process::id(),
780 std::time::SystemTime::now()
781 .duration_since(std::time::UNIX_EPOCH)
782 .map(|d| d.as_nanos())
783 .unwrap_or(0)
784 );
785 path.extend_from_slice(unique.as_bytes());
786 Ok(path)
787 }
788}
789
790pub fn io_tmpfile(state: &mut LuaState) -> Result<usize, LuaError> {
792 let hook = state.global().file_open_hook;
793 let Some(open_fn) = hook else {
794 let os_err = io::Error::new(
795 io::ErrorKind::Unsupported,
796 "no filesystem hook registered",
797 );
798 return file_result(state, false, None, os_err);
799 };
800
801 let temp_name_hook = state.global().temp_name_hook;
802 let path = match temp_name_hook {
803 Some(temp_fn) => match temp_fn() {
804 Ok(path) => path,
805 Err(e) => {
806 let msg = match &e {
807 LuaError::Runtime(LuaValue::Str(s)) => {
808 String::from_utf8_lossy(s.as_bytes()).into_owned()
809 }
810 other => format!("{:?}", other),
811 };
812 return file_result(
813 state,
814 false,
815 None,
816 io::Error::new(io::ErrorKind::Unsupported, msg),
817 );
818 }
819 },
820 None => match native_temp_name() {
821 Ok(path) => path,
822 Err(e) => return file_result(state, false, None, e),
823 },
824 };
825
826 match open_fn(&path, b"w+b") {
827 Ok(fh) => {
828 let cell = new_file(state)?;
829 cell.borrow_mut().file = Some(fh);
830 Ok(1)
831 }
832 Err(e) => {
833 let os_err = io::Error::new(
834 io::ErrorKind::Other,
835 match &e {
836 LuaError::Runtime(LuaValue::Str(s)) => {
837 String::from_utf8_lossy(s.as_bytes()).into_owned()
838 }
839 other => format!("{:?}", other),
840 },
841 );
842 file_result(state, false, None, os_err)
843 }
844 }
845}
846
847#[expect(dead_code, unreachable_code, unused_variables, reason = "io default-file helper: not yet wired; pending LStream-from-registry port")]
854fn get_io_file<'a>(
855 state: &'a mut LuaState,
856 key: &[u8],
857) -> Result<&'a mut dyn LuaFileHandle, LuaError> {
858 state.registry_get(key)?;
859 let label = &key[IO_PREFIX_LEN..]; let p: &mut LStream = todo!("TODO(port): extract LStream from registry userdata");
862 if p.is_closed() {
863 return Err(LuaError::runtime(format_args!(
864 "default {} file is closed",
865 label.escape_ascii()
866 )));
867 }
868 Ok(p.file.as_mut().expect("open stream has no file handle").as_mut())
869}
870
871fn g_iofile(state: &mut LuaState, key: &[u8], mode: &[u8]) -> Result<usize, LuaError> {
873 if !matches!(state.type_at(1), LuaType::None | LuaType::Nil) {
874 if state.type_at(1) == LuaType::String {
875 let filename = state.check_arg_string(1)?;
876 opencheck(state, &filename, mode)?;
877 } else {
878 let _ = tofile(state)?;
879 state.push_value_at(1)?;
880 }
881 state.registry_set(key)?;
882 }
883 state.registry_get(key)?;
884 Ok(1)
885}
886
887pub fn io_input(state: &mut LuaState) -> Result<usize, LuaError> {
889 g_iofile(state, IO_INPUT_KEY, b"r")
890}
891
892pub fn io_output(state: &mut LuaState) -> Result<usize, LuaError> {
894 g_iofile(state, IO_OUTPUT_KEY, b"w")
895}
896
897fn read_number_bytes(file: &mut dyn LuaFileHandle) -> Vec<u8> {
901 let first = loop {
902 let b = file.read_byte();
903 if b == EOF_SENTINEL || !(b as u8).is_ascii_whitespace() {
904 break b;
905 }
906 };
907
908 let mut rn = ReadNumState::new(first);
909
910 rn.try2(file, [b'-', b'+']);
911
912 let mut count: usize = 0;
913 let hex = if rn.try2(file, [b'0', b'0']) {
914 if rn.try2(file, [b'x', b'X']) {
915 true
916 } else {
917 count = 1;
918 false
919 }
920 } else {
921 false
922 };
923
924 count += rn.read_digits(file, hex);
925
926 let dec_point = b'.';
928 if rn.try2(file, [dec_point, b'.']) {
929 count += rn.read_digits(file, hex);
930 }
931
932 if count > 0 {
933 let exp_chars = if hex { [b'p', b'P'] } else { [b'e', b'E'] };
934 if rn.try2(file, exp_chars) {
935 rn.try2(file, [b'-', b'+']);
936 rn.read_digits(file, false);
937 }
938 }
939
940 file.unread_byte(rn.current);
941 rn.as_bytes().to_vec()
942}
943
944fn test_eof(file: &mut dyn LuaFileHandle) -> bool {
947 let c = file.read_byte();
948 if c != EOF_SENTINEL {
949 file.unread_byte(c);
950 }
951 c != EOF_SENTINEL
952}
953
954fn read_line(file: &mut dyn LuaFileHandle, chop: bool) -> (Vec<u8>, bool) {
960 let mut buf: Vec<u8> = Vec::new();
961 let mut c: i32;
962
963 'outer: loop {
968 for _ in 0..LUAL_BUFFER_SIZE {
969 c = file.read_byte();
970 if c == EOF_SENTINEL || c == b'\n' as i32 {
971 break 'outer;
972 }
973 buf.push(c as u8);
974 }
975 }
977
978 if !chop && c == b'\n' as i32 {
979 buf.push(b'\n');
980 }
981
982 let had_content = c == b'\n' as i32 || !buf.is_empty();
983 (buf, had_content)
984}
985
986fn read_all(file: &mut dyn LuaFileHandle) -> Vec<u8> {
992 let mut buf: Vec<u8> = Vec::new();
993 loop {
994 let mut chunk_read = 0usize;
995 for _ in 0..LUAL_BUFFER_SIZE {
996 let b = file.read_byte();
997 if b == EOF_SENTINEL {
998 break;
999 }
1000 buf.push(b as u8);
1001 chunk_read += 1;
1002 }
1003 if chunk_read < LUAL_BUFFER_SIZE {
1004 break;
1005 }
1006 }
1007 buf
1008}
1009
1010fn read_chars(file: &mut dyn LuaFileHandle, n: usize) -> (Vec<u8>, bool) {
1012 let mut buf = Vec::with_capacity(n);
1013 for _ in 0..n {
1014 let b = file.read_byte();
1015 if b == EOF_SENTINEL {
1016 break;
1017 }
1018 buf.push(b as u8);
1019 }
1020 let nr = buf.len();
1021 (buf, nr > 0)
1022}
1023
1024fn g_read(
1030 state: &mut LuaState,
1031 p_rc: &Rc<RefCell<LStream>>,
1032 first: i32,
1033) -> Result<usize, LuaError> {
1034 let nargs = (state.top() - first + 1).max(0);
1040 let mut n = first;
1041 let mut success = true;
1042
1043 {
1044 let mut p = p_rc.borrow_mut();
1045 let fh = p.file.as_mut().expect("open stream has no file handle");
1046 fh.clear_error();
1047 }
1048
1049 if nargs == 0 {
1050 let (bytes, had) = {
1051 let mut p = p_rc.borrow_mut();
1052 let fh = p.file.as_deref_mut().expect("open stream has no file handle");
1053 read_line(fh, true)
1054 };
1055 state.push_string(&bytes)?;
1056 success = had;
1057 n = first + 1;
1058 } else {
1059 state.ensure_stack((nargs as i32) + 20, "too many arguments")?;
1060 let mut remaining = nargs;
1061 while remaining > 0 && success {
1062 if state.type_at(n) == LuaType::Number {
1063 let l = state.check_arg_integer(n)? as usize;
1064 if l == 0 {
1065 let not_eof = {
1066 let mut p = p_rc.borrow_mut();
1067 let fh = p.file.as_deref_mut().expect("open stream has no file handle");
1068 test_eof(fh)
1069 };
1070 state.push_string(b"")?;
1071 success = not_eof;
1072 } else {
1073 let (bytes, had) = {
1074 let mut p = p_rc.borrow_mut();
1075 let fh = p.file.as_deref_mut().expect("open stream has no file handle");
1076 read_chars(fh, l)
1077 };
1078 state.push_string(&bytes)?;
1079 success = had;
1080 }
1081 } else {
1082 let s: Vec<u8> = state.check_arg_string(n)?;
1083 let pp: &[u8] = if s.first() == Some(&b'*') { &s[1..] } else { &s[..] };
1084 match pp.first() {
1085 Some(&b'n') => {
1086 let bytes = {
1087 let mut p = p_rc.borrow_mut();
1088 let fh = p.file.as_deref_mut().expect("open stream has no file handle");
1089 read_number_bytes(fh)
1090 };
1091 let pushed = state.string_to_number_push(&bytes)?;
1092 if pushed != 0 {
1093 success = true;
1094 } else {
1095 state.push(LuaValue::Nil);
1096 success = false;
1097 }
1098 }
1099 Some(&b'l') => {
1100 let (bytes, had) = {
1101 let mut p = p_rc.borrow_mut();
1102 let fh = p.file.as_deref_mut().expect("open stream has no file handle");
1103 read_line(fh, true)
1104 };
1105 state.push_string(&bytes)?;
1106 success = had;
1107 }
1108 Some(&b'L') => {
1109 let (bytes, had) = {
1110 let mut p = p_rc.borrow_mut();
1111 let fh = p.file.as_deref_mut().expect("open stream has no file handle");
1112 read_line(fh, false)
1113 };
1114 state.push_string(&bytes)?;
1115 success = had;
1116 }
1117 Some(&b'a') => {
1118 let bytes = {
1119 let mut p = p_rc.borrow_mut();
1120 let fh = p.file.as_deref_mut().expect("open stream has no file handle");
1121 read_all(fh)
1122 };
1123 state.push_string(&bytes)?;
1124 success = true;
1125 }
1126 _ => {
1127 return Err(LuaError::arg_error(n, "invalid format"));
1128 }
1129 }
1130 }
1131 n += 1;
1132 remaining -= 1;
1133 }
1134 }
1135
1136 let has_err = {
1137 let p = p_rc.borrow();
1138 match p.file.as_deref() {
1139 Some(fh) => fh.has_error(),
1140 None => false,
1141 }
1142 };
1143 if has_err {
1144 let err = {
1145 let p = p_rc.borrow();
1146 match p.file.as_deref().and_then(|fh| fh.last_error_info()) {
1147 Some((code, _msg)) if code != 0 => io::Error::from_raw_os_error(code),
1148 Some((_code, msg)) => io::Error::new(io::ErrorKind::Other, msg),
1149 None => io::Error::new(io::ErrorKind::Other, "file read error"),
1150 }
1151 };
1152 return file_result(
1153 state,
1154 false,
1155 None,
1156 err,
1157 );
1158 }
1159
1160 if !success {
1161 state.pop_n(1);
1162 state.push(LuaValue::Nil);
1163 }
1164
1165 Ok((n - first) as usize)
1166}
1167
1168fn get_io_file_rc(state: &mut LuaState, key: &[u8]) -> Result<Rc<RefCell<LStream>>, LuaError> {
1173 state.registry_get(key)?;
1174 let ud_id = state
1175 .test_arg_userdata(-1, LUA_FILE_HANDLE)
1176 .map(|ud| ud.identity());
1177 state.pop_n(1);
1178 let label = &key[IO_PREFIX_LEN..];
1179 let id = ud_id.ok_or_else(|| {
1180 LuaError::runtime(format_args!(
1181 "default {} file is invalid",
1182 label.escape_ascii()
1183 ))
1184 })?;
1185 let rc = lookup_lstream(id).ok_or_else(|| {
1186 LuaError::runtime(format_args!(
1187 "default {} file is invalid",
1188 label.escape_ascii()
1189 ))
1190 })?;
1191 if rc.borrow().is_closed() {
1192 return Err(LuaError::runtime(format_args!(
1193 "default {} file is closed",
1194 label.escape_ascii()
1195 )));
1196 }
1197 Ok(rc)
1198}
1199
1200pub fn io_read(state: &mut LuaState) -> Result<usize, LuaError> {
1202 let p_rc = get_io_file_rc(state, IO_INPUT_KEY)?;
1203 g_read(state, &p_rc, 1)
1204}
1205
1206pub fn f_read(state: &mut LuaState) -> Result<usize, LuaError> {
1208 let p_rc = tofile(state)?;
1209 g_read(state, &p_rc, 2)
1210}
1211
1212#[expect(dead_code, reason = "ported stdlib helper; not yet wired into the runtime")]
1218fn g_write(
1219 state: &mut LuaState,
1220 file: &mut dyn LuaFileHandle,
1221 arg: i32,
1222) -> Result<usize, LuaError> {
1223 let nargs = state.top() - arg;
1224 let mut overall_ok = true;
1225
1226 for i in 0..nargs {
1227 let idx = arg + i;
1228 if state.type_at(idx) == LuaType::Number {
1229 let s = if state.is_integer(idx) {
1232 let ival = state.to_integer(idx).unwrap_or(0);
1233 format!("{}", ival)
1234 } else {
1235 let fval = state.to_number(idx).unwrap_or(0.0);
1236 format!("{:.14e}", fval)
1238 };
1239 match file.write_bytes(s.as_bytes()) {
1240 Ok(n) => overall_ok = overall_ok && n == s.len(),
1241 Err(_) => overall_ok = false,
1242 }
1243 } else {
1244 let s: Vec<u8> = state.check_arg_string(idx)?;
1245 match file.write_bytes(&s) {
1246 Ok(n) => overall_ok = overall_ok && n == s.len(),
1247 Err(_) => overall_ok = false,
1248 }
1249 }
1250 }
1251
1252 if overall_ok {
1253 Ok(1) } else {
1255 file_result(
1256 state,
1257 false,
1258 None,
1259 io::Error::new(io::ErrorKind::Other, "write error"),
1260 )
1261 }
1262}
1263
1264pub fn io_write(state: &mut LuaState) -> Result<usize, LuaError> {
1274 let n = state.top();
1276 let mut chunks: Vec<Vec<u8>> = Vec::with_capacity(n as usize);
1277 for i in 1..=(n as i32) {
1278 if state.type_at(i) == LuaType::Number {
1279 let s = if state.is_integer(i) {
1280 let ival = state.to_integer(i).unwrap_or(0);
1281 format!("{}", ival).into_bytes()
1282 } else {
1283 let fval = state.to_number(i).unwrap_or(0.0);
1284 format!("{:.14e}", fval).into_bytes()
1286 };
1287 chunks.push(s);
1288 } else {
1289 let bytes: Vec<u8> = state.check_arg_string(i)?;
1290 chunks.push(bytes);
1291 }
1292 }
1293
1294 let p_rc = get_io_file_rc(state, IO_OUTPUT_KEY)?;
1297 {
1298 let mut p = p_rc.borrow_mut();
1299 let fh = p.file.as_mut().expect("open stream has no file handle");
1300 for chunk in &chunks {
1301 fh.write_bytes(chunk).map_err(|e| {
1302 LuaError::runtime(format_args!("io.write: {}", e))
1303 })?;
1304 }
1305 }
1306 state.registry_get(IO_OUTPUT_KEY)?;
1307 Ok(1)
1308}
1309
1310pub fn f_write(state: &mut LuaState) -> Result<usize, LuaError> {
1312 let p_rc = tofile(state)?;
1313
1314 let n = state.top();
1316 let mut chunks: Vec<Vec<u8>> = Vec::with_capacity(n.saturating_sub(1) as usize);
1317 for i in 2..=(n as i32) {
1318 if state.type_at(i) == LuaType::Number {
1319 let s = if state.is_integer(i) {
1320 let ival = state.to_integer(i).unwrap_or(0);
1321 format!("{}", ival).into_bytes()
1322 } else {
1323 let fval = state.to_number(i).unwrap_or(0.0);
1324 format!("{:.14e}", fval).into_bytes()
1326 };
1327 chunks.push(s);
1328 } else {
1329 let bytes: Vec<u8> = state.check_arg_string(i)?;
1330 chunks.push(bytes);
1331 }
1332 }
1333
1334 let result: io::Result<()> = {
1336 let mut p = p_rc.borrow_mut();
1337 let fh = p.file.as_mut().expect("open stream has no file handle");
1338 let mut r: io::Result<()> = Ok(());
1339 for chunk in &chunks {
1340 match fh.write_bytes(chunk) {
1341 Ok(written) if written == chunk.len() => {}
1342 Ok(_) => {
1343 r = Err(io::Error::new(io::ErrorKind::Other, "short write"));
1344 break;
1345 }
1346 Err(e) => {
1347 r = Err(e);
1348 break;
1349 }
1350 }
1351 }
1352 r
1353 };
1354
1355 match result {
1357 Ok(()) => {
1358 state.push_value_at(1)?;
1359 Ok(1)
1360 }
1361 Err(e) => file_result(state, false, None, e),
1362 }
1363}
1364
1365pub fn f_seek(state: &mut LuaState) -> Result<usize, LuaError> {
1369 static MODE_NAMES: &[&[u8]] = &[b"set", b"cur", b"end"];
1370
1371 let p_rc = tofile(state)?;
1372 let op = state.check_arg_option(2, Some(b"cur"), MODE_NAMES)?;
1373 let p3: i64 = state.opt_arg_integer(3, 0)?;
1374
1375 let seek_pos = match op {
1376 0 => SeekFrom::Start(p3 as u64),
1377 1 => SeekFrom::Current(p3),
1378 2 => SeekFrom::End(p3),
1379 _ => unreachable!(),
1380 };
1381
1382 let result = {
1383 let mut p = p_rc.borrow_mut();
1384 let fh = p.file.as_mut().expect("open stream has no file handle");
1385 fh.seek(seek_pos)
1386 };
1387 match result {
1388 Ok(pos) => {
1389 state.push(LuaValue::Int(pos as i64));
1390 Ok(1)
1391 }
1392 Err(e) => file_result(state, false, None, e),
1393 }
1394}
1395
1396pub fn f_setvbuf(state: &mut LuaState) -> Result<usize, LuaError> {
1398 static MODE_NAMES: &[&[u8]] = &[b"no", b"full", b"line"];
1399
1400 let p_rc = tofile(state)?;
1401 let op = state.check_arg_option(2, None, MODE_NAMES)?;
1402 let sz: i64 = state.opt_arg_integer(3, LUAL_BUFFER_SIZE as i64)?;
1403 let mode = match op {
1404 0 => BufMode::No,
1405 1 => BufMode::Full,
1406 2 => BufMode::Line,
1407 _ => unreachable!(),
1408 };
1409 let result = {
1410 let mut p = p_rc.borrow_mut();
1411 let fh = p.file.as_mut().expect("open stream has no file handle");
1412 let mode_index = match mode {
1413 BufMode::No => 0,
1414 BufMode::Full => 1,
1415 BufMode::Line => 2,
1416 };
1417 fh.set_buf_mode(mode_index, sz.max(0) as usize)
1418 };
1419 match result {
1420 Ok(()) => file_result(state, true, None, io::Error::last_os_error()),
1421 Err(e) => file_result(state, false, None, e),
1422 }
1423}
1424
1425pub fn io_flush(state: &mut LuaState) -> Result<usize, LuaError> {
1427 let ud_id: Option<usize> = {
1428 state.registry_get(IO_OUTPUT_KEY)?;
1429 let id = state
1430 .test_arg_userdata(-1, LUA_FILE_HANDLE)
1431 .map(|ud| ud.identity());
1432 state.pop_n(1);
1433 id
1434 };
1435 if let Some(id) = ud_id {
1436 if let Some(rc) = lookup_lstream(id) {
1437 let result = {
1438 let mut p = rc.borrow_mut();
1439 if p.is_closed() {
1440 return Err(LuaError::runtime(format_args!(
1441 "default output file is closed"
1442 )));
1443 }
1444 let fh = p.file.as_deref_mut().expect("open stream has no file handle");
1445 fh.flush()
1446 };
1447 return match result {
1448 Ok(()) => {
1449 state.push(LuaValue::Bool(true));
1450 Ok(1)
1451 }
1452 Err(e) => file_result(state, false, None, e),
1453 };
1454 }
1455 }
1456 state.push(LuaValue::Bool(true));
1458 Ok(1)
1459}
1460
1461pub fn f_flush(state: &mut LuaState) -> Result<usize, LuaError> {
1463 let p_rc = tofile(state)?;
1464 let result = {
1465 let mut p = p_rc.borrow_mut();
1466 let fh = p.file.as_mut().expect("open stream has no file handle");
1467 fh.flush()
1468 };
1469 match result {
1470 Ok(()) => {
1471 state.push(LuaValue::Bool(true));
1472 Ok(1)
1473 }
1474 Err(e) => file_result(state, false, None, e),
1475 }
1476}
1477
1478fn aux_lines(state: &mut LuaState, toclose: bool) -> Result<(), LuaError> {
1488 let n = state.top() - 1;
1491 if n > MAX_ARG_LINE as i32 {
1492 return Err(LuaError::arg_error(
1493 MAX_ARG_LINE as i32 + 2,
1494 "too many arguments",
1495 ));
1496 }
1497 state.push_value_at(1)?;
1498 state.push(LuaValue::Int(n as i64));
1499 state.push(LuaValue::Bool(toclose));
1500 state.rotate(2, 3)?;
1501 state.push_c_closure(io_readline, (3 + n) as i32)?;
1502 Ok(())
1503}
1504
1505pub fn f_lines(state: &mut LuaState) -> Result<usize, LuaError> {
1507 let _ = tofile(state)?; aux_lines(state, false)?;
1509 Ok(1)
1510}
1511
1512pub fn io_lines(state: &mut LuaState) -> Result<usize, LuaError> {
1514 if state.type_at(1) == LuaType::None {
1515 state.push(LuaValue::Nil);
1516 }
1517 let toclose = if state.type_at(1) == LuaType::Nil {
1518 state.registry_get(IO_INPUT_KEY)?;
1519 state.replace(1)?;
1520 let _ = tofile(state)?;
1521 false
1522 } else {
1523 let filename = state.check_arg_string(1)?;
1524 opencheck(state, &filename, b"r")?;
1525 state.replace(1)?;
1526 true
1527 };
1528
1529 aux_lines(state, toclose)?;
1530
1531 if toclose {
1532 state.push(LuaValue::Nil); state.push(LuaValue::Nil); state.push_value_at(1)?; Ok(4)
1536 } else {
1537 Ok(1)
1538 }
1539}
1540
1541fn io_readline(state: &mut LuaState) -> Result<usize, LuaError> {
1549 let n = match state.value_at(crate::state_stub::upvalue_index(2)) {
1550 LuaValue::Int(i) => i as usize,
1551 _ => 0,
1552 };
1553
1554 let p_rc = lstream_from_upvalue(state, 1)?;
1555
1556 if p_rc.borrow().is_closed() {
1557 return Err(LuaError::runtime(format_args!("file is already closed")));
1558 }
1559
1560 lua_vm::api::set_top(state, 1)?;
1561 state.ensure_stack(n as i32, "too many arguments")?;
1562
1563 for i in 1..=n {
1564 let uv = state.value_at(crate::state_stub::upvalue_index(3 + i as i32));
1565 state.push(uv);
1566 }
1567
1568 let result_n: usize = g_read(state, &p_rc, 2)?;
1569
1570 debug_assert!(result_n > 0, "g_read should return at least one value");
1571
1572 let top = state.top_idx().get() as i32;
1573 let first_result_idx = top - result_n as i32;
1574 let first_truthy = !matches!(
1575 state.stack_at(first_result_idx),
1576 LuaValue::Nil | LuaValue::Bool(false)
1577 );
1578 if first_truthy {
1579 return Ok(result_n);
1580 }
1581
1582 if result_n > 1 {
1583 let err_val = state.stack_at(first_result_idx + 1).clone();
1584 return Err(LuaError::from_value(err_val));
1585 }
1586
1587 let toclose = !matches!(
1588 state.value_at(crate::state_stub::upvalue_index(3)),
1589 LuaValue::Nil | LuaValue::Bool(false)
1590 );
1591 if toclose {
1592 lua_vm::api::set_top(state, 0)?;
1593 state.push_upvalue(1)?;
1594 aux_close(state)?;
1595 }
1596
1597 Ok(0)
1598}
1599
1600fn create_meta(state: &mut LuaState) -> Result<(), LuaError> {
1604 state.new_metatable(LUA_FILE_HANDLE)?;
1605 state.set_funcs(FILE_METAMETHODS, 0)?;
1606 state.new_lib_table(FILE_METHODS)?;
1607 state.set_funcs(FILE_METHODS, 0)?;
1608 state.set_field(-2, b"__index")?;
1609 state.pop_n(1);
1610 Ok(())
1611}
1612
1613fn create_std_file(
1615 state: &mut LuaState,
1616 std_kind: StdFileKind,
1617 registry_key: Option<&[u8]>,
1618 field_name: &[u8],
1619) -> Result<(), LuaError> {
1620 let cell = new_pre_file(state)?;
1621 let output_hook = match std_kind {
1622 StdFileKind::Stdout => state.global().stdout_hook,
1623 StdFileKind::Stderr => state.global().stderr_hook,
1624 StdFileKind::Stdin => None,
1625 };
1626 let input_hook = match std_kind {
1627 StdFileKind::Stdin => state.global().stdin_hook,
1628 StdFileKind::Stdout | StdFileKind::Stderr => None,
1629 };
1630 {
1631 let mut p = cell.borrow_mut();
1632 p.file = Some(Box::new(StdStreamHandle::new(
1633 std_kind,
1634 input_hook,
1635 output_hook,
1636 )));
1637 p.close_fn = Some(io_noclose);
1638 }
1639 if let Some(key) = registry_key {
1640 state.push_value_at(-1)?;
1641 state.registry_set(key)?;
1642 }
1643 state.set_field(-2, field_name)?;
1644 Ok(())
1645}
1646
1647pub fn luaopen_io(state: &mut LuaState) -> Result<usize, LuaError> {
1649 state.new_lib(IO_LIB)?;
1650 create_meta(state)?;
1651 create_std_file(state, StdFileKind::Stdin, Some(IO_INPUT_KEY), b"stdin")?;
1652 create_std_file(state, StdFileKind::Stdout, Some(IO_OUTPUT_KEY), b"stdout")?;
1653 create_std_file(state, StdFileKind::Stderr, None, b"stderr")?;
1654 Ok(1)
1655}
1656
1657