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 {
557 LuaError::Runtime(LuaValue::Str(s)) => {
558 String::from_utf8_lossy(s.as_bytes()).into_owned()
559 }
560 other => format!("{:?}", other),
561 }
562 ))
563 })?,
564 None => {
565 return Err(LuaError::runtime(format_args!(
566 "cannot open file '{}' (no filesystem hook registered)",
567 fname.escape_ascii()
568 )));
569 }
570 };
571 let cell = new_file(state)?;
572 cell.borrow_mut().file = Some(fh);
573 Ok(())
574}
575
576fn io_fclose(state: &mut LuaState) -> Result<usize, LuaError> {
585 let p_rc = get_lstream(state)?;
586 let _closed = p_rc.borrow_mut().file.take();
587 state.push(LuaValue::Bool(true));
588 Ok(1)
589}
590
591fn io_pclose(state: &mut LuaState) -> Result<usize, LuaError> {
596 let p_rc = get_lstream(state)?;
597 let _closed = p_rc.borrow_mut().file.take();
598 exec_result(state, 0)
599}
600
601fn io_noclose(state: &mut LuaState) -> Result<usize, LuaError> {
606 let p_rc = get_lstream(state)?;
607 p_rc.borrow_mut().close_fn = Some(io_noclose);
608 state.push(LuaValue::Bool(false));
609 state.push_string(b"cannot close standard file")?;
610 Ok(2)
611}
612
613fn aux_close(state: &mut LuaState) -> Result<usize, LuaError> {
615 let p_rc = get_lstream(state)?;
616 let cf = p_rc.borrow_mut().close_fn.take().ok_or_else(|| {
617 LuaError::runtime(format_args!("attempt to close an already-closed file"))
618 })?;
619 cf(state)
620}
621
622pub fn io_type(state: &mut LuaState) -> Result<usize, LuaError> {
632 state.check_arg_any(1)?;
633 let maybe_userdata = state.test_arg_userdata(1, LUA_FILE_HANDLE);
634 match maybe_userdata {
635 None => {
636 state.push(LuaValue::Nil);
637 }
638 Some(ud) => {
639 let is_closed = match lookup_lstream(ud.identity()) {
640 Some(rc) => rc.borrow().is_closed(),
641 None => true,
642 };
643 if is_closed {
644 state.push_string(b"closed file")?;
645 } else {
646 state.push_string(b"file")?;
647 }
648 }
649 }
650 Ok(1)
651}
652
653fn f_tostring(state: &mut LuaState) -> Result<usize, LuaError> {
662 let p_rc = get_lstream(state)?;
663 let closed = p_rc.borrow().is_closed();
664 if closed {
665 state.push_string(b"file (closed)")?;
666 } else {
667 state.push_string(b"file (0x?)")?;
668 }
669 Ok(1)
670}
671
672fn f_close(state: &mut LuaState) -> Result<usize, LuaError> {
676 let _ = tofile(state)?; aux_close(state)
678}
679
680pub fn io_close(state: &mut LuaState) -> Result<usize, LuaError> {
682 if state.type_at(1) == LuaType::None {
686 state.registry_get(IO_OUTPUT_KEY)?;
687 }
688 f_close(state)
689}
690
691fn f_gc(state: &mut LuaState) -> Result<usize, LuaError> {
693 let p_rc = get_lstream(state)?;
694 let needs_close = {
695 let p = p_rc.borrow();
696 !p.is_closed() && p.file.is_some()
697 };
698 if needs_close {
699 let _ = aux_close(state);
701 }
702 Ok(0)
703}
704
705pub fn io_open(state: &mut LuaState) -> Result<usize, LuaError> {
712 let filename: Vec<u8> = state.check_arg_string(1)?;
713 let mode: Vec<u8> = state.opt_arg_string(2, b"r")?;
714 if !check_mode(&mode) {
715 return Err(lua_vm::debug::arg_error_impl(state, 2, b"invalid mode"));
716 }
717 let hook = state.global().file_open_hook;
718 match hook {
719 Some(open_fn) => match open_fn(&filename, &mode) {
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, Some(&filename), os_err)
736 }
737 },
738 None => {
739 let os_err =
740 io::Error::new(io::ErrorKind::Unsupported, "no filesystem hook registered");
741 file_result(state, false, Some(&filename), os_err)
742 }
743 }
744}
745
746pub fn io_popen(state: &mut LuaState) -> Result<usize, LuaError> {
755 let filename: Vec<u8> = state.check_arg_string(1)?;
756 let mode: Vec<u8> = state.opt_arg_string(2, b"r")?;
757 if !check_mode_popen(&mode) {
758 return Err(lua_vm::debug::arg_error_impl(state, 2, b"invalid mode"));
759 }
760 let hook = state.global().popen_hook;
761 match hook {
762 Some(spawn_fn) => match spawn_fn(&filename, &mode) {
763 Ok(fh) => {
764 let cell = new_pre_file(state)?;
765 let mut p = cell.borrow_mut();
766 p.file = Some(fh);
767 p.close_fn = Some(io_pclose);
768 drop(p);
769 Ok(1)
770 }
771 Err(e) => {
772 let os_err = io::Error::new(
773 io::ErrorKind::Other,
774 match &e {
775 LuaError::Runtime(LuaValue::Str(s)) => {
776 String::from_utf8_lossy(s.as_bytes()).into_owned()
777 }
778 other => format!("{:?}", other),
779 },
780 );
781 file_result(state, false, Some(&filename), os_err)
782 }
783 },
784 None => {
785 let os_err = io::Error::new(
786 io::ErrorKind::Unsupported,
787 "popen not enabled in this build",
788 );
789 file_result(state, false, Some(&filename), os_err)
790 }
791 }
792}
793
794fn native_temp_name() -> io::Result<Vec<u8>> {
795 #[cfg(all(target_arch = "wasm32", target_os = "unknown"))]
796 {
797 return Err(io::Error::new(
798 io::ErrorKind::Unsupported,
799 "temporary files not available in this host",
800 ));
801 }
802
803 #[cfg(not(all(target_arch = "wasm32", target_os = "unknown")))]
804 {
805 let mut path = std::env::temp_dir().to_string_lossy().as_bytes().to_vec();
806 if path.last().copied() != Some(b'/') && path.last().copied() != Some(b'\\') {
807 path.push(b'/');
808 }
809 let unique = format!(
810 "lua_tmpfile_{}_{}",
811 std::process::id(),
812 std::time::SystemTime::now()
813 .duration_since(std::time::UNIX_EPOCH)
814 .map(|d| d.as_nanos())
815 .unwrap_or(0)
816 );
817 path.extend_from_slice(unique.as_bytes());
818 Ok(path)
819 }
820}
821
822pub fn io_tmpfile(state: &mut LuaState) -> Result<usize, LuaError> {
824 let hook = state.global().file_open_hook;
825 let Some(open_fn) = hook else {
826 let os_err = io::Error::new(io::ErrorKind::Unsupported, "no filesystem hook registered");
827 return file_result(state, false, None, os_err);
828 };
829
830 let temp_name_hook = state.global().temp_name_hook;
831 let path = match temp_name_hook {
832 Some(temp_fn) => match temp_fn() {
833 Ok(path) => path,
834 Err(e) => {
835 let msg = match &e {
836 LuaError::Runtime(LuaValue::Str(s)) => {
837 String::from_utf8_lossy(s.as_bytes()).into_owned()
838 }
839 other => format!("{:?}", other),
840 };
841 return file_result(
842 state,
843 false,
844 None,
845 io::Error::new(io::ErrorKind::Unsupported, msg),
846 );
847 }
848 },
849 None => match native_temp_name() {
850 Ok(path) => path,
851 Err(e) => return file_result(state, false, None, e),
852 },
853 };
854
855 match open_fn(&path, b"w+b") {
856 Ok(fh) => {
857 let cell = new_file(state)?;
858 cell.borrow_mut().file = Some(fh);
859 Ok(1)
860 }
861 Err(e) => {
862 let os_err = io::Error::new(
863 io::ErrorKind::Other,
864 match &e {
865 LuaError::Runtime(LuaValue::Str(s)) => {
866 String::from_utf8_lossy(s.as_bytes()).into_owned()
867 }
868 other => format!("{:?}", other),
869 },
870 );
871 file_result(state, false, None, os_err)
872 }
873 }
874}
875
876fn g_iofile(state: &mut LuaState, key: &[u8], mode: &[u8]) -> Result<usize, LuaError> {
880 if !matches!(state.type_at(1), LuaType::None | LuaType::Nil) {
881 if state.type_at(1) == LuaType::String {
882 let filename = state.check_arg_string(1)?;
883 opencheck(state, &filename, mode)?;
884 } else {
885 let _ = tofile(state)?;
886 state.push_value_at(1)?;
887 }
888 state.registry_set(key)?;
889 }
890 state.registry_get(key)?;
891 Ok(1)
892}
893
894pub fn io_input(state: &mut LuaState) -> Result<usize, LuaError> {
896 g_iofile(state, IO_INPUT_KEY, b"r")
897}
898
899pub fn io_output(state: &mut LuaState) -> Result<usize, LuaError> {
901 g_iofile(state, IO_OUTPUT_KEY, b"w")
902}
903
904fn read_number_bytes(file: &mut dyn LuaFileHandle) -> Vec<u8> {
912 let first = loop {
913 let b = file.read_byte();
914 if b == EOF_SENTINEL || !(b as u8).is_ascii_whitespace() {
915 break b;
916 }
917 };
918
919 let mut rn = ReadNumState::new(first);
920
921 rn.try2(file, [b'-', b'+']);
922
923 let mut count: usize = 0;
924 let hex = if rn.try2(file, [b'0', b'0']) {
925 if rn.try2(file, [b'x', b'X']) {
926 true
927 } else {
928 count = 1;
929 false
930 }
931 } else {
932 false
933 };
934
935 count += rn.read_digits(file, hex);
936
937 let dec_point = b'.';
938 if rn.try2(file, [dec_point, b'.']) {
939 count += rn.read_digits(file, hex);
940 }
941
942 if count > 0 {
943 let exp_chars = if hex { [b'p', b'P'] } else { [b'e', b'E'] };
944 if rn.try2(file, exp_chars) {
945 rn.try2(file, [b'-', b'+']);
946 rn.read_digits(file, false);
947 }
948 }
949
950 file.unread_byte(rn.current);
951 rn.as_bytes().to_vec()
952}
953
954fn test_eof(file: &mut dyn LuaFileHandle) -> bool {
957 let c = file.read_byte();
958 if c != EOF_SENTINEL {
959 file.unread_byte(c);
960 }
961 c != EOF_SENTINEL
962}
963
964fn read_line(file: &mut dyn LuaFileHandle, chop: bool) -> (Vec<u8>, bool) {
972 let mut buf: Vec<u8> = Vec::new();
973 let mut c: i32;
974
975 'outer: loop {
976 for _ in 0..LUAL_BUFFER_SIZE {
977 c = file.read_byte();
978 if c == EOF_SENTINEL || c == b'\n' as i32 {
979 break 'outer;
980 }
981 buf.push(c as u8);
982 }
983 }
984
985 if !chop && c == b'\n' as i32 {
986 buf.push(b'\n');
987 }
988
989 let had_content = c == b'\n' as i32 || !buf.is_empty();
990 (buf, had_content)
991}
992
993fn read_all(file: &mut dyn LuaFileHandle) -> Vec<u8> {
999 let mut buf: Vec<u8> = Vec::new();
1000 loop {
1001 let mut chunk_read = 0usize;
1002 for _ in 0..LUAL_BUFFER_SIZE {
1003 let b = file.read_byte();
1004 if b == EOF_SENTINEL {
1005 break;
1006 }
1007 buf.push(b as u8);
1008 chunk_read += 1;
1009 }
1010 if chunk_read < LUAL_BUFFER_SIZE {
1011 break;
1012 }
1013 }
1014 buf
1015}
1016
1017fn read_chars(file: &mut dyn LuaFileHandle, n: usize) -> (Vec<u8>, bool) {
1019 let mut buf = Vec::with_capacity(n);
1020 for _ in 0..n {
1021 let b = file.read_byte();
1022 if b == EOF_SENTINEL {
1023 break;
1024 }
1025 buf.push(b as u8);
1026 }
1027 let nr = buf.len();
1028 (buf, nr > 0)
1029}
1030
1031#[derive(Clone, Copy, PartialEq, Eq)]
1034enum ReadFormat {
1035 Number,
1036 Line,
1037 LineWithEol,
1038 All,
1039}
1040
1041fn read_format_requires_star(version: lua_types::LuaVersion) -> bool {
1047 matches!(
1048 version,
1049 lua_types::LuaVersion::V51 | lua_types::LuaVersion::V52
1050 )
1051}
1052
1053fn read_format_has_line_with_eol(version: lua_types::LuaVersion) -> bool {
1058 version != lua_types::LuaVersion::V51
1059}
1060
1061fn resolve_read_format(
1074 version: lua_types::LuaVersion,
1075 fmt: &[u8],
1076) -> Result<ReadFormat, &'static [u8]> {
1077 let option = if read_format_requires_star(version) {
1078 if fmt.first() != Some(&b'*') {
1079 return Err(b"invalid option");
1080 }
1081 fmt.get(1).copied()
1082 } else if fmt.first() == Some(&b'*') {
1083 fmt.get(1).copied()
1084 } else {
1085 fmt.first().copied()
1086 };
1087 match option {
1088 Some(b'n') => Ok(ReadFormat::Number),
1089 Some(b'l') => Ok(ReadFormat::Line),
1090 Some(b'L') if read_format_has_line_with_eol(version) => Ok(ReadFormat::LineWithEol),
1091 Some(b'a') => Ok(ReadFormat::All),
1092 _ => Err(b"invalid format"),
1093 }
1094}
1095
1096fn g_read(
1102 state: &mut LuaState,
1103 p_rc: &Rc<RefCell<LStream>>,
1104 first: i32,
1105) -> Result<usize, LuaError> {
1106 let nargs = (state.top() - first + 1).max(0);
1112 let mut n = first;
1113 let mut success = true;
1114
1115 {
1116 let mut p = p_rc.borrow_mut();
1117 let fh = p.file.as_mut().expect("open stream has no file handle");
1118 fh.clear_error();
1119 }
1120
1121 if nargs == 0 {
1122 let (bytes, had) = {
1123 let mut p = p_rc.borrow_mut();
1124 let fh = p
1125 .file
1126 .as_deref_mut()
1127 .expect("open stream has no file handle");
1128 read_line(fh, true)
1129 };
1130 state.push_string(&bytes)?;
1131 success = had;
1132 n = first + 1;
1133 } else {
1134 state.ensure_stack((nargs as i32) + 20, "too many arguments")?;
1135 let mut remaining = nargs;
1136 while remaining > 0 && success {
1137 if state.type_at(n) == LuaType::Number {
1138 let l = state.check_arg_integer(n)? as usize;
1139 if l == 0 {
1140 let not_eof = {
1141 let mut p = p_rc.borrow_mut();
1142 let fh = p
1143 .file
1144 .as_deref_mut()
1145 .expect("open stream has no file handle");
1146 test_eof(fh)
1147 };
1148 state.push_string(b"")?;
1149 success = not_eof;
1150 } else {
1151 let (bytes, had) = {
1152 let mut p = p_rc.borrow_mut();
1153 let fh = p
1154 .file
1155 .as_deref_mut()
1156 .expect("open stream has no file handle");
1157 read_chars(fh, l)
1158 };
1159 state.push_string(&bytes)?;
1160 success = had;
1161 }
1162 } else {
1163 let s: Vec<u8> = state.check_arg_string(n)?;
1164 let version = state.global().lua_version;
1165 let format = match resolve_read_format(version, &s) {
1166 Ok(format) => format,
1167 Err(extramsg) => {
1168 return Err(lua_vm::debug::arg_error_impl(state, n, extramsg));
1169 }
1170 };
1171 match format {
1172 ReadFormat::Number => {
1173 let bytes = {
1174 let mut p = p_rc.borrow_mut();
1175 let fh = p
1176 .file
1177 .as_deref_mut()
1178 .expect("open stream has no file handle");
1179 read_number_bytes(fh)
1180 };
1181 let pushed = state.string_to_number_push(&bytes)?;
1182 if pushed != 0 {
1183 success = true;
1184 } else {
1185 state.push(LuaValue::Nil);
1186 success = false;
1187 }
1188 }
1189 ReadFormat::Line => {
1190 let (bytes, had) = {
1191 let mut p = p_rc.borrow_mut();
1192 let fh = p
1193 .file
1194 .as_deref_mut()
1195 .expect("open stream has no file handle");
1196 read_line(fh, true)
1197 };
1198 state.push_string(&bytes)?;
1199 success = had;
1200 }
1201 ReadFormat::LineWithEol => {
1202 let (bytes, had) = {
1203 let mut p = p_rc.borrow_mut();
1204 let fh = p
1205 .file
1206 .as_deref_mut()
1207 .expect("open stream has no file handle");
1208 read_line(fh, false)
1209 };
1210 state.push_string(&bytes)?;
1211 success = had;
1212 }
1213 ReadFormat::All => {
1214 let bytes = {
1215 let mut p = p_rc.borrow_mut();
1216 let fh = p
1217 .file
1218 .as_deref_mut()
1219 .expect("open stream has no file handle");
1220 read_all(fh)
1221 };
1222 state.push_string(&bytes)?;
1223 success = true;
1224 }
1225 }
1226 }
1227 n += 1;
1228 remaining -= 1;
1229 }
1230 }
1231
1232 let has_err = {
1233 let p = p_rc.borrow();
1234 match p.file.as_deref() {
1235 Some(fh) => fh.has_error(),
1236 None => false,
1237 }
1238 };
1239 if has_err {
1240 let err = {
1241 let p = p_rc.borrow();
1242 match p.file.as_deref().and_then(|fh| fh.last_error_info()) {
1243 Some((code, _msg)) if code != 0 => io::Error::from_raw_os_error(code),
1244 Some((_code, msg)) => io::Error::new(io::ErrorKind::Other, msg),
1245 None => io::Error::new(io::ErrorKind::Other, "file read error"),
1246 }
1247 };
1248 return file_result(state, false, None, err);
1249 }
1250
1251 if !success {
1252 state.pop_n(1);
1253 state.push(LuaValue::Nil);
1254 }
1255
1256 Ok((n - first) as usize)
1257}
1258
1259fn get_io_file_rc(state: &mut LuaState, key: &[u8]) -> Result<Rc<RefCell<LStream>>, LuaError> {
1264 state.registry_get(key)?;
1265 let ud_id = state
1266 .test_arg_userdata(-1, LUA_FILE_HANDLE)
1267 .map(|ud| ud.identity());
1268 state.pop_n(1);
1269 let label = &key[IO_PREFIX_LEN..];
1270 let id = ud_id.ok_or_else(|| {
1271 LuaError::runtime(format_args!(
1272 "default {} file is invalid",
1273 label.escape_ascii()
1274 ))
1275 })?;
1276 let rc = lookup_lstream(id).ok_or_else(|| {
1277 LuaError::runtime(format_args!(
1278 "default {} file is invalid",
1279 label.escape_ascii()
1280 ))
1281 })?;
1282 if rc.borrow().is_closed() {
1283 return Err(LuaError::runtime(format_args!(
1284 "default {} file is closed",
1285 label.escape_ascii()
1286 )));
1287 }
1288 Ok(rc)
1289}
1290
1291pub fn io_read(state: &mut LuaState) -> Result<usize, LuaError> {
1293 let p_rc = get_io_file_rc(state, IO_INPUT_KEY)?;
1294 g_read(state, &p_rc, 1)
1295}
1296
1297pub fn f_read(state: &mut LuaState) -> Result<usize, LuaError> {
1299 let p_rc = tofile(state)?;
1300 g_read(state, &p_rc, 2)
1301}
1302
1303fn num_to_write_bytes(state: &mut LuaState, val: &LuaValue) -> Result<Vec<u8>, LuaError> {
1314 let s = lua_vm::object::num_to_string(state, val)?;
1315 Ok(s.as_bytes().to_vec())
1316}
1317
1318pub fn io_write(state: &mut LuaState) -> Result<usize, LuaError> {
1328 let n = state.top();
1330 let mut chunks: Vec<Vec<u8>> = Vec::with_capacity(n as usize);
1331 for i in 1..=(n as i32) {
1332 if state.type_at(i) == LuaType::Number {
1333 let val = state.value_at(i);
1334 chunks.push(num_to_write_bytes(state, &val)?);
1335 } else {
1336 let bytes: Vec<u8> = state.check_arg_string(i)?;
1337 chunks.push(bytes);
1338 }
1339 }
1340
1341 let p_rc = get_io_file_rc(state, IO_OUTPUT_KEY)?;
1344 {
1345 let mut p = p_rc.borrow_mut();
1346 let fh = p.file.as_mut().expect("open stream has no file handle");
1347 for chunk in &chunks {
1348 fh.write_bytes(chunk)
1349 .map_err(|e| LuaError::runtime(format_args!("io.write: {}", e)))?;
1350 }
1351 }
1352 state.registry_get(IO_OUTPUT_KEY)?;
1353 Ok(1)
1354}
1355
1356pub fn f_write(state: &mut LuaState) -> Result<usize, LuaError> {
1358 let p_rc = tofile(state)?;
1359
1360 let n = state.top();
1362 let mut chunks: Vec<Vec<u8>> = Vec::with_capacity(n.saturating_sub(1) as usize);
1363 for i in 2..=(n as i32) {
1364 if state.type_at(i) == LuaType::Number {
1365 let val = state.value_at(i);
1366 chunks.push(num_to_write_bytes(state, &val)?);
1367 } else {
1368 let bytes: Vec<u8> = state.check_arg_string(i)?;
1369 chunks.push(bytes);
1370 }
1371 }
1372
1373 let result: io::Result<()> = {
1375 let mut p = p_rc.borrow_mut();
1376 let fh = p.file.as_mut().expect("open stream has no file handle");
1377 let mut r: io::Result<()> = Ok(());
1378 for chunk in &chunks {
1379 match fh.write_bytes(chunk) {
1380 Ok(written) if written == chunk.len() => {}
1381 Ok(_) => {
1382 r = Err(io::Error::new(io::ErrorKind::Other, "short write"));
1383 break;
1384 }
1385 Err(e) => {
1386 r = Err(e);
1387 break;
1388 }
1389 }
1390 }
1391 r
1392 };
1393
1394 match result {
1396 Ok(()) => {
1397 state.push_value_at(1)?;
1398 Ok(1)
1399 }
1400 Err(e) => file_result(state, false, None, e),
1401 }
1402}
1403
1404pub fn f_seek(state: &mut LuaState) -> Result<usize, LuaError> {
1408 static MODE_NAMES: &[&[u8]] = &[b"set", b"cur", b"end"];
1409
1410 let p_rc = tofile(state)?;
1411 let op = state.check_arg_option(2, Some(b"cur"), MODE_NAMES)?;
1412 let p3: i64 = state.opt_arg_integer(3, 0)?;
1413
1414 let seek_pos = match op {
1415 0 => SeekFrom::Start(p3 as u64),
1416 1 => SeekFrom::Current(p3),
1417 2 => SeekFrom::End(p3),
1418 _ => unreachable!(),
1419 };
1420
1421 let result = {
1422 let mut p = p_rc.borrow_mut();
1423 let fh = p.file.as_mut().expect("open stream has no file handle");
1424 fh.seek(seek_pos)
1425 };
1426 match result {
1427 Ok(pos) => {
1428 state.push(LuaValue::Int(pos as i64));
1429 Ok(1)
1430 }
1431 Err(e) => file_result(state, false, None, e),
1432 }
1433}
1434
1435pub fn f_setvbuf(state: &mut LuaState) -> Result<usize, LuaError> {
1437 static MODE_NAMES: &[&[u8]] = &[b"no", b"full", b"line"];
1438
1439 let p_rc = tofile(state)?;
1440 let op = state.check_arg_option(2, None, MODE_NAMES)?;
1441 let sz: i64 = state.opt_arg_integer(3, LUAL_BUFFER_SIZE as i64)?;
1442 let mode = match op {
1443 0 => BufMode::No,
1444 1 => BufMode::Full,
1445 2 => BufMode::Line,
1446 _ => unreachable!(),
1447 };
1448 let result = {
1449 let mut p = p_rc.borrow_mut();
1450 let fh = p.file.as_mut().expect("open stream has no file handle");
1451 let mode_index = match mode {
1452 BufMode::No => 0,
1453 BufMode::Full => 1,
1454 BufMode::Line => 2,
1455 };
1456 fh.set_buf_mode(mode_index, sz.max(0) as usize)
1457 };
1458 match result {
1459 Ok(()) => file_result(state, true, None, io::Error::last_os_error()),
1460 Err(e) => file_result(state, false, None, e),
1461 }
1462}
1463
1464pub fn io_flush(state: &mut LuaState) -> Result<usize, LuaError> {
1466 let ud_id: Option<usize> = {
1467 state.registry_get(IO_OUTPUT_KEY)?;
1468 let id = state
1469 .test_arg_userdata(-1, LUA_FILE_HANDLE)
1470 .map(|ud| ud.identity());
1471 state.pop_n(1);
1472 id
1473 };
1474 if let Some(id) = ud_id {
1475 if let Some(rc) = lookup_lstream(id) {
1476 let result = {
1477 let mut p = rc.borrow_mut();
1478 if p.is_closed() {
1479 return Err(LuaError::runtime(format_args!(
1480 "default output file is closed"
1481 )));
1482 }
1483 let fh = p
1484 .file
1485 .as_deref_mut()
1486 .expect("open stream has no file handle");
1487 fh.flush()
1488 };
1489 return match result {
1490 Ok(()) => {
1491 state.push(LuaValue::Bool(true));
1492 Ok(1)
1493 }
1494 Err(e) => file_result(state, false, None, e),
1495 };
1496 }
1497 }
1498 state.push(LuaValue::Bool(true));
1500 Ok(1)
1501}
1502
1503pub fn f_flush(state: &mut LuaState) -> Result<usize, LuaError> {
1505 let p_rc = tofile(state)?;
1506 let result = {
1507 let mut p = p_rc.borrow_mut();
1508 let fh = p.file.as_mut().expect("open stream has no file handle");
1509 fh.flush()
1510 };
1511 match result {
1512 Ok(()) => {
1513 state.push(LuaValue::Bool(true));
1514 Ok(1)
1515 }
1516 Err(e) => file_result(state, false, None, e),
1517 }
1518}
1519
1520fn aux_lines(state: &mut LuaState, toclose: bool) -> Result<(), LuaError> {
1530 let n = state.top() - 1;
1533 if n > MAX_ARG_LINE as i32 {
1534 return Err(lua_vm::debug::arg_error_impl(
1535 state,
1536 MAX_ARG_LINE as i32 + 2,
1537 b"too many arguments",
1538 ));
1539 }
1540 state.push_value_at(1)?;
1541 state.push(LuaValue::Int(n as i64));
1542 state.push(LuaValue::Bool(toclose));
1543 state.rotate(2, 3)?;
1544 state.push_c_closure(io_readline, (3 + n) as i32)?;
1545 Ok(())
1546}
1547
1548pub fn f_lines(state: &mut LuaState) -> Result<usize, LuaError> {
1550 let _ = tofile(state)?; aux_lines(state, false)?;
1552 Ok(1)
1553}
1554
1555pub fn io_lines(state: &mut LuaState) -> Result<usize, LuaError> {
1557 if state.type_at(1) == LuaType::None {
1558 state.push(LuaValue::Nil);
1559 }
1560 let toclose = if state.type_at(1) == LuaType::Nil {
1561 state.registry_get(IO_INPUT_KEY)?;
1562 state.replace(1)?;
1563 let _ = tofile(state)?;
1564 false
1565 } else {
1566 let filename = state.check_arg_string(1)?;
1567 opencheck(state, &filename, b"r")?;
1568 state.replace(1)?;
1569 true
1570 };
1571
1572 aux_lines(state, toclose)?;
1573
1574 if toclose && state.global().lua_version.lines_returns_to_be_closed() {
1575 state.push(LuaValue::Nil); state.push(LuaValue::Nil); state.push_value_at(1)?; Ok(4)
1579 } else {
1580 Ok(1)
1581 }
1582}
1583
1584fn io_readline(state: &mut LuaState) -> Result<usize, LuaError> {
1592 let n = match state.value_at(crate::state_stub::upvalue_index(2)) {
1593 LuaValue::Int(i) => i as usize,
1594 _ => 0,
1595 };
1596
1597 let p_rc = lstream_from_upvalue(state, 1)?;
1598
1599 if p_rc.borrow().is_closed() {
1600 return Err(LuaError::runtime(format_args!("file is already closed")));
1601 }
1602
1603 lua_vm::api::set_top(state, 1)?;
1604 state.ensure_stack(n as i32, "too many arguments")?;
1605
1606 for i in 1..=n {
1607 let uv = state.value_at(crate::state_stub::upvalue_index(3 + i as i32));
1608 state.push(uv);
1609 }
1610
1611 let result_n: usize = g_read(state, &p_rc, 2)?;
1612
1613 debug_assert!(result_n > 0, "g_read should return at least one value");
1614
1615 let top = state.top_idx().get() as i32;
1616 let first_result_idx = top - result_n as i32;
1617 let first_truthy = !matches!(
1618 state.stack_at(first_result_idx),
1619 LuaValue::Nil | LuaValue::Bool(false)
1620 );
1621 if first_truthy {
1622 return Ok(result_n);
1623 }
1624
1625 if result_n > 1 {
1626 let err_val = state.stack_at(first_result_idx + 1).clone();
1627 return Err(LuaError::from_value(err_val));
1628 }
1629
1630 let toclose = !matches!(
1631 state.value_at(crate::state_stub::upvalue_index(3)),
1632 LuaValue::Nil | LuaValue::Bool(false)
1633 );
1634 if toclose {
1635 lua_vm::api::set_top(state, 0)?;
1636 state.push_upvalue(1)?;
1637 aux_close(state)?;
1638 }
1639
1640 Ok(0)
1641}
1642
1643fn create_meta(state: &mut LuaState) -> Result<(), LuaError> {
1647 state.new_metatable(LUA_FILE_HANDLE)?;
1648 state.set_funcs(FILE_METAMETHODS, 0)?;
1649 state.new_lib_table(FILE_METHODS)?;
1650 state.set_funcs(FILE_METHODS, 0)?;
1651 state.set_field(-2, b"__index")?;
1652 state.pop_n(1);
1653 Ok(())
1654}
1655
1656fn create_std_file(
1658 state: &mut LuaState,
1659 std_kind: StdFileKind,
1660 registry_key: Option<&[u8]>,
1661 field_name: &[u8],
1662) -> Result<(), LuaError> {
1663 let cell = new_pre_file(state)?;
1664 let output_hook = match std_kind {
1665 StdFileKind::Stdout => state.global().stdout_hook,
1666 StdFileKind::Stderr => state.global().stderr_hook,
1667 StdFileKind::Stdin => None,
1668 };
1669 let input_hook = match std_kind {
1670 StdFileKind::Stdin => state.global().stdin_hook,
1671 StdFileKind::Stdout | StdFileKind::Stderr => None,
1672 };
1673 {
1674 let mut p = cell.borrow_mut();
1675 p.file = Some(Box::new(StdStreamHandle::new(
1676 std_kind,
1677 input_hook,
1678 output_hook,
1679 )));
1680 p.close_fn = Some(io_noclose);
1681 }
1682 if let Some(key) = registry_key {
1683 state.push_value_at(-1)?;
1684 state.registry_set(key)?;
1685 }
1686 state.set_field(-2, field_name)?;
1687 Ok(())
1688}
1689
1690pub fn luaopen_io(state: &mut LuaState) -> Result<usize, LuaError> {
1692 state.new_lib(IO_LIB)?;
1693 create_meta(state)?;
1694 create_std_file(state, StdFileKind::Stdin, Some(IO_INPUT_KEY), b"stdin")?;
1695 create_std_file(state, StdFileKind::Stdout, Some(IO_OUTPUT_KEY), b"stdout")?;
1696 create_std_file(state, StdFileKind::Stderr, None, b"stderr")?;
1697 Ok(1)
1698}
1699
1700