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