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

     1  package iolib
     2  
     3  import (
     4  	"bufio"
     5  	"errors"
     6  	"fmt"
     7  	"io"
     8  	"io/fs"
     9  	"io/ioutil"
    10  	"os"
    11  	"strings"
    12  
    13  	rt "github.com/arnodel/golua/runtime"
    14  	"github.com/arnodel/golua/safeio"
    15  	"github.com/arnodel/golua/scanner"
    16  	"github.com/arnodel/golua/token"
    17  )
    18  
    19  const (
    20  	bufferedRead int = 1 << iota
    21  	bufferedWrite
    22  	notClosable
    23  	tempFile
    24  )
    25  
    26  var (
    27  	errCloseStandardFile = errors.New("cannot close standard file")
    28  	errFileAlreadyClosed = errors.New("file already closed")
    29  	errInvalidBufferMode = errors.New("invalid buffer mode")
    30  	errInvalidBufferSize = errors.New("invalid buffer size")
    31  )
    32  
    33  // A File wraps an os.File for manipulation by iolib.
    34  type File struct {
    35  	file   *os.File
    36  	name   string
    37  	close func(*rt.Thread, *rt.GoCont) (rt.Cont, error)
    38  	status fileStatus
    39  	reader bufReader
    40  	writer bufWriter
    41  }
    42  
    43  var _ rt.UserDataResourceReleaser = (*File)(nil)
    44  
    45  type fileStatus int
    46  
    47  const (
    48  	statusClosed = 1 << iota
    49  	statusTemp
    50  	statusNotClosable
    51  )
    52  
    53  // NewFile returns a new *File from an *os.File.
    54  func NewFile(file *os.File, options int) *File {
    55  	f := &File{file: file, name: file.Name()}
    56  	// TODO: find out if there is mileage in having unbuffered readers.
    57  	if true || options&bufferedRead != 0 {
    58  		f.reader = bufio.NewReader(file)
    59  	} else {
    60  		f.reader = &nobufReader{file}
    61  	}
    62  	if options&bufferedWrite != 0 {
    63  		f.writer = bufio.NewWriterSize(file, 65536)
    64  	} else {
    65  		f.writer = &nobufWriter{file}
    66  	}
    67  	if options&tempFile != 0 {
    68  		f.status |= statusTemp
    69  	}
    70  	if options&notClosable != 0 {
    71  		f.status |= statusNotClosable
    72  	}
    73  	return f
    74  }
    75  
    76  // OpenFile opens a file with the given name in the given lua mode.
    77  func OpenFile(r *rt.Runtime, name, mode string) (*File, error) {
    78  	var flag, options int
    79  	switch strings.TrimSuffix(mode, "b") {
    80  	case "r":
    81  		flag = os.O_RDONLY
    82  		options = bufferedRead
    83  	case "w":
    84  		flag = os.O_WRONLY | os.O_CREATE | os.O_TRUNC
    85  		options = bufferedWrite
    86  	case "a":
    87  		flag = os.O_WRONLY | os.O_CREATE | os.O_APPEND
    88  		options = bufferedWrite
    89  	case "r+":
    90  		flag = os.O_RDWR
    91  		options = bufferedRead | bufferedWrite
    92  	case "w+":
    93  		flag = os.O_RDWR | os.O_CREATE | os.O_TRUNC
    94  		options = bufferedRead | bufferedWrite
    95  	case "a+":
    96  		flag = os.O_RDWR | os.O_CREATE | os.O_APPEND
    97  		options = bufferedRead | bufferedWrite
    98  	default:
    99  		return nil, errors.New("invalid mode")
   100  	}
   101  	f, err := safeio.OpenFile(r, name, flag, 0666)
   102  	if err != nil {
   103  		return nil, err
   104  	}
   105  	return NewFile(f, options), nil
   106  }
   107  
   108  // TempFile tries to make a temporary file, and if successful schedules the file
   109  // to be removed when the process dies.
   110  func TempFile(r *rt.Runtime) (*File, error) {
   111  	f, err := safeio.TempFile(r, "", "golua")
   112  	if err != nil {
   113  		return nil, err
   114  	}
   115  	ff := NewFile(f, bufferedRead|bufferedWrite|tempFile)
   116  	return ff, nil
   117  }
   118  
   119  // FileArg turns a continuation argument into a *File.
   120  func FileArg(c *rt.GoCont, n int) (*File, error) {
   121  	f, ok := ValueToFile(c.Arg(n))
   122  	if ok {
   123  		return f, nil
   124  	}
   125  	return nil, fmt.Errorf("#%d must be a file", n+1)
   126  }
   127  
   128  // ValueToFile turns a lua value to a *File if possible.
   129  func ValueToFile(v rt.Value) (*File, bool) {
   130  	u, ok := v.TryUserData()
   131  	if ok {
   132  		return u.Value().(*File), true
   133  	}
   134  	return nil, false
   135  }
   136  
   137  // IsClosed returns true if the file is closed.
   138  func (f *File) IsClosed() bool {
   139  	return f.status&statusClosed != 0
   140  }
   141  
   142  // IsTemp returns true if the file is temporary.
   143  func (f *File) IsTemp() bool {
   144  	return f.status&statusTemp != 0
   145  }
   146  
   147  func (f *File) IsClosable() bool {
   148  	return f.status&statusNotClosable == 0
   149  }
   150  
   151  // Close attempts to close the file, returns an error if not successful.
   152  func (f *File) Close() error {
   153  	if !f.IsClosable() {
   154  		// Lua doesn't return a Lua error, so wrap this in a PathError
   155  		return &fs.PathError{
   156  			Op:   "close",
   157  			Path: f.file.Name(),
   158  			Err:  errCloseStandardFile,
   159  		}
   160  	}
   161  	if f.IsClosed() {
   162  		// Also this is undocumented, in this case an error is returned
   163  		return errFileAlreadyClosed
   164  	}
   165  	f.status |= statusClosed
   166  	errFlush := f.writer.Flush()
   167  
   168  	var err error
   169  	if f.file != nil {
   170  		err = f.file.Close()
   171  	}
   172  
   173  	if err == nil {
   174  		return errFlush
   175  	}
   176  	return err
   177  }
   178  
   179  // Flush attempts to sync the file, returns an error if a problem occurs.
   180  func (f *File) Flush() error {
   181  	if err := f.writer.Flush(); err != nil {
   182  		return err
   183  	}
   184  	if f.file != nil {
   185  		return f.file.Sync()
   186  	}
   187  
   188  	return nil
   189  }
   190  
   191  // ReadLine reads a line from the file.  If withEnd is true, it will include the
   192  // end of the line in the returned value.
   193  func (f *File) ReadLine(withEnd bool) (rt.Value, error) {
   194  	s, err := f.reader.ReadString('\n')
   195  	if err != nil && err != io.EOF {
   196  		return rt.NilValue, err
   197  	}
   198  	l := len(s)
   199  	if l == 0 {
   200  		return rt.NilValue, err
   201  	}
   202  	if !withEnd && l > 0 && s[l-1] == '\n' {
   203  		l--
   204  		if l > 1 && s[l-1] == '\r' {
   205  			l--
   206  		}
   207  		s = s[:l]
   208  	}
   209  	return rt.StringValue(s), nil
   210  }
   211  
   212  // Read return a lua string made of up to n bytes.
   213  func (f *File) Read(n int) (rt.Value, error) {
   214  	if n == 0 {
   215  		// Special case when n = 0: we try to peek 1 byte ahead to decide
   216  		// whether it's the end of the file or not.
   217  		_, err := f.reader.Peek(1)
   218  		switch err {
   219  		case nil:
   220  			return rt.StringValue(""), nil
   221  		case io.EOF:
   222  			return rt.NilValue, nil
   223  		default:
   224  			return rt.NilValue, err
   225  		}
   226  	}
   227  	b := make([]byte, n)
   228  	n, err := io.ReadFull(f.reader, b)
   229  	if err == nil || err == io.ErrUnexpectedEOF {
   230  		return rt.StringValue(string(b[:n])), nil
   231  	}
   232  	return rt.NilValue, err
   233  }
   234  
   235  // ReadAll attempts to read the whole file and return a lua string containing
   236  // it.
   237  func (f *File) ReadAll() (rt.Value, error) {
   238  	b, err := ioutil.ReadAll(f.reader)
   239  	if err != nil {
   240  		return rt.NilValue, err
   241  	}
   242  	return rt.StringValue(string(b)), nil
   243  }
   244  
   245  // ReadNumber tries to read a number from the file.
   246  func (f *File) ReadNumber() (rt.Value, error) {
   247  	const maxSize = 64
   248  	bytes, err := f.reader.Peek(maxSize) // Should be enough for any number
   249  	if err != nil && (err != io.EOF || len(bytes) == 0) {
   250  		return rt.NilValue, err
   251  	}
   252  	scan := scanner.New("", bytes, scanner.ForNumber())
   253  	tok := scan.Scan()
   254  	_, _ = f.reader.Discard(len(tok.Lit))
   255  	if tok.Type == token.INVALID || len(tok.Lit) == maxSize {
   256  		return rt.NilValue, nil
   257  	}
   258  	n, x, tp := rt.StringToNumber(string(tok.Lit))
   259  	switch tp {
   260  	case rt.IsInt:
   261  		return rt.IntValue(n), nil
   262  	case rt.IsFloat:
   263  		return rt.FloatValue(x), nil
   264  	default:
   265  		return rt.NilValue, nil
   266  	}
   267  }
   268  
   269  // WriteString writes a string to the file.
   270  func (f *File) WriteString(s string) error {
   271  	_, err := f.writer.Write([]byte(s))
   272  	return err
   273  }
   274  
   275  // Seek seeks from the file.
   276  func (f *File) Seek(offset int64, whence int) (n int64, err error) {
   277  	if f.file == nil {
   278  		// popen'd; seems you can't seek a popen file in original lua impl, so error
   279  		return 0, errors.New("Illegal seek") // not sure what error message to use
   280  	}
   281  
   282  	err = f.writer.Flush()
   283  	if err != nil {
   284  		return
   285  	}
   286  	switch whence {
   287  	case io.SeekStart, io.SeekEnd:
   288  		n, err = f.file.Seek(offset, whence)
   289  		f.reader.Reset(f.file)
   290  		f.writer.Reset(f.file)
   291  	case io.SeekCurrent:
   292  		var n0 int64
   293  		n0, err = f.file.Seek(0, whence)
   294  		bufCount := int64(f.reader.Buffered())
   295  		n = n0 - bufCount + offset
   296  		if err != nil {
   297  			return
   298  		}
   299  		if offset < 0 || bufCount < offset {
   300  			return f.Seek(n, io.SeekStart)
   301  		}
   302  		f.reader.Discard(int(offset))
   303  	}
   304  	return
   305  }
   306  
   307  func (f *File) SetWriteBuffer(mode string, size int) error {
   308  	if size < 0 {
   309  		return errInvalidBufferSize
   310  	}
   311  	f.Flush()
   312  	switch mode {
   313  	case "no":
   314  		f.writer = &nobufWriter{f.file}
   315  	case "full":
   316  		if size == 0 {
   317  			size = 65536
   318  		}
   319  		f.writer = bufio.NewWriterSize(f.file, size)
   320  	case "line":
   321  		if size == 0 {
   322  			size = 65536
   323  		}
   324  		f.writer = linebufWriter{bufio.NewWriterSize(f.file, size)}
   325  		// TODO
   326  	default:
   327  		return errInvalidBufferMode
   328  	}
   329  	return nil
   330  }
   331  
   332  // Name returns the file name.
   333  func (f *File) Name() string {
   334  	return f.name
   335  }
   336  
   337  // ReleaseResources cleans up the file
   338  func (f *File) ReleaseResources(d *rt.UserData) {
   339  	f.cleanup()
   340  }
   341  
   342  // Best effort to flush and close files when they are no longer accessible.
   343  func (f *File) cleanup() {
   344  	if !f.IsClosed() {
   345  		f.Close()
   346  	}
   347  	if f.IsTemp() {
   348  		_ = os.Remove(f.Name())
   349  	}
   350  }