1use std::cell::RefCell;
24use std::collections::HashMap;
25use std::io::{self, SeekFrom};
26use std::rc::Rc;
27
28use lua_types::{LuaError, LuaFileHandle, LuaType, LuaValue};
29use crate::state_stub::{LuaState, LuaStateStubExt as _};
30
31thread_local! {
32 static LSTREAM_REGISTRY: RefCell<HashMap<usize, Rc<RefCell<LStream>>>>
39 = RefCell::new(HashMap::new());
40}
41
42fn register_lstream(ud_id: usize, lstream: LStream) -> Rc<RefCell<LStream>> {
43 let cell = Rc::new(RefCell::new(lstream));
44 LSTREAM_REGISTRY.with(|reg| {
45 reg.borrow_mut().insert(ud_id, cell.clone());
46 });
47 cell
48}
49
50fn lookup_lstream(ud_id: usize) -> Option<Rc<RefCell<LStream>>> {
51 LSTREAM_REGISTRY.with(|reg| reg.borrow().get(&ud_id).cloned())
52}
53
54pub const LUA_FILE_HANDLE: &[u8] = b"FILE*";
58
59const IO_INPUT_KEY: &[u8] = b"_IO_input";
61
62const IO_OUTPUT_KEY: &[u8] = b"_IO_output";
64
65const IO_PREFIX_LEN: usize = 4;
67
68const MAX_ARG_LINE: usize = 250;
70
71const L_MAX_LEN_NUM: usize = 200;
73
74const EOF_SENTINEL: i32 = -1;
76
77const LUAL_BUFFER_SIZE: usize = 8192;
79
80pub trait LuaFileOps: LuaFileHandle {
89 fn set_buf_mode(&mut self, mode: BufMode, size: usize) -> io::Result<()>;
91}
92
93#[derive(Debug, Clone, Copy, PartialEq, Eq)]
97pub enum SeekWhence {
98 Set,
99 Cur,
100 End,
101}
102
103#[derive(Debug, Clone, Copy, PartialEq, Eq)]
105pub enum BufMode {
106 No,
107 Full,
108 Line,
109}
110
111pub enum StdFileKind {
113 Stdin,
114 Stdout,
115 Stderr,
116}
117
118pub struct LStream {
131 pub file: Option<Box<dyn LuaFileHandle>>,
135 pub close_fn: Option<fn(&mut LuaState) -> Result<usize, LuaError>>,
137}
138
139impl LStream {
140 pub fn is_closed(&self) -> bool {
142 self.close_fn.is_none()
143 }
144}
145
146struct StdStreamHandle {
151 kind: StdFileKind,
152}
153
154impl LuaFileHandle for StdStreamHandle {
155 fn read_byte(&mut self) -> i32 {
156 use std::io::Read;
157 match self.kind {
158 StdFileKind::Stdin => {
159 let mut buf = [0u8; 1];
160 match std::io::stdin().read(&mut buf) {
161 Ok(1) => buf[0] as i32,
162 _ => EOF_SENTINEL,
163 }
164 }
165 _ => EOF_SENTINEL,
166 }
167 }
168 fn unread_byte(&mut self, _byte: i32) {}
169 fn write_bytes(&mut self, data: &[u8]) -> io::Result<usize> {
170 use std::io::Write;
171 match self.kind {
172 StdFileKind::Stderr => {
173 std::io::stderr().write_all(data)?;
174 Ok(data.len())
175 }
176 _ => {
177 std::io::stdout().write_all(data)?;
178 Ok(data.len())
179 }
180 }
181 }
182 fn flush(&mut self) -> io::Result<()> {
183 use std::io::Write;
184 match self.kind {
185 StdFileKind::Stderr => std::io::stderr().flush(),
186 _ => std::io::stdout().flush(),
187 }
188 }
189 fn seek(&mut self, _pos: SeekFrom) -> io::Result<u64> {
190 Err(io::Error::new(io::ErrorKind::Unsupported, "stdio seek"))
191 }
192 fn tell(&mut self) -> io::Result<u64> {
193 Err(io::Error::new(io::ErrorKind::Unsupported, "stdio tell"))
194 }
195 fn clear_error(&mut self) {}
196 fn has_error(&self) -> bool { false }
197}
198
199impl LuaFileOps for StdStreamHandle {
200 fn set_buf_mode(&mut self, _mode: BufMode, _size: usize) -> io::Result<()> { Ok(()) }
201}
202
203impl StdStreamHandle {
204 fn new(kind: StdFileKind) -> Self { StdStreamHandle { kind } }
205}
206
207struct ReadNumState {
209 current: i32,
211 count: usize,
213 buf: [u8; L_MAX_LEN_NUM + 1],
215}
216
217impl ReadNumState {
218 fn new(first_byte: i32) -> Self {
219 ReadNumState {
220 current: first_byte,
221 count: 0,
222 buf: [0u8; L_MAX_LEN_NUM + 1],
223 }
224 }
225
226 fn advance(&mut self, file: &mut dyn LuaFileHandle) -> bool {
229 if self.count >= L_MAX_LEN_NUM {
230 self.buf[0] = 0;
231 return false;
232 }
233 self.buf[self.count] = self.current as u8;
234 self.count += 1;
235 self.current = file.read_byte();
236 true
237 }
238
239 fn try2(&mut self, file: &mut dyn LuaFileHandle, set: [u8; 2]) -> bool {
241 if self.current == set[0] as i32 || self.current == set[1] as i32 {
242 self.advance(file)
243 } else {
244 false
245 }
246 }
247
248 fn read_digits(&mut self, file: &mut dyn LuaFileHandle, hex: bool) -> usize {
250 let mut count = 0usize;
251 loop {
252 let is_digit = if hex {
253 (self.current as u8).is_ascii_hexdigit()
254 } else {
255 (self.current as u8).is_ascii_digit()
256 };
257 if !is_digit || self.current == EOF_SENTINEL {
258 break;
259 }
260 if !self.advance(file) {
261 break;
262 }
263 count += 1;
264 }
265 count
266 }
267
268 fn as_bytes(&self) -> &[u8] {
270 &self.buf[..self.count]
271 }
272}
273
274pub const IO_LIB: &[(&[u8], fn(&mut LuaState) -> Result<usize, LuaError>)] = &[
278 (b"close", io_close),
279 (b"flush", io_flush),
280 (b"input", io_input),
281 (b"lines", io_lines),
282 (b"open", io_open),
283 (b"output", io_output),
284 (b"popen", io_popen),
285 (b"read", io_read),
286 (b"tmpfile", io_tmpfile),
287 (b"type", io_type),
288 (b"write", io_write),
289];
290
291pub const FILE_METHODS: &[(&[u8], fn(&mut LuaState) -> Result<usize, LuaError>)] = &[
293 (b"read", f_read),
294 (b"write", f_write),
295 (b"lines", f_lines),
296 (b"flush", f_flush),
297 (b"seek", f_seek),
298 (b"close", f_close),
299 (b"setvbuf", f_setvbuf),
300];
301
302pub const FILE_METAMETHODS: &[(&[u8], fn(&mut LuaState) -> Result<usize, LuaError>)] = &[
304 (b"__gc", f_gc),
305 (b"__close", f_gc),
306 (b"__tostring", f_tostring),
307];
308
309fn check_mode(mode: &[u8]) -> bool {
315 if mode.is_empty() {
316 return false;
317 }
318 let mut idx = 0usize;
319 if !matches!(mode[idx], b'r' | b'w' | b'a') {
320 return false;
321 }
322 idx += 1;
323 if idx < mode.len() && mode[idx] == b'+' {
324 idx += 1;
325 }
326 mode[idx..].iter().all(|&b| b == b'b')
327}
328
329fn check_mode_popen(mode: &[u8]) -> bool {
331 matches!(mode, b"r" | b"w")
332}
333
334fn file_result(
338 state: &mut LuaState,
339 success: bool,
340 fname: Option<&[u8]>,
341 os_err: io::Error,
342) -> Result<usize, LuaError> {
343 if success {
344 state.push(LuaValue::Bool(true));
345 return Ok(1);
346 }
347 state.push(LuaValue::Bool(false));
348 let msg = os_err.to_string();
349 match fname {
350 Some(name) => {
351 let mut s = Vec::with_capacity(name.len() + 2 + msg.len());
352 s.extend_from_slice(name);
353 s.extend_from_slice(b": ");
354 s.extend_from_slice(msg.as_bytes());
355 state.push_string(&s)?;
356 }
357 None => {
358 state.push_string(msg.as_bytes())?;
359 }
360 }
361 let errno_code = os_err.raw_os_error().unwrap_or(0) as i64;
362 state.push(LuaValue::Int(errno_code));
363 Ok(3)
364}
365
366fn exec_result(state: &mut LuaState, stat: i32) -> Result<usize, LuaError> {
373 if stat == 0 {
374 state.push(LuaValue::Bool(true));
375 Ok(1)
376 } else {
377 state.push(LuaValue::Bool(false));
378 state.push_string(b"exit")?;
380 state.push(LuaValue::Int(stat as i64));
381 Ok(3)
382 }
383}
384
385fn get_lstream(state: &mut LuaState) -> Result<Rc<RefCell<LStream>>, LuaError> {
392 let ud = state.check_arg_userdata(1, LUA_FILE_HANDLE)?;
393 lookup_lstream(ud.identity()).ok_or_else(|| {
394 LuaError::runtime(format_args!("invalid file handle"))
395 })
396}
397
398fn lstream_from_upvalue(
405 state: &mut LuaState,
406 idx: i32,
407) -> Result<Rc<RefCell<LStream>>, LuaError> {
408 let v = state.value_at(crate::state_stub::upvalue_index(idx));
409 let ud_id = match v {
410 LuaValue::UserData(ud) => ud.identity(),
411 _ => {
412 return Err(LuaError::runtime(format_args!(
413 "invalid file handle in upvalue {}",
414 idx
415 )));
416 }
417 };
418 lookup_lstream(ud_id).ok_or_else(|| {
419 LuaError::runtime(format_args!("invalid file handle in upvalue {}", idx))
420 })
421}
422
423fn tofile(state: &mut LuaState) -> Result<Rc<RefCell<LStream>>, LuaError> {
425 let p_rc = get_lstream(state)?;
426 {
427 let p = p_rc.borrow();
428 if p.is_closed() {
429 return Err(LuaError::runtime(format_args!(
430 "attempt to use a closed file"
431 )));
432 }
433 debug_assert!(p.file.is_some());
434 }
435 Ok(p_rc)
436}
437
438fn new_pre_file(state: &mut LuaState) -> Result<Rc<RefCell<LStream>>, LuaError> {
445 let ud = state.new_userdata_typed(LUA_FILE_HANDLE, std::mem::size_of::<LStream>(), 0)?;
446 state.set_metatable_by_name(LUA_FILE_HANDLE)?;
447 let cell = register_lstream(ud.identity(), LStream { file: None, close_fn: None });
448 Ok(cell)
449}
450
451fn new_file(state: &mut LuaState) -> Result<Rc<RefCell<LStream>>, LuaError> {
453 let cell = new_pre_file(state)?;
454 cell.borrow_mut().close_fn = Some(io_fclose);
455 Ok(cell)
456}
457
458fn opencheck(state: &mut LuaState, fname: &[u8], mode: &[u8]) -> Result<(), LuaError> {
463 let hook = state.global().file_open_hook;
464 let fh = match hook {
465 Some(open_fn) => open_fn(fname, mode).map_err(|e| {
466 LuaError::runtime(format_args!(
467 "cannot open file '{}' ({})",
468 fname.escape_ascii(),
469 match &e {
470 LuaError::Runtime(LuaValue::Str(s)) => {
471 String::from_utf8_lossy(s.as_bytes()).into_owned()
472 }
473 other => format!("{:?}", other),
474 }
475 ))
476 })?,
477 None => {
478 return Err(LuaError::runtime(format_args!(
479 "cannot open file '{}' (no filesystem hook registered)",
480 fname.escape_ascii()
481 )));
482 }
483 };
484 let cell = new_file(state)?;
485 cell.borrow_mut().file = Some(fh);
486 Ok(())
487}
488
489fn io_fclose(state: &mut LuaState) -> Result<usize, LuaError> {
495 let p_rc = get_lstream(state)?;
496 let _closed = p_rc.borrow_mut().file.take();
498 state.push(LuaValue::Bool(true));
499 Ok(1)
500}
501
502fn io_pclose(state: &mut LuaState) -> Result<usize, LuaError> {
506 let p_rc = get_lstream(state)?;
507 let _closed = p_rc.borrow_mut().file.take();
508 exec_result(state, 0)
510}
511
512fn io_noclose(state: &mut LuaState) -> Result<usize, LuaError> {
514 let p_rc = get_lstream(state)?;
515 p_rc.borrow_mut().close_fn = Some(io_noclose); state.push(LuaValue::Bool(false));
517 state.push_string(b"cannot close standard file")?;
518 Ok(2)
519}
520
521fn aux_close(state: &mut LuaState) -> Result<usize, LuaError> {
523 let p_rc = get_lstream(state)?;
524 let cf = p_rc.borrow_mut().close_fn.take().ok_or_else(|| {
525 LuaError::runtime(format_args!("attempt to close an already-closed file"))
526 })?;
527 cf(state)
528}
529
530pub fn io_type(state: &mut LuaState) -> Result<usize, LuaError> {
534 state.check_arg_any(1)?;
535 let maybe_userdata = state.test_arg_userdata(1, LUA_FILE_HANDLE);
536 match maybe_userdata {
537 None => {
538 state.push(LuaValue::Bool(false));
539 }
540 Some(ud) => {
541 let is_closed = match lookup_lstream(ud.identity()) {
542 Some(rc) => rc.borrow().is_closed(),
543 None => true, };
545 if is_closed {
546 state.push_string(b"closed file")?;
547 } else {
548 state.push_string(b"file")?;
549 }
550 }
551 }
552 Ok(1)
553}
554
555fn f_tostring(state: &mut LuaState) -> Result<usize, LuaError> {
559 let p_rc = get_lstream(state)?;
560 let closed = p_rc.borrow().is_closed();
561 if closed {
562 state.push_string(b"file (closed)")?;
563 } else {
564 state.push_string(b"file (0x?)")?;
566 }
567 Ok(1)
568}
569
570fn f_close(state: &mut LuaState) -> Result<usize, LuaError> {
574 let _ = tofile(state)?; aux_close(state)
576}
577
578pub fn io_close(state: &mut LuaState) -> Result<usize, LuaError> {
580 if state.type_at(1) == LuaType::None {
584 state.registry_get(IO_OUTPUT_KEY)?;
585 }
586 f_close(state)
587}
588
589fn f_gc(state: &mut LuaState) -> Result<usize, LuaError> {
591 let p_rc = get_lstream(state)?;
592 let needs_close = {
593 let p = p_rc.borrow();
594 !p.is_closed() && p.file.is_some()
595 };
596 if needs_close {
597 let _ = aux_close(state);
599 }
600 Ok(0)
601}
602
603pub fn io_open(state: &mut LuaState) -> Result<usize, LuaError> {
610 let filename: Vec<u8> = state.check_arg_string(1)?;
611 let mode: Vec<u8> = state.opt_arg_string(2, b"r")?;
612 if !check_mode(&mode) {
613 return Err(LuaError::arg_error(2, "invalid mode"));
614 }
615 let hook = state.global().file_open_hook;
616 match hook {
617 Some(open_fn) => match open_fn(&filename, &mode) {
618 Ok(fh) => {
619 let cell = new_file(state)?;
620 cell.borrow_mut().file = Some(fh);
621 Ok(1)
622 }
623 Err(e) => {
624 let os_err = io::Error::new(
625 io::ErrorKind::Other,
626 match &e {
627 LuaError::Runtime(LuaValue::Str(s)) => {
628 String::from_utf8_lossy(s.as_bytes()).into_owned()
629 }
630 other => format!("{:?}", other),
631 },
632 );
633 file_result(state, false, Some(&filename), os_err)
634 }
635 },
636 None => {
637 let os_err = io::Error::new(
638 io::ErrorKind::Unsupported,
639 "no filesystem hook registered",
640 );
641 file_result(state, false, Some(&filename), os_err)
642 }
643 }
644}
645
646pub fn io_popen(state: &mut LuaState) -> Result<usize, LuaError> {
655 let filename: Vec<u8> = state.check_arg_string(1)?;
656 let mode: Vec<u8> = state.opt_arg_string(2, b"r")?;
657 if !check_mode_popen(&mode) {
658 return Err(LuaError::arg_error(2, "invalid mode"));
659 }
660 let hook = state.global().popen_hook;
661 match hook {
662 Some(spawn_fn) => match spawn_fn(&filename, &mode) {
663 Ok(fh) => {
664 let cell = new_pre_file(state)?;
665 let mut p = cell.borrow_mut();
666 p.file = Some(fh);
667 p.close_fn = Some(io_pclose);
668 drop(p);
669 Ok(1)
670 }
671 Err(e) => {
672 let os_err = io::Error::new(
673 io::ErrorKind::Other,
674 match &e {
675 LuaError::Runtime(LuaValue::Str(s)) => {
676 String::from_utf8_lossy(s.as_bytes()).into_owned()
677 }
678 other => format!("{:?}", other),
679 },
680 );
681 file_result(state, false, Some(&filename), os_err)
682 }
683 },
684 None => {
685 let os_err = io::Error::new(
686 io::ErrorKind::Unsupported,
687 "popen not enabled in this build",
688 );
689 file_result(state, false, Some(&filename), os_err)
690 }
691 }
692}
693
694pub fn io_tmpfile(state: &mut LuaState) -> Result<usize, LuaError> {
696 let hook = state.global().file_open_hook;
697 let Some(open_fn) = hook else {
698 let os_err = io::Error::new(
699 io::ErrorKind::Unsupported,
700 "no filesystem hook registered",
701 );
702 return file_result(state, false, None, os_err);
703 };
704
705 let mut path = std::env::temp_dir().to_string_lossy().as_bytes().to_vec();
706 if path.last().copied() != Some(b'/') && path.last().copied() != Some(b'\\') {
707 path.push(b'/');
708 }
709 let unique = format!(
710 "lua_tmpfile_{}_{}",
711 std::process::id(),
712 std::time::SystemTime::now()
713 .duration_since(std::time::UNIX_EPOCH)
714 .map(|d| d.as_nanos())
715 .unwrap_or(0)
716 );
717 path.extend_from_slice(unique.as_bytes());
718
719 match open_fn(&path, b"w+b") {
720 Ok(fh) => {
721 let cell = new_file(state)?;
722 cell.borrow_mut().file = Some(fh);
723 Ok(1)
724 }
725 Err(e) => {
726 let os_err = io::Error::new(
727 io::ErrorKind::Other,
728 match &e {
729 LuaError::Runtime(LuaValue::Str(s)) => {
730 String::from_utf8_lossy(s.as_bytes()).into_owned()
731 }
732 other => format!("{:?}", other),
733 },
734 );
735 file_result(state, false, None, os_err)
736 }
737 }
738}
739
740#[expect(dead_code, unreachable_code, unused_variables, reason = "io default-file helper: not yet wired; pending LStream-from-registry port")]
747fn get_io_file<'a>(
748 state: &'a mut LuaState,
749 key: &[u8],
750) -> Result<&'a mut dyn LuaFileHandle, LuaError> {
751 state.registry_get(key)?;
752 let label = &key[IO_PREFIX_LEN..]; let p: &mut LStream = todo!("TODO(port): extract LStream from registry userdata");
755 if p.is_closed() {
756 return Err(LuaError::runtime(format_args!(
757 "default {} file is closed",
758 label.escape_ascii()
759 )));
760 }
761 Ok(p.file.as_mut().expect("open stream has no file handle").as_mut())
762}
763
764fn g_iofile(state: &mut LuaState, key: &[u8], mode: &[u8]) -> Result<usize, LuaError> {
766 if !matches!(state.type_at(1), LuaType::None | LuaType::Nil) {
767 if state.type_at(1) == LuaType::String {
768 let filename = state.check_arg_string(1)?;
769 opencheck(state, &filename, mode)?;
770 } else {
771 let _ = tofile(state)?;
772 state.push_value_at(1)?;
773 }
774 state.registry_set(key)?;
775 }
776 state.registry_get(key)?;
777 Ok(1)
778}
779
780pub fn io_input(state: &mut LuaState) -> Result<usize, LuaError> {
782 g_iofile(state, IO_INPUT_KEY, b"r")
783}
784
785pub fn io_output(state: &mut LuaState) -> Result<usize, LuaError> {
787 g_iofile(state, IO_OUTPUT_KEY, b"w")
788}
789
790fn read_number_bytes(file: &mut dyn LuaFileHandle) -> Vec<u8> {
794 let first = loop {
795 let b = file.read_byte();
796 if b == EOF_SENTINEL || !(b as u8).is_ascii_whitespace() {
797 break b;
798 }
799 };
800
801 let mut rn = ReadNumState::new(first);
802
803 rn.try2(file, [b'-', b'+']);
804
805 let mut count: usize = 0;
806 let hex = if rn.try2(file, [b'0', b'0']) {
807 if rn.try2(file, [b'x', b'X']) {
808 true
809 } else {
810 count = 1;
811 false
812 }
813 } else {
814 false
815 };
816
817 count += rn.read_digits(file, hex);
818
819 let dec_point = b'.';
821 if rn.try2(file, [dec_point, b'.']) {
822 count += rn.read_digits(file, hex);
823 }
824
825 if count > 0 {
826 let exp_chars = if hex { [b'p', b'P'] } else { [b'e', b'E'] };
827 if rn.try2(file, exp_chars) {
828 rn.try2(file, [b'-', b'+']);
829 rn.read_digits(file, false);
830 }
831 }
832
833 file.unread_byte(rn.current);
834 rn.as_bytes().to_vec()
835}
836
837fn test_eof(file: &mut dyn LuaFileHandle) -> bool {
840 let c = file.read_byte();
841 if c != EOF_SENTINEL {
842 file.unread_byte(c);
843 }
844 c != EOF_SENTINEL
845}
846
847fn read_line(file: &mut dyn LuaFileHandle, chop: bool) -> (Vec<u8>, bool) {
853 let mut buf: Vec<u8> = Vec::new();
854 let mut c: i32;
855
856 'outer: loop {
861 for _ in 0..LUAL_BUFFER_SIZE {
862 c = file.read_byte();
863 if c == EOF_SENTINEL || c == b'\n' as i32 {
864 break 'outer;
865 }
866 buf.push(c as u8);
867 }
868 }
870
871 if !chop && c == b'\n' as i32 {
872 buf.push(b'\n');
873 }
874
875 let had_content = c == b'\n' as i32 || !buf.is_empty();
876 (buf, had_content)
877}
878
879fn read_all(file: &mut dyn LuaFileHandle) -> Vec<u8> {
885 let mut buf: Vec<u8> = Vec::new();
886 loop {
887 let mut chunk_read = 0usize;
888 for _ in 0..LUAL_BUFFER_SIZE {
889 let b = file.read_byte();
890 if b == EOF_SENTINEL {
891 break;
892 }
893 buf.push(b as u8);
894 chunk_read += 1;
895 }
896 if chunk_read < LUAL_BUFFER_SIZE {
897 break;
898 }
899 }
900 buf
901}
902
903fn read_chars(file: &mut dyn LuaFileHandle, n: usize) -> (Vec<u8>, bool) {
905 let mut buf = Vec::with_capacity(n);
906 for _ in 0..n {
907 let b = file.read_byte();
908 if b == EOF_SENTINEL {
909 break;
910 }
911 buf.push(b as u8);
912 }
913 let nr = buf.len();
914 (buf, nr > 0)
915}
916
917fn g_read(
923 state: &mut LuaState,
924 p_rc: &Rc<RefCell<LStream>>,
925 first: i32,
926) -> Result<usize, LuaError> {
927 let nargs = (state.top() - first + 1).max(0);
933 let mut n = first;
934 let mut success = true;
935
936 {
937 let mut p = p_rc.borrow_mut();
938 let fh = p.file.as_mut().expect("open stream has no file handle");
939 fh.clear_error();
940 }
941
942 if nargs == 0 {
943 let (bytes, had) = {
944 let mut p = p_rc.borrow_mut();
945 let fh = p.file.as_deref_mut().expect("open stream has no file handle");
946 read_line(fh, true)
947 };
948 state.push_string(&bytes)?;
949 success = had;
950 n = first + 1;
951 } else {
952 state.ensure_stack((nargs as i32) + 20, "too many arguments")?;
953 let mut remaining = nargs;
954 while remaining > 0 && success {
955 if state.type_at(n) == LuaType::Number {
956 let l = state.check_arg_integer(n)? as usize;
957 if l == 0 {
958 let not_eof = {
959 let mut p = p_rc.borrow_mut();
960 let fh = p.file.as_deref_mut().expect("open stream has no file handle");
961 test_eof(fh)
962 };
963 state.push_string(b"")?;
964 success = not_eof;
965 } else {
966 let (bytes, had) = {
967 let mut p = p_rc.borrow_mut();
968 let fh = p.file.as_deref_mut().expect("open stream has no file handle");
969 read_chars(fh, l)
970 };
971 state.push_string(&bytes)?;
972 success = had;
973 }
974 } else {
975 let s: Vec<u8> = state.check_arg_string(n)?;
976 let pp: &[u8] = if s.first() == Some(&b'*') { &s[1..] } else { &s[..] };
977 match pp.first() {
978 Some(&b'n') => {
979 let bytes = {
980 let mut p = p_rc.borrow_mut();
981 let fh = p.file.as_deref_mut().expect("open stream has no file handle");
982 read_number_bytes(fh)
983 };
984 let pushed = state.string_to_number_push(&bytes)?;
985 if pushed != 0 {
986 success = true;
987 } else {
988 state.push(LuaValue::Nil);
989 success = false;
990 }
991 }
992 Some(&b'l') => {
993 let (bytes, had) = {
994 let mut p = p_rc.borrow_mut();
995 let fh = p.file.as_deref_mut().expect("open stream has no file handle");
996 read_line(fh, true)
997 };
998 state.push_string(&bytes)?;
999 success = had;
1000 }
1001 Some(&b'L') => {
1002 let (bytes, had) = {
1003 let mut p = p_rc.borrow_mut();
1004 let fh = p.file.as_deref_mut().expect("open stream has no file handle");
1005 read_line(fh, false)
1006 };
1007 state.push_string(&bytes)?;
1008 success = had;
1009 }
1010 Some(&b'a') => {
1011 let bytes = {
1012 let mut p = p_rc.borrow_mut();
1013 let fh = p.file.as_deref_mut().expect("open stream has no file handle");
1014 read_all(fh)
1015 };
1016 state.push_string(&bytes)?;
1017 success = true;
1018 }
1019 _ => {
1020 return Err(LuaError::arg_error(n, "invalid format"));
1021 }
1022 }
1023 }
1024 n += 1;
1025 remaining -= 1;
1026 }
1027 }
1028
1029 let has_err = {
1030 let p = p_rc.borrow();
1031 match p.file.as_deref() {
1032 Some(fh) => fh.has_error(),
1033 None => false,
1034 }
1035 };
1036 if has_err {
1037 let err = {
1038 let p = p_rc.borrow();
1039 match p.file.as_deref().and_then(|fh| fh.last_error_info()) {
1040 Some((code, _msg)) if code != 0 => io::Error::from_raw_os_error(code),
1041 Some((_code, msg)) => io::Error::new(io::ErrorKind::Other, msg),
1042 None => io::Error::new(io::ErrorKind::Other, "file read error"),
1043 }
1044 };
1045 return file_result(
1046 state,
1047 false,
1048 None,
1049 err,
1050 );
1051 }
1052
1053 if !success {
1054 state.pop_n(1);
1055 state.push(LuaValue::Nil);
1056 }
1057
1058 Ok((n - first) as usize)
1059}
1060
1061fn get_io_file_rc(state: &mut LuaState, key: &[u8]) -> Result<Rc<RefCell<LStream>>, LuaError> {
1066 state.registry_get(key)?;
1067 let ud_id = state
1068 .test_arg_userdata(-1, LUA_FILE_HANDLE)
1069 .map(|ud| ud.identity());
1070 state.pop_n(1);
1071 let label = &key[IO_PREFIX_LEN..];
1072 let id = ud_id.ok_or_else(|| {
1073 LuaError::runtime(format_args!(
1074 "default {} file is invalid",
1075 label.escape_ascii()
1076 ))
1077 })?;
1078 let rc = lookup_lstream(id).ok_or_else(|| {
1079 LuaError::runtime(format_args!(
1080 "default {} file is invalid",
1081 label.escape_ascii()
1082 ))
1083 })?;
1084 if rc.borrow().is_closed() {
1085 return Err(LuaError::runtime(format_args!(
1086 "default {} file is closed",
1087 label.escape_ascii()
1088 )));
1089 }
1090 Ok(rc)
1091}
1092
1093pub fn io_read(state: &mut LuaState) -> Result<usize, LuaError> {
1095 let p_rc = get_io_file_rc(state, IO_INPUT_KEY)?;
1096 g_read(state, &p_rc, 1)
1097}
1098
1099pub fn f_read(state: &mut LuaState) -> Result<usize, LuaError> {
1101 let p_rc = tofile(state)?;
1102 g_read(state, &p_rc, 2)
1103}
1104
1105#[expect(dead_code, reason = "ported stdlib helper; not yet wired into the runtime")]
1111fn g_write(
1112 state: &mut LuaState,
1113 file: &mut dyn LuaFileHandle,
1114 arg: i32,
1115) -> Result<usize, LuaError> {
1116 let nargs = state.top() - arg;
1117 let mut overall_ok = true;
1118
1119 for i in 0..nargs {
1120 let idx = arg + i;
1121 if state.type_at(idx) == LuaType::Number {
1122 let s = if state.is_integer(idx) {
1125 let ival = state.to_integer(idx).unwrap_or(0);
1126 format!("{}", ival)
1127 } else {
1128 let fval = state.to_number(idx).unwrap_or(0.0);
1129 format!("{:.14e}", fval)
1131 };
1132 match file.write_bytes(s.as_bytes()) {
1133 Ok(n) => overall_ok = overall_ok && n == s.len(),
1134 Err(_) => overall_ok = false,
1135 }
1136 } else {
1137 let s: Vec<u8> = state.check_arg_string(idx)?;
1138 match file.write_bytes(&s) {
1139 Ok(n) => overall_ok = overall_ok && n == s.len(),
1140 Err(_) => overall_ok = false,
1141 }
1142 }
1143 }
1144
1145 if overall_ok {
1146 Ok(1) } else {
1148 file_result(
1149 state,
1150 false,
1151 None,
1152 io::Error::new(io::ErrorKind::Other, "write error"),
1153 )
1154 }
1155}
1156
1157pub fn io_write(state: &mut LuaState) -> Result<usize, LuaError> {
1167 let n = state.top();
1169 let mut chunks: Vec<Vec<u8>> = Vec::with_capacity(n as usize);
1170 for i in 1..=(n as i32) {
1171 if state.type_at(i) == LuaType::Number {
1172 let s = if state.is_integer(i) {
1173 let ival = state.to_integer(i).unwrap_or(0);
1174 format!("{}", ival).into_bytes()
1175 } else {
1176 let fval = state.to_number(i).unwrap_or(0.0);
1177 format!("{:.14e}", fval).into_bytes()
1179 };
1180 chunks.push(s);
1181 } else {
1182 let bytes: Vec<u8> = state.check_arg_string(i)?;
1183 chunks.push(bytes);
1184 }
1185 }
1186
1187 let p_rc = get_io_file_rc(state, IO_OUTPUT_KEY)?;
1190 {
1191 let mut p = p_rc.borrow_mut();
1192 let fh = p.file.as_mut().expect("open stream has no file handle");
1193 for chunk in &chunks {
1194 fh.write_bytes(chunk).map_err(|e| {
1195 LuaError::runtime(format_args!("io.write: {}", e))
1196 })?;
1197 }
1198 }
1199 state.registry_get(IO_OUTPUT_KEY)?;
1200 Ok(1)
1201}
1202
1203pub fn f_write(state: &mut LuaState) -> Result<usize, LuaError> {
1205 let p_rc = tofile(state)?;
1206
1207 let n = state.top();
1209 let mut chunks: Vec<Vec<u8>> = Vec::with_capacity(n.saturating_sub(1) as usize);
1210 for i in 2..=(n as i32) {
1211 if state.type_at(i) == LuaType::Number {
1212 let s = if state.is_integer(i) {
1213 let ival = state.to_integer(i).unwrap_or(0);
1214 format!("{}", ival).into_bytes()
1215 } else {
1216 let fval = state.to_number(i).unwrap_or(0.0);
1217 format!("{:.14e}", fval).into_bytes()
1219 };
1220 chunks.push(s);
1221 } else {
1222 let bytes: Vec<u8> = state.check_arg_string(i)?;
1223 chunks.push(bytes);
1224 }
1225 }
1226
1227 let result: io::Result<()> = {
1229 let mut p = p_rc.borrow_mut();
1230 let fh = p.file.as_mut().expect("open stream has no file handle");
1231 let mut r: io::Result<()> = Ok(());
1232 for chunk in &chunks {
1233 match fh.write_bytes(chunk) {
1234 Ok(written) if written == chunk.len() => {}
1235 Ok(_) => {
1236 r = Err(io::Error::new(io::ErrorKind::Other, "short write"));
1237 break;
1238 }
1239 Err(e) => {
1240 r = Err(e);
1241 break;
1242 }
1243 }
1244 }
1245 r
1246 };
1247
1248 match result {
1250 Ok(()) => {
1251 state.push_value_at(1)?;
1252 Ok(1)
1253 }
1254 Err(e) => file_result(state, false, None, e),
1255 }
1256}
1257
1258pub fn f_seek(state: &mut LuaState) -> Result<usize, LuaError> {
1262 static MODE_NAMES: &[&[u8]] = &[b"set", b"cur", b"end"];
1263
1264 let p_rc = tofile(state)?;
1265 let op = state.check_arg_option(2, Some(b"cur"), MODE_NAMES)?;
1266 let p3: i64 = state.opt_arg_integer(3, 0)?;
1267
1268 let seek_pos = match op {
1269 0 => SeekFrom::Start(p3 as u64),
1270 1 => SeekFrom::Current(p3),
1271 2 => SeekFrom::End(p3),
1272 _ => unreachable!(),
1273 };
1274
1275 let result = {
1276 let mut p = p_rc.borrow_mut();
1277 let fh = p.file.as_mut().expect("open stream has no file handle");
1278 fh.seek(seek_pos)
1279 };
1280 match result {
1281 Ok(pos) => {
1282 state.push(LuaValue::Int(pos as i64));
1283 Ok(1)
1284 }
1285 Err(e) => file_result(state, false, None, e),
1286 }
1287}
1288
1289pub fn f_setvbuf(state: &mut LuaState) -> Result<usize, LuaError> {
1291 static MODE_NAMES: &[&[u8]] = &[b"no", b"full", b"line"];
1292
1293 let p_rc = tofile(state)?;
1294 let op = state.check_arg_option(2, None, MODE_NAMES)?;
1295 let sz: i64 = state.opt_arg_integer(3, LUAL_BUFFER_SIZE as i64)?;
1296 let mode = match op {
1297 0 => BufMode::No,
1298 1 => BufMode::Full,
1299 2 => BufMode::Line,
1300 _ => unreachable!(),
1301 };
1302 let result = {
1303 let mut p = p_rc.borrow_mut();
1304 let fh = p.file.as_mut().expect("open stream has no file handle");
1305 let mode_index = match mode {
1306 BufMode::No => 0,
1307 BufMode::Full => 1,
1308 BufMode::Line => 2,
1309 };
1310 fh.set_buf_mode(mode_index, sz.max(0) as usize)
1311 };
1312 match result {
1313 Ok(()) => file_result(state, true, None, io::Error::last_os_error()),
1314 Err(e) => file_result(state, false, None, e),
1315 }
1316}
1317
1318pub fn io_flush(state: &mut LuaState) -> Result<usize, LuaError> {
1320 let ud_id: Option<usize> = {
1321 state.registry_get(IO_OUTPUT_KEY)?;
1322 let id = state
1323 .test_arg_userdata(-1, LUA_FILE_HANDLE)
1324 .map(|ud| ud.identity());
1325 state.pop_n(1);
1326 id
1327 };
1328 if let Some(id) = ud_id {
1329 if let Some(rc) = lookup_lstream(id) {
1330 let result = {
1331 let mut p = rc.borrow_mut();
1332 if p.is_closed() {
1333 return Err(LuaError::runtime(format_args!(
1334 "default output file is closed"
1335 )));
1336 }
1337 let fh = p.file.as_deref_mut().expect("open stream has no file handle");
1338 fh.flush()
1339 };
1340 return match result {
1341 Ok(()) => {
1342 state.push(LuaValue::Bool(true));
1343 Ok(1)
1344 }
1345 Err(e) => file_result(state, false, None, e),
1346 };
1347 }
1348 }
1349 state.push(LuaValue::Bool(true));
1351 Ok(1)
1352}
1353
1354pub fn f_flush(state: &mut LuaState) -> Result<usize, LuaError> {
1356 let p_rc = tofile(state)?;
1357 let result = {
1358 let mut p = p_rc.borrow_mut();
1359 let fh = p.file.as_mut().expect("open stream has no file handle");
1360 fh.flush()
1361 };
1362 match result {
1363 Ok(()) => {
1364 state.push(LuaValue::Bool(true));
1365 Ok(1)
1366 }
1367 Err(e) => file_result(state, false, None, e),
1368 }
1369}
1370
1371fn aux_lines(state: &mut LuaState, toclose: bool) -> Result<(), LuaError> {
1381 let n = state.top() - 1;
1384 if n > MAX_ARG_LINE as i32 {
1385 return Err(LuaError::arg_error(
1386 MAX_ARG_LINE as i32 + 2,
1387 "too many arguments",
1388 ));
1389 }
1390 state.push_value_at(1)?;
1391 state.push(LuaValue::Int(n as i64));
1392 state.push(LuaValue::Bool(toclose));
1393 state.rotate(2, 3)?;
1394 state.push_c_closure(io_readline, (3 + n) as i32)?;
1395 Ok(())
1396}
1397
1398pub fn f_lines(state: &mut LuaState) -> Result<usize, LuaError> {
1400 let _ = tofile(state)?; aux_lines(state, false)?;
1402 Ok(1)
1403}
1404
1405pub fn io_lines(state: &mut LuaState) -> Result<usize, LuaError> {
1407 if state.type_at(1) == LuaType::None {
1408 state.push(LuaValue::Nil);
1409 }
1410 let toclose = if state.type_at(1) == LuaType::Nil {
1411 state.registry_get(IO_INPUT_KEY)?;
1412 state.replace(1)?;
1413 let _ = tofile(state)?;
1414 false
1415 } else {
1416 let filename = state.check_arg_string(1)?;
1417 opencheck(state, &filename, b"r")?;
1418 state.replace(1)?;
1419 true
1420 };
1421
1422 aux_lines(state, toclose)?;
1423
1424 if toclose {
1425 state.push(LuaValue::Nil); state.push(LuaValue::Nil); state.push_value_at(1)?; Ok(4)
1429 } else {
1430 Ok(1)
1431 }
1432}
1433
1434fn io_readline(state: &mut LuaState) -> Result<usize, LuaError> {
1442 let n = match state.value_at(crate::state_stub::upvalue_index(2)) {
1443 LuaValue::Int(i) => i as usize,
1444 _ => 0,
1445 };
1446
1447 let p_rc = lstream_from_upvalue(state, 1)?;
1448
1449 if p_rc.borrow().is_closed() {
1450 return Err(LuaError::runtime(format_args!("file is already closed")));
1451 }
1452
1453 lua_vm::api::set_top(state, 1)?;
1454 state.ensure_stack(n as i32, "too many arguments")?;
1455
1456 for i in 1..=n {
1457 let uv = state.value_at(crate::state_stub::upvalue_index(3 + i as i32));
1458 state.push(uv);
1459 }
1460
1461 let result_n: usize = g_read(state, &p_rc, 2)?;
1462
1463 debug_assert!(result_n > 0, "g_read should return at least one value");
1464
1465 let top = state.top_idx().get() as i32;
1466 let first_result_idx = top - result_n as i32;
1467 let first_truthy = !matches!(
1468 state.stack_at(first_result_idx),
1469 LuaValue::Nil | LuaValue::Bool(false)
1470 );
1471 if first_truthy {
1472 return Ok(result_n);
1473 }
1474
1475 if result_n > 1 {
1476 let err_val = state.stack_at(first_result_idx + 1).clone();
1477 return Err(LuaError::from_value(err_val));
1478 }
1479
1480 let toclose = !matches!(
1481 state.value_at(crate::state_stub::upvalue_index(3)),
1482 LuaValue::Nil | LuaValue::Bool(false)
1483 );
1484 if toclose {
1485 lua_vm::api::set_top(state, 0)?;
1486 state.push_upvalue(1)?;
1487 aux_close(state)?;
1488 }
1489
1490 Ok(0)
1491}
1492
1493fn create_meta(state: &mut LuaState) -> Result<(), LuaError> {
1497 state.new_metatable(LUA_FILE_HANDLE)?;
1498 state.set_funcs(FILE_METAMETHODS, 0)?;
1499 state.new_lib_table(FILE_METHODS)?;
1500 state.set_funcs(FILE_METHODS, 0)?;
1501 state.set_field(-2, b"__index")?;
1502 state.pop_n(1);
1503 Ok(())
1504}
1505
1506fn create_std_file(
1508 state: &mut LuaState,
1509 std_kind: StdFileKind,
1510 registry_key: Option<&[u8]>,
1511 field_name: &[u8],
1512) -> Result<(), LuaError> {
1513 let cell = new_pre_file(state)?;
1514 {
1515 let mut p = cell.borrow_mut();
1516 p.file = Some(Box::new(StdStreamHandle::new(std_kind)));
1517 p.close_fn = Some(io_noclose);
1518 }
1519 if let Some(key) = registry_key {
1520 state.push_value_at(-1)?;
1521 state.registry_set(key)?;
1522 }
1523 state.set_field(-2, field_name)?;
1524 Ok(())
1525}
1526
1527pub fn luaopen_io(state: &mut LuaState) -> Result<usize, LuaError> {
1529 state.new_lib(IO_LIB)?;
1530 create_meta(state)?;
1531 create_std_file(state, StdFileKind::Stdin, Some(IO_INPUT_KEY), b"stdin")?;
1532 create_std_file(state, StdFileKind::Stdout, Some(IO_OUTPUT_KEY), b"stdout")?;
1533 create_std_file(state, StdFileKind::Stderr, None, b"stderr")?;
1534 Ok(1)
1535}
1536
1537