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
411#[cfg(any(unix, target_arch = "wasm32"))]
426fn posix_strerror(code: i32) -> Option<&'static str> {
427 Some(match code {
428 1 => "Operation not permitted",
429 2 => "No such file or directory",
430 3 => "No such process",
431 4 => "Interrupted system call",
432 5 => "Input/output error",
433 7 => "Argument list too long",
434 8 => "Exec format error",
435 9 => "Bad file descriptor",
436 10 => "No child processes",
437 12 => "Cannot allocate memory",
438 13 => "Permission denied",
439 14 => "Bad address",
440 17 => "File exists",
441 20 => "Not a directory",
442 21 => "Is a directory",
443 22 => "Invalid argument",
444 24 => "Too many open files",
445 27 => "File too large",
446 28 => "No space left on device",
447 29 => "Illegal seek",
448 30 => "Read-only file system",
449 32 => "Broken pipe",
450 _ => return None,
451 })
452}
453
454#[cfg(any(unix, target_arch = "wasm32"))]
461fn target_posix_strerror(code: i32) -> Option<&'static str> {
462 posix_strerror(code)
463}
464
465#[cfg(not(any(unix, target_arch = "wasm32")))]
466fn target_posix_strerror(_code: i32) -> Option<&'static str> {
467 None
468}
469
470fn strerror_text(err: &io::Error) -> String {
481 match err.raw_os_error() {
482 Some(code) => match target_posix_strerror(code) {
483 Some(msg) => msg.to_string(),
484 None => {
485 let full = err.to_string();
486 let suffix = format!(" (os error {code})");
487 full.strip_suffix(&suffix).unwrap_or(&full).to_string()
488 }
489 },
490 None => err.to_string(),
491 }
492}
493
494pub(crate) fn file_result(
519 state: &mut LuaState,
520 success: bool,
521 fname: Option<&[u8]>,
522 os_err: io::Error,
523) -> Result<usize, LuaError> {
524 if success {
525 state.push(LuaValue::Bool(true));
526 return Ok(1);
527 }
528 state.push(LuaValue::Nil);
529 let msg = strerror_text(&os_err);
530 match fname {
531 Some(name) => {
532 let mut s = Vec::with_capacity(name.len() + 2 + msg.len());
533 s.extend_from_slice(name);
534 s.extend_from_slice(b": ");
535 s.extend_from_slice(msg.as_bytes());
536 state.push_string(&s)?;
537 }
538 None => {
539 state.push_string(msg.as_bytes())?;
540 }
541 }
542 match os_err.raw_os_error() {
543 Some(code) => {
544 state.push(LuaValue::Int(code as i64));
545 Ok(3)
546 }
547 None => Ok(2),
548 }
549}
550
551fn exec_result(state: &mut LuaState, stat: i32) -> Result<usize, LuaError> {
558 if stat == 0 {
559 state.push(LuaValue::Bool(true));
560 Ok(1)
561 } else {
562 state.push(LuaValue::Bool(false));
563 state.push_string(b"exit")?;
564 state.push(LuaValue::Int(stat as i64));
565 Ok(3)
566 }
567}
568
569fn get_lstream(state: &mut LuaState) -> Result<Rc<RefCell<LStream>>, LuaError> {
576 let ud = state.check_arg_userdata(1, LUA_FILE_HANDLE)?;
577 lookup_lstream(ud.identity())
578 .ok_or_else(|| LuaError::runtime(format_args!("invalid file handle")))
579}
580
581fn lstream_from_upvalue(state: &mut LuaState, idx: i32) -> Result<Rc<RefCell<LStream>>, LuaError> {
588 let v = state.value_at(crate::state_stub::upvalue_index(idx));
589 let ud_id = match v {
590 LuaValue::UserData(ud) => ud.identity(),
591 _ => {
592 return Err(LuaError::runtime(format_args!(
593 "invalid file handle in upvalue {}",
594 idx
595 )));
596 }
597 };
598 lookup_lstream(ud_id)
599 .ok_or_else(|| LuaError::runtime(format_args!("invalid file handle in upvalue {}", idx)))
600}
601
602fn tofile(state: &mut LuaState) -> Result<Rc<RefCell<LStream>>, LuaError> {
608 let p_rc = get_lstream(state)?;
609 let closed = {
610 let p = p_rc.borrow();
611 debug_assert!(p.is_closed() || p.file.is_some());
612 p.is_closed()
613 };
614 if closed {
615 return Err(lua_vm::debug::c_api_runtime(
616 state,
617 b"attempt to use a closed file".to_vec(),
618 ));
619 }
620 Ok(p_rc)
621}
622
623fn new_pre_file(state: &mut LuaState) -> Result<Rc<RefCell<LStream>>, LuaError> {
630 let ud = state.new_userdata_typed(LUA_FILE_HANDLE, std::mem::size_of::<LStream>(), 0)?;
631 state.set_metatable_by_name(LUA_FILE_HANDLE)?;
632 let cell = register_lstream(
633 ud.identity(),
634 LStream {
635 file: None,
636 close_fn: None,
637 },
638 );
639 Ok(cell)
640}
641
642fn new_file(state: &mut LuaState) -> Result<Rc<RefCell<LStream>>, LuaError> {
644 let cell = new_pre_file(state)?;
645 cell.borrow_mut().close_fn = Some(io_fclose);
646 Ok(cell)
647}
648
649fn open_failure_error(state: &mut LuaState, fname: &[u8], detail: &[u8]) -> LuaError {
661 if matches!(state.global().lua_version, lua_types::LuaVersion::V51) {
662 let mut extramsg = Vec::with_capacity(fname.len() + 2 + detail.len());
663 extramsg.extend_from_slice(fname);
664 extramsg.extend_from_slice(b": ");
665 extramsg.extend_from_slice(detail);
666 lua_vm::debug::arg_error_impl(state, 1, &extramsg)
667 } else {
668 let mut msg = Vec::with_capacity(fname.len() + detail.len() + 20);
669 msg.extend_from_slice(b"cannot open file '");
670 msg.extend_from_slice(fname);
671 msg.extend_from_slice(b"' (");
672 msg.extend_from_slice(detail);
673 msg.push(b')');
674 lua_vm::debug::c_api_runtime(state, msg)
675 }
676}
677
678fn opencheck(state: &mut LuaState, fname: &[u8], mode: &[u8]) -> Result<(), LuaError> {
683 let hook = state.global().file_open_hook;
684 let fh = match hook {
685 Some(open_fn) => match open_fn(fname, mode) {
686 Ok(fh) => fh,
687 Err(e) => {
688 let detail = strerror_text(&e);
689 return Err(open_failure_error(state, fname, detail.as_bytes()));
690 }
691 },
692 None => {
693 return Err(open_failure_error(
694 state,
695 fname,
696 b"no filesystem hook registered",
697 ));
698 }
699 };
700 let cell = new_file(state)?;
701 cell.borrow_mut().file = Some(fh);
702 Ok(())
703}
704
705fn io_fclose(state: &mut LuaState) -> Result<usize, LuaError> {
714 let p_rc = get_lstream(state)?;
715 let _closed = p_rc.borrow_mut().file.take();
716 state.push(LuaValue::Bool(true));
717 Ok(1)
718}
719
720fn io_pclose(state: &mut LuaState) -> Result<usize, LuaError> {
725 let p_rc = get_lstream(state)?;
726 let _closed = p_rc.borrow_mut().file.take();
727 exec_result(state, 0)
728}
729
730fn io_noclose(state: &mut LuaState) -> Result<usize, LuaError> {
735 let p_rc = get_lstream(state)?;
736 p_rc.borrow_mut().close_fn = Some(io_noclose);
737 state.push(LuaValue::Bool(false));
738 state.push_string(b"cannot close standard file")?;
739 Ok(2)
740}
741
742fn aux_close(state: &mut LuaState) -> Result<usize, LuaError> {
744 let p_rc = get_lstream(state)?;
745 let cf = p_rc.borrow_mut().close_fn.take().ok_or_else(|| {
746 LuaError::runtime(format_args!("attempt to close an already-closed file"))
747 })?;
748 cf(state)
749}
750
751pub fn io_type(state: &mut LuaState) -> Result<usize, LuaError> {
761 state.check_arg_any(1)?;
762 let maybe_userdata = state.test_arg_userdata(1, LUA_FILE_HANDLE);
763 match maybe_userdata {
764 None => {
765 state.push(LuaValue::Nil);
766 }
767 Some(ud) => {
768 let is_closed = match lookup_lstream(ud.identity()) {
769 Some(rc) => rc.borrow().is_closed(),
770 None => true,
771 };
772 if is_closed {
773 state.push_string(b"closed file")?;
774 } else {
775 state.push_string(b"file")?;
776 }
777 }
778 }
779 Ok(1)
780}
781
782fn f_tostring(state: &mut LuaState) -> Result<usize, LuaError> {
791 let p_rc = get_lstream(state)?;
792 let closed = p_rc.borrow().is_closed();
793 if closed {
794 state.push_string(b"file (closed)")?;
795 } else {
796 state.push_string(b"file (0x?)")?;
797 }
798 Ok(1)
799}
800
801fn f_close(state: &mut LuaState) -> Result<usize, LuaError> {
805 let _ = tofile(state)?; aux_close(state)
807}
808
809pub fn io_close(state: &mut LuaState) -> Result<usize, LuaError> {
811 if state.type_at(1) == LuaType::None {
815 state.registry_get(IO_OUTPUT_KEY)?;
816 }
817 f_close(state)
818}
819
820fn f_gc(state: &mut LuaState) -> Result<usize, LuaError> {
822 let p_rc = get_lstream(state)?;
823 let needs_close = {
824 let p = p_rc.borrow();
825 !p.is_closed() && p.file.is_some()
826 };
827 if needs_close {
828 let _ = aux_close(state);
830 }
831 Ok(0)
832}
833
834pub fn io_open(state: &mut LuaState) -> Result<usize, LuaError> {
841 let filename: Vec<u8> = state.check_arg_string(1)?;
842 let mode: Vec<u8> = state.opt_arg_string(2, b"r")?;
843 if !check_mode(&mode) {
844 return Err(lua_vm::debug::arg_error_impl(state, 2, b"invalid mode"));
845 }
846 let hook = state.global().file_open_hook;
847 match hook {
848 Some(open_fn) => match open_fn(&filename, &mode) {
849 Ok(fh) => {
850 let cell = new_file(state)?;
851 cell.borrow_mut().file = Some(fh);
852 Ok(1)
853 }
854 Err(os_err) => file_result(state, false, Some(&filename), os_err),
855 },
856 None => {
857 let os_err =
858 io::Error::new(io::ErrorKind::Unsupported, "no filesystem hook registered");
859 file_result(state, false, Some(&filename), os_err)
860 }
861 }
862}
863
864pub fn io_popen(state: &mut LuaState) -> Result<usize, LuaError> {
873 let filename: Vec<u8> = state.check_arg_string(1)?;
874 let mode: Vec<u8> = state.opt_arg_string(2, b"r")?;
875 if !check_mode_popen(&mode) {
876 return Err(lua_vm::debug::arg_error_impl(state, 2, b"invalid mode"));
877 }
878 let hook = state.global().popen_hook;
879 match hook {
880 Some(spawn_fn) => match spawn_fn(&filename, &mode) {
881 Ok(fh) => {
882 let cell = new_pre_file(state)?;
883 let mut p = cell.borrow_mut();
884 p.file = Some(fh);
885 p.close_fn = Some(io_pclose);
886 drop(p);
887 Ok(1)
888 }
889 Err(e) => {
890 let os_err = io::Error::new(
891 io::ErrorKind::Other,
892 match e.message_bytes() {
893 Some(b) => String::from_utf8_lossy(b).into_owned(),
894 None => format!("{:?}", &e),
895 },
896 );
897 file_result(state, false, Some(&filename), os_err)
898 }
899 },
900 None => {
901 let os_err = io::Error::new(
902 io::ErrorKind::Unsupported,
903 "popen not enabled in this build",
904 );
905 file_result(state, false, Some(&filename), os_err)
906 }
907 }
908}
909
910fn native_temp_name() -> io::Result<Vec<u8>> {
911 #[cfg(all(target_arch = "wasm32", target_os = "unknown"))]
912 {
913 return Err(io::Error::new(
914 io::ErrorKind::Unsupported,
915 "temporary files not available in this host",
916 ));
917 }
918
919 #[cfg(not(all(target_arch = "wasm32", target_os = "unknown")))]
920 {
921 let mut path = std::env::temp_dir().to_string_lossy().as_bytes().to_vec();
922 if path.last().copied() != Some(b'/') && path.last().copied() != Some(b'\\') {
923 path.push(b'/');
924 }
925 let unique = format!(
926 "lua_tmpfile_{}_{}",
927 std::process::id(),
928 std::time::SystemTime::now()
929 .duration_since(std::time::UNIX_EPOCH)
930 .map(|d| d.as_nanos())
931 .unwrap_or(0)
932 );
933 path.extend_from_slice(unique.as_bytes());
934 Ok(path)
935 }
936}
937
938pub fn io_tmpfile(state: &mut LuaState) -> Result<usize, LuaError> {
940 let hook = state.global().file_open_hook;
941 let Some(open_fn) = hook else {
942 let os_err = io::Error::new(io::ErrorKind::Unsupported, "no filesystem hook registered");
943 return file_result(state, false, None, os_err);
944 };
945
946 let temp_name_hook = state.global().temp_name_hook;
947 let path = match temp_name_hook {
948 Some(temp_fn) => match temp_fn() {
949 Ok(path) => path,
950 Err(e) => {
951 let msg = match e.message_bytes() {
952 Some(b) => String::from_utf8_lossy(b).into_owned(),
953 None => format!("{:?}", &e),
954 };
955 return file_result(
956 state,
957 false,
958 None,
959 io::Error::new(io::ErrorKind::Unsupported, msg),
960 );
961 }
962 },
963 None => match native_temp_name() {
964 Ok(path) => path,
965 Err(e) => return file_result(state, false, None, e),
966 },
967 };
968
969 match open_fn(&path, b"w+b") {
970 Ok(fh) => {
971 let cell = new_file(state)?;
972 cell.borrow_mut().file = Some(fh);
973 Ok(1)
974 }
975 Err(os_err) => file_result(state, false, None, os_err),
976 }
977}
978
979fn g_iofile(state: &mut LuaState, key: &[u8], mode: &[u8]) -> Result<usize, LuaError> {
983 if !matches!(state.type_at(1), LuaType::None | LuaType::Nil) {
984 if state.type_at(1) == LuaType::String {
985 let filename = state.check_arg_string(1)?;
986 opencheck(state, &filename, mode)?;
987 } else {
988 let _ = tofile(state)?;
989 state.push_value_at(1)?;
990 }
991 state.registry_set(key)?;
992 }
993 state.registry_get(key)?;
994 Ok(1)
995}
996
997pub fn io_input(state: &mut LuaState) -> Result<usize, LuaError> {
999 g_iofile(state, IO_INPUT_KEY, b"r")
1000}
1001
1002pub fn io_output(state: &mut LuaState) -> Result<usize, LuaError> {
1004 g_iofile(state, IO_OUTPUT_KEY, b"w")
1005}
1006
1007fn read_number_bytes(file: &mut dyn LuaFileHandle) -> Vec<u8> {
1015 let first = loop {
1016 let b = file.read_byte();
1017 if b == EOF_SENTINEL || !(b as u8).is_ascii_whitespace() {
1018 break b;
1019 }
1020 };
1021
1022 let mut rn = ReadNumState::new(first);
1023
1024 rn.try2(file, [b'-', b'+']);
1025
1026 let mut count: usize = 0;
1027 let hex = if rn.try2(file, [b'0', b'0']) {
1028 if rn.try2(file, [b'x', b'X']) {
1029 true
1030 } else {
1031 count = 1;
1032 false
1033 }
1034 } else {
1035 false
1036 };
1037
1038 count += rn.read_digits(file, hex);
1039
1040 let dec_point = b'.';
1041 if rn.try2(file, [dec_point, b'.']) {
1042 count += rn.read_digits(file, hex);
1043 }
1044
1045 if count > 0 {
1046 let exp_chars = if hex { [b'p', b'P'] } else { [b'e', b'E'] };
1047 if rn.try2(file, exp_chars) {
1048 rn.try2(file, [b'-', b'+']);
1049 rn.read_digits(file, false);
1050 }
1051 }
1052
1053 file.unread_byte(rn.current);
1054 rn.as_bytes().to_vec()
1055}
1056
1057fn test_eof(file: &mut dyn LuaFileHandle) -> bool {
1060 let c = file.read_byte();
1061 if c != EOF_SENTINEL {
1062 file.unread_byte(c);
1063 }
1064 c != EOF_SENTINEL
1065}
1066
1067fn read_line(file: &mut dyn LuaFileHandle, chop: bool) -> (Vec<u8>, bool) {
1075 let mut buf: Vec<u8> = Vec::new();
1076 let mut c: i32;
1077
1078 'outer: loop {
1079 for _ in 0..LUAL_BUFFER_SIZE {
1080 c = file.read_byte();
1081 if c == EOF_SENTINEL || c == b'\n' as i32 {
1082 break 'outer;
1083 }
1084 buf.push(c as u8);
1085 }
1086 }
1087
1088 if !chop && c == b'\n' as i32 {
1089 buf.push(b'\n');
1090 }
1091
1092 let had_content = c == b'\n' as i32 || !buf.is_empty();
1093 (buf, had_content)
1094}
1095
1096fn read_all(file: &mut dyn LuaFileHandle) -> Vec<u8> {
1102 let mut buf: Vec<u8> = Vec::new();
1103 loop {
1104 let mut chunk_read = 0usize;
1105 for _ in 0..LUAL_BUFFER_SIZE {
1106 let b = file.read_byte();
1107 if b == EOF_SENTINEL {
1108 break;
1109 }
1110 buf.push(b as u8);
1111 chunk_read += 1;
1112 }
1113 if chunk_read < LUAL_BUFFER_SIZE {
1114 break;
1115 }
1116 }
1117 buf
1118}
1119
1120fn read_chars(file: &mut dyn LuaFileHandle, n: usize) -> (Vec<u8>, bool) {
1122 let mut buf = Vec::with_capacity(n);
1123 for _ in 0..n {
1124 let b = file.read_byte();
1125 if b == EOF_SENTINEL {
1126 break;
1127 }
1128 buf.push(b as u8);
1129 }
1130 let nr = buf.len();
1131 (buf, nr > 0)
1132}
1133
1134#[derive(Clone, Copy, PartialEq, Eq)]
1137enum ReadFormat {
1138 Number,
1139 Line,
1140 LineWithEol,
1141 All,
1142}
1143
1144fn read_format_requires_star(version: lua_types::LuaVersion) -> bool {
1150 matches!(
1151 version,
1152 lua_types::LuaVersion::V51 | lua_types::LuaVersion::V52
1153 )
1154}
1155
1156fn read_format_has_line_with_eol(version: lua_types::LuaVersion) -> bool {
1161 version != lua_types::LuaVersion::V51
1162}
1163
1164fn resolve_read_format(
1177 version: lua_types::LuaVersion,
1178 fmt: &[u8],
1179) -> Result<ReadFormat, &'static [u8]> {
1180 let option = if read_format_requires_star(version) {
1181 if fmt.first() != Some(&b'*') {
1182 return Err(b"invalid option");
1183 }
1184 fmt.get(1).copied()
1185 } else if fmt.first() == Some(&b'*') {
1186 fmt.get(1).copied()
1187 } else {
1188 fmt.first().copied()
1189 };
1190 match option {
1191 Some(b'n') => Ok(ReadFormat::Number),
1192 Some(b'l') => Ok(ReadFormat::Line),
1193 Some(b'L') if read_format_has_line_with_eol(version) => Ok(ReadFormat::LineWithEol),
1194 Some(b'a') => Ok(ReadFormat::All),
1195 _ => Err(b"invalid format"),
1196 }
1197}
1198
1199fn g_read(
1205 state: &mut LuaState,
1206 p_rc: &Rc<RefCell<LStream>>,
1207 first: i32,
1208) -> Result<usize, LuaError> {
1209 let nargs = (state.top() - first + 1).max(0);
1215 let mut n = first;
1216 let mut success = true;
1217
1218 {
1219 let mut p = p_rc.borrow_mut();
1220 let fh = p.file.as_mut().expect("open stream has no file handle");
1221 fh.clear_error();
1222 }
1223
1224 if nargs == 0 {
1225 let (bytes, had) = {
1226 let mut p = p_rc.borrow_mut();
1227 let fh = p
1228 .file
1229 .as_deref_mut()
1230 .expect("open stream has no file handle");
1231 read_line(fh, true)
1232 };
1233 state.push_string(&bytes)?;
1234 success = had;
1235 n = first + 1;
1236 } else {
1237 state.ensure_stack((nargs as i32) + 20, "too many arguments")?;
1238 let mut remaining = nargs;
1239 while remaining > 0 && success {
1240 if state.type_at(n) == LuaType::Number {
1241 let l = state.check_arg_integer(n)? as usize;
1242 if l == 0 {
1243 let not_eof = {
1244 let mut p = p_rc.borrow_mut();
1245 let fh = p
1246 .file
1247 .as_deref_mut()
1248 .expect("open stream has no file handle");
1249 test_eof(fh)
1250 };
1251 state.push_string(b"")?;
1252 success = not_eof;
1253 } else {
1254 let (bytes, had) = {
1255 let mut p = p_rc.borrow_mut();
1256 let fh = p
1257 .file
1258 .as_deref_mut()
1259 .expect("open stream has no file handle");
1260 read_chars(fh, l)
1261 };
1262 state.push_string(&bytes)?;
1263 success = had;
1264 }
1265 } else {
1266 let s: Vec<u8> = state.check_arg_string(n)?;
1267 let version = state.global().lua_version;
1268 let format = match resolve_read_format(version, &s) {
1269 Ok(format) => format,
1270 Err(extramsg) => {
1271 return Err(lua_vm::debug::arg_error_impl(state, n, extramsg));
1272 }
1273 };
1274 match format {
1275 ReadFormat::Number => {
1276 let bytes = {
1277 let mut p = p_rc.borrow_mut();
1278 let fh = p
1279 .file
1280 .as_deref_mut()
1281 .expect("open stream has no file handle");
1282 read_number_bytes(fh)
1283 };
1284 let pushed = state.string_to_number_push(&bytes)?;
1285 if pushed != 0 {
1286 success = true;
1287 } else {
1288 state.push(LuaValue::Nil);
1289 success = false;
1290 }
1291 }
1292 ReadFormat::Line => {
1293 let (bytes, had) = {
1294 let mut p = p_rc.borrow_mut();
1295 let fh = p
1296 .file
1297 .as_deref_mut()
1298 .expect("open stream has no file handle");
1299 read_line(fh, true)
1300 };
1301 state.push_string(&bytes)?;
1302 success = had;
1303 }
1304 ReadFormat::LineWithEol => {
1305 let (bytes, had) = {
1306 let mut p = p_rc.borrow_mut();
1307 let fh = p
1308 .file
1309 .as_deref_mut()
1310 .expect("open stream has no file handle");
1311 read_line(fh, false)
1312 };
1313 state.push_string(&bytes)?;
1314 success = had;
1315 }
1316 ReadFormat::All => {
1317 let bytes = {
1318 let mut p = p_rc.borrow_mut();
1319 let fh = p
1320 .file
1321 .as_deref_mut()
1322 .expect("open stream has no file handle");
1323 read_all(fh)
1324 };
1325 state.push_string(&bytes)?;
1326 success = true;
1327 }
1328 }
1329 }
1330 n += 1;
1331 remaining -= 1;
1332 }
1333 }
1334
1335 let has_err = {
1336 let p = p_rc.borrow();
1337 match p.file.as_deref() {
1338 Some(fh) => fh.has_error(),
1339 None => false,
1340 }
1341 };
1342 if has_err {
1343 let err = {
1344 let p = p_rc.borrow();
1345 match p.file.as_deref().and_then(|fh| fh.last_error_info()) {
1346 Some((code, _msg)) if code != 0 => io::Error::from_raw_os_error(code),
1347 Some((_code, msg)) => io::Error::new(io::ErrorKind::Other, msg),
1348 None => io::Error::new(io::ErrorKind::Other, "file read error"),
1349 }
1350 };
1351 return file_result(state, false, None, err);
1352 }
1353
1354 if !success {
1355 state.pop_n(1);
1356 state.push(LuaValue::Nil);
1357 }
1358
1359 Ok((n - first) as usize)
1360}
1361
1362fn get_io_file_rc(state: &mut LuaState, key: &[u8]) -> Result<Rc<RefCell<LStream>>, LuaError> {
1367 state.registry_get(key)?;
1368 let ud_id = state
1369 .test_arg_userdata(-1, LUA_FILE_HANDLE)
1370 .map(|ud| ud.identity());
1371 state.pop_n(1);
1372 let label = &key[IO_PREFIX_LEN..];
1373 let id = ud_id.ok_or_else(|| {
1374 LuaError::runtime(format_args!(
1375 "default {} file is invalid",
1376 label.escape_ascii()
1377 ))
1378 })?;
1379 let rc = lookup_lstream(id).ok_or_else(|| {
1380 LuaError::runtime(format_args!(
1381 "default {} file is invalid",
1382 label.escape_ascii()
1383 ))
1384 })?;
1385 if rc.borrow().is_closed() {
1386 return Err(LuaError::runtime(format_args!(
1387 "default {} file is closed",
1388 label.escape_ascii()
1389 )));
1390 }
1391 Ok(rc)
1392}
1393
1394pub fn io_read(state: &mut LuaState) -> Result<usize, LuaError> {
1396 let p_rc = get_io_file_rc(state, IO_INPUT_KEY)?;
1397 g_read(state, &p_rc, 1)
1398}
1399
1400pub fn f_read(state: &mut LuaState) -> Result<usize, LuaError> {
1402 let p_rc = tofile(state)?;
1403 g_read(state, &p_rc, 2)
1404}
1405
1406fn num_to_write_bytes(state: &mut LuaState, val: &LuaValue) -> Result<Vec<u8>, LuaError> {
1417 let s = lua_vm::object::num_to_string(state, val)?;
1418 Ok(s.as_bytes().to_vec())
1419}
1420
1421fn collect_write_chunks(state: &mut LuaState, first_arg: i32) -> Result<Vec<Vec<u8>>, LuaError> {
1426 let n = state.top() as i32;
1427 let mut chunks: Vec<Vec<u8>> = Vec::with_capacity((n - first_arg + 1).max(0) as usize);
1428 for i in first_arg..=n {
1429 if state.type_at(i) == LuaType::Number {
1430 let val = state.value_at(i);
1431 chunks.push(num_to_write_bytes(state, &val)?);
1432 } else {
1433 let bytes: Vec<u8> = state.check_arg_string(i)?;
1434 chunks.push(bytes);
1435 }
1436 }
1437 Ok(chunks)
1438}
1439
1440fn g_write(
1452 state: &mut LuaState,
1453 p_rc: &Rc<RefCell<LStream>>,
1454 chunks: &[Vec<u8>],
1455) -> Result<Option<usize>, LuaError> {
1456 let mut total: usize = 0;
1457 let write_err: Option<io::Error> = {
1458 let mut p = p_rc.borrow_mut();
1459 let fh = p.file.as_mut().expect("open stream has no file handle");
1460 let mut err = None;
1461 for chunk in chunks {
1462 match fh.write_bytes(chunk) {
1463 Ok(written) => {
1464 total += written;
1465 if written < chunk.len() {
1466 err = Some(io::Error::new(io::ErrorKind::Other, "short write"));
1467 break;
1468 }
1469 }
1470 Err(e) => {
1471 err = Some(e);
1472 break;
1473 }
1474 }
1475 }
1476 err
1477 };
1478
1479 match write_err {
1480 None => Ok(None),
1481 Some(e) => {
1482 let mut nres = file_result(state, false, None, e)?;
1483 if matches!(state.global().lua_version, lua_types::LuaVersion::V55) {
1484 state.push(LuaValue::Int(total as i64));
1485 nres += 1;
1486 }
1487 Ok(Some(nres))
1488 }
1489 }
1490}
1491
1492pub fn io_write(state: &mut LuaState) -> Result<usize, LuaError> {
1498 let chunks = collect_write_chunks(state, 1)?;
1499 let p_rc = get_io_file_rc(state, IO_OUTPUT_KEY)?;
1500 match g_write(state, &p_rc, &chunks)? {
1501 None => {
1502 state.registry_get(IO_OUTPUT_KEY)?;
1503 Ok(1)
1504 }
1505 Some(nres) => Ok(nres),
1506 }
1507}
1508
1509pub fn f_write(state: &mut LuaState) -> Result<usize, LuaError> {
1511 let p_rc = tofile(state)?;
1512 let chunks = collect_write_chunks(state, 2)?;
1513 match g_write(state, &p_rc, &chunks)? {
1514 None => {
1515 state.push_value_at(1)?;
1516 Ok(1)
1517 }
1518 Some(nres) => Ok(nres),
1519 }
1520}
1521
1522pub fn f_seek(state: &mut LuaState) -> Result<usize, LuaError> {
1526 static MODE_NAMES: &[&[u8]] = &[b"set", b"cur", b"end"];
1527
1528 let p_rc = tofile(state)?;
1529 let op = state.check_arg_option(2, Some(b"cur"), MODE_NAMES)?;
1530 let p3: i64 = state.opt_arg_integer(3, 0)?;
1531
1532 let seek_pos = match op {
1533 0 => SeekFrom::Start(p3 as u64),
1534 1 => SeekFrom::Current(p3),
1535 2 => SeekFrom::End(p3),
1536 _ => unreachable!(),
1537 };
1538
1539 let result = {
1540 let mut p = p_rc.borrow_mut();
1541 let fh = p.file.as_mut().expect("open stream has no file handle");
1542 fh.seek(seek_pos)
1543 };
1544 match result {
1545 Ok(pos) => {
1546 state.push(LuaValue::Int(pos as i64));
1547 Ok(1)
1548 }
1549 Err(e) => file_result(state, false, None, e),
1550 }
1551}
1552
1553pub fn f_setvbuf(state: &mut LuaState) -> Result<usize, LuaError> {
1555 static MODE_NAMES: &[&[u8]] = &[b"no", b"full", b"line"];
1556
1557 let p_rc = tofile(state)?;
1558 let op = state.check_arg_option(2, None, MODE_NAMES)?;
1559 let sz: i64 = state.opt_arg_integer(3, LUAL_BUFFER_SIZE as i64)?;
1560 let mode = match op {
1561 0 => BufMode::No,
1562 1 => BufMode::Full,
1563 2 => BufMode::Line,
1564 _ => unreachable!(),
1565 };
1566 let result = {
1567 let mut p = p_rc.borrow_mut();
1568 let fh = p.file.as_mut().expect("open stream has no file handle");
1569 let mode_index = match mode {
1570 BufMode::No => 0,
1571 BufMode::Full => 1,
1572 BufMode::Line => 2,
1573 };
1574 fh.set_buf_mode(mode_index, sz.max(0) as usize)
1575 };
1576 match result {
1577 Ok(()) => file_result(state, true, None, io::Error::last_os_error()),
1578 Err(e) => file_result(state, false, None, e),
1579 }
1580}
1581
1582pub fn io_flush(state: &mut LuaState) -> Result<usize, LuaError> {
1584 let ud_id: Option<usize> = {
1585 state.registry_get(IO_OUTPUT_KEY)?;
1586 let id = state
1587 .test_arg_userdata(-1, LUA_FILE_HANDLE)
1588 .map(|ud| ud.identity());
1589 state.pop_n(1);
1590 id
1591 };
1592 if let Some(id) = ud_id {
1593 if let Some(rc) = lookup_lstream(id) {
1594 let result = {
1595 let mut p = rc.borrow_mut();
1596 if p.is_closed() {
1597 return Err(LuaError::runtime(format_args!(
1598 "default output file is closed"
1599 )));
1600 }
1601 let fh = p
1602 .file
1603 .as_deref_mut()
1604 .expect("open stream has no file handle");
1605 fh.flush()
1606 };
1607 return match result {
1608 Ok(()) => {
1609 state.push(LuaValue::Bool(true));
1610 Ok(1)
1611 }
1612 Err(e) => file_result(state, false, None, e),
1613 };
1614 }
1615 }
1616 state.push(LuaValue::Bool(true));
1618 Ok(1)
1619}
1620
1621pub fn f_flush(state: &mut LuaState) -> Result<usize, LuaError> {
1623 let p_rc = tofile(state)?;
1624 let result = {
1625 let mut p = p_rc.borrow_mut();
1626 let fh = p.file.as_mut().expect("open stream has no file handle");
1627 fh.flush()
1628 };
1629 match result {
1630 Ok(()) => {
1631 state.push(LuaValue::Bool(true));
1632 Ok(1)
1633 }
1634 Err(e) => file_result(state, false, None, e),
1635 }
1636}
1637
1638fn aux_lines(state: &mut LuaState, toclose: bool) -> Result<(), LuaError> {
1648 let n = state.top() - 1;
1651 if n > MAX_ARG_LINE as i32 {
1652 return Err(lua_vm::debug::arg_error_impl(
1653 state,
1654 MAX_ARG_LINE as i32 + 2,
1655 b"too many arguments",
1656 ));
1657 }
1658 state.push_value_at(1)?;
1659 state.push(LuaValue::Int(n as i64));
1660 state.push(LuaValue::Bool(toclose));
1661 state.rotate(2, 3)?;
1662 state.push_c_closure(io_readline, (3 + n) as i32)?;
1663 Ok(())
1664}
1665
1666pub fn f_lines(state: &mut LuaState) -> Result<usize, LuaError> {
1668 let _ = tofile(state)?; aux_lines(state, false)?;
1670 Ok(1)
1671}
1672
1673pub fn io_lines(state: &mut LuaState) -> Result<usize, LuaError> {
1675 if state.type_at(1) == LuaType::None {
1676 state.push(LuaValue::Nil);
1677 }
1678 let toclose = if state.type_at(1) == LuaType::Nil {
1679 state.registry_get(IO_INPUT_KEY)?;
1680 state.replace(1)?;
1681 let _ = tofile(state)?;
1682 false
1683 } else {
1684 let filename = state.check_arg_string(1)?;
1685 opencheck(state, &filename, b"r")?;
1686 state.replace(1)?;
1687 true
1688 };
1689
1690 aux_lines(state, toclose)?;
1691
1692 if toclose && state.global().lua_version.lines_returns_to_be_closed() {
1693 state.push(LuaValue::Nil); state.push(LuaValue::Nil); state.push_value_at(1)?; Ok(4)
1697 } else {
1698 Ok(1)
1699 }
1700}
1701
1702fn io_readline(state: &mut LuaState) -> Result<usize, LuaError> {
1710 let n = match state.value_at(crate::state_stub::upvalue_index(2)) {
1711 LuaValue::Int(i) => i as usize,
1712 _ => 0,
1713 };
1714
1715 let p_rc = lstream_from_upvalue(state, 1)?;
1716
1717 if p_rc.borrow().is_closed() {
1718 return Err(LuaError::runtime(format_args!("file is already closed")));
1719 }
1720
1721 lua_vm::api::set_top(state, 1)?;
1722 state.ensure_stack(n as i32, "too many arguments")?;
1723
1724 for i in 1..=n {
1725 let uv = state.value_at(crate::state_stub::upvalue_index(3 + i as i32));
1726 state.push(uv);
1727 }
1728
1729 let result_n: usize = g_read(state, &p_rc, 2)?;
1730
1731 debug_assert!(result_n > 0, "g_read should return at least one value");
1732
1733 let top = state.top_idx().get() as i32;
1734 let first_result_idx = top - result_n as i32;
1735 let first_truthy = !matches!(
1736 state.stack_at(first_result_idx),
1737 LuaValue::Nil | LuaValue::Bool(false)
1738 );
1739 if first_truthy {
1740 return Ok(result_n);
1741 }
1742
1743 if result_n > 1 {
1744 let err_val = state.stack_at(first_result_idx + 1).clone();
1745 return Err(LuaError::from_value(err_val));
1746 }
1747
1748 let toclose = !matches!(
1749 state.value_at(crate::state_stub::upvalue_index(3)),
1750 LuaValue::Nil | LuaValue::Bool(false)
1751 );
1752 if toclose {
1753 lua_vm::api::set_top(state, 0)?;
1754 state.push_upvalue(1)?;
1755 aux_close(state)?;
1756 }
1757
1758 Ok(0)
1759}
1760
1761fn create_meta(state: &mut LuaState) -> Result<(), LuaError> {
1765 state.new_metatable(LUA_FILE_HANDLE)?;
1766 state.set_funcs(FILE_METAMETHODS, 0)?;
1767 state.new_lib_table(FILE_METHODS)?;
1768 state.set_funcs(FILE_METHODS, 0)?;
1769 state.set_field(-2, b"__index")?;
1770 state.pop_n(1);
1771 Ok(())
1772}
1773
1774fn create_std_file(
1776 state: &mut LuaState,
1777 std_kind: StdFileKind,
1778 registry_key: Option<&[u8]>,
1779 field_name: &[u8],
1780) -> Result<(), LuaError> {
1781 let cell = new_pre_file(state)?;
1782 let output_hook = match std_kind {
1783 StdFileKind::Stdout => state.global().stdout_hook,
1784 StdFileKind::Stderr => state.global().stderr_hook,
1785 StdFileKind::Stdin => None,
1786 };
1787 let input_hook = match std_kind {
1788 StdFileKind::Stdin => state.global().stdin_hook,
1789 StdFileKind::Stdout | StdFileKind::Stderr => None,
1790 };
1791 {
1792 let mut p = cell.borrow_mut();
1793 p.file = Some(Box::new(StdStreamHandle::new(
1794 std_kind,
1795 input_hook,
1796 output_hook,
1797 )));
1798 p.close_fn = Some(io_noclose);
1799 }
1800 if let Some(key) = registry_key {
1801 state.push_value_at(-1)?;
1802 state.registry_set(key)?;
1803 }
1804 state.set_field(-2, field_name)?;
1805 Ok(())
1806}
1807
1808pub fn luaopen_io(state: &mut LuaState) -> Result<usize, LuaError> {
1810 state.new_lib(IO_LIB)?;
1811 create_meta(state)?;
1812 create_std_file(state, StdFileKind::Stdin, Some(IO_INPUT_KEY), b"stdin")?;
1813 create_std_file(state, StdFileKind::Stdout, Some(IO_OUTPUT_KEY), b"stdout")?;
1814 create_std_file(state, StdFileKind::Stderr, None, b"stderr")?;
1815 Ok(1)
1816}