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¬Closable != 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 }