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