github.com/xmx/lua@v0.0.0-20230324063450-8a298e091302/iolib.go (about) 1 package lua 2 3 import ( 4 "bufio" 5 "errors" 6 "fmt" 7 "io" 8 "io/ioutil" 9 "os" 10 "os/exec" 11 "syscall" 12 ) 13 14 var ioFuncs = map[string]LGFunction{ 15 "close": ioClose, 16 "flush": ioFlush, 17 "lines": ioLines, 18 "input": ioInput, 19 "output": ioOutput, 20 "open": ioOpenFile, 21 "popen": ioPopen, 22 "read": ioRead, 23 "type": ioType, 24 "tmpfile": ioTmpFile, 25 "write": ioWrite, 26 } 27 28 const lFileClass = "FILE*" 29 30 type lFile struct { 31 fp *os.File 32 pp *exec.Cmd 33 writer io.Writer 34 reader *bufio.Reader 35 stdout io.ReadCloser 36 closed bool 37 } 38 39 type lFileType int 40 41 const ( 42 lFileFile lFileType = iota 43 lFileProcess 44 ) 45 46 const ( 47 fileDefOutIndex = 1 48 fileDefInIndex = 2 49 fileDefaultWriteBuffer = 4096 50 fileDefaultReadBuffer = 4096 51 ) 52 53 func checkFile(L *LState) *lFile { 54 ud := L.CheckUserData(1) 55 if file, ok := ud.Value.(*lFile); ok { 56 return file 57 } 58 L.ArgError(1, "file expected") 59 return nil 60 } 61 62 func errorIfFileIsClosed(L *LState, file *lFile) { 63 if file.closed { 64 L.ArgError(1, "file is closed") 65 } 66 } 67 68 func newFile(L *LState, file *os.File, path string, flag int, perm os.FileMode, writable, readable bool) (*LUserData, error) { 69 ud := L.NewUserData() 70 var err error 71 if file == nil { 72 file, err = os.OpenFile(path, flag, perm) 73 if err != nil { 74 return nil, err 75 } 76 } 77 lfile := &lFile{fp: file, pp: nil, writer: nil, reader: nil, stdout: nil, closed: false} 78 ud.Value = lfile 79 if writable { 80 lfile.writer = file 81 } 82 if readable { 83 lfile.reader = bufio.NewReaderSize(file, fileDefaultReadBuffer) 84 } 85 L.SetMetatable(ud, L.GetTypeMetatable(lFileClass)) 86 return ud, nil 87 } 88 89 func newProcess(L *LState, cmd string, writable, readable bool) (*LUserData, error) { 90 ud := L.NewUserData() 91 c, args := popenArgs(cmd) 92 pp := exec.Command(c, args...) 93 lfile := &lFile{fp: nil, pp: pp, writer: nil, reader: nil, stdout: nil, closed: false} 94 ud.Value = lfile 95 96 var err error 97 if writable { 98 lfile.writer, err = pp.StdinPipe() 99 } 100 if readable { 101 lfile.stdout, err = pp.StdoutPipe() 102 lfile.reader = bufio.NewReaderSize(lfile.stdout, fileDefaultReadBuffer) 103 } 104 if err != nil { 105 return nil, err 106 } 107 err = pp.Start() 108 if err != nil { 109 return nil, err 110 } 111 112 L.SetMetatable(ud, L.GetTypeMetatable(lFileClass)) 113 return ud, nil 114 } 115 116 func (file *lFile) Type() lFileType { 117 if file.fp == nil { 118 return lFileProcess 119 } 120 return lFileFile 121 } 122 123 func (file *lFile) Name() string { 124 switch file.Type() { 125 case lFileFile: 126 return fmt.Sprintf("file %s", file.fp.Name()) 127 case lFileProcess: 128 return fmt.Sprintf("process %s", file.pp.Path) 129 } 130 return "" 131 } 132 133 func (file *lFile) AbandonReadBuffer() error { 134 if file.Type() == lFileFile && file.reader != nil { 135 _, err := file.fp.Seek(-int64(file.reader.Buffered()), 1) 136 if err != nil { 137 return err 138 } 139 file.reader = bufio.NewReaderSize(file.fp, fileDefaultReadBuffer) 140 } 141 return nil 142 } 143 144 func fileDefOut(L *LState) *LUserData { 145 return L.Get(UpvalueIndex(1)).(*LTable).RawGetInt(fileDefOutIndex).(*LUserData) 146 } 147 148 func fileDefIn(L *LState) *LUserData { 149 return L.Get(UpvalueIndex(1)).(*LTable).RawGetInt(fileDefInIndex).(*LUserData) 150 } 151 152 func fileIsWritable(L *LState, file *lFile) int { 153 if file.writer == nil { 154 L.Push(LNil) 155 L.Push(LString(fmt.Sprintf("%s is opened for only reading.", file.Name()))) 156 L.Push(LNumber(1)) // C-Lua compatibility: Original Lua pushes errno to the stack 157 return 3 158 } 159 return 0 160 } 161 162 func fileIsReadable(L *LState, file *lFile) int { 163 if file.reader == nil { 164 L.Push(LNil) 165 L.Push(LString(fmt.Sprintf("%s is opened for only writing.", file.Name()))) 166 L.Push(LNumber(1)) // C-Lua compatibility: Original Lua pushes errno to the stack 167 return 3 168 } 169 return 0 170 } 171 172 var stdFiles = []struct { 173 name string 174 file *os.File 175 writable bool 176 readable bool 177 }{ 178 {"stdout", os.Stdout, true, false}, 179 {"stdin", os.Stdin, false, true}, 180 {"stderr", os.Stderr, true, false}, 181 } 182 183 func OpenIo(L *LState) int { 184 mod := L.RegisterModule(IoLibName, map[string]LGFunction{}).(*LTable) 185 mt := L.NewTypeMetatable(lFileClass) 186 mt.RawSetString("__index", mt) 187 L.SetFuncs(mt, fileMethods) 188 mt.RawSetString("lines", L.NewClosure(fileLines, L.NewFunction(fileLinesIter))) 189 190 for _, finfo := range stdFiles { 191 file, _ := newFile(L, finfo.file, "", 0, os.FileMode(0), finfo.writable, finfo.readable) 192 mod.RawSetString(finfo.name, file) 193 } 194 uv := L.CreateTable(2, 0) 195 uv.RawSetInt(fileDefOutIndex, mod.RawGetString("stdout")) 196 uv.RawSetInt(fileDefInIndex, mod.RawGetString("stdin")) 197 for name, fn := range ioFuncs { 198 mod.RawSetString(name, L.NewClosure(fn, uv)) 199 } 200 mod.RawSetString("lines", L.NewClosure(ioLines, uv, L.NewClosure(ioLinesIter, uv))) 201 // Modifications are being made in-place rather than returned? 202 L.Push(mod) 203 return 1 204 } 205 206 var fileMethods = map[string]LGFunction{ 207 "__tostring": fileToString, 208 "write": fileWrite, 209 "close": fileClose, 210 "flush": fileFlush, 211 "lines": fileLines, 212 "read": fileRead, 213 "seek": fileSeek, 214 "setvbuf": fileSetVBuf, 215 } 216 217 func fileToString(L *LState) int { 218 file := checkFile(L) 219 if file.Type() == lFileFile { 220 if file.closed { 221 L.Push(LString("file (closed)")) 222 } else { 223 L.Push(LString("file")) 224 } 225 } else { 226 if file.closed { 227 L.Push(LString("process (closed)")) 228 } else { 229 L.Push(LString("process")) 230 } 231 } 232 return 1 233 } 234 235 func fileWriteAux(L *LState, file *lFile, idx int) int { 236 if n := fileIsWritable(L, file); n != 0 { 237 return n 238 } 239 errorIfFileIsClosed(L, file) 240 top := L.GetTop() 241 out := file.writer 242 var err error 243 for i := idx; i <= top; i++ { 244 L.CheckTypes(i, LTNumber, LTString) 245 s := LVAsString(L.Get(i)) 246 if _, err = out.Write(unsafeFastStringToReadOnlyBytes(s)); err != nil { 247 goto errreturn 248 } 249 } 250 251 file.AbandonReadBuffer() 252 L.Push(LTrue) 253 return 1 254 errreturn: 255 256 file.AbandonReadBuffer() 257 L.Push(LNil) 258 L.Push(LString(err.Error())) 259 L.Push(LNumber(1)) // C-Lua compatibility: Original Lua pushes errno to the stack 260 return 3 261 } 262 263 func fileCloseAux(L *LState, file *lFile) int { 264 file.closed = true 265 var err error 266 if file.writer != nil { 267 if bwriter, ok := file.writer.(*bufio.Writer); ok { 268 if err = bwriter.Flush(); err != nil { 269 goto errreturn 270 } 271 } 272 } 273 file.AbandonReadBuffer() 274 275 switch file.Type() { 276 case lFileFile: 277 if err = file.fp.Close(); err != nil { 278 goto errreturn 279 } 280 L.Push(LTrue) 281 return 1 282 case lFileProcess: 283 if file.stdout != nil { 284 file.stdout.Close() // ignore errors 285 } 286 err = file.pp.Wait() 287 var exitStatus int // Initialised to zero value = 0 288 if err != nil { 289 if e2, ok := err.(*exec.ExitError); ok { 290 if s, ok := e2.Sys().(syscall.WaitStatus); ok { 291 exitStatus = s.ExitStatus() 292 } else { 293 err = errors.New("Unimplemented for system where exec.ExitError.Sys() is not syscall.WaitStatus.") 294 } 295 } 296 } else { 297 exitStatus = 0 298 } 299 L.Push(LNumber(exitStatus)) 300 return 1 301 } 302 303 errreturn: 304 L.RaiseError(err.Error()) 305 return 0 306 } 307 308 func fileFlushAux(L *LState, file *lFile) int { 309 if n := fileIsWritable(L, file); n != 0 { 310 return n 311 } 312 errorIfFileIsClosed(L, file) 313 314 if bwriter, ok := file.writer.(*bufio.Writer); ok { 315 if err := bwriter.Flush(); err != nil { 316 L.Push(LNil) 317 L.Push(LString(err.Error())) 318 return 2 319 } 320 } 321 L.Push(LTrue) 322 return 1 323 } 324 325 func fileReadAux(L *LState, file *lFile, idx int) int { 326 if n := fileIsReadable(L, file); n != 0 { 327 return n 328 } 329 errorIfFileIsClosed(L, file) 330 if L.GetTop() == idx-1 { 331 L.Push(LString("*l")) 332 } 333 var err error 334 top := L.GetTop() 335 for i := idx; i <= top; i++ { 336 switch lv := L.Get(i).(type) { 337 case LNumber: 338 size := int64(lv) 339 if size == 0 { 340 _, err = file.reader.ReadByte() 341 if err == io.EOF { 342 L.Push(LNil) 343 goto normalreturn 344 } 345 file.reader.UnreadByte() 346 } 347 var buf []byte 348 var iseof bool 349 buf, err, iseof = readBufioSize(file.reader, size) 350 if iseof { 351 L.Push(LNil) 352 goto normalreturn 353 } 354 if err != nil { 355 goto errreturn 356 } 357 L.Push(LString(string(buf))) 358 case LString: 359 options := L.CheckString(i) 360 if len(options) > 0 && options[0] != '*' { 361 L.ArgError(2, "invalid options:"+options) 362 } 363 for _, opt := range options[1:] { 364 switch opt { 365 case 'n': 366 var v LNumber 367 _, err = fmt.Fscanf(file.reader, LNumberScanFormat, &v) 368 if err == io.EOF { 369 L.Push(LNil) 370 goto normalreturn 371 } 372 if err != nil { 373 goto errreturn 374 } 375 L.Push(v) 376 case 'a': 377 var buf []byte 378 buf, err = ioutil.ReadAll(file.reader) 379 if err == io.EOF { 380 L.Push(emptyLString) 381 goto normalreturn 382 } 383 if err != nil { 384 goto errreturn 385 } 386 L.Push(LString(string(buf))) 387 case 'l': 388 var buf []byte 389 var iseof bool 390 buf, err, iseof = readBufioLine(file.reader) 391 if iseof { 392 L.Push(LNil) 393 goto normalreturn 394 } 395 if err != nil { 396 goto errreturn 397 } 398 L.Push(LString(string(buf))) 399 default: 400 L.ArgError(2, "invalid options:"+string(opt)) 401 } 402 } 403 } 404 } 405 normalreturn: 406 return L.GetTop() - top 407 408 errreturn: 409 L.RaiseError(err.Error()) 410 // L.Push(LNil) 411 // L.Push(LString(err.Error())) 412 return 2 413 } 414 415 var fileSeekOptions = []string{"set", "cur", "end"} 416 417 func fileSeek(L *LState) int { 418 file := checkFile(L) 419 if file.Type() != lFileFile { 420 L.Push(LNil) 421 L.Push(LString("can not seek a process.")) 422 return 2 423 } 424 425 top := L.GetTop() 426 if top == 1 { 427 L.Push(LString("cur")) 428 L.Push(LNumber(0)) 429 } else if top == 2 { 430 L.Push(LNumber(0)) 431 } 432 433 var pos int64 434 var err error 435 436 err = file.AbandonReadBuffer() 437 if err != nil { 438 goto errreturn 439 } 440 441 pos, err = file.fp.Seek(L.CheckInt64(3), L.CheckOption(2, fileSeekOptions)) 442 if err != nil { 443 goto errreturn 444 } 445 446 L.Push(LNumber(pos)) 447 return 1 448 449 errreturn: 450 L.Push(LNil) 451 L.Push(LString(err.Error())) 452 return 2 453 } 454 455 func fileWrite(L *LState) int { 456 return fileWriteAux(L, checkFile(L), 2) 457 } 458 459 func fileClose(L *LState) int { 460 return fileCloseAux(L, checkFile(L)) 461 } 462 463 func fileFlush(L *LState) int { 464 return fileFlushAux(L, checkFile(L)) 465 } 466 467 func fileLinesIter(L *LState) int { 468 var file *lFile 469 if ud, ok := L.Get(1).(*LUserData); ok { 470 file = ud.Value.(*lFile) 471 } else { 472 file = L.Get(UpvalueIndex(2)).(*LUserData).Value.(*lFile) 473 } 474 buf, _, err := file.reader.ReadLine() 475 if err != nil { 476 if err == io.EOF { 477 L.Push(LNil) 478 return 1 479 } 480 L.RaiseError(err.Error()) 481 } 482 L.Push(LString(string(buf))) 483 return 1 484 } 485 486 func fileLines(L *LState) int { 487 file := checkFile(L) 488 ud := L.CheckUserData(1) 489 if n := fileIsReadable(L, file); n != 0 { 490 return 0 491 } 492 L.Push(L.NewClosure(fileLinesIter, L.Get(UpvalueIndex(1)), ud)) 493 return 1 494 } 495 496 func fileRead(L *LState) int { 497 return fileReadAux(L, checkFile(L), 2) 498 } 499 500 var filebufOptions = []string{"no", "full"} 501 502 func fileSetVBuf(L *LState) int { 503 var err error 504 var writer io.Writer 505 file := checkFile(L) 506 if n := fileIsWritable(L, file); n != 0 { 507 return n 508 } 509 switch filebufOptions[L.CheckOption(2, filebufOptions)] { 510 case "no": 511 switch file.Type() { 512 case lFileFile: 513 file.writer = file.fp 514 case lFileProcess: 515 file.writer, err = file.pp.StdinPipe() 516 if err != nil { 517 goto errreturn 518 } 519 } 520 case "full", "line": // TODO line buffer not supported 521 bufsize := L.OptInt(3, fileDefaultWriteBuffer) 522 switch file.Type() { 523 case lFileFile: 524 file.writer = bufio.NewWriterSize(file.fp, bufsize) 525 case lFileProcess: 526 writer, err = file.pp.StdinPipe() 527 if err != nil { 528 goto errreturn 529 } 530 file.writer = bufio.NewWriterSize(writer, bufsize) 531 } 532 } 533 L.Push(LTrue) 534 return 1 535 errreturn: 536 L.Push(LNil) 537 L.Push(LString(err.Error())) 538 return 2 539 } 540 541 func ioInput(L *LState) int { 542 if L.GetTop() == 0 { 543 L.Push(fileDefIn(L)) 544 return 1 545 } 546 switch lv := L.Get(1).(type) { 547 case LString: 548 file, err := newFile(L, nil, string(lv), os.O_RDONLY, 0o600, false, true) 549 if err != nil { 550 L.RaiseError(err.Error()) 551 } 552 L.Get(UpvalueIndex(1)).(*LTable).RawSetInt(fileDefInIndex, file) 553 L.Push(file) 554 return 1 555 case *LUserData: 556 if _, ok := lv.Value.(*lFile); ok { 557 L.Get(UpvalueIndex(1)).(*LTable).RawSetInt(fileDefInIndex, lv) 558 L.Push(lv) 559 return 1 560 } 561 562 } 563 L.ArgError(1, "string or file expedted, but got "+L.Get(1).Type().String()) 564 return 0 565 } 566 567 func ioClose(L *LState) int { 568 if L.GetTop() == 0 { 569 return fileCloseAux(L, fileDefOut(L).Value.(*lFile)) 570 } 571 return fileClose(L) 572 } 573 574 func ioFlush(L *LState) int { 575 return fileFlushAux(L, fileDefOut(L).Value.(*lFile)) 576 } 577 578 func ioLinesIter(L *LState) int { 579 var file *lFile 580 toclose := false 581 if ud, ok := L.Get(1).(*LUserData); ok { 582 file = ud.Value.(*lFile) 583 } else { 584 file = L.Get(UpvalueIndex(2)).(*LUserData).Value.(*lFile) 585 toclose = true 586 } 587 buf, _, err := file.reader.ReadLine() 588 if err != nil { 589 if err == io.EOF { 590 if toclose { 591 fileCloseAux(L, file) 592 } 593 L.Push(LNil) 594 return 1 595 } 596 L.RaiseError(err.Error()) 597 } 598 L.Push(LString(string(buf))) 599 return 1 600 } 601 602 func ioLines(L *LState) int { 603 if L.GetTop() == 0 { 604 L.Push(L.Get(UpvalueIndex(2))) 605 L.Push(fileDefIn(L)) 606 return 2 607 } 608 609 path := L.CheckString(1) 610 ud, err := newFile(L, nil, path, os.O_RDONLY, os.FileMode(0o600), false, true) 611 if err != nil { 612 return 0 613 } 614 L.Push(L.NewClosure(ioLinesIter, L.Get(UpvalueIndex(1)), ud)) 615 return 1 616 } 617 618 var ioOpenOpions = []string{"r", "rb", "w", "wb", "a", "ab", "r+", "rb+", "w+", "wb+", "a+", "ab+"} 619 620 func ioOpenFile(L *LState) int { 621 path := L.CheckString(1) 622 if L.GetTop() == 1 { 623 L.Push(LString("r")) 624 } 625 mode := os.O_RDONLY 626 perm := 0o600 627 writable := true 628 readable := true 629 switch ioOpenOpions[L.CheckOption(2, ioOpenOpions)] { 630 case "r", "rb": 631 mode = os.O_RDONLY 632 writable = false 633 case "w", "wb": 634 mode = os.O_WRONLY | os.O_CREATE 635 readable = false 636 case "a", "ab": 637 mode = os.O_WRONLY | os.O_APPEND | os.O_CREATE 638 case "r+", "rb+": 639 mode = os.O_RDWR 640 case "w+", "wb+": 641 mode = os.O_RDWR | os.O_TRUNC | os.O_CREATE 642 case "a+", "ab+": 643 mode = os.O_APPEND | os.O_RDWR | os.O_CREATE 644 } 645 file, err := newFile(L, nil, path, mode, os.FileMode(perm), writable, readable) 646 if err != nil { 647 L.Push(LNil) 648 L.Push(LString(err.Error())) 649 L.Push(LNumber(1)) // C-Lua compatibility: Original Lua pushes errno to the stack 650 return 3 651 } 652 L.Push(file) 653 return 1 654 } 655 656 var ioPopenOptions = []string{"r", "w"} 657 658 func ioPopen(L *LState) int { 659 cmd := L.CheckString(1) 660 if L.GetTop() == 1 { 661 L.Push(LString("r")) 662 } 663 var file *LUserData 664 var err error 665 666 switch ioPopenOptions[L.CheckOption(2, ioPopenOptions)] { 667 case "r": 668 file, err = newProcess(L, cmd, false, true) 669 case "w": 670 file, err = newProcess(L, cmd, true, false) 671 } 672 if err != nil { 673 L.Push(LNil) 674 L.Push(LString(err.Error())) 675 return 2 676 } 677 L.Push(file) 678 return 1 679 } 680 681 func ioRead(L *LState) int { 682 return fileReadAux(L, fileDefIn(L).Value.(*lFile), 1) 683 } 684 685 func ioType(L *LState) int { 686 ud, udok := L.Get(1).(*LUserData) 687 if !udok { 688 L.Push(LNil) 689 return 1 690 } 691 file, ok := ud.Value.(*lFile) 692 if !ok { 693 L.Push(LNil) 694 return 1 695 } 696 if file.closed { 697 L.Push(LString("closed file")) 698 return 1 699 } 700 L.Push(LString("file")) 701 return 1 702 } 703 704 func ioTmpFile(L *LState) int { 705 file, err := ioutil.TempFile("", "") 706 if err != nil { 707 L.Push(LNil) 708 L.Push(LString(err.Error())) 709 return 2 710 } 711 L.G.tempFiles = append(L.G.tempFiles, file) 712 ud, _ := newFile(L, file, "", 0, os.FileMode(0), true, true) 713 L.Push(ud) 714 return 1 715 } 716 717 func ioOutput(L *LState) int { 718 if L.GetTop() == 0 { 719 L.Push(fileDefOut(L)) 720 return 1 721 } 722 switch lv := L.Get(1).(type) { 723 case LString: 724 file, err := newFile(L, nil, string(lv), os.O_WRONLY|os.O_CREATE, 0o600, true, false) 725 if err != nil { 726 L.RaiseError(err.Error()) 727 } 728 L.Get(UpvalueIndex(1)).(*LTable).RawSetInt(fileDefOutIndex, file) 729 L.Push(file) 730 return 1 731 case *LUserData: 732 if _, ok := lv.Value.(*lFile); ok { 733 L.Get(UpvalueIndex(1)).(*LTable).RawSetInt(fileDefOutIndex, lv) 734 L.Push(lv) 735 return 1 736 } 737 738 } 739 L.ArgError(1, "string or file expedted, but got "+L.Get(1).Type().String()) 740 return 0 741 } 742 743 func ioWrite(L *LState) int { 744 return fileWriteAux(L, fileDefOut(L).Value.(*lFile), 1) 745 } 746 747 //