1use std::cell::RefCell;
21use std::collections::HashMap;
22use std::io::{self, SeekFrom};
23use std::rc::Rc;
24
25use crate::state_stub::{LuaState, LuaStateStubExt as _};
26use lua_types::{LuaError, LuaFileHandle, LuaType, LuaValue};
27use lua_vm::state::{InputHook, OutputHook};
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 {
254 false
255 }
256}
257
258impl LuaFileOps for StdStreamHandle {
259 fn set_buf_mode(&mut self, _mode: BufMode, _size: usize) -> io::Result<()> {
260 Ok(())
261 }
262}
263
264impl StdStreamHandle {
265 fn new(
266 kind: StdFileKind,
267 input_hook: Option<InputHook>,
268 output_hook: Option<OutputHook>,
269 ) -> Self {
270 StdStreamHandle {
271 kind,
272 input_hook,
273 output_hook,
274 unread: None,
275 }
276 }
277}
278
279struct ReadNumState {
281 current: i32,
283 count: usize,
285 buf: [u8; L_MAX_LEN_NUM + 1],
287}
288
289impl ReadNumState {
290 fn new(first_byte: i32) -> Self {
291 ReadNumState {
292 current: first_byte,
293 count: 0,
294 buf: [0u8; L_MAX_LEN_NUM + 1],
295 }
296 }
297
298 fn advance(&mut self, file: &mut dyn LuaFileHandle) -> bool {
301 if self.count >= L_MAX_LEN_NUM {
302 self.buf[0] = 0;
303 return false;
304 }
305 self.buf[self.count] = self.current as u8;
306 self.count += 1;
307 self.current = file.read_byte();
308 true
309 }
310
311 fn try2(&mut self, file: &mut dyn LuaFileHandle, set: [u8; 2]) -> bool {
313 if self.current == set[0] as i32 || self.current == set[1] as i32 {
314 self.advance(file)
315 } else {
316 false
317 }
318 }
319
320 fn read_digits(&mut self, file: &mut dyn LuaFileHandle, hex: bool) -> usize {
322 let mut count = 0usize;
323 loop {
324 let is_digit = if hex {
325 (self.current as u8).is_ascii_hexdigit()
326 } else {
327 (self.current as u8).is_ascii_digit()
328 };
329 if !is_digit || self.current == EOF_SENTINEL {
330 break;
331 }
332 if !self.advance(file) {
333 break;
334 }
335 count += 1;
336 }
337 count
338 }
339
340 fn as_bytes(&self) -> &[u8] {
342 &self.buf[..self.count]
343 }
344}
345
346pub const IO_LIB: &[(&[u8], fn(&mut LuaState) -> Result<usize, LuaError>)] = &[
350 (b"close", io_close),
351 (b"flush", io_flush),
352 (b"input", io_input),
353 (b"lines", io_lines),
354 (b"open", io_open),
355 (b"output", io_output),
356 (b"popen", io_popen),
357 (b"read", io_read),
358 (b"tmpfile", io_tmpfile),
359 (b"type", io_type),
360 (b"write", io_write),
361];
362
363pub const FILE_METHODS: &[(&[u8], fn(&mut LuaState) -> Result<usize, LuaError>)] = &[
365 (b"read", f_read),
366 (b"write", f_write),
367 (b"lines", f_lines),
368 (b"flush", f_flush),
369 (b"seek", f_seek),
370 (b"close", f_close),
371 (b"setvbuf", f_setvbuf),
372];
373
374pub const FILE_METAMETHODS: &[(&[u8], fn(&mut LuaState) -> Result<usize, LuaError>)] = &[
376 (b"__gc", f_gc),
377 (b"__close", f_gc),
378 (b"__tostring", f_tostring),
379];
380
381fn check_mode(mode: &[u8]) -> bool {
387 if mode.is_empty() {
388 return false;
389 }
390 let mut idx = 0usize;
391 if !matches!(mode[idx], b'r' | b'w' | b'a') {
392 return false;
393 }
394 idx += 1;
395 if idx < mode.len() && mode[idx] == b'+' {
396 idx += 1;
397 }
398 mode[idx..].iter().all(|&b| b == b'b')
399}
400
401fn check_mode_popen(mode: &[u8]) -> bool {
403 matches!(mode, b"r" | b"w")
404}
405
406fn file_result(
410 state: &mut LuaState,
411 success: bool,
412 fname: Option<&[u8]>,
413 os_err: io::Error,
414) -> Result<usize, LuaError> {
415 if success {
416 state.push(LuaValue::Bool(true));
417 return Ok(1);
418 }
419 state.push(LuaValue::Bool(false));
420 let msg = os_err.to_string();
421 match fname {
422 Some(name) => {
423 let mut s = Vec::with_capacity(name.len() + 2 + msg.len());
424 s.extend_from_slice(name);
425 s.extend_from_slice(b": ");
426 s.extend_from_slice(msg.as_bytes());
427 state.push_string(&s)?;
428 }
429 None => {
430 state.push_string(msg.as_bytes())?;
431 }
432 }
433 let errno_code = os_err.raw_os_error().unwrap_or(0) as i64;
434 state.push(LuaValue::Int(errno_code));
435 Ok(3)
436}
437
438fn exec_result(state: &mut LuaState, stat: i32) -> Result<usize, LuaError> {
445 if stat == 0 {
446 state.push(LuaValue::Bool(true));
447 Ok(1)
448 } else {
449 state.push(LuaValue::Bool(false));
450 state.push_string(b"exit")?;
452 state.push(LuaValue::Int(stat as i64));
453 Ok(3)
454 }
455}
456
457fn get_lstream(state: &mut LuaState) -> Result<Rc<RefCell<LStream>>, LuaError> {
464 let ud = state.check_arg_userdata(1, LUA_FILE_HANDLE)?;
465 lookup_lstream(ud.identity())
466 .ok_or_else(|| LuaError::runtime(format_args!("invalid file handle")))
467}
468
469fn lstream_from_upvalue(state: &mut LuaState, idx: i32) -> 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)
487 .ok_or_else(|| LuaError::runtime(format_args!("invalid file handle in upvalue {}", idx)))
488}
489
490fn tofile(state: &mut LuaState) -> Result<Rc<RefCell<LStream>>, LuaError> {
492 let p_rc = get_lstream(state)?;
493 {
494 let p = p_rc.borrow();
495 if p.is_closed() {
496 return Err(LuaError::runtime(format_args!(
497 "attempt to use a closed file"
498 )));
499 }
500 debug_assert!(p.file.is_some());
501 }
502 Ok(p_rc)
503}
504
505fn new_pre_file(state: &mut LuaState) -> Result<Rc<RefCell<LStream>>, LuaError> {
512 let ud = state.new_userdata_typed(LUA_FILE_HANDLE, std::mem::size_of::<LStream>(), 0)?;
513 state.set_metatable_by_name(LUA_FILE_HANDLE)?;
514 let cell = register_lstream(
515 ud.identity(),
516 LStream {
517 file: None,
518 close_fn: None,
519 },
520 );
521 Ok(cell)
522}
523
524fn new_file(state: &mut LuaState) -> Result<Rc<RefCell<LStream>>, LuaError> {
526 let cell = new_pre_file(state)?;
527 cell.borrow_mut().close_fn = Some(io_fclose);
528 Ok(cell)
529}
530
531fn opencheck(state: &mut LuaState, fname: &[u8], mode: &[u8]) -> Result<(), LuaError> {
536 let hook = state.global().file_open_hook;
537 let fh = match hook {
538 Some(open_fn) => open_fn(fname, mode).map_err(|e| {
539 LuaError::runtime(format_args!(
540 "cannot open file '{}' ({})",
541 fname.escape_ascii(),
542 match &e {
543 LuaError::Runtime(LuaValue::Str(s)) => {
544 String::from_utf8_lossy(s.as_bytes()).into_owned()
545 }
546 other => format!("{:?}", other),
547 }
548 ))
549 })?,
550 None => {
551 return Err(LuaError::runtime(format_args!(
552 "cannot open file '{}' (no filesystem hook registered)",
553 fname.escape_ascii()
554 )));
555 }
556 };
557 let cell = new_file(state)?;
558 cell.borrow_mut().file = Some(fh);
559 Ok(())
560}
561
562fn io_fclose(state: &mut LuaState) -> Result<usize, LuaError> {
568 let p_rc = get_lstream(state)?;
569 let _closed = p_rc.borrow_mut().file.take();
571 state.push(LuaValue::Bool(true));
572 Ok(1)
573}
574
575fn io_pclose(state: &mut LuaState) -> Result<usize, LuaError> {
579 let p_rc = get_lstream(state)?;
580 let _closed = p_rc.borrow_mut().file.take();
581 exec_result(state, 0)
583}
584
585fn io_noclose(state: &mut LuaState) -> Result<usize, LuaError> {
587 let p_rc = get_lstream(state)?;
588 p_rc.borrow_mut().close_fn = Some(io_noclose); state.push(LuaValue::Bool(false));
590 state.push_string(b"cannot close standard file")?;
591 Ok(2)
592}
593
594fn aux_close(state: &mut LuaState) -> Result<usize, LuaError> {
596 let p_rc = get_lstream(state)?;
597 let cf = p_rc.borrow_mut().close_fn.take().ok_or_else(|| {
598 LuaError::runtime(format_args!("attempt to close an already-closed file"))
599 })?;
600 cf(state)
601}
602
603pub fn io_type(state: &mut LuaState) -> Result<usize, LuaError> {
607 state.check_arg_any(1)?;
608 let maybe_userdata = state.test_arg_userdata(1, LUA_FILE_HANDLE);
609 match maybe_userdata {
610 None => {
611 state.push(LuaValue::Bool(false));
612 }
613 Some(ud) => {
614 let is_closed = match lookup_lstream(ud.identity()) {
615 Some(rc) => rc.borrow().is_closed(),
616 None => true, };
618 if is_closed {
619 state.push_string(b"closed file")?;
620 } else {
621 state.push_string(b"file")?;
622 }
623 }
624 }
625 Ok(1)
626}
627
628fn f_tostring(state: &mut LuaState) -> Result<usize, LuaError> {
632 let p_rc = get_lstream(state)?;
633 let closed = p_rc.borrow().is_closed();
634 if closed {
635 state.push_string(b"file (closed)")?;
636 } else {
637 state.push_string(b"file (0x?)")?;
639 }
640 Ok(1)
641}
642
643fn f_close(state: &mut LuaState) -> Result<usize, LuaError> {
647 let _ = tofile(state)?; aux_close(state)
649}
650
651pub fn io_close(state: &mut LuaState) -> Result<usize, LuaError> {
653 if state.type_at(1) == LuaType::None {
657 state.registry_get(IO_OUTPUT_KEY)?;
658 }
659 f_close(state)
660}
661
662fn f_gc(state: &mut LuaState) -> Result<usize, LuaError> {
664 let p_rc = get_lstream(state)?;
665 let needs_close = {
666 let p = p_rc.borrow();
667 !p.is_closed() && p.file.is_some()
668 };
669 if needs_close {
670 let _ = aux_close(state);
672 }
673 Ok(0)
674}
675
676pub fn io_open(state: &mut LuaState) -> Result<usize, LuaError> {
683 let filename: Vec<u8> = state.check_arg_string(1)?;
684 let mode: Vec<u8> = state.opt_arg_string(2, b"r")?;
685 if !check_mode(&mode) {
686 return Err(lua_vm::debug::arg_error_impl(state, 2, b"invalid mode"));
687 }
688 let hook = state.global().file_open_hook;
689 match hook {
690 Some(open_fn) => match open_fn(&filename, &mode) {
691 Ok(fh) => {
692 let cell = new_file(state)?;
693 cell.borrow_mut().file = Some(fh);
694 Ok(1)
695 }
696 Err(e) => {
697 let os_err = io::Error::new(
698 io::ErrorKind::Other,
699 match &e {
700 LuaError::Runtime(LuaValue::Str(s)) => {
701 String::from_utf8_lossy(s.as_bytes()).into_owned()
702 }
703 other => format!("{:?}", other),
704 },
705 );
706 file_result(state, false, Some(&filename), os_err)
707 }
708 },
709 None => {
710 let os_err =
711 io::Error::new(io::ErrorKind::Unsupported, "no filesystem hook registered");
712 file_result(state, false, Some(&filename), os_err)
713 }
714 }
715}
716
717pub fn io_popen(state: &mut LuaState) -> Result<usize, LuaError> {
726 let filename: Vec<u8> = state.check_arg_string(1)?;
727 let mode: Vec<u8> = state.opt_arg_string(2, b"r")?;
728 if !check_mode_popen(&mode) {
729 return Err(lua_vm::debug::arg_error_impl(state, 2, b"invalid mode"));
730 }
731 let hook = state.global().popen_hook;
732 match hook {
733 Some(spawn_fn) => match spawn_fn(&filename, &mode) {
734 Ok(fh) => {
735 let cell = new_pre_file(state)?;
736 let mut p = cell.borrow_mut();
737 p.file = Some(fh);
738 p.close_fn = Some(io_pclose);
739 drop(p);
740 Ok(1)
741 }
742 Err(e) => {
743 let os_err = io::Error::new(
744 io::ErrorKind::Other,
745 match &e {
746 LuaError::Runtime(LuaValue::Str(s)) => {
747 String::from_utf8_lossy(s.as_bytes()).into_owned()
748 }
749 other => format!("{:?}", other),
750 },
751 );
752 file_result(state, false, Some(&filename), os_err)
753 }
754 },
755 None => {
756 let os_err = io::Error::new(
757 io::ErrorKind::Unsupported,
758 "popen not enabled in this build",
759 );
760 file_result(state, false, Some(&filename), os_err)
761 }
762 }
763}
764
765fn native_temp_name() -> io::Result<Vec<u8>> {
766 #[cfg(all(target_arch = "wasm32", target_os = "unknown"))]
767 {
768 return Err(io::Error::new(
769 io::ErrorKind::Unsupported,
770 "temporary files not available in this host",
771 ));
772 }
773
774 #[cfg(not(all(target_arch = "wasm32", target_os = "unknown")))]
775 {
776 let mut path = std::env::temp_dir().to_string_lossy().as_bytes().to_vec();
777 if path.last().copied() != Some(b'/') && path.last().copied() != Some(b'\\') {
778 path.push(b'/');
779 }
780 let unique = format!(
781 "lua_tmpfile_{}_{}",
782 std::process::id(),
783 std::time::SystemTime::now()
784 .duration_since(std::time::UNIX_EPOCH)
785 .map(|d| d.as_nanos())
786 .unwrap_or(0)
787 );
788 path.extend_from_slice(unique.as_bytes());
789 Ok(path)
790 }
791}
792
793pub fn io_tmpfile(state: &mut LuaState) -> Result<usize, LuaError> {
795 let hook = state.global().file_open_hook;
796 let Some(open_fn) = hook else {
797 let os_err = io::Error::new(io::ErrorKind::Unsupported, "no filesystem hook registered");
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(
854 dead_code,
855 unreachable_code,
856 unused_variables,
857 reason = "io default-file helper: not yet wired; pending LStream-from-registry port"
858)]
859fn get_io_file<'a>(
860 state: &'a mut LuaState,
861 key: &[u8],
862) -> Result<&'a mut dyn LuaFileHandle, LuaError> {
863 state.registry_get(key)?;
864 let label = &key[IO_PREFIX_LEN..]; let p: &mut LStream = todo!("TODO(port): extract LStream from registry userdata");
867 if p.is_closed() {
868 return Err(LuaError::runtime(format_args!(
869 "default {} file is closed",
870 label.escape_ascii()
871 )));
872 }
873 Ok(p.file
874 .as_mut()
875 .expect("open stream has no file handle")
876 .as_mut())
877}
878
879fn g_iofile(state: &mut LuaState, key: &[u8], mode: &[u8]) -> Result<usize, LuaError> {
881 if !matches!(state.type_at(1), LuaType::None | LuaType::Nil) {
882 if state.type_at(1) == LuaType::String {
883 let filename = state.check_arg_string(1)?;
884 opencheck(state, &filename, mode)?;
885 } else {
886 let _ = tofile(state)?;
887 state.push_value_at(1)?;
888 }
889 state.registry_set(key)?;
890 }
891 state.registry_get(key)?;
892 Ok(1)
893}
894
895pub fn io_input(state: &mut LuaState) -> Result<usize, LuaError> {
897 g_iofile(state, IO_INPUT_KEY, b"r")
898}
899
900pub fn io_output(state: &mut LuaState) -> Result<usize, LuaError> {
902 g_iofile(state, IO_OUTPUT_KEY, b"w")
903}
904
905fn read_number_bytes(file: &mut dyn LuaFileHandle) -> Vec<u8> {
909 let first = loop {
910 let b = file.read_byte();
911 if b == EOF_SENTINEL || !(b as u8).is_ascii_whitespace() {
912 break b;
913 }
914 };
915
916 let mut rn = ReadNumState::new(first);
917
918 rn.try2(file, [b'-', b'+']);
919
920 let mut count: usize = 0;
921 let hex = if rn.try2(file, [b'0', b'0']) {
922 if rn.try2(file, [b'x', b'X']) {
923 true
924 } else {
925 count = 1;
926 false
927 }
928 } else {
929 false
930 };
931
932 count += rn.read_digits(file, hex);
933
934 let dec_point = b'.';
936 if rn.try2(file, [dec_point, b'.']) {
937 count += rn.read_digits(file, hex);
938 }
939
940 if count > 0 {
941 let exp_chars = if hex { [b'p', b'P'] } else { [b'e', b'E'] };
942 if rn.try2(file, exp_chars) {
943 rn.try2(file, [b'-', b'+']);
944 rn.read_digits(file, false);
945 }
946 }
947
948 file.unread_byte(rn.current);
949 rn.as_bytes().to_vec()
950}
951
952fn test_eof(file: &mut dyn LuaFileHandle) -> bool {
955 let c = file.read_byte();
956 if c != EOF_SENTINEL {
957 file.unread_byte(c);
958 }
959 c != EOF_SENTINEL
960}
961
962fn read_line(file: &mut dyn LuaFileHandle, chop: bool) -> (Vec<u8>, bool) {
968 let mut buf: Vec<u8> = Vec::new();
969 let mut c: i32;
970
971 'outer: loop {
976 for _ in 0..LUAL_BUFFER_SIZE {
977 c = file.read_byte();
978 if c == EOF_SENTINEL || c == b'\n' as i32 {
979 break 'outer;
980 }
981 buf.push(c as u8);
982 }
983 }
985
986 if !chop && c == b'\n' as i32 {
987 buf.push(b'\n');
988 }
989
990 let had_content = c == b'\n' as i32 || !buf.is_empty();
991 (buf, had_content)
992}
993
994fn read_all(file: &mut dyn LuaFileHandle) -> Vec<u8> {
1000 let mut buf: Vec<u8> = Vec::new();
1001 loop {
1002 let mut chunk_read = 0usize;
1003 for _ in 0..LUAL_BUFFER_SIZE {
1004 let b = file.read_byte();
1005 if b == EOF_SENTINEL {
1006 break;
1007 }
1008 buf.push(b as u8);
1009 chunk_read += 1;
1010 }
1011 if chunk_read < LUAL_BUFFER_SIZE {
1012 break;
1013 }
1014 }
1015 buf
1016}
1017
1018fn read_chars(file: &mut dyn LuaFileHandle, n: usize) -> (Vec<u8>, bool) {
1020 let mut buf = Vec::with_capacity(n);
1021 for _ in 0..n {
1022 let b = file.read_byte();
1023 if b == EOF_SENTINEL {
1024 break;
1025 }
1026 buf.push(b as u8);
1027 }
1028 let nr = buf.len();
1029 (buf, nr > 0)
1030}
1031
1032fn g_read(
1038 state: &mut LuaState,
1039 p_rc: &Rc<RefCell<LStream>>,
1040 first: i32,
1041) -> Result<usize, LuaError> {
1042 let nargs = (state.top() - first + 1).max(0);
1048 let mut n = first;
1049 let mut success = true;
1050
1051 {
1052 let mut p = p_rc.borrow_mut();
1053 let fh = p.file.as_mut().expect("open stream has no file handle");
1054 fh.clear_error();
1055 }
1056
1057 if nargs == 0 {
1058 let (bytes, had) = {
1059 let mut p = p_rc.borrow_mut();
1060 let fh = p
1061 .file
1062 .as_deref_mut()
1063 .expect("open stream has no file handle");
1064 read_line(fh, true)
1065 };
1066 state.push_string(&bytes)?;
1067 success = had;
1068 n = first + 1;
1069 } else {
1070 state.ensure_stack((nargs as i32) + 20, "too many arguments")?;
1071 let mut remaining = nargs;
1072 while remaining > 0 && success {
1073 if state.type_at(n) == LuaType::Number {
1074 let l = state.check_arg_integer(n)? as usize;
1075 if l == 0 {
1076 let not_eof = {
1077 let mut p = p_rc.borrow_mut();
1078 let fh = p
1079 .file
1080 .as_deref_mut()
1081 .expect("open stream has no file handle");
1082 test_eof(fh)
1083 };
1084 state.push_string(b"")?;
1085 success = not_eof;
1086 } else {
1087 let (bytes, had) = {
1088 let mut p = p_rc.borrow_mut();
1089 let fh = p
1090 .file
1091 .as_deref_mut()
1092 .expect("open stream has no file handle");
1093 read_chars(fh, l)
1094 };
1095 state.push_string(&bytes)?;
1096 success = had;
1097 }
1098 } else {
1099 let s: Vec<u8> = state.check_arg_string(n)?;
1100 let pp: &[u8] = if s.first() == Some(&b'*') {
1101 &s[1..]
1102 } else {
1103 &s[..]
1104 };
1105 match pp.first() {
1106 Some(&b'n') => {
1107 let bytes = {
1108 let mut p = p_rc.borrow_mut();
1109 let fh = p
1110 .file
1111 .as_deref_mut()
1112 .expect("open stream has no file handle");
1113 read_number_bytes(fh)
1114 };
1115 let pushed = state.string_to_number_push(&bytes)?;
1116 if pushed != 0 {
1117 success = true;
1118 } else {
1119 state.push(LuaValue::Nil);
1120 success = false;
1121 }
1122 }
1123 Some(&b'l') => {
1124 let (bytes, had) = {
1125 let mut p = p_rc.borrow_mut();
1126 let fh = p
1127 .file
1128 .as_deref_mut()
1129 .expect("open stream has no file handle");
1130 read_line(fh, true)
1131 };
1132 state.push_string(&bytes)?;
1133 success = had;
1134 }
1135 Some(&b'L') => {
1136 let (bytes, had) = {
1137 let mut p = p_rc.borrow_mut();
1138 let fh = p
1139 .file
1140 .as_deref_mut()
1141 .expect("open stream has no file handle");
1142 read_line(fh, false)
1143 };
1144 state.push_string(&bytes)?;
1145 success = had;
1146 }
1147 Some(&b'a') => {
1148 let bytes = {
1149 let mut p = p_rc.borrow_mut();
1150 let fh = p
1151 .file
1152 .as_deref_mut()
1153 .expect("open stream has no file handle");
1154 read_all(fh)
1155 };
1156 state.push_string(&bytes)?;
1157 success = true;
1158 }
1159 _ => {
1160 return Err(lua_vm::debug::arg_error_impl(state, n, b"invalid format"));
1161 }
1162 }
1163 }
1164 n += 1;
1165 remaining -= 1;
1166 }
1167 }
1168
1169 let has_err = {
1170 let p = p_rc.borrow();
1171 match p.file.as_deref() {
1172 Some(fh) => fh.has_error(),
1173 None => false,
1174 }
1175 };
1176 if has_err {
1177 let err = {
1178 let p = p_rc.borrow();
1179 match p.file.as_deref().and_then(|fh| fh.last_error_info()) {
1180 Some((code, _msg)) if code != 0 => io::Error::from_raw_os_error(code),
1181 Some((_code, msg)) => io::Error::new(io::ErrorKind::Other, msg),
1182 None => io::Error::new(io::ErrorKind::Other, "file read error"),
1183 }
1184 };
1185 return file_result(state, false, None, err);
1186 }
1187
1188 if !success {
1189 state.pop_n(1);
1190 state.push(LuaValue::Nil);
1191 }
1192
1193 Ok((n - first) as usize)
1194}
1195
1196fn get_io_file_rc(state: &mut LuaState, key: &[u8]) -> Result<Rc<RefCell<LStream>>, LuaError> {
1201 state.registry_get(key)?;
1202 let ud_id = state
1203 .test_arg_userdata(-1, LUA_FILE_HANDLE)
1204 .map(|ud| ud.identity());
1205 state.pop_n(1);
1206 let label = &key[IO_PREFIX_LEN..];
1207 let id = ud_id.ok_or_else(|| {
1208 LuaError::runtime(format_args!(
1209 "default {} file is invalid",
1210 label.escape_ascii()
1211 ))
1212 })?;
1213 let rc = lookup_lstream(id).ok_or_else(|| {
1214 LuaError::runtime(format_args!(
1215 "default {} file is invalid",
1216 label.escape_ascii()
1217 ))
1218 })?;
1219 if rc.borrow().is_closed() {
1220 return Err(LuaError::runtime(format_args!(
1221 "default {} file is closed",
1222 label.escape_ascii()
1223 )));
1224 }
1225 Ok(rc)
1226}
1227
1228pub fn io_read(state: &mut LuaState) -> Result<usize, LuaError> {
1230 let p_rc = get_io_file_rc(state, IO_INPUT_KEY)?;
1231 g_read(state, &p_rc, 1)
1232}
1233
1234pub fn f_read(state: &mut LuaState) -> Result<usize, LuaError> {
1236 let p_rc = tofile(state)?;
1237 g_read(state, &p_rc, 2)
1238}
1239
1240fn num_to_write_bytes(state: &mut LuaState, val: &LuaValue) -> Result<Vec<u8>, LuaError> {
1253 let s = lua_vm::object::num_to_string(state, val)?;
1254 Ok(s.as_bytes().to_vec())
1255}
1256
1257#[expect(
1261 dead_code,
1262 reason = "ported stdlib helper; not yet wired into the runtime"
1263)]
1264fn g_write(
1265 state: &mut LuaState,
1266 file: &mut dyn LuaFileHandle,
1267 arg: i32,
1268) -> Result<usize, LuaError> {
1269 let nargs = state.top() - arg;
1270 let mut overall_ok = true;
1271
1272 for i in 0..nargs {
1273 let idx = arg + i;
1274 if state.type_at(idx) == LuaType::Number {
1275 let val = state.value_at(idx);
1281 let s = num_to_write_bytes(state, &val)?;
1282 match file.write_bytes(&s) {
1283 Ok(n) => overall_ok = overall_ok && n == s.len(),
1284 Err(_) => overall_ok = false,
1285 }
1286 } else {
1287 let s: Vec<u8> = state.check_arg_string(idx)?;
1288 match file.write_bytes(&s) {
1289 Ok(n) => overall_ok = overall_ok && n == s.len(),
1290 Err(_) => overall_ok = false,
1291 }
1292 }
1293 }
1294
1295 if overall_ok {
1296 Ok(1) } else {
1298 file_result(
1299 state,
1300 false,
1301 None,
1302 io::Error::new(io::ErrorKind::Other, "write error"),
1303 )
1304 }
1305}
1306
1307pub fn io_write(state: &mut LuaState) -> Result<usize, LuaError> {
1317 let n = state.top();
1319 let mut chunks: Vec<Vec<u8>> = Vec::with_capacity(n as usize);
1320 for i in 1..=(n as i32) {
1321 if state.type_at(i) == LuaType::Number {
1322 let val = state.value_at(i);
1323 chunks.push(num_to_write_bytes(state, &val)?);
1324 } else {
1325 let bytes: Vec<u8> = state.check_arg_string(i)?;
1326 chunks.push(bytes);
1327 }
1328 }
1329
1330 let p_rc = get_io_file_rc(state, IO_OUTPUT_KEY)?;
1333 {
1334 let mut p = p_rc.borrow_mut();
1335 let fh = p.file.as_mut().expect("open stream has no file handle");
1336 for chunk in &chunks {
1337 fh.write_bytes(chunk)
1338 .map_err(|e| LuaError::runtime(format_args!("io.write: {}", e)))?;
1339 }
1340 }
1341 state.registry_get(IO_OUTPUT_KEY)?;
1342 Ok(1)
1343}
1344
1345pub fn f_write(state: &mut LuaState) -> Result<usize, LuaError> {
1347 let p_rc = tofile(state)?;
1348
1349 let n = state.top();
1351 let mut chunks: Vec<Vec<u8>> = Vec::with_capacity(n.saturating_sub(1) as usize);
1352 for i in 2..=(n as i32) {
1353 if state.type_at(i) == LuaType::Number {
1354 let val = state.value_at(i);
1355 chunks.push(num_to_write_bytes(state, &val)?);
1356 } else {
1357 let bytes: Vec<u8> = state.check_arg_string(i)?;
1358 chunks.push(bytes);
1359 }
1360 }
1361
1362 let result: io::Result<()> = {
1364 let mut p = p_rc.borrow_mut();
1365 let fh = p.file.as_mut().expect("open stream has no file handle");
1366 let mut r: io::Result<()> = Ok(());
1367 for chunk in &chunks {
1368 match fh.write_bytes(chunk) {
1369 Ok(written) if written == chunk.len() => {}
1370 Ok(_) => {
1371 r = Err(io::Error::new(io::ErrorKind::Other, "short write"));
1372 break;
1373 }
1374 Err(e) => {
1375 r = Err(e);
1376 break;
1377 }
1378 }
1379 }
1380 r
1381 };
1382
1383 match result {
1385 Ok(()) => {
1386 state.push_value_at(1)?;
1387 Ok(1)
1388 }
1389 Err(e) => file_result(state, false, None, e),
1390 }
1391}
1392
1393pub fn f_seek(state: &mut LuaState) -> Result<usize, LuaError> {
1397 static MODE_NAMES: &[&[u8]] = &[b"set", b"cur", b"end"];
1398
1399 let p_rc = tofile(state)?;
1400 let op = state.check_arg_option(2, Some(b"cur"), MODE_NAMES)?;
1401 let p3: i64 = state.opt_arg_integer(3, 0)?;
1402
1403 let seek_pos = match op {
1404 0 => SeekFrom::Start(p3 as u64),
1405 1 => SeekFrom::Current(p3),
1406 2 => SeekFrom::End(p3),
1407 _ => unreachable!(),
1408 };
1409
1410 let result = {
1411 let mut p = p_rc.borrow_mut();
1412 let fh = p.file.as_mut().expect("open stream has no file handle");
1413 fh.seek(seek_pos)
1414 };
1415 match result {
1416 Ok(pos) => {
1417 state.push(LuaValue::Int(pos as i64));
1418 Ok(1)
1419 }
1420 Err(e) => file_result(state, false, None, e),
1421 }
1422}
1423
1424pub fn f_setvbuf(state: &mut LuaState) -> Result<usize, LuaError> {
1426 static MODE_NAMES: &[&[u8]] = &[b"no", b"full", b"line"];
1427
1428 let p_rc = tofile(state)?;
1429 let op = state.check_arg_option(2, None, MODE_NAMES)?;
1430 let sz: i64 = state.opt_arg_integer(3, LUAL_BUFFER_SIZE as i64)?;
1431 let mode = match op {
1432 0 => BufMode::No,
1433 1 => BufMode::Full,
1434 2 => BufMode::Line,
1435 _ => unreachable!(),
1436 };
1437 let result = {
1438 let mut p = p_rc.borrow_mut();
1439 let fh = p.file.as_mut().expect("open stream has no file handle");
1440 let mode_index = match mode {
1441 BufMode::No => 0,
1442 BufMode::Full => 1,
1443 BufMode::Line => 2,
1444 };
1445 fh.set_buf_mode(mode_index, sz.max(0) as usize)
1446 };
1447 match result {
1448 Ok(()) => file_result(state, true, None, io::Error::last_os_error()),
1449 Err(e) => file_result(state, false, None, e),
1450 }
1451}
1452
1453pub fn io_flush(state: &mut LuaState) -> Result<usize, LuaError> {
1455 let ud_id: Option<usize> = {
1456 state.registry_get(IO_OUTPUT_KEY)?;
1457 let id = state
1458 .test_arg_userdata(-1, LUA_FILE_HANDLE)
1459 .map(|ud| ud.identity());
1460 state.pop_n(1);
1461 id
1462 };
1463 if let Some(id) = ud_id {
1464 if let Some(rc) = lookup_lstream(id) {
1465 let result = {
1466 let mut p = rc.borrow_mut();
1467 if p.is_closed() {
1468 return Err(LuaError::runtime(format_args!(
1469 "default output file is closed"
1470 )));
1471 }
1472 let fh = p
1473 .file
1474 .as_deref_mut()
1475 .expect("open stream has no file handle");
1476 fh.flush()
1477 };
1478 return match result {
1479 Ok(()) => {
1480 state.push(LuaValue::Bool(true));
1481 Ok(1)
1482 }
1483 Err(e) => file_result(state, false, None, e),
1484 };
1485 }
1486 }
1487 state.push(LuaValue::Bool(true));
1489 Ok(1)
1490}
1491
1492pub fn f_flush(state: &mut LuaState) -> Result<usize, LuaError> {
1494 let p_rc = tofile(state)?;
1495 let result = {
1496 let mut p = p_rc.borrow_mut();
1497 let fh = p.file.as_mut().expect("open stream has no file handle");
1498 fh.flush()
1499 };
1500 match result {
1501 Ok(()) => {
1502 state.push(LuaValue::Bool(true));
1503 Ok(1)
1504 }
1505 Err(e) => file_result(state, false, None, e),
1506 }
1507}
1508
1509fn aux_lines(state: &mut LuaState, toclose: bool) -> Result<(), LuaError> {
1519 let n = state.top() - 1;
1522 if n > MAX_ARG_LINE as i32 {
1523 return Err(lua_vm::debug::arg_error_impl(
1524 state,
1525 MAX_ARG_LINE as i32 + 2,
1526 b"too many arguments",
1527 ));
1528 }
1529 state.push_value_at(1)?;
1530 state.push(LuaValue::Int(n as i64));
1531 state.push(LuaValue::Bool(toclose));
1532 state.rotate(2, 3)?;
1533 state.push_c_closure(io_readline, (3 + n) as i32)?;
1534 Ok(())
1535}
1536
1537pub fn f_lines(state: &mut LuaState) -> Result<usize, LuaError> {
1539 let _ = tofile(state)?; aux_lines(state, false)?;
1541 Ok(1)
1542}
1543
1544pub fn io_lines(state: &mut LuaState) -> Result<usize, LuaError> {
1546 if state.type_at(1) == LuaType::None {
1547 state.push(LuaValue::Nil);
1548 }
1549 let toclose = if state.type_at(1) == LuaType::Nil {
1550 state.registry_get(IO_INPUT_KEY)?;
1551 state.replace(1)?;
1552 let _ = tofile(state)?;
1553 false
1554 } else {
1555 let filename = state.check_arg_string(1)?;
1556 opencheck(state, &filename, b"r")?;
1557 state.replace(1)?;
1558 true
1559 };
1560
1561 aux_lines(state, toclose)?;
1562
1563 if toclose {
1564 state.push(LuaValue::Nil); state.push(LuaValue::Nil); state.push_value_at(1)?; Ok(4)
1568 } else {
1569 Ok(1)
1570 }
1571}
1572
1573fn io_readline(state: &mut LuaState) -> Result<usize, LuaError> {
1581 let n = match state.value_at(crate::state_stub::upvalue_index(2)) {
1582 LuaValue::Int(i) => i as usize,
1583 _ => 0,
1584 };
1585
1586 let p_rc = lstream_from_upvalue(state, 1)?;
1587
1588 if p_rc.borrow().is_closed() {
1589 return Err(LuaError::runtime(format_args!("file is already closed")));
1590 }
1591
1592 lua_vm::api::set_top(state, 1)?;
1593 state.ensure_stack(n as i32, "too many arguments")?;
1594
1595 for i in 1..=n {
1596 let uv = state.value_at(crate::state_stub::upvalue_index(3 + i as i32));
1597 state.push(uv);
1598 }
1599
1600 let result_n: usize = g_read(state, &p_rc, 2)?;
1601
1602 debug_assert!(result_n > 0, "g_read should return at least one value");
1603
1604 let top = state.top_idx().get() as i32;
1605 let first_result_idx = top - result_n as i32;
1606 let first_truthy = !matches!(
1607 state.stack_at(first_result_idx),
1608 LuaValue::Nil | LuaValue::Bool(false)
1609 );
1610 if first_truthy {
1611 return Ok(result_n);
1612 }
1613
1614 if result_n > 1 {
1615 let err_val = state.stack_at(first_result_idx + 1).clone();
1616 return Err(LuaError::from_value(err_val));
1617 }
1618
1619 let toclose = !matches!(
1620 state.value_at(crate::state_stub::upvalue_index(3)),
1621 LuaValue::Nil | LuaValue::Bool(false)
1622 );
1623 if toclose {
1624 lua_vm::api::set_top(state, 0)?;
1625 state.push_upvalue(1)?;
1626 aux_close(state)?;
1627 }
1628
1629 Ok(0)
1630}
1631
1632fn create_meta(state: &mut LuaState) -> Result<(), LuaError> {
1636 state.new_metatable(LUA_FILE_HANDLE)?;
1637 state.set_funcs(FILE_METAMETHODS, 0)?;
1638 state.new_lib_table(FILE_METHODS)?;
1639 state.set_funcs(FILE_METHODS, 0)?;
1640 state.set_field(-2, b"__index")?;
1641 state.pop_n(1);
1642 Ok(())
1643}
1644
1645fn create_std_file(
1647 state: &mut LuaState,
1648 std_kind: StdFileKind,
1649 registry_key: Option<&[u8]>,
1650 field_name: &[u8],
1651) -> Result<(), LuaError> {
1652 let cell = new_pre_file(state)?;
1653 let output_hook = match std_kind {
1654 StdFileKind::Stdout => state.global().stdout_hook,
1655 StdFileKind::Stderr => state.global().stderr_hook,
1656 StdFileKind::Stdin => None,
1657 };
1658 let input_hook = match std_kind {
1659 StdFileKind::Stdin => state.global().stdin_hook,
1660 StdFileKind::Stdout | StdFileKind::Stderr => None,
1661 };
1662 {
1663 let mut p = cell.borrow_mut();
1664 p.file = Some(Box::new(StdStreamHandle::new(
1665 std_kind,
1666 input_hook,
1667 output_hook,
1668 )));
1669 p.close_fn = Some(io_noclose);
1670 }
1671 if let Some(key) = registry_key {
1672 state.push_value_at(-1)?;
1673 state.registry_set(key)?;
1674 }
1675 state.set_field(-2, field_name)?;
1676 Ok(())
1677}
1678
1679pub fn luaopen_io(state: &mut LuaState) -> Result<usize, LuaError> {
1681 state.new_lib(IO_LIB)?;
1682 create_meta(state)?;
1683 create_std_file(state, StdFileKind::Stdin, Some(IO_INPUT_KEY), b"stdin")?;
1684 create_std_file(state, StdFileKind::Stdout, Some(IO_OUTPUT_KEY), b"stdout")?;
1685 create_std_file(state, StdFileKind::Stderr, None, b"stderr")?;
1686 Ok(1)
1687}
1688
1689