package sys

import (
	
	
	

	
	
	
	socketapi 
	
)

const (
	FdStdin int32 = iota
	FdStdout
	FdStderr
	// FdPreopen is the file descriptor of the first pre-opened directory.
	//
	// # Why file descriptor 3?
	//
	// While not specified, the most common WASI implementation, wasi-libc,
	// expects POSIX style file descriptor allocation, where the lowest
	// available number is used to open the next file. Since 1 and 2 are taken
	// by stdout and stderr, the next is 3.
	//   - https://github.com/WebAssembly/WASI/issues/122
	//   - https://pubs.opengroup.org/onlinepubs/9699919799/functions/V2_chap02.html#tag_15_14
	//   - https://github.com/WebAssembly/wasi-libc/blob/wasi-sdk-16/libc-bottom-half/sources/preopens.c#L215
	FdPreopen
)

const modeDevice = fs.ModeDevice | 0o640

// FileEntry maps a path to an open file in a file system.
type FileEntry struct {
	// Name is the name of the directory up to its pre-open, or the pre-open
	// name itself when IsPreopen.
	//
	// # Notes
	//
	//   - This can drift on rename.
	//   - This relates to the guest path, which is not the real file path
	//     except if the entire host filesystem was made available.
	Name string

	// IsPreopen is a directory that is lazily opened.
	IsPreopen bool

	// FS is the filesystem associated with the pre-open.
	FS sys.FS

	// File is always non-nil.
	File fsapi.File

	// direntCache is nil until DirentCache was called.
	direntCache *DirentCache
}

// DirentCache gets or creates a DirentCache for this file or returns an error.
//
// # Errors
//
// A zero sys.Errno is success. The below are expected otherwise:
//   - sys.ENOSYS: the implementation does not support this function.
//   - sys.EBADF: the dir was closed or not readable.
//   - sys.ENOTDIR: the file was not a directory.
//
// # Notes
//
//   - See /RATIONALE.md for design notes.
func ( *FileEntry) () (*DirentCache, sys.Errno) {
	if  := .direntCache;  != nil {
		return , 0
	}

	// Require the file to be a directory vs a late error on the same.
	if ,  := .File.IsDir();  != 0 {
		return nil, 
	} else if ! {
		return nil, sys.ENOTDIR
	}

	// Generate the dotEntries only once.
	if ,  := synthesizeDotEntries();  != 0 {
		return nil, 
	} else {
		.direntCache = &DirentCache{f: .File, dotEntries: }
	}

	return .direntCache, 0
}

// DirentCache is a caching abstraction of sys.File Readdir.
//
// This is special-cased for "wasi_snapshot_preview1.fd_readdir", and may be
// unneeded, or require changes, to support preview1 or preview2.
//   - The position of the dirents are serialized as `d_next`. For reasons
//     described below, any may need to be re-read. This accepts any positions
//     in the cache, rather than track the position of the last dirent.
//   - dot entries ("." and "..") must be returned. See /RATIONALE.md for why.
//   - An sys.Dirent Name is variable length, it could exceed memory size and
//     need to be re-read.
//   - Multiple dirents may be returned. It is more efficient to read from the
//     underlying file in bulk vs one-at-a-time.
//
// The last results returned by Read are cached, but entries before that
// position are not. This support re-reading entries that couldn't fit into
// memory without accidentally caching all entries in a large directory. This
// approach is sometimes called a sliding window.
type DirentCache struct {
	// f is the underlying file
	f sys.File

	// dotEntries are the "." and ".." entries added when the directory is
	// initialized.
	dotEntries []sys.Dirent

	// dirents are the potentially unread directory entries.
	//
	// Internal detail: nil is different from zero length. Zero length is an
	// exhausted directory (eof). nil means the re-read.
	dirents []sys.Dirent

	// countRead is the total count of dirents read since last rewind.
	countRead uint64

	// eof is true when the underlying file is at EOF. This avoids re-reading
	// the directory when it is exhausted. Entires in an exhausted directory
	// are not visible until it is rewound via calling Read with `pos==0`.
	eof bool
}

// synthesizeDotEntries generates a slice of the two elements "." and "..".
func synthesizeDotEntries( *FileEntry) ([]sys.Dirent, sys.Errno) {
	,  := .File.Ino()
	if  != 0 {
		return nil, 
	}
	 := [2]sys.Dirent{}
	[0] = sys.Dirent{Name: ".", Ino: , Type: fs.ModeDir}
	// See /RATIONALE.md for why we don't attempt to get an inode for ".." and
	// why in wasi-libc this won't fan-out either.
	[1] = sys.Dirent{Name: "..", Ino: 0, Type: fs.ModeDir}
	return [:], 0
}

// exhaustedDirents avoids allocating empty slices.
var exhaustedDirents = [0]sys.Dirent{}

// Read is similar to and returns the same errors as `Readdir` on sys.File.
// The main difference is this caches entries returned, resulting in multiple
// valid positions to read from.
//
// When zero, `pos` means rewind to the beginning of this directory. This
// implies a rewind (Seek to zero on the underlying sys.File), unless the
// initial entries are still cached.
//
// When non-zero, `pos` is the zero based index of all dirents returned since
// last rewind. Only entries beginning at `pos` are cached for subsequent
// calls. A non-zero `pos` before the cache returns sys.ENOENT for reasons
// described on DirentCache documentation.
//
// Up to `n` entries are cached and returned. When `n` exceeds the cache, the
// difference are read from the underlying sys.File via `Readdir`. EOF is
// when `len(dirents)` returned are less than `n`.
func ( *DirentCache) ( uint64,  uint32) ( []sys.Dirent,  sys.Errno) {
	switch {
	case  > .countRead: // farther than read or negative coerced to uint64.
		return nil, sys.ENOENT
	case  == 0 && .dirents != nil:
		// Rewind if we have already read entries. This allows us to see new
		// entries added after the directory was opened.
		if _,  = .f.Seek(0, io.SeekStart);  != 0 {
			return
		}
		.dirents = nil // dump cache
		.countRead = 0
	}

	if  == 0 {
		return // special case no entries.
	}

	if .dirents == nil {
		// Always populate dot entries, which makes min len(dirents) == 2.
		.dirents = .dotEntries
		.countRead = 2
		.eof = false

		if  := int( - 2);  <= 0 {
			return
		} else if ,  = .f.Readdir();  != 0 {
			return
		} else if  := len();  > 0 {
			.eof =  < 
			.dirents = append(.dotEntries, ...)
			.countRead += uint64()
		}

		return .cachedDirents(), 0
	}

	// Reset our cache to the first entry being read.
	 := .countRead - uint64(len(.dirents))
	if  <  {
		// We don't currently allow reads before our cache because Seek(0) is
		// the only portable way. Doing otherwise requires skipping, which we
		// won't do unless wasi-testsuite starts requiring it. Implementing
		// this would allow re-reading a large directory, so care would be
		// needed to not buffer the entire directory in memory while skipping.
		 = sys.ENOENT
		return
	} else if  :=  - ;  != 0 {
		if uint64(len(.dirents)) ==  {
			// Avoid allocation re-slicing to zero length.
			.dirents = exhaustedDirents[:]
		} else {
			.dirents = .dirents[:]
		}
	}

	// See if we need more entries.
	if  := int() - len(.dirents);  > 0 && !.eof {
		// Try to read more, which could fail.
		if ,  = .f.Readdir();  != 0 {
			return
		}

		// Append the next read entries if we weren't at EOF.
		if  := len();  > 0 {
			.eof =  < 
			.dirents = append(.dirents, ...)
			.countRead += uint64()
		}
	}

	return .cachedDirents(), 0
}

// cachedDirents returns up to `n` dirents from the cache.
func ( *DirentCache) ( uint32) []sys.Dirent {
	 := uint32(len(.dirents))
	switch {
	case  == 0:
		return nil
	case  > :
		return .dirents[:]
	}
	return .dirents
}

type FSContext struct {
	// openedFiles is a map of file descriptor numbers (>=FdPreopen) to open files
	// (or directories) and defaults to empty.
	// TODO: This is unguarded, so not goroutine-safe!
	openedFiles FileTable
}

// FileTable is a specialization of the descriptor.Table type used to map file
// descriptors to file entries.
type FileTable = descriptor.Table[int32, *FileEntry]

// LookupFile returns a file if it is in the table.
func ( *FSContext) ( int32) (*FileEntry, bool) {
	return .openedFiles.Lookup()
}

// OpenFile opens the file into the table and returns its file descriptor.
// The result must be closed by CloseFile or Close.
func ( *FSContext) ( sys.FS,  string,  sys.Oflag,  fs.FileMode) (int32, sys.Errno) {
	if ,  := .OpenFile(, , );  != 0 {
		return 0, 
	} else {
		 := &FileEntry{FS: , File: fsapi.Adapt()}
		if  == "/" ||  == "." {
			.Name = ""
		} else {
			.Name = 
		}
		if ,  := .openedFiles.Insert(); ! {
			return 0, sys.EBADF
		} else {
			return , 0
		}
	}
}

// Renumber assigns the file pointed by the descriptor `from` to `to`.
func ( *FSContext) (,  int32) sys.Errno {
	,  := .openedFiles.Lookup()
	if ! ||  < 0 {
		return sys.EBADF
	} else if .IsPreopen {
		return sys.ENOTSUP
	}

	// If toFile is already open, we close it to prevent windows lock issues.
	//
	// The doc is unclear and other implementations do nothing for already-opened To FDs.
	// https://github.com/WebAssembly/WASI/blob/snapshot-01/phases/snapshot/docs.md#-fd_renumberfd-fd-to-fd---errno
	// https://github.com/bytecodealliance/wasmtime/blob/main/crates/wasi-common/src/snapshots/preview_1.rs#L531-L546
	if ,  := .openedFiles.Lookup();  {
		if .IsPreopen {
			return sys.ENOTSUP
		}
		_ = .File.Close()
	}

	.openedFiles.Delete()
	if !.openedFiles.InsertAt(, ) {
		return sys.EBADF
	}
	return 0
}

// SockAccept accepts a sock.TCPConn into the file table and returns its file
// descriptor.
func ( *FSContext) ( int32,  bool) (int32, sys.Errno) {
	var  socketapi.TCPSock
	if ,  := .LookupFile(); ! || !.IsPreopen {
		return 0, sys.EBADF // Not a preopen
	} else if ,  = .File.(socketapi.TCPSock); ! {
		return 0, sys.EBADF // Not a sock
	}

	,  := .Accept()
	if  != 0 {
		return 0, 
	}

	 := &FileEntry{File: fsapi.Adapt()}

	if  {
		if  = .File.SetNonblock(true);  != 0 {
			_ = .Close()
			return 0, 
		}
	}

	if ,  := .openedFiles.Insert(); ! {
		return 0, sys.EBADF
	} else {
		return , 0
	}
}

// CloseFile returns any error closing the existing file.
func ( *FSContext) ( int32) ( sys.Errno) {
	,  := .openedFiles.Lookup()
	if ! {
		return sys.EBADF
	}
	if  = .File.Close();  != 0 {
		return 
	}
	.openedFiles.Delete()
	return 
}

// Close implements io.Closer
func ( *FSContext) () ( error) {
	// Close any files opened in this context
	.openedFiles.Range(func( int32,  *FileEntry) bool {
		if  := .File.Close();  != 0 {
			 =  // This means err returned == the last non-nil error.
		}
		return true
	})
	// A closed FSContext cannot be reused so clear the state.
	.openedFiles = FileTable{}
	return
}

// InitFSContext initializes a FSContext with stdio streams and optional
// pre-opened filesystems and TCP listeners.
func ( *Context) (
	 io.Reader,
	,  io.Writer,
	 []sys.FS,  []string,
	 []*net.TCPListener,
) ( error) {
	,  := stdinFileEntry()
	if  != nil {
		return 
	}
	.fsc.openedFiles.Insert()
	,  := stdioWriterFileEntry("stdout", )
	if  != nil {
		return 
	}
	.fsc.openedFiles.Insert()
	,  := stdioWriterFileEntry("stderr", )
	if  != nil {
		return 
	}
	.fsc.openedFiles.Insert()

	for ,  := range  {
		 := []

		if StripPrefixesAndTrailingSlash() == "" {
			// Default to bind to '/' when guestPath is effectively empty.
			 = "/"
		}
		.fsc.openedFiles.Insert(&FileEntry{
			FS:        ,
			Name:      ,
			IsPreopen: true,
			File:      &lazyDir{fs: },
		})
	}

	for ,  := range  {
		.fsc.openedFiles.Insert(&FileEntry{IsPreopen: true, File: fsapi.Adapt(sysfs.NewTCPListenerFile())})
	}
	return nil
}

// StripPrefixesAndTrailingSlash skips any leading "./" or "/" such that the
// result index begins with another string. A result of "." coerces to the
// empty string "" because the current directory is handled by the guest.
//
// Results are the offset/len pair which is an optimization to avoid re-slicing
// overhead, as this function is called for every path operation.
//
// Note: Relative paths should be handled by the guest, as that's what knows
// what the current directory is. However, paths that escape the current
// directory e.g. "../.." have been found in `tinygo test` and this
// implementation takes care to avoid it.
func ( string) string {
	// strip trailing slashes
	 := len()
	for ;  > 0 && [-1] == '/'; -- {
	}

	 := 0
:
	for  <  {
		switch [] {
		case '/':
			++
		case '.':
			 :=  + 1
			if  <  && [] == '/' {
				 =  + 1
			} else if  ==  {
				 = 
			} else {
				break 
			}
		default:
			break 
		}
	}
	return [:]
}