1use std::cell::RefCell;
32use std::collections::HashMap;
33use std::io::{self, SeekFrom};
34use std::rc::Rc;
35
36use crate::state_stub::{LuaState, LuaStateStubExt as _};
37use lua_types::{LuaError, LuaFileHandle, LuaType, LuaValue};
38use lua_vm::state::{InputHook, OutputHook};
39
40thread_local! {
41 static LSTREAM_REGISTRY: RefCell<HashMap<usize, Rc<RefCell<LStream>>>>
47 = RefCell::new(HashMap::new());
48}
49
50fn register_lstream(ud_id: usize, lstream: LStream) -> Rc<RefCell<LStream>> {
51 let cell = Rc::new(RefCell::new(lstream));
52 LSTREAM_REGISTRY.with(|reg| {
53 reg.borrow_mut().insert(ud_id, cell.clone());
54 });
55 cell
56}
57
58fn lookup_lstream(ud_id: usize) -> Option<Rc<RefCell<LStream>>> {
59 LSTREAM_REGISTRY.with(|reg| reg.borrow().get(&ud_id).cloned())
60}
61
62pub const LUA_FILE_HANDLE: &[u8] = b"FILE*";
66
67const IO_INPUT_KEY: &[u8] = b"_IO_input";
69
70const IO_OUTPUT_KEY: &[u8] = b"_IO_output";
72
73const IO_PREFIX_LEN: usize = 4;
75
76const MAX_ARG_LINE: usize = 250;
78
79const L_MAX_LEN_NUM: usize = 200;
81
82const EOF_SENTINEL: i32 = -1;
84
85const LUAL_BUFFER_SIZE: usize = 8192;
87
88pub trait LuaFileOps: LuaFileHandle {
97 fn set_buf_mode(&mut self, mode: BufMode, size: usize) -> io::Result<()>;
99}
100
101#[derive(Debug, Clone, Copy, PartialEq, Eq)]
105pub enum SeekWhence {
106 Set,
107 Cur,
108 End,
109}
110
111#[derive(Debug, Clone, Copy, PartialEq, Eq)]
113pub enum BufMode {
114 No,
115 Full,
116 Line,
117}
118
119pub enum StdFileKind {
121 Stdin,
122 Stdout,
123 Stderr,
124}
125
126pub struct LStream {
134 pub file: Option<Box<dyn LuaFileHandle>>,
138 pub close_fn: Option<fn(&mut LuaState) -> Result<usize, LuaError>>,
140}
141
142impl LStream {
143 pub fn is_closed(&self) -> bool {
145 self.close_fn.is_none()
146 }
147}
148
149struct StdStreamHandle {
155 kind: StdFileKind,
156 input_hook: Option<InputHook>,
157 output_hook: Option<OutputHook>,
158 unread: Option<u8>,
159}
160
161impl LuaFileHandle for StdStreamHandle {
162 fn read_byte(&mut self) -> i32 {
163 if let Some(byte) = self.unread.take() {
164 return byte as i32;
165 }
166 match self.kind {
167 StdFileKind::Stdin => {
168 if let Some(read_fn) = self.input_hook {
169 let mut buf = [0u8; 1];
170 return match read_fn(&mut buf) {
171 Ok(1) => buf[0] as i32,
172 _ => EOF_SENTINEL,
173 };
174 }
175
176 #[cfg(all(target_arch = "wasm32", target_os = "unknown"))]
177 {
178 EOF_SENTINEL
179 }
180
181 #[cfg(not(all(target_arch = "wasm32", target_os = "unknown")))]
182 {
183 use std::io::Read;
184 let mut buf = [0u8; 1];
185 match std::io::stdin().read(&mut buf) {
186 Ok(1) => buf[0] as i32,
187 _ => EOF_SENTINEL,
188 }
189 }
190 }
191 _ => EOF_SENTINEL,
192 }
193 }
194 fn unread_byte(&mut self, byte: i32) {
195 if (0..=u8::MAX as i32).contains(&byte) {
196 self.unread = Some(byte as u8);
197 }
198 }
199 fn write_bytes(&mut self, data: &[u8]) -> io::Result<usize> {
200 if let Some(write_fn) = self.output_hook {
201 write_fn(data)?;
202 return Ok(data.len());
203 }
204
205 #[cfg(all(target_arch = "wasm32", target_os = "unknown"))]
206 {
207 let _ = data;
208 return Err(io::Error::new(
209 io::ErrorKind::Unsupported,
210 "standard output not available in this host",
211 ));
212 }
213
214 #[cfg(not(all(target_arch = "wasm32", target_os = "unknown")))]
215 {
216 use std::io::Write;
217 match self.kind {
218 StdFileKind::Stderr => {
219 std::io::stderr().write_all(data)?;
220 Ok(data.len())
221 }
222 _ => {
223 std::io::stdout().write_all(data)?;
224 Ok(data.len())
225 }
226 }
227 }
228 }
229 fn flush(&mut self) -> io::Result<()> {
230 if self.output_hook.is_some() {
231 return Ok(());
232 }
233
234 #[cfg(all(target_arch = "wasm32", target_os = "unknown"))]
235 {
236 return Err(io::Error::new(
237 io::ErrorKind::Unsupported,
238 "standard output not available in this host",
239 ));
240 }
241
242 #[cfg(not(all(target_arch = "wasm32", target_os = "unknown")))]
243 {
244 use std::io::Write;
245 match self.kind {
246 StdFileKind::Stderr => std::io::stderr().flush(),
247 _ => std::io::stdout().flush(),
248 }
249 }
250 }
251 fn seek(&mut self, _pos: SeekFrom) -> io::Result<u64> {
252 Err(io::Error::new(io::ErrorKind::Unsupported, "stdio seek"))
253 }
254 fn tell(&mut self) -> io::Result<u64> {
255 Err(io::Error::new(io::ErrorKind::Unsupported, "stdio tell"))
256 }
257 fn clear_error(&mut self) {}
258 fn has_error(&self) -> bool {
259 false
260 }
261}
262
263impl LuaFileOps for StdStreamHandle {
264 fn set_buf_mode(&mut self, _mode: BufMode, _size: usize) -> io::Result<()> {
265 Ok(())
266 }
267}
268
269impl StdStreamHandle {
270 fn new(
271 kind: StdFileKind,
272 input_hook: Option<InputHook>,
273 output_hook: Option<OutputHook>,
274 ) -> Self {
275 StdStreamHandle {
276 kind,
277 input_hook,
278 output_hook,
279 unread: None,
280 }
281 }
282}
283
284struct ReadNumState {
286 current: i32,
288 count: usize,
290 buf: [u8; L_MAX_LEN_NUM + 1],
292}
293
294impl ReadNumState {
295 fn new(first_byte: i32) -> Self {
296 ReadNumState {
297 current: first_byte,
298 count: 0,
299 buf: [0u8; L_MAX_LEN_NUM + 1],
300 }
301 }
302
303 fn advance(&mut self, file: &mut dyn LuaFileHandle) -> bool {
306 if self.count >= L_MAX_LEN_NUM {
307 self.buf[0] = 0;
308 return false;
309 }
310 self.buf[self.count] = self.current as u8;
311 self.count += 1;
312 self.current = file.read_byte();
313 true
314 }
315
316 fn try2(&mut self, file: &mut dyn LuaFileHandle, set: [u8; 2]) -> bool {
318 if self.current == set[0] as i32 || self.current == set[1] as i32 {
319 self.advance(file)
320 } else {
321 false
322 }
323 }
324
325 fn read_digits(&mut self, file: &mut dyn LuaFileHandle, hex: bool) -> usize {
327 let mut count = 0usize;
328 loop {
329 let is_digit = if hex {
330 (self.current as u8).is_ascii_hexdigit()
331 } else {
332 (self.current as u8).is_ascii_digit()
333 };
334 if !is_digit || self.current == EOF_SENTINEL {
335 break;
336 }
337 if !self.advance(file) {
338 break;
339 }
340 count += 1;
341 }
342 count
343 }
344
345 fn as_bytes(&self) -> &[u8] {
347 &self.buf[..self.count]
348 }
349}
350
351pub const IO_LIB: &[(&[u8], fn(&mut LuaState) -> Result<usize, LuaError>)] = &[
355 (b"close", io_close),
356 (b"flush", io_flush),
357 (b"input", io_input),
358 (b"lines", io_lines),
359 (b"open", io_open),
360 (b"output", io_output),
361 (b"popen", io_popen),
362 (b"read", io_read),
363 (b"tmpfile", io_tmpfile),
364 (b"type", io_type),
365 (b"write", io_write),
366];
367
368pub const FILE_METHODS: &[(&[u8], fn(&mut LuaState) -> Result<usize, LuaError>)] = &[
370 (b"read", f_read),
371 (b"write", f_write),
372 (b"lines", f_lines),
373 (b"flush", f_flush),
374 (b"seek", f_seek),
375 (b"close", f_close),
376 (b"setvbuf", f_setvbuf),
377];
378
379pub const FILE_METAMETHODS: &[(&[u8], fn(&mut LuaState) -> Result<usize, LuaError>)] = &[
381 (b"__gc", f_gc),
382 (b"__close", f_gc),
383 (b"__tostring", f_tostring),
384];
385
386fn check_mode(mode: &[u8]) -> bool {
392 if mode.is_empty() {
393 return false;
394 }
395 let mut idx = 0usize;
396 if !matches!(mode[idx], b'r' | b'w' | b'a') {
397 return false;
398 }
399 idx += 1;
400 if idx < mode.len() && mode[idx] == b'+' {
401 idx += 1;
402 }
403 mode[idx..].iter().all(|&b| b == b'b')
404}
405
406fn check_mode_popen(mode: &[u8]) -> bool {
408 matches!(mode, b"r" | b"w")
409}
410
411fn file_result(
420 state: &mut LuaState,
421 success: bool,
422 fname: Option<&[u8]>,
423 os_err: io::Error,
424) -> Result<usize, LuaError> {
425 if success {
426 state.push(LuaValue::Bool(true));
427 return Ok(1);
428 }
429 state.push(LuaValue::Nil);
430 let msg = os_err.to_string();
431 match fname {
432 Some(name) => {
433 let mut s = Vec::with_capacity(name.len() + 2 + msg.len());
434 s.extend_from_slice(name);
435 s.extend_from_slice(b": ");
436 s.extend_from_slice(msg.as_bytes());
437 state.push_string(&s)?;
438 }
439 None => {
440 state.push_string(msg.as_bytes())?;
441 }
442 }
443 let errno_code = os_err.raw_os_error().unwrap_or(0) as i64;
444 state.push(LuaValue::Int(errno_code));
445 Ok(3)
446}
447
448fn exec_result(state: &mut LuaState, stat: i32) -> Result<usize, LuaError> {
455 if stat == 0 {
456 state.push(LuaValue::Bool(true));
457 Ok(1)
458 } else {
459 state.push(LuaValue::Bool(false));
460 state.push_string(b"exit")?;
461 state.push(LuaValue::Int(stat as i64));
462 Ok(3)
463 }
464}
465
466fn get_lstream(state: &mut LuaState) -> Result<Rc<RefCell<LStream>>, LuaError> {
473 let ud = state.check_arg_userdata(1, LUA_FILE_HANDLE)?;
474 lookup_lstream(ud.identity())
475 .ok_or_else(|| LuaError::runtime(format_args!("invalid file handle")))
476}
477
478fn lstream_from_upvalue(state: &mut LuaState, idx: i32) -> Result<Rc<RefCell<LStream>>, LuaError> {
485 let v = state.value_at(crate::state_stub::upvalue_index(idx));
486 let ud_id = match v {
487 LuaValue::UserData(ud) => ud.identity(),
488 _ => {
489 return Err(LuaError::runtime(format_args!(
490 "invalid file handle in upvalue {}",
491 idx
492 )));
493 }
494 };
495 lookup_lstream(ud_id)
496 .ok_or_else(|| LuaError::runtime(format_args!("invalid file handle in upvalue {}", idx)))
497}
498
499fn tofile(state: &mut LuaState) -> Result<Rc<RefCell<LStream>>, LuaError> {
505 let p_rc = get_lstream(state)?;
506 let closed = {
507 let p = p_rc.borrow();
508 debug_assert!(p.is_closed() || p.file.is_some());
509 p.is_closed()
510 };
511 if closed {
512 return Err(lua_vm::debug::c_api_runtime(
513 state,
514 b"attempt to use a closed file".to_vec(),
515 ));
516 }
517 Ok(p_rc)
518}
519
520fn new_pre_file(state: &mut LuaState) -> Result<Rc<RefCell<LStream>>, LuaError> {
527 let ud = state.new_userdata_typed(LUA_FILE_HANDLE, std::mem::size_of::<LStream>(), 0)?;
528 state.set_metatable_by_name(LUA_FILE_HANDLE)?;
529 let cell = register_lstream(
530 ud.identity(),
531 LStream {
532 file: None,
533 close_fn: None,
534 },
535 );
536 Ok(cell)
537}
538
539fn new_file(state: &mut LuaState) -> Result<Rc<RefCell<LStream>>, LuaError> {
541 let cell = new_pre_file(state)?;
542 cell.borrow_mut().close_fn = Some(io_fclose);
543 Ok(cell)
544}
545
546fn opencheck(state: &mut LuaState, fname: &[u8], mode: &[u8]) -> Result<(), LuaError> {
551 let hook = state.global().file_open_hook;
552 let fh = match hook {
553 Some(open_fn) => open_fn(fname, mode).map_err(|e| {
554 LuaError::runtime(format_args!(
555 "cannot open file '{}' ({})",
556 fname.escape_ascii(),
557 match e.message_bytes() {
558 Some(b) => String::from_utf8_lossy(b).into_owned(),
559 None => format!("{:?}", &e),
560 }
561 ))
562 })?,
563 None => {
564 return Err(LuaError::runtime(format_args!(
565 "cannot open file '{}' (no filesystem hook registered)",
566 fname.escape_ascii()
567 )));
568 }
569 };
570 let cell = new_file(state)?;
571 cell.borrow_mut().file = Some(fh);
572 Ok(())
573}
574
575fn io_fclose(state: &mut LuaState) -> Result<usize, LuaError> {
584 let p_rc = get_lstream(state)?;
585 let _closed = p_rc.borrow_mut().file.take();
586 state.push(LuaValue::Bool(true));
587 Ok(1)
588}
589
590fn io_pclose(state: &mut LuaState) -> Result<usize, LuaError> {
595 let p_rc = get_lstream(state)?;
596 let _closed = p_rc.borrow_mut().file.take();
597 exec_result(state, 0)
598}
599
600fn io_noclose(state: &mut LuaState) -> Result<usize, LuaError> {
605 let p_rc = get_lstream(state)?;
606 p_rc.borrow_mut().close_fn = Some(io_noclose);
607 state.push(LuaValue::Bool(false));
608 state.push_string(b"cannot close standard file")?;
609 Ok(2)
610}
611
612fn aux_close(state: &mut LuaState) -> Result<usize, LuaError> {
614 let p_rc = get_lstream(state)?;
615 let cf = p_rc.borrow_mut().close_fn.take().ok_or_else(|| {
616 LuaError::runtime(format_args!("attempt to close an already-closed file"))
617 })?;
618 cf(state)
619}
620
621pub fn io_type(state: &mut LuaState) -> Result<usize, LuaError> {
631 state.check_arg_any(1)?;
632 let maybe_userdata = state.test_arg_userdata(1, LUA_FILE_HANDLE);
633 match maybe_userdata {
634 None => {
635 state.push(LuaValue::Nil);
636 }
637 Some(ud) => {
638 let is_closed = match lookup_lstream(ud.identity()) {
639 Some(rc) => rc.borrow().is_closed(),
640 None => true,
641 };
642 if is_closed {
643 state.push_string(b"closed file")?;
644 } else {
645 state.push_string(b"file")?;
646 }
647 }
648 }
649 Ok(1)
650}
651
652fn f_tostring(state: &mut LuaState) -> Result<usize, LuaError> {
661 let p_rc = get_lstream(state)?;
662 let closed = p_rc.borrow().is_closed();
663 if closed {
664 state.push_string(b"file (closed)")?;
665 } else {
666 state.push_string(b"file (0x?)")?;
667 }
668 Ok(1)
669}
670
671fn f_close(state: &mut LuaState) -> Result<usize, LuaError> {
675 let _ = tofile(state)?; aux_close(state)
677}
678
679pub fn io_close(state: &mut LuaState) -> Result<usize, LuaError> {
681 if state.type_at(1) == LuaType::None {
685 state.registry_get(IO_OUTPUT_KEY)?;
686 }
687 f_close(state)
688}
689
690fn f_gc(state: &mut LuaState) -> Result<usize, LuaError> {
692 let p_rc = get_lstream(state)?;
693 let needs_close = {
694 let p = p_rc.borrow();
695 !p.is_closed() && p.file.is_some()
696 };
697 if needs_close {
698 let _ = aux_close(state);
700 }
701 Ok(0)
702}
703
704pub fn io_open(state: &mut LuaState) -> Result<usize, LuaError> {
711 let filename: Vec<u8> = state.check_arg_string(1)?;
712 let mode: Vec<u8> = state.opt_arg_string(2, b"r")?;
713 if !check_mode(&mode) {
714 return Err(lua_vm::debug::arg_error_impl(state, 2, b"invalid mode"));
715 }
716 let hook = state.global().file_open_hook;
717 match hook {
718 Some(open_fn) => match open_fn(&filename, &mode) {
719 Ok(fh) => {
720 let cell = new_file(state)?;
721 cell.borrow_mut().file = Some(fh);
722 Ok(1)
723 }
724 Err(e) => {
725 let os_err = io::Error::new(
726 io::ErrorKind::Other,
727 match e.message_bytes() {
728 Some(b) => String::from_utf8_lossy(b).into_owned(),
729 None => format!("{:?}", &e),
730 },
731 );
732 file_result(state, false, Some(&filename), os_err)
733 }
734 },
735 None => {
736 let os_err =
737 io::Error::new(io::ErrorKind::Unsupported, "no filesystem hook registered");
738 file_result(state, false, Some(&filename), os_err)
739 }
740 }
741}
742
743pub fn io_popen(state: &mut LuaState) -> Result<usize, LuaError> {
752 let filename: Vec<u8> = state.check_arg_string(1)?;
753 let mode: Vec<u8> = state.opt_arg_string(2, b"r")?;
754 if !check_mode_popen(&mode) {
755 return Err(lua_vm::debug::arg_error_impl(state, 2, b"invalid mode"));
756 }
757 let hook = state.global().popen_hook;
758 match hook {
759 Some(spawn_fn) => match spawn_fn(&filename, &mode) {
760 Ok(fh) => {
761 let cell = new_pre_file(state)?;
762 let mut p = cell.borrow_mut();
763 p.file = Some(fh);
764 p.close_fn = Some(io_pclose);
765 drop(p);
766 Ok(1)
767 }
768 Err(e) => {
769 let os_err = io::Error::new(
770 io::ErrorKind::Other,
771 match e.message_bytes() {
772 Some(b) => String::from_utf8_lossy(b).into_owned(),
773 None => format!("{:?}", &e),
774 },
775 );
776 file_result(state, false, Some(&filename), os_err)
777 }
778 },
779 None => {
780 let os_err = io::Error::new(
781 io::ErrorKind::Unsupported,
782 "popen not enabled in this build",
783 );
784 file_result(state, false, Some(&filename), os_err)
785 }
786 }
787}
788
789fn native_temp_name() -> io::Result<Vec<u8>> {
790 #[cfg(all(target_arch = "wasm32", target_os = "unknown"))]
791 {
792 return Err(io::Error::new(
793 io::ErrorKind::Unsupported,
794 "temporary files not available in this host",
795 ));
796 }
797
798 #[cfg(not(all(target_arch = "wasm32", target_os = "unknown")))]
799 {
800 let mut path = std::env::temp_dir().to_string_lossy().as_bytes().to_vec();
801 if path.last().copied() != Some(b'/') && path.last().copied() != Some(b'\\') {
802 path.push(b'/');
803 }
804 let unique = format!(
805 "lua_tmpfile_{}_{}",
806 std::process::id(),
807 std::time::SystemTime::now()
808 .duration_since(std::time::UNIX_EPOCH)
809 .map(|d| d.as_nanos())
810 .unwrap_or(0)
811 );
812 path.extend_from_slice(unique.as_bytes());
813 Ok(path)
814 }
815}
816
817pub fn io_tmpfile(state: &mut LuaState) -> Result<usize, LuaError> {
819 let hook = state.global().file_open_hook;
820 let Some(open_fn) = hook else {
821 let os_err = io::Error::new(io::ErrorKind::Unsupported, "no filesystem hook registered");
822 return file_result(state, false, None, os_err);
823 };
824
825 let temp_name_hook = state.global().temp_name_hook;
826 let path = match temp_name_hook {
827 Some(temp_fn) => match temp_fn() {
828 Ok(path) => path,
829 Err(e) => {
830 let msg = match e.message_bytes() {
831 Some(b) => String::from_utf8_lossy(b).into_owned(),
832 None => format!("{:?}", &e),
833 };
834 return file_result(
835 state,
836 false,
837 None,
838 io::Error::new(io::ErrorKind::Unsupported, msg),
839 );
840 }
841 },
842 None => match native_temp_name() {
843 Ok(path) => path,
844 Err(e) => return file_result(state, false, None, e),
845 },
846 };
847
848 match open_fn(&path, b"w+b") {
849 Ok(fh) => {
850 let cell = new_file(state)?;
851 cell.borrow_mut().file = Some(fh);
852 Ok(1)
853 }
854 Err(e) => {
855 let os_err = io::Error::new(
856 io::ErrorKind::Other,
857 match e.message_bytes() {
858 Some(b) => String::from_utf8_lossy(b).into_owned(),
859 None => format!("{:?}", &e),
860 },
861 );
862 file_result(state, false, None, os_err)
863 }
864 }
865}
866
867fn g_iofile(state: &mut LuaState, key: &[u8], mode: &[u8]) -> Result<usize, LuaError> {
871 if !matches!(state.type_at(1), LuaType::None | LuaType::Nil) {
872 if state.type_at(1) == LuaType::String {
873 let filename = state.check_arg_string(1)?;
874 opencheck(state, &filename, mode)?;
875 } else {
876 let _ = tofile(state)?;
877 state.push_value_at(1)?;
878 }
879 state.registry_set(key)?;
880 }
881 state.registry_get(key)?;
882 Ok(1)
883}
884
885pub fn io_input(state: &mut LuaState) -> Result<usize, LuaError> {
887 g_iofile(state, IO_INPUT_KEY, b"r")
888}
889
890pub fn io_output(state: &mut LuaState) -> Result<usize, LuaError> {
892 g_iofile(state, IO_OUTPUT_KEY, b"w")
893}
894
895fn read_number_bytes(file: &mut dyn LuaFileHandle) -> Vec<u8> {
903 let first = loop {
904 let b = file.read_byte();
905 if b == EOF_SENTINEL || !(b as u8).is_ascii_whitespace() {
906 break b;
907 }
908 };
909
910 let mut rn = ReadNumState::new(first);
911
912 rn.try2(file, [b'-', b'+']);
913
914 let mut count: usize = 0;
915 let hex = if rn.try2(file, [b'0', b'0']) {
916 if rn.try2(file, [b'x', b'X']) {
917 true
918 } else {
919 count = 1;
920 false
921 }
922 } else {
923 false
924 };
925
926 count += rn.read_digits(file, hex);
927
928 let dec_point = b'.';
929 if rn.try2(file, [dec_point, b'.']) {
930 count += rn.read_digits(file, hex);
931 }
932
933 if count > 0 {
934 let exp_chars = if hex { [b'p', b'P'] } else { [b'e', b'E'] };
935 if rn.try2(file, exp_chars) {
936 rn.try2(file, [b'-', b'+']);
937 rn.read_digits(file, false);
938 }
939 }
940
941 file.unread_byte(rn.current);
942 rn.as_bytes().to_vec()
943}
944
945fn test_eof(file: &mut dyn LuaFileHandle) -> bool {
948 let c = file.read_byte();
949 if c != EOF_SENTINEL {
950 file.unread_byte(c);
951 }
952 c != EOF_SENTINEL
953}
954
955fn read_line(file: &mut dyn LuaFileHandle, chop: bool) -> (Vec<u8>, bool) {
963 let mut buf: Vec<u8> = Vec::new();
964 let mut c: i32;
965
966 'outer: loop {
967 for _ in 0..LUAL_BUFFER_SIZE {
968 c = file.read_byte();
969 if c == EOF_SENTINEL || c == b'\n' as i32 {
970 break 'outer;
971 }
972 buf.push(c as u8);
973 }
974 }
975
976 if !chop && c == b'\n' as i32 {
977 buf.push(b'\n');
978 }
979
980 let had_content = c == b'\n' as i32 || !buf.is_empty();
981 (buf, had_content)
982}
983
984fn read_all(file: &mut dyn LuaFileHandle) -> Vec<u8> {
990 let mut buf: Vec<u8> = Vec::new();
991 loop {
992 let mut chunk_read = 0usize;
993 for _ in 0..LUAL_BUFFER_SIZE {
994 let b = file.read_byte();
995 if b == EOF_SENTINEL {
996 break;
997 }
998 buf.push(b as u8);
999 chunk_read += 1;
1000 }
1001 if chunk_read < LUAL_BUFFER_SIZE {
1002 break;
1003 }
1004 }
1005 buf
1006}
1007
1008fn read_chars(file: &mut dyn LuaFileHandle, n: usize) -> (Vec<u8>, bool) {
1010 let mut buf = Vec::with_capacity(n);
1011 for _ in 0..n {
1012 let b = file.read_byte();
1013 if b == EOF_SENTINEL {
1014 break;
1015 }
1016 buf.push(b as u8);
1017 }
1018 let nr = buf.len();
1019 (buf, nr > 0)
1020}
1021
1022#[derive(Clone, Copy, PartialEq, Eq)]
1025enum ReadFormat {
1026 Number,
1027 Line,
1028 LineWithEol,
1029 All,
1030}
1031
1032fn read_format_requires_star(version: lua_types::LuaVersion) -> bool {
1038 matches!(
1039 version,
1040 lua_types::LuaVersion::V51 | lua_types::LuaVersion::V52
1041 )
1042}
1043
1044fn read_format_has_line_with_eol(version: lua_types::LuaVersion) -> bool {
1049 version != lua_types::LuaVersion::V51
1050}
1051
1052fn resolve_read_format(
1065 version: lua_types::LuaVersion,
1066 fmt: &[u8],
1067) -> Result<ReadFormat, &'static [u8]> {
1068 let option = if read_format_requires_star(version) {
1069 if fmt.first() != Some(&b'*') {
1070 return Err(b"invalid option");
1071 }
1072 fmt.get(1).copied()
1073 } else if fmt.first() == Some(&b'*') {
1074 fmt.get(1).copied()
1075 } else {
1076 fmt.first().copied()
1077 };
1078 match option {
1079 Some(b'n') => Ok(ReadFormat::Number),
1080 Some(b'l') => Ok(ReadFormat::Line),
1081 Some(b'L') if read_format_has_line_with_eol(version) => Ok(ReadFormat::LineWithEol),
1082 Some(b'a') => Ok(ReadFormat::All),
1083 _ => Err(b"invalid format"),
1084 }
1085}
1086
1087fn g_read(
1093 state: &mut LuaState,
1094 p_rc: &Rc<RefCell<LStream>>,
1095 first: i32,
1096) -> Result<usize, LuaError> {
1097 let nargs = (state.top() - first + 1).max(0);
1103 let mut n = first;
1104 let mut success = true;
1105
1106 {
1107 let mut p = p_rc.borrow_mut();
1108 let fh = p.file.as_mut().expect("open stream has no file handle");
1109 fh.clear_error();
1110 }
1111
1112 if nargs == 0 {
1113 let (bytes, had) = {
1114 let mut p = p_rc.borrow_mut();
1115 let fh = p
1116 .file
1117 .as_deref_mut()
1118 .expect("open stream has no file handle");
1119 read_line(fh, true)
1120 };
1121 state.push_string(&bytes)?;
1122 success = had;
1123 n = first + 1;
1124 } else {
1125 state.ensure_stack((nargs as i32) + 20, "too many arguments")?;
1126 let mut remaining = nargs;
1127 while remaining > 0 && success {
1128 if state.type_at(n) == LuaType::Number {
1129 let l = state.check_arg_integer(n)? as usize;
1130 if l == 0 {
1131 let not_eof = {
1132 let mut p = p_rc.borrow_mut();
1133 let fh = p
1134 .file
1135 .as_deref_mut()
1136 .expect("open stream has no file handle");
1137 test_eof(fh)
1138 };
1139 state.push_string(b"")?;
1140 success = not_eof;
1141 } else {
1142 let (bytes, had) = {
1143 let mut p = p_rc.borrow_mut();
1144 let fh = p
1145 .file
1146 .as_deref_mut()
1147 .expect("open stream has no file handle");
1148 read_chars(fh, l)
1149 };
1150 state.push_string(&bytes)?;
1151 success = had;
1152 }
1153 } else {
1154 let s: Vec<u8> = state.check_arg_string(n)?;
1155 let version = state.global().lua_version;
1156 let format = match resolve_read_format(version, &s) {
1157 Ok(format) => format,
1158 Err(extramsg) => {
1159 return Err(lua_vm::debug::arg_error_impl(state, n, extramsg));
1160 }
1161 };
1162 match format {
1163 ReadFormat::Number => {
1164 let bytes = {
1165 let mut p = p_rc.borrow_mut();
1166 let fh = p
1167 .file
1168 .as_deref_mut()
1169 .expect("open stream has no file handle");
1170 read_number_bytes(fh)
1171 };
1172 let pushed = state.string_to_number_push(&bytes)?;
1173 if pushed != 0 {
1174 success = true;
1175 } else {
1176 state.push(LuaValue::Nil);
1177 success = false;
1178 }
1179 }
1180 ReadFormat::Line => {
1181 let (bytes, had) = {
1182 let mut p = p_rc.borrow_mut();
1183 let fh = p
1184 .file
1185 .as_deref_mut()
1186 .expect("open stream has no file handle");
1187 read_line(fh, true)
1188 };
1189 state.push_string(&bytes)?;
1190 success = had;
1191 }
1192 ReadFormat::LineWithEol => {
1193 let (bytes, had) = {
1194 let mut p = p_rc.borrow_mut();
1195 let fh = p
1196 .file
1197 .as_deref_mut()
1198 .expect("open stream has no file handle");
1199 read_line(fh, false)
1200 };
1201 state.push_string(&bytes)?;
1202 success = had;
1203 }
1204 ReadFormat::All => {
1205 let bytes = {
1206 let mut p = p_rc.borrow_mut();
1207 let fh = p
1208 .file
1209 .as_deref_mut()
1210 .expect("open stream has no file handle");
1211 read_all(fh)
1212 };
1213 state.push_string(&bytes)?;
1214 success = true;
1215 }
1216 }
1217 }
1218 n += 1;
1219 remaining -= 1;
1220 }
1221 }
1222
1223 let has_err = {
1224 let p = p_rc.borrow();
1225 match p.file.as_deref() {
1226 Some(fh) => fh.has_error(),
1227 None => false,
1228 }
1229 };
1230 if has_err {
1231 let err = {
1232 let p = p_rc.borrow();
1233 match p.file.as_deref().and_then(|fh| fh.last_error_info()) {
1234 Some((code, _msg)) if code != 0 => io::Error::from_raw_os_error(code),
1235 Some((_code, msg)) => io::Error::new(io::ErrorKind::Other, msg),
1236 None => io::Error::new(io::ErrorKind::Other, "file read error"),
1237 }
1238 };
1239 return file_result(state, false, None, err);
1240 }
1241
1242 if !success {
1243 state.pop_n(1);
1244 state.push(LuaValue::Nil);
1245 }
1246
1247 Ok((n - first) as usize)
1248}
1249
1250fn get_io_file_rc(state: &mut LuaState, key: &[u8]) -> Result<Rc<RefCell<LStream>>, LuaError> {
1255 state.registry_get(key)?;
1256 let ud_id = state
1257 .test_arg_userdata(-1, LUA_FILE_HANDLE)
1258 .map(|ud| ud.identity());
1259 state.pop_n(1);
1260 let label = &key[IO_PREFIX_LEN..];
1261 let id = ud_id.ok_or_else(|| {
1262 LuaError::runtime(format_args!(
1263 "default {} file is invalid",
1264 label.escape_ascii()
1265 ))
1266 })?;
1267 let rc = lookup_lstream(id).ok_or_else(|| {
1268 LuaError::runtime(format_args!(
1269 "default {} file is invalid",
1270 label.escape_ascii()
1271 ))
1272 })?;
1273 if rc.borrow().is_closed() {
1274 return Err(LuaError::runtime(format_args!(
1275 "default {} file is closed",
1276 label.escape_ascii()
1277 )));
1278 }
1279 Ok(rc)
1280}
1281
1282pub fn io_read(state: &mut LuaState) -> Result<usize, LuaError> {
1284 let p_rc = get_io_file_rc(state, IO_INPUT_KEY)?;
1285 g_read(state, &p_rc, 1)
1286}
1287
1288pub fn f_read(state: &mut LuaState) -> Result<usize, LuaError> {
1290 let p_rc = tofile(state)?;
1291 g_read(state, &p_rc, 2)
1292}
1293
1294fn num_to_write_bytes(state: &mut LuaState, val: &LuaValue) -> Result<Vec<u8>, LuaError> {
1305 let s = lua_vm::object::num_to_string(state, val)?;
1306 Ok(s.as_bytes().to_vec())
1307}
1308
1309pub fn io_write(state: &mut LuaState) -> Result<usize, LuaError> {
1319 let n = state.top();
1321 let mut chunks: Vec<Vec<u8>> = Vec::with_capacity(n as usize);
1322 for i in 1..=(n as i32) {
1323 if state.type_at(i) == LuaType::Number {
1324 let val = state.value_at(i);
1325 chunks.push(num_to_write_bytes(state, &val)?);
1326 } else {
1327 let bytes: Vec<u8> = state.check_arg_string(i)?;
1328 chunks.push(bytes);
1329 }
1330 }
1331
1332 let p_rc = get_io_file_rc(state, IO_OUTPUT_KEY)?;
1335 {
1336 let mut p = p_rc.borrow_mut();
1337 let fh = p.file.as_mut().expect("open stream has no file handle");
1338 for chunk in &chunks {
1339 fh.write_bytes(chunk)
1340 .map_err(|e| LuaError::runtime(format_args!("io.write: {}", e)))?;
1341 }
1342 }
1343 state.registry_get(IO_OUTPUT_KEY)?;
1344 Ok(1)
1345}
1346
1347pub fn f_write(state: &mut LuaState) -> Result<usize, LuaError> {
1349 let p_rc = tofile(state)?;
1350
1351 let n = state.top();
1353 let mut chunks: Vec<Vec<u8>> = Vec::with_capacity(n.saturating_sub(1) as usize);
1354 for i in 2..=(n as i32) {
1355 if state.type_at(i) == LuaType::Number {
1356 let val = state.value_at(i);
1357 chunks.push(num_to_write_bytes(state, &val)?);
1358 } else {
1359 let bytes: Vec<u8> = state.check_arg_string(i)?;
1360 chunks.push(bytes);
1361 }
1362 }
1363
1364 let result: io::Result<()> = {
1366 let mut p = p_rc.borrow_mut();
1367 let fh = p.file.as_mut().expect("open stream has no file handle");
1368 let mut r: io::Result<()> = Ok(());
1369 for chunk in &chunks {
1370 match fh.write_bytes(chunk) {
1371 Ok(written) if written == chunk.len() => {}
1372 Ok(_) => {
1373 r = Err(io::Error::new(io::ErrorKind::Other, "short write"));
1374 break;
1375 }
1376 Err(e) => {
1377 r = Err(e);
1378 break;
1379 }
1380 }
1381 }
1382 r
1383 };
1384
1385 match result {
1387 Ok(()) => {
1388 state.push_value_at(1)?;
1389 Ok(1)
1390 }
1391 Err(e) => file_result(state, false, None, e),
1392 }
1393}
1394
1395pub fn f_seek(state: &mut LuaState) -> Result<usize, LuaError> {
1399 static MODE_NAMES: &[&[u8]] = &[b"set", b"cur", b"end"];
1400
1401 let p_rc = tofile(state)?;
1402 let op = state.check_arg_option(2, Some(b"cur"), MODE_NAMES)?;
1403 let p3: i64 = state.opt_arg_integer(3, 0)?;
1404
1405 let seek_pos = match op {
1406 0 => SeekFrom::Start(p3 as u64),
1407 1 => SeekFrom::Current(p3),
1408 2 => SeekFrom::End(p3),
1409 _ => unreachable!(),
1410 };
1411
1412 let result = {
1413 let mut p = p_rc.borrow_mut();
1414 let fh = p.file.as_mut().expect("open stream has no file handle");
1415 fh.seek(seek_pos)
1416 };
1417 match result {
1418 Ok(pos) => {
1419 state.push(LuaValue::Int(pos as i64));
1420 Ok(1)
1421 }
1422 Err(e) => file_result(state, false, None, e),
1423 }
1424}
1425
1426pub fn f_setvbuf(state: &mut LuaState) -> Result<usize, LuaError> {
1428 static MODE_NAMES: &[&[u8]] = &[b"no", b"full", b"line"];
1429
1430 let p_rc = tofile(state)?;
1431 let op = state.check_arg_option(2, None, MODE_NAMES)?;
1432 let sz: i64 = state.opt_arg_integer(3, LUAL_BUFFER_SIZE as i64)?;
1433 let mode = match op {
1434 0 => BufMode::No,
1435 1 => BufMode::Full,
1436 2 => BufMode::Line,
1437 _ => unreachable!(),
1438 };
1439 let result = {
1440 let mut p = p_rc.borrow_mut();
1441 let fh = p.file.as_mut().expect("open stream has no file handle");
1442 let mode_index = match mode {
1443 BufMode::No => 0,
1444 BufMode::Full => 1,
1445 BufMode::Line => 2,
1446 };
1447 fh.set_buf_mode(mode_index, sz.max(0) as usize)
1448 };
1449 match result {
1450 Ok(()) => file_result(state, true, None, io::Error::last_os_error()),
1451 Err(e) => file_result(state, false, None, e),
1452 }
1453}
1454
1455pub fn io_flush(state: &mut LuaState) -> Result<usize, LuaError> {
1457 let ud_id: Option<usize> = {
1458 state.registry_get(IO_OUTPUT_KEY)?;
1459 let id = state
1460 .test_arg_userdata(-1, LUA_FILE_HANDLE)
1461 .map(|ud| ud.identity());
1462 state.pop_n(1);
1463 id
1464 };
1465 if let Some(id) = ud_id {
1466 if let Some(rc) = lookup_lstream(id) {
1467 let result = {
1468 let mut p = rc.borrow_mut();
1469 if p.is_closed() {
1470 return Err(LuaError::runtime(format_args!(
1471 "default output file is closed"
1472 )));
1473 }
1474 let fh = p
1475 .file
1476 .as_deref_mut()
1477 .expect("open stream has no file handle");
1478 fh.flush()
1479 };
1480 return match result {
1481 Ok(()) => {
1482 state.push(LuaValue::Bool(true));
1483 Ok(1)
1484 }
1485 Err(e) => file_result(state, false, None, e),
1486 };
1487 }
1488 }
1489 state.push(LuaValue::Bool(true));
1491 Ok(1)
1492}
1493
1494pub fn f_flush(state: &mut LuaState) -> Result<usize, LuaError> {
1496 let p_rc = tofile(state)?;
1497 let result = {
1498 let mut p = p_rc.borrow_mut();
1499 let fh = p.file.as_mut().expect("open stream has no file handle");
1500 fh.flush()
1501 };
1502 match result {
1503 Ok(()) => {
1504 state.push(LuaValue::Bool(true));
1505 Ok(1)
1506 }
1507 Err(e) => file_result(state, false, None, e),
1508 }
1509}
1510
1511fn aux_lines(state: &mut LuaState, toclose: bool) -> Result<(), LuaError> {
1521 let n = state.top() - 1;
1524 if n > MAX_ARG_LINE as i32 {
1525 return Err(lua_vm::debug::arg_error_impl(
1526 state,
1527 MAX_ARG_LINE as i32 + 2,
1528 b"too many arguments",
1529 ));
1530 }
1531 state.push_value_at(1)?;
1532 state.push(LuaValue::Int(n as i64));
1533 state.push(LuaValue::Bool(toclose));
1534 state.rotate(2, 3)?;
1535 state.push_c_closure(io_readline, (3 + n) as i32)?;
1536 Ok(())
1537}
1538
1539pub fn f_lines(state: &mut LuaState) -> Result<usize, LuaError> {
1541 let _ = tofile(state)?; aux_lines(state, false)?;
1543 Ok(1)
1544}
1545
1546pub fn io_lines(state: &mut LuaState) -> Result<usize, LuaError> {
1548 if state.type_at(1) == LuaType::None {
1549 state.push(LuaValue::Nil);
1550 }
1551 let toclose = if state.type_at(1) == LuaType::Nil {
1552 state.registry_get(IO_INPUT_KEY)?;
1553 state.replace(1)?;
1554 let _ = tofile(state)?;
1555 false
1556 } else {
1557 let filename = state.check_arg_string(1)?;
1558 opencheck(state, &filename, b"r")?;
1559 state.replace(1)?;
1560 true
1561 };
1562
1563 aux_lines(state, toclose)?;
1564
1565 if toclose && state.global().lua_version.lines_returns_to_be_closed() {
1566 state.push(LuaValue::Nil); state.push(LuaValue::Nil); state.push_value_at(1)?; Ok(4)
1570 } else {
1571 Ok(1)
1572 }
1573}
1574
1575fn io_readline(state: &mut LuaState) -> Result<usize, LuaError> {
1583 let n = match state.value_at(crate::state_stub::upvalue_index(2)) {
1584 LuaValue::Int(i) => i as usize,
1585 _ => 0,
1586 };
1587
1588 let p_rc = lstream_from_upvalue(state, 1)?;
1589
1590 if p_rc.borrow().is_closed() {
1591 return Err(LuaError::runtime(format_args!("file is already closed")));
1592 }
1593
1594 lua_vm::api::set_top(state, 1)?;
1595 state.ensure_stack(n as i32, "too many arguments")?;
1596
1597 for i in 1..=n {
1598 let uv = state.value_at(crate::state_stub::upvalue_index(3 + i as i32));
1599 state.push(uv);
1600 }
1601
1602 let result_n: usize = g_read(state, &p_rc, 2)?;
1603
1604 debug_assert!(result_n > 0, "g_read should return at least one value");
1605
1606 let top = state.top_idx().get() as i32;
1607 let first_result_idx = top - result_n as i32;
1608 let first_truthy = !matches!(
1609 state.stack_at(first_result_idx),
1610 LuaValue::Nil | LuaValue::Bool(false)
1611 );
1612 if first_truthy {
1613 return Ok(result_n);
1614 }
1615
1616 if result_n > 1 {
1617 let err_val = state.stack_at(first_result_idx + 1).clone();
1618 return Err(LuaError::from_value(err_val));
1619 }
1620
1621 let toclose = !matches!(
1622 state.value_at(crate::state_stub::upvalue_index(3)),
1623 LuaValue::Nil | LuaValue::Bool(false)
1624 );
1625 if toclose {
1626 lua_vm::api::set_top(state, 0)?;
1627 state.push_upvalue(1)?;
1628 aux_close(state)?;
1629 }
1630
1631 Ok(0)
1632}
1633
1634fn create_meta(state: &mut LuaState) -> Result<(), LuaError> {
1638 state.new_metatable(LUA_FILE_HANDLE)?;
1639 state.set_funcs(FILE_METAMETHODS, 0)?;
1640 state.new_lib_table(FILE_METHODS)?;
1641 state.set_funcs(FILE_METHODS, 0)?;
1642 state.set_field(-2, b"__index")?;
1643 state.pop_n(1);
1644 Ok(())
1645}
1646
1647fn create_std_file(
1649 state: &mut LuaState,
1650 std_kind: StdFileKind,
1651 registry_key: Option<&[u8]>,
1652 field_name: &[u8],
1653) -> Result<(), LuaError> {
1654 let cell = new_pre_file(state)?;
1655 let output_hook = match std_kind {
1656 StdFileKind::Stdout => state.global().stdout_hook,
1657 StdFileKind::Stderr => state.global().stderr_hook,
1658 StdFileKind::Stdin => None,
1659 };
1660 let input_hook = match std_kind {
1661 StdFileKind::Stdin => state.global().stdin_hook,
1662 StdFileKind::Stdout | StdFileKind::Stderr => None,
1663 };
1664 {
1665 let mut p = cell.borrow_mut();
1666 p.file = Some(Box::new(StdStreamHandle::new(
1667 std_kind,
1668 input_hook,
1669 output_hook,
1670 )));
1671 p.close_fn = Some(io_noclose);
1672 }
1673 if let Some(key) = registry_key {
1674 state.push_value_at(-1)?;
1675 state.registry_set(key)?;
1676 }
1677 state.set_field(-2, field_name)?;
1678 Ok(())
1679}
1680
1681pub fn luaopen_io(state: &mut LuaState) -> Result<usize, LuaError> {
1683 state.new_lib(IO_LIB)?;
1684 create_meta(state)?;
1685 create_std_file(state, StdFileKind::Stdin, Some(IO_INPUT_KEY), b"stdin")?;
1686 create_std_file(state, StdFileKind::Stdout, Some(IO_OUTPUT_KEY), b"stdout")?;
1687 create_std_file(state, StdFileKind::Stderr, None, b"stderr")?;
1688 Ok(1)
1689}