github.com/arnodel/golua@v0.0.0-20230215163904-e0b5347eaaa1/lib/iolib/iolib.go (about)

     1  package iolib
     2  
     3  import (
     4  	"bufio"
     5  	"errors"
     6  	"fmt"
     7  	"io"
     8  	"os"
     9  	"os/exec"
    10  	"runtime"
    11  	"syscall"
    12  
    13  	"github.com/arnodel/golua/lib/packagelib"
    14  	rt "github.com/arnodel/golua/runtime"
    15  )
    16  
    17  // BufferedStdFiles sets wether std files should be buffered
    18  var BufferedStdFiles bool = true
    19  
    20  type ioKeyType struct{}
    21  
    22  var ioKey = rt.AsValue(ioKeyType{})
    23  
    24  // LibLoader can load the io lib.
    25  var LibLoader = packagelib.Loader{
    26  	Load: load,
    27  	Name: "io",
    28  }
    29  
    30  func load(r *rt.Runtime) (rt.Value, func()) {
    31  	methods := rt.NewTable()
    32  
    33  	meta := rt.NewTable()
    34  	r.SetEnv(meta, "__name", rt.StringValue("file"))
    35  	r.SetEnv(meta, "__index", rt.TableValue(methods))
    36  
    37  	rt.SolemnlyDeclareCompliance(
    38  		rt.ComplyCpuSafe|rt.ComplyMemSafe|rt.ComplyIoSafe,
    39  
    40  		r.SetEnvGoFunc(methods, "read", fileread, 1, true),
    41  		r.SetEnvGoFunc(methods, "lines", filelines, 1, true),
    42  		r.SetEnvGoFunc(methods, "close", fileclose, 1, false),
    43  		r.SetEnvGoFunc(methods, "flush", fileflush, 1, false),
    44  		r.SetEnvGoFunc(methods, "seek", fileseek, 3, false),
    45  		r.SetEnvGoFunc(methods, "setvbuf", filesetvbuf, 3, false),
    46  		// TODO: setvbuf,
    47  		r.SetEnvGoFunc(methods, "write", filewrite, 1, true),
    48  
    49  		r.SetEnvGoFunc(meta, "__close", file__close, 1, false),
    50  		// r.SetEnvGoFunc(meta, "__gc", file__close, 1, false),
    51  	)
    52  
    53  	rt.SolemnlyDeclareCompliance(
    54  		rt.ComplyCpuSafe|rt.ComplyMemSafe|rt.ComplyTimeSafe|rt.ComplyTimeSafe|rt.ComplyIoSafe,
    55  
    56  		r.SetEnvGoFunc(meta, "__tostring", tostring, 1, false),
    57  	)
    58  
    59  	var (
    60  		stdoutOpts = statusNotClosable
    61  		stderrOpts = statusNotClosable
    62  		stdinOpts  = statusNotClosable
    63  	)
    64  	if BufferedStdFiles {
    65  		stdoutOpts |= bufferedWrite
    66  		stdinOpts |= bufferedRead
    67  	}
    68  
    69  	stdinFile := NewFile(os.Stdin, stdinOpts)
    70  	stdoutFile := NewFile(os.Stdout, stdoutOpts)
    71  	stderrFile := NewFile(os.Stderr, stderrOpts)
    72  	// This is not a good pattern - it has to do for now.
    73  	if r.Stdout == nil {
    74  		r.Stdout = stdoutFile.writer
    75  	}
    76  	stdin := r.NewUserDataValue(stdinFile, meta)
    77  	stdout := r.NewUserDataValue(stdoutFile, meta)
    78  	stderr := r.NewUserDataValue(stderrFile, meta) // I''m guessing, don't buffer stderr?
    79  
    80  	r.SetRegistry(ioKey, rt.AsValue(&ioData{
    81  		defaultOutput: stdout.AsUserData(),
    82  		defaultInput:  stdin.AsUserData(),
    83  		metatable:     meta,
    84  	}))
    85  	pkg := rt.NewTable()
    86  	r.SetEnv(pkg, "stdin", stdin)
    87  	r.SetEnv(pkg, "stdout", stdout)
    88  	r.SetEnv(pkg, "stderr", stderr)
    89  
    90  	rt.SolemnlyDeclareCompliance(
    91  		rt.ComplyCpuSafe|rt.ComplyMemSafe|rt.ComplyIoSafe,
    92  
    93  		r.SetEnvGoFunc(pkg, "close", ioclose, 1, false),
    94  		r.SetEnvGoFunc(pkg, "flush", ioflush, 0, false),
    95  		r.SetEnvGoFunc(pkg, "input", input, 1, false),
    96  		r.SetEnvGoFunc(pkg, "lines", iolines, 1, true),
    97  		r.SetEnvGoFunc(pkg, "open", open, 2, false),
    98  		r.SetEnvGoFunc(pkg, "output", output, 1, false),
    99  		r.SetEnvGoFunc(pkg, "popen", popen, 2, false),
   100  		r.SetEnvGoFunc(pkg, "read", ioread, 0, true),
   101  		r.SetEnvGoFunc(pkg, "tmpfile", tmpfile, 0, false),
   102  		r.SetEnvGoFunc(pkg, "write", iowrite, 0, true),
   103  	)
   104  
   105  	rt.SolemnlyDeclareCompliance(
   106  		rt.ComplyCpuSafe|rt.ComplyMemSafe|rt.ComplyTimeSafe|rt.ComplyIoSafe,
   107  
   108  		r.SetEnvGoFunc(pkg, "type", typef, 1, false),
   109  	)
   110  
   111  	// This function should make sure known buffers are flushed before quitting
   112  	var cleanup = func() {
   113  		getIoData(r).defaultOutputFile().Flush()
   114  		stdoutFile.Flush()
   115  		stderrFile.Flush()
   116  	}
   117  
   118  	return rt.TableValue(pkg), cleanup
   119  }
   120  
   121  type ioData struct {
   122  	defaultOutput *rt.UserData
   123  	defaultInput  *rt.UserData
   124  	metatable     *rt.Table
   125  }
   126  
   127  func getIoData(r *rt.Runtime) *ioData {
   128  	return r.Registry(ioKey).Interface().(*ioData)
   129  }
   130  
   131  func (d *ioData) defaultOutputFile() *File {
   132  	return d.defaultOutput.Value().(*File)
   133  }
   134  
   135  func (d *ioData) defaultInputFile() *File {
   136  	return d.defaultInput.Value().(*File)
   137  }
   138  
   139  func pushingNextIoResult(r *rt.Runtime, c *rt.GoCont, ioErr error) (rt.Cont, error) {
   140  	next := c.Next()
   141  	if ioErr != nil {
   142  		return r.ProcessIoError(next, ioErr)
   143  	}
   144  	r.Push1(next, rt.BoolValue(true))
   145  	return next, nil
   146  }
   147  
   148  func ioclose(t *rt.Thread, c *rt.GoCont) (rt.Cont, error) {
   149  	var f *File
   150  	if c.NArgs() == 0 {
   151  		f = getIoData(t.Runtime).defaultOutputFile()
   152  	} else {
   153  		var err error
   154  		f, err = FileArg(c, 0)
   155  		if err != nil {
   156  			return nil, err
   157  		}
   158  	}
   159  
   160  	var cont rt.Cont
   161  	var err error
   162  	if f.file == nil {
   163  		cont, err = f.close(t, c)
   164  	} else {
   165  		cont, err = pushingNextIoResult(t.Runtime, c, f.Close())
   166  	}
   167  
   168  	return cont, err
   169  }
   170  
   171  func fileclose(t *rt.Thread, c *rt.GoCont) (rt.Cont, error) {
   172  	if err := c.Check1Arg(); err != nil {
   173  		return nil, err
   174  	}
   175  	return ioclose(t, c)
   176  }
   177  
   178  func file__close(t *rt.Thread, c *rt.GoCont) (rt.Cont, error) {
   179  	if err := c.Check1Arg(); err != nil {
   180  		return nil, err
   181  	}
   182  	f, err := FileArg(c, 0)
   183  	if err != nil {
   184  		return nil, err
   185  	}
   186  	f.cleanup()
   187  	return c.Next(), nil
   188  }
   189  
   190  func ioflush(t *rt.Thread, c *rt.GoCont) (rt.Cont, error) {
   191  	var f *File
   192  	if c.NArgs() == 0 {
   193  		f = getIoData(t.Runtime).defaultOutputFile()
   194  	} else {
   195  		var err error
   196  		f, err = FileArg(c, 0)
   197  		if err != nil {
   198  			return nil, err
   199  		}
   200  	}
   201  	return pushingNextIoResult(t.Runtime, c, f.Flush())
   202  }
   203  
   204  func fileflush(t *rt.Thread, c *rt.GoCont) (rt.Cont, error) {
   205  	if err := c.Check1Arg(); err != nil {
   206  		return nil, err
   207  	}
   208  	return ioflush(t, c)
   209  }
   210  
   211  func errFileOrFilename() error {
   212  	return errors.New("#1 must be a file or a filename")
   213  }
   214  
   215  func input(t *rt.Thread, c *rt.GoCont) (rt.Cont, error) {
   216  	ioData := getIoData(t.Runtime)
   217  	if c.NArgs() == 0 {
   218  		return c.PushingNext1(t.Runtime, rt.UserDataValue(ioData.defaultInput)), nil
   219  	}
   220  	var (
   221  		fv  rt.Value
   222  		arg = c.Arg(0)
   223  	)
   224  	switch arg.Type() {
   225  	case rt.StringType:
   226  		f, ioErr := OpenFile(t.Runtime, arg.AsString(), "r")
   227  		if ioErr != nil {
   228  			return nil, ioErr
   229  		}
   230  		fv = t.NewUserDataValue(f, ioData.metatable)
   231  	case rt.UserDataType:
   232  		_, err := FileArg(c, 0)
   233  		if err != nil {
   234  			return nil, errFileOrFilename()
   235  		}
   236  		fv = arg
   237  	default:
   238  		return nil, errFileOrFilename()
   239  	}
   240  	ioData.defaultInput = fv.AsUserData()
   241  	return c.PushingNext1(t.Runtime, fv), nil
   242  }
   243  
   244  func output(t *rt.Thread, c *rt.GoCont) (rt.Cont, error) {
   245  	ioData := getIoData(t.Runtime)
   246  	if c.NArgs() == 0 {
   247  		return c.PushingNext1(t.Runtime, rt.UserDataValue(ioData.defaultOutput)), nil
   248  	}
   249  	var (
   250  		fv  rt.Value
   251  		arg = c.Arg(0)
   252  	)
   253  	switch arg.Type() {
   254  	case rt.StringType:
   255  		f, ioErr := OpenFile(t.Runtime, arg.AsString(), "w")
   256  		if ioErr != nil {
   257  			return nil, ioErr
   258  		}
   259  		fv = t.NewUserDataValue(f, ioData.metatable)
   260  	case rt.UserDataType:
   261  		_, err := FileArg(c, 0)
   262  		if err != nil {
   263  			return nil, errFileOrFilename()
   264  		}
   265  		fv = arg
   266  	default:
   267  		return nil, errFileOrFilename()
   268  	}
   269  	// Make sure the current output is flushed
   270  	ioData.defaultOutput.Value().(*File).Flush()
   271  	ioData.defaultOutput = fv.AsUserData()
   272  	return c.PushingNext1(t.Runtime, fv), nil
   273  }
   274  
   275  func iolines(t *rt.Thread, c *rt.GoCont) (rt.Cont, error) {
   276  	var (
   277  		f         *File
   278  		eofAction = closeAtEOF
   279  	)
   280  	if c.NArgs() == 0 || c.Arg(0) == rt.NilValue {
   281  		f = getIoData(t.Runtime).defaultInputFile()
   282  		eofAction = doNotCloseAtEOF
   283  	} else {
   284  		fname, err := c.StringArg(0)
   285  		if err != nil {
   286  			return nil, err
   287  		}
   288  		var ioErr error
   289  		f, ioErr = OpenFile(t.Runtime, string(fname), "r")
   290  		if ioErr != nil {
   291  			return nil, ioErr
   292  		}
   293  	}
   294  	readers, fmtErr := getFormatReaders(c.Etc())
   295  	if fmtErr != nil {
   296  		return nil, fmtErr
   297  	}
   298  	return c.PushingNext(t.Runtime, rt.FunctionValue(lines(t.Runtime, f, readers, eofAction))), nil
   299  }
   300  
   301  func filelines(t *rt.Thread, c *rt.GoCont) (rt.Cont, error) {
   302  	if err := c.Check1Arg(); err != nil {
   303  		return nil, err
   304  	}
   305  	f, err := FileArg(c, 0)
   306  	if err != nil {
   307  		return nil, err
   308  	}
   309  	readers, fmtErr := getFormatReaders(c.Etc())
   310  	if fmtErr != nil {
   311  		return nil, fmtErr
   312  	}
   313  
   314  	return c.PushingNext(t.Runtime, rt.FunctionValue(lines(t.Runtime, f, readers, doNotCloseAtEOF))), nil
   315  }
   316  
   317  const (
   318  	closeAtEOF      = 1
   319  	doNotCloseAtEOF = 0
   320  )
   321  
   322  func lines(r *rt.Runtime, f *File, readers []formatReader, flags int) *rt.GoFunction {
   323  	if len(readers) == 0 {
   324  		readers = []formatReader{lineReader(false)}
   325  	}
   326  	iterator := func(t *rt.Thread, c *rt.GoCont) (rt.Cont, error) {
   327  		next := c.Next()
   328  		// if f.closed {
   329  		// 	return next, nil
   330  		// }
   331  		err := read(r, f, readers, next)
   332  		if err != nil {
   333  			if err == io.EOF {
   334  				if flags&closeAtEOF != 0 {
   335  					if err := f.Close(); err != nil {
   336  						return t.ProcessIoError(next, err)
   337  					}
   338  				}
   339  				t.Push1(next, rt.NilValue)
   340  				return next, nil
   341  			}
   342  			return nil, err
   343  		}
   344  		return next, nil
   345  	}
   346  	iterGof := rt.NewGoFunction(iterator, "linesiterator", 0, false)
   347  	iterGof.SolemnlyDeclareCompliance(rt.ComplyCpuSafe | rt.ComplyMemSafe | rt.ComplyIoSafe)
   348  	return iterGof
   349  
   350  }
   351  
   352  func open(t *rt.Thread, c *rt.GoCont) (rt.Cont, error) {
   353  	if err := c.Check1Arg(); err != nil {
   354  		return nil, err
   355  	}
   356  	fname, err := c.StringArg(0)
   357  	if err != nil {
   358  		return nil, err
   359  	}
   360  	mode := "r"
   361  	if c.NArgs() >= 2 {
   362  		mode, err = c.StringArg(1)
   363  		if err != nil {
   364  			return nil, err
   365  		}
   366  	}
   367  	f, ioErr := OpenFile(t.Runtime, fname, mode)
   368  	if ioErr != nil {
   369  		return pushingNextIoResult(t.Runtime, c, ioErr)
   370  	}
   371  
   372  	fv := t.NewUserDataValue(f, getIoData(t.Runtime).metatable)
   373  	return c.PushingNext(t.Runtime, fv), nil
   374  }
   375  
   376  func popen(t *rt.Thread, c *rt.GoCont) (rt.Cont, error) {
   377  	if err := c.Check1Arg(); err != nil {
   378  		return nil, err
   379  	}
   380  	cmdStr, err := c.StringArg(0)
   381  	if err != nil {
   382  		return nil, err
   383  	}
   384  
   385  	mode := "r"
   386  	if c.NArgs() >= 2 {
   387  		mode, err = c.StringArg(1)
   388  		if err != nil {
   389  			return nil, err
   390  		}
   391  	}
   392  
   393  	var cmdArgs []string
   394  	if runtime.GOOS == "windows" {
   395  		cmdArgs = []string{"C:\\Windows\\system32\\cmd.exe", "/c", cmdStr}
   396  	} else {
   397  		cmdArgs = []string{"/bin/sh", "-c", cmdStr}
   398  	}
   399  
   400  	cmd := exec.Cmd{
   401  		Path: cmdArgs[0],
   402  		Args: cmdArgs,
   403  	}
   404  
   405  	outDummy, inDummy, err := os.Pipe()
   406  	if err != nil {
   407  		return nil, err
   408  	}
   409  
   410  	f := &File{
   411  		writer: &nobufWriter{inDummy},
   412  		reader: &nobufReader{outDummy},
   413  		name: cmdStr,
   414  	}
   415  
   416  	var stdout io.ReadCloser
   417  	var stdin io.WriteCloser
   418  	switch mode {
   419  		case "r":
   420  			stdout, err = cmd.StdoutPipe()
   421  			f.reader = bufio.NewReader(stdout)
   422  		case "w":
   423  			stdin, err = cmd.StdinPipe()
   424  			f.writer = bufio.NewWriterSize(stdin, 65536)
   425  		default:
   426  			err = errors.New("invalid mode")
   427  	}
   428  	// called *only* from io.close
   429  	f.close = func(t *rt.Thread, c *rt.GoCont) (rt.Cont, error) {
   430  		err := f.Close()
   431  		if err != nil {
   432  			return pushingNextIoResult(t.Runtime, c, err)
   433  		}
   434  	
   435  		if stdout != nil {
   436  			err := stdout.Close()
   437  			if err != nil {
   438  				return pushingNextIoResult(t.Runtime, c, err)
   439  			}
   440  		}
   441  		if stdin != nil {
   442  			err := stdin.Close()
   443  			if err != nil {
   444  				return pushingNextIoResult(t.Runtime, c, err)
   445  			}
   446  		}
   447  
   448  		cont := c.Next()
   449  
   450  		cmd.Wait()
   451  		ps := cmd.ProcessState
   452  		if ps.Success() {
   453  			t.Runtime.Push(cont, rt.BoolValue(true))
   454  		} else {
   455  			t.Runtime.Push(cont, rt.NilValue)
   456  		}
   457  
   458  		exit := rt.StringValue("exit")
   459  		code := rt.IntValue(int64(ps.ExitCode()))
   460  		if !ps.Exited() {
   461  			// received signal instead of normal exit
   462  			exit = rt.StringValue("signal")
   463  			if runtime.GOOS != "windows" {
   464  				// i can't find out what Sys() is on windows ...
   465  				ws := ps.Sys().(syscall.WaitStatus)
   466  				sig := ws.Signal()
   467  				code = rt.IntValue(int64(sig)) // syscall.Signal
   468  			}
   469  		}
   470  
   471  		t.Runtime.Push(cont, exit, code)
   472  
   473  		return c.Next(), nil
   474  	}
   475  
   476  	if err != nil {
   477  		return pushingNextIoResult(t.Runtime, c, err)
   478  	}
   479  
   480  	err = cmd.Start()
   481  	if err != nil {
   482  		return nil, err
   483  	}
   484  
   485  	fv := t.NewUserDataValue(f, getIoData(t.Runtime).metatable)
   486  	return c.PushingNext(t.Runtime, fv), nil
   487  }
   488  
   489  func typef(t *rt.Thread, c *rt.GoCont) (rt.Cont, error) {
   490  	if err := c.Check1Arg(); err != nil {
   491  		return nil, err
   492  	}
   493  	f, err := FileArg(c, 0)
   494  	var val rt.Value
   495  	if err != nil {
   496  		val = rt.NilValue
   497  	} else if f.IsClosed() {
   498  		val = rt.StringValue("closed file")
   499  	} else {
   500  		val = rt.StringValue("file")
   501  	}
   502  	return c.PushingNext(t.Runtime, val), nil
   503  }
   504  
   505  func iowrite(t *rt.Thread, c *rt.GoCont) (rt.Cont, error) {
   506  	return write(t.Runtime, rt.UserDataValue(getIoData(t.Runtime).defaultOutput), c)
   507  }
   508  
   509  func filewrite(t *rt.Thread, c *rt.GoCont) (rt.Cont, error) {
   510  	if err := c.Check1Arg(); err != nil {
   511  		return nil, err
   512  	}
   513  	return write(t.Runtime, c.Arg(0), c)
   514  }
   515  
   516  func write(r *rt.Runtime, vf rt.Value, c *rt.GoCont) (rt.Cont, error) {
   517  	f, ok := ValueToFile(vf)
   518  	if !ok {
   519  		return nil, errors.New("#1 must be a file")
   520  	}
   521  	if f.IsClosed() {
   522  		return nil, errFileAlreadyClosed
   523  	}
   524  	var err error
   525  	for _, val := range c.Etc() {
   526  		switch val.Type() {
   527  		case rt.StringType:
   528  		case rt.IntType:
   529  		case rt.FloatType:
   530  		default:
   531  			return nil, errors.New("argument must be a string or a number")
   532  		}
   533  		s, _ := val.ToString()
   534  		if err = f.WriteString(s); err != nil {
   535  			break
   536  		}
   537  	}
   538  	next := c.Next()
   539  	if err != nil {
   540  		return r.ProcessIoError(next, err)
   541  	} else {
   542  		r.Push(next, vf)
   543  	}
   544  	return next, nil
   545  }
   546  
   547  func fileseek(t *rt.Thread, c *rt.GoCont) (rt.Cont, error) {
   548  	if err := c.Check1Arg(); err != nil {
   549  		return nil, err
   550  	}
   551  	f, err := FileArg(c, 0)
   552  	if err != nil {
   553  		return nil, err
   554  	}
   555  	whence := io.SeekCurrent
   556  	offset := int64(0)
   557  	nargs := c.NArgs()
   558  	if nargs >= 2 {
   559  		whenceName, err := c.StringArg(1)
   560  		if err != nil {
   561  			return nil, err
   562  		}
   563  		switch whenceName {
   564  		case "cur":
   565  			whence = io.SeekCurrent
   566  		case "set":
   567  			whence = io.SeekStart
   568  		case "end":
   569  			whence = io.SeekEnd
   570  		default:
   571  			return nil, errors.New(`#1 must be "cur", "set" or "end"`)
   572  		}
   573  	}
   574  	if nargs >= 3 {
   575  		offsetI, err := c.IntArg(2)
   576  		if err != nil {
   577  			return nil, err
   578  		}
   579  		offset = int64(offsetI)
   580  	}
   581  	pos, ioErr := f.Seek(offset, whence)
   582  	next := c.Next()
   583  	if ioErr != nil {
   584  		t.Push1(next, rt.NilValue)
   585  		t.Push1(next, rt.StringValue(ioErr.Error()))
   586  	} else {
   587  		t.Push1(next, rt.IntValue(pos))
   588  	}
   589  	return next, nil
   590  }
   591  
   592  func filesetvbuf(t *rt.Thread, c *rt.GoCont) (rt.Cont, error) {
   593  	if err := c.CheckNArgs(2); err != nil {
   594  		return nil, err
   595  	}
   596  	f, err := FileArg(c, 0)
   597  	if err != nil {
   598  		return nil, err
   599  	}
   600  	mode, err := c.StringArg(1)
   601  	if err != nil {
   602  		return nil, err
   603  	}
   604  	var size int64
   605  	if c.NArgs() > 2 {
   606  		size, err = c.IntArg(2)
   607  		if err != nil {
   608  			return nil, err
   609  		}
   610  	}
   611  	bufErr := f.SetWriteBuffer(mode, int(size))
   612  	if bufErr != nil {
   613  		return nil, bufErr
   614  	}
   615  	return c.PushingNext1(t.Runtime, rt.BoolValue(true)), nil
   616  }
   617  
   618  func tmpfile(t *rt.Thread, c *rt.GoCont) (rt.Cont, error) {
   619  	f, err := TempFile(t.Runtime)
   620  	if err != nil {
   621  		return nil, err
   622  	}
   623  	fv := t.NewUserDataValue(f, getIoData(t.Runtime).metatable)
   624  	return c.PushingNext(t.Runtime, fv), nil
   625  }
   626  
   627  func tostring(t *rt.Thread, c *rt.GoCont) (rt.Cont, error) {
   628  	if err := c.Check1Arg(); err != nil {
   629  		return nil, err
   630  	}
   631  	f, err := FileArg(c, 0)
   632  	if err != nil {
   633  		return nil, err
   634  	}
   635  	var s string
   636  	if f.IsClosed() {
   637  		s = "file (closed)"
   638  	} else {
   639  		s = fmt.Sprintf("file (%q)", f.Name())
   640  	}
   641  	t.RequireBytes(len(s))
   642  	return c.PushingNext(t.Runtime, rt.StringValue(s)), nil
   643  }