diff --git a/.vscode/settings.json b/.vscode/settings.json index 3b6ceacf28b..c0006ad597b 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -28,5 +28,8 @@ ], "url": "./.vscode/schemas/meta.schema.json" } + ], + "haxe.configurations": [ + ["--cwd", "tests/asys", "build.hxml"] ] } \ No newline at end of file diff --git a/src/macro/eval/evalHash.ml b/src/macro/eval/evalHash.ml index c1f6d6e31f7..48bcaa0f721 100644 --- a/src/macro/eval/evalHash.ml +++ b/src/macro/eval/evalHash.ml @@ -143,14 +143,12 @@ let key_sys_net_Lock = hash "sys.thread.Lock" let key_sys_net_Tls = hash "sys.thread.Tls" let key_sys_net_Deque = hash "sys.thread.Deque" let key_sys_thread_EventLoop = hash "sys.thread.EventLoop" - let key_mbedtls_Config = hash "mbedtls.Config" let key_mbedtls_CtrDrbg = hash "mbedtls.CtrDrbg" let key_mbedtls_Entropy = hash "mbedtls.Entropy" let key_mbedtls_PkContext = hash "mbedtls.PkContext" let key_mbedtls_Ssl = hash "mbedtls.Ssl" let key_mbedtls_X509Crt = hash "mbedtls.X509Crt" - let key_eval_luv_Result = hash "eval.luv.Result" let key_eval_luv_LuvException = hash "eval.luv.LuvException" let key_eval_luv_ReceiveHandle = hash "eval.luv.ReceiveHandle" diff --git a/std/asys/native/IDuplex.hx b/std/asys/native/IDuplex.hx new file mode 100644 index 00000000000..8d1ae793884 --- /dev/null +++ b/std/asys/native/IDuplex.hx @@ -0,0 +1,11 @@ +package asys.native; + +import haxe.NoData; +import haxe.io.Bytes; + +/** + An interface to read and write bytes. + If a class has to implement both `IReadable` and `IWritable` it is strongly + recommended to implement `IDuplex` instead. +**/ +interface IDuplex extends IReadable extends IWritable {} \ No newline at end of file diff --git a/std/asys/native/IReadable.hx b/std/asys/native/IReadable.hx new file mode 100644 index 00000000000..eb456cc820e --- /dev/null +++ b/std/asys/native/IReadable.hx @@ -0,0 +1,22 @@ +package asys.native; + +import haxe.NoData; +import haxe.io.Bytes; +import haxe.Exception; +import haxe.Callback; + +/** + An interface to read bytes from a source of bytes. +**/ +interface IReadable { + /** + Read up to `length` bytes and write them into `buffer` starting from `offset` + position in `buffer`, then invoke `callback` with the amount of bytes read. + **/ + function read(buffer:Bytes, offset:Int, length:Int, callback:Callback):Void; + + /** + Close this stream. + **/ + function close(callback:Callback):Void; +} \ No newline at end of file diff --git a/std/asys/native/IWritable.hx b/std/asys/native/IWritable.hx new file mode 100644 index 00000000000..b57137f572d --- /dev/null +++ b/std/asys/native/IWritable.hx @@ -0,0 +1,27 @@ +package asys.native; + +import haxe.NoData; +import haxe.io.Bytes; +import haxe.Exception; +import haxe.Callback; + +/** + An interface to write bytes into an out-going stream of bytes. +**/ +interface IWritable { + /** + Write up to `length` bytes from `buffer` (starting from buffer `offset`), + then invoke `callback` with the amount of bytes written. + **/ + function write(buffer:Bytes, offset:Int, length:Int, callback:Callback):Void; + + /** + Force all buffered data to be committed. + **/ + function flush(callback:Callback):Void; + + /** + Close this stream. + **/ + function close(callback:Callback):Void; +} \ No newline at end of file diff --git a/std/asys/native/IoErrorType.hx b/std/asys/native/IoErrorType.hx new file mode 100644 index 00000000000..01f35476dbe --- /dev/null +++ b/std/asys/native/IoErrorType.hx @@ -0,0 +1,80 @@ +package asys.native; + +/** + Error types. + + TODO: add more error types +**/ +@:using(asys.native.IoErrorType.IoErrorTypeTools) +enum IoErrorType { + /** File or directory not found */ + FileNotFound; + /** File or directory already exist */ + FileExists; + /** No such process */ + ProcessNotFound; + /** Permission denied */ + AccessDenied; + /** The given path was not a directory as expected */ + NotDirectory; + /** The given path is a directory, but a file was expected */ + IsDirectory; + /** Too many open files */ + TooManyOpenFiles; + /** Broken pipe */ + BrokenPipe; + /** Directory not empty */ + NotEmpty; + /** Requested address is not available */ + AddressNotAvailable; + /** Connection reset by peer */ + ConnectionReset; + /** Operation timed out */ + TimedOut; + /** Connection refused */ + ConnectionRefused; + /** Bad file descriptor */ + BadFile; + /** Any other error */ + CustomError(message:String); +} + +class IoErrorTypeTools { + /** + Error type description + **/ + static public function toString(type:IoErrorType):String { + return switch type { + case FileNotFound: + "File or directory not found"; + case FileExists: + "File or directory already exists"; + case ProcessNotFound: + "No such process"; + case AccessDenied: + "Permission denied"; + case NotDirectory: + "The given path was not a directory as expected"; + case IsDirectory: + "The given path is a directory, but a file was expected"; + case TooManyOpenFiles: + "Too many open files"; + case BrokenPipe: + "Broken pipe"; + case NotEmpty: + "Directory not empty"; + case AddressNotAvailable: + "Address already in use"; + case ConnectionReset: + "Connection reset by peer"; + case TimedOut: + "Operation timed out"; + case ConnectionRefused: + "Connection refused"; + case BadFile: + "Bad file descriptor"; + case CustomError(message): + message; + } + } +} diff --git a/std/asys/native/IoException.hx b/std/asys/native/IoException.hx new file mode 100644 index 00000000000..5aee4975c23 --- /dev/null +++ b/std/asys/native/IoException.hx @@ -0,0 +1,16 @@ +package asys.native; + +import asys.native.IoErrorType; +import haxe.Exception; + +class IoException extends Exception { + /** + Error type + **/ + public final type:IoErrorType; + + public function new(type:IoErrorType, ?previous:Exception) { + super(type.toString(), previous); + this.type = type; + } +} \ No newline at end of file diff --git a/std/asys/native/Stream.hx b/std/asys/native/Stream.hx new file mode 100644 index 00000000000..1e2b030b045 --- /dev/null +++ b/std/asys/native/Stream.hx @@ -0,0 +1,17 @@ +package asys.native; + +import asys.native.IDuplex; +import asys.native.IReadable; +import asys.native.IWritable; + +/** + Enum which is used to stay type-safe when a stream can be of any type. +**/ +enum Stream { + /** Read-only stream. */ + Read(stream:IReadable); + /** Write-only stream. */ + Write(stream:IWritable); + /** The stream is both readable and writable. */ + ReadWrite(stream:IDuplex); +} \ No newline at end of file diff --git a/std/asys/native/filesystem/Callback.hx b/std/asys/native/filesystem/Callback.hx new file mode 100644 index 00000000000..c2418fba54f --- /dev/null +++ b/std/asys/native/filesystem/Callback.hx @@ -0,0 +1,4 @@ +package asys.native.filesystem; + +//TODO: remove after https://github.com/HaxeFoundation/haxe-evolution/pull/50 is implemented +typedef Callback = haxe.Callback; \ No newline at end of file diff --git a/std/asys/native/filesystem/Directory.hx b/std/asys/native/filesystem/Directory.hx new file mode 100644 index 00000000000..6dc79f08fa4 --- /dev/null +++ b/std/asys/native/filesystem/Directory.hx @@ -0,0 +1,38 @@ +package asys.native.filesystem; + +import haxe.NoData; +import haxe.exceptions.NotImplementedException; + +/** + Represents a directory. +**/ +@:coreApi +class Directory { + /** The path of this directory as it was at the moment of opening the directory */ + public final path:FilePath; + + function new() { + path = 'stub'; + } + + /** + Read next batch of directory entries. + Passes an empty array to `callback` if no more entries left to read. + Ignores `.` and `..` entries. + + The size of the array is always equal to or less than `maxBatchSize` value used + for opening this directory. + + @see asys.native.filesystem.FileSystem.openDirectory + **/ + public function next(callback:Callback>):Void { + throw new NotImplementedException(); + } + + /** + Close the directory. + **/ + public function close(callback:Callback):Void { + throw new NotImplementedException(); + } +} \ No newline at end of file diff --git a/std/asys/native/filesystem/File.hx b/std/asys/native/filesystem/File.hx new file mode 100644 index 00000000000..e68847f4ccb --- /dev/null +++ b/std/asys/native/filesystem/File.hx @@ -0,0 +1,136 @@ +package asys.native.filesystem; + +import haxe.Int64; +import haxe.io.Bytes; +import haxe.NoData; +import haxe.exceptions.NotImplementedException; +import asys.native.system.SystemUser; +import asys.native.system.SystemGroup; + +/** + TODO: + Add methods for reading/writing `String` data because it may be more efficient + on some targets (lua) than reading/writing `haxe.io.Bytes` +**/ +@:coreApi +class File { + /** The path of this file as it was at the moment of opening the file **/ + public final path:FilePath; + + function new():Void { + path = 'stub'; + } + + /** + Write up to `length` bytes from `buffer` starting at the buffer `offset` + to the file starting at the file `position`, then invoke `callback` with + the amount of bytes written. + + If `position` is greater than the file size then the file will be grown + to the required size with the zero bytes before writing. + + If `position` is negative or `offset` is outside of `buffer` bounds or + if `length` is negative, an error is passed to the `callback`. + **/ + public function write(position:Int64, buffer:Bytes, offset:Int, length:Int, callback:Callback):Void { + throw new NotImplementedException(); + } + + /** + Read up to `length` bytes from the file `position` and write them into + `buffer` starting at `offset` position in `buffer`, then invoke `callback` + with the amount of bytes read. + + If `position` is greater or equal to the file size at the moment of reading + then `0` is passed to the `callback` and `buffer` is unaffected. + + If `position` is negative or `offset` is outside of `buffer` bounds, an + error is passed to the `callback`. + **/ + public function read(position:Int64, buffer:Bytes, offset:Int, length:Int, callback:Callback):Void { + throw new NotImplementedException(); + } + + /** + Force all buffered data to be written to disk. + **/ + public function flush(callback:Callback):Void { + throw new NotImplementedException(); + } + + /** + Get file status information. + **/ + public function info(callback:Callback):Void { + throw new NotImplementedException(); + } + + /** + Set file permissions. + **/ + public function setPermissions(permissions:FilePermissions, callback:Callback):Void { + throw new NotImplementedException(); + } + + /** + Set file owner and group. + **/ + public function setOwner(user:SystemUser, group:SystemGroup, callback:Callback):Void { + throw new NotImplementedException(); + } + + /** + Shrink or expand the file to `newSize` bytes. + + If the file is larger than `newSize`, the extra data is lost. + If the file is shorter, zero bytes are used to fill the added length. + **/ + public function resize(newSize:Int, callback:Callback):Void { + throw new NotImplementedException(); + } + + /** + Change access and modification times of the file. + + TODO: Decide on type for `accessTime` and `modificationTime` - see TODO in `asys.native.filesystem.FileInfo.FileStat` + **/ + public function setTimes(accessTime:Int, modificationTime:Int, callback:Callback):Void { + throw new NotImplementedException(); + } + + /** + TODO: this requires a separate work for design and implementation + to find a solid cross-platform solution. + + Acquire or release a file lock for the current process. + + The `callback` is supplied with `true` if a lock was successfully acquired. + + Modes: + - `Shared` - acquire a shared lock (usually used for reading) + - `Exclusive` - acquire an exclusive lock (usually used for writing) + - `Unlock` - release a lock. + + By default (`wait` is `true`) `lock` waits until a lock can be acquired. + Pass `false` to `wait` to invoke `callback` with `false` if a lock cannot + be acquired immediately. + + Although a lock may be released automatically on file closing, for a + consistent cross-platform behavior it is strongly recommended to always + release a lock manually. + + This lock is _not_ suitable for controlling access to a file by multiple threads. + **/ + // public function lock(mode:FileLock = Exclusive, wait:Bool = true, callback:Callback):Void { + // throw new NotImplementedException(); + // } + + /** + Close the file. + + Does not fail if the file is already closed. + **/ + public function close(callback:Callback):Void { + throw new NotImplementedException(); + } +} \ No newline at end of file diff --git a/std/asys/native/filesystem/FileAccessMode.hx b/std/asys/native/filesystem/FileAccessMode.hx new file mode 100644 index 00000000000..8f032285a07 --- /dev/null +++ b/std/asys/native/filesystem/FileAccessMode.hx @@ -0,0 +1,21 @@ +package asys.native.filesystem; + +import haxe.exceptions.ArgumentException; +import haxe.exceptions.NotImplementedException; + +enum abstract FileAccessMode(Int) to Int { + /** File exists and is visible for the current process */ + var Exists = 1; + /** File can be executed bye the current process */ + var Executable = 2; + /** File can be written by the current process */ + var Writable = 4; + /** File can be read by the current process */ + var Readable = 8; + + public inline function has(mode:FileAccessMode):Bool { + return this & mode != 0; + } + + @:op(A | B) function join(other:FileAccessMode):FileAccessMode; +} \ No newline at end of file diff --git a/std/asys/native/filesystem/FileInfo.hx b/std/asys/native/filesystem/FileInfo.hx new file mode 100644 index 00000000000..86083bb28af --- /dev/null +++ b/std/asys/native/filesystem/FileInfo.hx @@ -0,0 +1,115 @@ +package asys.native.filesystem; + +import haxe.exceptions.NotImplementedException; +import asys.native.system.SystemUser; +import asys.native.system.SystemGroup; + +/** + Data from system call to `stat`. + + TODO: + - Decide on data type for time fields: `Date` means additional allocations; + `Int` means "end-of-time" issue. Maybe `Float`? + - Decide on `ino` type: theoretically it could be any big number. For example + on Windows it could be a 64-bit unsigned integer. So may overflow. + - Decide on `size` type: `Int` limits `size` to ~2GB. +**/ +private typedef NativeInfo = { + /** Time of last access (Unix timestamp) */ + final atime:Int; + /** Time of last modification (Unix timestamp) */ + final mtime:Int; + /** Time of last inode change (Unix timestamp) */ + final ctime:Int; + /** Device number */ + final dev:Int; + /** Owning user */ + final uid:Int; + /** Owning group */ + final gid:Int; + /** Inode number */ + final ino:Int; + /** Inode protection mode */ + final mode:Int; + /** Number of links */ + final nlink:Int; + /** Device type, if inode device */ + final rdev:Int; + /** Size in bytes */ + final size:Int; + /** Block size of filesystem for IO operations */ + final blksize:Int; + /** Number of 512-bytes blocks allocated */ + final blocks:Int; +}; + +/** + Provides information about a file. +**/ +@:coreApi +abstract FileInfo(NativeInfo) from NativeInfo to NativeInfo { + /** Time of last access (Unix timestamp) */ + public var accessTime(get,never):Int; + inline function get_accessTime():Int + return this.atime; + + /** Time of last modification (Unix timestamp) */ + public var modificationTime(get,never):Int; + inline function get_modificationTime():Int + return this.mtime; + + /** Time of last inode change (Unix timestamp) */ + public var creationTime(get,never):Int; + inline function get_creationTime():Int + return this.ctime; + + /** Device number */ + public var deviceNumber(get,never):Int; + inline function get_deviceNumber():Int + return this.dev; + + /** Owning group **/ + public var group(get,never):SystemGroup; + inline function get_group():SystemGroup + return this.gid; + + /** Owning user **/ + public var user(get,never):SystemUser; + inline function get_user():SystemUser + return this.uid; + + /** Inode number */ + public var inodeNumber(get,never):Int; + inline function get_inodeNumber():Int + return this.ino; + + /** File type and permissions */ + public var mode(get,never):FileMode; + inline function get_mode():FileMode + return this.mode; + + /** Number of links */ + public var links(get,never):Int; + inline function get_links():Int + return this.nlink; + + /** Device type, if inode device */ + public var deviceType(get,never):Int; + inline function get_deviceType():Int + return this.rdev; + + /** Size in bytes */ + public var size(get,never):Int; + inline function get_size():Int + return this.size; + + /** Block size of filesystem for IO operations */ + public var blockSize(get,never):Int; + inline function get_blockSize():Int + return this.blksize; + + /** Number of 512-bytes blocks allocated */ + public var blocks(get,never):Int; + inline function get_blocks():Int + return this.blocks; +} \ No newline at end of file diff --git a/std/asys/native/filesystem/FileLink.hx b/std/asys/native/filesystem/FileLink.hx new file mode 100644 index 00000000000..3ec5fbfabd4 --- /dev/null +++ b/std/asys/native/filesystem/FileLink.hx @@ -0,0 +1,8 @@ +package asys.native.filesystem; + +enum abstract FileLink(Int) { + /** Hard link. */ + var HardLink; + /** Symbolic link. */ + var SymLink; +} \ No newline at end of file diff --git a/std/asys/native/filesystem/FileLock.hx b/std/asys/native/filesystem/FileLock.hx new file mode 100644 index 00000000000..a2013eb2193 --- /dev/null +++ b/std/asys/native/filesystem/FileLock.hx @@ -0,0 +1,22 @@ +package asys.native.filesystem; + +/** + File locking modes. + @see asys.native.filesystem.FileSystem.lock +**/ +enum abstract FileLock(Int) { + /** + Shared lock. + Useful for reading a file. + **/ + var Shared; + /** + Exclusive lock. + Useful for writing to a file. + **/ + var Exclusive; + /** + Release a lock. + **/ + var Unlock; +} \ No newline at end of file diff --git a/std/asys/native/filesystem/FileMode.hx b/std/asys/native/filesystem/FileMode.hx new file mode 100644 index 00000000000..8c5b40fc9b1 --- /dev/null +++ b/std/asys/native/filesystem/FileMode.hx @@ -0,0 +1,62 @@ +package asys.native.filesystem; + +import haxe.exceptions.ArgumentException; +import haxe.exceptions.NotImplementedException; + +private typedef NativeMode = Int; + +/** + File mode contains file type and permissions. +**/ +@:coreApi +abstract FileMode(NativeMode) from NativeMode { + /** file type bit mask */ + static inline var S_IFMT:Int = 61440; + /** named pipe (fifo) */ + static inline var S_IFIFO:Int = 4096; + /** character special */ + static inline var S_IFCHR:Int = 8192; + /** directory */ + static inline var S_IFDIR:Int = 16384; + /** block special */ + static inline var S_IFBLK:Int = 24576; + /** regular */ + static inline var S_IFREG:Int = 32768; + /** symbolic link */ + static inline var S_IFLNK:Int = 40960; + /** socket */ + static inline var S_IFSOCK:Int = 49152; + /** whiteout */ + static inline var S_IFWHT:Int = 57344; + + /** + Check if all the permissions are set in this mode. + **/ + public inline function has(permissions:FilePermissions):Bool { + return this & (permissions:Int) == (permissions:Int); + } + + public inline function isBlockDevice():Bool + return this & S_IFMT == S_IFBLK; + + public inline function isCharacterDevice():Bool + return this & S_IFMT == S_IFCHR; + + public inline function isDirectory():Bool + return this & S_IFMT == S_IFDIR; + + /** + TODO: Fifo? FiFo? + **/ + public inline function isFIFO():Bool + return this & S_IFMT == S_IFIFO; + + public inline function isFile():Bool + return this & S_IFMT == S_IFREG; + + public inline function isSocket():Bool + return this & S_IFMT == S_IFSOCK; + + public inline function isLink():Bool + return this & S_IFMT == S_IFLNK; +} \ No newline at end of file diff --git a/std/asys/native/filesystem/FileOpenFlag.hx b/std/asys/native/filesystem/FileOpenFlag.hx new file mode 100644 index 00000000000..538fecfeb47 --- /dev/null +++ b/std/asys/native/filesystem/FileOpenFlag.hx @@ -0,0 +1,98 @@ +package asys.native.filesystem; + +import asys.native.filesystem.File; +import haxe.io.Bytes; + +enum abstract FileOpenFlag(Int) { + /** + Open file for appending. + The file is created if it does not exist. + **/ + var Append:FileOpenFlag; + + /** + Open file for reading. + Fails if the file does not exist. + **/ + var Read:FileOpenFlag; + + /** + Open file for reading and writing. + Fails if the file does not exist. + **/ + var ReadWrite:FileOpenFlag; + + /** + Open file for writing. + The file is truncated if it exists. + The file is created if it doesn't exist. + **/ + var Write:FileOpenFlag; + + /** + Create and open file for writing. + Fails if the file already exists. + **/ + var WriteX:FileOpenFlag; + + /** + Open file for writing and reading. + The file is truncated if it exists. + The file is created if it doesn't exist. + **/ + var WriteRead:FileOpenFlag; + + /** + Create and open file for writing and reading. + Fails if the file already exists. + **/ + var WriteReadX:FileOpenFlag; + + /** + Open file for writing. + The file is _not_ truncated if it exists (as opposed to `Write`). + The file is created if it doesn't exist. + **/ + var Overwrite:FileOpenFlag; + + /** + Open file for writing and reading. + The file is _not_ truncated if it exists (as opposed to `WriteRead`). + The file is created if it doesn't exist. + **/ + var OverwriteRead:FileOpenFlag; +} + +/** + Limits file operations to reading. + @see asys.native.filesystem.File +**/ +@:forward(path,read,info,setPermissions,setOwner,setGroup,setTimes,lock,close,isOpen) +abstract FileRead(File) from File {} + +/** + Limits file operations to writing. + @see asys.native.filesystem.File +**/ +@:forward(path,write,flush,sync,info,setPermissions,setOwner,setGroup,setTimes,lock,resize,close,isOpen) +abstract FileWrite(File) from File {} + +/** + Limits file operations to writing at the end of file. + @see asys.native.filesystem.File +**/ +@:forward(path,flush,sync,info,setPermissions,setOwner,setGroup,setTimes,lock,resize,close,isOpen) +abstract FileAppend(File) from File { + /** + Append up to `length` bytes from `buffer` starting at the buffer `offset` + to the file, then invoke `callback` with the amount of bytes written. + + If `offset` is outside of `buffer` bounds or if `length` is negative, an + error passed to the `callback`. + + TODO: Is `append` a better name for this method? + **/ + public inline function write(buffer:Bytes, offset:Int, length:Int, callback:Callback):Void { + this.write(0, buffer, offset, length, callback); + } +} diff --git a/std/asys/native/filesystem/FilePath.hx b/std/asys/native/filesystem/FilePath.hx new file mode 100644 index 00000000000..087707e0ad5 --- /dev/null +++ b/std/asys/native/filesystem/FilePath.hx @@ -0,0 +1,171 @@ +package asys.native.filesystem; + +import haxe.exceptions.NotImplementedException; + +private typedef NativeFilePath = Dynamic; + +/** + Represents a relative or absolute file path. + + TODO: add API from `haxe.io.Path` +**/ +@:coreApi abstract FilePath(NativeFilePath) { + /** + Standard directory separator character for current platform. + E.g. `\\` for Windows or `/` for Unix-like systems. + **/ + public static var SEPARATOR(get,never):String; + static function get_SEPARATOR():String { + throw new NotImplementedException(); + } + + /** + Create a path by sequantually adding `appendices` to `path`. + + If any of the appendices is an absolute path then `path` and all previous + appendices are discarded. + Any empty component is ignored. + + TODO: see TODO of "add" method below + **/ + overload extern static public inline function createPath(path:String, ...appendices:String):FilePath { + throw new NotImplementedException(); + } + + /** + Create a path by combining items of `parts` array. + + If any of the `parts` is an absolute path then all previous parts are discarded. + Any empty component is ignored. + + @throws haxe.exceptions.ArgumentException if `parts` is empty. + + TODO: see TODO of "add" method below + **/ + overload extern static public inline function createPath(parts:Array):FilePath { + throw new NotImplementedException(); + } + + /** + Create file path from plain string. + Removes trailing slashes. + + Creates a path of `.` if `path` is empty. + That is `FilePath.ofString('') == FilePath.ofString('.')`. + **/ + @:noUsing + @:from static public function ofString(path:String):FilePath { + throw new NotImplementedException(); + } + + /** + Alias of `createPath(Array)` + **/ + @:from static function ofArray(parts:Array):FilePath { + throw new NotImplementedException(); + } + + /** + Get string representation of this path. + + Trailing slashes are always removed unless this is a root path. + **/ + @:to public function toString():String { + throw new NotImplementedException(); + } + + /** + TODO: Should `my/path` and `my\\path` be equal on windows? + **/ + @:op(A == B) function equals(p:FilePath):Bool { + throw new NotImplementedException(); + } + + /** + Check if this is an absolute path. + **/ + public function isAbsolute():Bool { + throw new NotImplementedException(); + } + + /** + Get an absolute path of this path. + + If this path is already an absolute path, then it's returned as is. + + It does not matter if the path does not exist, however some implementations + may need current working directory to actually exist. + **/ + public function absolute():FilePath { + throw new NotImplementedException(); + } + + /** + Returns this path with all the redundant elements removed. + + Resolves `.` and `..` and removes excessive slashes and trailing slashes. + Does not resolve symbolic links. + + This method may return an empty path if all elements of this path are redundant. + + It does not matter if the path does not exist. + **/ + public function normalize():FilePath { + throw new NotImplementedException(); + } + + /** + Get the parent element of this path. + E.g. for `dir/to/path` this method returns `dir/to`. + + Ignores trailing slashes. + E.g. for `dir/to/path/` this method returns `dir/to`. + + Returns `null` if this path does not have a parent element. + + This method does not resolve special names like `.` and `..`. + That is the parent of `some/..` is `some`. + **/ + public function parent():Null { + throw new NotImplementedException(); + } + + /** + Get the last element (farthest from the root) of this path. + E.g. for `dir/to/path` this method returns `path`. + + Ignores trailing slashes. + E.g. for `dir/to/path/` this method returns `path`. + **/ + public function name():FilePath { + throw new NotImplementedException(); + } + + /** + Creates a new path by appending `path` to this one. + This path is treated as a directory and a directory separator is inserted + between this and `path` if needed. + + If `path` is an absolute path, then this method simply returns `path`. + If either this or `path` is empty then this method simply return the other one. + + ```haxe + FilePath.ofString('dir').add('file'); // result: dir/file + FilePath.ofString('dir/').add('file'); // result: dir/file + FilePath.ofString('dir').add('/file'); // result: /file + FilePath.ofString('').add('file'); // result: file + FilePath.ofString('dir').add(''); // result: dir + ``` + + TODO: + What to do with windows paths relative to a drive? + ```haxe + 'D:dir'.add('C:file') == exception ? + '/dir'.add('C:file') == 'C:/dir/file' ? + ``` + **/ + public function add(path:FilePath):FilePath { + throw new NotImplementedException(); + } + +} \ No newline at end of file diff --git a/std/asys/native/filesystem/FilePermissions.hx b/std/asys/native/filesystem/FilePermissions.hx new file mode 100644 index 00000000000..022381e359d --- /dev/null +++ b/std/asys/native/filesystem/FilePermissions.hx @@ -0,0 +1,88 @@ +package asys.native.filesystem; + +import haxe.exceptions.ArgumentException; +import haxe.exceptions.NotImplementedException; + +private typedef NativePermissions = Int; + +/** + Filesystem permissions. + + Note that this is not an octal number. + For octal numbers use `FilePermissions.octal` method. +**/ +@:coreApi +abstract FilePermissions(NativePermissions) to NativePermissions { + /** + Returns `true` if the special bit (sticky, SETUID, SETGUID) is ignored + by current implementation. + **/ + static public inline function ignoresSpecialBit():Bool { + return false; + } + + /** + Specify file access mode as octal digits. + + For example an octal access mode `0o0765` + could be set as `FilePermissions.octal(0, 7, 6, 5)` + + @param s - sticky bit, SETUID, SETGUID. This may be ignored by some implementations. + @param u - permissions for file owner + @param g - permissions for file group + @param o - permissions for other users + + For possible values of `s` check https://en.wikipedia.org/wiki/Setuid + + Possible values for `u`, `g`, and `o`: + 0 - no permission + 1 - execute only + 2 - write only + 3 - write and execute + 4 - read only + 5 - read and execute + 6 - read and write + 7 - read, write, and execute + **/ + static public inline function octal(s:Int, u:Int, g:Int, o:Int):FilePermissions { + return new FilePermissions(512 * s + 64 * u + 8 * g + 1 * o); + } + + /** + Same as `FilePermissions.octal` except required arguments are taken from + respective positions of `mode` array. + For example: + ```haxe + var mode:FilePermissions = [0, 7, 6, 5]; + //is the same as + var mode = FilePermissions.octal(0, 7, 6, 5); + ``` + + `mode` should contain exactly four items, otherwise + `haxe.exceptions.ArgumentException` is thrown. + + Thanks to Haxe optimizations this method does not allocate an array at + run time if supplied with an array declaration. + **/ + @:from static inline function fromOctal(mode:Array):FilePermissions { + if(mode.length != 4) { + throw new ArgumentException('mode', '"mode" array should contain exactly four items'); + } + return octal(mode[0], mode[1], mode[2], mode[3]); + } + + @:from static inline function fromDecimal(mode:Int):FilePermissions { + return new FilePermissions(mode); + } + + @:op(A & B) static function intersect(perm1:FilePermissions, perm2:FilePermissions):FilePermissions; + @:op(A | B) static function merge(perm1:FilePermissions, perm2:FilePermissions):FilePermissions; + + inline function new(perm:Int) { + this = perm; + } + + public inline function toString():String { + return '$this'; + } +} \ No newline at end of file diff --git a/std/asys/native/filesystem/FileSystem.hx b/std/asys/native/filesystem/FileSystem.hx new file mode 100644 index 00000000000..1bfe67ea7f0 --- /dev/null +++ b/std/asys/native/filesystem/FileSystem.hx @@ -0,0 +1,313 @@ +package asys.native.filesystem; + +import haxe.io.Bytes; +import haxe.NoData; +import haxe.exceptions.NotImplementedException; +import asys.native.system.SystemUser; +import asys.native.system.SystemGroup; + +/** + File system operations. +**/ +@:coreApi +class FileSystem { + /** + Open file for reading and/or writing. + + Depending on `flag` value `callback` will be invoked with the appropriate + object type to read and/or write the file: + - `asys.native.filesystem.File` for reading and writing; + - `asys.native.filesystem.FileRead` for reading only; + - `asys.native.filesystem.FileWrite` for writing only; + - `asys.native.filesystem.FileAppend` for writing to the end of file only; + + @see asys.native.filesystem.FileOpenFlag for more details. + **/ + static public function openFile(path:FilePath, flag:FileOpenFlag, callback:Callback):Void { + throw new NotImplementedException(); + } + + /** + Create and open a unique temporary file for writing and reading. + + The file will be automatically deleted when it is closed. + + Depending on a target platform the file may be automatically deleted upon + application shutdown, but in general deletion is not guaranteed if the `close` + method is not called. + + Depending on a target platform the directory entry for the file may be deleted + immediately after the file is created or even not created at all. + **/ + static public function tempFile(callback:Callback):Void { + throw new NotImplementedException(); + } + + /** + Read the contents of a file specified by `path`. + **/ + static public function readBytes(path:FilePath, callback:Callback):Void { + throw new NotImplementedException(); + } + + /** + Read the contents of a file specified by `path` as a `String`. + + TODO: + Should this return an error if the file does not contain a valid unicode string? + **/ + static public function readString(path:FilePath, callback:Callback):Void { + throw new NotImplementedException(); + } + + /** + Write `data` into a file specified by `path` + + `flag` controls the behavior. + By default the file truncated if it exists and created if it does not exist. + + @see asys.native.filesystem.FileOpenFlag for more details. + **/ + static public function writeBytes(path:FilePath, data:Bytes, flag:FileOpenFlag = Write, callback:Callback):Void { + throw new NotImplementedException(); + } + + /** + Write `text` into a file specified by `path` + + `flag` controls the behavior. + By default the file is truncated if it exists and is created if it does not exist. + + @see asys.native.filesystem.FileOpenFlag for more details. + **/ + static public function writeString(path:FilePath, text:String, flag:FileOpenFlag = Write, callback:Callback):Void { + throw new NotImplementedException(); + } + + /** + Open directory for listing. + + `maxBatchSize` sets maximum amount of entries returned by a call to `directory.next`. + + In general bigger `maxBatchSize` allows to iterate faster, but requires more + memory per call to `directory.next`. + + @see asys.native.filesystem.Directory.next + **/ + static public function openDirectory(path:FilePath, maxBatchSize:Int = 64, callback:Callback):Void { + throw new NotImplementedException(); + } + + /** + List directory contents. + Does not add `.` and `..` to the result. + Entries are provided as paths relative to the directory. + **/ + static public function listDirectory(path:FilePath, callback:Callback>):Void { + throw new NotImplementedException(); + } + + /** + Create a directory. + + Default `permissions` equals to octal `0777`, which means read+write+execution + permissions for everyone. + + If `recursive` is `true`: create missing directories tree all the way down to `path`. + If `recursive` is `false`: fail if any parent directory of `path` does not exist. + + [cs] `permissions` parameter is ignored when targeting C# + **/ + static public function createDirectory(path:FilePath, ?permissions:FilePermissions, recursive:Bool = false, callback:Callback):Void { + throw new NotImplementedException(); + } + + /** + Create a directory with auto-generated unique name. + + `prefix` (if provided) is used as the beginning of a generated name. + The created directory path is passed to the `callback`. + + Default `permissions` equals to octal `0777`, which means read+write+execution + permissions for everyone. + + If `recursive` is `true`: create missing directories tree all the way down to the generated path. + If `recursive` is `false`: fail if any parent directory of the generated path does not exist. + + [cs] `permissions` parameter is ignored when targeting C# + **/ + static public function uniqueDirectory(parentDirectory:FilePath, ?prefix:String, ?permissions:FilePermissions, recursive:Bool = false, callback:Callback):Void { + throw new NotImplementedException(); + } + + /** + Move and/or rename the file or directory from `oldPath` to `newPath`. + + If `newPath` already exists and `overwrite` is `true` (which is the default) + the destination is overwritten. However, operation fails if `newPath` is + a non-empty directory. + + If `overwrite` is `false` the operation is not guaranteed to be atomic. + That means if a third-party process creates `newPath` right in between the + check for existance and the actual move operation then the data created + by that third-party process may be overwritten. + **/ + static public function move(oldPath:FilePath, newPath:FilePath, overwrite:Bool = true, callback:Callback):Void { + throw new NotImplementedException(); + } + + /** + Remove a file or symbolic link. + **/ + static public function deleteFile(path:FilePath, callback:Callback):Void { + throw new NotImplementedException(); + } + + /** + Remove an empty directory. + **/ + static public function deleteDirectory(path:FilePath, callback:Callback):Void { + throw new NotImplementedException(); + } + + /** + Get file or directory information at the given path. + If `path` is a symbolic link then the link is followed. + + @see `asys.native.filesystem.FileSystem.linkInfo` to get information of the + link itself. + **/ + static public function info(path:FilePath, callback:Callback):Void { + throw new NotImplementedException(); + } + + /** + Check user's access for a path. + + For example to check if a file is readable and writable: + ```haxe + import asys.native.filesystem.FileAccessMode; + FileSystem.check(path, Readable | Writable, (error, result) -> trace(result)); + ``` + **/ + static public function check(path:FilePath, mode:FileAccessMode, callback:Callback):Void { + throw new NotImplementedException(); + } + + /** + Check if the path is a directory. + If `path` is a symbolic links then it will be resolved and checked. + Returns `false` if `path` does not exist. + **/ + static public function isDirectory(path:FilePath, callback:Callback):Void { + throw new NotImplementedException(); + } + + /** + Check if the path is a regular file. + If `path` is a symbolic links then it will be resolved and checked. + Returns `false` if `path` does not exist. + **/ + static public function isFile(path:FilePath, callback:Callback):Void { + throw new NotImplementedException(); + } + + /** + Set path permissions. + + If `path` is a symbolic link it is dereferenced. + **/ + static public function setPermissions(path:FilePath, permissions:FilePermissions, callback:Callback):Void { + throw new NotImplementedException(); + } + + /** + Set path owner and group. + + If `path` is a symbolic link it is dereferenced. + **/ + static public function setOwner(path:FilePath, user:SystemUser, group:SystemGroup, callback:Callback):Void { + throw new NotImplementedException(); + } + + /** + Set symbolic link owner and group. + **/ + static public function setLinkOwner(path:FilePath, user:SystemUser, group:SystemGroup, callback:Callback):Void { + throw new NotImplementedException(); + } + + /** + Create a link to `target` at `path`. + + If `type` is `SymLink` the `target` is expected to be an absolute path or + a path relative to `path`, however the existance of `target` is not checked + and the link is created even if `target` does not exist. + + If `type` is `HardLink` the `target` is expected to be an existing path either + absolute or relative to the current working directory. + **/ + static public function link(target:FilePath, path:FilePath, type:FileLink = SymLink, callback:Callback):Void { + throw new NotImplementedException(); + } + + /** + Check if the path is a symbolic link. + Returns `false` if `path` does not exist. + **/ + static public function isLink(path:FilePath, callback:Callback):Void { + throw new NotImplementedException(); + } + + /** + Get the value of a symbolic link. + **/ + static public function readLink(path:FilePath, callback:Callback):Void { + throw new NotImplementedException(); + } + + /** + Get information at the given path without following symbolic links. + **/ + static public function linkInfo(path:FilePath, callback:Callback):Void { + throw new NotImplementedException(); + } + + /** + Copy a file from `source` path to `destination` path. + **/ + static public function copyFile(source:FilePath, destination:FilePath, overwrite:Bool = true, callback:Callback):Void { + throw new NotImplementedException(); + } + + /** + Shrink or expand a file specified by `path` to `newSize` bytes. + + If the file does not exist, it is created. + + If the file is larger than `newSize`, the extra data is lost. + If the file is shorter, zero bytes are used to fill the added length. + **/ + static public function resize(path:FilePath, newSize:Int, callback:Callback):Void { + throw new NotImplementedException(); + } + + /** + Change access and modification times of an existing file. + + TODO: Decide on type for `accessTime` and `modificationTime` - see TODO in `asys.native.filesystem.FileInfo.FileStat` + **/ + static public function setTimes(path:FilePath, accessTime:Int, modificationTime:Int, callback:Callback):Void { + throw new NotImplementedException(); + } + + /** + Get a canonical absolute path. The path must exist. + + Resolves intermediate `.`, `..`, excessive slashes. + Resolves symbolic links on all targets except C#. + **/ + static public function realPath(path:FilePath, callback:Callback):Void { + throw new NotImplementedException(); + } +} \ No newline at end of file diff --git a/std/asys/native/filesystem/FsException.hx b/std/asys/native/filesystem/FsException.hx new file mode 100644 index 00000000000..a6f9ff0fd1f --- /dev/null +++ b/std/asys/native/filesystem/FsException.hx @@ -0,0 +1,27 @@ +package asys.native.filesystem; + +import haxe.Exception; + +/** + File system errors. + + TODO: `FileSystemException` is probably a better name +**/ +class FsException extends IoException { + /** + A target path of a failed operation. + **/ + public final path:FilePath; + + public function new(type:IoErrorType, path:FilePath, ?previous:Exception) { + super(type, previous); + this.path = path; + } + + /** + Error description. + **/ + override function get_message():String { + return super.get_message() + ' on ${path.toString()}'; + } +} \ No newline at end of file diff --git a/std/asys/native/net/Callback.hx b/std/asys/native/net/Callback.hx new file mode 100644 index 00000000000..5fc26e27be4 --- /dev/null +++ b/std/asys/native/net/Callback.hx @@ -0,0 +1,6 @@ +package asys.native.net; + +import haxe.Exception; + +//TODO: remove after https://github.com/HaxeFoundation/haxe-evolution/pull/50 is implemented +typedef Callback = haxe.Callback; \ No newline at end of file diff --git a/std/asys/native/net/Dns.hx b/std/asys/native/net/Dns.hx new file mode 100644 index 00000000000..a5dfe8e7095 --- /dev/null +++ b/std/asys/native/net/Dns.hx @@ -0,0 +1,22 @@ +package asys.native.net; + +import haxe.exceptions.NotImplementedException; + +/** + Methods related to Domain Name System. +**/ +class Dns { + /** + Lookup the given `host` name. + **/ + static public function resolve(host:String, callback:Callback>) { + throw new NotImplementedException(); + } + + /** + Find host names associated with the given IP address. + **/ + static public function reverse(ip:Ip, callback:Callback>) { + throw new NotImplementedException(); + } +} \ No newline at end of file diff --git a/std/asys/native/net/Ip.hx b/std/asys/native/net/Ip.hx new file mode 100644 index 00000000000..54322b5eee3 --- /dev/null +++ b/std/asys/native/net/Ip.hx @@ -0,0 +1,87 @@ +package asys.native.net; + +import haxe.exceptions.NotImplementedException; +import haxe.io.Bytes; + +/** + Represents a resolved IP address. +**/ +@:using(asys.native.net.Ip.IpTools) +enum Ip { + /** + 32-bit IPv4 address. As an example, the IP address `127.0.0.1` is + represented as `Ipv4(0x7F000001)`. + **/ + Ipv4(raw:Int); + + /** + 128-bit IPv6 address. + **/ + Ipv6(raw:Bytes); +} + +class IpTools { + /** + String representation of `ip`. + Examples: + - IPv4: "192.168.0.1" + - IPv6: "::ffff:c0a8:1" + **/ + static public function toString(ip:Ip):String { + throw new NotImplementedException(); + } + + /** + String representation of `ip`. + Examples: + - IPv4: "192.168.0.1" + - IPv6: "0000:0000:0000:0000:0000:ffff:c0a8:1" + **/ + static public function toFullString(ip:Ip):String { + throw new NotImplementedException(); + } + + /** + Parse a string representation of an IP address. + + Throws an exception if provided string does not represent a valid IP address. + **/ + static public function parseIp(ip:String):Ip { + throw new NotImplementedException(); + } + + /** + Check if `str` contains a valid IPv6 or IPv4 address. + **/ + static public function isIp(str:String):Bool { + throw new NotImplementedException(); + } + + /** + Check if `str` contains a valid IPv4 address. + **/ + static public function isIpv4(str:String):Bool { + throw new NotImplementedException(); + } + + /** + Check if `str` contains a valid IPv6 address. + **/ + static public function isIpv6(str:String):Bool { + throw new NotImplementedException(); + } + + /** + Convert any IP address to IPv6 format. + **/ + static public function toIpv6(ip:Ip):Ip { + throw new NotImplementedException(); + } + + /** + Check if `a` and `b` contain the same IP address. + **/ + static public function equals(a:Ip, b:Ip):Bool { + throw new NotImplementedException(); + } +} \ No newline at end of file diff --git a/std/asys/native/net/SecureServer.hx b/std/asys/native/net/SecureServer.hx new file mode 100644 index 00000000000..2c1124a3afa --- /dev/null +++ b/std/asys/native/net/SecureServer.hx @@ -0,0 +1,28 @@ +package asys.native.net; + +import haxe.exceptions.NotImplementedException; + +typedef SecureServerOptions = SocketOptions & { + //TODO +} + +/** + Secure TCP server socket. +**/ +class SecureServer extends Server { + /** + Start a secure server on specified address. + + This methods creates a secure socket, binds it to `address` and starts + listening for incoming connections. + Connections may be accepted with `server.accept` method. + + Maximum size of incoming connections queue is specified by `backlog`. + If the queue is full, any new incoming connection will be rejected. + **/ + static public function open(address:SocketAddress, options:SecureServerOptions, callback:Callback) { + throw new NotImplementedException(); + } + + //TODO +} \ No newline at end of file diff --git a/std/asys/native/net/SecureSocket.hx b/std/asys/native/net/SecureSocket.hx new file mode 100644 index 00000000000..a2216d03e41 --- /dev/null +++ b/std/asys/native/net/SecureSocket.hx @@ -0,0 +1,21 @@ +package asys.native.net; + +import haxe.exceptions.NotImplementedException; + +typedef SecureSocketOptions = SocketOptions & { + //TODO +} + +/** + Secure TCP socket. +**/ +class SecureSocket extends Socket { + /** + Establish a secure connection to specified address. + **/ + static public function connect(address:SocketAddress, options:SecureSocketOptions, callback:Callback) { + throw new NotImplementedException(); + } + + //TODO +} \ No newline at end of file diff --git a/std/asys/native/net/Server.hx b/std/asys/native/net/Server.hx new file mode 100644 index 00000000000..1718be6168d --- /dev/null +++ b/std/asys/native/net/Server.hx @@ -0,0 +1,67 @@ +package asys.native.net; + +import asys.native.net.SocketOptions.SocketOptionKind; +import haxe.NoData; +import haxe.exceptions.NotImplementedException; + +typedef ServerOptions = SocketOptions & { + /** + Maximum size of incoming connections queue. + Default: 0 + TODO: decide on a meaningful default value. + **/ + var ?backlog:Int; +} + +/** + Server socket. +**/ +class Server { + /** + Local address of this server. + **/ + public var localAddress(get,never):SocketAddress; + function get_localAddress():SocketAddress throw new NotImplementedException(); + + /** + Start a server on specified `address`. + + This methods creates a socket, binds it to `address` and starts listening + for incoming connections. + Connections may be accepted with `server.accept` method. + + Maximum size of incoming connections queue is specified by `options.backlog`. + If the queue is full, any new incoming connection will be rejected. + **/ + static public function open(address:SocketAddress, ?options:ServerOptions, callback:Callback) { + throw new NotImplementedException(); + } + + /** + Accept an incoming connection. + **/ + public function accept(callback:Callback) { + throw new NotImplementedException(); + } + + /** + Get the value of a specified socket option. + **/ + public function getOption(option:SocketOptionKind, callback:Callback) { + throw new NotImplementedException(); + } + + /** + Set socket option. + **/ + public function setOption(option:SocketOptionKind, value:T, callback:Callback) { + throw new NotImplementedException(); + } + + /** + Stop the server. + **/ + public function close(callback:Callback) { + throw new NotImplementedException(); + } +} \ No newline at end of file diff --git a/std/asys/native/net/Socket.hx b/std/asys/native/net/Socket.hx new file mode 100644 index 00000000000..d4135a23776 --- /dev/null +++ b/std/asys/native/net/Socket.hx @@ -0,0 +1,71 @@ +package asys.native.net; + +import asys.native.net.SocketOptions.SocketOptionKind; +import haxe.NoData; +import haxe.io.Bytes; +import haxe.exceptions.NotImplementedException; + +class Socket implements IDuplex { + /** + Local address of this socket. + **/ + public var localAddress(get,never):SocketAddress; + function get_localAddress():SocketAddress throw new NotImplementedException(); + + /** + Remote address of this socket if it is bound. + **/ + public var remoteAddress(get,never):Null; + function get_remoteAddress():Null throw new NotImplementedException(); + + /** + Establish a connection to `address`. + **/ + static public function connect(address:SocketAddress, ?options:SocketOptions, callback:Callback) { + throw new NotImplementedException(); + } + + /** + Read up to `length` bytes and write them into `buffer` starting from `offset` + position in `buffer`, then invoke `callback` with the amount of bytes read. + **/ + public function read(buffer:Bytes, offset:Int, length:Int, callback:Callback) { + throw new NotImplementedException(); + } + + /** + Write up to `length` bytes from `buffer` (starting from buffer `offset`), + then invoke `callback` with the amount of bytes written. + **/ + public function write(buffer:Bytes, offset:Int, length:Int, callback:Callback) { + throw new NotImplementedException(); + } + + /** + Force all buffered data to be committed. + **/ + public function flush(callback:Callback):Void { + throw new NotImplementedException(); + } + + /** + Get the value of a specified socket option. + **/ + public function getOption(option:SocketOptionKind, callback:Callback) { + throw new NotImplementedException(); + } + + /** + Set socket option. + **/ + public function setOption(option:SocketOptionKind, value:T, callback:Callback) { + throw new NotImplementedException(); + } + + /** + Close the connection. + **/ + public function close(callback:Callback) { + throw new NotImplementedException(); + } +} \ No newline at end of file diff --git a/std/asys/native/net/SocketAddress.hx b/std/asys/native/net/SocketAddress.hx new file mode 100644 index 00000000000..4500915715e --- /dev/null +++ b/std/asys/native/net/SocketAddress.hx @@ -0,0 +1,15 @@ +package asys.native.net; + +import asys.native.filesystem.FilePath; + +enum SocketAddress { + /** + A network address. + `host` can be either a host name or IPv4 or IPv6 address. + **/ + Net(host:String, port:Int); + /** + An address in the file system for Inter-Process Communications. + */ + Ipc(path:FilePath); +} diff --git a/std/asys/native/net/SocketOptions.hx b/std/asys/native/net/SocketOptions.hx new file mode 100644 index 00000000000..63978f31c2e --- /dev/null +++ b/std/asys/native/net/SocketOptions.hx @@ -0,0 +1,95 @@ +package asys.native.net; + +typedef SocketOptions = { + /** + Whether local addresses can be reused. + **/ + var reuseAddress:Bool; + /** + Whether local ports can be reused. + **/ + var reusePort:Bool; + /** + Enable sending of keep-alive messages on connection-oriented sockets. + **/ + var keepAlive:Bool; + /** + The maximum size of the send buffer in bytes. + **/ + var sendBuffer:Int; + /** + The maximum size of the receive buffer in bytes. + **/ + var receiveBuffer:Int; + /** + Whether UDP sockets are allowed to send packets to a broadcast address. + **/ + var broadcast:Bool; + /** + The outgoing interface for multicast packets. + **/ + var multicastInterface:String; + /** + The multicast loopback policy, which determines whether multicast packets + sent by the socket also reach receivers in the same host. + This is the case by default. + **/ + var multicastLoop:Bool; + /** + The time-to-live of outgoing multicast packets. + This should be a value between 0 (don't leave the interface) and 255. + The default value is 1 (only the local network is reached). + **/ + var multicastTtl:Int; +} + +enum abstract SocketOptionKind(Int) { + /** + Whether local addresses can be reused. + **/ + var ReuseAddress:SocketOptionKind; + + /** + Whether local ports can be reused. + **/ + var ReusePort:SocketOptionKind; + + /** + Enable sending of keep-alive messages on connection-oriented sockets. + **/ + var KeepAlive:SocketOptionKind; + + /** + The maximum size of the send buffer in bytes. + **/ + var SendBuffer:SocketOptionKind; + + /** + The maximum size of the receive buffer in bytes. + **/ + var ReceiveBuffer:SocketOptionKind; + + /** + Whether UDP sockets are allowed to send packets to a broadcast address. + **/ + var Broadcast:SocketOptionKind; + + /** + The outgoing interface for multicast packets. + **/ + var MulticastInterface:SocketOptionKind; + + /** + The multicast loopback policy, which determines whether multicast packets + sent by the socket also reach receivers in the same host. + This is the case by default. + **/ + var MulticastLoop:SocketOptionKind; + + /** + The time-to-live of outgoing multicast packets. + This should be a value between 0 (don't leave the interface) and 255. + The default value is 1 (only the local network is reached). + **/ + var MulticastTtl:SocketOptionKind; +} \ No newline at end of file diff --git a/std/asys/native/net/UdpSocket.hx b/std/asys/native/net/UdpSocket.hx new file mode 100644 index 00000000000..3eb88835230 --- /dev/null +++ b/std/asys/native/net/UdpSocket.hx @@ -0,0 +1,91 @@ +package asys.native.net; + +import asys.native.net.SocketOptions.SocketOptionKind; +import haxe.io.Bytes; +import haxe.NoData; +import haxe.exceptions.NotImplementedException; + +class UdpSocket { + /** + Indicates if the socket is currently bound to a specific remote address. + **/ + public var bound(get,never):Bool; + function get_bound():Bool throw new NotImplementedException(); + + /** + Local address of this socket. + **/ + public var localAddress(get,never):{host:String, port:Int}; + function get_localAddress():{host:String, port:Int} throw new NotImplementedException(); + + /** + Remote address of this socket if it is bound. + **/ + public var remoteAddress(get,never):Null<{host:String, port:Int}>; + function get_remoteAddress():Null<{host:String, port:Int}> throw new NotImplementedException(); + + /** + Open a UDP socket. + **/ + static public function open(?address:{host:String, port:Int}, ?options:SocketOptions, callback:Callback) { + throw new NotImplementedException(); + } + + /** + Bind the socket to the `host` and `port`. + The callback is supplied with the new write function, which allows to send + data without the need to specify remote address on each call. + **/ + public function bind(host:String, port:Int, callback:Callback<(buffer:Bytes, offset:Int, length:Int)->Void>) { + throw new NotImplementedException(); + } + + /** + Unbind previously bound socket. + **/ + public function unbind(callback:Callback) { + throw new NotImplementedException(); + } + + /** + Send up to `length` bytes from `buffer` (starting from buffer `offset`) to + the remote `host`. + The `callback` is supplied with the amount of bytes sent. + **/ + public function write(buffer:Bytes, offset:Int, length:Int, host:String, port:Int, callback:Callback) { + throw new NotImplementedException(); + } + + /** + Read up to `length` bytes and write them into `buffer` starting from `offset` + position in `buffer`. + The `callback` is supplied with the amount of bytes read and the peer address. + + If `recycle` is `true` then the structure passed to `callback` will be reused + instead of allocating a new one on the next read call with recycling enabled. + **/ + public function read(buffer:Bytes, offset:Int, length:Int, recycle:Bool = false, callback:Callback<{bytesReceived:Int, remoteHost:Ip, remotePort:Int}>) { + throw new NotImplementedException(); + } + + /** + Get the value of a specified socket option. + **/ + public function getOption(option:SocketOptionKind, callback:Callback) { + throw new NotImplementedException(); + } + + /** + Set socket option. + **/ + public function setOption(option:SocketOptionKind, value:T, callback:Callback) { + throw new NotImplementedException(); + } + + /** + Close the socket. + **/ + public function close(callback:Callback) { + throw new NotImplementedException(); + } +} \ No newline at end of file diff --git a/std/asys/native/system/Callback.hx b/std/asys/native/system/Callback.hx new file mode 100644 index 00000000000..1287115f4da --- /dev/null +++ b/std/asys/native/system/Callback.hx @@ -0,0 +1,6 @@ +package asys.native.system; + +import haxe.Exception; + +//TODO: remove after https://github.com/HaxeFoundation/haxe-evolution/pull/50 is implemented +typedef Callback = haxe.Callback; \ No newline at end of file diff --git a/std/asys/native/system/ChildProcess.hx b/std/asys/native/system/ChildProcess.hx new file mode 100644 index 00000000000..c7b5b17c7c2 --- /dev/null +++ b/std/asys/native/system/ChildProcess.hx @@ -0,0 +1,49 @@ +package asys.native.system; + +import haxe.ds.ReadOnlyArray; +import haxe.io.Bytes; +import haxe.NoData; +import haxe.exceptions.NotImplementedException; + +/** + Additional API for child processes spawned by the current process. + + @see asys.native.system.Process.open +**/ +class ChildProcess extends Process { + /** + A stream used by the process as standard input. + **/ + public var stdin(get,never):IWritable; + function get_stdin():IWritable throw new NotImplementedException(); + + /** + A stream used by the process as standard output. + **/ + public var stdout(get,never):IReadable; + function get_stdout():IReadable throw new NotImplementedException(); + + /** + A stream used by the process as standard error output. + **/ + public var stderr(get,never):IReadable; + function get_stderr():IReadable throw new NotImplementedException(); + + /** + Wait the process to shutdown and get the exit code. + If the process is already dead at the moment of this call, then `callback` + may be invoked with the exit code immediately. + **/ + public function exitCode(callback:Callback) { + throw new NotImplementedException(); + } + + /** + Close the process handle and release associated resources. + + TODO: should this method wait for the process to finish? + **/ + public function close(callback:Callback) { + throw new NotImplementedException(); + } +} \ No newline at end of file diff --git a/std/asys/native/system/CurrentProcess.hx b/std/asys/native/system/CurrentProcess.hx new file mode 100644 index 00000000000..eb2c030839e --- /dev/null +++ b/std/asys/native/system/CurrentProcess.hx @@ -0,0 +1,42 @@ +package asys.native.system; + +import haxe.exceptions.NotImplementedException; + +/** + Additional API for the current process. + + @see asys.native.system.Process.current +**/ +class CurrentProcess extends Process { + /** + A stream used by the process as standard input. + **/ + public var stdin(get,never):IReadable; + function get_stdin():IReadable throw new NotImplementedException(); + + /** + A stream used by the process as standard output. + **/ + public var stdout(get,never):IWritable; + function get_stdout():IWritable throw new NotImplementedException(); + + /** + A stream used by the process as standard error output. + **/ + public var stderr(get,never):IWritable; + function get_stderr():IWritable throw new NotImplementedException(); + + /** + Set the action taken by the process on receipt of a `signal`. + + Possible `action` values: + - `Ignore` - ignore the signal; + - `Default` - restore default action; + - `Handle(handler:() -> Void)` - execute `handler` on `signal` receipt. + + Actions for `Kill` and `Stop` signals cannot be changed. + **/ + public function setSignalAction(signal:Signal, action:SignalAction):Void { + throw new NotImplementedException(); + } +} \ No newline at end of file diff --git a/std/asys/native/system/PosixSignalCode.hx b/std/asys/native/system/PosixSignalCode.hx new file mode 100644 index 00000000000..6fe5f0e2a73 --- /dev/null +++ b/std/asys/native/system/PosixSignalCode.hx @@ -0,0 +1,67 @@ +package asys.native.system; + +/** + Signal codes as described in signal(7) man page. + + TODO: + Docs below are copy-pasted from `man 7 signal`. + Rewrite to avoid legal issues. +**/ +extern enum abstract PosixSignalCode(Int) from Int to Int { + /** Hangup detected on controlling terminal or death of controlling process */ + var SIGHUP; + /** Interrupt from keyboard */ + var SIGINT; + /** Quit from keyboard */ + var SIGQUIT; + /** Illegal Instruction */ + var SIGILL; + /** Abort signal from abort(3) */ + var SIGABRT; + /** Floating-point exception */ + var SIGFPE; + /** Kill signal */ + var SIGKILL; + /** Invalid memory reference */ + var SIGSEGV; + /** Broken pipe: write to pipe with no readers */ + var SIGPIPE; + /** Timer signal from alarm(2) */ + var SIGALRM; + /** Termination signal */ + var SIGTERM; + /** User-defined signal 1 */ + var SIGUSR1; + /** User-defined signal 2 */ + var SIGUSR2; + /** Child stopped or terminated */ + var SIGCHLD; + /** Continue if stopped */ + var SIGCONT; + /** Stop process */ + var SIGSTOP; + /** Stop typed at terminal */ + var SIGTSTP; + /** Terminal input for background process */ + var SIGTTIN; + /** Terminal output for background process */ + var SIGTTOU; + /** Bus error (bad memory access) */ + var SIGBUS; + /** Pollable event (Sys V) */ + var SIGPOLL; + /** Profiling timer expired */ + var SIGPROF; + /** Bad system call (SVr4) */ + var SIGSYS; + /** Trace/breakpoint trap */ + var SIGTRAP; + /** Urgent condition on socket (4.2BSD) */ + var SIGURG; + /** Virtual alarm clock (4.2BSD) */ + var SIGVTALRM; + /** CPU time limit exceeded (4.2BSD); */ + var SIGXCPU; + /** File size limit exceeded (4.2BSD) */ + var SIGXFSZ; +} \ No newline at end of file diff --git a/std/asys/native/system/Process.hx b/std/asys/native/system/Process.hx new file mode 100644 index 00000000000..32d950af121 --- /dev/null +++ b/std/asys/native/system/Process.hx @@ -0,0 +1,82 @@ +package asys.native.system; + +import haxe.ds.ReadOnlyArray; +import haxe.io.Bytes; +import haxe.NoData; +import haxe.exceptions.NotImplementedException; + +/** + Process execution API +**/ +class Process { + /** + Current process handle. + Can be used to communicate with the parent process and for self-signalling. + **/ + static public var current(get,never):CurrentProcess; + static function get_current():CurrentProcess throw new NotImplementedException(); + + /** + Process id. + **/ + public final pid:Int; + + /** + Initial IO streams opened for this process. + The first three indices always are: + - 0 - stdin + - 1 - stdout + - 2 - stderr + Indices from 3 and higher contain handlers for streams created as configured + by the corresponding indices in `options.stdio` field of `options` argument + for `asys.native.system.Process.open` call. + @see asys.native.system.ProcessOptions.stdio + **/ + public var stdio(get,never):ReadOnlyArray; + function get_stdio():ReadOnlyArray throw new NotImplementedException(); + + //TODO: this is a dummy constructor to make the compiler shut up about uninitialized finals. + function new() { + pid = -1; + } + + /** + Execute and wait for the `command` to fully finish and invoke `callback` with + the exit code and the contents of stdout, and stderr. + + The `command` argument should not contain command line arguments. Those should + be passed to `options.args` + + In case the command didn't emit anything to stdout or stderr, the respective + field of the result structure will be `null`. + + @see asys.native.system.ProcessOptions for various process configuration options. + */ + static public function execute(command:String, ?options:ProcessOptions, callback:Callback<{?stdout:Bytes, ?stderr:Bytes, exitCode:Int}>) { + throw new NotImplementedException(); + } + + /** + Start `command` execution. + + The `command` argument should not contain command line arguments. Those should + be passed to `options.args` + + @see asys.native.system.ProcessOptions for various process configuration options. + */ + static public function open(command:String, ?options:ProcessOptions, callback:Callback) { + throw new NotImplementedException(); + } + + /** + Send `signal` to this process. + + This function does not wait for the process to finish. + The `callback` only indicates if the signal was sent successfully. + + @see asys.native.system.Signal + **/ + public function sendSignal(signal:Signal, callback:Callback) { + throw new NotImplementedException(); + } +} \ No newline at end of file diff --git a/std/asys/native/system/ProcessOptions.hx b/std/asys/native/system/ProcessOptions.hx new file mode 100644 index 00000000000..8a901cce5cf --- /dev/null +++ b/std/asys/native/system/ProcessOptions.hx @@ -0,0 +1,56 @@ +package asys.native.system; + +import asys.native.system.StdioConfig; +import asys.native.filesystem.FilePath; + +/** + Options to configure new processes spawned via `asys.native.system.Process.execute` +**/ +typedef ProcessOptions = { + /** + Command line arguments. + **/ + var ?args:Array; + /** + Working directory for a new process. + By default current process working directory is used. + **/ + var ?cwd:FilePath; + /** + Environment variables for a new process. + By default current process environment is used. + **/ + var ?env:Map; + /** + Setup standard IO streams for a new process. + First three indices are always used as follows: + - 0 - stdin + - 1 - stdout + - 2 - stderr + Indices from 3 and higher can be used to setup additional IO streams. + If the array has less than three items, then default setup will be used + for missing items. + If `stdio` contains less than 3 items, default behavior will be used for + missing ones. + If `stdio` field is not specified at all, three anonymous pipes will be + initiated for stdin, stdout and stderr of a new process. + @see asys.native.system.StdioConfig + **/ + var ?stdio:Array; + /** + Run new process with `user` identity. + By default: the owner of the current process. + **/ + var ?user:Int; + /** + Run new process on behalf of `group`. + By default: the group of the current process. + **/ + var ?group:Int; + /** + When `true`, creates a detached process which can continue running after + the current process exits. + By default: `false`. + **/ + var ?detached:Bool; +} \ No newline at end of file diff --git a/std/asys/native/system/Signal.hx b/std/asys/native/system/Signal.hx new file mode 100644 index 00000000000..617eec4dbcc --- /dev/null +++ b/std/asys/native/system/Signal.hx @@ -0,0 +1,47 @@ +package asys.native.system; + +/** + Signals for inter-process communication. + Signals are sent by operating system or by a process to another process. + Also, a process can signal itself. + + TODO: any other signals to have own constructors here? +**/ +enum Signal { + /** + Terminate a process. + The process is _not_ immediately killed. + Instead the process can handle this signal to gracefully shutdown + (remove temporary files, close socket connections etc). + POSIX equivalent: SIGTERM + **/ + Terminate; + /** + Immediately terminate a process. + The process cannot handle this signal. + That means, for example, temporary files may stay in file system. + POSIX equivalent: SIGKILL + **/ + Kill; + /** + Interrupt a process. + The same as pressing "CTRL+C" in a terminal. + POSIX equivalent: SIGINT + **/ + Interrupt; + /** + _Pause_ a process. + The process cannot handle this signal. + POSIX equivalent: SIGSTOP + **/ + Stop; + /** + Continue previously stopped process. + POSIX equivalent: SIGCONT + **/ + Resume; + /** + Any signal can be specified by its code. + **/ + SignalCode(code:PosixSignalCode); +} \ No newline at end of file diff --git a/std/asys/native/system/SignalAction.hx b/std/asys/native/system/SignalAction.hx new file mode 100644 index 00000000000..4870d5e6f89 --- /dev/null +++ b/std/asys/native/system/SignalAction.hx @@ -0,0 +1,21 @@ +package asys.native.system; + +/** + Possible actions to handle inter-process signals. +**/ +enum SignalAction { + /** + Ignore a signal. + **/ + Ignore; + + /** + Use default action for a signal. + **/ + Default; + + /** + Execute `handler` on a signal receipt. + **/ + Handle(handler:() -> Void); +} \ No newline at end of file diff --git a/std/asys/native/system/StdioConfig.hx b/std/asys/native/system/StdioConfig.hx new file mode 100644 index 00000000000..ecfca6839c7 --- /dev/null +++ b/std/asys/native/system/StdioConfig.hx @@ -0,0 +1,51 @@ +package asys.native.system; + +import asys.native.filesystem.File; +import asys.native.filesystem.FileOpenFlag; +import asys.native.filesystem.FilePath; + +/** + This enum allows to configure IO channels of a process being created with functions + like `asys.native.system.Process.open` and such. + @see asys.native.system.Process +**/ +enum StdioConfig { + /** + Create a unidirectional pipe for IO channel. + The child process will be able to read from the pipe, while the parent + process will be able to write to the pipe. + This is the default behavior for stdin. + **/ + PipeRead; + /** + Create a unidirectional pipe for IO channel. + The child process will be able to write to the pipe, while the parent + process will be able to read from the pipe. + This is the default behavior for stdout and stderr. + **/ + PipeWrite; + /** + Create a bidirectional pipe for IO channel. + Both child and parent processes will be able to read from and to write to + the pipe. + **/ + PipeReadWrite; + /** + Use the corresponding IO stream of the parent process. + For example if `Inherit` is used for stdin of the child process, then stdin + of the parent process will be used. + **/ + Inherit; + /** + Connect IO channel to `/dev/null` on unix-like systems and to `NUL` on windows. + **/ + Ignore; + /** + Use specified file as a source and/or a target for IO. + **/ + File(path:FilePath, flags:FileOpenFlag); + /** + Use an opened file as a source and/or a target for IO. + **/ + OpenedFile(file:File); +} \ No newline at end of file diff --git a/std/asys/native/system/SystemGroup.hx b/std/asys/native/system/SystemGroup.hx new file mode 100644 index 00000000000..49ec42aeb6b --- /dev/null +++ b/std/asys/native/system/SystemGroup.hx @@ -0,0 +1,14 @@ +package asys.native.system; + +private typedef NativeGroup = Int; + +/** + Represents an OS group account. +**/ +@:coreApi +abstract SystemGroup(NativeGroup) from NativeGroup to NativeGroup { + + public inline function toString():String { + return '$this'; + } +} \ No newline at end of file diff --git a/std/asys/native/system/SystemUser.hx b/std/asys/native/system/SystemUser.hx new file mode 100644 index 00000000000..b197a895a3e --- /dev/null +++ b/std/asys/native/system/SystemUser.hx @@ -0,0 +1,14 @@ +package asys.native.system; + +private typedef NativeUser = Int; + +/** + Represents an OS user account. +**/ +@:coreApi +abstract SystemUser(NativeUser) from NativeUser to NativeUser { + + public inline function toString():String { + return '$this'; + } +} \ No newline at end of file diff --git a/std/cpp/_std/sys/thread/Thread.hx b/std/cpp/_std/sys/thread/Thread.hx index f97de446b93..63ec841a316 100644 --- a/std/cpp/_std/sys/thread/Thread.hx +++ b/std/cpp/_std/sys/thread/Thread.hx @@ -52,9 +52,10 @@ abstract Thread(ThreadImpl) from ThreadImpl { } function get_events():EventLoop { - if(this.events == null) - throw new NoEventLoopException(); - return this.events; + switch this.events { + case null: throw new NoEventLoopException(); + case events: return events; + } } @:keep diff --git a/std/cs/_std/asys/native/filesystem/Directory.hx b/std/cs/_std/asys/native/filesystem/Directory.hx new file mode 100644 index 00000000000..787e6375942 --- /dev/null +++ b/std/cs/_std/asys/native/filesystem/Directory.hx @@ -0,0 +1,78 @@ +package asys.native.filesystem; + +import haxe.NoData; +import haxe.exceptions.NotImplementedException; +import cs.system.Exception as CsException; +import asys.native.filesystem.FileSystem.pool; +import asys.native.filesystem.FileSystem.rethrow; +import cs.system.collections.IEnumerator; + +@:coreApi +class Directory { + public final path:FilePath; + final maxBatchSize:Int; + +#if (net_ver >= 40) + final contents:IEnumerator; + + @:allow(asys.native.filesystem) + function new(contents:IEnumerator, path:FilePath, maxBatchSize:Int) { + this.contents = contents; + this.maxBatchSize = maxBatchSize; + this.path = path; + } + + public function next(callback:Callback>):Void { + pool.runFor( + () -> { + try { + var result = []; + for(i in 0...maxBatchSize) { + if(!contents.MoveNext()) + break; + result.push((contents.Current:FilePath).name()); + } + result; + } catch(e:CsException) { + rethrow(e, path); + } + }, + callback + ); + } +#else + final contents:Array; + var current = 0; + + @:allow(asys.native.filesystem) + function new(contents:Array, path:FilePath, maxBatchSize:Int) { + this.contents = contents; + this.maxBatchSize = maxBatchSize; + this.path = path; + } + + public function next(callback:Callback>):Void { + pool.runFor( + () -> { + try { + if(current < contents.length) { + var result = contents.slice(current, current + maxBatchSize); + for(i => entry in result) + result[i] = entry.name(); + current += maxBatchSize; + result; + } else { + []; + } + } catch(e:CsException) { + rethrow(e, path); + } + }, + callback + ); + } +#end + public function close(callback:Callback):Void { + pool.runFor(() -> NoData, callback); + } +} \ No newline at end of file diff --git a/std/cs/_std/asys/native/filesystem/File.hx b/std/cs/_std/asys/native/filesystem/File.hx new file mode 100644 index 00000000000..cccf9e07c6f --- /dev/null +++ b/std/cs/_std/asys/native/filesystem/File.hx @@ -0,0 +1,209 @@ +package asys.native.filesystem; + +import haxe.Int64; +import haxe.io.Bytes; +import haxe.NoData; +import haxe.exceptions.NotSupportedException; +import asys.native.system.SystemUser; +import asys.native.system.SystemGroup; +import cs.system.Exception as CsException; +import cs.system.io.File as CsFile; +import cs.system.io.FileInfo as CsFileInfo; +import cs.system.io.FileStream; +import cs.system.io.SeekOrigin; +import cs.system.io.FileAttributes; +import cs.system.DateTime; +import cs.system.DateTimeKind; +import cs.system.DateTimeOffset; +import asys.native.filesystem.FileSystem.pool; +import asys.native.filesystem.FileSystem.rethrow; +import asys.native.filesystem.FileSystem.unixEpoch; + +@:coreApi +class File { + public final path:FilePath; + final stream:FileStream; + final forAppend:Bool; + + @:allow(asys.native.filesystem) + function new(stream:FileStream, path:FilePath, forAppend:Bool):Void { + this.stream = stream; + this.path = path; + this.forAppend = forAppend; + } + + public function write(position:Int64, buffer:Bytes, offset:Int, length:Int, callback:Callback):Void { + pool.runFor( + () -> { + try { + if(!forAppend) + stream.Seek(position, SeekOrigin.Begin); + if(buffer.length < offset + length) + length = buffer.length - offset; + if(offset == buffer.length && length >= 0) { + 0; + } else { + stream.Write(buffer.getData(), offset, length); + length; + } + } catch(e:CsException) { + rethrow(e, path); + } + }, + callback + ); + } + + public function read(position:Int64, buffer:Bytes, offset:Int, length:Int, callback:Callback):Void { + pool.runFor( + () -> { + try { + if(buffer.length < offset + length) + length = buffer.length - offset; + stream.Seek(position, SeekOrigin.Begin); + var bytesRead = stream.Read(buffer.getData(), offset, length); + bytesRead; + } catch(e:CsException) { + rethrow(e, path); + } + }, + callback + ); + } + + public function flush(callback:Callback):Void { + pool.runFor( + () -> { + try { + stream.Flush(); + NoData; + } catch(e:CsException) { + rethrow(e, path); + } + }, + callback + ); + } + + public function info(callback:Callback):Void { + pool.runFor( + () -> { + try { + var fi = new CsFileInfo(path); + ({ + gid: 0, + uid: 0, + atime: Std.int(fi.LastAccessTime.ToUniversalTime().Subtract(unixEpoch).TotalSeconds), + mtime: Std.int(fi.LastWriteTime.ToUniversalTime().Subtract(unixEpoch).TotalSeconds), + ctime: Std.int(fi.CreationTime.ToUniversalTime().Subtract(unixEpoch).TotalSeconds), + size: cast(fi.Length, Int), + dev: 0, + ino: 0, + nlink: 0, + rdev: 0, + mode: @:privateAccess FileMode.S_IFREG, + blksize: 0, + blocks: 0 + }:FileInfo); + } catch(e:CsException) { + rethrow(e, path); + } + }, + callback + ); + } + + public function setPermissions(permissions:FilePermissions, callback:Callback):Void { + pool.runFor( + () -> { + try { + var attr = (cast CsFile.GetAttributes(path):Int); + var ro = (cast FileAttributes.ReadOnly:Int); + if(attr & 128 == 0) // u+w + CsFile.SetAttributes(path, cast (attr | ro)) + else + CsFile.SetAttributes(path, cast (attr & ~ro)); + NoData; + } catch(e:CsException) { + rethrow(e, path); + } + }, + callback + ); + } + + public function setOwner(user:SystemUser, group:SystemGroup, callback:Callback):Void { + throw NotSupportedException.field(); + } + + public function resize(newSize:Int, callback:Callback):Void { + pool.runFor( + () -> { + try { + stream.SetLength(newSize); + NoData; + } catch(e:CsException) { + rethrow(e, path); + } + }, + callback + ); + } + + public function setTimes(accessTime:Int, modificationTime:Int, callback:Callback):Void { + pool.runFor( + () -> { + try { + var epoch = new DateTime(1970, 1, 1, 0, 0, 0, 0, DateTimeKind.Utc); + CsFile.SetLastAccessTimeUtc(path, epoch.AddSeconds(accessTime)); + CsFile.SetLastWriteTimeUtc(path, epoch.AddSeconds(modificationTime)); + NoData; + } catch(e:CsException) { + rethrow(e, path); + } + }, + callback + ); + } + + /** + TODO: this requires a separate work for design and implementation + to find a solid cross-platform solution. + + Acquire or release a file lock for the current process. + + The `callback` is supplied with `true` if a lock was successfully acquired. + + Modes: + - `Shared` - acquire a shared lock (usually used for reading) + - `Exclusive` - acquire an exclusive lock (usually used for writing) + - `Unlock` - release a lock. + + By default (`wait` is `true`) `lock` waits until a lock can be acquired. + Pass `false` to `wait` to invoke `callback` with `false` if a lock cannot + be acquired immediately. + + Although a lock may be released automatically on file closing, for a + consistent cross-platform behavior it is strongly recommended to always + release a lock manually. + + This lock is _not_ suitable for controlling access to a file by multiple threads. + **/ + // public function lock(mode:FileLock = Exclusive, wait:Bool = true, callback:Callback):Void { + // throw new NotImplementedException(); + // } + + public function close(callback:Callback):Void { + pool.runFor( + () -> { + try { + stream.Close(); + NoData; + } catch(e:CsException) { + rethrow(e, path); + } + }, + callback + ); + } +} \ No newline at end of file diff --git a/std/cs/_std/asys/native/filesystem/FilePath.hx b/std/cs/_std/asys/native/filesystem/FilePath.hx new file mode 100644 index 00000000000..4a54291aff3 --- /dev/null +++ b/std/cs/_std/asys/native/filesystem/FilePath.hx @@ -0,0 +1,144 @@ +package asys.native.filesystem; + +import haxe.exceptions.ArgumentException; +import cs.system.io.Path; +import cs.NativeArray; + +using StringTools; + +private typedef NativeFilePath = String; +private typedef NativeString = cs.system.String; + +@:coreApi abstract FilePath(NativeFilePath) { + public static var SEPARATOR(get,never):String; + @:pure(true) static inline function get_SEPARATOR():String { + return untyped __cs__('{0}.ToString()', Path.DirectorySeparatorChar); + } + + static inline function isSeparator(c:Int):Bool { + return c == '/'.code || (SEPARATOR == '\\' && c == '\\'.code); + } + + static function trimSlashes(s:String):String { + var i = s.length - 1; + if(i <= 0) + return s; + var sep = isSeparator(s.fastCodeAt(i)); + if(sep) { + do { + --i; + sep = isSeparator(s.fastCodeAt(i)); + } while(i > 0 && sep); + return s.substr(0, i + 1); + } else { + return s; + } + } + + overload extern static public inline function createPath(path:String, ...appendices:String):FilePath { + return createPathImpl(path, ...appendices); + } + + @:native('createPath') + static function createPathImpl(path:String, ...appendices:String):FilePath { + var path = ofString(path); + for(p in appendices) + path = path.add(p); + return path; + } + + overload extern static public inline function createPath(parts:Array):FilePath { + return ofArray(parts); + } + + @:noUsing + @:from public static inline function ofString(path:String):FilePath { + return new FilePath(path); + } + + @:from static function ofArray(parts:Array):FilePath { + if(parts.length == 0) + throw new ArgumentException('parts'); + var result = parts[0]; + for(i in 1...parts.length) { + result = Path.Combine(result, parts[i]); + } + return new FilePath(result); + } + + @:from static inline function ofNative(path:NativeFilePath):FilePath { + return new FilePath(path); + } + + inline function new(path:NativeFilePath) { + this = path; + } + + public inline function add(path:FilePath):FilePath { + return Path.Combine(this, path); + } + + @:to public inline function toString():String { + return this; + } + + public function isAbsolute():Bool { + if(Path.IsPathRooted(this)) { + if(SEPARATOR == '\\' && this.length >= 2 && this.fastCodeAt(1) == ':'.code) { + return isSeparator(this.fastCodeAt(2)); + } else { + return true; + } + } else { + return false; + } + } + + public function absolute():FilePath { + return Path.GetFullPath(this == '' ? '.' : this); + } + + public function parent():Null { + return switch Path.GetDirectoryName(this == '' ? '.' : trimSlashes(this)) { + case '': null; + case path: new FilePath(path); + } + } + + public function name():FilePath { + return Path.GetFileName(trimSlashes(this)); + } + + public function normalize():FilePath { + var delimiter = if(SEPARATOR == '\\') { + var str = new NativeArray(2); + str[0] = '\\'; + str[1] = '/'; + str; + } else { + var str = new NativeArray(1); + str[0] = '/'; + str; + } + var parts = (cast this:NativeString).Split(delimiter, cs.system.StringSplitOptions.None); + var i = parts.Length - 1; + var result = []; + var skip = 0; + while(i >= 0) { + switch parts[i] { + case '.' | '': + case '..': + ++skip; + case _ if(skip > 0): + --skip; + case part: + result.unshift(part); + } + --i; + } + for(i in 0...skip) + result.unshift('..'); + var result = ofString(result.join(SEPARATOR)); + return isAbsolute() && !result.isAbsolute() ? SEPARATOR + result : result; + } +} \ No newline at end of file diff --git a/std/cs/_std/asys/native/filesystem/FileSystem.hx b/std/cs/_std/asys/native/filesystem/FileSystem.hx new file mode 100644 index 00000000000..8f431aabe4a --- /dev/null +++ b/std/cs/_std/asys/native/filesystem/FileSystem.hx @@ -0,0 +1,544 @@ +package asys.native.filesystem; + +import haxe.io.Bytes; +import haxe.NoData; +import haxe.exceptions.NotImplementedException; +import haxe.exceptions.NotSupportedException; +import cs.NativeArray; +import cs.system.Exception as CsException; +import sys.thread.ElasticThreadPool; +import asys.native.system.SystemUser; +import asys.native.system.SystemGroup; +import cs.system.io.Path; +import cs.system.io.File as CsFile; +import cs.system.io.Directory as CsDirectory; +import cs.system.io.FileMode; +import cs.system.io.FileOptions; +import cs.system.io.FileAccess; +import cs.system.io.FileStream; +import cs.system.io.FileAttributes; +import cs.system.io.FileInfo as CsFileInfo; +import cs.system.io.DirectoryInfo; +import cs.system.io.FileNotFoundException; +import cs.system.io.DirectoryNotFoundException; +import cs.system.security.SecurityException; +import cs.system.text.Encoding.UTF8; +import cs.StdTypes.UInt8; +import cs.system.DateTime; +import cs.system.DateTimeKind; +import cs.system.DateTimeOffset; +import cs.system.Guid; +import cs.system.security.accesscontrol.FileSystemRights; +import cs.system.security.accesscontrol.AccessControlType; +import cs.system.security.PermissionSet; +import cs.system.security.permissions.PermissionState; +import cs.system.security.permissions.FileIOPermission; +import cs.system.security.permissions.FileIOPermissionAccess; +import cs.system.AppDomain; + + +@:coreApi +class FileSystem { + @:allow(asys.native.filesystem) + static final pool = new ElasticThreadPool(2 * cs.system.Environment.ProcessorCount); + @:allow(asys.native.filesystem) + static final unixEpoch = new DateTime(1970, 1, 1, 0, 0, 0, 0, DateTimeKind.Utc); + + static public function openFile(path:FilePath, flag:FileOpenFlag, callback:Callback):Void { + pool.runFor( + () -> { + var stream = null; + try { + stream = streamFile(path, flag); + var forAppend = switch flag { + case Append: true; + case _: false; + } + cast new File(stream, path, forAppend); + } catch(e:CsException) { + closeStream(stream); + rethrow(e, path); + } + }, + callback + ); + } + + static public function tempFile(callback:Callback):Void { + pool.runFor( + () -> { + var stream = null; + var path = Path.GetTempFileName(); + try { + var options:FileOptions = cast ((cast RandomAccess:Int) | (cast DeleteOnClose:Int)); + stream = new FileStream(path, Create, ReadWrite, ReadWrite, 4096, options); + cast new File(stream, path, false); + } catch(e:CsException) { + closeStream(stream); + rethrow(e, path); + } + }, + callback + ); + } + + static public function readBytes(path:FilePath, callback:Callback):Void { + pool.runFor( + () -> { + try Bytes.ofData(CsFile.ReadAllBytes(path)) + catch(e:CsException) rethrow(e, path); + }, + callback + ); + } + + static public function readString(path:FilePath, callback:Callback):Void { + pool.runFor( + () -> { + try CsFile.ReadAllText(path) + catch(e:CsException) rethrow(e, path); + }, + callback + ); + } + + static public inline function writeBytes(path:FilePath, data:Bytes, flag:FileOpenFlag = Write, callback:Callback):Void { + writeNativeBytes(path, data.getData(), flag, callback); + } + + static public inline function writeString(path:FilePath, text:String, flag:FileOpenFlag = Write, callback:Callback):Void { + writeNativeBytes(path, UTF8.GetBytes(text), flag, callback); + } + + static function writeNativeBytes(path:String, bytes:NativeArray, flag:FileOpenFlag, callback:Callback):Void { + pool.runFor( + () -> { + var stream = null; + try { + stream = streamFile(path, flag); + stream.Write(bytes, 0, bytes.Length); + stream.Close(); + NoData; + } catch(e:CsException) { + closeStream(stream); + rethrow(e, path); + } + }, + callback + ); + } + + static public function openDirectory(path:FilePath, maxBatchSize:Int = 64, callback:Callback):Void { + pool.runFor( + () -> { + try { + if(!CsDirectory.Exists(path)) + if(CsFile.Exists(path)) + throw new FsException(NotDirectory, path) + else + throw new FsException(FileNotFound, path); + #if (net_ver >= 40) + var contents = CsDirectory.EnumerateFileSystemEntries(path).GetEnumerator(); + #else + var entries = CsDirectory.GetFileSystemEntries(path); + var contents:Array = @:privateAccess Array.alloc(entries.length); + for(i in 0...entries.length) + contents[i] = FilePath.ofString(entries[i]); + #end + new Directory(contents, path, maxBatchSize); + } catch(e:FsException) { + throw e; + } catch(e:CsException) { + rethrow(e, path); + } + }, + callback + ); + } + + static public function listDirectory(path:FilePath, callback:Callback>):Void { + pool.runFor( + () -> { + try { + var entries = CsDirectory.GetFileSystemEntries(path); + var result:Array = @:privateAccess Array.alloc(entries.length); + for(i in 0...entries.length) + result[i] = FilePath.ofString(entries[i]).name(); + result; + } catch(e:CsException) { + rethrow(e, path); + } + }, + callback + ); + } + + static public function createDirectory(path:FilePath, ?permissions:FilePermissions, recursive:Bool = false, callback:Callback):Void { + pool.runFor( + () -> { + try { + if(recursive) + CsDirectory.CreateDirectory(path) + else { + switch path.parent() { + case null: + case parent if(!CsDirectory.Exists(parent)): + throw new DirectoryNotFoundException(parent); + } + CsDirectory.CreateDirectory(path); + } + NoData; + } catch(e:CsException) { + rethrow(e, path); + } + }, + callback + ); + } + + static public function uniqueDirectory(parentDirectory:FilePath, ?prefix:String, ?permissions:FilePermissions, recursive:Bool = false, callback:Callback):Void { + pool.runFor( + () -> { + if(prefix == null) + prefix = ''; + var path = parentDirectory.add(prefix + Guid.NewGuid().ToString()); + try { + if(recursive || CsDirectory.Exists(parentDirectory)) + CsDirectory.CreateDirectory(path) + else + throw new DirectoryNotFoundException(parentDirectory); + path; + } catch(e:DirectoryNotFoundException) { + throw new FsException(FileNotFound, parentDirectory); + } catch(e:CsException) { + rethrow(e, path); + } + }, + callback + ); + } + + static public function move(oldPath:FilePath, newPath:FilePath, overwrite:Bool = true, callback:Callback):Void { + pool.runFor( + () -> { + try { + try { + if(overwrite && CsFile.Exists(newPath)) + CsFile.Delete(newPath); + CsFile.Move(oldPath, newPath); + } catch(e:FileNotFoundException) { + if(!overwrite && CsDirectory.Exists(newPath)) + throw new FsException(FileExists, newPath); + CsDirectory.Move(oldPath, newPath); + } + NoData; + } catch(e:FsException) { + throw e; + } catch(e:CsException) { + rethrow(e, oldPath); + } + }, + callback + ); + } + + static public function deleteFile(path:FilePath, callback:Callback):Void { + pool.runFor( + () -> { + try { + if(!CsFile.Exists(path)) + throw new FileNotFoundException(path); + CsFile.Delete(path); + NoData; + } catch(e:CsException) { + rethrow(e, path); + } + }, + callback + ); + } + + static public function deleteDirectory(path:FilePath, callback:Callback):Void { + pool.runFor( + () -> { + try { + if(!CsDirectory.Exists(path)) + throw new DirectoryNotFoundException(path); + CsDirectory.Delete(path); + NoData; + } catch(e:CsException) { + rethrow(e, path); + } + }, + callback + ); + } + + static public function info(path:FilePath, callback:Callback):Void { + pool.runFor( + () -> { + try { + var fi = new CsFileInfo(path); + if(!fi.Exists) + throw new FileNotFoundException(path); + ({ + gid: 0, + uid: 0, + atime: Std.int(fi.LastAccessTime.ToUniversalTime().Subtract(unixEpoch).TotalSeconds), + mtime: Std.int(fi.LastWriteTime.ToUniversalTime().Subtract(unixEpoch).TotalSeconds), + ctime: Std.int(fi.CreationTime.ToUniversalTime().Subtract(unixEpoch).TotalSeconds), + size: cast(fi.Length, Int), + dev: 0, + ino: 0, + nlink: 0, + rdev: 0, + mode: @:privateAccess FileMode.S_IFREG, + blksize: 0, + blocks: 0 + }:FileInfo); + } catch(e:FileNotFoundException) { + try { + var di = new DirectoryInfo(path); + if(!di.Exists) + throw new DirectoryNotFoundException(path); + ({ + gid: 0, + uid: 0, + atime: Std.int(di.LastAccessTime.ToUniversalTime().Subtract(unixEpoch).TotalSeconds), + mtime: Std.int(di.LastWriteTime.ToUniversalTime().Subtract(unixEpoch).TotalSeconds), + ctime: Std.int(di.CreationTime.ToUniversalTime().Subtract(unixEpoch).TotalSeconds), + size: 0, + dev: 0, + ino: 0, + nlink: 0, + rdev: 0, + mode: @:privateAccess FileMode.S_IFDIR, + blksize: 0, + blocks: 0 + }:FileInfo); + } catch(e:CsException) { + rethrow(e, path); + } + } catch(e:CsException) { + rethrow(e, path); + } + }, + callback + ); + } + + static public function check(path:FilePath, mode:FileAccessMode, callback:Callback):Void { + pool.runFor( + () -> { + var stream = null; + try { + var result = true; + var isFile = CsFile.Exists(path); + var isDir = !isFile && CsDirectory.Exists(path); + if(mode.has(Exists) && !isFile && !isDir) { + result = false; + } else if(isFile) { + stream = streamFile(path, ReadWrite); + if(mode.has(Readable)) { + result = result && stream.CanRead; + } + if(mode.has(Writable)) { + result = result && stream.CanWrite; + } + if(mode.has(Executable)) { + //TODO + } + stream.Close(); + } else if(isDir) { //if `isDir` is `true` it means the directory is at least readable, so check only for writable + if(mode.has(Writable)) { + var permissionSet = new PermissionSet(PermissionState.None); + var writePermission = new FileIOPermission(FileIOPermissionAccess.Write, path.absolute()); + permissionSet.AddPermission(writePermission); + #if (net_ver >= 40) + if(!permissionSet.IsSubsetOf(AppDomain.CurrentDomain.PermissionSet)) { + result = false; + } + #else + //TODO + #end + } + } else { + result = false; + } + result; + } catch(e:CsException) { + closeStream(stream); + rethrow(e, path); + } + }, + callback + ); + } + + static public function isDirectory(path:FilePath, callback:Callback):Void { + pool.runFor( + () -> { + try { + CsDirectory.Exists(path); + } catch(e:CsException) { + rethrow(e, path); + } + }, + callback + ); + } + + static public function isFile(path:FilePath, callback:Callback):Void { + pool.runFor( + () -> { + try { + CsFile.Exists(path); + } catch(e:CsException) { + rethrow(e, path); + } + }, + callback + ); + } + + static public function setPermissions(path:FilePath, permissions:FilePermissions, callback:Callback):Void { + pool.runFor( + () -> { + try { + var attr = (cast CsFile.GetAttributes(path):Int); + var ro = (cast FileAttributes.ReadOnly:Int); + if(attr & 128 == 0) // u+w + CsFile.SetAttributes(path, cast (attr | ro)) + else + CsFile.SetAttributes(path, cast (attr & ~ro)); + NoData; + } catch(e:CsException) { + rethrow(e, path); + } + }, + callback + ); + } + + static public function setOwner(path:FilePath, user:SystemUser, group:SystemGroup, callback:Callback):Void { + throw NotSupportedException.field(); + } + + static public function setLinkOwner(path:FilePath, user:SystemUser, group:SystemGroup, callback:Callback):Void { + throw NotSupportedException.field(); + } + + static public function link(target:FilePath, path:FilePath, type:FileLink = SymLink, callback:Callback):Void { + throw NotSupportedException.field(); + } + + static public function isLink(path:FilePath, callback:Callback):Void { + throw NotSupportedException.field(); + } + + static public function readLink(path:FilePath, callback:Callback):Void { + throw NotSupportedException.field(); + } + + static public function linkInfo(path:FilePath, callback:Callback):Void { + throw NotSupportedException.field(); + } + + static public function copyFile(source:FilePath, destination:FilePath, overwrite:Bool = true, callback:Callback):Void { + pool.runFor( + () -> { + try { + CsFile.Copy(source, destination, overwrite); + NoData; + } catch(e:CsException) { + rethrow(e, source); + } + }, + callback + ); + } + + static public function resize(path:FilePath, newSize:Int, callback:Callback):Void { + pool.runFor( + () -> { + var stream = null; + try { + stream = streamFile(path, OverwriteRead); + stream.SetLength(newSize); + stream.Close(); + NoData; + } catch(e:CsException) { + closeStream(stream); + rethrow(e, path); + } + }, + callback + ); + } + + static public function setTimes(path:FilePath, accessTime:Int, modificationTime:Int, callback:Callback):Void { + pool.runFor( + () -> { + try { + var epoch = new DateTime(1970, 1, 1, 0, 0, 0, 0, DateTimeKind.Utc); + CsFile.SetLastAccessTimeUtc(path, epoch.AddSeconds(accessTime)); + CsFile.SetLastWriteTimeUtc(path, epoch.AddSeconds(modificationTime)); + NoData; + } catch(e:CsException) { + rethrow(e, path); + } + }, + callback + ); + } + + static public function realPath(path:FilePath, callback:Callback):Void { + pool.runFor( + () -> { + try { + //C# does not have API to resolve symlinks + if(!CsFile.Exists(path)) + throw new FileNotFoundException('File not found', path); + path.absolute().normalize(); + } catch(e:CsException) { + rethrow(e, path); + } + }, + callback + ); + } + + static function streamFile(path:String, flag:FileOpenFlag):FileStream { + var mode = FileMode.Create; + var access = FileAccess.ReadWrite; + switch flag { + case Append: mode = Append; access = Write; + case Read: mode = Open; access = Read; + case ReadWrite: mode = Open; + case Write: mode = Create; access = Write; + case WriteX: mode = CreateNew; access = Write; + case WriteRead: mode = Create; + case WriteReadX: mode = CreateNew; + case Overwrite: mode = OpenOrCreate; access = Write; + case OverwriteRead: mode = OpenOrCreate; + } + return new FileStream(path, mode, access, ReadWrite); + } + + @:allow(asys.native.filesystem) + static inline function rethrow(e:CsException, path:FilePath):T { + var error:IoErrorType = if(Std.isOfType(e, FileNotFoundException)) { + FileNotFound; + } else if(Std.isOfType(e, DirectoryNotFoundException)) { + FileNotFound; + } else { + CustomError(e.Message); + } + throw new FsException(error, path); + } + + static inline function closeStream(stream:Null):Void { + if(stream != null) + stream.Close(); + } +} \ No newline at end of file diff --git a/std/cs/_std/sys/thread/Thread.hx b/std/cs/_std/sys/thread/Thread.hx index ba26e7ba9ef..c78e46f2d7b 100644 --- a/std/cs/_std/sys/thread/Thread.hx +++ b/std/cs/_std/sys/thread/Thread.hx @@ -84,9 +84,10 @@ abstract Thread(ThreadImpl) from ThreadImpl { } function get_events():EventLoop { - if(this.events == null) - throw new NoEventLoopException(); - return this.events; + switch this.events { + case null: throw new NoEventLoopException(); + case events: return events; + } } @:keep diff --git a/std/eval/_std/asys/native/filesystem/Directory.hx b/std/eval/_std/asys/native/filesystem/Directory.hx new file mode 100644 index 00000000000..ed5c091e9f6 --- /dev/null +++ b/std/eval/_std/asys/native/filesystem/Directory.hx @@ -0,0 +1,36 @@ +package asys.native.filesystem; + +import haxe.NoData; +import eval.luv.Dir; +import sys.thread.Thread; + +@:coreApi +class Directory { + public final path:FilePath; + + final dir:Dir; + final maxBatchSize:Int; + + function new(dir:Dir, path:FilePath, maxBatchSize:Int) { + this.dir = dir; + this.path = path; + this.maxBatchSize = maxBatchSize; + } + + public function next(callback:Callback>):Void { + dir.read(Thread.current().events, maxBatchSize, null, r -> switch r { + case Error(e): + callback.fail(new FsException(e, path)); + case Ok(entries): + var result = [for(e in entries) @:privateAccess new FilePath(e.name)]; + callback.success(result); + }); + } + + public function close(callback:Callback):Void { + dir.close(Thread.current().events, null, r -> switch r { + case Error(e): callback.fail(new FsException(e, path)); + case Ok(_): callback.success(NoData); + }); + } +} \ No newline at end of file diff --git a/std/eval/_std/asys/native/filesystem/File.hx b/std/eval/_std/asys/native/filesystem/File.hx new file mode 100644 index 00000000000..6648f6d73b5 --- /dev/null +++ b/std/eval/_std/asys/native/filesystem/File.hx @@ -0,0 +1,143 @@ +package asys.native.filesystem; + +import haxe.Int64; +import haxe.io.Bytes; +import haxe.NoData; +import haxe.exceptions.NotImplementedException; +import sys.thread.Thread; +import asys.native.IWritable; +import asys.native.IReadable; +import asys.native.system.SystemUser; +import asys.native.system.SystemGroup; +import eval.luv.File as LFile; +import eval.luv.Loop; +import eval.luv.Buffer; +import eval.luv.Idle; + +@:coreApi +class File { + public final path:FilePath; + + final file:LFile; + final deleteOnClose:Bool; + + static inline function currentLoop():Loop { + return Thread.current().events; + } + + function new(file:LFile, path:FilePath, deleteOnClose:Bool = false):Void { + this.file = file; + this.path = path; + this.deleteOnClose = deleteOnClose; + } + + public function write(position:Int64, buffer:Bytes, offset:Int, length:Int, callback:Callback):Void { + var error = null; + if(length < 0) { + error = 'Negative length'; + } else if(position < 0) { + error = 'Negative position'; + } else if(offset < 0 || offset > buffer.length) { + error = 'Offset out of buffer bounds'; + } + if(error != null) { + failAsync(callback, CustomError(error), path); + } else { + var b = Buffer.create(offset + length > buffer.length ? buffer.length - offset : length); + b.blitFromBytes(buffer, offset); + file.write(currentLoop(), position, [b], null, r -> switch r { + case Error(e): callback.fail(new FsException(e, path)); + case Ok(bytesWritten): callback.success(bytesWritten.toInt()); + }); + } + } + + public function read(position:Int64, buffer:Bytes, offset:Int, length:Int, callback:Callback):Void { + var error = null; + if(length < 0) { + error = 'Negative length'; + } else if(position < 0) { + error = 'Negative position'; + } else if(offset < 0 || offset > buffer.length) { + error = 'Offset out of buffer bounds'; + } + if(error != null) { + failAsync(callback, CustomError(error), path); + } else { + var b = Buffer.create(offset + length > buffer.length ? buffer.length - offset : length); + file.read(currentLoop(), position, [b], null, r -> switch r { + case Error(e): + callback.fail(new FsException(e, path)); + case Ok(bytesRead): + b.sub(0, bytesRead.toInt()).blitToBytes(buffer, offset); + callback.success(bytesRead.toInt()); + }); + } + } + + public function flush(callback:Callback):Void { + file.fsync(currentLoop(), null, r -> switch r { + case Error(e): callback.fail(new FsException(e, path)); + case Ok(_): callback.success(NoData); + }); + } + + public function info(callback:Callback):Void { + file.fstat(currentLoop(), null, r -> switch r { + case Error(e): callback.fail(new FsException(e, path)); + case Ok(stat): callback.success(stat); + }); + } + + public function setPermissions(permissions:FilePermissions, callback:Callback):Void { + file.fchmod(currentLoop(), permissions, null, r -> switch r { + case Error(e): callback.fail(new FsException(e, path)); + case Ok(_): callback.success(NoData); + }); + } + + public function setOwner(user:SystemUser, group:SystemGroup, callback:Callback):Void { + file.fchown(currentLoop(), user, group, null, r -> switch r { + case Error(e): callback.fail(new FsException(e, path)); + case Ok(_): callback.success(NoData); + }); + } + + public function resize(newSize:Int, callback:Callback):Void { + file.ftruncate(currentLoop(), Int64.ofInt(newSize), null, r -> switch r { + case Error(e): callback.fail(new FsException(e, path)); + case Ok(_): callback.success(NoData); + }); + } + + public function setTimes(accessTime:Int, modificationTime:Int, callback:Callback):Void { + file.futime(currentLoop(), accessTime, modificationTime, null, r -> switch r { + case Error(e): callback.fail(new FsException(e, path)); + case Ok(real): callback.success(NoData); + }); + } + + // public function lock(mode:FileLock = Exclusive, wait:Bool = true, callback:Callback):Void { + // throw new NotImplementedException(); + // } + + public function close(callback:Callback):Void { + var loop = currentLoop(); + if(deleteOnClose) { + LFile.unlink(loop, path, null, _ -> {}); + } + file.close(loop, null, r -> switch r { + case Ok(_) | Error(UV_EBADF): callback.success(NoData); + case Error(e): callback.fail(new FsException(e, path)); + }); + } + + function failAsync(callback:Callback, error:IoErrorType, path:FilePath):Void { + var idle = Idle.init(currentLoop()).resolve(); + idle.start(() -> { + idle.stop(); + idle.close(() -> {}); + callback.fail(new FsException(error, path)); + }); + } +} \ No newline at end of file diff --git a/std/eval/_std/asys/native/filesystem/FileInfo.hx b/std/eval/_std/asys/native/filesystem/FileInfo.hx new file mode 100644 index 00000000000..152ea4ea14f --- /dev/null +++ b/std/eval/_std/asys/native/filesystem/FileInfo.hx @@ -0,0 +1,62 @@ +package asys.native.filesystem; + +import asys.native.system.SystemUser; +import asys.native.system.SystemGroup; +import eval.luv.File as LFile; + +private typedef NativeInfo = eval.luv.File.FileStat; + +@:coreApi +abstract FileInfo(NativeInfo) from NativeInfo to NativeInfo { + public var accessTime(get,never):Int; + inline function get_accessTime():Int + return this.atim.sec.toInt(); + + public var modificationTime(get,never):Int; + inline function get_modificationTime():Int + return this.mtim.sec.toInt(); + + public var creationTime(get,never):Int; + inline function get_creationTime():Int + return this.ctim.sec.toInt(); + + public var deviceNumber(get,never):Int; + inline function get_deviceNumber():Int + return this.dev.toInt(); + + public var group(get,never):SystemGroup; + inline function get_group():SystemGroup + return this.gid.toInt(); + + public var user(get,never):SystemUser; + inline function get_user():SystemUser + return this.uid.toInt(); + + public var inodeNumber(get,never):Int; + inline function get_inodeNumber():Int + return this.ino.toInt(); + + public var mode(get,never):FileMode; + inline function get_mode():FileMode + return this.mode; + + public var links(get,never):Int; + inline function get_links():Int + return this.nlink.toInt(); + + public var deviceType(get,never):Int; + inline function get_deviceType():Int + return this.rdev.toInt(); + + public var size(get,never):Int; + inline function get_size():Int + return this.size.toInt(); + + public var blockSize(get,never):Int; + inline function get_blockSize():Int + return this.blksize.toInt(); + + public var blocks(get,never):Int; + inline function get_blocks():Int + return this.blocks.toInt(); +} \ No newline at end of file diff --git a/std/eval/_std/asys/native/filesystem/FileMode.hx b/std/eval/_std/asys/native/filesystem/FileMode.hx new file mode 100644 index 00000000000..cd951e4268d --- /dev/null +++ b/std/eval/_std/asys/native/filesystem/FileMode.hx @@ -0,0 +1,35 @@ +package asys.native.filesystem; + +import eval.luv.File; +import haxe.exceptions.NotSupportedException; + +private typedef NativeMode = FileModeNumeric; + +@:coreApi +abstract FileMode(NativeMode) from NativeMode { + + public inline function has(permissions:FilePermissions):Bool { + return File.testMode(permissions, this); + } + + public inline function isBlockDevice():Bool + return File.testMode([IFBLK], this); + + public inline function isCharacterDevice():Bool + return File.testMode([IFCHR], this); + + public inline function isDirectory():Bool + return File.testMode([IFDIR], this); + + public inline function isFIFO():Bool + return File.testMode([IFIFO], this); + + public inline function isFile():Bool + return File.testMode([IFREG], this) && !File.testMode([IFLNK], this); + + public inline function isSocket():Bool + throw NotSupportedException.field(); + + public inline function isLink():Bool + return File.testMode([IFLNK], this); +} \ No newline at end of file diff --git a/std/eval/_std/asys/native/filesystem/FilePath.hx b/std/eval/_std/asys/native/filesystem/FilePath.hx new file mode 100644 index 00000000000..69cfd65c224 --- /dev/null +++ b/std/eval/_std/asys/native/filesystem/FilePath.hx @@ -0,0 +1,203 @@ +package asys.native.filesystem; + +import eval.NativeString; +import eval.luv.File.FileSync; +import haxe.exceptions.ArgumentException; + +private typedef NativeFilePath = NativeString; + +@:coreApi abstract FilePath(NativeFilePath) to NativeString { + public static var SEPARATOR(get, never):String; + + static inline function get_SEPARATOR():String { + return _SEPARATOR; + } + + static var _SEPARATOR:String; + + static function __init__():Void { + _SEPARATOR = Sys.systemName() == 'Windows' ? '\\' : '/'; + } + + overload extern static public inline function createPath(path:String, ...appendices:String):FilePath { + return createPathImpl(path, ...appendices); + } + + static function createPathImpl(path:String, ...appendices:String):FilePath { + var path = ofString(path); + for (p in appendices) + path = path.add(p); + return path; + } + + overload extern static public inline function createPath(parts:Array):FilePath { + return ofArray(parts); + } + + @:noUsing + @:from public static inline function ofString(path:String):FilePath { + return new FilePath(path); + } + + @:from static function ofArray(parts:Array):FilePath { + if (parts.length == 0) + throw new ArgumentException('parts'); + var path = ofString(parts[0]); + for (i in 1...parts.length) + path = path.add(parts[i]); + return path; + } + + inline function new(s:NativeString) { + this = s; + } + + @:to public inline function toString():String { + return this == null ? null : this.toString(); + } + + @:op(A == B) inline function equals(p:FilePath):Bool { + return this == (p : NativeString); + } + + public function isAbsolute():Bool { + return switch this.length { + case 0: false; + case _ if (isSeparator(this.code(0))): true; + case 1: false; + case length if (SEPARATOR == '\\'): this.code(1) == ':'.code && length >= 3 && isSeparator(this.code(2)); + case _: false; + } + } + + public function parent():Null { + var s = trimSlashes(this); + switch s.length { + case 0: + return null; + case 1 if (isSeparator(s.code(0))): + return null; + case 2 | 3 if (SEPARATOR == '\\' && s.code(1) == ':'.code): + return null; + case(_ - 1) => i: + while (!isSeparator(s.code(i))) { + --i; + if (i < 0) + return null; + } + return new FilePath(s.sub(0, i + 1)); + } + } + + public function name():FilePath { + var s = trimSlashes(this); + var i = s.length - 1; + while (!isSeparator(s.code(i))) { + --i; + if (i < 0) + return new FilePath(s); + } + return new FilePath(s.sub(i + 1)); + } + + // TODO: use `get_full_path` from path.ml + public function absolute():FilePath { + var result:NativeString = if (this.length == 0) { + trimSlashes(Sys.getCwd()); + } else if (this.code(0) == '/'.code) { + this; + } else if (SEPARATOR == '\\') { + if (this.code(0) == '\\'.code) { + this; + } else if (this.length >= 2 && isDriveLetter(this.code(0)) && this.code(1) == ':'.code) { + if (this.length > 2 && isSeparator(this.code(3))) { + this; + } else { + try { + var driveCwd = FileSync.realPath(this.sub(0, 2) + '.').resolve(); + driveCwd + SEPARATOR + this.sub(2); + } catch (_) { + throw new FsException(CustomError('Unable to get current working directory of drive ${this.sub(0,1)]}'), new FilePath(this)); + } + } + } else { + trimSlashes(Sys.getCwd()) + SEPARATOR + this; + } + } else { + trimSlashes(Sys.getCwd()) + SEPARATOR + this; + } + return new FilePath(result); + } + + public function normalize():FilePath { + var parts = if (SEPARATOR == '\\') { + StringTools.replace(this.toString(), '\\', '/').split('/'); + } else { + this.toString().split('/'); + } + var i = parts.length - 1; + var result = []; + var skip = 0; + while (i >= 0) { + switch parts[i] { + case '.' | '': + case '..': + ++skip; + case _ if (skip > 0): + --skip; + case part: + result.unshift(part); + } + --i; + } + for (i in 0...skip) + result.unshift('..'); + var result = ofString(result.join(SEPARATOR)); + return isAbsolute() && !result.isAbsolute() ? SEPARATOR + result : result; + } + + public function add(path:FilePath):FilePath { + if (path.isAbsolute() || this.length == 0) + return path; + var path = (path : NativeString); + if (path.length == 0) + return new FilePath(this); + if (SEPARATOR == '\\') { + if (path.length >= 2 && path.code(1) == ':'.code) { + if (this.length >= 2 && this.code(1) == ':'.code) { + if (path.char(0).toLowerCase() != this.char(0).toLowerCase()) { + throw new ArgumentException('path', 'Cannot combine paths on different drives'); + } + return new FilePath(trimSlashes(this) + SEPARATOR + path.sub(2)); + } else if (isSeparator(this.code(0))) { + return new FilePath(path.sub(0, 2) + trimSlashes(this) + SEPARATOR + path.sub(2)); + } + } + } + return new FilePath(trimSlashes(this) + SEPARATOR + path); + } + + static inline function isSeparator(c:Int):Bool { + return c == '/'.code || (SEPARATOR == '\\' && c == '\\'.code); + } + + static function trimSlashes(s:NativeString):NativeString { + var i = s.length - 1; + if (i <= 0) + return s; + var sep = isSeparator(s.code(i)); + if (sep) { + do { + --i; + sep = isSeparator(s.code(i)); + } while (i > 0 && sep); + return s.sub(0, i + 1); + } else { + return s; + } + } + + static inline function isDriveLetter(c:Int):Bool { + return ('a'.code <= c && c <= 'z'.code) || ('A'.code <= c && c <= 'Z'.code); + } +} diff --git a/std/eval/_std/asys/native/filesystem/FilePermissions.hx b/std/eval/_std/asys/native/filesystem/FilePermissions.hx new file mode 100644 index 00000000000..4a12090dde9 --- /dev/null +++ b/std/eval/_std/asys/native/filesystem/FilePermissions.hx @@ -0,0 +1,142 @@ +package asys.native.filesystem; + +import haxe.exceptions.ArgumentException; +import eval.luv.File; + +private typedef NativePermissions = Array; + +@:coreApi +abstract FilePermissions(NativePermissions) to NativePermissions { + static public inline function ignoresSpecialBit():Bool { + return false; + } + + static public function octal(s:Int, u:Int, g:Int, o:Int):FilePermissions { + var set = switch u { + case 0: []; + case 1: [IXUSR]; + case 2: [IWUSR]; + case 3: [IXUSR, IWUSR]; + case 4: [IRUSR]; + case 5: [IXUSR, IRUSR]; + case 6: [IWUSR, IRUSR]; + case 7: [IRWXU]; + case _: throw new ArgumentException('u'); + } + switch g { + case 0: + case 1: set.push(IXGRP); + case 2: set.push(IWGRP); + case 3: set.push(IXGRP); set.push(IWGRP); + case 4: set.push(IRGRP); + case 5: set.push(IXGRP); set.push(IRGRP); + case 6: set.push(IWGRP); set.push(IRGRP); + case 7: set.push(IRWXG); + case _: throw new ArgumentException('g'); + } + switch o { + case 0: + case 1: set.push(IXOTH); + case 2: set.push(IWOTH); + case 3: set.push(IXOTH); set.push(IWOTH); + case 4: set.push(IROTH); + case 5: set.push(IXOTH); set.push(IROTH); + case 6: set.push(IWOTH); set.push(IROTH); + case 7: set.push(IRWXO); + case _: throw new ArgumentException('g'); + } + switch s { + case 0: + case 1: set.push(ISVTX); + case 2: set.push(ISGID); + case 3: set.push(ISVTX); set.push(ISGID); + case 4: set.push(ISUID); + case 5: set.push(ISVTX); set.push(ISUID); + case 6: set.push(ISGID); set.push(ISUID); + case 7: set.push(ISVTX); set.push(ISGID); set.push(ISUID); + case _: throw new ArgumentException('s'); + } + return new FilePermissions(set); + } + + @:from static inline function fromOctal(mode:Array):FilePermissions { + if(mode.length != 4) { + throw new ArgumentException('mode', '"mode" array should contain exactly four items'); + } + return octal(mode[0], mode[1], mode[2], mode[3]); + } + + @:from static function fromDecimal(dec:Int):FilePermissions { + var set = []; + if(dec & (1 << 0) != 0) set.push(IXOTH); + if(dec & (1 << 1) != 0) set.push(IWOTH); + if(dec & (1 << 2) != 0) set.push(IROTH); + if(dec & (1 << 3) != 0) set.push(IXGRP); + if(dec & (1 << 4) != 0) set.push(IWGRP); + if(dec & (1 << 5) != 0) set.push(IRGRP); + if(dec & (1 << 6) != 0) set.push(IXUSR); + if(dec & (1 << 7) != 0) set.push(IWUSR); + if(dec & (1 << 8) != 0) set.push(IRUSR); + if(dec & (1 << 9) != 0) set.push(ISVTX); + if(dec & (1 << 10) != 0) set.push(ISGID); + if(dec & (1 << 11) != 0) set.push(ISUID); + return new FilePermissions(set); + } + + @:to function toDecimal():Int { + var result = 0; + for(v in this) { + switch v { + case IXOTH: result = result | 1; + case IWOTH: result = result | (1 << 1); + case IROTH: result = result | (1 << 2); + case IRWXO: result = result | 1 | (1 << 1) | (1 << 2); + case IXGRP: result = result | (1 << 3); + case IWGRP: result = result | (1 << 4); + case IRGRP: result = result | (1 << 5); + case IRWXG: result = result | (1 << 3) | (1 << 4) | (1 << 5); + case IXUSR: result = result | (1 << 6); + case IWUSR: result = result | (1 << 7); + case IRUSR: result = result | (1 << 8); + case IRWXU: result = result | (1 << 6) | (1 << 7) | (1 << 8); + case ISVTX: result = result | (1 << 9); + case ISGID: result = result | (1 << 10); + case ISUID: result = result | (1 << 11); + case _: + } + } + return result; + } + + @:op(A & B) static function intersect(perm1:FilePermissions, perm2:FilePermissions):FilePermissions { + return fromDecimal((perm1:Int) & (perm2:Int)); + } + + @:op(A | B) static function merge(perm1:FilePermissions, perm2:FilePermissions):FilePermissions { + return fromDecimal((perm1:Int) & (perm2:Int)); + } + + @:op(A == B) static function equals(perm1:Null, perm2:Null):Bool { + var p1:Array = perm1; + var p2:Array = perm2; + if(p1 == p2) { + return true; + } else if(p1 == null || p2 == null) { + return false; + } else { + return (perm1:Int) == (perm2:Int); + } + } + + @:op(A == B) @:commutative static inline function equalsDecimal(perm1:Null, dec:Int):Bool { + return equals(perm1, fromDecimal(dec)); + } + + inline function new(perm:NativePermissions) { + this = perm; + } + + public inline function toString():String { + return '${toDecimal()}'; + } +} \ No newline at end of file diff --git a/std/eval/_std/asys/native/filesystem/FileSystem.hx b/std/eval/_std/asys/native/filesystem/FileSystem.hx new file mode 100644 index 00000000000..cfde8dde8ff --- /dev/null +++ b/std/eval/_std/asys/native/filesystem/FileSystem.hx @@ -0,0 +1,388 @@ +package asys.native.filesystem; + +import haxe.io.Bytes; +import haxe.io.BytesBuffer; +import haxe.NoData; +import asys.native.system.SystemUser; +import asys.native.system.SystemGroup; +import sys.thread.Thread; +import eval.NativeString; +import eval.integers.Int64; +import eval.integers.UInt64; +import eval.luv.Loop; +import eval.luv.Buffer; +import eval.luv.File as LFile; +import eval.luv.Dir; +import eval.luv.File.FileOpenFlag as LFileOpenFlag; +import eval.luv.File.FileAccessFlag; +import eval.luv.LuvException; + +using eval.luv.Result; + +@:coreApi +class FileSystem { + static inline function currentLoop():Loop { + return Thread.current().events; + } + + static public function openFile(path:FilePath, flag:FileOpenFlag, callback:Callback):Void { + LFile.open(currentLoop(), path, evalOpenFlags(flag), null, null, r -> switch r { + case Error(e): callback.fail(new FsException(e, path)); + case Ok(f): callback.success(cast @:privateAccess new File(f, path)); + }); + } + + static public function tempFile(callback:Callback):Void { + var pattern = switch eval.luv.Path.tmpdir() { + case Error(_): NativeString.fromString('./XXXXXX'); + case Ok(dir): dir.concat('/XXXXXX'); + } + LFile.mkstemp(currentLoop(), pattern, null, r -> switch r { + case Error(e): callback.fail(new FsException(e, '(unknown path)')); + case Ok(f): callback.success(@:privateAccess new File(f.file, @:privateAccess new FilePath(f.name), true)); + }); + } + + static public function readBytes(path:FilePath, callback:Callback):Void { + readFile(path, callback); + } + + static public function readString(path:FilePath, callback:Callback):Void { + readFile(path, (e, r) -> { + if(e == null) + callback.success(r.toString()) + else + callback.fail(e); + }); + } + + static inline function readFile(path:FilePath, callback:Callback):Void { + var loop = currentLoop(); + LFile.open(loop, path, [RDONLY], r -> switch r { + case Error(e): + callback.fail(new FsException(e, path)); + case Ok(f): + f.fstat(loop, null, r -> switch r { + case Error(e): + f.close(loop, null, _ -> callback.fail(new FsException(e, path))); + case Ok(stat): + var buf = Buffer.create(stat.size.toInt()); + f.read(loop, Int64.ZERO, [buf], r -> switch r { + case Error(e): + f.close(loop, null, _ -> callback.fail(new FsException(e, path))); + case Ok(bytesRead): + f.close(loop, null, _ -> callback.success(buf.sub(0, bytesRead.toInt()).toBytes())); + }); + }); + }); + } + + static public function writeBytes(path:FilePath, data:Bytes, flag:FileOpenFlag = Write, callback:Callback):Void { + writeFile(path, data, flag, callback); + } + + static public function writeString(path:FilePath, text:String, flag:FileOpenFlag = Write, callback:Callback):Void { + writeFile(path, text, flag, callback); + } + + static inline function writeFile(path:FilePath, data:Buffer, flag:FileOpenFlag, callback:Callback):Void { + var loop = currentLoop(); + LFile.open(loop, path, evalOpenFlags(flag), r -> switch r { + case Error(e): + callback.fail(new FsException(e, path)); + case Ok(f): + f.write(loop, Int64.ZERO, [data], r -> switch r { + case Error(e): + f.close(loop, null, _ -> callback.fail(new FsException(e, path))); + case Ok(_): + f.close(loop, null, r -> switch r { + case Error(e): callback.fail(new FsException(e, path)); + case Ok(_): callback.success(NoData); + }); + }); + }); + } + + static public function openDirectory(path:FilePath, maxBatchSize:Int = 64, callback:Callback):Void { + Dir.open(currentLoop(), path, null, r -> switch r { + case Error(e): callback.fail(new FsException(e, path)); + case Ok(dir): callback.success(@:privateAccess new Directory(dir, path, maxBatchSize)); + }); + } + + static public function listDirectory(path:FilePath, callback:Callback>):Void { + var loop = currentLoop(); + Dir.open(loop, path, null, r -> switch r { + case Error(e): + callback.fail(new FsException(e, path)); + case Ok(dir): + var result = []; + function collect(r:Result>) { + switch r { + case Error(e): + dir.close(loop, null, _ -> callback.fail(new FsException(e, path))); + case Ok(entries): + if(entries.length == 0) { + dir.close(loop, null, _ -> callback.success(result)); + } else { + for(entry in entries) { + result.push(@:privateAccess new FilePath(entry.name)); + } + dir.read(loop, 32, null, collect); + } + } + } + dir.read(loop, 32, null, collect); + }); + } + + static public function createDirectory(path:FilePath, ?permissions:FilePermissions, recursive:Bool = false, callback:Callback):Void { + if(permissions == null) permissions = 511; + inline mkdir(path, permissions, recursive, r -> switch r { + case Error(e): callback.fail(new FsException(e, path)); + case Ok(_): callback.success(NoData); + }); + } + + static function mkdir(path:FilePath, permissions:FilePermissions, recursive:Bool, callback:(r:Result)->Void):Void { + var loop = currentLoop(); + function mk(path:FilePath, callback:(r:Result)->Void) { + LFile.mkdir(loop, path, permissions, null, r -> switch r { + case Error(UV_ENOENT) if(recursive): + switch path.parent() { + case null: + callback(r); + case parent: + mk(parent, r -> switch r { + case Error(_): + callback(r); + case Ok(_): + LFile.mkdir(loop, path, permissions, null, callback); + }); + } + case _: + callback(r); + }); + } + mk(path, callback); + } + + static public function uniqueDirectory(parentDirectory:FilePath, ?prefix:String, ?permissions:FilePermissions, recursive:Bool = false, callback:Callback):Void { + if(permissions == null) permissions = 511; + + var name = (prefix == null ? '' : prefix) + getRandomChar() + getRandomChar() + getRandomChar() + getRandomChar(); + var path = @:privateAccess new FilePath((parentDirectory:NativeString).concat(FilePath.SEPARATOR + name)); + + function create(callback:(r:Result)->Void) { + inline mkdir(path, permissions, recursive, r -> switch r { + case Error(UV_EEXIST): + var next = (path:NativeString).concat(getRandomChar()); + path = @:privateAccess new FilePath(next); + create(callback); + case _: + callback(r); + }); + } + create(r -> switch r { + case Error(e): callback.fail(new FsException(e, parentDirectory)); + case Ok(_): callback.success(path); + }); + } + + static var __codes:Null>; + static function getRandomChar():String { + //TODO: null safety issue if `switch` result is assigned directly to this var declaration + var codes:Array; + switch __codes { + case null: + var a = [for(c in '0'.code...'9'.code) String.fromCharCode(c)]; + for(c in 'A'.code...'Z'.code) a.push(String.fromCharCode(c)); + for(c in 'a'.code...'z'.code) a.push(String.fromCharCode(c)); + codes = __codes = a; + case a: + codes = a; + } + return codes[Std.random(codes.length)]; + } + + static public function move(oldPath:FilePath, newPath:FilePath, overwrite:Bool = true, callback:Callback):Void { + var loop = currentLoop(); + inline function move() { + LFile.rename(loop, oldPath, newPath, null, r -> switch r { + case Error(e): callback.fail(new FsException(e, oldPath)); + case Ok(_): callback.success(NoData); + }); + } + if(overwrite) { + move(); + } else { + LFile.access(loop, newPath, [F_OK], null, r -> switch r { + case Error(UV_ENOENT): move(); + case Error(e): callback.fail(new FsException(e, newPath)); + case Ok(_): callback.fail(new FsException(FileExists, newPath)); + }); + } + } + + static public function deleteFile(path:FilePath, callback:Callback):Void { + LFile.unlink(currentLoop(), path, null, r -> switch r { + case Error(e): callback.fail(new FsException(e, path)); + case Ok(_): callback.success(NoData); + }); + } + + static public function deleteDirectory(path:FilePath, callback:Callback):Void { + LFile.rmdir(currentLoop(), path, null, r -> switch r { + case Error(e): callback.fail(new FsException(e, path)); + case Ok(_): callback.success(NoData); + }); + } + + static public function info(path:FilePath, callback:Callback):Void { + LFile.stat(currentLoop(), path, null, r -> switch r { + case Error(e): callback.fail(new FsException(e, path)); + case Ok(stat): callback.success(stat); + }); + } + + static public function check(path:FilePath, mode:FileAccessMode, callback:Callback):Void { + var flags = []; + if(mode.has(Exists)) flags.push(F_OK); + if(mode.has(Executable)) flags.push(X_OK); + if(mode.has(Writable)) flags.push(W_OK); + if(mode.has(Readable)) flags.push(R_OK); + LFile.access(currentLoop(), path, flags, null, r -> switch r { + case Error(UV_ENOENT | UV_EACCES): callback.success(false); + case Error(e): callback.fail(new FsException(e, path)); + case Ok(_): callback.success(true); + }); + } + + static public function isDirectory(path:FilePath, callback:Callback):Void { + LFile.stat(currentLoop(), path, null, r -> switch r { + case Error(UV_ENOENT): callback.success(false); + case Error(e): callback.fail(new FsException(e, path)); + case Ok(stat): callback.success((stat:FileInfo).mode.isDirectory()); + }); + } + + static public function isFile(path:FilePath, callback:Callback):Void { + LFile.stat(currentLoop(), path, null, r -> switch r { + case Error(UV_ENOENT): callback.success(false); + case Error(e): callback.fail(new FsException(e, path)); + case Ok(stat): callback.success((stat:FileInfo).mode.isFile()); + }); + } + + static public function setPermissions(path:FilePath, permissions:FilePermissions, callback:Callback):Void { + LFile.chmod(currentLoop(), path, permissions, null, r -> switch r { + case Error(e): callback.fail(new FsException(e, path)); + case Ok(_): callback.success(NoData); + }); + } + + static public function setOwner(path:FilePath, user:SystemUser, group:SystemGroup, callback:Callback):Void { + LFile.chown(currentLoop(), path, user, group, null, r -> switch r { + case Error(e): callback.fail(new FsException(e, path)); + case Ok(_): callback.success(NoData); + }); + } + + static public function setLinkOwner(path:FilePath, user:SystemUser, group:SystemGroup, callback:Callback):Void { + LFile.lchown(currentLoop(), path, user, group, null, r -> switch r { + case Error(e): callback.fail(new FsException(e, path)); + case Ok(_): callback.success(NoData); + }); + } + + static public function link(target:FilePath, path:FilePath, type:FileLink = SymLink, callback:Callback):Void { + var cb:(r:Result)->Void = r -> switch r { + case Error(e): callback.fail(new FsException(e, path)); + case Ok(_): callback.success(NoData); + } + switch type { + case HardLink: + LFile.link(currentLoop(), target, path, null, cb); + case SymLink: + LFile.symlink(currentLoop(), target, path, null, null, cb); + } + } + + static public function isLink(path:FilePath, callback:Callback):Void { + LFile.lstat(currentLoop(), path, null, r -> switch r { + case Error(UV_ENOENT): callback.success(false); + case Error(e): callback.fail(new FsException(e, path)); + case Ok(stat): callback.success((stat:FileInfo).mode.isLink()); + }); + } + + static public function readLink(path:FilePath, callback:Callback):Void { + LFile.readLink(currentLoop(), path, null, r -> switch r { + case Error(e): callback.fail(new FsException(e, path)); + case Ok(real): callback.success(@:privateAccess new FilePath(real)); + }); + } + + static public function linkInfo(path:FilePath, callback:Callback):Void { + LFile.lstat(currentLoop(), path, null, r -> switch r { + case Error(e): callback.fail(new FsException(e, path)); + case Ok(stat): callback.success(stat); + }); + } + + static public function copyFile(source:FilePath, destination:FilePath, overwrite:Bool = true, callback:Callback):Void { + LFile.copyFile(currentLoop(), source, destination, (overwrite ? null : [COPYFILE_EXCL]), null, r -> switch r { + case Error(UV_EEXIST): callback.fail(new FsException(FileExists, destination)); + case Error(e): callback.fail(new FsException(e, source)); + case Ok(stat): callback.success(stat); + }); + } + + static public function resize(path:FilePath, newSize:Int, callback:Callback):Void { + var loop = currentLoop(); + LFile.open(loop, path, [CREAT, WRONLY], null, null, r -> switch r { + case Error(e): + callback.fail(new FsException(e, path)); + case Ok(file): + file.ftruncate(loop, Int64.ofInt(newSize), null, r -> switch r { + case Error(e): + file.close(loop, null, _ -> callback.fail(new FsException(e, path))); + case Ok(_): + file.close(loop, null, r -> switch r { + case Error(e): + callback.fail(new FsException(e, path)); + case Ok(_): + callback.success(NoData); + }); + }); + }); + } + + static public function setTimes(path:FilePath, accessTime:Int, modificationTime:Int, callback:Callback):Void { + LFile.utime(currentLoop(), path, accessTime, modificationTime, null, r -> switch r { + case Error(e): callback.fail(new FsException(e, path)); + case Ok(real): callback.success(NoData); + }); + } + + static public function realPath(path:FilePath, callback:Callback):Void { + LFile.realPath(currentLoop(), path, null, r -> switch r { + case Error(e): callback.fail(new FsException(e, path)); + case Ok(real): callback.success(@:privateAccess new FilePath(real)); + }); + } + + static function evalOpenFlags(flag:FileOpenFlag):Array { + return switch flag { + case Append: [WRONLY, APPEND, CREAT]; + case Read: [RDONLY]; + case ReadWrite: [RDWR]; + case Write: [WRONLY, CREAT, TRUNC]; + case WriteX: [WRONLY, CREAT, EXCL]; + case WriteRead: [RDWR, CREAT, TRUNC]; + case WriteReadX: [RDWR, CREAT, EXCL]; + case Overwrite: [WRONLY, CREAT]; + case OverwriteRead: [RDWR, CREAT]; + } + } +} \ No newline at end of file diff --git a/std/eval/_std/sys/thread/Thread.hx b/std/eval/_std/sys/thread/Thread.hx index eb6eb1d357b..d05b35dc22f 100644 --- a/std/eval/_std/sys/thread/Thread.hx +++ b/std/eval/_std/sys/thread/Thread.hx @@ -93,9 +93,10 @@ abstract Thread(ThreadImpl) from ThreadImpl { } function get_events():EventLoop { - if(this.events == null) - throw new NoEventLoopException(); - return this.events; + switch this.events { + case null: throw new NoEventLoopException(); + case events: return events; + } } @:keep diff --git a/std/eval/luv/Async.hx b/std/eval/luv/Async.hx index 441a0851fe0..210146358ba 100644 --- a/std/eval/luv/Async.hx +++ b/std/eval/luv/Async.hx @@ -17,5 +17,5 @@ package eval.luv; /** Triggers a call to the handle's callback by the handle's loop. **/ - public function send():Result; + public function send():Result; } \ No newline at end of file diff --git a/std/eval/luv/Check.hx b/std/eval/luv/Check.hx index b3bd647c4e4..b8b58b1d725 100644 --- a/std/eval/luv/Check.hx +++ b/std/eval/luv/Check.hx @@ -17,10 +17,10 @@ package eval.luv; /** Starts the handle with the given callback. **/ - public function start(callback:()->Void):Result; + public function start(callback:()->Void):Result; /** Stops the handle. **/ - public function stop():Result; + public function stop():Result; } \ No newline at end of file diff --git a/std/eval/luv/ConnectedUdp.hx b/std/eval/luv/ConnectedUdp.hx index 5df83a09329..df73f1d2888 100644 --- a/std/eval/luv/ConnectedUdp.hx +++ b/std/eval/luv/ConnectedUdp.hx @@ -13,7 +13,7 @@ abstract ConnectedUdp(Udp) to Udp to Handle { /** Removes the peer address assigned to the given socket. **/ - extern public function disconnect():Result; + extern public function disconnect():Result; /** Retrieves the peer address assigned to the given socket. @@ -24,11 +24,11 @@ abstract ConnectedUdp(Udp) to Udp to Handle { Like `eval.luv.UDP.send`, but the remote address used is the peer address assigned to the socket. **/ - extern public function send(data:Array, callback:(result:Result)->Void):Void; + extern public function send(data:Array, callback:(result:Result)->Void):Void; /** Like `eval.luv.UDP.trySend`, but the remote address used is the peer address assigned to the socket. **/ - extern public function trySend(data:Array):Result; + extern public function trySend(data:Array):Result; } \ No newline at end of file diff --git a/std/eval/luv/Dir.hx b/std/eval/luv/Dir.hx index 4fd3f65c6da..471bde972d2 100644 --- a/std/eval/luv/Dir.hx +++ b/std/eval/luv/Dir.hx @@ -37,22 +37,22 @@ typedef DirectoryScan = { /** Opens the directory at the given path for listing. **/ - static public function open(loop:Loop, path:NativeString, ?request:FileRequest, callback:(result:Result)->Void):Void; + static public function open(loop:Loop, path:NativeString, ?request:FileRequest, callback:(result:Result) -> Void):Void; /** Closes the directory. **/ - public function close(loop:Loop, ?request:FileRequest, callback:(result:Result)->Void):Void; + public function close(loop:Loop, ?request:FileRequest, callback:(result:Result) -> Void):Void; /** Retrieves a directory entry. **/ - public function read(loop:Loop, ?numberOfEntries:Int, ?request:FileRequest, callback:(result:Result>)->Void):Void; + public function read(loop:Loop, ?numberOfEntries:Int, ?request:FileRequest, callback:(result:Result>) -> Void):Void; /** Begins directory listing. **/ - static public function scan(loop:Loop, path:NativeString, ?request:FileRequest, callback:(result:Result)->Void):Void; + static public function scan(loop:Loop, path:NativeString, ?request:FileRequest, callback:(result:Result) -> Void):Void; } /** @@ -63,11 +63,11 @@ extern class DirSync { static public function open(path:NativeString):Result; @:inheritDoc(eval.luv.Dir.close) - static public function close(dir:Dir):Result; + static public function close(dir:Dir):Result; @:inheritDoc(eval.luv.Dir.read) static public function read(dir:Dir, ?numberOfEntries:Int):Result>; @:inheritDoc(eval.luv.Dir.scan) static public function scan(path:NativeString):Result; -} \ No newline at end of file +} diff --git a/std/eval/luv/Env.hx b/std/eval/luv/Env.hx index fae97f72e3b..915ab650b99 100644 --- a/std/eval/luv/Env.hx +++ b/std/eval/luv/Env.hx @@ -14,7 +14,7 @@ extern class Env { /** Sets an environment variable. **/ - static function setEnv(name:String, value:NativeString):Result; + static function setEnv(name:String, value:NativeString):Result; /** Deletes an environment variable. diff --git a/std/eval/luv/File.hx b/std/eval/luv/File.hx index d6233432050..a92c93e65c9 100644 --- a/std/eval/luv/File.hx +++ b/std/eval/luv/File.hx @@ -151,7 +151,7 @@ enum abstract FileSymlinkFlag(Int) { /** Closes the file. **/ - public function close(loop:Loop, ?request:FileRequest, callback:(result:Result)->Void):Void; + public function close(loop:Loop, ?request:FileRequest, callback:(result:Result)->Void):Void; /** Reads from the file. @@ -173,12 +173,12 @@ enum abstract FileSymlinkFlag(Int) { /** Deletes the file at the given path. **/ - static public function unlink(loop:Loop, path:NativeString, ?request:FileRequest, callback:(result:Result)->Void):Void; + static public function unlink(loop:Loop, path:NativeString, ?request:FileRequest, callback:(result:Result)->Void):Void; /** Moves the file at the given path to the path given by `toPath` **/ - static public function rename(loop:Loop, path:NativeString, toPath:NativeString, ?request:FileRequest, callback:(result:Result)->Void):Void; + static public function rename(loop:Loop, path:NativeString, toPath:NativeString, ?request:FileRequest, callback:(result:Result)->Void):Void; /** Creates a temporary file with name based on the given pattern. @@ -193,12 +193,12 @@ enum abstract FileSymlinkFlag(Int) { /** Creates a directory. **/ - static public function mkdir(loop:Loop, path:NativeString, ?mode:Array, ?request:FileRequest, callback:(result:Result)->Void):Void; + static public function mkdir(loop:Loop, path:NativeString, ?mode:Array, ?request:FileRequest, callback:(result:Result)->Void):Void; /** Deletes a directory. **/ - static public function rmdir(loop:Loop, path:NativeString, ?request:FileRequest, callback:(result:Result)->Void):Void; + static public function rmdir(loop:Loop, path:NativeString, ?request:FileRequest, callback:(result:Result)->Void):Void; /** Retrieves status information for the file at the given path. @@ -223,22 +223,22 @@ enum abstract FileSymlinkFlag(Int) { /** Flushes file changes to storage. **/ - public function fsync(loop:Loop, ?request:FileRequest, callback:(result:Result)->Void):Void; + public function fsync(loop:Loop, ?request:FileRequest, callback:(result:Result)->Void):Void; /** Like `eval.luv.File.fsync`, but may omit some metadata. **/ - public function fdataSync(loop:Loop, ?request:FileRequest, callback:(result:Result)->Void):Void; + public function fdataSync(loop:Loop, ?request:FileRequest, callback:(result:Result)->Void):Void; /** Truncates the given file to the given length. **/ - public function ftruncate(loop:Loop, length:Int64, ?request:FileRequest, callback:(result:Result)->Void):Void; + public function ftruncate(loop:Loop, length:Int64, ?request:FileRequest, callback:(result:Result)->Void):Void; /** Copies the file at the given path to the path given by `toPath`. **/ - static public function copyFile(loop:Loop, path:NativeString, toPath:NativeString, ?flags:Array, ?request:FileRequest, callback:(result:Result)->Void):Void; + static public function copyFile(loop:Loop, path:NativeString, toPath:NativeString, ?flags:Array, ?request:FileRequest, callback:(result:Result)->Void):Void; /** Transfers data between file descriptors. @@ -248,42 +248,42 @@ enum abstract FileSymlinkFlag(Int) { /** Checks whether the calling process can access the file at the given path. **/ - static public function access(loop:Loop, path:NativeString, flags:Array, ?request:FileRequest, callback:(result:Result)->Void):Void; + static public function access(loop:Loop, path:NativeString, flags:Array, ?request:FileRequest, callback:(result:Result)->Void):Void; /** Changes permissions of the file at the given path. **/ - static public function chmod(loop:Loop, path:NativeString, mode:Array, ?request:FileRequest, callback:(result:Result)->Void):Void; + static public function chmod(loop:Loop, path:NativeString, mode:Array, ?request:FileRequest, callback:(result:Result)->Void):Void; /** Changes permissions of the file. **/ - public function fchmod(loop:Loop, mode:Array, ?request:FileRequest, callback:(result:Result)->Void):Void; + public function fchmod(loop:Loop, mode:Array, ?request:FileRequest, callback:(result:Result)->Void):Void; /** Sets timestamps of the file at the given path. **/ - static public function utime(loop:Loop, path:NativeString, atime:Float, mtime:Float, ?request:FileRequest, callback:(result:Result)->Void):Void; + static public function utime(loop:Loop, path:NativeString, atime:Float, mtime:Float, ?request:FileRequest, callback:(result:Result)->Void):Void; /** Sets timestamps of the file. **/ - public function futime(loop:Loop, atime:Float, mtime:Float, ?request:FileRequest, callback:(result:Result)->Void):Void; + public function futime(loop:Loop, atime:Float, mtime:Float, ?request:FileRequest, callback:(result:Result)->Void):Void; /** Sets timestamps of the file at the given path without dereferencing symlinks. **/ - static public function lutime(loop:Loop, path:NativeString, atime:Float, mtime:Float, ?request:FileRequest, callback:(result:Result)->Void):Void; + static public function lutime(loop:Loop, path:NativeString, atime:Float, mtime:Float, ?request:FileRequest, callback:(result:Result)->Void):Void; /** Hardlinks a file at the location given by `link`. **/ - static public function link(loop:Loop, path:NativeString, link:NativeString, ?request:FileRequest, callback:(result:Result)->Void):Void; + static public function link(loop:Loop, path:NativeString, link:NativeString, ?request:FileRequest, callback:(result:Result)->Void):Void; /** Symlinks a file at the location given by `link`. **/ - static public function symlink(loop:Loop, path:NativeString, link:NativeString, ?flags:Array, ?request:FileRequest, callback:(result:Result)->Void):Void; + static public function symlink(loop:Loop, path:NativeString, link:NativeString, ?flags:Array, ?request:FileRequest, callback:(result:Result)->Void):Void; /** Reads the target path of a symlink. @@ -298,17 +298,17 @@ enum abstract FileSymlinkFlag(Int) { /** Changes owneship of the file at the given path. **/ - static public function chown(loop:Loop, path:NativeString, uid:Int, gid:Int, ?request:FileRequest, callback:(result:Result)->Void):Void; + static public function chown(loop:Loop, path:NativeString, uid:Int, gid:Int, ?request:FileRequest, callback:(result:Result)->Void):Void; /** Changes owneship of the file at the given path. without dereferencing symlinks. **/ - static public function lchown(loop:Loop, path:NativeString, uid:Int, gid:Int, ?request:FileRequest, callback:(result:Result)->Void):Void; + static public function lchown(loop:Loop, path:NativeString, uid:Int, gid:Int, ?request:FileRequest, callback:(result:Result)->Void):Void; /** Changes owneship of the file. **/ - public function fchown(loop:Loop, uid:Int, gid:Int, ?request:FileRequest, callback:(result:Result)->Void):Void; + public function fchown(loop:Loop, uid:Int, gid:Int, ?request:FileRequest, callback:(result:Result)->Void):Void; /** Returns the integer representation of `eval.luv.File`. @@ -329,7 +329,7 @@ extern class FileSync { static function open(path:NativeString, flags:Array, ?mode:Array):Result; @:inheritDoc(eval.luv.File.close) - static function close(file:File):Result; + static function close(file:File):Result; @:inheritDoc(eval.luv.File.read) static function read(file:File, fileOffset:Int64, buffers:Array):Result; @@ -338,10 +338,10 @@ extern class FileSync { static function write(file:File, fileOffset:Int64, buffers:Array):Result; @:inheritDoc(eval.luv.File.unlink) - static function unlink(path:NativeString):Result; + static function unlink(path:NativeString):Result; @:inheritDoc(eval.luv.File.rename) - static function rename(path:NativeString, toPath:NativeString):Result; + static function rename(path:NativeString, toPath:NativeString):Result; @:inheritDoc(eval.luv.File.mkstemp) static function mkstemp(pattern:NativeString):Result<{name:NativeString,file:File}>; @@ -350,10 +350,10 @@ extern class FileSync { static function mkdtemp(pattern:NativeString):Result; @:inheritDoc(eval.luv.File.mkdir) - static function mkdir(path:NativeString, ?mode:Array):Result; + static function mkdir(path:NativeString, ?mode:Array):Result; @:inheritDoc(eval.luv.File.rmdir) - static function rmdir(path:NativeString):Result; + static function rmdir(path:NativeString):Result; @:inheritDoc(eval.luv.File.stat) static function stat(path:NativeString):Result; @@ -368,43 +368,43 @@ extern class FileSync { static function statFs(path:NativeString):Result; @:inheritDoc(eval.luv.File.fsync) - static function fsync(file:File):Result; + static function fsync(file:File):Result; @:inheritDoc(eval.luv.File.fdataSync) - static function fdataSync(file:File):Result; + static function fdataSync(file:File):Result; @:inheritDoc(eval.luv.File.ftruncate) - static function ftruncate(file:File, length:Int64):Result; + static function ftruncate(file:File, length:Int64):Result; @:inheritDoc(eval.luv.File.copyFile) - static function copyFile(path:NativeString, toPath:NativeString, ?flags:Array):Result; + static function copyFile(path:NativeString, toPath:NativeString, ?flags:Array):Result; @:inheritDoc(eval.luv.File.sendFile) static function sendFile(file:File, toFile:File, offset:Int64, length:UInt64):Result; @:inheritDoc(eval.luv.File.access) - static function access(path:NativeString, flags:Array):Result; + static function access(path:NativeString, flags:Array):Result; @:inheritDoc(eval.luv.File.chmod) - static function chmod(path:NativeString, mode:Array):Result; + static function chmod(path:NativeString, mode:Array):Result; @:inheritDoc(eval.luv.File.fchmod) - static function fchmod(file:File, mode:Array):Result; + static function fchmod(file:File, mode:Array):Result; @:inheritDoc(eval.luv.File.utime) - static function utime(path:NativeString, atime:Float, mtime:Float):Result; + static function utime(path:NativeString, atime:Float, mtime:Float):Result; @:inheritDoc(eval.luv.File.futime) - static function futime(file:File, atime:Float, mtime:Float):Result; + static function futime(file:File, atime:Float, mtime:Float):Result; @:inheritDoc(eval.luv.File.lutime) - static function lutime(path:NativeString, atime:Float, mtime:Float):Result; + static function lutime(path:NativeString, atime:Float, mtime:Float):Result; @:inheritDoc(eval.luv.File.link) - static function link(path:NativeString, link:NativeString):Result; + static function link(path:NativeString, link:NativeString):Result; @:inheritDoc(eval.luv.File.symlink) - static function symlink(path:NativeString, link:NativeString, ?flags:Array):Result; + static function symlink(path:NativeString, link:NativeString, ?flags:Array):Result; @:inheritDoc(eval.luv.File.readLink) static function readLink(path:NativeString):Result; @@ -413,12 +413,12 @@ extern class FileSync { static function realPath(path:NativeString):Result; @:inheritDoc(eval.luv.File.chown) - static function chown(path:NativeString, uid:Int, gid:Int):Result; + static function chown(path:NativeString, uid:Int, gid:Int):Result; @:inheritDoc(eval.luv.File.lchown) - static function lchown(path:NativeString, uid:Int, gid:Int):Result; + static function lchown(path:NativeString, uid:Int, gid:Int):Result; @:inheritDoc(eval.luv.File.fchown) - static function fchown(file:File, uid:Int, gid:Int):Result; + static function fchown(file:File, uid:Int, gid:Int):Result; } \ No newline at end of file diff --git a/std/eval/luv/FsEvent.hx b/std/eval/luv/FsEvent.hx index 5571138197f..069a49a0d6f 100644 --- a/std/eval/luv/FsEvent.hx +++ b/std/eval/luv/FsEvent.hx @@ -31,6 +31,6 @@ enum abstract FsEventFlag(Int) { /** Stops the handle. **/ - public function stop():Result; + public function stop():Result; } \ No newline at end of file diff --git a/std/eval/luv/FsPoll.hx b/std/eval/luv/FsPoll.hx index 3587d84662b..89bff936f77 100644 --- a/std/eval/luv/FsPoll.hx +++ b/std/eval/luv/FsPoll.hx @@ -26,5 +26,5 @@ import eval.luv.File; /** Stops the handle. **/ - public function stop():Result; + public function stop():Result; } \ No newline at end of file diff --git a/std/eval/luv/Handle.hx b/std/eval/luv/Handle.hx index 53524baa025..8205896763c 100644 --- a/std/eval/luv/Handle.hx +++ b/std/eval/luv/Handle.hx @@ -59,7 +59,7 @@ package eval.luv; @see https://aantron.github.io/luv/luv/Luv/Handle/#val-set_send_buffer_size **/ - static public function setSendBufferSize(handle:SocketHandle, size:Int):Result; + static public function setSendBufferSize(handle:SocketHandle, size:Int):Result; /** Gets the size of the OS receive buffer for a socket. @@ -73,7 +73,7 @@ package eval.luv; @see https://aantron.github.io/luv/luv/Luv/Handle/#val-set_recv_buffer_size **/ - static public function setRecvBufferSize(handle:SocketHandle, size:Int):Result; + static public function setRecvBufferSize(handle:SocketHandle, size:Int):Result; // TODO // /** diff --git a/std/eval/luv/Idle.hx b/std/eval/luv/Idle.hx index 388fd664cf8..c5ba3b05d71 100644 --- a/std/eval/luv/Idle.hx +++ b/std/eval/luv/Idle.hx @@ -17,10 +17,10 @@ package eval.luv; /** Starts the handle with the given callback. **/ - public function start(callback:()->Void):Result; + public function start(callback:()->Void):Result; /** Stops the handle. **/ - public function stop():Result; + public function stop():Result; } \ No newline at end of file diff --git a/std/eval/luv/Loop.hx b/std/eval/luv/Loop.hx index 2122211c7fe..2d142491931 100644 --- a/std/eval/luv/Loop.hx +++ b/std/eval/luv/Loop.hx @@ -17,7 +17,7 @@ enum abstract LoopOption(Int) { extern static public final sigprof:Int; var LOOP_BLOCK_SIGNAL:LoopOption = 0; - var METRICS_IDLE_TIME:LoopOption = 1; + var METRICS_IDLE_TIME:LoopOption = 1; } /** @@ -64,7 +64,7 @@ enum abstract LoopOption(Int) { /** Releases resources associated with an event loop. **/ - public function close():Result; + public function close():Result; /** Indicates whether the loop is monitoring any activity. @@ -89,5 +89,5 @@ enum abstract LoopOption(Int) { /** Sets the loop option. **/ - public function configure(option:LoopOption, value:T):Result; + public function configure(option:LoopOption, value:T):Result; } \ No newline at end of file diff --git a/std/eval/luv/Mutex.hx b/std/eval/luv/Mutex.hx index 35f06bb3e07..0d60a965608 100644 --- a/std/eval/luv/Mutex.hx +++ b/std/eval/luv/Mutex.hx @@ -26,7 +26,7 @@ package eval.luv; /** Tries to take the mutex without blocking. **/ - public function tryLock():Result; + public function tryLock():Result; /** Releases the mutex. diff --git a/std/eval/luv/Path.hx b/std/eval/luv/Path.hx index 758c94b4cac..938d4927eb1 100644 --- a/std/eval/luv/Path.hx +++ b/std/eval/luv/Path.hx @@ -23,7 +23,7 @@ extern class Path { /** Changes the current working directory. **/ - static function chdir(dir:NativeString):Result; + static function chdir(dir:NativeString):Result; /** Evaluates to the path of the home directory. diff --git a/std/eval/luv/Pipe.hx b/std/eval/luv/Pipe.hx index bc04e936f54..0dd6f63b073 100644 --- a/std/eval/luv/Pipe.hx +++ b/std/eval/luv/Pipe.hx @@ -8,8 +8,8 @@ enum abstract PipeMode(Int) { enum ReceiveHandle { NONE; - TCP(associate:(tcp:Tcp)->Result); - PIPE(associate:(pipe:Pipe)->Result); + TCP(associate:(tcp:Tcp)->Result); + PIPE(associate:(pipe:Pipe)->Result); } /** @@ -32,12 +32,12 @@ enum ReceiveHandle { /** Assigns a pipe a name or an address. **/ - public function bind(nameOrAddress:NativeString):Result; + public function bind(nameOrAddress:NativeString):Result; /** Connects to the pipe at the given name or address. **/ - public function connect(target:NativeString, callback:(result:Result)->Void):Void; + public function connect(target:NativeString, callback:(result:Result)->Void):Void; /** Retrieves the name or address assigned to the pipe. @@ -73,5 +73,5 @@ enum ReceiveHandle { /** Sets pipe permissions. **/ - public function chmod(mode:PipeMode):Result; + public function chmod(mode:PipeMode):Result; } \ No newline at end of file diff --git a/std/eval/luv/Prepare.hx b/std/eval/luv/Prepare.hx index c74de4310c7..67ba32f66d4 100644 --- a/std/eval/luv/Prepare.hx +++ b/std/eval/luv/Prepare.hx @@ -17,10 +17,10 @@ package eval.luv; /** Starts the handle with the given callback. **/ - public function start(callback:()->Void):Result; + public function start(callback:()->Void):Result; /** Stops the handle. **/ - public function stop():Result; + public function stop():Result; } \ No newline at end of file diff --git a/std/eval/luv/Process.hx b/std/eval/luv/Process.hx index 9bbf4b9e600..3603939f406 100644 --- a/std/eval/luv/Process.hx +++ b/std/eval/luv/Process.hx @@ -75,12 +75,12 @@ typedef ProcessOptions = { /** Sends the given signal to the process with the given pid. **/ - static public function killPid(pid:Int, sigNum:Signal.SigNum):Result; + static public function killPid(pid:Int, sigNum:Signal.SigNum):Result; /** Sends the given signal to the process. **/ - public function kill(sigNum:Signal.SigNum):Result; + public function kill(sigNum:Signal.SigNum):Result; /** Evaluates to the pid of the process. diff --git a/std/eval/luv/Random.hx b/std/eval/luv/Random.hx index de159362150..0c776734818 100644 --- a/std/eval/luv/Random.hx +++ b/std/eval/luv/Random.hx @@ -15,12 +15,12 @@ extern class Random { /** Fills the given buffer with bits from the system entropy source. **/ - static function random(loop:Loop, buffer:Buffer, ?request:RandomRequest, callback:(result:Result)->Void):Void; + static function random(loop:Loop, buffer:Buffer, ?request:RandomRequest, callback:(result:Result)->Void):Void; } extern class RandomSync { /** Fills the given buffer with bits from the system entropy source. **/ - static function random(buffer:Buffer):Result; + static function random(buffer:Buffer):Result; } \ No newline at end of file diff --git a/std/eval/luv/Request.hx b/std/eval/luv/Request.hx index ef3c1466973..ee5bd35fc3c 100644 --- a/std/eval/luv/Request.hx +++ b/std/eval/luv/Request.hx @@ -9,5 +9,5 @@ package eval.luv; /** Tries to cancel a pending request. **/ - public function cancel():Result; + public function cancel():Result; } \ No newline at end of file diff --git a/std/eval/luv/Resource.hx b/std/eval/luv/Resource.hx index 5457e74f6ea..0c33a6c8d24 100644 --- a/std/eval/luv/Resource.hx +++ b/std/eval/luv/Resource.hx @@ -63,7 +63,7 @@ extern class Resource { /** Sets the priority of the process with the given pid. **/ - static function setPriority(pid:Int, priority:Int):Result; + static function setPriority(pid:Int, priority:Int):Result; /** Evaluates to the resident set size for the current process. diff --git a/std/eval/luv/Result.hx b/std/eval/luv/Result.hx index e48d34eed5d..389a56eb079 100644 --- a/std/eval/luv/Result.hx +++ b/std/eval/luv/Result.hx @@ -11,10 +11,6 @@ enum Result { Error(e:UVError); } -enum abstract NoData(Dynamic) { - var NoData = null; -} - class ResultTools { /** Returns the result value on success or throws `eval.luv.LuvException` diff --git a/std/eval/luv/RwLock.hx b/std/eval/luv/RwLock.hx index 2fd943ed259..edc2232a058 100644 --- a/std/eval/luv/RwLock.hx +++ b/std/eval/luv/RwLock.hx @@ -24,7 +24,7 @@ package eval.luv; /** Tries to take a read-write lock for reading without blocking. **/ - public function rdTryLock():Result; + public function rdTryLock():Result; /** Releases a read-write lock after it was taken for reading. @@ -39,7 +39,7 @@ package eval.luv; /** Tries to take a read-write lock for writing without blocking. **/ - public function wrTryLock():Result; + public function wrTryLock():Result; /** Releases a read-write lock after it was taken for writing. diff --git a/std/eval/luv/Semaphore.hx b/std/eval/luv/Semaphore.hx index bcbc4a36f0e..49c9728475e 100644 --- a/std/eval/luv/Semaphore.hx +++ b/std/eval/luv/Semaphore.hx @@ -29,5 +29,5 @@ package eval.luv; /** Tries to decrement a semaphore without blocking. **/ - public function tryWait():Result; + public function tryWait():Result; } \ No newline at end of file diff --git a/std/eval/luv/Signal.hx b/std/eval/luv/Signal.hx index 5699c854ac3..d9fdf091296 100644 --- a/std/eval/luv/Signal.hx +++ b/std/eval/luv/Signal.hx @@ -38,17 +38,17 @@ extern enum abstract SigNum(Int) from Int to Int { /** Starts the signal handle. **/ - public function start(sigNum:SigNum, callback:()->Void):Result; + public function start(sigNum:SigNum, callback:()->Void):Result; /** Like `eval.luv.Signal.start`, but the handle is stopped after one callback call. **/ - public function startOneshot(sigNum:SigNum, callback:()->Void):Result; + public function startOneshot(sigNum:SigNum, callback:()->Void):Result; /** Stops the signal handle. **/ - public function stop():Result; + public function stop():Result; /** Evaluates to the signal number associated with the handle. diff --git a/std/eval/luv/Stream.hx b/std/eval/luv/Stream.hx index 4768735c41f..7e9da9158d1 100644 --- a/std/eval/luv/Stream.hx +++ b/std/eval/luv/Stream.hx @@ -17,7 +17,7 @@ enum SendHandle { /** Shuts down the write side of the stream. **/ - extern static public function shutdown(stream:Stream, callback:(result:Result)->Void):Void; + extern static public function shutdown(stream:Stream, callback:(result:Result)->Void):Void; /** Starts listening for incoming connections. @@ -25,7 +25,7 @@ enum SendHandle { `backlog` indicates the number of connections the kernel might queue. When a new incoming connection is received the `callback` is called. **/ - extern static public function listen(stream:Stream, callback:(result:Result)->Void, ?backlog:Int):Void; + extern static public function listen(stream:Stream, callback:(result:Result)->Void, ?backlog:Int):Void; /** This call is used in conjunction with `Stream.listen()` to accept incoming @@ -38,7 +38,7 @@ enum SendHandle { `client` should be a freshly-initialized stream. **/ - extern static public function accept(server:TStream, client:TStream):Result; + extern static public function accept(server:TStream, client:TStream):Result; /** Calls the `callback` whenever data is available on the stream. @@ -68,7 +68,7 @@ enum SendHandle { /** Stops reading. **/ - extern static public function readStop(stream:Stream):Result; + extern static public function readStop(stream:Stream):Result; /** Writes the given buffer to the stream. @@ -78,13 +78,13 @@ enum SendHandle { that writes can be partial at the libuv API level, so it is possible to receive both an `UVError` result, and for some data to have been successfully written. **/ - extern static public function write(stream:Stream, data:Array, callback:(result:Result, bytesWritten:Int)->Void):Result; + extern static public function write(stream:Stream, data:Array, callback:(result:Result, bytesWritten:Int)->Void):Result; /** Like `eval.luv.Stream.write`, but allows sending a TCP socket or pipe over the stream. **/ - extern static public function write2(stream:TStream, data:Array, sendHandle:SendHandle, callback:(result:Result, bytesWritten:Int)->Void):Result; + extern static public function write2(stream:TStream, data:Array, sendHandle:SendHandle, callback:(result:Result, bytesWritten:Int)->Void):Result; /** Same as `eval.luv.Stream.write()`, but won’t queue a write request if it can’t @@ -107,5 +107,5 @@ enum SendHandle { /** Sets the blocking mode of the stream. **/ - extern static public function setBlocking(stream:Stream, block:Bool):Result; + extern static public function setBlocking(stream:Stream, block:Bool):Result; } \ No newline at end of file diff --git a/std/eval/luv/Tcp.hx b/std/eval/luv/Tcp.hx index abfab2d3e3d..d7d8df00860 100644 --- a/std/eval/luv/Tcp.hx +++ b/std/eval/luv/Tcp.hx @@ -28,17 +28,17 @@ import eval.luv.SockAddr; /** Sets the TCP keepalive. **/ - public function keepAlive(value:Option):Result; + public function keepAlive(value:Option):Result; /** Sets simultaneous accept. **/ - public function simultaneousAccepts(value:Bool):Result; + public function simultaneousAccepts(value:Bool):Result; /** Assigns an address to the TCP socket. **/ - public function bind(addr:SockAddr, ipv6Only:Bool = false):Result; + public function bind(addr:SockAddr, ipv6Only:Bool = false):Result; /** Retrieves the address assigned to the TCP socket. @@ -53,10 +53,10 @@ import eval.luv.SockAddr; /** Connects to a host. **/ - public function connect(addr:SockAddr, callback:(result:Result)->Void):Void; + public function connect(addr:SockAddr, callback:(result:Result)->Void):Void; /** Resets the connection. **/ - public function closeReset(callback:(result:Result)->Void):Void; + public function closeReset(callback:(result:Result)->Void):Void; } \ No newline at end of file diff --git a/std/eval/luv/Thread.hx b/std/eval/luv/Thread.hx index 6dd406ac29c..fd812897119 100644 --- a/std/eval/luv/Thread.hx +++ b/std/eval/luv/Thread.hx @@ -29,6 +29,6 @@ package eval.luv; /** Waits for the thread to terminate. **/ - public function join():Result; + public function join():Result; } \ No newline at end of file diff --git a/std/eval/luv/ThreadPool.hx b/std/eval/luv/ThreadPool.hx index 4c6fd7cf407..aeb87248fc2 100644 --- a/std/eval/luv/ThreadPool.hx +++ b/std/eval/luv/ThreadPool.hx @@ -19,7 +19,7 @@ extern class ThreadPool { `callback` will be called by the `loop` after `work` completes, or immediately, in case there is an error scheduling `work`. **/ - static function queueWork(loop:Loop, ?request:ThreadPoolRequest, work:()->Void, callback:(result:Result)->Void):Void; + static function queueWork(loop:Loop, ?request:ThreadPoolRequest, work:()->Void, callback:(result:Result)->Void):Void; /** Sets thread pool size. diff --git a/std/eval/luv/Timer.hx b/std/eval/luv/Timer.hx index 8b4f591f814..0b30e26a8a6 100644 --- a/std/eval/luv/Timer.hx +++ b/std/eval/luv/Timer.hx @@ -26,15 +26,15 @@ package eval.luv; /** Starts a timer. **/ - public function start(callback:()->Void, timeoutMs:Int, ?repeatMs:Int):Result; + public function start(callback:()->Void, timeoutMs:Int, ?repeatMs:Int):Result; /** Stops a timer. **/ - public function stop():Result; + public function stop():Result; /** Restarts a timer. **/ - public function again():Result; + public function again():Result; } \ No newline at end of file diff --git a/std/eval/luv/Tty.hx b/std/eval/luv/Tty.hx index 58940483a75..e002067d4b9 100644 --- a/std/eval/luv/Tty.hx +++ b/std/eval/luv/Tty.hx @@ -23,7 +23,7 @@ enum abstract VTermState(Int) { To be called when the program exits. Resets TTY settings to default values for the next process to take over. **/ - static public function resetMode():Result; + static public function resetMode():Result; /** Controls whether console virtual terminal sequences are processed by libuv @@ -54,7 +54,7 @@ enum abstract VTermState(Int) { /** Sets the TTY's mode. **/ - public function setMode(mode:TtyMode):Result; + public function setMode(mode:TtyMode):Result; /** Retrieves the current window size. diff --git a/std/eval/luv/UVError.hx b/std/eval/luv/UVError.hx index fd99941b01d..e2ff3146612 100644 --- a/std/eval/luv/UVError.hx +++ b/std/eval/luv/UVError.hx @@ -1,5 +1,7 @@ package eval.luv; +import asys.native.IoErrorType; + /** Error handling. @@ -188,4 +190,23 @@ enum abstract UVError(Int) { Returns the error message corresponding to the given error. **/ extern public function toString():String; + + @:to public function toIoErrorType():IoErrorType { + return switch (cast this:UVError) { + case UV_ENOENT: FileNotFound; + case UV_EEXIST: FileExists; + case UV_ESRCH: ProcessNotFound; + case UV_EACCES: AccessDenied; + case UV_ENOTDIR: NotDirectory; + case UV_EMFILE: TooManyOpenFiles; + case UV_EPIPE: BrokenPipe; + case UV_ENOTEMPTY: NotEmpty; + case UV_EADDRNOTAVAIL: AddressNotAvailable; + case UV_ECONNRESET: ConnectionReset; + case UV_ETIMEDOUT: TimedOut; + case UV_ECONNREFUSED: ConnectionRefused; + case UV_EBADF: BadFile; + case _: CustomError(toString()); + } + } } \ No newline at end of file diff --git a/std/eval/luv/Udp.hx b/std/eval/luv/Udp.hx index 1336006cb64..e78b6b5949a 100644 --- a/std/eval/luv/Udp.hx +++ b/std/eval/luv/Udp.hx @@ -31,7 +31,7 @@ enum abstract RecvFlag(Int) { /** Assigns an address to the UDP socket. **/ - public function bind(addr:SockAddr, ipv6Only:Bool = false, reuseAddr:Bool = false):Result; + public function bind(addr:SockAddr, ipv6Only:Bool = false, reuseAddr:Bool = false):Result; /** Assigns a peer address to the socket. @@ -46,49 +46,49 @@ enum abstract RecvFlag(Int) { /** Sets multicast group membership. **/ - public function setMembership(group:String, interfaceName:String, membership:UdpMembership):Result; + public function setMembership(group:String, interfaceName:String, membership:UdpMembership):Result; /** Sets source-specific multicast group membership. **/ - public function setSourceMembership(group:String, interfaceName:String, source:String, membership:UdpMembership):Result; + public function setSourceMembership(group:String, interfaceName:String, source:String, membership:UdpMembership):Result; /** Set multicast loopback. **/ - public function setMulticastLoop(value:Bool):Result; + public function setMulticastLoop(value:Bool):Result; /** Set multicast TTL. **/ - public function setMulticastTtl(value:Int):Result; + public function setMulticastTtl(value:Int):Result; /** Sets the interface to be used for multicast. **/ - public function setMulticastInterface(value:Int):Result; + public function setMulticastInterface(value:Int):Result; /** Sets broadcast. **/ - public function setBroadcast(value:Bool):Result; + public function setBroadcast(value:Bool):Result; /** Sets the TTL. **/ - public function setTtl(value:Int):Result; + public function setTtl(value:Int):Result; /** Sends a datagram. For connected UDP sockets, see `eval.luv.UDP.Connected.send`. **/ - public function send(data:Array, addr:SockAddr, callback:(result:Result)->Void):Void; + public function send(data:Array, addr:SockAddr, callback:(result:Result)->Void):Void; /** Like `eval.luv.UDP.send`, but only attempts to send the datagram immediately. **/ - public function trySend(data:Array, addr:SockAddr):Result; + public function trySend(data:Array, addr:SockAddr):Result; /** Calls `callback` whenever a datagram is received on the UDP socket. @@ -100,7 +100,7 @@ enum abstract RecvFlag(Int) { /** Stops the callback provided to `eval.luv.UDP.recvStart`. **/ - public function recvStop():Result; + public function recvStop():Result; /** Evaluates to true if and only if the UDP was created with `recvmmsg = true` diff --git a/std/haxe/Callback.hx b/std/haxe/Callback.hx new file mode 100644 index 00000000000..06f9564ae77 --- /dev/null +++ b/std/haxe/Callback.hx @@ -0,0 +1,47 @@ +package haxe; + +typedef CallbackHandler = (error:Null, result:R) -> Void; + +/** + A callback. + + All callbacks in the standard library are functions which accept + two arguments: an error (`haxe.Exception`) and a result (`T`). + + Non-null `error` means an operation failed to finish successfully. + In case of failure the value of the second argument has no meaning and should + not be used. + + The underlying function type is declared in `haxe.CallbackHandler`. +**/ +abstract Callback(CallbackHandler) from CallbackHandler { + /** + This method may be used instead of allocating an anonymous function to ignore + the outcome of an operation. + **/ + static public function ignore(?e:Null, result:R):Void {} + + /** + Create a callback, which ignores the result of an operation. + + TODO: type inference does not work for arguments of `fn` if `fromNoResult` is + used through an implicit cast. Submit compiler issue. + **/ + // @:from static public inline function ignoreResult(fn:(error:Null) -> Void):Callback { + // return (e:Null, r:Null) -> fn(e); + // } + + /** + Report a failure. + **/ + public function fail(error:E):Void { + this(error, @:nullSafety(Off) (null:R)); + } + + /** + Emit the result of a successful operation. + **/ + public inline function success(result:R):Void { + this(null, result); + } +} \ No newline at end of file diff --git a/std/haxe/NoData.hx b/std/haxe/NoData.hx new file mode 100644 index 00000000000..3eff4bc7d4f --- /dev/null +++ b/std/haxe/NoData.hx @@ -0,0 +1,9 @@ +package haxe; + +/** + Data type used to indicate the absence of a value instead of `Void` in + value-places in types with type parameters. +**/ +enum abstract NoData(Null) from Dynamic { + var NoData = null; +} \ No newline at end of file diff --git a/std/haxe/exceptions/NotSupportedException.hx b/std/haxe/exceptions/NotSupportedException.hx new file mode 100644 index 00000000000..dbf19b5052b --- /dev/null +++ b/std/haxe/exceptions/NotSupportedException.hx @@ -0,0 +1,19 @@ +package haxe.exceptions; + +/** + An exception that is thrown when requested function or operation is + not supported or cannot be implemented. +**/ +class NotSupportedException extends Exception { + /** + Returns an instance of `NotSupportedException` with the message telling + that the caller of this method is not supported on current platform. + **/ + static public function field(?pos:PosInfos):NotSupportedException { + return new NotSupportedException('${@:privateAccess PosException.fieldPath(pos)} is not supported on this platform'); + } + + public function new(message:String = 'Operation not supported', ?previous:Exception):Void { + super(message, previous); + } +} \ No newline at end of file diff --git a/std/haxe/exceptions/PosException.hx b/std/haxe/exceptions/PosException.hx index 2b2af5a3ea7..da44e4600ac 100644 --- a/std/haxe/exceptions/PosException.hx +++ b/std/haxe/exceptions/PosException.hx @@ -1,5 +1,7 @@ package haxe.exceptions; +using StringTools; + /** An exception that carry position information of a place where it was created. **/ @@ -9,6 +11,8 @@ class PosException extends Exception { **/ public final posInfos:PosInfos; + var __fieldPath:Null; + public function new(message:String, ?previous:Exception, ?pos:PosInfos):Void { super(message, previous); if (pos == null) { @@ -22,6 +26,26 @@ class PosException extends Exception { Returns exception message. **/ override function toString():String { - return '${super.toString()} in ${posInfos.className}.${posInfos.methodName} at ${posInfos.fileName}:${posInfos.lineNumber}'; + var fieldPath = switch __fieldPath { + case null: __fieldPath = fieldPath(posInfos); + case s: s; + } + return '${super.toString()} in $__fieldPath at ${posInfos.fileName}:${posInfos.lineNumber}'; + } + + static function fieldPath(pos:PosInfos):String { + var className = if(pos.className.endsWith('_Impl_')) { + var parts = pos.className.split('.'); + parts.pop(); + parts[parts.length - 1] = parts[parts.length - 1].substr(1); + parts.join('.'); + } else { + pos.className; + } + var fieldName = switch pos.methodName.substr(0, 4) { + case 'get_' | 'set_': pos.methodName.substr(4); + case _: pos.methodName; + } + return '$className.$fieldName'; } } \ No newline at end of file diff --git a/std/haxe/io/BigBuffer.hx b/std/haxe/io/BigBuffer.hx new file mode 100644 index 00000000000..b448e73dac7 --- /dev/null +++ b/std/haxe/io/BigBuffer.hx @@ -0,0 +1,276 @@ +package haxe.io; + +import haxe.exceptions.NotImplementedException; + +enum abstract Endian(Int) { + var BigEndian; + var LittleEndian; +} + +/** + TODO: + This is an attempt to design a cross-platform API for big byte buffers (more than 2GB) + without any unnecessary allocations. +**/ +class BigBuffer { + /** + Current byte order for reading and writing numbers. + **/ + public var endian(get,set):Endian; + function get_endian():Endian throw new NotImplementedException(); + function set_endian(v:Endian):Endian throw new NotImplementedException(); + + /** + Buffer size (amount of bytes). + **/ + public function getLength():Int64 { + throw new NotImplementedException(); + } + + /** + Move internal pointer to the beginning - to the byte at index 0. + **/ + public function rewind():Void { + throw new NotImplementedException(); + } + + /** + Move internal pointer past the last byte. + **/ + public function fastForward():Void { + throw new NotImplementedException(); + } + + /** + Move internal pointer by `step` bytes forward (if `step` is positive) + or backward (if `step` is negative) + **/ + public function movePointer(step:Int):Void { + throw new NotImplementedException(); + } + + /** + Copy up to `length` bytes from this buffer starting at the internal + pointer position into `buffer` starting at `offset`. + + Returns amount of bytes copied. + + Advances internal pointer by the return value. + **/ + public function copyTo(buffer:Bytes, offset:Int, length:Int):Int { + throw new NotImplementedException(); + } + + /** + Copy up to `length` bytes from `buffer` starting at `offset` into this + buffer starting at the internal pointer position. + + Returns amount of bytes copied. + + Advances internal pointer by the return value. + **/ + public function copyFrom(buffer:Bytes, offset:Int, length:Int):Int { + throw new NotImplementedException(); + } + + /** + Sets up to `length` consecutive bytes starting from internal pointer position + to `value`. + + Returns amount of bytes filled. + + Advances internal pointer by the return value. + **/ + public function fill(length:Int, value:Int):Int { + throw new NotImplementedException(); + } + + /** + Returns a new `Bytes` instance that contains a copy of up to `length` bytes of + `this` instance, starting at the internal pointer position. + + Throws if internal pointer is at the end of this buffer. + + Advances internal pointer by the amount of bytes returned. + **/ + public function slice(length:Int):Bytes { + throw new NotImplementedException(); + } + + /** + Returns the IEEE double-precision value at the internal pointer position. + + Throws if internal pointer is less than 8 bytes to the end of this buffer. + + Advances internal pointer by 8 bytes. + **/ + public function getDouble():Float { + throw new NotImplementedException(); + } + + /** + Returns the IEEE single-precision value at the internal pointer position. + + Throws if internal pointer is less than 8 bytes to the end of this buffer. + + Advances internal pointer by 8 bytes. + **/ + public function getFloat():Float { + throw new NotImplementedException(); + } + + /** + Stores the given IEEE double-precision value `value` at the internal pointer + position. + + Throws if internal pointer is less than 8 bytes to the end of this buffer. + + Advances internal pointer by 8 bytes. + **/ + public function setDouble(value:Float):Void { + throw new NotImplementedException(); + } + + /** + Stores the given IEEE single-precision value `value` at the internal pointer + position. + + Throws if internal pointer is less than 8 bytes to the end of this buffer. + + Advances internal pointer by 8 bytes. + **/ + public function setFloat(value:Float):Void { + throw new NotImplementedException(); + } + + /** + Returns the 8-bit unsigned integer at the internal pointer position. + + Throws if internal pointer is at the end of this buffer. + + Advances internal pointer by 1 byte. + **/ + public function getByte():Int { + throw new NotImplementedException(); + } + + /** + Stores the given 8-bit unsigned integer `value` at the internal pointer position. + + Throws if internal pointer is at the end of this buffer. + Throws if `value` overflows 8-bit unsigned integer. + + Advances internal pointer by 1 byte. + **/ + public function setByte(value:Int):Void { + throw new NotImplementedException(); + } + + /** + Returns the 16-bit unsigned integer at the internal pointer position (in + little-endian encoding). + + Throws if internal pointer is less than 2 bytes to the end of this buffer. + + Advances internal pointer by 2 bytes. + **/ + public function getUInt16():Int { + throw new NotImplementedException(); + } + + /** + Stores the given 16-bit unsigned integer `value` at the internal pointer + position (in little-endian encoding). + + Throws if internal pointer is less than 2 bytes to the end of this buffer. + Throws if `value` overflows 16-bit unsigned integer. + + Advances internal pointer by 2 bytes. + **/ + public function setUInt16(value:Int):Void { + throw new NotImplementedException(); + } + + /** + Returns the 32-bit integer at the internal pointer position (in little-endian + encoding). + + Throws if internal pointer is less than 4 bytes to the end of this buffer. + + Advances internal pointer by 4 bytes. + **/ + public function getInt32():Int { + throw new NotImplementedException(); + } + + /** + Returns the 64-bit integer at the internal pointer position (in little-endian + encoding). + + Throws if internal pointer is less than 8 bytes to the end of this buffer. + + Advances internal pointer by 8 bytes. + **/ + public function getInt64():Int64 { + throw new NotImplementedException(); + } + + /** + Stores the given 32-bit integer `v` at the internal pointer position (in + little-endian encoding). + + Throws if internal pointer is less than 4 bytes to the end of this buffer. + Throws if `value` overflows 32-bit signed integer. + + Advances internal pointer by 4 bytes. + **/ + public function setInt32(value:Int):Void { + throw new NotImplementedException(); + } + + /** + Stores the given 64-bit integer `v` at the internal pointer position (in + little-endian encoding). + + Throws if internal pointer is less than 8 bytes to the end of this buffer. + + Advances internal pointer by 8 bytes. + **/ + public function setInt64(v:Int64):Void { + throw new NotImplementedException(); + } + + /** + Returns the `length`-bytes long string stored at the internal pointer position, + interpreted with the given `encoding` (UTF-8 by default). + + Throws if internal pointer is less than `length` bytes to the end of this buffer. + Throws if the requested bytes don't represent a valid encoded string. + + Advances internal pointer by `length` bytes. + **/ + public function getString(length:Int, ?encoding:Encoding):String { + throw new NotImplementedException(); + } + + public function toString():String { + return '[BigBuffer]'; + } + + /** + Returns a new `BigBuffer` instance with the given `length`. The values of the + bytes are not initialized and may not be zero. + **/ + public static function alloc(length:Int64, endian:Endian = LittleEndian):BigBuffer { + throw new NotImplementedException(); + } + + /** + Join `bytes` into one big buffer. + + Total length of the result buffer always equals the sum of `bytes` lengths. + **/ + public static function join(bytes:Array, endian:Endian = LittleEndian):BigBuffer { + throw new NotImplementedException(); + } +} diff --git a/std/hl/I64.hx b/std/hl/I64.hx index a4102139fa8..6eced76494f 100644 --- a/std/hl/I64.hx +++ b/std/hl/I64.hx @@ -31,6 +31,10 @@ package hl; return cast this; } + @:hlNative("std", "num_i64_of_int") + @:from public static function ofInt(i:Int):I64 + return cast 0; + @:to @:deprecated("Implicit cast from I64 to Int (32 bits) is deprecated. Use .toInt() or explicitly cast instead.") inline function implicitToInt(): Int { diff --git a/std/hl/_std/asys/native/filesystem/Directory.hx b/std/hl/_std/asys/native/filesystem/Directory.hx new file mode 100644 index 00000000000..d83422bf73e --- /dev/null +++ b/std/hl/_std/asys/native/filesystem/Directory.hx @@ -0,0 +1,38 @@ +package asys.native.filesystem; + +import haxe.NoData; +import hl.uv.UVError; +import hl.uv.Dir; +import asys.native.filesystem.FileSystem.currentLoop; +import asys.native.filesystem.FileSystem.ioError; + +@:coreApi +class Directory { + public final path:FilePath; + + final dir:Dir; + final maxBatchSize:Int; + + function new(dir:Dir, path:FilePath, maxBatchSize:Int) { + this.dir = dir; + this.path = path; + this.maxBatchSize = maxBatchSize; + } + + public function next(callback:Callback>):Void { + dir.read(currentLoop(), maxBatchSize, (e, entries) -> switch e { + case UV_NOERR: + var result = [for(e in entries) @:privateAccess new FilePath(e.name)]; + callback.success(result); + case _: + callback.fail(new FsException(ioError(e), path)); + }); + } + + public function close(callback:Callback):Void { + dir.close(currentLoop(), e -> switch e { + case UV_NOERR: callback.success(NoData); + case _: callback.fail(new FsException(ioError(e), path)); + }); + } +} \ No newline at end of file diff --git a/std/hl/_std/asys/native/filesystem/File.hx b/std/hl/_std/asys/native/filesystem/File.hx new file mode 100644 index 00000000000..8c9bb36fd44 --- /dev/null +++ b/std/hl/_std/asys/native/filesystem/File.hx @@ -0,0 +1,143 @@ +package asys.native.filesystem; + +import haxe.Int64; +import haxe.io.Bytes; +import haxe.NoData; +import sys.thread.Thread; +import asys.native.system.SystemUser; +import asys.native.system.SystemGroup; +import hl.I64; +import hl.uv.UVError; +import hl.uv.File as LFile; +import hl.uv.Loop; +import hl.uv.Idle; +import hl.Bytes as HlBytes; +import asys.native.filesystem.FileSystem.currentLoop; +import asys.native.filesystem.FileSystem.ioError; + +@:coreApi +class File { + public final path:FilePath; + + final file:LFile; + final deleteOnClose:Bool; + + function new(file:LFile, path:FilePath, deleteOnClose:Bool = false):Void { + this.file = file; + this.path = path; + this.deleteOnClose = deleteOnClose; + } + + public function write(position:Int64, buffer:Bytes, offset:Int, length:Int, callback:Callback):Void { + var error = null; + if(length < 0) { + error = 'Negative length'; + } else if(position < 0) { + error = 'Negative position'; + } else if(offset < 0 || offset > buffer.length) { + error = 'Offset out of buffer bounds'; + } + if(error != null) { + failAsync(callback, CustomError(error), path); + } else { + var pos = I64.ofInt(Int64.toInt(position)); //TODO: convert haxe.Int64 to hl.I64 directly + var l = offset + length > buffer.length ? buffer.length - offset : length; + file.write(currentLoop(), buffer.getData().bytes.sub(offset, l), l, pos, (e, bytesWritten) -> switch e { + case UV_NOERR: callback.success(bytesWritten.toInt()); + case _: callback.fail(new FsException(ioError(e), path)); + }); + } + } + + public function read(position:Int64, buffer:Bytes, offset:Int, length:Int, callback:Callback):Void { + var error = null; + if(length < 0) { + error = 'Negative length'; + } else if(position < 0) { + error = 'Negative position'; + } else if(offset < 0 || offset > buffer.length) { + error = 'Offset out of buffer bounds'; + } + if(error != null) { + failAsync(callback, CustomError(error), path); + } else { + var l = offset + length > buffer.length ? buffer.length - offset : length; + var b = new HlBytes(l); + var pos = I64.ofInt(Int64.toInt(position)); //TODO: convert haxe.Int64 to hl.I64 directly + file.read(currentLoop(), b, l, pos, (e, bytesRead) -> switch e { + case UV_NOERR: + var bytesRead = bytesRead.toInt(); + buffer.getData().bytes.blit(offset, b, 0, bytesRead); + callback.success(bytesRead); + case _: + callback.fail(new FsException(ioError(e), path)); + }); + } + } + + public function flush(callback:Callback):Void { + file.fsync(currentLoop(), e -> switch e { + case UV_NOERR: callback.success(NoData); + case _: callback.fail(new FsException(ioError(e), path)); + }); + } + + public function info(callback:Callback):Void { + file.fstat(currentLoop(), (e, stat) -> switch e { + case UV_NOERR: callback.success(stat); + case _: callback.fail(new FsException(ioError(e), path)); + }); + } + + public function setPermissions(permissions:FilePermissions, callback:Callback):Void { + file.fchmod(currentLoop(), permissions, e -> switch e { + case UV_NOERR: callback.success(NoData); + case _: callback.fail(new FsException(ioError(e), path)); + }); + } + + public function setOwner(user:SystemUser, group:SystemGroup, callback:Callback):Void { + file.fchown(currentLoop(), user, group, e -> switch e { + case UV_NOERR: callback.success(NoData); + case _: callback.fail(new FsException(ioError(e), path)); + }); + } + + public function resize(newSize:Int, callback:Callback):Void { + file.ftruncate(currentLoop(),I64.ofInt(newSize), e -> switch e { + case UV_NOERR: callback.success(NoData); + case _: callback.fail(new FsException(ioError(e), path)); + }); + } + + public function setTimes(accessTime:Int, modificationTime:Int, callback:Callback):Void { + file.futime(currentLoop(), accessTime, modificationTime, e -> switch e { + case UV_NOERR: callback.success(NoData); + case _: callback.fail(new FsException(ioError(e), path)); + }); + } + + // public function lock(mode:FileLock = Exclusive, wait:Bool = true, callback:Callback):Void { + // throw new NotImplementedException(); + // } + + public function close(callback:Callback):Void { + var loop = currentLoop(); + if(deleteOnClose) { + LFile.unlink(loop, path, _ -> {}); + } + file.close(loop, e -> switch e { + case UV_NOERR | UV_EBADF: callback.success(NoData); + case _: callback.fail(new FsException(ioError(e), path)); + }); + } + + function failAsync(callback:Callback, error:IoErrorType, path:FilePath):Void { + var idle = Idle.init(currentLoop()); + idle.start(() -> { + idle.stop(); + idle.close(() -> {}); + callback.fail(new FsException(error, path)); + }); + } +} \ No newline at end of file diff --git a/std/hl/_std/asys/native/filesystem/FileInfo.hx b/std/hl/_std/asys/native/filesystem/FileInfo.hx new file mode 100644 index 00000000000..1725362a90d --- /dev/null +++ b/std/hl/_std/asys/native/filesystem/FileInfo.hx @@ -0,0 +1,62 @@ +package asys.native.filesystem; + +import asys.native.system.SystemUser; +import asys.native.system.SystemGroup; +import hl.uv.File as LFile; + +private typedef NativeInfo = hl.uv.File.FileStat; + +@:coreApi +abstract FileInfo(NativeInfo) from NativeInfo to NativeInfo { + public var accessTime(get,never):Int; + inline function get_accessTime():Int + return this.atim.sec.toInt(); + + public var modificationTime(get,never):Int; + inline function get_modificationTime():Int + return this.mtim.sec.toInt(); + + public var creationTime(get,never):Int; + inline function get_creationTime():Int + return this.ctim.sec.toInt(); + + public var deviceNumber(get,never):Int; + inline function get_deviceNumber():Int + return this.dev.toInt(); + + public var group(get,never):SystemGroup; + inline function get_group():SystemGroup + return this.gid.toInt(); + + public var user(get,never):SystemUser; + inline function get_user():SystemUser + return this.uid.toInt(); + + public var inodeNumber(get,never):Int; + inline function get_inodeNumber():Int + return this.ino.toInt(); + + public var mode(get,never):FileMode; + inline function get_mode():FileMode + return this.mode.toInt(); + + public var links(get,never):Int; + inline function get_links():Int + return this.nlink.toInt(); + + public var deviceType(get,never):Int; + inline function get_deviceType():Int + return this.rdev.toInt(); + + public var size(get,never):Int; + inline function get_size():Int + return this.size.toInt(); + + public var blockSize(get,never):Int; + inline function get_blockSize():Int + return this.blksize.toInt(); + + public var blocks(get,never):Int; + inline function get_blocks():Int + return this.blocks.toInt(); +} \ No newline at end of file diff --git a/std/hl/_std/asys/native/filesystem/FilePath.hx b/std/hl/_std/asys/native/filesystem/FilePath.hx new file mode 100644 index 00000000000..0d4238f53a9 --- /dev/null +++ b/std/hl/_std/asys/native/filesystem/FilePath.hx @@ -0,0 +1,200 @@ +package asys.native.filesystem; + +import haxe.exceptions.ArgumentException; +import hl.uv.FileSync; + +using StringTools; + +private typedef NativeFilePath = String; + +@:coreApi abstract FilePath(NativeFilePath) to String { + public static var SEPARATOR(get,never):String; + static inline function get_SEPARATOR():String { + return _SEPARATOR; + } + + static var _SEPARATOR:String; + + static function __init__():Void { + _SEPARATOR = Sys.systemName() == 'Windows' ? '\\' : '/'; + } + + overload extern static public inline function createPath(path:String, ...appendices:String):FilePath { + return createPathImpl(path, ...appendices); + } + + static function createPathImpl(path:String, ...appendices:String):FilePath { + var path = ofString(path); + for(p in appendices) + path = path.add(p); + return path; + } + + overload extern static public inline function createPath(parts:Array):FilePath { + return ofArray(parts); + } + + @:noUsing + @:from public static inline function ofString(path:String):FilePath { + return new FilePath(path); + } + + @:from static function ofArray(parts:Array):FilePath { + if(parts.length == 0) + throw new ArgumentException('parts'); + var path = ofString(parts[0]); + for(i in 1...parts.length) + path = path.add(parts[i]); + return path; + } + + inline function new(s:String) { + this = s; + } + + @:to public inline function toString():String { + return this == null ? null : this.toString(); + } + + public function isAbsolute():Bool { + return switch this.length { + case 0: false; + case _ if(isSeparator(this.fastCodeAt(0))): true; + case 1: false; + case length if(SEPARATOR == '\\'): this.fastCodeAt(1) == ':'.code && length >= 3 && isSeparator(this.fastCodeAt(2)); + case _: false; + } + } + + public function parent():Null { + var s = trimSlashes(this); + switch s.length { + case 0: + return null; + case 1 if(isSeparator(s.fastCodeAt(0))): + return null; + case 2 | 3 if(SEPARATOR == '\\' && s.fastCodeAt(1) == ':'.code): + return null; + case (_ - 1) => i: + while(!isSeparator(s.fastCodeAt(i))) { + --i; + if(i < 0) + return null; + } + return new FilePath(s.substr(0, i + 1)); + } + } + + public function name():FilePath { + var s = trimSlashes(this); + var i = s.length - 1; + if(i < 0) + return s; + while(!isSeparator(s.fastCodeAt(i))) { + --i; + if(i < 0) + return s; + } + return new FilePath(s.substr(i + 1)); + } + + public function absolute():FilePath { + var result = if(this.length == 0) { + trimSlashes(Sys.getCwd()); + } else if(this.fastCodeAt(0) == '/'.code) { + this; + } else if(SEPARATOR == '\\') { + if(this.fastCodeAt(0) == '\\'.code) { + this; + } else if(this.length >= 2 && isDriveLetter(this.fastCodeAt(0)) && this.fastCodeAt(1) == ':'.code) { + if(this.length > 2 && isSeparator(this.fastCodeAt(3))) { + this; + } else { + try { + var driveCwd = FileSync.realPath(this.substr(0, 2) + '.'); + driveCwd + SEPARATOR + this.substr(2); + } catch(_) { + throw new FsException(CustomError('Unable to get current working directory of drive ${this.substr(0,1)]}'), new FilePath(this)); + } + } + } else { + trimSlashes(Sys.getCwd()) + SEPARATOR + this; + } + } else { + trimSlashes(Sys.getCwd()) + SEPARATOR + this; + } + return new FilePath(result); + } + + public function normalize():FilePath { + var parts = if(SEPARATOR == '\\') { + this.replace('\\', '/').split('/'); + } else { + this.split('/'); + } + var i = parts.length - 1; + var result = []; + var skip = 0; + while(i >= 0) { + switch parts[i] { + case '.' | '': + case '..': + ++skip; + case _ if(skip > 0): + --skip; + case part: + result.unshift(part); + } + --i; + } + for(i in 0...skip) + result.unshift('..'); + var result = ofString(result.join(SEPARATOR)); + return isAbsolute() && !result.isAbsolute() ? SEPARATOR + result : result; + } + + public function add(path:FilePath):FilePath { + if(path.isAbsolute() || this.length == 0) + return path; + var strPath = (path:String); + if(strPath.length == 0) + return new FilePath(this); + if(SEPARATOR == '\\') { + if(strPath.length >= 2 && strPath.fastCodeAt(1) == ':'.code) { + if(this.length >= 2 && this.fastCodeAt(1) == ':'.code) { + if(strPath.charAt(0).toLowerCase() != this.charAt(0).toLowerCase()) { + throw new ArgumentException('path', 'Cannot combine paths on different drives'); + } + return new FilePath(trimSlashes(this) + SEPARATOR + strPath.substr(2)); + } else if(isSeparator(this.fastCodeAt(0))) { + return new FilePath(strPath.substr(0, 2) + trimSlashes(this) + SEPARATOR + strPath.substr(2)); + } + } + } + return new FilePath(trimSlashes(this) + SEPARATOR + strPath); + } + + static inline function isSeparator(c:Int):Bool { + return c == '/'.code || (SEPARATOR == '\\' && c == '\\'.code); + } + + static function trimSlashes(s:String):String { + var i = s.length - 1; + if(i <= 0) + return s; + var sep = isSeparator(s.fastCodeAt(i)); + if(sep) { + do { + --i; + sep = isSeparator(s.fastCodeAt(i)); + } while(i > 0 && sep); + return s.substr(0, i + 1); + } else { + return s; + } + } + + static inline function isDriveLetter(c:Int):Bool { + return ('a'.code <= c && c <= 'z'.code) || ('A'.code <= c && c <= 'Z'.code); + } +} \ No newline at end of file diff --git a/std/hl/_std/asys/native/filesystem/FileSystem.hx b/std/hl/_std/asys/native/filesystem/FileSystem.hx new file mode 100644 index 00000000000..2c07cc7def9 --- /dev/null +++ b/std/hl/_std/asys/native/filesystem/FileSystem.hx @@ -0,0 +1,402 @@ +package asys.native.filesystem; + +import haxe.io.Bytes; +import haxe.io.BytesData; +import haxe.NoData; +import asys.native.system.SystemUser; +import asys.native.system.SystemGroup; +import sys.thread.Thread; +import hl.I64; +import hl.uv.UVError; +import hl.uv.Loop; +import hl.Bytes as HlBytes; +import hl.uv.File as LFile; +import hl.uv.Dir; +import hl.uv.File.FileOpenFlag as LFileOpenFlag; +import hl.uv.File.FileAccessMode as LFileAccessMode; + +@:coreApi +class FileSystem { + @:allow(asys.native.filesystem) + static inline function currentLoop():Loop { + return Thread.current().events; + } + + static public function openFile(path:FilePath, flag:FileOpenFlag, callback:Callback):Void { + LFile.open(currentLoop(), path, hlOpenFlags(flag), (e,f) -> switch e { + case UV_NOERR: callback.success(cast @:privateAccess new File(f, path)); + case _: callback.fail(new FsException(ioError(e), path)); + }); + } + + static public function tempFile(callback:Callback):Void { + var pattern = hl.uv.Misc.tmpDir() + '/XXXXXX'; + LFile.mkstemp(currentLoop(), pattern, (e, f, path) -> switch e { + case UV_NOERR: callback.success(@:privateAccess new File(f, @:privateAccess new FilePath(path), true)); + case _: callback.fail(new FsException(ioError(e), '(unknown path)')); + }); + } + + static public function readBytes(path:FilePath, callback:Callback):Void { + readFile(path, callback); + } + + static public function readString(path:FilePath, callback:Callback):Void { + readFile(path, (e, r) -> { + if(e == null) + callback.success(r.toString()) + else + callback.fail(e); + }); + } + + static inline function readFile(path:FilePath, callback:Callback):Void { + var loop = currentLoop(); + LFile.open(loop, path, [O_RDONLY], (e, f) -> switch e { + case UV_NOERR: + f.fstat(loop, (e, stat) -> switch e { + case UV_NOERR: + var length = stat.size.toInt(); + var buf = new HlBytes(length); + f.read(loop, buf, length, I64.ofInt(0), (e, bytesRead) -> switch e { + case UV_NOERR: + var bytesRead = bytesRead.toInt(); + f.close(loop, _ -> callback.success(Bytes.ofData(new BytesData(buf.sub(0, bytesRead), bytesRead)))); + case _: + f.close(loop, _ -> callback.fail(new FsException(ioError(e), path))); + }); + case _: + f.close(loop, _ -> callback.fail(new FsException(ioError(e), path))); + }); + case _: + callback.fail(new FsException(ioError(e), path)); + }); + } + + static public function writeBytes(path:FilePath, data:Bytes, flag:FileOpenFlag = Write, callback:Callback):Void { + writeFile(path, data.getData().bytes, data.length, flag, callback); + } + + static public function writeString(path:FilePath, text:String, flag:FileOpenFlag = Write, callback:Callback):Void { + var length = 0; + var utf8 = @:privateAccess text.bytes.utf16ToUtf8(0, length); + writeFile(path, utf8, length, flag, callback); + } + + static inline function writeFile(path:FilePath, data:HlBytes, length:Int, flag:FileOpenFlag, callback:Callback):Void { + var loop = currentLoop(); + LFile.open(loop, path, hlOpenFlags(flag), (e,f) -> switch e { + case UV_NOERR: + f.write(loop, data, length, I64.ofInt(0), (e, _) -> switch e { + case UV_NOERR: + f.close(loop, e -> switch e { + case UV_NOERR: callback.success(NoData); + case _: callback.fail(new FsException(ioError(e), path)); + }); + case _: + f.close(loop, _ -> callback.fail(new FsException(ioError(e), path))); + }); + case _: + callback.fail(new FsException(ioError(e), path)); + }); + } + + static public function openDirectory(path:FilePath, maxBatchSize:Int = 64, callback:Callback):Void { + Dir.open(currentLoop(), path, (e, dir) -> switch e { + case UV_NOERR: callback.success(@:privateAccess new Directory(dir, path, maxBatchSize)); + case _: callback.fail(new FsException(ioError(e), path)); + }); + } + + static public function listDirectory(path:FilePath, callback:Callback>):Void { + var loop = currentLoop(); + Dir.open(loop, path, (e, dir) -> switch e { + case UV_NOERR: + var result = []; + function collect(e:UVError, entries:Null>) { + switch e { + case UV_NOERR: + if(entries.length == 0) { + dir.close(loop, _ -> callback.success(result)); + } else { + for(entry in entries) + result.push(@:privateAccess new FilePath(entry.name)); + dir.read(loop, 32, collect); + } + case _: + dir.close(loop, _ -> callback.fail(new FsException(ioError(e), path))); + } + } + dir.read(loop, 32, collect); + case _: + callback.fail(new FsException(ioError(e), path)); + }); + } + + static public function createDirectory(path:FilePath, ?permissions:FilePermissions, recursive:Bool = false, callback:Callback):Void { + if(permissions == null) permissions = 511; + inline mkdir(path, permissions, recursive, e -> switch e { + case UV_NOERR: callback.success(NoData); + case _: callback.fail(new FsException(ioError(e), path)); + }); + } + + static function mkdir(path:FilePath, permissions:FilePermissions, recursive:Bool, callback:(e:UVError)->Void):Void { + var loop = currentLoop(); + function mk(path:FilePath, callback:(e:UVError)->Void) { + LFile.mkdir(loop, path, permissions, e -> switch e { + case UV_ENOENT if(recursive): + switch path.parent() { + case null: + callback(e); + case parent: + mk(parent, e -> switch e { + case UV_NOERR: + LFile.mkdir(loop, path, permissions, callback); + case _: + callback(e); + }); + } + case _: + callback(e); + }); + } + mk(path, callback); + } + + static public function uniqueDirectory(parentDirectory:FilePath, ?prefix:String, ?permissions:FilePermissions, recursive:Bool = false, callback:Callback):Void { + if(permissions == null) permissions = 511; + + var name = (prefix == null ? '' : prefix) + getRandomChar() + getRandomChar() + getRandomChar() + getRandomChar(); + var path = @:privateAccess new FilePath(parentDirectory.add(name)); + + function create(callback:(e:UVError)->Void) { + inline mkdir(path, permissions, recursive, e -> switch e { + case UV_EEXIST: + var next = (path:String) + getRandomChar(); + path = @:privateAccess new FilePath(next); + create(callback); + case _: + callback(e); + }); + } + create(e -> switch e { + case UV_NOERR: callback.success(path); + case _: callback.fail(new FsException(ioError(e), parentDirectory)); + }); + } + + static var __codes:Null>; + static function getRandomChar():String { + if(__codes == null) { + var a = [for(c in '0'.code...'9'.code) String.fromCharCode(c)]; + for(c in 'A'.code...'Z'.code) a.push(String.fromCharCode(c)); + for(c in 'a'.code...'z'.code) a.push(String.fromCharCode(c)); + __codes = a; + } + return __codes[Std.random(__codes.length)]; + } + + static public function move(oldPath:FilePath, newPath:FilePath, overwrite:Bool = true, callback:Callback):Void { + var loop = currentLoop(); + inline function move() { + LFile.rename(loop, oldPath, newPath, e -> switch e { + case UV_NOERR: callback.success(NoData); + case _: callback.fail(new FsException(ioError(e), oldPath)); + }); + } + if(overwrite) { + move(); + } else { + LFile.access(loop, newPath, [F_OK], e -> switch e { + case UV_ENOENT: move(); + case UV_NOERR: callback.fail(new FsException(FileExists, newPath)); + case _: callback.fail(new FsException(ioError(e), newPath)); + }); + } + } + + static public function deleteFile(path:FilePath, callback:Callback):Void { + LFile.unlink(currentLoop(), path, e -> switch e { + case UV_NOERR: callback.success(NoData); + case _: callback.fail(new FsException(ioError(e), path)); + }); + } + + static public function deleteDirectory(path:FilePath, callback:Callback):Void { + LFile.rmdir(currentLoop(), path, e -> switch e { + case UV_NOERR: callback.success(NoData); + case e: callback.fail(new FsException(ioError(e), path)); + }); + } + + static public function info(path:FilePath, callback:Callback):Void { + LFile.stat(currentLoop(), path, (e, stat) -> switch e { + case UV_NOERR: callback.success(stat); + case _: callback.fail(new FsException(ioError(e), path)); + }); + } + + static public function check(path:FilePath, mode:FileAccessMode, callback:Callback):Void { + var flags = []; + if(mode.has(Exists)) flags.push(F_OK); + if(mode.has(Executable)) flags.push(X_OK); + if(mode.has(Writable)) flags.push(W_OK); + if(mode.has(Readable)) flags.push(R_OK); + LFile.access(currentLoop(), path, flags, e -> switch e { + case UV_ENOENT | UV_EACCES: callback.success(false); + case UV_NOERR: callback.success(true); + case _: callback.fail(new FsException(ioError(e), path)); + }); + } + + static public function isDirectory(path:FilePath, callback:Callback):Void { + LFile.stat(currentLoop(), path, (e, stat) -> switch e { + case UV_ENOENT: callback.success(false); + case UV_NOERR: callback.success((stat:FileInfo).mode.isDirectory()); + case _: callback.fail(new FsException(ioError(e), path)); + }); + } + + static public function isFile(path:FilePath, callback:Callback):Void { + LFile.stat(currentLoop(), path, (e, stat) -> switch e { + case UV_ENOENT: callback.success(false); + case UV_NOERR: callback.success((stat:FileInfo).mode.isFile()); + case _: callback.fail(new FsException(ioError(e), path)); + }); + } + + static public function setPermissions(path:FilePath, permissions:FilePermissions, callback:Callback):Void { + LFile.chmod(currentLoop(), path, permissions, e -> switch e { + case UV_NOERR: callback.success(NoData); + case _: callback.fail(new FsException(ioError(e), path)); + }); + } + + static public function setOwner(path:FilePath, user:SystemUser, group:SystemGroup, callback:Callback):Void { + LFile.chown(currentLoop(), path, user, group, e -> switch e { + case UV_NOERR: callback.success(NoData); + case _: callback.fail(new FsException(ioError(e), path)); + }); + } + + static public function setLinkOwner(path:FilePath, user:SystemUser, group:SystemGroup, callback:Callback):Void { + LFile.lchown(currentLoop(), path, user, group, e -> switch e { + case UV_NOERR: callback.success(NoData); + case _: callback.fail(new FsException(ioError(e), path)); + }); + } + + static public function link(target:FilePath, path:FilePath, type:FileLink = SymLink, callback:Callback):Void { + var cb = e -> switch e { + case UV_NOERR: callback.success(NoData); + case _: callback.fail(new FsException(ioError(e), path)); + } + switch type { + case HardLink: + LFile.link(currentLoop(), target, path, cb); + case SymLink: + LFile.symlink(currentLoop(), target, path, null, cb); + } + } + + static public function isLink(path:FilePath, callback:Callback):Void { + LFile.lstat(currentLoop(), path, (e, stat) -> switch e { + case UV_ENOENT: callback.success(false); + case UV_NOERR: callback.success((stat:FileInfo).mode.isLink()); + case _: callback.fail(new FsException(ioError(e), path)); + }); + } + + static public function readLink(path:FilePath, callback:Callback):Void { + LFile.readLink(currentLoop(), path, (e, real) -> switch e { + case UV_NOERR: callback.success(@:privateAccess new FilePath(real)); + case _: callback.fail(new FsException(ioError(e), path)); + }); + } + + static public function linkInfo(path:FilePath, callback:Callback):Void { + LFile.lstat(currentLoop(), path, (e, stat) -> switch e { + case UV_NOERR: callback.success(stat); + case _: callback.fail(new FsException(ioError(e), path)); + }); + } + + static public function copyFile(source:FilePath, destination:FilePath, overwrite:Bool = true, callback:Callback):Void { + LFile.copyFile(currentLoop(), source, destination, (overwrite ? null : [EXCL]), e -> switch e { + case UV_EEXIST: callback.fail(new FsException(FileExists, destination)); + case UV_NOERR: callback.success(NoData); + case _: callback.fail(new FsException(ioError(e), source)); + }); + } + + static public function resize(path:FilePath, newSize:Int, callback:Callback):Void { + var loop = currentLoop(); + LFile.open(loop, path, [O_CREAT(420), O_WRONLY], (e, file) -> switch e { + case UV_NOERR: + file.ftruncate(loop, I64.ofInt(newSize), e -> switch e { + case UV_NOERR: + file.close(loop, e -> switch e { + case UV_NOERR: + callback.success(NoData); + case _: + callback.fail(new FsException(ioError(e), path)); + }); + case _: + file.close(loop, _ -> callback.fail(new FsException(ioError(e), path))); + }); + case _: + callback.fail(new FsException(ioError(e), path)); + }); + } + + static public function setTimes(path:FilePath, accessTime:Int, modificationTime:Int, callback:Callback):Void { + LFile.utime(currentLoop(), path, accessTime, modificationTime, e -> switch e { + case UV_NOERR: callback.success(NoData); + case _: callback.fail(new FsException(ioError(e), path)); + }); + } + + static public function realPath(path:FilePath, callback:Callback):Void { + LFile.realPath(currentLoop(), path, (e, real) -> switch e { + case UV_NOERR: callback.success(@:privateAccess new FilePath(real)); + case _: callback.fail(new FsException(ioError(e), path)); + }); + } + + // mode 420 == 0644 + static function hlOpenFlags(flag:FileOpenFlag, mode:Int = 420):Array { + return switch flag { + case Append: [O_WRONLY, O_APPEND, O_CREAT(mode)]; + case Read: [O_RDONLY]; + case ReadWrite: [O_RDWR]; + case Write: [O_WRONLY, O_CREAT(mode), O_TRUNC]; + case WriteX: [O_WRONLY, O_CREAT(mode), O_EXCL]; + case WriteRead: [O_RDWR, O_CREAT(mode), O_TRUNC]; + case WriteReadX: [O_RDWR, O_CREAT(mode), O_EXCL]; + case Overwrite: [O_WRONLY, O_CREAT(mode)]; + case OverwriteRead: [O_RDWR, O_CREAT(mode)]; + } + } + + @:allow(asys.native.filesystem) + static function ioError(e:UVError):Null { + return switch e { + case UV_NOERR: null; + case UV_ENOENT: FileNotFound; + case UV_EEXIST: FileExists; + case UV_ESRCH: ProcessNotFound; + case UV_EACCES: AccessDenied; + case UV_ENOTDIR: NotDirectory; + case UV_EMFILE: TooManyOpenFiles; + case UV_EPIPE: BrokenPipe; + case UV_ENOTEMPTY: NotEmpty; + case UV_EADDRNOTAVAIL: AddressNotAvailable; + case UV_ECONNRESET: ConnectionReset; + case UV_ETIMEDOUT: TimedOut; + case UV_ECONNREFUSED: ConnectionRefused; + case UV_EBADF: BadFile; + case _: CustomError(e.toString()); + } + } +} \ No newline at end of file diff --git a/std/hl/_std/sys/thread/Thread.hx b/std/hl/_std/sys/thread/Thread.hx index 8fccbeef905..74a3a1ab365 100644 --- a/std/hl/_std/sys/thread/Thread.hx +++ b/std/hl/_std/sys/thread/Thread.hx @@ -63,9 +63,10 @@ abstract Thread(ThreadImpl) from ThreadImpl { #end function get_events():EventLoop { - if(this.events == null) - throw new NoEventLoopException(); - return this.events; + switch this.events { + case null: throw new NoEventLoopException(); + case events: return events; + } } @:keep diff --git a/std/java/_std/asys/native/filesystem/Directory.hx b/std/java/_std/asys/native/filesystem/Directory.hx new file mode 100644 index 00000000000..46d186736e3 --- /dev/null +++ b/std/java/_std/asys/native/filesystem/Directory.hx @@ -0,0 +1,62 @@ +package asys.native.filesystem; + +import haxe.NoData; +import java.nio.file.DirectoryStream; +import java.nio.file.Path; +import java.util.Iterator as JIterator; +import java.util.NoSuchElementException; +import java.lang.Throwable; +import java.nio.file.FileSystemException; +import asys.native.filesystem.FileSystem.pool; + +@:coreApi +class Directory { + public final path:FilePath; + + final stream:DirectoryStream; + final iterator:JIterator; + final maxBatchSize:Int; + + @:allow(asys.native.filesystem) + function new(path:FilePath, stream:DirectoryStream, maxBatchSize:Int) { + this.path = path; + this.stream = stream; + this.maxBatchSize = maxBatchSize; + this.iterator = stream.iterator(); + } + + public function next(callback:Callback>):Void { + pool.runFor( + () -> { + var result = []; + try { + while(result.length < maxBatchSize) { + result.push((iterator.next().getFileName():FilePath)); + } + result; + } catch(_:NoSuchElementException) { + result; + } catch(e:Throwable) { + throw new FsException(CustomError(e.toString()), path); + } + }, + callback + ); + } + + public function close(callback:Callback):Void { + pool.runFor( + () -> { + try { + stream.close(); + NoData; + } catch(e:FileSystemException) { + throw new FsException(CustomError(e.getReason()), path); + } catch(e:Throwable) { + throw new FsException(CustomError(e.toString()), path); + } + }, + callback + ); + } +} \ No newline at end of file diff --git a/std/java/_std/asys/native/filesystem/File.hx b/std/java/_std/asys/native/filesystem/File.hx new file mode 100644 index 00000000000..01f1586e618 --- /dev/null +++ b/std/java/_std/asys/native/filesystem/File.hx @@ -0,0 +1,175 @@ +package asys.native.filesystem; + +import haxe.Int64; +import haxe.io.Bytes; +import haxe.NoData; +import haxe.exceptions.NotImplementedException; +import haxe.exceptions.NotSupportedException; +import asys.native.IWritable; +import asys.native.IReadable; +import asys.native.filesystem.FileLock; +import asys.native.system.SystemUser; +import asys.native.system.SystemGroup; +import java.nio.file.Files; +import java.nio.channels.FileChannel; +import java.nio.ByteBuffer; +import java.nio.channels.FileLock as JFileLock; +import java.nio.file.FileSystemException; +import java.lang.Throwable; +import asys.native.filesystem.FileSystem.pool; + +@:coreApi +class File { + public final path:FilePath; + + final channel:FileChannel; + var deleteOnClose:Bool; + var interProcessLock:Null; + + @:allow(asys.native.filesystem) + function new(path:FilePath, channel:FileChannel, deleteOnClose:Bool = false) { + this.path = path; + this.channel = channel; + this.deleteOnClose = deleteOnClose; + } + + public function write(position:Int64, buffer:Bytes, offset:Int, length:Int, callback:Callback):Void { + pool.runFor( + () -> { + try { + var realLength = length > buffer.length - offset ? buffer.length - offset : length; + var jBuffer = ByteBuffer.wrap(buffer.getData(), offset, realLength); + channel.write(jBuffer, position); + } catch(e:FileSystemException) { + throw new FsException(CustomError(e.getReason()), path); + } catch(e:Throwable) { + throw new FsException(CustomError(e.toString()), path); + } + }, + callback + ); + } + + public function read(position:Int64, buffer:Bytes, offset:Int, length:Int, callback:Callback):Void { + pool.runFor( + () -> { + try { + var realLength = length > buffer.length - offset ? buffer.length - offset : length; + var jBuffer = ByteBuffer.wrap(buffer.getData(), offset, realLength); + var cnt = channel.read(jBuffer, position); + cnt < 0 ? 0 : cnt; + } catch(e:FileSystemException) { + throw new FsException(CustomError(e.getReason()), path); + } catch(e:Throwable) { + throw new FsException(CustomError(e.toString()), path); + } + }, + callback + ); + } + + public function flush(callback:Callback):Void { + pool.runFor( + () -> { + try { + channel.force(false); + NoData; + } catch(e:FileSystemException) { + throw new FsException(CustomError(e.getReason()), path); + } catch(e:Throwable) { + throw new FsException(CustomError(e.toString()), path); + } + }, + callback + ); + } + + public function info(callback:Callback):Void { + FileSystem.info(path, callback); + } + + public function setPermissions(permissions:FilePermissions, callback:Callback):Void { + FileSystem.setPermissions(path, permissions, callback); + } + + public function setOwner(user:SystemUser, group:SystemGroup, callback:Callback):Void { + FileSystem.setOwner(path, user, group, callback); + } + + public function resize(newSize:Int, callback:Callback):Void { + pool.runFor( + () -> { + try { + var current = channel.size(); + if(current > newSize) { + channel.truncate(newSize); + } else if(current < newSize) { + var buffer = ByteBuffer.allocate(Int64.toInt(newSize - current)); + channel.write(buffer, current); + } + NoData; + } catch(e:FileSystemException) { + throw new FsException(CustomError(e.getReason()), path); + } catch(e:Throwable) { + throw new FsException(CustomError(e.toString()), path); + } + }, + callback + ); + } + + public function setTimes(accessTime:Int, modificationTime:Int, callback:Callback):Void { + FileSystem.setTimes(path, accessTime, modificationTime, callback); + } + + // public function lock(mode:FileLock = Exclusive, wait:Bool = true, callback:Callback):Void { + // pool.runFor( + // () -> { + // try { + // interProcessLock = switch [mode, wait] { + // case [Exclusive, true]: channel.lock(); + // case [Shared, true]: channel.lock(0, java.lang.Long.MAX_VALUE, true); + // case [Exclusive, false]: channel.tryLock(); + // case [Shared, false]: channel.tryLock(0, java.lang.Long.MAX_VALUE, true); + // case [Unlock, _]: + // switch interProcessLock { + // case null: null; + // case l: + // l.release(); + // null; + // } + // } + // switch mode { + // case Unlock: interProcessLock == null; + // case _: interProcessLock != null; + // } + // } catch(e:FileSystemException) { + // throw new FsException(CustomError(e.getReason()), path); + // } catch(e:Throwable) { + // throw new FsException(CustomError(e.toString()), path); + // } + // }, + // callback + // ); + // } + + public function close(callback:Callback):Void { + pool.runFor( + () -> { + try { + channel.close(); + if(deleteOnClose) { + deleteOnClose = false; + try Files.delete(path) catch(_) {} + } + NoData; + } catch(e:FileSystemException) { + throw new FsException(CustomError(e.getReason()), path); + } catch(e:Throwable) { + throw new FsException(CustomError(e.toString()), path); + } + }, + callback + ); + } +} \ No newline at end of file diff --git a/std/java/_std/asys/native/filesystem/FileInfo.hx b/std/java/_std/asys/native/filesystem/FileInfo.hx new file mode 100644 index 00000000000..e1b2c9b0019 --- /dev/null +++ b/std/java/_std/asys/native/filesystem/FileInfo.hx @@ -0,0 +1,66 @@ +package asys.native.filesystem; + +import asys.native.system.SystemUser; +import asys.native.system.SystemGroup; +import java.util.concurrent.TimeUnit; +import haxe.exceptions.NotSupportedException; + +using haxe.Int64; + +private typedef NativeInfo = java.nio.file.attribute.PosixFileAttributes; + +@:coreApi +abstract FileInfo(NativeInfo) from NativeInfo to NativeInfo { + public var accessTime(get,never):Int; + inline function get_accessTime():Int + return this.lastAccessTime().to(SECONDS).toInt(); + + public var modificationTime(get,never):Int; + inline function get_modificationTime():Int + return this.lastModifiedTime().to(SECONDS).toInt(); + + public var creationTime(get,never):Int; + inline function get_creationTime():Int + return this.creationTime().to(SECONDS).toInt(); + + public var deviceNumber(get,never):Int; + inline function get_deviceNumber():Int + throw NotSupportedException.field(); + + public var group(get,never):SystemGroup; + inline function get_group():SystemGroup + return this.group(); + + public var user(get,never):SystemUser; + inline function get_user():SystemUser + return this.owner(); + + public var inodeNumber(get,never):Int; + inline function get_inodeNumber():Int + throw NotSupportedException.field(); + + public var mode(get,never):FileMode; + inline function get_mode():FileMode { + return this; + } + + public var links(get,never):Int; + inline function get_links():Int + throw NotSupportedException.field(); + + public var deviceType(get,never):Int; + inline function get_deviceType():Int + throw NotSupportedException.field(); + + public var size(get,never):Int; + inline function get_size():Int + return this.size().toInt(); + + public var blockSize(get,never):Int; + inline function get_blockSize():Int + throw NotSupportedException.field(); + + public var blocks(get,never):Int; + inline function get_blocks():Int + throw NotSupportedException.field(); +} \ No newline at end of file diff --git a/std/java/_std/asys/native/filesystem/FileMode.hx b/std/java/_std/asys/native/filesystem/FileMode.hx new file mode 100644 index 00000000000..a0196ba8a21 --- /dev/null +++ b/std/java/_std/asys/native/filesystem/FileMode.hx @@ -0,0 +1,33 @@ +package asys.native.filesystem; + +import haxe.exceptions.NotSupportedException; + +private typedef NativeMode = java.nio.file.attribute.PosixFileAttributes; + +@:coreApi +abstract FileMode(NativeMode) from NativeMode { + + public function has(permissions:FilePermissions):Bool + return this.permissions().containsAll(permissions); + + public inline function isBlockDevice():Bool + throw NotSupportedException.field(); + + public inline function isCharacterDevice():Bool + throw NotSupportedException.field(); + + public inline function isDirectory():Bool + return this.isDirectory(); + + public inline function isFIFO():Bool + throw NotSupportedException.field(); + + public inline function isFile():Bool + return this.isRegularFile(); + + public inline function isSocket():Bool + throw NotSupportedException.field(); + + public inline function isLink():Bool + return this.isSymbolicLink(); +} \ No newline at end of file diff --git a/std/java/_std/asys/native/filesystem/FilePath.hx b/std/java/_std/asys/native/filesystem/FilePath.hx new file mode 100644 index 00000000000..b6482ba6e81 --- /dev/null +++ b/std/java/_std/asys/native/filesystem/FilePath.hx @@ -0,0 +1,94 @@ +package asys.native.filesystem; + +import haxe.exceptions.ArgumentException; +import java.nio.file.Paths; +import java.nio.file.Path; +import java.NativeArray; +import java.NativeString; +import java.io.File as JFile; + +private typedef NativeFilePath = Path; + +@:coreApi abstract FilePath(NativeFilePath) to NativeFilePath { + public static var SEPARATOR(get,never):String; + static inline function get_SEPARATOR():String { + return JFile.separator; + } + + overload extern static public inline function createPath(path:String, ...appendices:String):FilePath { + return createPathImpl(path, ...appendices); + } + + @:native('createPath') + static function createPathImpl(path:String, ...appendices:String):FilePath { + var path = ofString(path); + for(p in appendices) + path = path.add(p); + return path; + } + + overload extern static public inline function createPath(parts:Array):FilePath { + return ofArray(parts); + } + + @:noUsing + @:from public static inline function ofString(path:String):FilePath { + return new FilePath(Paths.get(path)); + } + + @:from static function ofArray(parts:Array):FilePath { + if(parts.length == 0) + throw new ArgumentException('parts'); + var path = ofString(parts[0]); + for(i in 1...parts.length) + path = path.add(parts[i]); + return path; + } + + @:from static inline function ofNative(path:NativeFilePath):FilePath { + return new FilePath(path); + } + + inline function new(path:NativeFilePath) { + this = path; + } + + @:to public inline function toString():String { + return this == null ? null : jObj(this).toString(); + } + + @:op(A == B) inline function equals(p:FilePath):Bool { + return jObj(this).equals(jObj(this)); + } + + public inline function isAbsolute():Bool { + return this.isAbsolute(); + } + + public inline function normalize():FilePath { + return this.normalize(); + } + + public inline function absolute():FilePath { + return this.toAbsolutePath(); + } + + public inline function parent():Null { + return this.getParent(); + } + + public function name():FilePath { + return switch this.getFileName() { + case null: ''; + case path: path; + } + } + + public inline function add(path:FilePath):FilePath { + return this.resolve(path); + } + + static inline function jObj(o:NativeFilePath):java.lang.Object { + return cast o; + } +} \ No newline at end of file diff --git a/std/java/_std/asys/native/filesystem/FilePermissions.hx b/std/java/_std/asys/native/filesystem/FilePermissions.hx new file mode 100644 index 00000000000..9c02fda8102 --- /dev/null +++ b/std/java/_std/asys/native/filesystem/FilePermissions.hx @@ -0,0 +1,144 @@ +package asys.native.filesystem; + +import haxe.exceptions.ArgumentException; +import haxe.exceptions.NotSupportedException; +import java.nio.file.attribute.PosixFilePermission; +import java.util.EnumSet; +import java.util.Set; + +private typedef NativePermissions = Set; + +@:coreApi +abstract FilePermissions(NativePermissions) to NativePermissions { + static public inline function ignoresSpecialBit():Bool { + return true; + } + + static inline function empty():NativePermissions { + return new FilePermissions(EnumSet.noneOf((cast PosixFilePermission:java.lang.Class))); + } + + static public function octal(s:Int, u:Int, g:Int, o:Int):FilePermissions { + var set = empty(); + switch u { + case 0: + case 1: set.add(OWNER_EXECUTE); + case 2: set.add(OWNER_WRITE); + case 3: set.add(OWNER_EXECUTE); set.add(OWNER_WRITE); + case 4: set.add(OWNER_READ); + case 5: set.add(OWNER_EXECUTE); set.add(OWNER_READ); + case 6: set.add(OWNER_WRITE); set.add(OWNER_READ); + case 7: set.add(OWNER_EXECUTE); set.add(OWNER_WRITE); set.add(OWNER_READ); + case _: throw new ArgumentException('u'); + } + switch g { + case 0: + case 1: set.add(GROUP_EXECUTE); + case 2: set.add(GROUP_WRITE); + case 3: set.add(GROUP_EXECUTE); set.add(GROUP_WRITE); + case 4: set.add(GROUP_READ); + case 5: set.add(GROUP_EXECUTE); set.add(GROUP_READ); + case 6: set.add(GROUP_WRITE); set.add(GROUP_READ); + case 7: set.add(GROUP_EXECUTE); set.add(GROUP_WRITE); set.add(GROUP_READ); + case _: throw new ArgumentException('g'); + } + switch o { + case 0: + case 1: set.add(OTHERS_EXECUTE); + case 2: set.add(OTHERS_WRITE); + case 3: set.add(OTHERS_EXECUTE); set.add(OTHERS_WRITE); + case 4: set.add(OTHERS_READ); + case 5: set.add(OTHERS_EXECUTE); set.add(OTHERS_READ); + case 6: set.add(OTHERS_WRITE); set.add(OTHERS_READ); + case 7: set.add(OTHERS_EXECUTE); set.add(OTHERS_WRITE); set.add(OTHERS_READ); + case _: throw new ArgumentException('g'); + } + return new FilePermissions(set); + } + + @:from static inline function fromOctal(mode:Array):FilePermissions { + if(mode.length != 4) { + throw new ArgumentException('mode', '"mode" array should contain exactly four items'); + } + return octal(mode[0], mode[1], mode[2], mode[3]); + } + + @:from static function fromDecimal(dec:Int):FilePermissions { + var set = empty(); + if(dec & (1 << 0) != 0) set.add(OTHERS_EXECUTE); + if(dec & (1 << 1) != 0) set.add(OTHERS_WRITE); + if(dec & (1 << 2) != 0) set.add(OTHERS_READ); + if(dec & (1 << 3) != 0) set.add(GROUP_EXECUTE); + if(dec & (1 << 4) != 0) set.add(GROUP_WRITE); + if(dec & (1 << 5) != 0) set.add(GROUP_READ); + if(dec & (1 << 6) != 0) set.add(OWNER_EXECUTE); + if(dec & (1 << 7) != 0) set.add(OWNER_WRITE); + if(dec & (1 << 8) != 0) set.add(OWNER_READ); + return new FilePermissions(set); + } + + @:to function toDecimal():Int { + var result = 0; + for(v in this) { + switch v { + case OTHERS_EXECUTE: result = result | (1 << 0); + case OTHERS_WRITE: result = result | (1 << 1); + case OTHERS_READ: result = result | (1 << 2); + case GROUP_EXECUTE: result = result | (1 << 3); + case GROUP_WRITE: result = result | (1 << 4); + case GROUP_READ: result = result | (1 << 5); + case OWNER_EXECUTE: result = result | (1 << 6); + case OWNER_WRITE: result = result | (1 << 7); + case OWNER_READ: result = result | (1 << 8); + } + } + return result; + } + + @:op(A & B) static function intersect(perm1:FilePermissions, perm2:FilePermissions):FilePermissions { + var set1:NativePermissions = perm1; + var set2:NativePermissions = perm2; + var result = empty(); + var values = java.NativeArray.make( + OTHERS_EXECUTE, OTHERS_WRITE, OTHERS_READ, + GROUP_EXECUTE, GROUP_WRITE, GROUP_READ, + OWNER_EXECUTE, OWNER_WRITE, OWNER_READ + ); + for(i in 0...values.length) { + if(set1.contains(values[i]) && set2.contains(values[i])) { + result.add(values[i]); + } + } + return new FilePermissions(result); + } + + @:op(A | B) static function merge(perm1:FilePermissions, perm2:FilePermissions):FilePermissions { + var result = EnumSet.copyOf(perm1); + result.addAll(perm2); + return new FilePermissions(result); + } + + @:op(A == B) static function equals(perm1:Null, perm2:Null):Bool { + var p1:Null = perm1; + var p2:Null = perm2; + if(p1 == p2) { + return true; + } else if(p1 == null || p2 == null) { + return false; + } else { + return #if jvm p1.equals(p2) #else (cast p1:java.lang.Object).equals(p2) #end; + } + } + + @:op(A == B) @:commutative static inline function equalsDecimal(perm1:Null, dec:Int):Bool { + return equals(perm1, fromDecimal(dec)); + } + + inline function new(perm:NativePermissions) { + this = perm; + } + + public inline function toString():String { + return '${toDecimal()}'; + } +} \ No newline at end of file diff --git a/std/java/_std/asys/native/filesystem/FileSystem.hx b/std/java/_std/asys/native/filesystem/FileSystem.hx new file mode 100644 index 00000000000..138ac88b1d7 --- /dev/null +++ b/std/java/_std/asys/native/filesystem/FileSystem.hx @@ -0,0 +1,527 @@ +package asys.native.filesystem; + +import haxe.io.Bytes; +import haxe.NoData; +import asys.native.system.SystemUser; +import asys.native.system.SystemGroup; +import sys.thread.ElasticThreadPool; +import java.NativeArray; +import java.lang.Exception as JException; +import java.lang.Throwable; +import java.lang.Class as JClass; +import java.lang.Runtime; +import java.util.Set; +import java.io.RandomAccessFile; +import java.util.concurrent.TimeUnit; +import java.nio.file.Files; +import java.nio.file.Path as JPath; +import java.nio.file.StandardOpenOption; +import java.nio.file.OpenOption; +import java.nio.file.LinkOption; +import java.nio.file.StandardCopyOption; +import java.nio.file.CopyOption; +import java.nio.file.NoSuchFileException; +import java.nio.file.FileSystemException; +import java.nio.file.NotDirectoryException; +import java.nio.file.attribute.FileTime; +import java.nio.file.attribute.PosixFilePermission; +import java.nio.file.attribute.PosixFilePermissions; +import java.nio.file.attribute.PosixFileAttributes; +import java.nio.file.attribute.PosixFileAttributeView; +import java.nio.channels.FileChannel; + +@:coreApi +class FileSystem { + @:allow(asys.native.filesystem) + static final pool = new ElasticThreadPool(2 * Runtime.getRuntime().availableProcessors()); + + static public function openFile(path:FilePath, flag:FileOpenFlag, callback:Callback):Void { + inline pool.runFor( + () -> { + try { + var channel = FileChannel.open(path, ...hxOpenFlagToJavaOption(flag)); + cast new File(path, channel); + } catch(e:FileSystemException) { + var reason = e.getReason(); + throw new FsException(CustomError(reason == null ? e.toString() : reason), path); + } catch(e:Throwable) { + throw new FsException(CustomError(e.toString()), path); + } + }, + callback + ); + } + + static public function tempFile(callback:Callback):Void { + pool.runFor( + () -> { + try { + var path = Files.createTempFile(@:nullSafety(Off) (null:String), @:nullSafety(Off) (null:String)); + var channel = FileChannel.open(path, ...hxOpenFlagToJavaOption(ReadWrite)); + cast new File(path, channel, true); + } catch(e:FileSystemException) { + var reason = e.getReason(); + throw new FsException(CustomError(reason == null ? e.toString() : reason), '(unknown path)'); + } catch(e:Throwable) { + throw new FsException(CustomError(e.toString()), '(unknown path)'); + } + }, + callback + ); + } + + static public function readBytes(path:FilePath, callback:Callback):Void { + pool.runFor( + () -> { + try { + Bytes.ofData(Files.readAllBytes(path)); + } catch(e:FileSystemException) { + throw new FsException(CustomError(e.getReason()), path); + } catch(e:Throwable) { + throw new FsException(CustomError(e.toString()), path); + } + }, + callback + ); + } + + static public function readString(path:FilePath, callback:Callback):Void { + pool.runFor( + () -> { + try { + var bytes = Files.readAllBytes(path); + new String(bytes, 0, bytes.length, "UTF-8"); + } catch(e:FileSystemException) { + throw new FsException(CustomError(e.getReason()), path); + } catch(e:Throwable) { + throw new FsException(CustomError(e.toString()), path); + } + }, + callback + ); + } + + static public function writeBytes(path:FilePath, data:Bytes, flag:FileOpenFlag = Write, callback:Callback):Void { + pool.runFor( + () -> { + try { + Files.write(path, data.getData(), ...hxOpenFlagToJavaOption(flag)); + NoData; + } catch(e:FileSystemException) { + throw new FsException(CustomError(e.getReason()), path); + } catch(e:Throwable) { + throw new FsException(CustomError(e.toString()), path); + } + }, + callback + ); + } + + static public function writeString(path:FilePath, text:String, flag:FileOpenFlag = Write, callback:Callback):Void { + pool.runFor( + () -> { + try { + Files.write(path, @:privateAccess text.getBytes("UTF-8"), ...hxOpenFlagToJavaOption(flag)); + NoData; + } catch(e:FileSystemException) { + throw new FsException(CustomError(e.getReason()), path); + } catch(e:Throwable) { + throw new FsException(CustomError(e.toString()), path); + } + }, + callback + ); + } + + static public function openDirectory(path:FilePath, maxBatchSize:Int = 64, callback:Callback):Void { + pool.runFor( + () -> { + try { + new Directory(path, Files.newDirectoryStream(path), maxBatchSize); + } catch(e:FileSystemException) { + throw new FsException(CustomError(e.getReason()), path); + } catch(e:Throwable) { + throw new FsException(CustomError(e.toString()), path); + } + }, + callback + ); + } + + static public function listDirectory(path:FilePath, callback:Callback>):Void { + pool.runFor( + () -> { + try { + var result = []; + var dir = Files.newDirectoryStream(path); + for(entry in dir) { + result.push((entry.getFileName():FilePath)); + } + result; + } catch(e:FileSystemException) { + throw new FsException(CustomError(e.getReason()), path); + } catch(e:Throwable) { + throw new FsException(CustomError(e.toString()), path); + } + }, + callback + ); + } + + static public function createDirectory(path:FilePath, ?permissions:FilePermissions, recursive:Bool = false, callback:Callback):Void { + var jPerm:Null> = permissions; + var jPerm:Set = jPerm == null ? FilePermissions.octal(0, 7, 7, 7) : jPerm; + pool.runFor( + () -> { + try { + var attributes = PosixFilePermissions.asFileAttribute(jPerm); + if(recursive) + Files.createDirectories(path, attributes) + else + Files.createDirectory(path, attributes); + NoData; + } catch(e:FileSystemException) { + throw new FsException(CustomError(e.getReason()), path); + } catch(e:Throwable) { + throw new FsException(CustomError(e.toString()), path); + } + }, + callback + ); + } + + static public function uniqueDirectory(parentDirectory:FilePath, ?prefix:String, ?permissions:FilePermissions, recursive:Bool = false, callback:Callback):Void { + var prefix:String = prefix == null ? '' : prefix; + var jPerm:Null> = permissions; + var jPerm:Set = jPerm == null ? FilePermissions.octal(0, 7, 7, 7) : jPerm; + pool.runFor( + () -> { + try { + var attributes = PosixFilePermissions.asFileAttribute(jPerm); + if(recursive) + Files.createDirectories(parentDirectory, attributes); + Files.createTempDirectory(parentDirectory, prefix, attributes); + } catch(e:FileSystemException) { + throw new FsException(CustomError(e.getReason()), parentDirectory); + } catch(e:Throwable) { + throw new FsException(CustomError(e.toString()), parentDirectory); + } + }, + callback + ); + } + + static public function move(oldPath:FilePath, newPath:FilePath, overwrite:Bool = true, callback:Callback):Void { + pool.runFor( + () -> { + try { + var options = overwrite ? NativeArray.make((cast StandardCopyOption.REPLACE_EXISTING:CopyOption)) : new NativeArray(0); + Files.move(oldPath, newPath, ...options); + NoData; + } catch(e:FileSystemException) { + throw new FsException(CustomError(e.getReason()), oldPath); + } catch(e:Throwable) { + throw new FsException(CustomError(e.toString()), oldPath); + } + }, + callback + ); + } + + static public function deleteFile(path:FilePath, callback:Callback):Void { + pool.runFor( + () -> { + try { + if(Files.isDirectory(path)) { + throw new JException('Not a file'); + } + Files.delete(path); + NoData; + } catch(e:FileSystemException) { + throw new FsException(CustomError(e.getReason()), path); + } catch(e:Throwable) { + throw new FsException(CustomError(e.toString()), path); + } + }, + callback + ); + } + + static public function deleteDirectory(path:FilePath, callback:Callback):Void { + pool.runFor( + () -> { + try { + if(Files.isDirectory(path)) { + Files.delete(path); + } else { + throw new NotDirectoryException(path); + } + NoData; + } catch(e:FileSystemException) { + throw new FsException(CustomError(e.getReason()), path); + } catch(e:Throwable) { + throw new FsException(CustomError(e.toString()), path); + } + }, + callback + ); + } + + static public function info(path:FilePath, callback:Callback):Void { + pool.runFor( + () -> { + try { + Files.readAttributes(path, javaClass(PosixFileAttributes)); + } catch(e:FileSystemException) { + throw new FsException(CustomError(e.getReason()), path); + } catch(e:Throwable) { + throw new FsException(CustomError(e.toString()), path); + } + }, + callback + ); + } + + static public function check(path:FilePath, mode:FileAccessMode, callback:Callback):Void { + pool.runFor( + () -> { + try { + (!mode.has(Exists) || Files.exists(path)) + && (!mode.has(Readable) || Files.isReadable(path)) + && (!mode.has(Writable) || Files.isWritable(path)) + && (!mode.has(Executable) || Files.isExecutable(path)); + } catch(e:Throwable) { + throw new FsException(CustomError(e.toString()), path); + } + }, + callback + ); + } + + static public function isDirectory(path:FilePath, callback:Callback):Void { + pool.runFor( + () -> { + try { + Files.isDirectory(path); + } catch(e:Throwable) { + throw new FsException(CustomError(e.toString()), path); + } + }, + callback + ); + } + + static public function isFile(path:FilePath, callback:Callback):Void { + pool.runFor( + () -> { + try { + Files.isRegularFile(path); + } catch(e:Throwable) { + throw new FsException(CustomError(e.toString()), path); + } + }, + callback + ); + } + + static public function setPermissions(path:FilePath, permissions:FilePermissions, callback:Callback):Void { + pool.runFor( + () -> { + try { + Files.setPosixFilePermissions(path, permissions); + NoData; + } catch(e:FileSystemException) { + throw new FsException(CustomError(e.getReason()), path); + } catch(e:Throwable) { + throw new FsException(CustomError(e.toString()), path); + } + }, + callback + ); + } + + static public function setOwner(path:FilePath, user:SystemUser, group:SystemGroup, callback:Callback):Void { + pool.runFor( + () -> { + try { + var attributes = Files.getFileAttributeView(path, javaClass(PosixFileAttributeView)); + attributes.setOwner(user); + attributes.setGroup(group); + NoData; + } catch(e:FileSystemException) { + throw new FsException(CustomError(e.getReason()), path); + } catch(e:Throwable) { + throw new FsException(CustomError(e.toString()), path); + } + }, + callback + ); + } + + static public function setLinkOwner(path:FilePath, user:SystemUser, group:SystemGroup, callback:Callback):Void { + pool.runFor( + () -> { + try { + var attributes = Files.getFileAttributeView(path, javaClass(PosixFileAttributeView), NOFOLLOW_LINKS); + attributes.setOwner(user); + attributes.setGroup(group); + NoData; + } catch(e:FileSystemException) { + throw new FsException(CustomError(e.getReason()), path); + } catch(e:Throwable) { + throw new FsException(CustomError(e.toString()), path); + } + }, + callback + ); + } + + static public function link(target:FilePath, path:FilePath, type:FileLink = SymLink, callback:Callback):Void { + pool.runFor( + () -> { + try { + switch type { + case HardLink: Files.createLink(path, target); + case SymLink: Files.createSymbolicLink(path, target); + } + NoData; + } catch(e:FileSystemException) { + throw new FsException(CustomError(e.getReason()), path); + } catch(e:Throwable) { + throw new FsException(CustomError(e.toString()), path); + } + }, + callback + ); + } + + static public function isLink(path:FilePath, callback:Callback):Void { + pool.runFor( + () -> { + try { + Files.isSymbolicLink(path); + } catch(e:Throwable) { + throw new FsException(CustomError(e.toString()), path); + } + }, + callback + ); + } + + static public function readLink(path:FilePath, callback:Callback):Void { + pool.runFor( + () -> { + try { + Files.readSymbolicLink(path); + } catch(e:FileSystemException) { + throw new FsException(CustomError(e.getReason()), path); + } catch(e:Throwable) { + throw new FsException(CustomError(e.toString()), path); + } + }, + callback + ); + } + + static public function linkInfo(path:FilePath, callback:Callback):Void { + pool.runFor( + () -> { + try { + Files.readAttributes(path, javaClass(PosixFileAttributes), NOFOLLOW_LINKS); + } catch(e:FileSystemException) { + throw new FsException(CustomError(e.getReason()), path); + } catch(e:Throwable) { + throw new FsException(CustomError(e.toString()), path); + } + }, + callback + ); + } + + static public function copyFile(source:FilePath, destination:FilePath, overwrite:Bool = true, callback:Callback):Void { + pool.runFor( + () -> { + try { + var options = overwrite + ? NativeArray.make((cast NOFOLLOW_LINKS:CopyOption), (cast REPLACE_EXISTING:CopyOption)) + : NativeArray.make((cast NOFOLLOW_LINKS:CopyOption)); + Files.copy(source, destination, ...options); + NoData; + } catch(e:FileSystemException) { + throw new FsException(CustomError(e.getReason()), source); + } catch(e:Throwable) { + throw new FsException(CustomError(e.toString()), source); + } + }, + callback + ); + } + + static public function resize(path:FilePath, newSize:Int, callback:Callback):Void { + pool.runFor( + () -> { + try { + var f = new RandomAccessFile((path:JPath).toFile(), 'rw'); + f.setLength(newSize); + f.close(); + NoData; + } catch(e:FileSystemException) { + throw new FsException(CustomError(e.getReason()), path); + } catch(e:Throwable) { + throw new FsException(CustomError(e.toString()), path); + } + }, + callback + ); + } + + static public function setTimes(path:FilePath, accessTime:Int, modificationTime:Int, callback:Callback):Void { + pool.runFor( + () -> { + try { + var attributes = Files.getFileAttributeView(path, javaClass(PosixFileAttributeView)); + attributes.setTimes(FileTime.from(modificationTime, SECONDS), FileTime.from(accessTime, SECONDS), @:nullSafety(Off) (null:FileTime)); + NoData; + } catch(e:FileSystemException) { + throw new FsException(CustomError(e.getReason()), path); + } catch(e:Throwable) { + throw new FsException(CustomError(e.toString()), path); + } + }, + callback + ); + } + + static public function realPath(path:FilePath, callback:Callback):Void { + pool.runFor( + () -> { + try { + (path:JPath).toRealPath(); + } catch(e:FileSystemException) { + throw new FsException(CustomError(e.getReason()), path); + } catch(e:Throwable) { + throw new FsException(CustomError(e.toString()), path); + } + }, + callback + ); + } + + static function hxOpenFlagToJavaOption(flag:FileOpenFlag):NativeArray { + return switch flag { + case Append: cast NativeArray.make(CREATE, WRITE, APPEND); + case Read: cast NativeArray.make(READ); + case ReadWrite: cast NativeArray.make(READ, WRITE); + case Write: cast NativeArray.make(CREATE, WRITE, TRUNCATE_EXISTING); + case WriteX: cast NativeArray.make(CREATE_NEW, WRITE); + case WriteRead: cast NativeArray.make(CREATE, WRITE, READ, TRUNCATE_EXISTING); + case WriteReadX: cast NativeArray.make(CREATE_NEW, WRITE, READ); + case Overwrite: cast NativeArray.make(CREATE, WRITE); + case OverwriteRead: cast NativeArray.make(CREATE, WRITE, READ); + } + } + + static inline function javaClass(cls:Class):JClass { + return cast cls; + } +} \ No newline at end of file diff --git a/std/java/_std/asys/native/system/SystemGroup.hx b/std/java/_std/asys/native/system/SystemGroup.hx new file mode 100644 index 00000000000..561776ced74 --- /dev/null +++ b/std/java/_std/asys/native/system/SystemGroup.hx @@ -0,0 +1,11 @@ +package asys.native.system; + +private typedef NativeGroup = java.nio.file.attribute.GroupPrincipal; + +@:coreApi +abstract SystemGroup(NativeGroup) from NativeGroup to NativeGroup { + + public inline function toString():String { + return #if jvm this.toString() #else (cast this:java.lang.Object).toString() #end; + } +} \ No newline at end of file diff --git a/std/java/_std/asys/native/system/SystemUser.hx b/std/java/_std/asys/native/system/SystemUser.hx new file mode 100644 index 00000000000..ca28afddec2 --- /dev/null +++ b/std/java/_std/asys/native/system/SystemUser.hx @@ -0,0 +1,11 @@ +package asys.native.system; + +private typedef NativeUser = java.nio.file.attribute.UserPrincipal; + +@:coreApi +abstract SystemUser(NativeUser) from NativeUser to NativeUser { + + public inline function toString():String { + return #if jvm this.toString() #else (cast this:java.lang.Object).toString() #end; + } +} \ No newline at end of file diff --git a/std/java/_std/sys/thread/Thread.hx b/std/java/_std/sys/thread/Thread.hx index e76717b6045..d3ad6cd7f43 100644 --- a/std/java/_std/sys/thread/Thread.hx +++ b/std/java/_std/sys/thread/Thread.hx @@ -70,9 +70,10 @@ abstract Thread(ThreadImpl) from ThreadImpl { } function get_events():EventLoop { - if(this.events == null) - throw new NoEventLoopException(); - return this.events; + switch this.events { + case null: throw new NoEventLoopException(); + case events: return events; + } } @:keep //TODO: keep only if events are actually used diff --git a/std/lua/_std/asys/native/filesystem/Directory.hx b/std/lua/_std/asys/native/filesystem/Directory.hx new file mode 100644 index 00000000000..903896cc23f --- /dev/null +++ b/std/lua/_std/asys/native/filesystem/Directory.hx @@ -0,0 +1,44 @@ +package asys.native.filesystem; + +import haxe.NoData; +import lua.lib.luv.Handle; +import lua.lib.luv.fs.FileSystem as Fs; +import lua.lib.luv.Idle; +import lua.NativeStringTools; +import asys.native.filesystem.FileSystem.xpcall; +import asys.native.filesystem.FileSystem.ioError; + +/** + Represents a directory. +**/ +@:coreApi +class Directory { + /** The path of this directory as it was at the moment of opening the directory */ + public final path:FilePath; + + final dir:Handle; + + + function new(dir:Handle, path:FilePath) { + this.dir = dir; + this.path = path; + } + + public function next(callback:Callback>):Void { + Fs.readdir(dir, xpcall((e,entries) -> switch ioError(e) { + case null: + var entries = lua.Table.toArray(entries); + var result = [for(entry in entries) FilePath.ofString(entry.name)]; + callback.success(result); + case e: + callback.fail(new FsException(e, path)); + })); + } + + public function close(callback:Callback):Void { + Fs.closedir(dir, xpcall((e,_) -> switch ioError(e) { + case null: callback.success(NoData); + case e: callback.fail(new FsException(e, path)); + })); + } +} \ No newline at end of file diff --git a/std/lua/_std/asys/native/filesystem/File.hx b/std/lua/_std/asys/native/filesystem/File.hx new file mode 100644 index 00000000000..16831706f7d --- /dev/null +++ b/std/lua/_std/asys/native/filesystem/File.hx @@ -0,0 +1,151 @@ +package asys.native.filesystem; + +import haxe.Int64; +import haxe.io.Bytes; +import haxe.NoData; +import asys.native.IWritable; +import asys.native.IReadable; +import asys.native.system.SystemUser; +import asys.native.system.SystemGroup; +import lua.lib.luv.fs.FileDescriptor; +import lua.lib.luv.fs.FileSystem as Fs; +import lua.lib.luv.Idle; +import lua.NativeStringTools; +import asys.native.filesystem.FileSystem.xpcall; +import asys.native.filesystem.FileSystem.ioError; + +@:coreApi +class File { + public final path:FilePath; + + final fd:FileDescriptor; + final deleteOnClose:Bool; + + function new(fd:FileDescriptor, path:FilePath, deleteOnClose:Bool = false):Void { + this.fd = fd; + this.path = path; + this.deleteOnClose = deleteOnClose; + } + + public function write(position:Int64, buffer:Bytes, offset:Int, length:Int, callback:Callback):Void { + var error = null; + if(length < 0) { + error = 'Negative length'; + } else if(position < 0) { + error = 'Negative position'; + } else if(offset < 0 || offset > buffer.length) { + error = 'Offset out of buffer bounds'; + } + if(error != null) { + failAsync(callback, CustomError(error), path); + } else { + var maxLength = buffer.length - offset; + if(length > maxLength) + length = maxLength; + Fs.write(fd, buffer.getString(offset, length, RawNative), ofInt64(position), xpcall((e,bytesWritten) -> { + switch ioError(e) { + case null: callback.success(bytesWritten); + case e: callback.fail(new FsException(e, path)); + } + })); + } + } + + public function read(position:Int64, buffer:Bytes, offset:Int, length:Int, callback:Callback):Void { + var error = null; + if(length < 0) { + error = 'Negative length'; + } else if(position < 0) { + error = 'Negative position'; + } else if(offset < 0 || offset > buffer.length) { + error = 'Offset out of buffer bounds'; + } + if(error != null) { + failAsync(callback, CustomError(error), path); + } else { + var maxLength = buffer.length - offset; + if(length > maxLength) + length = maxLength; + Fs.read(fd, length, ofInt64(position), xpcall((e,data) -> switch ioError(e) { + case null: + var bytesRead = NativeStringTools.len(data); + for(i in 0...bytesRead) + buffer.set(offset + i, NativeStringTools.byte(data, i + 1)); + callback.success(bytesRead); + case e: + callback.fail(new FsException(e, path)); + })); + } + } + + public function flush(callback:Callback):Void { + Fs.fsync(fd, xpcall((e,_) -> switch ioError(e) { + case null: callback.success(NoData); + case e: callback.fail(new FsException(e, path)); + })); + } + + public function info(callback:Callback):Void { + Fs.fstat(fd, xpcall((e,stat) -> switch ioError(e) { + case null: callback.success(stat); + case e: callback.fail(new FsException(e, path)); + })); + } + + public function setPermissions(permissions:FilePermissions, callback:Callback):Void { + Fs.fchmod(fd, permissions, xpcall((e,_) -> switch ioError(e) { + case null: callback.success(NoData); + case e: callback.fail(new FsException(e, path)); + })); + } + + public function setOwner(user:SystemUser, group:SystemGroup, callback:Callback):Void { + Fs.fchown(fd, user, group, xpcall((e,_) -> switch ioError(e) { + case null: callback.success(NoData); + case e: callback.fail(new FsException(e, path)); + })); + } + + public function resize(newSize:Int, callback:Callback):Void { + Fs.ftruncate(fd, newSize, xpcall((e,_) -> switch ioError(e) { + case null: callback.success(NoData); + case e: callback.fail(new FsException(e, path)); + })); + } + + public function setTimes(accessTime:Int, modificationTime:Int, callback:Callback):Void { + Fs.futime(fd, accessTime, modificationTime, xpcall((e,_) -> switch ioError(e) { + case null: callback.success(NoData); + case e: callback.fail(new FsException(e, path)); + })); + } + + // public function lock(mode:FileLock = Exclusive, wait:Bool = true, callback:Callback):Void { + // throw new NotImplementedException(); + // } + + public function close(callback:Callback):Void { + if(deleteOnClose) + Fs.unlink(path, (_,_) -> {}); + Fs.close(fd, xpcall((e,_) -> switch ioError(e) { + case null | BadFile: callback.success(NoData); + case e: callback.fail(new FsException(e, path)); + })); + } + + //TODO: convert Int64 to lua's number without losing high bits. + static inline function ofInt64(i64:haxe.Int64):Int { + return Int64.toInt(i64); + } + + function failAsync(callback:Callback, error:IoErrorType, path:FilePath):Void { + var idle = new Idle(); + idle.start(() -> { + lua.Lua.xpcall(() -> { + idle.stop(() -> {}); + idle.close(); + callback.fail(new FsException(error, path)); + }, untyped __lua__('_hx_error')); + }); + } +} \ No newline at end of file diff --git a/std/lua/_std/asys/native/filesystem/FileInfo.hx b/std/lua/_std/asys/native/filesystem/FileInfo.hx new file mode 100644 index 00000000000..0b1f11499a9 --- /dev/null +++ b/std/lua/_std/asys/native/filesystem/FileInfo.hx @@ -0,0 +1,61 @@ +package asys.native.filesystem; + +import asys.native.system.SystemUser; +import asys.native.system.SystemGroup; + +private typedef NativeInfo = lua.lib.luv.fs.FileSystem.Stat; + +@:coreApi +abstract FileInfo(NativeInfo) from NativeInfo to NativeInfo { + public var accessTime(get,never):Int; + inline function get_accessTime():Int + return this.atime.sec; + + public var modificationTime(get,never):Int; + inline function get_modificationTime():Int + return this.mtime.sec; + + public var creationTime(get,never):Int; + inline function get_creationTime():Int + return this.ctime.sec; + + public var deviceNumber(get,never):Int; + inline function get_deviceNumber():Int + return this.dev; + + public var group(get,never):SystemGroup; + inline function get_group():SystemGroup + return this.gid; + + public var user(get,never):SystemUser; + inline function get_user():SystemUser + return this.uid; + + public var inodeNumber(get,never):Int; + inline function get_inodeNumber():Int + return this.ino; + + public var mode(get,never):FileMode; + inline function get_mode():FileMode + return this.mode; + + public var links(get,never):Int; + inline function get_links():Int + return this.nlink; + + public var deviceType(get,never):Int; + inline function get_deviceType():Int + return this.rdev; + + public var size(get,never):Int; + inline function get_size():Int + return this.size; + + public var blockSize(get,never):Int; + inline function get_blockSize():Int + return this.blksize; + + public var blocks(get,never):Int; + inline function get_blocks():Int + return this.blocks; +} \ No newline at end of file diff --git a/std/lua/_std/asys/native/filesystem/FilePath.hx b/std/lua/_std/asys/native/filesystem/FilePath.hx new file mode 100644 index 00000000000..5c7be465577 --- /dev/null +++ b/std/lua/_std/asys/native/filesystem/FilePath.hx @@ -0,0 +1,226 @@ +package asys.native.filesystem; + +import haxe.exceptions.ArgumentException; +import lua.lib.luv.fs.FileSystem as LuvFs; + +using lua.NativeStringTools; + +private abstract NativeString(String) from String to String { + public var length(get,never):Int; + inline function get_length():Int + return NativeStringTools.len(this); + + @:op([]) inline function get(i:Int) + return NativeStringTools.byte(this, i); + + public inline function sub(start:Int, ?end:Int) + return NativeStringTools.sub(this, start, end).match; +} + +private typedef NativeFilePath = NativeString; + +@:coreApi abstract FilePath(NativeFilePath) { + public static var SEPARATOR(get,never):String; + static inline function get_SEPARATOR():String { + return _SEPARATOR; + } + + static var _SEPARATOR:String; + + static function __init__():Void { + _SEPARATOR = Sys.systemName() == 'Windows' ? '\\' : '/'; + } + + @:noUsing + @:from public static inline function ofString(path:String):FilePath { + return new FilePath(path); + } + + @:from static function ofArray(parts:Array):FilePath { + if(parts.length == 0) + throw new ArgumentException('parts'); + var path = ofString(parts[0]); + for(i in 1...parts.length) + path = path.add(parts[i]); + return path; + } + + overload extern static public inline function createPath(path:String, ...appendices:String):FilePath { + return createPathImpl(path, ...appendices); + } + + overload extern static public inline function createPath(parts:Array):FilePath { + return ofArray(parts); + } + + @:native('createPath') + static function createPathImpl(path:String, ...appendices:String):FilePath { + var path = ofString(path); + for(p in appendices) + path = path.add(p); + return path; + } + + inline function new(s:NativeString) { + this = s; + } + + @:to public inline function toString():String { + return this; + } + + public function isAbsolute():Bool { + if(this == '') + return false; + if(this[1] == '/'.code) + return true; + if(SEPARATOR == '\\') { + return switch this[1] { + case '\\'.code: + true; + case c if(isDriveLetter(c)): + this.length > 1 && this[2] == ':'.code; + case _: + false; + } + } + return false; + } + + public function absolute():FilePath { + var result = if(this == '') { + Sys.getCwd(); + } else if(this[1] == '/'.code) { + this; + } else if(SEPARATOR == '\\') { + if(this[1] == '\\'.code) { + this; + } else if(this.length >= 2 && isDriveLetter(this[1]) && this[2] == ':'.code) { + if(this.length > 2 && isSeparator(this[3])) { + this; + } else { + try { + var driveCwd = LuvFs.realpath(this.sub(1, 2) + '.'); + driveCwd + SEPARATOR + this.sub(3); + } catch(_) { + throw new FsException(CustomError('Unable to get current working directory of drive ${this.sub(1,1)]}'), new FilePath(this)); + } + } + } else { + Sys.getCwd() + SEPARATOR + this; + } + } else { + Sys.getCwd() + SEPARATOR + this; + } + return new FilePath(result); + } + + public function normalize():FilePath { + var parts = if(SEPARATOR == '\\') { + StringTools.replace(this, '\\', '/').split('/'); + } else { + (this:String).split('/'); + } + var i = parts.length - 1; + var result = []; + var skip = 0; + while(i >= 0) { + switch parts[i] { + case '.' | '': + case '..': + ++skip; + case _ if(skip > 0): + --skip; + case part: + result.unshift(part); + } + --i; + } + for(i in 0...skip) + result.unshift('..'); + var result = ofString(result.join(SEPARATOR)); + return isAbsolute() && !result.isAbsolute() ? SEPARATOR + result : result; + } + + public function parent():Null { + var s = trimSlashes(this); + var i = s.length; + var isWin = SEPARATOR == '\\'; + while(i >= 1) { + switch s[i] { + case '/'.code: break; + case '\\'.code if(isWin): break; + case _: + } + --i; + } + //no directory in this path + return if(i < 1) { + null; + //this == '/' or this == '\' + } else if(i == 1 && s.length == 1) { + null; + } else { + new FilePath(s.sub(1, i)); + } + } + + public function name():Null { + var s = trimSlashes(this); + var i = s.length; + var isWin = SEPARATOR == '\\'; + while(i >= 1) { + if(s[i] == '/'.code || (isWin && s[i] == '\\'.code)) { + break; + } + --i; + } + //no directory in this path + return new FilePath(i < 1 ? s : s.sub(i + 1)); + } + + public function add(path:FilePath):FilePath { + if(path.isAbsolute() || this == '') + return path; + if(path == '') + return new FilePath(this); + if(SEPARATOR == '\\') { + var s = (cast path:NativeString); + if(s.length >= 2 && s[2] == ':'.code) { + if(this.length >= 2 && this[2] == ':'.code) { + if(s.sub(1,1).lower() != this.sub(1,1).lower()) { + throw new ArgumentException('path', 'Cannot combine paths on different drives'); + } + return new FilePath(trimSlashes(this) + SEPARATOR + s.sub(3)); + } else if(isSeparator(this[1])) { + return new FilePath(s.sub(1, 2) + trimSlashes(this) + SEPARATOR + s.sub(3)); + } + } + } + return new FilePath(trimSlashes(this) + SEPARATOR + path); + } + + static inline function isDriveLetter(c:Int):Bool { + return ('a'.code <= c && c <= 'z'.code) || ('A'.code <= c && c <= 'Z'.code); + } + + /** + * Trims all trailing slashes even if it's the last slash of a root path. + */ + static function trimSlashes(s:NativeString):NativeString { + var i = s.length; + while(i >= 1) { + switch s[i] { + case '/'.code: + case '\\'.code if(SEPARATOR == '\\'): + case _: break; + } + --i; + } + return i == s.length ? s : s.sub(1, i); + } + + static inline function isSeparator(char:Int):Bool { + return char == '/'.code || (SEPARATOR == '\\' && char == '\\'.code); + } +} \ No newline at end of file diff --git a/std/lua/_std/asys/native/filesystem/FileSystem.hx b/std/lua/_std/asys/native/filesystem/FileSystem.hx new file mode 100644 index 00000000000..3c0ba5e5fc4 --- /dev/null +++ b/std/lua/_std/asys/native/filesystem/FileSystem.hx @@ -0,0 +1,400 @@ +package asys.native.filesystem; + +import haxe.io.Bytes; +import haxe.NoData; +import asys.native.system.SystemUser; +import asys.native.system.SystemGroup; +import lua.Lib; +import lua.Table; +import lua.lib.luv.fs.FileSystem as Fs; +import lua.lib.luv.fs.FileSystem.NameType; +import lua.lib.luv.fs.FileSystem.AccessMode; + +using lua.NativeStringTools; + +@:coreApi +class FileSystem { + static public function openFile(path:FilePath, flag:FileOpenFlag, callback:Callback):Void { + Fs.open(path, luvOpenFlags(flag), FilePermissions.octal(0, 6, 4, 4), xpcall((e,fd) -> switch ioError(e) { + case null: callback.success(cast @:privateAccess new File(fd, path)); + case e: callback.fail(new FsException(e, path)); + })); + } + + static public function tempFile(callback:Callback):Void { + var pattern = switch lua.lib.luv.Os.tmpdir() { + case null: './XXXXXX'; + case dir: '$dir/XXXXXX'; + } + Fs.mkstemp(pattern, (e,fd,path) -> { + xpcall((e,fd) -> switch ioError(e) { + case null: callback.success(@:privateAccess new File(fd, path, true)); + case e: callback.fail(new FsException(e, '(unknown path)')); + })(e,fd); + }); + } + + static public function readBytes(path:FilePath, callback:Callback):Void { + readFile(path, s -> Bytes.ofString(s, RawNative), callback); + } + + static public function readString(path:FilePath, callback:Callback):Void { + readFile(path, s -> s, callback); + } + + static function readFile(path:FilePath, fn:(String)->T, callback:Callback):Void { + Fs.open(path, Fs.constants.O_RDONLY, 0, xpcall((e,fd) -> switch ioError(e) { + case null: + Fs.fstat(fd, xpcall(function(e,stat) { + switch ioError(e) { + case null: + Fs.read(fd, stat.size, 0, xpcall((e,data) -> switch ioError(e) { + case null: + Fs.close(fd, xpcall((_,_) -> callback.success(fn(data)))); + case e: + Fs.close(fd, xpcall((_,_) -> callback.fail(new FsException(e, path)))); + })); + case e: + Fs.close(fd, xpcall((_,_) -> callback.fail(new FsException(e, path)))); + } + })); + case e: + callback.fail(new FsException(e, path)); + })); + } + + static public function writeBytes(path:FilePath, data:Bytes, flag:FileOpenFlag = Write, callback:Callback):Void { + writeFile(path, data.getString(0, data.length, RawNative), flag, callback); + } + + static public function writeString(path:FilePath, text:String, flag:FileOpenFlag = Write, callback:Callback):Void { + writeFile(path, text, flag, callback); + } + + static function writeFile(path:FilePath, data:String, flag:FileOpenFlag, callback:Callback):Void { + Fs.open(path, luvOpenFlags(flag), FilePermissions.octal(0, 6, 4, 4), xpcall((e,fd) -> switch ioError(e) { + case null: + Fs.write(fd, data, 0, xpcall((e,bytesWritten) -> switch ioError(e) { + case null: + Fs.close(fd, xpcall((e,_) -> switch ioError(e) { + case null: callback.success(NoData); + case e: callback.fail(new FsException(e, path)); + })); + case e: + Fs.close(fd, xpcall((_,_) -> callback.fail(new FsException(e, path)))); + })); + case e: + callback.fail(new FsException(e, path)); + })); + } + + static public function openDirectory(path:FilePath, maxBatchSize:Int = 64, callback:Callback):Void { + Fs.opendir(path, xpcall((e,dir) -> switch ioError(e) { + case null: callback.success(@:privateAccess new Directory(dir, path)); + case e: callback.fail(new FsException(e, path)); + }), maxBatchSize); + } + + static public function listDirectory(path:FilePath, callback:Callback>):Void { + Fs.opendir(path, xpcall((e,dir) -> switch ioError(e) { + case null: + var result = []; + function collect(e, entries:Table) { + switch ioError(e) { + case null if(entries == null): + Fs.closedir(dir, xpcall((_,_) -> callback.success(result))); + case null: + //TODO: do this without intermediate array + var entries = Table.toArray(entries); + for(entry in entries) + result.push(entry.name); + Fs.readdir(dir, xpcall(collect)); + case e: + Fs.closedir(dir, xpcall((_,_) -> callback.fail(new FsException(e, path)))); + } + } + Fs.readdir(dir, xpcall(collect)); + case e: + callback.fail(new FsException(e, path)); + })); + } + + static public function createDirectory(path:FilePath, ?permissions:FilePermissions, recursive:Bool = false, callback:Callback):Void { + if(permissions == null) permissions = 511; + inline mkdir(path, permissions, recursive, (e,_) -> switch ioError(e) { + case null: callback.success(NoData); + case e: callback.fail(new FsException(e, path)); + }); + } + + static function mkdir(path:FilePath, permissions:FilePermissions, recursive:Bool, callback:(e:String,r:NoData)->Void):Void { + function mk(path:FilePath, callback:(e:String,r:NoData)->Void) { + Fs.mkdir(path, permissions, xpcall((e,_) -> switch ioError(e) { + case FileNotFound if(recursive): + switch path.parent() { + case null: + callback(e, NoData); + case parent: + mk(parent, (e,_) -> switch e { + case null: Fs.mkdir(path, permissions, xpcall(callback)); + case _: callback(e,NoData); + }); + } + case _: + callback(e,NoData); + })); + } + mk(path, callback); + } + + static public function uniqueDirectory(parentDirectory:FilePath, ?prefix:String, ?permissions:FilePermissions, recursive:Bool = false, callback:Callback):Void { + if(permissions == null) permissions = 511; + + var name = (prefix == null ? '' : prefix) + getRandomChar() + getRandomChar() + getRandomChar() + getRandomChar(); + var path = FilePath.ofString(parentDirectory.toString() + FilePath.SEPARATOR + name); + + function create(callback:(String,NoData)->Void) { + inline mkdir(path, permissions, recursive, (e,_) -> switch ioError(e) { + case FileExists: + path = path.toString() + getRandomChar(); + create(callback); + case _: + callback(e,NoData); + }); + } + create((e,_) -> switch ioError(e) { + case null: callback.success(path); + case e: callback.fail(new FsException(e, parentDirectory)); + }); + } + + static var __codes:Null>; + static function getRandomChar():String { + var codes:Array; + switch __codes { + case null: + var a = [for(c in '0'.code...'9'.code) String.fromCharCode(c)]; + for(c in 'A'.code...'Z'.code) a.push(String.fromCharCode(c)); + for(c in 'a'.code...'z'.code) a.push(String.fromCharCode(c)); + codes = __codes = a; + case a: + codes = a; + } + return codes[Std.random(codes.length)]; + } + + static public function move(oldPath:FilePath, newPath:FilePath, overwrite:Bool = true, callback:Callback):Void { + inline function move() { + Fs.rename(oldPath, newPath, xpcall((e,_) -> switch ioError(e) { + case null: callback.success(NoData); + case e: callback.fail(new FsException(e, oldPath)); + })); + } + if(overwrite) { + move(); + } else { + Fs.access(newPath, F_OK, xpcall((e,_) -> switch ioError(e) { + case null: callback.fail(new FsException(FileExists, newPath)); + case FileNotFound: move(); + case e: callback.fail(new FsException(e, newPath)); + })); + } + } + + static public function deleteFile(path:FilePath, callback:Callback):Void { + Fs.unlink(path, xpcall((e,_) -> switch ioError(e) { + case null: callback.success(NoData); + case e: callback.fail(new FsException(e, path)); + })); + } + + static public function deleteDirectory(path:FilePath, callback:Callback):Void { + Fs.rmdir(path, xpcall((e,_) -> switch ioError(e) { + case null: callback.success(NoData); + case e: callback.fail(new FsException(e, path)); + })); + } + + static public function info(path:FilePath, callback:Callback):Void { + Fs.stat(path, xpcall((e,stat) -> switch ioError(e) { + case null: callback.success(stat); + case e: callback.fail(new FsException(e, path)); + })); + } + + static public function check(path:FilePath, mode:FileAccessMode, callback:Callback):Void { + var flags:Int = F_OK; + if(mode.has(Executable)) flags = flags | X_OK; + if(mode.has(Writable)) flags = flags | W_OK; + if(mode.has(Readable)) flags = flags | R_OK; + Fs.access(path, flags, xpcall((e,ok) -> switch ioError(e) { + case null: callback.success(ok); + case e: callback.fail(new FsException(e, path)); + })); + } + + static public function isDirectory(path:FilePath, callback:Callback):Void { + Fs.stat(path, xpcall((e,stat) -> switch ioError(e) { + case null: callback.success((stat:FileInfo).mode.isDirectory()); + case FileNotFound: callback.success(false); + case e: callback.fail(new FsException(e, path)); + })); + } + + static public function isFile(path:FilePath, callback:Callback):Void { + Fs.stat(path, xpcall((e,stat) -> switch ioError(e) { + case null: callback.success((stat:FileInfo).mode.isFile()); + case FileNotFound: callback.success(false); + case e: callback.fail(new FsException(e, path)); + })); + } + + static public function setPermissions(path:FilePath, permissions:FilePermissions, callback:Callback):Void { + Fs.chmod(path, permissions, xpcall((e,_) -> switch ioError(e) { + case null: callback.success(NoData); + case e: callback.fail(new FsException(e, path)); + })); + } + + static public function setOwner(path:FilePath, user:SystemUser, group:SystemGroup, callback:Callback):Void { + Fs.chown(path, user, group, xpcall((e,_) -> switch ioError(e) { + case null: callback.success(NoData); + case e: callback.fail(new FsException(e, path)); + })); + } + + static public function setLinkOwner(path:FilePath, user:SystemUser, group:SystemGroup, callback:Callback):Void { + Fs.lchown(path, user, group, xpcall((e,_) -> switch ioError(e) { + case null: callback.success(NoData); + case e: callback.fail(new FsException(e, path)); + })); + } + + static public function link(target:FilePath, path:FilePath, type:FileLink = SymLink, callback:Callback):Void { + var cb:(String,Bool)->Void = xpcall((e,_) -> switch ioError(e) { + case null: callback.success(NoData); + case e: callback.fail(new FsException(e, path)); + }); + switch type { + case HardLink: + Fs.link(target, path, cb); + case SymLink: + Fs.symlink(target, path, null, cb); + } + } + + static public function isLink(path:FilePath, callback:Callback):Void { + Fs.lstat(path, xpcall((e,stat) -> switch ioError(e) { + case null: callback.success((stat:FileInfo).mode.isLink()); + case FileNotFound: callback.success(false); + case e: callback.fail(new FsException(e, path)); + })); + } + + static public function readLink(path:FilePath, callback:Callback):Void { + Fs.readlink(path, xpcall((e, r:String)-> switch ioError(e) { + case null: callback.success(r); + case e: callback.fail(new FsException(e, path)); + })); + } + + static public function linkInfo(path:FilePath, callback:Callback):Void { + Fs.lstat(path, xpcall((e,stat) -> switch ioError(e) { + case null: callback.success(stat); + case e: callback.fail(new FsException(e, path)); + })); + } + + static public function copyFile(source:FilePath, destination:FilePath, overwrite:Bool = true, callback:Callback):Void { + var flags = overwrite ? null : {excl:true}; + Fs.copyfile(source, destination, flags, xpcall((e,_) -> switch ioError(e) { + case null: callback.success(NoData); + case FileExists: callback.fail(new FsException(FileExists, destination)); + case e: callback.fail(new FsException(e, source)); + })); + } + + static public function resize(path:FilePath, newSize:Int, callback:Callback):Void { + Fs.open(path, Fs.constants.O_CREAT | Fs.constants.O_WRONLY, FilePermissions.octal(0, 6, 4, 4), xpcall((e,fd) -> switch ioError(e) { + case null: + Fs.ftruncate(fd, newSize, xpcall((e,r) -> switch ioError(e) { + case null: + Fs.close(fd, xpcall((e,_) -> switch ioError(e) { + case null: callback.success(NoData); + case e: callback.fail(new FsException(e, path)); + })); + case e: + callback.fail(new FsException(e, path)); + })); + case e: + callback.fail(new FsException(e, path)); + })); + } + + static public function setTimes(path:FilePath, accessTime:Int, modificationTime:Int, callback:Callback):Void { + Fs.utime(path, accessTime, modificationTime, xpcall((e,_) -> switch ioError(e) { + case null: callback.success(NoData); + case e: callback.fail(new FsException(e, path)); + })); + } + + static public function realPath(path:FilePath, callback:Callback):Void { + Fs.realpath(path, xpcall((e, r:String) -> switch ioError(e) { + case null: callback.success(r); + case e: callback.fail(new FsException(e, path)); + })); + } + + /** + Adds exceptions handling to callbacks. + Otherwise lua just prints `Uncaught Error: (null)` on unhandled exceptions. + **/ + @:allow(asys.native.filesystem) + static inline function xpcall(cb:(e:String, r:T)->Void):(e:String, r:T)->Void { + return (e, r) -> lua.Lua.xpcall(() -> cb(e, r), untyped __lua__('_hx_error')); + } + + static function luvOpenFlags(flag:FileOpenFlag):Int { + return switch flag { + case Append: Fs.constants.O_WRONLY | Fs.constants.O_APPEND | Fs.constants.O_CREAT; + case Read: Fs.constants.O_RDONLY; + case ReadWrite: Fs.constants.O_RDWR; + case Write: Fs.constants.O_WRONLY | Fs.constants.O_CREAT | Fs.constants.O_TRUNC; + case WriteX: Fs.constants.O_WRONLY | Fs.constants.O_CREAT | Fs.constants.O_EXCL; + case WriteRead: Fs.constants.O_RDWR | Fs.constants.O_CREAT | Fs.constants.O_TRUNC; + case WriteReadX: Fs.constants.O_RDWR | Fs.constants.O_CREAT | Fs.constants.O_EXCL; + case Overwrite: Fs.constants.O_WRONLY | Fs.constants.O_CREAT; + case OverwriteRead: Fs.constants.O_RDWR | Fs.constants.O_CREAT; + } + } + + @:allow(asys.native.filesystem) + static function ioError(e:Null):Null { + return if(e == null) + null + else + switch e.find(':', 1, true) { + case null: null; + case {begin:pos}: + if(pos < 1) + null + else + switch e.sub(1, pos - 1).match { + case 'ENOENT': FileNotFound; + case 'EEXIST': FileExists; + case 'ESRCH': ProcessNotFound; + case 'EACCES': AccessDenied; + case 'ENOTDIR': NotDirectory; + case 'EMFILE': TooManyOpenFiles; + case 'EPIPE': BrokenPipe; + case 'ENOTEMPTY': NotEmpty; + case 'EADDRNOTAVAIL': AddressNotAvailable; + case 'ECONNRESET': ConnectionReset; + case 'ETIMEDOUT': TimedOut; + case 'ECONNREFUSED': ConnectionRefused; + case 'EBADF': BadFile; + case _: CustomError(e); + } + } + } +} \ No newline at end of file diff --git a/std/neko/_std/asys/native/filesystem/File.hx b/std/neko/_std/asys/native/filesystem/File.hx new file mode 100644 index 00000000000..8dc4641ed3a --- /dev/null +++ b/std/neko/_std/asys/native/filesystem/File.hx @@ -0,0 +1,132 @@ +package asys.native.filesystem; + +import haxe.Int64; +import haxe.io.Bytes; +import haxe.NoData; +import haxe.exceptions.NotImplementedException; +import haxe.exceptions.NotSupportedException; +import asys.native.system.SystemUser; +import asys.native.system.SystemGroup; +import asys.native.filesystem.FileSystem as Fs; + +@:coreApi +class File { + public final path:FilePath; + + final handle:FileHandle; + final deleteOnClose:Bool; + + @:allow(asys.native.filesystem.FileSystem) + function new(f:FileHandle, path:FilePath, deleteOnClose:Bool):Void { + this.path = path; + this.handle = f; + this.deleteOnClose = deleteOnClose; + } + + public function write(position:Int64, buffer:Bytes, offset:Int, length:Int, callback:Callback):Void { + Fs.pool.runFor( + () -> { + try { + if(length < 0) + throw new FsException(CustomError('File.write(): negative length'), path); + if(position < 0) + throw new FsException(CustomError('File.write(): negative position'), path); + if(offset < 0 || offset > buffer.length) + throw new FsException(CustomError('File.write(): offset out of buffer bounds'), path); + Fs.file_seek(handle, Int64.toInt(position), 0); + Fs.file_write(handle, buffer.getData(), offset, length); + } catch(e:FsException) { + throw e; + } catch(e) { + throw new FsException(CustomError(e.toString()), path); + } + }, + callback + ); + } + + public function read(position:Int64, buffer:Bytes, offset:Int, length:Int, callback:Callback):Void { + Fs.pool.runFor( + () -> { + try { + if(length < 0) + throw new FsException(CustomError('File.read(): negative length'), path); + if(position < 0) + throw new FsException(CustomError('File.read(): negative position'), path); + if(offset < 0 || offset > buffer.length) + throw new FsException(CustomError('File.read(): offset out of buffer bounds'), path); + Fs.file_seek(handle, Int64.toInt(position), 0); + Fs.file_read(handle, buffer.getData(), offset, length); + } catch(e:FsException) { + throw e; + } catch(e) { + throw new FsException(CustomError(e.toString()), path); + } + }, + callback + ); + } + + public function flush(callback:Callback):Void { + Fs.pool.runFor( + () -> { + try { + Fs.file_flush(handle); + NoData; + } catch(e) { + throw new FsException(CustomError(e.toString()), path); + } + }, + callback + ); + } + + public function info(callback:Callback):Void { + Fs.pool.runFor( + () -> { + try { + var data = Fs.sys_stat(path); + data.atime = Std.int(data.atime / 1000); + data.ctime = Std.int(data.ctime / 1000); + data.mtime = Std.int(data.mtime / 1000); + cast data; + } catch(e) { + throw new FsException(CustomError(e.toString()), path); + } + }, + callback + ); + } + + public function setPermissions(permissions:FilePermissions, callback:Callback):Void { + throw NotSupportedException.field(); + } + + public function setOwner(user:SystemUser, group:SystemGroup, callback:Callback):Void { + throw NotSupportedException.field(); + } + + public function resize(newSize:Int, callback:Callback):Void { + throw NotSupportedException.field(); + } + + public function setTimes(accessTime:Int, modificationTime:Int, callback:Callback):Void { + throw NotSupportedException.field(); + } + + public function close(callback:Callback):Void { + Fs.pool.runFor( + () -> { + try { + Fs.file_close(handle); + if(deleteOnClose) + Fs.file_delete(path); + NoData; + } catch(e) { + throw new FsException(CustomError(e.toString()), path); + } + }, + callback + ); + } +} \ No newline at end of file diff --git a/std/neko/_std/asys/native/filesystem/FileHandle.hx b/std/neko/_std/asys/native/filesystem/FileHandle.hx new file mode 100644 index 00000000000..44e480934fa --- /dev/null +++ b/std/neko/_std/asys/native/filesystem/FileHandle.hx @@ -0,0 +1,3 @@ +package asys.native.filesystem; + +enum FileHandle {} \ No newline at end of file diff --git a/std/neko/_std/asys/native/filesystem/FilePath.hx b/std/neko/_std/asys/native/filesystem/FilePath.hx new file mode 100644 index 00000000000..08dfd8c716a --- /dev/null +++ b/std/neko/_std/asys/native/filesystem/FilePath.hx @@ -0,0 +1,204 @@ +package asys.native.filesystem; + +import haxe.exceptions.NotImplementedException; +import haxe.exceptions.ArgumentException; +import neko.NativeString; + +using StringTools; + +private typedef NativeFilePath = String; + +@:coreApi abstract FilePath(NativeFilePath) { + public static var SEPARATOR(get,never):String; + static inline function get_SEPARATOR():String { + return _SEPARATOR; + } + + static var _SEPARATOR:String; + + static function __init__():Void { + _SEPARATOR = neko.Lib.load("std","sys_string",0)() == NativeString.ofString('Windows') ? '\\' : '/'; + } + + overload extern static public inline function createPath(path:String, ...appendices:String):FilePath { + return createPathImpl(path, ...appendices); + } + + static function createPathImpl(path:String, ...appendices:String):FilePath { + var path = ofString(path); + for(p in appendices) + path = path.add(p); + return path; + } + + overload extern static public inline function createPath(parts:Array):FilePath { + return ofArray(parts); + } + + @:noUsing + @:from public static inline function ofString(path:String):FilePath { + return new FilePath(path); + } + + @:from static function ofArray(parts:Array):FilePath { + if(parts.length == 0) + throw new ArgumentException('parts'); + var path = ofString(parts[0]); + for(i in 1...parts.length) + path = path.add(parts[i]); + return path; + } + + @:to inline function toNativeString():NativeString { + return NativeString.ofString(this); + } + + inline function new(s:String) { + this = s; + } + + @:to public inline function toString():String { + return this; + } + + public function isAbsolute():Bool { + if(this == '') + return false; + if(this.fastCodeAt(0) == '/'.code) + return true; + if(SEPARATOR == '\\') { + return switch this.fastCodeAt(0) { + case '\\'.code: + true; + case c if(isDriveLetter(c)): + this.length > 1 && this.fastCodeAt(1) == ':'.code; + case _: + false; + } + } + return false; + } + + public function absolute():FilePath { + var result = if(this == '') { + Sys.getCwd(); + } else if(this.fastCodeAt(0) == '/'.code) { + this; + } else if(SEPARATOR == '\\') { + if(this.fastCodeAt(0) == '\\'.code) { + this; + } else if(this.length >= 2 && isDriveLetter(this.fastCodeAt(0)) && this.fastCodeAt(1) == ':'.code) { + if(this.length > 2 && isSeparator(this.fastCodeAt(2))) { + this; + } else { + try { + var driveCwd = @:privateAccess FileSystem.file_full_path(NativeString.ofString(this.charAt(0) + ':.')); + NativeString.toString(driveCwd) + SEPARATOR + this.substr(2); + } catch(_) { + throw new FsException(CustomError('Unable to get current working directory of drive ${this.charAt(0)]}'), this); + } + } + } else { + Sys.getCwd() + this; + } + } else { + Sys.getCwd() + this; + } + return result; + } + + public function normalize():FilePath { + var parts = if(SEPARATOR == '\\') { + this.replace('\\', '/').split('/'); + } else { + this.split('/'); + } + var i = parts.length - 1; + var result = []; + var skip = 0; + while(i >= 0) { + switch parts[i] { + case '.' | '': + case '..': + ++skip; + case _ if(skip > 0): + --skip; + case part: + result.unshift(part); + } + --i; + } + for(i in 0...skip) + result.unshift('..'); + var result = ofString(result.join(SEPARATOR)); + return isAbsolute() && !result.isAbsolute() ? SEPARATOR + result : result; + } + + public function parent():Null { + var s = trimSlashes(this); + var i = s.length; + var isWin = SEPARATOR == '\\'; + while(i >= 0) { + switch s.fastCodeAt(i) { + case '/'.code: break; + case '\\'.code if(isWin): break; + case _: + } + --i; + } + //no directory in this path + return if(i < 0) { + null; + //this == '/' or this == '\' + } else if(i == 0 && s.length == 1) { + null; + } else { + new FilePath(s.substr(0, i)); + } + } + + public function add(path:FilePath):FilePath { + if(path.isAbsolute() || this == '') + return path; + if(path == '') + return this; + if(SEPARATOR == '\\') { + var s = path.toString(); + if(s.length >= 2 && s.fastCodeAt(1) == ':'.code) { + if(this.length >= 2 && this.fastCodeAt(1) == ':'.code) { + if(s.substr(0, 1).toLowerCase() != this.substr(0, 1).toLowerCase()) { + throw new ArgumentException('path', 'Cannot combine paths on different drives'); + } + return trimSlashes(this) + SEPARATOR + s.substr(2); + } else if(isSeparator(this.fastCodeAt(0))) { + return s.substr(0, 2) + trimSlashes(this) + SEPARATOR + s.substr(2); + } + } + } + return trimSlashes(this) + SEPARATOR + path; + } + + static inline function isDriveLetter(c:Int):Bool { + return ('a'.code <= c && c <= 'z'.code) || ('A'.code <= c && c <= 'Z'.code); + } + + /** + * Trims all trailing slashes even if it's the last slash of a root path. + */ + static function trimSlashes(s:String):String { + var i = s.length - 1; + while(i >= 0) { + switch s.fastCodeAt(i) { + case '/'.code: + case '\\'.code if(SEPARATOR == '\\'): + case _: break; + } + --i; + } + return i == s.length - 1 ? s : s.substr(0, i + 1); + } + + static inline function isSeparator(char:Int):Bool { + return char == '/'.code || (SEPARATOR == '\\' && char == '\\'.code); + } +} \ No newline at end of file diff --git a/std/neko/_std/asys/native/filesystem/FileSystem.hx b/std/neko/_std/asys/native/filesystem/FileSystem.hx new file mode 100644 index 00000000000..315c60c734f --- /dev/null +++ b/std/neko/_std/asys/native/filesystem/FileSystem.hx @@ -0,0 +1,462 @@ +package asys.native.filesystem; + +import haxe.io.Bytes; +import haxe.NoData; +import haxe.Exception; +import haxe.exceptions.NotImplementedException; +import haxe.exceptions.NotSupportedException; +import asys.native.system.SystemUser; +import asys.native.system.SystemGroup; +import sys.thread.ElasticThreadPool; +import neko.Lib; + +using neko.NativeString; + +private typedef FileStat = { + var gid:Int; + var uid:Int; + var atime:Float; + var mtime:Float; + var ctime:Float; + var size:Int; + var dev:Int; + var ino:Int; + var nlink:Int; + var rdev:Int; + var mode:Int; +} + +@:coreApi +@:allow(asys.native.filesystem) +class FileSystem { + static final sys_exists:(NativeString)->Bool = Lib.load("std", "sys_exists", 1); + static final file_delete:(NativeString)->Void = Lib.load("std", "file_delete", 1); + static final sys_rename:(NativeString, NativeString)->Void = Lib.load("std", "sys_rename", 2); + static final sys_stat:(NativeString)->FileStat = Lib.load("std", "sys_stat", 1); + static final sys_file_type:(NativeString)->NativeString = Lib.load("std", "sys_file_type", 1); + static final sys_create_dir:(path:NativeString,mode:Int)->Void = Lib.load("std", "sys_create_dir", 2); + static final sys_remove_dir:(NativeString)->Void = Lib.load("std", "sys_remove_dir", 1); + static final sys_read_dir:(NativeString)->Array = Lib.load("std", "sys_read_dir", 1); + static final file_full_path:(NativeString)->NativeString = Lib.load("std", "file_full_path", 1); + static final file_contents:(NativeString)->NativeString = neko.Lib.load("std", "file_contents", 1); + static final file_open:(path:NativeString, mode:NativeString)->FileHandle = neko.Lib.load("std", "file_open", 2); + static final file_close:(FileHandle)->Void = neko.Lib.load("std", "file_close", 1); + static final file_seek:(f:FileHandle, pos:Int, kind:Int)->Void = neko.Lib.load("std", "file_seek", 3); + static final file_tell:(FileHandle)->Int = neko.Lib.load("std", "file_tell", 1); + static final file_flush:(FileHandle)->Void = neko.Lib.load("std", "file_flush", 1); + static final file_write:(file:FileHandle, data:NativeString, pos:Int, length:Int)->Int = neko.Lib.load("std", "file_write", 4); + static final file_write_char = neko.Lib.load("std", "file_write_char", 2); + static final file_eof:(FileHandle)->Bool = neko.Lib.load("std", "file_eof", 1); + static final file_read:(f:FileHandle, buf:NativeString, pos:Int, length:Int)->Int = neko.Lib.load("std", "file_read", 4); + static final file_read_char:(FileHandle)->Int = neko.Lib.load("std", "file_read_char", 1); + + @:allow(asys.native.filesystem) + static final pool = new ElasticThreadPool(4); + + static public function openFile(path:FilePath, flag:FileOpenFlag, callback:Callback):Void { + pool.runFor( + () -> { + try { + cast new File(fopenHx(path, flag), path, false); + } catch(e) { + throw new FsException(CustomError(e.toString()), path); + } + }, + callback + ); + } + + static public function tempFile(callback:Callback):Void { + pool.runFor( + () -> { + try { + var name = getRandomChar() + getRandomChar() + getRandomChar() + getRandomChar(); + var dir = FilePath.ofString(Sys.getCwd()); + var path = dir.add(name); + while(sys_exists(path)) { + name += getRandomChar(); + } + cast new File(fopenHx(path, WriteRead), path, true); + } catch(e) { + throw new FsException(CustomError(e.toString()), '(unknown path)'); + } + }, + callback + ); + } + + static public function readBytes(path:FilePath, callback:Callback):Void { + pool.runFor( + () -> { + try { + Lib.bytesReference(file_contents(path).toString()); + } catch(e) { + throw new FsException(CustomError(e.toString()), path); + } + }, + callback + ); + } + + static public function readString(path:FilePath, callback:Callback):Void { + pool.runFor( + () -> { + try { + file_contents(path).toString(); + } catch(e) { + throw new FsException(CustomError(e.toString()), path); + } + }, + callback + ); + } + + static public function writeBytes(path:FilePath, data:Bytes, flag:FileOpenFlag = Write, callback:Callback):Void { + pool.runFor( + () -> inline writeToFile(path, data.getData(), flag), + callback + ); + } + + static function writeToFile(path:FilePath, data:NativeString, flag:FileOpenFlag):NoData { + var f = null; + try { + f = fopenHx(path, flag); + var length = data.length(); + var pos = 0; + while (length > 0) { + var bytesWritten = file_write(f, data, pos, length); + if (bytesWritten == 0) + throw new Exception('Blocked'); + pos += bytesWritten; + length -= bytesWritten; + } + file_close(f); + return NoData; + } catch(e) { + if(f != null) + try file_close(f) catch(_) {} + throw new FsException(CustomError(e.toString()), path); + } + } + + static public function writeString(path:FilePath, text:String, flag:FileOpenFlag = Write, callback:Callback):Void { + pool.runFor( + () -> inline writeToFile(path, NativeString.ofString(text), flag), + callback + ); + } + + /** + Open directory for listing. + **/ + static public function openDirectory(path:FilePath, maxBatchSize:Int = 64, callback:Callback):Void { + throw new NotImplementedException(); + } + + static public function listDirectory(path:FilePath, callback:Callback>):Void { + pool.runFor( + () -> { + try { + var list = sys_read_dir(path); + var result = []; + while (list != null) { + result.push(FilePath.ofString((list[0]:NativeString).toString())); + list = list[1]; + } + result; + } catch(e) { + throw new FsException(CustomError(e.toString()), path); + } + }, + callback + ); + } + + static public function createDirectory(path:FilePath, ?permissions:FilePermissions, recursive:Bool = false, callback:Callback):Void { + if(permissions == null) permissions = 511; + pool.runFor( + () -> { + try { + mkdir(path, permissions, recursive); + NoData; + } catch(e) { + throw new FsException(CustomError(e.toString()), path); + } + }, + callback + ); + } + + static function mkdir(path:FilePath, mode:Int, recursive:Bool):Void { + if(recursive) { + var parent = path.parent(); + if(!sys_exists(parent)) { + mkdir(parent, mode, recursive); + } + } + sys_create_dir(path, mode); + } + + static public function uniqueDirectory(parentDirectory:FilePath, ?prefix:String, ?permissions:FilePermissions, recursive:Bool = false, callback:Callback):Void { + if(permissions == null) permissions = 511; + pool.runFor( + () -> { + try { + prefix = (prefix == null ? '' : prefix) + getRandomChar() + getRandomChar() + getRandomChar() + getRandomChar(); + var path = parentDirectory.add(prefix); + while(sys_exists(path)) { + prefix += getRandomChar(); + path = parentDirectory.add(prefix); + } + mkdir(path, permissions, recursive); + path; + } catch(e) { + throw new FsException(CustomError(e.toString()), parentDirectory); + } + }, + callback + ); + } + + static var __codes:Null>; + static function getRandomChar():String { + switch __codes { + case null: + var a = [for(c in '0'.code...'9'.code) String.fromCharCode(c)]; + for(c in 'A'.code...'Z'.code) a.push(String.fromCharCode(c)); + for(c in 'a'.code...'z'.code) a.push(String.fromCharCode(c)); + __codes = a; + return a[Std.random(a.length)]; + case a: + return a[Std.random(a.length)]; + } + } + + static public function move(oldPath:FilePath, newPath:FilePath, overwrite:Bool = true, callback:Callback):Void { + pool.runFor( + () -> { + try { + if(!overwrite && sys_exists(newPath)) + throw new FsException(FileExists, newPath); + sys_rename(oldPath, newPath); + NoData; + } catch(e:FsException) { + throw e; + } catch(e) { + throw new FsException(CustomError(e.toString()), oldPath); + } + }, + callback + ); + } + + static public function deleteFile(path:FilePath, callback:Callback):Void { + pool.runFor( + () -> { + try { + file_delete(path); + NoData; + } catch(e) { + throw new FsException(CustomError(e.toString()), path); + } + }, + callback + ); + } + + static public function deleteDirectory(path:FilePath, callback:Callback):Void { + pool.runFor( + () -> { + try { + sys_remove_dir(path); + NoData; + } catch(e) { + throw new FsException(CustomError(e.toString()), path); + } + }, + callback + ); + } + + static public function info(path:FilePath, callback:Callback):Void { + pool.runFor( + () -> { + try { + var data = sys_stat(path); + data.atime = Std.int(data.atime / 1000); + data.ctime = Std.int(data.ctime / 1000); + data.mtime = Std.int(data.mtime / 1000); + cast data; + } catch(e) { + throw new FsException(CustomError(e.toString()), path); + } + }, + callback + ); + } + + static public function check(path:FilePath, mode:FileAccessMode, callback:Callback):Void { + switch mode { + case Executable: + throw new NotSupportedException('File access mode "Executable" is not supported on neko'); + case Writable: + throw new NotSupportedException('File access mode "Writable" is not supported on neko'); + case Readable: + throw new NotSupportedException('File access mode "Readable" is not supported on neko'); + case Exists: + pool.runFor( + () -> { + try { + sys_exists(path); + } catch(e) { + throw new FsException(CustomError(e.toString()), path); + } + }, + callback + ); + } + } + + static public function isDirectory(path:FilePath, callback:Callback):Void { + pool.runFor( + () -> { + try { + sys_file_type(path).toString() == "dir"; + } catch(e) { + if(!sys_exists(path)) + false + else + throw new FsException(CustomError(e.toString()), path); + } + }, + callback + ); + } + + static public function isFile(path:FilePath, callback:Callback):Void { + pool.runFor( + () -> { + try { + sys_file_type(path).toString() == "file"; + } catch(e) { + if(!sys_exists(path)) + false + else + throw new FsException(CustomError(e.toString()), path); + } + }, + callback + ); + } + + static public function setPermissions(path:FilePath, permissions:FilePermissions, callback:Callback):Void { + throw NotSupportedException.field(); + } + + static public function setOwner(path:FilePath, user:SystemUser, group:SystemGroup, callback:Callback):Void { + throw NotSupportedException.field(); + } + + static public function setLinkOwner(path:FilePath, user:SystemUser, group:SystemGroup, callback:Callback):Void { + throw NotSupportedException.field(); + } + + static public function link(target:FilePath, path:FilePath, type:FileLink = SymLink, callback:Callback):Void { + throw NotSupportedException.field(); + } + + static public function isLink(path:FilePath, callback:Callback):Void { + throw NotSupportedException.field(); + // pool.runFor( + // () -> { + // try { + // var info:FileInfo = cast sys_stat(path); + // info.mode.isLink(); + // } catch(e) { + // if(!sys_exists(path)) + // false + // else + // throw new FsException(CustomError(e.toString()), path); + // } + // }, + // callback + // ); + } + + static public function readLink(path:FilePath, callback:Callback):Void { + throw NotSupportedException.field(); + } + + static public function linkInfo(path:FilePath, callback:Callback):Void { + throw NotSupportedException.field(); + // pool.runFor( + // () -> { + // try { + // var data = sys_stat(path); + // data.atime = Std.int(data.atime / 1000); + // data.ctime = Std.int(data.ctime / 1000); + // data.mtime = Std.int(data.mtime / 1000); + // cast data; + // } catch(e) { + // throw new FsException(CustomError(e.toString()), path); + // } + // }, + // callback + // ); + } + + static public function copyFile(source:FilePath, destination:FilePath, overwrite:Bool = true, callback:Callback):Void { + pool.runFor( + () -> { + try { + if(!overwrite && sys_exists(destination)) + throw new FsException(FileExists, destination); + sys.io.File.copy(source, destination); + NoData; + } catch(e:FsException) { + throw e; + } catch(e) { + throw new FsException(CustomError(e.toString()), source); + } + }, + callback + ); + } + + static public function resize(path:FilePath, newSize:Int, callback:Callback):Void { + throw NotSupportedException.field(); + } + + static public function setTimes(path:FilePath, accessTime:Int, modificationTime:Int, callback:Callback):Void { + throw NotSupportedException.field(); + } + + static public function realPath(path:FilePath, callback:Callback):Void { + pool.runFor( + () -> { + try { + FilePath.ofString(file_full_path(path).toString()); + } catch(e) { + throw new FsException(CustomError(e.toString()), path); + } + }, + callback + ); + } + + static function fopenHx(path:FilePath, flag:FileOpenFlag):FileHandle { + var flags = switch flag { + case Append: 'ab'; + case Read: 'rb'; + case ReadWrite: 'rb+'; + case Write: 'wb'; + case WriteX: 'wxb'; + case WriteRead: 'wb+'; + case WriteReadX: 'wxb+'; + case Overwrite: throw new NotSupportedException('"Overwrite" flag is not supported on neko'); + case OverwriteRead: throw new NotSupportedException('"OverwriteRead" flag is not supported on neko'); + } + return file_open(path, NativeString.ofString(flags)); + } +} \ No newline at end of file diff --git a/std/neko/_std/sys/thread/Thread.hx b/std/neko/_std/sys/thread/Thread.hx index 108ecdda1a9..cd551cdfb29 100644 --- a/std/neko/_std/sys/thread/Thread.hx +++ b/std/neko/_std/sys/thread/Thread.hx @@ -53,9 +53,10 @@ abstract Thread(ThreadImpl) from ThreadImpl { } function get_events():EventLoop { - if(this.events == null) - throw new NoEventLoopException(); - return this.events; + switch this.events { + case null: throw new NoEventLoopException(); + case events: return events; + } } @:keep diff --git a/std/php/Const.hx b/std/php/Const.hx index fe3bdd37017..e67392cf0e8 100644 --- a/std/php/Const.hx +++ b/std/php/Const.hx @@ -63,7 +63,6 @@ extern class Const { @see https://php.net/manual/en/dir.constants.php **/ static final DIRECTORY_SEPARATOR:String; - static final PATH_SEPARATOR:String; static final SCANDIR_SORT_ASCENDING:Int; static final SCANDIR_SORT_DESCENDING:Int; @@ -214,7 +213,6 @@ extern class Const { @see http://php.net/manual/en/function.feof.php **/ static final SEEK_SET:Int; - static final SEEK_CUR:Int; static final SEEK_END:Int; diff --git a/std/php/Global.hx b/std/php/Global.hx index c2a63847e0b..22216d07db6 100644 --- a/std/php/Global.hx +++ b/std/php/Global.hx @@ -93,6 +93,11 @@ extern class Global { **/ static function is_null(value:Dynamic):Bool; + /** + @see http://php.net/manual/en/function.is-resource.php + **/ + static function is_resource(value:Dynamic):Bool; + /** @see http://php.net/manual/en/function.is-subclass-of.php **/ @@ -660,6 +665,11 @@ extern class Global { **/ static function fclose(handle:Resource):Bool; + /** + @see http://php.net/manual/en/function.flock.php + **/ + static function flock(handle:Resource, operation:Int, ?wouldblock:Ref):Bool; + /** @see http://php.net/manual/en/function.feof.php **/ @@ -675,6 +685,16 @@ extern class Global { **/ static function ftell(handle:Resource):EitherType; + /** + @see http://php.net/manual/en/function.ftruncate.php + **/ + static function ftruncate(handle:Resource, size:Int):Bool; + + /** + @see http://php.net/manual/en/function.touch.php + **/ + static function touch(filename:String, ?time:Int, ?atime:Int):Bool; + /** @see http://php.net/manual/en/function.rewind.php **/ @@ -695,11 +715,6 @@ extern class Global { **/ static function fflush(handle:Resource):Bool; - /** - @see http://php.net/manual/en/function.flock.php - **/ - static function flock(handle:Resource, operation:Int, ?wouldblock:Ref):Bool; - /** @see http://php.net/manual/en/function.fwrite.php **/ @@ -765,6 +780,36 @@ extern class Global { **/ static function stat(filename:String):EitherType; + /** + @see http://php.net/manual/en/function.lstat.php + **/ + static function lstat(filename:String):EitherType; + + /** + @see http://php.net/manual/en/function.chmod.php + **/ + static function chmod(filename:String, mode:Int):Bool; + + /** + @see http://php.net/manual/en/function.chown.php + **/ + static function chown(filename:String, user:EitherType):Bool; + + /** + @see http://php.net/manual/en/function.chgrp.php + **/ + static function chgrp(filename:String, group:EitherType):Bool; + + /** + @see http://php.net/manual/en/function.lchown.php + **/ + static function lchown(filename:String, user:EitherType):Bool; + + /** + @see http://php.net/manual/en/function.lchgrp.php + **/ + static function lchgrp(filename:String, group:EitherType):Bool; + /** @see http://php.net/manual/en/function.fnmatch.php **/ @@ -825,6 +870,11 @@ extern class Global { **/ static function glob(pattern:String, flags:Int = 0):NativeArray; + /** + @see http://php.net/manual/en/function.scandir.php + **/ + static function scandir(directory:String, ?sorting_order:Int, ?context:Resource):EitherType>; + /** @see http://php.net/manual/en/function.opendir.php **/ @@ -1287,6 +1337,11 @@ extern class Global { **/ static function stream_get_contents(handle:Resource, maxlength:Int = -1, offset:Int = -1):EitherType; + /** + @see http://php.net/manual/en/function.stream-get-meta-data.php + **/ + static function stream_get_meta_data(handle:Resource):NativeArray; + /** @see http://php.net/manual/en/function.stream-socket-shutdown.php **/ @@ -1803,11 +1858,6 @@ extern class Global { **/ static function opcache_reset():Bool; - /** - @see http://php.net/manual/en/function.touch.php - **/ - static function touch(filename:String, ?time:Int, ?atime: Int):Bool; - /** @see http://php.net/manual/en/function.checkdnsrr.php **/ diff --git a/std/php/_std/asys/native/filesystem/Directory.hx b/std/php/_std/asys/native/filesystem/Directory.hx new file mode 100644 index 00000000000..d76f43972a5 --- /dev/null +++ b/std/php/_std/asys/native/filesystem/Directory.hx @@ -0,0 +1,58 @@ +package asys.native.filesystem; + +import haxe.NoData; +import php.Resource; +import php.Global.*; + +class Directory { + public final path:FilePath; + + final handle:Resource; + final maxBatchSize:Int; + + static inline function run(job:()->Void):Void { + inline haxe.EntryPoint.runInMainThread(job); + } + + function new(handle:Resource, path:FilePath, maxBatchSize:Int) { + this.handle = handle; + this.path = path; + this.maxBatchSize = maxBatchSize; + } + + public function next(callback:Callback>) { + run(() -> { + var result = try { + var entries = []; + while(entries.length < maxBatchSize) { + var entry = readdir(handle); + while(entry != false && (entry == '.' || entry == '..')) { + entry = readdir(handle); + } + if(entry != false) { + entries.push(@:privateAccess new FilePath(entry)); + } else { + break; + } + } + entries; + } catch(e:php.Exception) { + callback.fail(new FsException(CustomError(e.getMessage()), path)); + return; + } + callback.success(result); + }); + } + + public function close(callback:Callback) { + run(() -> { + try { + closedir(handle); + } catch(e:php.Exception) { + callback.fail(new FsException(CustomError(e.getMessage()), path)); + return; + } + callback.success(NoData); + }); + } +} \ No newline at end of file diff --git a/std/php/_std/asys/native/filesystem/File.hx b/std/php/_std/asys/native/filesystem/File.hx new file mode 100644 index 00000000000..1282c8d367a --- /dev/null +++ b/std/php/_std/asys/native/filesystem/File.hx @@ -0,0 +1,197 @@ +package asys.native.filesystem; + +import haxe.Int64; +import haxe.io.Bytes; +import haxe.NoData; +import asys.native.IWritable; +import asys.native.IReadable; +import php.Resource; +import php.Global.*; +import php.Const.*; +import php.Syntax; +import asys.native.system.SystemUser; +import asys.native.system.SystemGroup; + +class File { + + public final path:FilePath; + + final handle:Resource; + var isClosed:Bool = false; + + static inline function run(job:()->Void):Void { + inline haxe.EntryPoint.runInMainThread(job); + } + + function new(handle:Resource, path:FilePath) { + this.handle = handle; + this.path = path; + } + + public function write(position:Int64, buffer:Bytes, offset:Int, length:Int, callback:Callback) { + run(() -> { + var result = try { + if(length < 0) + throw new php.Exception('File.write(): negative length'); + if(position < 0) + throw new php.Exception('File.write(): negative position'); + if(offset < 0 || offset > buffer.length) + throw new php.Exception('File.write(): offset out of buffer bounds'); + if(fseek(handle, int64ToInt(position)) == 0) + switch fwrite(handle, buffer.getData().sub(offset, length)) { + case false: throw new php.Exception('Failed to read a file'); + case r: r; + } + else + throw new php.Exception('File.write(): Failed to set file position'); + } catch(e:php.Exception) { + callback.fail(new FsException(CustomError(e.getMessage()), path)); + return; + } + callback.success(result); + }); + } + + public function read(position:Int64, buffer:Bytes, offset:Int, length:Int, callback:Callback) { + run(() -> { + var result = try { + if(length < 0) + throw new php.Exception('File.read(): negative length'); + if(position < 0) + throw new php.Exception('File.read(): negative position'); + if(offset < 0 || offset > buffer.length) + throw new php.Exception('File.read(): offset out of buffer bounds'); + if(offset == buffer.length) + ('':haxe.extern.EitherType) + else if(fseek(handle, int64ToInt(position)) == 0) + fread(handle, length) + else + throw new php.Exception('File.read(): Failed to set file position'); + } catch(e:php.Exception) { + callback.fail(new FsException(CustomError(e.getMessage()), path)); + return; + } + switch result { + case false: + callback.fail(new FsException(CustomError('Failed to read a file'), path)); + case (_:String) => s: + var result = if(strlen(s) == 0) { + 0; + } else { + var bytesRead = try { + var bytes = Bytes.ofString(s); + buffer.blit(offset, bytes, 0, bytes.length); + bytes.length; + } catch(e) { + callback.fail(new FsException(CustomError('Failed to write to buffer: ${e.message}'), path, e)); + return; + } + bytesRead; + } + callback.success(result); + } + }); + } + + public function flush(callback:Callback) { + run(() -> { + var success = try { + fflush(handle); + } catch(e:php.Exception) { + callback.fail(new FsException(CustomError(e.getMessage()), path)); + return; + } + if(success) + callback.success(NoData) + else + callback.fail(new FsException(CustomError('Failed to flush a file'), path)); + }); + } + + public function info(callback:Callback) { + run(() -> { + var result = try { + fstat(handle); + } catch(e:php.Exception) { + callback.fail(new FsException(CustomError(e.getMessage()), path)); + return; + } + callback.success(@:privateAccess FileSystem.phpStatToHx(result)); + }); + } + + public function setPermissions(permissions:FilePermissions, callback:Callback) { + //PHP does not have `fchmod` + FileSystem.setPermissions(path, permissions, callback); + } + + public function setOwner(user:SystemUser, group:SystemGroup, callback:Callback) { + //PHP does not have `fchown` + FileSystem.setOwner(path, user, group, callback); + } + + public function resize(newSize:Int, callback:Callback) { + run(() -> { + var result = try { + var result = ftruncate(handle, newSize); + result; + } catch(e:php.Exception) { + callback.fail(new FsException(CustomError(e.getMessage()), path)); + return; + } + switch result { + case false: + callback.fail(new FsException(CustomError('Failed to resize file'), path)); + case _: + callback.success(NoData); + } + }); + } + + public function setTimes(accessTime:Int, modificationTime:Int, callback:Callback) { + //PHP does not have `utime` or `utimes` + FileSystem.setTimes(path, accessTime, modificationTime, callback); + } + + // public function lock(mode:FileLock = Exclusive, wait:Bool = true, callback:Callback) { + // run( + // () -> { + // var result = try { + // var mode = switch mode { + // case Exclusive: LOCK_EX; + // case Shared: LOCK_SH; + // case Unlock: LOCK_UN; + // } + // flock(handle, wait ? mode : mode | LOCK_NB); + // } catch(e:php.Exception) { + // throw new FsException(CustomError(e.getMessage()), path); + // } + // result; + // }, + // callback + // ); + // } + + public function close(callback:Callback) { + run(() -> { + var result = try { + is_resource(handle) ? fclose(handle) : true; + } catch(e:php.Exception) { + callback.fail(new FsException(CustomError(e.getMessage()), path)); + return; + } + if(result) + callback.success(NoData); + else + callback.fail(new FsException(CustomError('Failed to close a file'), path)); + }); + } + + inline function int64ToInt(i64:Int64):Int { + return if(PHP_INT_SIZE == 4) { + Int64.toInt(i64); + } else { + ((cast i64:{high:Int}).high << 32) | (cast i64:{low:Int}).low; + } + } +} diff --git a/std/php/_std/asys/native/filesystem/FilePath.hx b/std/php/_std/asys/native/filesystem/FilePath.hx new file mode 100644 index 00000000000..d560826e3f9 --- /dev/null +++ b/std/php/_std/asys/native/filesystem/FilePath.hx @@ -0,0 +1,186 @@ +package asys.native.filesystem; + +import php.*; +import php.Const.*; +import php.Global.*; +import php.Syntax.*; +import php.NativeArray; +import haxe.exceptions.ArgumentException; + +private typedef NativeFilePath = NativeString; + +@:coreApi abstract FilePath(NativeFilePath) to NativeString { + public static var SEPARATOR(get,never):String; + static inline function get_SEPARATOR():String { + return DIRECTORY_SEPARATOR; + } + + overload extern static public inline function createPath(path:String, ...appendices:String):FilePath { + return createPathImpl(path, ...appendices); + } + + @:native('createPath') + static function createPathImpl(path:String, ...appendices:String):FilePath { + var path = ofString(path); + for(p in appendices) + path = path.add(p); + return path; + } + + overload extern static public inline function createPath(parts:Array):FilePath { + return ofArray(parts); + } + + @:noUsing + @:from public static inline function ofString(path:String):FilePath { + return new FilePath(path); + } + + @:from static function ofArray(parts:Array):FilePath { + if(parts.length == 0) + throw new ArgumentException('parts'); + var path = ofString(parts[0]); + for(i in 1...parts.length) + path = path.add(parts[i]); + return path; + } + + inline function new(s:NativeString) { + this = s; + } + + @:to public inline function toString():String { + return this; + } + + public function isAbsolute():Bool { + if(this == '') + return false; + if(this[0] == '/') + return true; + if(SEPARATOR == '\\') { + if(this[0] == '\\') + return true; + if(preg_match('#^[a-zA-Z]:(\\\\|/)#', this)) + return true; + } + return false; + } + + public function absolute():FilePath { + inline function cwd():String { + var result = getcwd(); + if(result == false) + throw new FsException(CustomError('Unable to get current working directory'), this); + return result; + } + return if(this == '') { + cwd(); + } else if(this[0] == '/') { + this; + } else if(SEPARATOR == '\\') { + if(this[0] == '\\') { + this; + } else if(preg_match('/^[a-zA-Z]:/', this)) { + if(strlen(this) > 2 && isSeparator(this[2])) { + this; + } else { + try { + var driveCwd = realpath(substr(this, 0, 2) + '.'); + driveCwd + SEPARATOR + substr(this, 2); + } catch(e:php.Throwable) { + throw new FsException(CustomError('Unable to get current working directory of drive ${this[0]}'), this); + } + } + } else { + cwd() + SEPARATOR + this; + } + } else { + cwd() + SEPARATOR + this; + } + } + + public function normalize():FilePath { + var isWin = SEPARATOR == '\\'; + var parts:NativeIndexedArray = if(isWin) { + (preg_split('#\\\\|/#', this):NativeArray); + } else { + explode('/', this); + } + var i = count(parts) - 1; + var result = new NativeIndexedArray(); + var skip = 0; + while(i >= 0) { + switch parts[i] { + case '.' | '': + case '..': + ++skip; + case _ if(skip > 0): + --skip; + case part: + array_unshift(result, part); + } + --i; + } + for(i in 0...skip) + array_unshift(result, '..'); + var result = ofString(implode(SEPARATOR, result)); + return if(isAbsolute() && !result.isAbsolute()) { + isSeparator(this[0]) ? SEPARATOR + result : (result == '' ? parts[0] : result) + SEPARATOR; + } else if(isWin && strlen(this) >= 2 && this[1] == ':' && (strlen(result) < 2 || (result:NativeString)[1] != ':')) { + (substr(this, 0, 2):String) + (result:NativeString); + } else { + result; + } + } + + public function parent():Null { + var path = switch dirname(this) { + case '.': + strlen(this) > 1 && this[0] == '.' && isSeparator(this[1]) ? '.' : null; + case path: + if(trimSlashes(path) == trimSlashes(this)) { + null; + //relative to current drive. E.g. `dirname("C:dir")` returns `C:.` + } else if(SEPARATOR == '\\' && strlen(path) == 3 && preg_match('/^[a-zA-Z]:\\./', path)) { + strlen(this) >= 4 && this[2] == '.' && isSeparator(this[3]) ? path : substr(path, 0, 2); + } else { + path; + } + } + return new FilePath(path); + } + + public function name():FilePath { + return basename(this); + } + + public function add(path:FilePath):FilePath { + if(path.isAbsolute() || this == '') + return path; + if(path == '') + return this; + if(SEPARATOR == '\\') { + var s = (path:NativeString); + if(strlen(s) >= 2 && s[1] == ':') { + if(strlen(this) >= 2 && this[1] == ':') { + if(strtolower(s[0]) != strtolower(this[0])) { + throw new ArgumentException('path', 'Cannot combine paths on different drives'); + } + return trimSlashes(this) + SEPARATOR + (substr(s, 2):String); + } else if(isSeparator(this[0])) { + return (substr(s, 0, 2):String) + trimSlashes(this) + SEPARATOR + (substr(s, 2):String); + } + } + } + return trimSlashes(this) + SEPARATOR + path; + } + + static inline function isSeparator(char:String):Bool { + return char == '/' || (char == SEPARATOR); + } + + static inline function trimSlashes(s:NativeString):NativeString { + return rtrim(s, SEPARATOR == '\\' ? '/\\' : '/'); + } +} \ No newline at end of file diff --git a/std/php/_std/asys/native/filesystem/FileSystem.hx b/std/php/_std/asys/native/filesystem/FileSystem.hx new file mode 100644 index 00000000000..6a1c14ea82b --- /dev/null +++ b/std/php/_std/asys/native/filesystem/FileSystem.hx @@ -0,0 +1,530 @@ +package asys.native.filesystem; + +import haxe.io.Bytes; +import haxe.NoData; +import php.Global.*; +import php.Syntax; +import php.NativeArray; +import php.NativeIndexedArray; +import php.Resource; +import asys.native.system.SystemUser; +import asys.native.system.SystemGroup; + +@:coreApi +class FileSystem { + static inline function run(job:()->Void):Void { + inline haxe.EntryPoint.runInMainThread(job); + } + + static public function openFile(path:FilePath, flag:FileOpenFlag, callback:Callback):Void { + run(() -> { + var result = try { + cast @:privateAccess new File(fopenHx(path, flag), path); + } catch(e:php.Exception) { + callback.fail(new FsException(CustomError(e.getMessage()), path)); + return; + } + callback.success(result); + }); + } + + static public function tempFile(callback:Callback):Void { + run(() -> { + var result = try { + switch tmpfile() { + case false: + throw new php.Exception('Failed to create a temporary file'); + case fd: + @:privateAccess new File(fd, stream_get_meta_data(fd)['uri']); + } + } catch(e:php.Exception) { + callback.fail(new FsException(CustomError(e.getMessage()), '(unknown path)')); + return; + } + callback.success(result); + }); + } + + static public function readBytes(path:FilePath, callback:Callback):Void { + run(() -> { + var result = try { + switch file_get_contents(path) { + case false: + throw new php.Exception('Failed to read a file'); + case r: + Bytes.ofString(r); + } + } catch(e:php.Exception) { + callback.fail(new FsException(CustomError(e.getMessage()), path)); + return; + } + callback.success(result); + }); + } + + static public function readString(path:FilePath, callback:Callback):Void { + run(() -> { + var result = try { + switch file_get_contents(path) { + case false: + throw new php.Exception('Failed to read a file'); + case r: + r; + } + } catch(e:php.Exception) { + callback.fail(new FsException(CustomError(e.getMessage()), path)); + return; + } + callback.success(result); + }); + } + + static public function writeBytes(path:FilePath, data:Bytes, flag:FileOpenFlag = Write, callback:Callback):Void { + run(() -> { + var result = try { + var f = fopenHx(path, flag); + fwrite(f, data.getData().toString()); + fclose(f); + NoData.NoData; + } catch(e:php.Exception) { + callback.fail(new FsException(CustomError(e.getMessage()), path)); + return; + } + callback.success(result); + }); + } + + static public function writeString(path:FilePath, text:String, flag:FileOpenFlag = Write, callback:Callback):Void { + run(() -> { + var result = try { + var f = fopenHx(path, flag); + fwrite(f, text); + fclose(f); + NoData.NoData; + } catch(e:php.Exception) { + callback.fail(new FsException(CustomError(e.getMessage()), path)); + return; + } + callback.success(result); + }); + } + + static public function openDirectory(path:FilePath, maxBatchSize:Int = 64, callback:Callback):Void { + run(() -> { + var result = try { + switch opendir(path) { + case false: + throw new php.Exception('Failed to open a directory'); + case result: + @:privateAccess new Directory(result, path, maxBatchSize); + } + } catch(e:php.Exception) { + callback.fail(new FsException(CustomError(e.getMessage()), path)); + return; + } + callback.success(result); + }); + } + + static public function listDirectory(path:FilePath, callback:Callback>):Void { + run(() -> { + var result = try { + switch scandir(path) { + case false: + throw new php.Exception('Failed to list a directory'); + case (_:NativeIndexedArray) => list: + [for(item in list) if(item != '.' && item != '..') (item:FilePath)]; + } + } catch(e:php.Exception) { + callback.fail(new FsException(CustomError(e.getMessage()), path)); + return; + } + callback.success(result); + }); + } + + static public function createDirectory(path:FilePath, ?permissions:FilePermissions, recursive:Bool = false, callback:Callback):Void { + if(permissions == null) permissions = 511; + run(() -> { + var result = try { + if(mkdir(path, permissions, recursive)) + NoData.NoData + else + throw new php.Exception('Failed to create a directory'); + } catch(e:php.Exception) { + callback.fail(new FsException(CustomError(e.getMessage()), path)); + return; + } + callback.success(result); + }); + } + + static public function uniqueDirectory(parentDirectory:FilePath, ?prefix:String, ?permissions:FilePermissions, recursive:Bool = false, callback:Callback):Void { + if(permissions == null) permissions = 511; + run(() -> { + var result = try { + prefix = (prefix == null ? '' : prefix) + getRandomChar() + getRandomChar() + getRandomChar() + getRandomChar(); + var path:String = rtrim(parentDirectory, FilePath.SEPARATOR == '/' ? '/' : '\\/') + FilePath.SEPARATOR + prefix; + while(true) { + try { + if(mkdir(path, permissions, recursive)) + break + else + throw new php.Exception('Failed to create a directory'); + } catch(e:php.Exception) { + switch strpos(e.getMessage(), 'mkdir(): File exists') { + case false: + throw e; + case _: + path += getRandomChar(); + } + } + } + (path:FilePath); + } catch(e:php.Exception) { + callback.fail(new FsException(CustomError(e.getMessage()), parentDirectory)); + return; + } + callback.success(result); + }); + } + + static var __codes:Null>; + static function getRandomChar():String { + switch __codes { + case null: + var a = [for(c in '0'.code...'9'.code) String.fromCharCode(c)]; + for(c in 'A'.code...'Z'.code) a.push(String.fromCharCode(c)); + for(c in 'a'.code...'z'.code) a.push(String.fromCharCode(c)); + __codes = a; + return a[Std.random(a.length)]; + case a: + return a[Std.random(a.length)]; + } + } + + static public function move(oldPath:FilePath, newPath:FilePath, overwrite:Bool = true, callback:Callback):Void { + run(() -> { + if(!overwrite && file_exists(newPath)) { + callback.fail(new FsException(FileExists, newPath)); + return; + } + var result = try { + if(rename(oldPath, newPath)) + NoData + else + throw new php.Exception('Failed to move file or directory'); + } catch(e:php.Exception) { + callback.fail(new FsException(CustomError(e.getMessage()), oldPath)); + return; + } + callback.success(result); + }); + } + + static public function deleteFile(path:FilePath, callback:Callback):Void { + run(() -> { + var result = try { + if(unlink(path)) + NoData.NoData + else + throw new php.Exception('Failed to delete a file'); + } catch(e:php.Exception) { + callback.fail(new FsException(CustomError(e.getMessage()), path)); + return; + } + callback.success(result); + }); + } + + static public function deleteDirectory(path:FilePath, callback:Callback):Void { + run(() -> { + var result = try { + if(rmdir(path)) + NoData.NoData + else + throw new php.Exception('Failed to delete a file'); + } catch(e:php.Exception) { + callback.fail(new FsException(CustomError(e.getMessage()), path)); + return; + } + callback.success(result); + }); + } + + static public function info(path:FilePath, callback:Callback):Void { + run(() -> { + var result = try { + switch stat(path) { + case false: + throw new php.Exception('Failed to stat'); + case result: + @:privateAccess FileSystem.phpStatToHx(result); + } + } catch(e:php.Exception) { + callback.fail(new FsException(CustomError(e.getMessage()), path)); + return; + } + callback.success(result); + }); + } + + static public function check(path:FilePath, mode:FileAccessMode, callback:Callback):Void { + run(() -> { + var result = try { + (!mode.has(Exists) || file_exists(path)) + && (!mode.has(Readable) || is_readable(path)) + && (!mode.has(Writable) || is_writable(path)) + && (!mode.has(Executable) || is_executable(path)); + } catch(e:php.Exception) { + callback.fail(new FsException(CustomError(e.getMessage()), path)); + return; + } + callback.success(result); + }); + } + + static public function isDirectory(path:FilePath, callback:Callback):Void { + run(() -> { + var result = try { + is_dir(path); + } catch(e:php.Exception) { + callback.fail(new FsException(CustomError(e.getMessage()), path)); + return; + } + callback.success(result); + }); + } + + static public function isFile(path:FilePath, callback:Callback):Void { + run(() -> { + var result = try { + is_file(path); + } catch(e:php.Exception) { + callback.fail(new FsException(CustomError(e.getMessage()), path)); + return; + } + callback.success(result); + }); + } + + static public function setPermissions(path:FilePath, permissions:FilePermissions, callback:Callback):Void { + run(() -> { + var result = try { + if(chmod(path, permissions)) + NoData.NoData + else + throw new php.Exception('Failed to set permissions'); + } catch(e:php.Exception) { + callback.fail(new FsException(CustomError(e.getMessage()), path)); + return; + } + callback.success(result); + }); + } + + static public function setOwner(path:FilePath, user:SystemUser, group:SystemGroup, callback:Callback):Void { + run(() -> { + var result = try { + if(chown(path, user) && chgrp(path, group)) + NoData.NoData + else + throw new php.Exception('Failed to set owner'); + } catch(e:php.Exception) { + callback.fail(new FsException(CustomError(e.getMessage()), path)); + return; + } + callback.success(result); + }); + } + + static public function setLinkOwner(path:FilePath, user:SystemUser, group:SystemGroup, callback:Callback):Void { + run(() -> { + var result = try { + if(lchown(path, user) && lchgrp(path, group)) + NoData.NoData + else + throw new php.Exception('Failed to set owner'); + } catch(e:php.Exception) { + callback.fail(new FsException(CustomError(e.getMessage()), path)); + return; + } + callback.success(result); + }); + } + + static public function link(target:FilePath, path:FilePath, type:FileLink = SymLink, callback:Callback):Void { + run(() -> { + var result = try { + var success = switch type { + case SymLink: symlink(target, path); + case HardLink: php.Global.link(target, path); + } + if(success) + NoData.NoData + else + throw new php.Exception('Failed to create a link'); + } catch(e:php.Exception) { + callback.fail(new FsException(CustomError(e.getMessage()), path)); + return; + } + callback.success(result); + }); + } + + static public function isLink(path:FilePath, callback:Callback):Void { + run(() -> { + var result = try { + is_link(path); + } catch(e:php.Exception) { + callback.fail(new FsException(CustomError(e.getMessage()), path)); + return; + } + callback.success(result); + }); + } + + static public function readLink(path:FilePath, callback:Callback):Void { + run(() -> { + var result = try { + switch readlink(path) { + case false: + throw new php.Exception('Failed to read a link'); + case (_:String) => r: + if(FilePath.SEPARATOR == '\\' && !is_link(path)) + throw new php.Exception('Failed to read a link'); + (r:FilePath); + } + } catch(e:php.Exception) { + callback.fail(new FsException(CustomError(e.getMessage()), path)); + return; + } + callback.success(result); + }); + } + + static public function linkInfo(path:FilePath, callback:Callback):Void { + run(() -> { + var result = try { + switch lstat(path) { + case false: + throw new php.Exception('Failed to stat'); + case result: + @:privateAccess FileSystem.phpStatToHx(result); + } + } catch(e:php.Exception) { + callback.fail(new FsException(CustomError(e.getMessage()), path)); + return; + } + callback.success(result); + }); + } + + static public function copyFile(source:FilePath, destination:FilePath, overwrite:Bool = true, callback:Callback):Void { + run(() -> { + if(!overwrite && file_exists(destination)) { + callback.fail(new FsException(FileExists, destination)); + return; + } + var result = try { + if(copy(source, destination)) + NoData.NoData + else + throw new php.Exception('Failed to copy a file'); + } catch(e:php.Exception) { + callback.fail(new FsException(CustomError(e.getMessage()), source)); + return; + } + callback.success(result); + }); + } + + static public function resize(path:FilePath, newSize:Int, callback:Callback):Void { + run(() -> { + var result = try { + var f = fopen(path, 'a'); + var success = ftruncate(f, newSize); + fclose(f); + if(success) + NoData.NoData + else + throw new php.Exception('Failed to resize file'); + } catch(e:php.Exception) { + callback.fail(new FsException(CustomError(e.getMessage()), path)); + return; + } + callback.success(result); + }); + } + + static public function setTimes(path:FilePath, accessTime:Int, modificationTime:Int, callback:Callback):Void { + run(() -> { + var result = try { + if(file_exists(path)) { + if(touch(path, modificationTime, accessTime)) + NoData.NoData + else + throw new php.Exception('Failed to set file times'); + } else { + throw new php.Exception('No such file'); + } + } catch(e:php.Exception) { + callback.fail(new FsException(CustomError(e.getMessage()), path)); + return; + } + callback.success(result); + }); + } + + static public function realPath(path:FilePath, callback:Callback):Void { + run(() -> { + var result = try { + switch realpath(path) { + case false: + throw new php.Exception('Unable to resolve real path'); + case (_:String) => r: + (r:FilePath); + } + } catch(e:php.Exception) { + callback.fail(new FsException(CustomError(e.getMessage()), path)); + return; + } + callback.success(result); + }); + } + + static function fopenHx(file:String, flag:FileOpenFlag):Resource { + var f = switch flag { + case Append: fopen(file, 'a'); + case Read: fopen(file, 'r'); + case ReadWrite: fopen(file, 'r+'); + case Write: fopen(file, 'w'); + case WriteX: fopen(file, 'x'); + case WriteRead: fopen(file, 'w+'); + case WriteReadX: fopen(file, 'x+'); + case Overwrite: fopen(file, 'c'); + case OverwriteRead: fopen(file, 'c+'); + } + if(f == false) + throw new php.Exception('Cannot open file'); + return f; + } + + static function phpStatToHx(phpStat:NativeArray):FileInfo { + return { + atime: phpStat['atime'], + mtime: phpStat['mtime'], + ctime: phpStat['ctime'], + dev: phpStat['dev'], + gid: phpStat['gid'], + uid: phpStat['uid'], + ino: phpStat['ino'], + mode: phpStat['mode'], + nlink: phpStat['nlink'], + rdev: phpStat['rdev'], + size: phpStat['size'], + blksize: phpStat['blksize'], + blocks: phpStat['blocks'] + } + } +} \ No newline at end of file diff --git a/std/php/_std/haxe/io/Bytes.hx b/std/php/_std/haxe/io/Bytes.hx index 69179f021e3..805042f7a49 100644 --- a/std/php/_std/haxe/io/Bytes.hx +++ b/std/php/_std/haxe/io/Bytes.hx @@ -30,7 +30,7 @@ class Bytes { var b:BytesData; - function new(length:Int, b:BytesData):Void { + inline function new(length:Int, b:BytesData):Void { this.length = length; this.b = b; } diff --git a/std/python/_std/asys/native/filesystem/Directory.hx b/std/python/_std/asys/native/filesystem/Directory.hx new file mode 100644 index 00000000000..ba8d151f1bb --- /dev/null +++ b/std/python/_std/asys/native/filesystem/Directory.hx @@ -0,0 +1,55 @@ +package asys.native.filesystem; + +import haxe.NoData; +import python.lib.Os; +import python.internal.UBuiltins; +import python.Exceptions.StopIteration; +import asys.native.filesystem.FileSystem.pool; + +@:coreApi +class Directory { + public final path:FilePath; + + final iter:ScandirIterator; + final maxBatchSize:Int; + + @:allow(asys.native.filesystem) + function new(iter:ScandirIterator, path:FilePath, maxBatchSize:Int) { + this.path = path; + this.iter = iter; + this.maxBatchSize = maxBatchSize; + } + + public function next(callback:Callback>):Void { + pool.runFor( + () -> { + var result = []; + try { + while(result.length < maxBatchSize) + result.push(FilePath.ofString(iter.__next__().name)); + result; + } catch(_:StopIteration) { + result; + } catch(e) { + throw new FsException(CustomError(e.toString()), path); + } + }, + callback + ); + } + + public function close(callback:Callback):Void { + pool.runFor( + () -> { + try { + if(UBuiltins.hasattr(iter, 'close')) + iter.close(); + NoData; + } catch(e) { + throw new FsException(CustomError(e.toString()), path); + } + }, + callback + ); + } +} \ No newline at end of file diff --git a/std/python/_std/asys/native/filesystem/File.hx b/std/python/_std/asys/native/filesystem/File.hx new file mode 100644 index 00000000000..bcf3388cc2d --- /dev/null +++ b/std/python/_std/asys/native/filesystem/File.hx @@ -0,0 +1,169 @@ +package asys.native.filesystem; + +import python.lib.Os; +import python.Syntax; +import python.Tuple; +import python.lib.io.RawIOBase; +import haxe.Int64; +import haxe.io.Bytes; +import haxe.NoData; +import haxe.exceptions.NotImplementedException; +import haxe.exceptions.NotSupportedException; +import asys.native.system.SystemUser; +import asys.native.system.SystemGroup; +import asys.native.filesystem.FileSystem.pool; + +@:coreApi +class File { + public final path:FilePath; + + final handle:RawIOBase; + + @:allow(asys.native.filesystem.FileSystem) + function new(handle:RawIOBase, path:FilePath):Void { + this.path = path; + this.handle = handle; + } + + public function write(position:Int64, buffer:Bytes, offset:Int, length:Int, callback:Callback):Void { + pool.runFor( + () -> { + if(length < 0) + throw new FsException(CustomError('File.write(): negative length'), path); + if(position < 0) + throw new FsException(CustomError('File.write(): negative position'), path); + if(offset < 0 || offset > buffer.length) + throw new FsException(CustomError('File.write(): offset out of buffer bounds'), path); + try { + handle.seek(Int64.toInt(position), SeekSet); + handle.write(Syntax.code("{0}[{1}:{2}]", buffer.getData(), offset, offset + length)); + } catch(e) { + throw new FsException(CustomError(e.toString()), path); + } + }, + callback + ); + } + + public function read(position:Int64, buffer:Bytes, offset:Int, length:Int, callback:Callback):Void { + pool.runFor( + () -> { + if(length < 0) + throw new FsException(CustomError('File.read(): negative length'), path); + if(position < 0) + throw new FsException(CustomError('File.read(): negative position'), path); + if(offset < 0 || offset > buffer.length) + throw new FsException(CustomError('File.read(): offset out of buffer bounds'), path); + if(offset + length > buffer.length) + length = buffer.length - offset; + try { + handle.seek(Int64.toInt(position), SeekSet); + var b = handle.read(length); + Syntax.code("{0}.b[{1}:{1}+{2}] = {3}", buffer, offset, b.length, b); + b.length; + } catch(e) { + throw new FsException(CustomError(e.toString()), path); + } + }, + callback + ); + } + + public function flush(callback:Callback):Void { + pool.runFor( + () -> { + try { + handle.flush(); + NoData; + } catch(e) { + throw new FsException(CustomError(e.toString()), path); + } + }, + callback + ); + } + + public function info(callback:Callback):Void { + pool.runFor( + () -> { + try { + Os.stat(handle.fileno()); + } catch(e) { + throw new FsException(CustomError(e.toString()), path); + } + }, + callback + ); + } + + public function setPermissions(permissions:FilePermissions, callback:Callback):Void { + pool.runFor( + () -> { + try { + Os.chmod(handle.fileno(), permissions); + NoData; + } catch(e) { + throw new FsException(CustomError(e.toString()), path); + } + }, + callback + ); + } + + public function setOwner(user:SystemUser, group:SystemGroup, callback:Callback):Void { + pool.runFor( + () -> { + try { + if(Os.name == 'posix') + Os.chown(handle.fileno(), user, group); + NoData; + } catch(e) { + throw new FsException(CustomError(e.toString()), path); + } + }, + callback + ); + } + + public function resize(newSize:Int, callback:Callback):Void { + pool.runFor( + () -> { + try { + Os.ftruncate(handle.fileno(), newSize); + NoData; + } catch(e) { + throw new FsException(CustomError(e.toString()), path); + } + }, + callback + ); + } + + public function setTimes(accessTime:Int, modificationTime:Int, callback:Callback):Void { + pool.runFor( + () -> { + try { + Os.utime(handle.fileno(), Tuple2.make(accessTime, modificationTime)); + NoData; + } catch(e) { + throw new FsException(CustomError(e.toString()), path); + } + }, + callback + ); + } + + public function close(callback:Callback):Void { + pool.runFor( + () -> { + try { + handle.close(); + NoData; + } catch(e) { + throw new FsException(CustomError(e.toString()), path); + } + }, + callback + ); + } +} \ No newline at end of file diff --git a/std/python/_std/asys/native/filesystem/FileInfo.hx b/std/python/_std/asys/native/filesystem/FileInfo.hx new file mode 100644 index 00000000000..fdb9b3abc5f --- /dev/null +++ b/std/python/_std/asys/native/filesystem/FileInfo.hx @@ -0,0 +1,62 @@ +package asys.native.filesystem; + +import haxe.exceptions.NotImplementedException; +import asys.native.system.SystemUser; +import asys.native.system.SystemGroup; + +private typedef NativeInfo = python.lib.Os.Stat; + +@:coreApi +abstract FileInfo(NativeInfo) from NativeInfo to NativeInfo { + public var accessTime(get,never):Int; + inline function get_accessTime():Int + return this.st_atime; + + public var modificationTime(get,never):Int; + inline function get_modificationTime():Int + return this.st_mtime; + + public var creationTime(get,never):Int; + inline function get_creationTime():Int + return this.st_ctime; + + public var deviceNumber(get,never):Int; + inline function get_deviceNumber():Int + return this.st_dev; + + public var group(get,never):SystemGroup; + inline function get_group():SystemGroup + return this.st_gid; + + public var user(get,never):SystemUser; + inline function get_user():SystemUser + return this.st_uid; + + public var inodeNumber(get,never):Int; + inline function get_inodeNumber():Int + return this.st_ino; + + public var mode(get,never):FileMode; + inline function get_mode():FileMode + return this.st_mode; + + public var links(get,never):Int; + inline function get_links():Int + return this.st_nlink; + + public var deviceType(get,never):Int; + inline function get_deviceType():Int + return this.st_rdev; + + public var size(get,never):Int; + inline function get_size():Int + return this.st_size; + + public var blockSize(get,never):Int; + inline function get_blockSize():Int + return this.st_blksize; + + public var blocks(get,never):Int; + inline function get_blocks():Int + return this.st_blocks; +} \ No newline at end of file diff --git a/std/python/_std/asys/native/filesystem/FilePath.hx b/std/python/_std/asys/native/filesystem/FilePath.hx new file mode 100644 index 00000000000..671148caa9d --- /dev/null +++ b/std/python/_std/asys/native/filesystem/FilePath.hx @@ -0,0 +1,212 @@ +package asys.native.filesystem; + +import python.Syntax; +import python.lib.Os; +import python.lib.os.Path; +import haxe.exceptions.ArgumentException; + +using StringTools; + +private typedef NativeFilePath = String; + +@:coreApi abstract FilePath(NativeFilePath) { + public static var SEPARATOR(get,never):String; + static inline function get_SEPARATOR():String { + return Os.sep; + } + + overload extern static public inline function createPath(path:String, ...appendices:String):FilePath { + return createPathImpl(path, ...appendices); + } + + static function createPathImpl(path:String, ...appendices:String):FilePath { + var path = ofString(path); + for(p in appendices) + path = path.add(p); + return path; + } + + overload extern static public inline function createPath(parts:Array):FilePath { + return ofArray(parts); + } + + @:noUsing + @:from public static inline function ofString(path:String):FilePath { + return new FilePath(path); + } + + @:from static function ofArray(parts:Array):FilePath { + if(parts.length == 0) + throw new ArgumentException('parts'); + var path = ofString(parts[0]); + for(i in 1...parts.length) + path = path.add(parts[i]); + return path; + } + + inline function new(s:String) { + this = s; + } + + @:to public inline function toString():String { + return this; + } + + public function isAbsolute():Bool { + if(this == '') + return false; + if(this.fastCodeAt(0) == '/'.code) + return true; + if(SEPARATOR == '\\') { + return switch this.fastCodeAt(0) { + case '\\'.code: + true; + case c if(isDriveLetter(c)): + this.length > 1 && this.fastCodeAt(1) == ':'.code; + case _: + false; + } + } + return false; + } + + public function absolute():FilePath { + var result = if(this == '') { + Sys.getCwd(); + } else if(this.fastCodeAt(0) == '/'.code) { + this; + } else if(SEPARATOR == '\\') { + if(this.fastCodeAt(0) == '\\'.code) { + this; + } else if(this.length >= 2 && isDriveLetter(this.fastCodeAt(0)) && this.fastCodeAt(1) == ':'.code) { + if(this.length > 2 && isSeparator(this.fastCodeAt(2))) { + this; + } else { + try { + var driveCwd = Path.realpath(this.charAt(0) + ':.'); + driveCwd + SEPARATOR + this.substr(2); + } catch(_) { + throw new FsException(CustomError('Unable to get current working directory of drive ${this.charAt(0)]}'), this); + } + } + } else { + Sys.getCwd() + SEPARATOR + this; + } + } else { + Sys.getCwd() + SEPARATOR + this; + } + return result; + } + + public function normalize():FilePath { + var parts = if(SEPARATOR == '\\') { + this.replace('\\', '/').split('/'); + } else { + this.split('/'); + } + var i = parts.length - 1; + var result = []; + var skip = 0; + while(i >= 0) { + switch parts[i] { + case '.' | '': + case '..': + ++skip; + case _ if(skip > 0): + --skip; + case part: + result.unshift(part); + } + --i; + } + for(i in 0...skip) + result.unshift('..'); + var result = ofString(result.join(SEPARATOR)); + return isAbsolute() && !result.isAbsolute() ? SEPARATOR + result : result; + } + + public function parent():Null { + var s = trimSlashes(this); + var i = s.length; + var isWin = SEPARATOR == '\\'; + while(i >= 0) { + switch s.fastCodeAt(i) { + case '/'.code: break; + case '\\'.code if(isWin): break; + case _: + } + --i; + } + //no directory in this path + return if(i < 0) { + null; + //this == '/' or this == '\' + } else if(i == 0 && s.length == 1) { + null; + } else { + new FilePath(s.substr(0, i)); + } + } + + public function name():Null { + var s = trimSlashes(this); + var i = s.length; + var isWin = SEPARATOR == '\\'; + while(i >= 0) { + switch s.fastCodeAt(i) { + case '/'.code: break; + case '\\'.code if(isWin): break; + case _: + } + --i; + } + return new FilePath(i < 0 ? s : s.substr(i + 1)); + } + + public function add(path:FilePath):FilePath { + if(path.isAbsolute() || this == '') + return path; + if(path == '') + return this; + if(SEPARATOR == '\\') { + var s = path.toString(); + if(s.length >= 2 && s.fastCodeAt(1) == ':'.code) { + if(this.length >= 2 && this.fastCodeAt(1) == ':'.code) { + if(s.substr(0, 1).toLowerCase() != this.substr(0, 1).toLowerCase()) { + throw new ArgumentException('path', 'Cannot combine paths on different drives'); + } + return trimSlashes(this) + SEPARATOR + s.substr(2); + } else if(isSeparator(this.fastCodeAt(0))) { + return s.substr(0, 2) + trimSlashes(this) + SEPARATOR + s.substr(2); + } + } + } + return trimSlashes(this) + SEPARATOR + path; + } + + static inline function isDriveLetter(c:Int):Bool { + return ('a'.code <= c && c <= 'z'.code) || ('A'.code <= c && c <= 'Z'.code); + } + + /** + * Trims all trailing slashes even if it's the last slash of a root path. + */ + static inline function trimSlashes(s:String):String { + var slashes = SEPARATOR == '\\' ? '/\\' : '/'; + return Syntax.callField(s, "rstrip", slashes); + // var i = s.length - 1; + // while(i >= 0) { + // switch s.fastCodeAt(i) { + // case '/'.code: + // case '\\'.code if(SEPARATOR == '\\'): + // case _: break; + // } + // --i; + // } + // return i == s.length - 1 ? s : s.substr(0, i + 1); + } + + static inline function isSeparator(char:Int):Bool { + return char == '/'.code || (SEPARATOR == '\\' && char == '\\'.code); + } +} \ No newline at end of file diff --git a/std/python/_std/asys/native/filesystem/FileSystem.hx b/std/python/_std/asys/native/filesystem/FileSystem.hx new file mode 100644 index 00000000000..3a74d419fd3 --- /dev/null +++ b/std/python/_std/asys/native/filesystem/FileSystem.hx @@ -0,0 +1,490 @@ +package asys.native.filesystem; + +import python.lib.io.RawIOBase; +import python.lib.io.TextIOBase; +import python.lib.Builtins; +import python.lib.Os; +import python.lib.Tempfile; +import python.lib.os.Path; +import python.lib.Shutil; +import python.Tuple; +import python.Syntax; +import haxe.io.Bytes; +import haxe.NoData; +import haxe.Exception; +import haxe.exceptions.NotImplementedException; +import haxe.exceptions.NotSupportedException; +import asys.native.system.SystemUser; +import asys.native.system.SystemGroup; +import sys.thread.ElasticThreadPool; + +@:coreApi +@:allow(asys.native.filesystem) +class FileSystem { + + @:allow(asys.native.filesystem) + static final pool = new ElasticThreadPool(4); + + static public function openFile(path:FilePath, flag:FileOpenFlag, callback:Callback):Void { + pool.runFor( + () -> { + var mode = switch flag { + case Append: 'ab'; + case Read: 'rb'; + case _: 'r+b'; + } + try { + var f = cast Os.fdopen(Os.open(path, pyOpenFlags(flag, true)), mode); + cast new File(cast f, path); + } catch(e) { + throw new FsException(CustomError(e.toString()), path); + } + }, + callback + ); + } + + static public function tempFile(callback:Callback):Void { + pool.runFor( + () -> { + try { + var f = Tempfile.NamedTemporaryFile(); + new File(cast f, Syntax.code('{0}.name', f)); + } catch(e) { + throw new FsException(CustomError(e.toString()), '(unknown path)'); + } + }, + callback + ); + } + + static public function readBytes(path:FilePath, callback:Callback):Void { + pool.runFor( + () -> { + try { + var f:RawIOBase = cast Builtins.open(path, 'rb'); + var size = f.read(-1); + var data = haxe.io.Bytes.ofData(size); + f.close(); + data; + } catch(e) { + throw new FsException(CustomError(e.toString()), path); + } + }, + callback + ); + } + + static public function readString(path:FilePath, callback:Callback):Void { + pool.runFor( + () -> { + try { + var f:TextIOBase = cast Builtins.open(path, 'r', -1, "utf-8", null, ""); + var content = f.read(-1); + f.close(); + content; + } catch(e) { + throw new FsException(CustomError(e.toString()), path); + } + }, + callback + ); + } + + static public function writeBytes(path:FilePath, data:Bytes, flag:FileOpenFlag = Write, callback:Callback):Void { + pool.runFor( + () -> { + try { + var f:RawIOBase = cast Os.fdopen(Os.open(path, pyOpenFlags(flag, true)), 'r+b'); + f.write(data.getData()); + f.close(); + NoData; + } catch(e) { + throw new FsException(CustomError(e.toString()), path); + } + }, + callback + ); + } + + static public function writeString(path:FilePath, text:String, flag:FileOpenFlag = Write, callback:Callback):Void { + pool.runFor( + () -> { + try { + var f:TextIOBase = cast Os.fdopen(Os.open(path, pyOpenFlags(flag, false)), 'r+', -1, "utf-8", null, ""); + f.write(text); + f.close(); + NoData; + } catch(e) { + throw new FsException(CustomError(e.toString()), path); + } + }, + callback + ); + } + + static public function openDirectory(path:FilePath, maxBatchSize:Int = 64, callback:Callback):Void { + pool.runFor( + () -> { + try { + new Directory(Os.scandir(path), path, maxBatchSize); + } catch(e) { + throw new FsException(CustomError(e.toString()), path); + } + }, + callback + ); + } + + static public function listDirectory(path:FilePath, callback:Callback>):Void { + pool.runFor( + () -> { + try { + cast Os.listdir(path); + } catch(e) { + throw new FsException(CustomError(e.toString()), path); + } + }, + callback + ); + } + + static public function createDirectory(path:FilePath, ?permissions:FilePermissions, recursive:Bool = false, callback:Callback):Void { + if(permissions == null) permissions = 511; + pool.runFor( + () -> { + try { + if(recursive) + Os.makedirs(path, permissions) + else + Os.mkdir(path, permissions); + NoData; + } catch(e) { + throw new FsException(CustomError(e.toString()), path); + } + }, + callback + ); + } + + static public function uniqueDirectory(parentDirectory:FilePath, ?prefix:String, ?permissions:FilePermissions, recursive:Bool = false, callback:Callback):Void { + if(permissions == null) permissions = 511; + pool.runFor( + () -> { + try { + prefix = (prefix == null ? '' : prefix) + getRandomChar() + getRandomChar() + getRandomChar() + getRandomChar(); + var path = parentDirectory.add(prefix); + while(Path.exists(path)) { + prefix += getRandomChar(); + path = parentDirectory.add(prefix); + } + if(recursive) + Os.makedirs(path, permissions) + else + Os.mkdir(path, permissions); + path; + } catch(e) { + throw new FsException(CustomError(e.toString()), parentDirectory); + } + }, + callback + ); + } + + static var __codes:Null>; + static function getRandomChar():String { + switch __codes { + case null: + var a = [for(c in '0'.code...'9'.code) String.fromCharCode(c)]; + for(c in 'A'.code...'Z'.code) a.push(String.fromCharCode(c)); + for(c in 'a'.code...'z'.code) a.push(String.fromCharCode(c)); + __codes = a; + return a[Std.random(a.length)]; + case a: + return a[Std.random(a.length)]; + } + } + + static public function move(oldPath:FilePath, newPath:FilePath, overwrite:Bool = true, callback:Callback):Void { + pool.runFor( + () -> { + try { + if(overwrite) + Os.replace(oldPath, newPath) + else { + if(Os.name != 'nt' && Path.exists(newPath)) + throw new FsException(FileExists, newPath); + Os.rename(oldPath, newPath); + } + NoData; + } catch(e:FsException) { + throw e; + } catch(e) { + throw new FsException(CustomError(e.toString()), oldPath); + } + }, + callback + ); + } + + static public function deleteFile(path:FilePath, callback:Callback):Void { + pool.runFor( + () -> { + try { + Os.remove(path); + NoData; + } catch(e) { + throw new FsException(CustomError(e.toString()), path); + } + }, + callback + ); + } + + static public function deleteDirectory(path:FilePath, callback:Callback):Void { + pool.runFor( + () -> { + try { + Os.rmdir(path); + NoData; + } catch(e) { + throw new FsException(CustomError(e.toString()), path); + } + }, + callback + ); + } + + static public function info(path:FilePath, callback:Callback):Void { + pool.runFor( + () -> { + try { + Os.stat(path); + } catch(e) { + throw new FsException(CustomError(e.toString()), path); + } + }, + callback + ); + } + + static public function check(path:FilePath, mode:FileAccessMode, callback:Callback):Void { + pool.runFor( + () -> { + try { + var checks = 0; + if(mode.has(Exists)) checks = checks | Os.F_OK; + if(mode.has(Executable)) checks = checks | Os.X_OK; + if(mode.has(Writable)) checks = checks | Os.W_OK; + if(mode.has(Readable)) checks = checks | Os.R_OK; + Os.access(path, checks); + } catch(e) { + throw new FsException(CustomError(e.toString()), path); + } + }, + callback + ); + } + + static public function isDirectory(path:FilePath, callback:Callback):Void { + pool.runFor( + () -> { + try { + Path.isdir(path); + } catch(e) { + throw new FsException(CustomError(e.toString()), path); + } + }, + callback + ); + } + + static public function isFile(path:FilePath, callback:Callback):Void { + pool.runFor( + () -> { + try { + Path.isfile(path); + } catch(e) { + throw new FsException(CustomError(e.toString()), path); + } + }, + callback + ); + } + + static public function setPermissions(path:FilePath, permissions:FilePermissions, callback:Callback):Void { + pool.runFor( + () -> { + try { + Os.chmod(path, permissions); + NoData; + } catch(e) { + throw new FsException(CustomError(e.toString()), path); + } + }, + callback + ); + } + + static public function setOwner(path:FilePath, user:SystemUser, group:SystemGroup, callback:Callback):Void { + pool.runFor( + () -> { + try { + if(Os.name == 'posix') + Os.chown(path, user, group); + NoData; + } catch(e) { + throw new FsException(CustomError(e.toString()), path); + } + }, + callback + ); + } + + static public function setLinkOwner(path:FilePath, user:SystemUser, group:SystemGroup, callback:Callback):Void { + pool.runFor( + () -> { + try { + if(Os.name == 'posix') + Os.lchown(path, user, group); + NoData; + } catch(e) { + throw new FsException(CustomError(e.toString()), path); + } + }, + callback + ); + } + + static public function link(target:FilePath, path:FilePath, type:FileLink = SymLink, callback:Callback):Void { + pool.runFor( + () -> { + try { + switch type { + case SymLink: Os.symlink(target, path); + case HardLink: Os.link(target, path); + } + NoData; + } catch(e) { + throw new FsException(CustomError(e.toString()), path); + } + }, + callback + ); + } + + static public function isLink(path:FilePath, callback:Callback):Void { + pool.runFor( + () -> { + try { + Path.islink(path); + } catch(e) { + throw new FsException(CustomError(e.toString()), path); + } + }, + callback + ); + } + + static public function readLink(path:FilePath, callback:Callback):Void { + pool.runFor( + () -> { + try { + FilePath.ofString(Os.readlink(path)); + } catch(e) { + throw new FsException(CustomError(e.toString()), path); + } + }, + callback + ); + } + + static public function linkInfo(path:FilePath, callback:Callback):Void { + pool.runFor( + () -> { + try { + Os.lstat(path); + } catch(e) { + throw new FsException(CustomError(e.toString()), path); + } + }, + callback + ); + } + + static public function copyFile(source:FilePath, destination:FilePath, overwrite:Bool = true, callback:Callback):Void { + pool.runFor( + () -> { + if(!overwrite && Path.exists(destination)) + throw new FsException(FileExists, destination); + try { + Shutil.copyfile(source, destination); + NoData; + } catch(e) { + throw new FsException(CustomError(e.toString()), source); + } + }, + callback + ); + } + + static public function resize(path:FilePath, newSize:Int, callback:Callback):Void { + pool.runFor( + () -> { + try { + var fd = Os.open(path, Os.O_WRONLY | Os.O_CREAT); + Os.ftruncate(fd, newSize); + Os.close(fd); + NoData; + } catch(e) { + throw new FsException(CustomError(e.toString()), path); + } + }, + callback + ); + } + + static public function setTimes(path:FilePath, accessTime:Int, modificationTime:Int, callback:Callback):Void { + pool.runFor( + () -> { + try { + Os.utime(path, Tuple2.make(accessTime, modificationTime)); + NoData; + } catch(e) { + throw new FsException(CustomError(e.toString()), path); + } + }, + callback + ); + } + + static public function realPath(path:FilePath, callback:Callback):Void { + pool.runFor( + () -> { + if(!Path.exists(path)) + throw new FsException(FileNotFound, path); + try { + FilePath.ofString(Path.realpath(path)); + } catch(e) { + throw new FsException(CustomError(e.toString()), path); + } + }, + callback + ); + } + + static function pyOpenFlags(flag:FileOpenFlag, binary:Bool):Int { + var flags = switch flag { + case Append: Os.O_WRONLY | Os.O_APPEND | Os.O_CREAT; + case Read: Os.O_RDONLY; + case ReadWrite: Os.O_RDWR; + case Write: Os.O_WRONLY | Os.O_CREAT | Os.O_TRUNC; + case WriteX: Os.O_WRONLY | Os.O_CREAT | Os.O_EXCL; + case WriteRead: Os.O_RDWR | Os.O_CREAT | Os.O_TRUNC; + case WriteReadX: Os.O_RDWR | Os.O_CREAT | Os.O_EXCL; + case Overwrite: Os.O_WRONLY | Os.O_CREAT; + case OverwriteRead: Os.O_RDWR | Os.O_CREAT; + } + return binary && Os.name == 'nt' ? Os.O_BINARY | flags : flags; + } +} \ No newline at end of file diff --git a/std/python/_std/sys/thread/Thread.hx b/std/python/_std/sys/thread/Thread.hx index 547e6eb25af..a1948ac58e6 100644 --- a/std/python/_std/sys/thread/Thread.hx +++ b/std/python/_std/sys/thread/Thread.hx @@ -57,9 +57,10 @@ abstract Thread(ThreadImpl) from ThreadImpl { } function get_events():EventLoop { - if(this.events == null) - throw new NoEventLoopException(); - return this.events; + switch this.events { + case null: throw new NoEventLoopException(); + case events: return events; + } } @:keep diff --git a/std/python/lib/Os.hx b/std/python/lib/Os.hx index bf8a9839d64..f2aefd1836e 100644 --- a/std/python/lib/Os.hx +++ b/std/python/lib/Os.hx @@ -25,6 +25,9 @@ package python.lib; import python.Exceptions.OSError; import python.Tuple; import python.Dict; +import python.lib.io.IOBase; +import python.lib.os.DirEntry; +import python.NativeIterator.NativeIteratorRaw; extern class Stat { var st_mode:Int; @@ -51,8 +54,33 @@ extern class Stat { @:optional var st_type:Int; } +typedef ScandirIterator = NativeIteratorRaw & { + function close():Void; // since Python 3.6 +} + @:pythonImport("os") extern class Os { + + static final O_RDONLY:Int; + static final O_WRONLY:Int; + static final O_RDWR:Int; + static final O_APPEND:Int; + static final O_CREAT:Int; + static final O_EXCL:Int; + static final O_TRUNC:Int; + static final O_BINARY:Int; + + static final F_OK:Int; + static final R_OK:Int; + static final W_OK:Int; + static final X_OK:Int; + + static final SEEK_SET:Int; + static final SEEK_CUR:Int; + static final SEEK_END:Int; + + static final name:String; + static var environ:Dict; static function putenv(name:String, value:String):Void; @@ -78,10 +106,15 @@ extern class Os { static function renames(oldName:String, newName:String):Void; + static function replace(src:String, dest:String):Void; + static function rmdir(path:String):Void; + @:overload(function (fd:Int):Stat {}) static function stat(path:String):Stat; + static function lstat(path:String):Stat; + static function fchdir(fd:FileDescriptor):Void; static function listdir(path:String = "."):Array; @@ -95,4 +128,41 @@ extern class Os { static function makedirs(path:String, mode:Int = 511 /* Oktal 777 */, exist_ok:Bool = false):Void; static function mkdir(path:String, mode:Int = 511 /* Oktal 777 */):Void; + + static function open(path:String, flags:Int, mode:Int = 511):Int; + + static function fdopen(fd:Int, ...args:Any):IOBase; + + static function close(fd:Int):Void; + + static function ftruncate(fd:Int, length:Int):Void; + + static function truncate(path:String, length:Int):Void; + + static function readlink(path:String):String; + + static function symlink(src:String, dst:String, target_is_directory:Bool = false):Void; + + static function link(src:String, dst:String):Void; + + @:overload(function (fd:Int, uid:Int, gid:Int):Void {}) + static function chown(path:String, uid:Int, gid:Int):Void; + + static function lchown(path:String, uid:Int, gid:Int):Void; + + static function fchown(fd:Int, uid:Int, gid:Int):Void; + + @:overload(function (fd:Int, mode:Int):Void {}) + static function chmod(path:String, mode:Int):Void; + + static function lchmod(path:String, mode:Int):Void; + + @:overload(function (fd:Int, ?times:Tuple2):Void {}) + static function utime(path:String, ?times:Tuple2):Void; + + static function access(path:String, mode:Int):Bool; + + static function lseek(fd:String, pos:Int, how:Int):Void; + + static function scandir(?path:String):ScandirIterator; } diff --git a/std/python/lib/Tempfile.hx b/std/python/lib/Tempfile.hx index fdb4d4a356f..47e743eacf9 100644 --- a/std/python/lib/Tempfile.hx +++ b/std/python/lib/Tempfile.hx @@ -22,7 +22,15 @@ package python.lib; +import python.lib.io.IOBase; + @:pythonImport("tempfile") extern class Tempfile { static function gettempdir():String; + + static function TemporaryFile(?mode:String, ?buffering:Int, ?encoding:String, + ?newline:String, ?suffix:String, ?prefix:String, ?dir:String):IOBase; + + static function NamedTemporaryFile(?mode:String, ?buffering:Int, ?encoding:String, + ?newline:String, ?suffix:String, ?prefix:String, ?dir:String):IOBase; } diff --git a/std/python/lib/os/DirEntry.hx b/std/python/lib/os/DirEntry.hx new file mode 100644 index 00000000000..fcaeaae0cb5 --- /dev/null +++ b/std/python/lib/os/DirEntry.hx @@ -0,0 +1,11 @@ +package python.lib.os; + +extern class DirEntry { + final name:String; + final path:String; + function inode():Int; + function is_file():Bool; + function is_dir():Bool; + function is_symlink():Bool; + function stat():python.lib.Os.Stat; +} \ No newline at end of file diff --git a/std/sys/thread/IThreadPool.hx b/std/sys/thread/IThreadPool.hx index 214cb243c8f..f54037ce461 100644 --- a/std/sys/thread/IThreadPool.hx +++ b/std/sys/thread/IThreadPool.hx @@ -25,6 +25,7 @@ package sys.thread; /** A thread pool interface. **/ +@:using(sys.thread.IThreadPool.ThreadPoolUtils) interface IThreadPool { /** Amount of alive threads in this pool. */ @@ -48,4 +49,26 @@ interface IThreadPool { Multiple calls to this method have no effect. **/ function shutdown():Void; +} + +class ThreadPoolUtils { + /** + Run `task` and then invoke `callback` with the outcome of the task. + + The callback will be invoked in the same thread `.runFor` was called. + The calling thread must have an event loop set up (see `sys.thread.Thread.events`) + **/ + static public function runFor(pool:IThreadPool, task:()->R, callback:haxe.Callback):Void { + var events = sys.thread.Thread.current().events; + events.promise(); + pool.run(() -> { + var result = try { + task(); + } catch(e) { + events.runPromised(() -> callback.fail(e)); + return; + } + events.runPromised(() -> callback.success(result)); + }); + } } \ No newline at end of file diff --git a/std/sys/thread/Thread.hx b/std/sys/thread/Thread.hx index 97eaeb69386..87413d91779 100644 --- a/std/sys/thread/Thread.hx +++ b/std/sys/thread/Thread.hx @@ -80,4 +80,4 @@ extern abstract Thread(ThreadImpl) from ThreadImpl { Run event loop of the current thread **/ private static function processEvents():Void; -} \ No newline at end of file +} diff --git a/tests/asys/.gitignore b/tests/asys/.gitignore new file mode 100644 index 00000000000..6008fadbfa6 --- /dev/null +++ b/tests/asys/.gitignore @@ -0,0 +1,3 @@ +bin/* +dump/* +test-data/temp/* \ No newline at end of file diff --git a/tests/asys/compile-cs.hxml b/tests/asys/compile-cs.hxml new file mode 100644 index 00000000000..eaf3718eea2 --- /dev/null +++ b/tests/asys/compile-cs.hxml @@ -0,0 +1,3 @@ +compile-each.hxml + +--cs bin/cs \ No newline at end of file diff --git a/tests/asys/compile-each.hxml b/tests/asys/compile-each.hxml new file mode 100644 index 00000000000..6e777a67666 --- /dev/null +++ b/tests/asys/compile-each.hxml @@ -0,0 +1,7 @@ +--class-path src +--main Main +--library utest +--dce full +-D analyzer-optimize +--debug +# --macro nullSafety('asys.native', StrictThreaded) \ No newline at end of file diff --git a/tests/asys/compile-hl.hxml b/tests/asys/compile-hl.hxml new file mode 100644 index 00000000000..d791ae231c0 --- /dev/null +++ b/tests/asys/compile-hl.hxml @@ -0,0 +1,3 @@ +compile-each.hxml + +--hl bin/asys.hl \ No newline at end of file diff --git a/tests/asys/compile-java.hxml b/tests/asys/compile-java.hxml new file mode 100644 index 00000000000..36b8e42da5e --- /dev/null +++ b/tests/asys/compile-java.hxml @@ -0,0 +1,3 @@ +compile-each.hxml + +--java bin/java \ No newline at end of file diff --git a/tests/asys/compile-jvm.hxml b/tests/asys/compile-jvm.hxml new file mode 100644 index 00000000000..3a99e93d262 --- /dev/null +++ b/tests/asys/compile-jvm.hxml @@ -0,0 +1,3 @@ +compile-each.hxml + +--jvm bin/asys.jar \ No newline at end of file diff --git a/tests/asys/compile-lua.hxml b/tests/asys/compile-lua.hxml new file mode 100644 index 00000000000..9e127f02251 --- /dev/null +++ b/tests/asys/compile-lua.hxml @@ -0,0 +1,3 @@ +compile-each.hxml + +--lua bin/asys.lua \ No newline at end of file diff --git a/tests/asys/compile-macro.hxml b/tests/asys/compile-macro.hxml new file mode 100644 index 00000000000..f29748c232f --- /dev/null +++ b/tests/asys/compile-macro.hxml @@ -0,0 +1,3 @@ +compile-each.hxml + +--interp \ No newline at end of file diff --git a/tests/asys/compile-neko.hxml b/tests/asys/compile-neko.hxml new file mode 100644 index 00000000000..0a05799af03 --- /dev/null +++ b/tests/asys/compile-neko.hxml @@ -0,0 +1,3 @@ +compile-each.hxml + +--neko bin/asys.n \ No newline at end of file diff --git a/tests/asys/compile-php.hxml b/tests/asys/compile-php.hxml new file mode 100644 index 00000000000..fba7cea8b38 --- /dev/null +++ b/tests/asys/compile-php.hxml @@ -0,0 +1,3 @@ +compile-each.hxml + +--php bin/php \ No newline at end of file diff --git a/tests/asys/compile-python.hxml b/tests/asys/compile-python.hxml new file mode 100644 index 00000000000..9ef1679778a --- /dev/null +++ b/tests/asys/compile-python.hxml @@ -0,0 +1,3 @@ +compile-each.hxml + +--python bin/test.py \ No newline at end of file diff --git a/tests/asys/src/FsTest.hx b/tests/asys/src/FsTest.hx new file mode 100644 index 00000000000..f46f58b65a1 --- /dev/null +++ b/tests/asys/src/FsTest.hx @@ -0,0 +1,81 @@ +import asys.native.filesystem.FilePath; +import haxe.io.Bytes; +import haxe.PosInfos; + +using haxe.io.Path; +using StringTools; + +/** + Base class for filesystem-related tests +**/ +class FsTest extends Test { + + function setup() { + var tempDir = 'test-data/temp'; + //TODO: Find a way to create & cleanup `test-data/temp` directory without using old sys API + if(!sys.FileSystem.exists(tempDir)) + sys.FileSystem.createDirectory(tempDir); + switch sys.FileSystem.readDirectory(tempDir) { + case []: + case _: + if(isWindows) + Sys.command('rmdir', [tempDir, '/S', '/Q']) + else + Sys.command('rm', ['-rf', tempDir]); + sys.FileSystem.createDirectory(tempDir); + } + } + + /** + * Expected content of `test-data/bytes.bin` file + */ + function bytesBinContent():Bytes { + var data = Bytes.alloc(256); + for(i in 0...data.length) data.set(i, i); + return data; + } + + /** + "Slash-insensitive" comparison of two strings representing file paths. + E.g. `equalPaths("path/to/file", "path\\to\\file");` passes on windows + (but still fails on other systems) + **/ + function equalPaths(expected:Null, actual:Null, ?msg:String, ?pos:PosInfos) { + if(expected == null || actual == null) { + equals(expected, actual, msg, pos); + } else { + if(isWindows) { + expected = expected.replace('/', '\\'); + actual = actual.replace('/', '\\'); + } + if(expected != actual) { + expected = removeTrailingNoise(expected); + actual = removeTrailingNoise(actual); + } + equals(expected, actual, msg, pos); + } + } + + static final driveOnly = ~/^[a-zA-Z]:$/; + + /** + * Removes trailing slashes and trailing single dots + * + * @param path + * @return String + */ + function removeTrailingNoise(path:String):String { + var i = path.length - 1; + while(i > 0) { + switch path.fastCodeAt(i) { + case '/'.code: + case '\\'.code if(isWindows && !(i == 2 && path.fastCodeAt(1) != ':'.code)): + // case '.'.code if(i > 0 && path.fastCodeAt(i - 1) != '.'.code): + case _: + break; + } + i--; + } + return path.substr(0, i + 1); + } +} \ No newline at end of file diff --git a/tests/asys/src/Main.hx b/tests/asys/src/Main.hx new file mode 100644 index 00000000000..7cb1925f6fb --- /dev/null +++ b/tests/asys/src/Main.hx @@ -0,0 +1,11 @@ +import utest.ui.Report; +import utest.Runner; + +function main() { + var runner = new Runner(); + var report = Report.create(runner); + report.displayHeader = AlwaysShowHeader; + report.displaySuccessResults = NeverShowSuccessResults; + runner.addCases('cases'); + runner.run(); +} \ No newline at end of file diff --git a/tests/asys/src/Test.hx b/tests/asys/src/Test.hx new file mode 100644 index 00000000000..a8430a9cd74 --- /dev/null +++ b/tests/asys/src/Test.hx @@ -0,0 +1,102 @@ +import haxe.PosInfos; +import haxe.macro.Expr; +import haxe.Exception; +import haxe.io.Bytes; + +/** + Base class for asys tests +**/ +class Test extends utest.Test { + static final __systemName = Sys.systemName(); + + var isWindows(get,never):Bool; + function get_isWindows():Bool { + return __systemName == 'Windows'; + } + + /** + Allocate `haxe.io.Bytes` with the provided bytes values. + **/ + function bytes(data:Array):Bytes { + var b = Bytes.alloc(data.length); + for (index => value in data) { + b.set(index, value); + } + return b; + } + + /** + Setup test environment for filesystem-related tests. + **/ + function setupFileSystem() { + var tempDir = 'test-data/temp'; + //TODO: Find a way to create & cleanup `test-data/temp` directory without using old sys API + if(!sys.FileSystem.exists(tempDir)) + sys.FileSystem.createDirectory(tempDir); + switch sys.FileSystem.readDirectory(tempDir) { + case []: + case _: + if(isWindows) + Sys.command('rmdir', [tempDir, '/S', '/Q']) + else + Sys.command('rm', ['-rf', tempDir]); + sys.FileSystem.createDirectory(tempDir); + } + } + + /** + Asserts `e` is `null`. + Otherwise fails with the message of `e.message`. + **/ + function noException(e:Null, ?pos:PosInfos):Bool { + return if(e == null) { + //utest.Assert.isNull is to register the check in utest's report + isNull(e, pos); + } else { + fail(e.message, pos); + } + } + + /** + Assert `v` is of type `type`. Executes `callback(v)` If assertion holds. + **/ + inline function assertType(v:Any, type:Class, callback:(v:T)->Void, ?pos:PosInfos):Void { + if(isOfType(v, type, pos)) + callback(v); + } + + /** + Takes a list of expressions with Continuation-Passing-Style calls like these: + ```haxe + asyncAll(asyncVar, + cpsCall1(arg1, arg2, (error, result) -> { + doSomething(); + }), + { + job(); + cpsCall2(arg1, (error, result) -> { + doAnotherThing(); + }); + } + ) + ``` + and injects `asyncVar.done()` expressions into continuations: + ```haxe + { + asyncVar.branch(__async__ -> cpsCall1(arg1, arg2, (error, result) -> { + doSomething(); + __async__.done(); + })); + asyncVar.branch(__async__ -> { + job(); + cpsCall2(arg1, (error, result) -> { + doAnotherThing(); + __async__.done(); + }); + }); + } + ``` + INFO: does not inject `async.done()` calls into loops. + **/ + macro function asyncAll(eThis:Expr, asyncVar:ExprOf, cpsCalls:Array):ExprOf; +} \ No newline at end of file diff --git a/tests/asys/src/Test.macro.hx b/tests/asys/src/Test.macro.hx new file mode 100644 index 00000000000..49e8f6c0715 --- /dev/null +++ b/tests/asys/src/Test.macro.hx @@ -0,0 +1,104 @@ +import haxe.macro.Expr; +import haxe.macro.Context; + +using haxe.macro.ExprTools; + +class Test extends utest.Test { + + macro function asyncAll(eThis:Expr, asyncVar:ExprOf, cpsCalls:Array):ExprOf { + if(#if display true || #end Context.defined('display')) { + return macro $b{cpsCalls}; + } + function error(pos) { + Context.error('This expression is not a CPS-call or the last argument of a call is not an anonymous function', pos); + } + function injectAsyncDoneBeforeReturns(e:Expr) { + return switch e.expr { + case EMeta(m, macro return $e1) if(m.name == ':implicitReturn'): + e1 = macro return ${injectAsyncDoneBeforeReturns(e1)}; + { expr:EMeta(m, e1), pos:e.pos }; + case EReturn(null): + macro @:pos(e.pos) { + __async__.done(); + return; + } + case EReturn(e): + macro @:pos(e.pos) { + __async__.done(); + return $e; + } + case _: + e.map(injectAsyncDoneBeforeReturns); + } + } + function injectAsyncDone(e:Expr, requireContinuation:Bool):Expr { + switch e.expr { + case ECall(_,args) if(args.length > 0): + switch args[args.length - 1].expr { + case EFunction(_, fn) if(fn.expr != null): + fn.expr = switch fn.expr.expr { + case EMeta(m, macro return $e1) if(m.name == ':implicitReturn'): + e1 = injectAsyncDone(injectAsyncDoneBeforeReturns(e1), false); + macro @:pos(fn.expr.pos) @:implicitReturn return $e1; + case _: + injectAsyncDone(injectAsyncDoneBeforeReturns(fn.expr), false); + } + return e; + case _: + if(requireContinuation) { + error(e.pos); + } + return macro @:pos(e.pos) { + $e; + __async__.done(); + } + } + case EBlock(exprs) if(exprs.length > 0): + exprs[exprs.length - 1] = injectAsyncDone(exprs[exprs.length - 1], requireContinuation); + return e; + case EIf(econd, eif, eelse): + eif = injectAsyncDone(eif, requireContinuation); + if(eelse == null) { + eelse = macro @:pos(e.pos) __async__.done(); + } else { + eelse = injectAsyncDone(eelse, requireContinuation); + } + e.expr = EIf(econd, eif, eelse); + return e; + case ETry(etry, catches): + etry = injectAsyncDone(etry, requireContinuation); + for (c in catches) { + c.expr = injectAsyncDone(c.expr, requireContinuation); + } + e.expr = ETry(etry, catches); + return e; + case ESwitch(etarget, cases, edef): + for(c in cases) { + if(c.expr == null) { + c.expr = macro @:pos(c.values[0].pos) __async__.done(); + } else { + c.expr = injectAsyncDone(c.expr, requireContinuation); + } + } + if(edef == null) { + edef = macro @:pos(e.pos) __async__.done(); + } else { + edef = injectAsyncDone(edef, requireContinuation); + } + e.expr = ESwitch(etarget, cases, edef); + return e; + case _: + if(requireContinuation) { + error(e.pos); + } + return macro @:pos(e.pos) { + $e; + __async__.done(); + } + } + } + var exprs = cpsCalls.map(e -> macro @:pos(e.pos) $asyncVar.branch(__async__ -> ${injectAsyncDone(e, true)})); + var pos = Context.currentPos(); + return macro @:pos(pos) $b{exprs}; + } +} \ No newline at end of file diff --git a/tests/asys/src/cases/asys/native/filesystem/TestDirectory.hx b/tests/asys/src/cases/asys/native/filesystem/TestDirectory.hx new file mode 100644 index 00000000000..6d1ac6a57ea --- /dev/null +++ b/tests/asys/src/cases/asys/native/filesystem/TestDirectory.hx @@ -0,0 +1,43 @@ +package cases.asys.native.filesystem; + +import asys.native.filesystem.FsException; +import asys.native.filesystem.FileSystem; +import asys.native.filesystem.Directory; + +@:depends(cases.asys#if (java && !jvm) ._native #else .native #end.filesystem.TestFileSystem) +class TestDirectory extends FsTest { + function testOpenNonExistent(async:Async) { + FileSystem.openDirectory('test-data/temp/non-existent', (e, _) -> { + assertType(e, FsException, e -> { + equalPaths('test-data/temp/non-existent', e.path); + async.done(); + }); + }); + } + + function testNext(async:Async) { + var expected = ['sub', 'symlink-dir', 'temp', 'bytes.bin', 'symlink']; + var batchSize = 4; + var actual = []; + asyncAll(async, + FileSystem.openDirectory('test-data', batchSize, (e, dir) -> { + if(noException(e)) + dir.next((e, r) -> { + if(noException(e)) { + equals(batchSize, r.length); + for(f in r) actual.push(f.toString()); + dir.next((e, r) -> { + if(noException(e)) { + for(f in r) actual.push(f.toString()); + expected.sort(Reflect.compare); + actual.sort(Reflect.compare); + same(expected, actual); + } + dir.close((e, _) -> noException(e)); + }); + } + }); + }) + ); + } +} \ No newline at end of file diff --git a/tests/asys/src/cases/asys/native/filesystem/TestFile.hx b/tests/asys/src/cases/asys/native/filesystem/TestFile.hx new file mode 100644 index 00000000000..2ea519a4d13 --- /dev/null +++ b/tests/asys/src/cases/asys/native/filesystem/TestFile.hx @@ -0,0 +1,813 @@ +package cases.asys.native.filesystem; + +import asys.native.filesystem.FilePermissions; +import haxe.io.BytesOutput; +import haxe.Int64; +import haxe.Callback; +import haxe.io.Bytes; +import asys.native.filesystem.FsException; +import asys.native.filesystem.FileSystem; +import asys.native.filesystem.File; + +@:depends( + cases.asys#if (java && !jvm) ._native #else .native #end.filesystem.TestFilePath, + cases.asys#if (java && !jvm) ._native #else .native #end.filesystem.TestFilePermissions, + cases.asys#if (java && !jvm) ._native #else .native #end.filesystem.TestFileSystem +) +class TestFile extends FsTest { + function testOpenRead(async:Async) { + asyncAll(async, + FileSystem.openFile('test-data/bytes.bin', Read, (e, file) -> { + if(noException(e)) { + var expected = bytesBinContent(); + var bufOffset = 5; + var buf = Bytes.alloc(expected.length + bufOffset); + var firstReadLength = 10; + var pos = 0; + //read less than EOF + file.read(pos, buf, bufOffset, firstReadLength, (e, r) -> { + if(noException(e) && equals(firstReadLength, r)) { + var expectedRead = expected.sub(0, r); + var actualRead = buf.sub(bufOffset, r); + if(same(expectedRead, actualRead)) { + bufOffset += r; + pos += r; + //read more than EOF + file.read(pos, buf, bufOffset, expected.length, (e, r) -> { + if(noException(e) && equals(expected.length - firstReadLength, r)) { + var expectedRead = expected.sub(firstReadLength, r); + var actualRead = buf.sub(bufOffset, r); + if(same(expectedRead, actualRead)) { + pos += r; + //read after EOF + file.read(pos, buf, 0, 1, (e, r) -> { + if(noException(e) && equals(0, r)) + file.close((e, _) -> noException(e)); + }); + } + } + }); + } + } + }); + } + }), + //Read non-existent + FileSystem.openFile('test-data/temp/non-existent', Read, (e, r) -> { + assertType(e, FsException, e -> { + equalPaths('test-data/temp/non-existent', e.path); + }); + }) + ); + } + + function testOpenAppend(async:Async) { + asyncAll(async, + //existing file + FileSystem.copyFile('test-data/bytes.bin', 'test-data/temp/append.bin', (_, _) -> { + FileSystem.openFile('test-data/temp/append.bin', Append, (e, file) -> { + if(noException(e)) { + var data = bytes([3, 2, 1, 0]); + var b = new BytesOutput(); + var bb = bytesBinContent(); + b.writeBytes(bb, 0, bb.length); + b.writeBytes(data, 1, 2); + var expected = b.getBytes(); + + file.write(data, 1, 2, (e, r) -> { + if(noException(e) && equals(2, r)) + file.close((e, _) -> { + if(noException(e)) + FileSystem.readBytes('test-data/temp/append.bin', (_, r) -> { + same(expected, r); + }); + }); + }); + } + }); + }), + //non-existent file + FileSystem.openFile('test-data/temp/non-existent.bin', Append, (e, file) -> { + if(noException(e)) { + var buffer = bytes([1, 2, 3, 4, 5]); + //try to write more bytes than `buffer` contains. + file.write(buffer, 2, buffer.length + 2, (e, r) -> { + if(noException(e) && equals(buffer.length - 2, r)) + file.close((e, _) -> { + if(noException(e)) + FileSystem.readBytes('test-data/temp/non-existent.bin', (_, r) -> { + same(buffer.sub(2, buffer.length - 2), r); + }); + }); + }); + } + }), + //in non-existent directory + FileSystem.openFile('test-data/temp/non/existent.bin', Append, (e, file) -> { + assertType(e, FsException, e -> { + equalPaths('test-data/temp/non/existent.bin', e.path); + }); + }) + ); + } + + /* + Remove `AppendRead` flag because not all targets support this combination. + */ + // function testOpenAppendRead(async:Async) { + // asyncAll(async, + // //existing file + // FileSystem.copyFile('test-data/bytes.bin', 'test-data/temp/append-read.bin', (_, _) -> { + // FileSystem.openFile('test-data/temp/append-read.bin', AppendRead, (e, file) -> { + // if(noException(e)) { + // var data = bytes([3, 2, 1, 0]); + // var b = new BytesOutput(); + // var bytesBin = bytesBinContent(); + // b.writeBytes(bytesBin, 0, bytesBin.length); + // b.writeBytes(data, 1, 2); + // var expected = b.getBytes(); + // file.write(data, 1, 2, (e, r) -> { + // if(noException(e)) { + // equals(2, r); + // var readBuf = Bytes.alloc(4); + // file.read(bytesBin.length - 2, readBuf, 0, 4, (e, r) -> { + // if(noException(e)) { + // equals(4, Int64.toInt(r)); + // same(expected.sub(expected.length - 4, 4), readBuf); + // file.close((e, _) -> { + // if(noException(e)) + // FileSystem.readBytes('test-data/temp/append-read.bin', (_, r) -> { + // same(expected, r); + // }); + // }); + // } + // }); + // } + // }); + // } + // }); + // }), + // //non-existent file + // FileSystem.openFile('test-data/temp/non-existent.bin', AppendRead, (e, file) -> { + // if(noException(e)) { + // var buffer = bytes([1, 2, 3, 4, 5]); + // file.write(buffer, 0, buffer.length, (e, r) -> { + // if(noException(e) && equals(buffer.length, r)) { + // var readBuf = Bytes.alloc(buffer.length); + // file.read(0, readBuf, 0, buffer.length, (e, r) -> { + // if(noException(e)) { + // equals(buffer.length, r); + // same(buffer, readBuf); + // file.close((e, _) -> { + // if(noException(e)) + // FileSystem.readBytes('test-data/temp/non-existent.bin', (_, r) -> { + // same(buffer, r); + // }); + // }); + // } + // }); + // } + // }); + // } + // }), + // //in non-existent directory + // FileSystem.openFile('test-data/temp/non/existent.bin', AppendRead, (e, file) -> { + // assertType(e, FsException, e -> { + // equals('test-data/temp/non/existent.bin', e.path.toString()); + // }); + // }) + // ); + // } + + @:depends(testOpenRead) + function testRead_BufferOutOfBounds(async:Async) { + asyncAll(async, + FileSystem.openFile('test-data/sub/hello.world', Read, (_, file) -> { + var buf = Bytes.alloc(10); + //position negative + file.read(-1, buf, 0, buf.length, (e, _) -> { + assertType(e, FsException, e -> equalPaths('test-data/sub/hello.world', e.path)); + //offset negative + file.read(0, buf, -1, buf.length, (e, _) -> { + assertType(e, FsException, e -> equalPaths('test-data/sub/hello.world', e.path)); + //offset == buf.length + file.read(0, buf, buf.length, buf.length, (e, r) -> { + if(noException(e)) + equals(0, r); + //offset > buf.length + file.read(0, buf, buf.length + 1, buf.length, (e, _) -> { + assertType(e, FsException, e -> equalPaths('test-data/sub/hello.world', e.path)); + //length negative + file.read(0, buf, buf.length, -1, (e, _) -> { + assertType(e, FsException, e -> equalPaths('test-data/sub/hello.world', e.path)); + file.close((_, _) -> {}); + }); + }); + }); + }); + }); + }) + ); + } + + @:depends(testOpenWrite) + function testWrite_BufferOutOfBounds(async:Async) { + asyncAll(async, + FileSystem.openFile('test-data/temp/write.oob', Write, (_, file) -> { + var buf = bytes([1, 2, 3]); + //position negative + file.write(-1, buf, 0, buf.length, (e, _) -> { + assertType(e, FsException, e -> equalPaths('test-data/temp/write.oob', e.path)); + //offset negative + file.write(0, buf, -1, buf.length, (e, _) -> { + assertType(e, FsException, e -> equalPaths('test-data/temp/write.oob', e.path)); + //offset == buf.length + file.write(0, buf, buf.length, buf.length, (e, r) -> { + if(noException(e)) + equals(0, r); + //offset > buf.length + file.write(0, buf, buf.length + 1, buf.length, (e, _) -> { + assertType(e, FsException, e -> equalPaths('test-data/temp/write.oob', e.path)); + //length negative + file.write(0, buf, 0, -1, (e, _) -> { + assertType(e, FsException, e -> equalPaths('test-data/temp/write.oob', e.path)); + file.close((_, _) -> {}); + }); + }); + }); + }); + }); + }) + ); + } + + function testOpenReadWrite(async:Async) { + asyncAll(async, + FileSystem.copyFile('test-data/bytes.bin', 'test-data/temp/read-write.bin', (_, _) -> { + FileSystem.openFile('test-data/temp/read-write.bin', ReadWrite, (e, file) -> { + if(noException(e)) { + var expected = bytesBinContent(); + var buf = Bytes.alloc(10); + file.read(0, buf, 0, buf.length, (e, bytesRead) -> { + if(noException(e)) { + equals(buf.length, bytesRead); + same(expected.sub(0, buf.length), buf); + buf = bytes([100, 50, 25]); + expected.blit(bytesRead, buf, 0, buf.length); + file.write(bytesRead, buf, 0, buf.length, (e, r) -> { + if(noException(e) && equals(buf.length, r)) { + file.close((e, _) -> { + if(noException(e)) + FileSystem.readBytes('test-data/temp/read-write.bin', (_, r) -> { + same(expected, r); + }); + }); + } + }); + } + }); + } + }); + }), + FileSystem.openFile('test-data/temp/non-existent', ReadWrite, (e, _) -> { + assertType(e, FsException, e -> { + equalPaths('test-data/temp/non-existent', e.path); + }); + }) + ); + } + + function testOpenWrite(async:Async) { + asyncAll(async, + //existing file + FileSystem.copyFile('test-data/bytes.bin', 'test-data/temp/write.bin', (_, _) -> { + FileSystem.openFile('test-data/temp/write.bin', Write, (e, file) -> { + if(noException(e)) { + var data = bytes([99, 88, 77]); + file.write(0, data, 0, data.length, (e, r) -> { + if(noException(e)) { + equals(data.length, r); + file.close((e, _) -> { + if(noException(e)) + FileSystem.readBytes('test-data/temp/write.bin', (_, r) -> { + same(data, r); + }); + }); + } + }); + } + }); + }), + //non-existent file + FileSystem.openFile('test-data/temp/non-existent', Write, (e, file) -> { + if(noException(e)) { + var data = bytes([66, 55, 44]); + file.write(0, data, 0, data.length, (e, r) -> { + if(noException(e)) { + equals(data.length, r); + file.close((e, _) -> { + if(noException(e)) + FileSystem.readBytes('test-data/temp/non-existent', (_, r) -> { + same(data, r); + }); + }); + } + }); + } + }), + //exceptions + FileSystem.openFile('test-data/temp/non/existent', Write, (e, _) -> { + assertType(e, FsException, e -> { + equalPaths('test-data/temp/non/existent', e.path); + }); + }) + ); + } + + function testOpenWriteX(async:Async) { + asyncAll(async, + //existing file + FileSystem.copyFile('test-data/bytes.bin', 'test-data/temp/writeX.bin', (_, _) -> { + FileSystem.openFile('test-data/temp/writeX.bin', WriteX, (e, _) -> { + assertType(e, FsException, e -> { + equalPaths('test-data/temp/writeX.bin', e.path); + }); + }); + }), + //non-existent file + FileSystem.openFile('test-data/temp/non-existent', WriteX, (e, file) -> { + if(noException(e)) { + var data = bytes([12, 34, 56, 78]); + file.write(0, data, 0, data.length, (e, r) -> { + if(noException(e)) { + equals(data.length, r); + file.close((e, _) -> { + if(noException(e)) + FileSystem.readBytes('test-data/temp/non-existent', (_, r) -> { + same(data, r); + }); + }); + } + }); + } + }), + //exceptions + FileSystem.openFile('test-data/temp/non/existent', WriteX, (e, _) -> { + assertType(e, FsException, e -> { + equalPaths('test-data/temp/non/existent', e.path); + }); + }) + ); + } + + function testOpenWriteRead(async:Async) { + asyncAll(async, + //existing file + FileSystem.copyFile('test-data/bytes.bin', 'test-data/temp/write-read.bin', (_, _) -> { + FileSystem.openFile('test-data/temp/write-read.bin', WriteRead, (e, file) -> { + if(noException(e)) { + var readBuf = Bytes.alloc(10); + file.read(0, readBuf, 0, readBuf.length, (e, r) -> { + if(noException(e)) { + equals(0, r); + same(Bytes.alloc(10), readBuf); + var writeBuf = bytes([5, 7, 8, 9]); + file.write(0, writeBuf, 0, writeBuf.length, (e, r) -> { + if(noException(e)) { + equals(writeBuf.length, r); + file.read(0, readBuf, 0, writeBuf.length, (e, r) -> { + if(noException(e)) { + equals(writeBuf.length, r); + same(writeBuf, readBuf.sub(0, writeBuf.length)); + file.close((e, _) -> { + if(noException(e)) + FileSystem.readBytes('test-data/temp/write-read.bin', (e, r) -> { + same(writeBuf, r); + }); + }); + } + }); + } + }); + } + }); + } + }); + }), + //non-existent file + FileSystem.openFile('test-data/temp/non-existent', WriteRead, (e, file) -> { + if(noException(e)) { + var data = bytes([12, 34, 56, 78]); + file.write(0, data, 0, data.length, (e, r) -> { + if(noException(e)) { + equals(data.length, r); + if(noException(e)) { + var buf = Bytes.alloc(data.length); + file.read(0, buf, 0, data.length, (e, r) -> { + if(noException(e)) { + equals(data.length, r); + same(data, buf); + file.close((e, _) -> { + if(noException(e)) + FileSystem.readBytes('test-data/temp/non-existent', (_, r) -> { + same(data, r); + }); + }); + } + }); + } + } + }); + } + }), + //exceptions + FileSystem.openFile('test-data/temp/non/existent', WriteRead, (e, _) -> { + assertType(e, FsException, e -> { + equalPaths('test-data/temp/non/existent', e.path); + }); + }) + ); + } + + function testOpenWriteReadX(async:Async) { + asyncAll(async, + //existing file + FileSystem.copyFile('test-data/bytes.bin', 'test-data/temp/write-readX.bin', (_, _) -> { + FileSystem.openFile('test-data/temp/write-readX.bin', WriteReadX, (e, file) -> { + assertType(e, FsException, e -> { + equalPaths('test-data/temp/write-readX.bin', e.path); + }); + }); + }), + //non-existent file + FileSystem.openFile('test-data/temp/non-existent', WriteReadX, (e, file) -> { + if(noException(e)) { + var data = bytes([12, 34, 56, 78]); + file.write(0, data, 0, data.length, (e, r) -> { + if(noException(e)) { + equals(data.length, r); + var buf = Bytes.alloc(data.length); + file.read(0, buf, 0, data.length, (e, r) -> { + if(noException(e)) { + equals(data.length, r); + same(data, buf); + file.close((e, _) -> { + if(noException(e)) + FileSystem.readBytes('test-data/temp/non-existent', (_, r) -> { + same(data, r); + }); + }); + } + }); + } + }); + } + }), + //exceptions + FileSystem.openFile('test-data/temp/non/existent', WriteReadX, (e, _) -> { + assertType(e, FsException, e -> { + equalPaths('test-data/temp/non/existent', e.path); + }); + }) + ); + } + + function testOpenOverwrite(async:Async) { + asyncAll(async, + //existing file + FileSystem.copyFile('test-data/bytes.bin', 'test-data/temp/overwrite.bin', (_, _) -> { + FileSystem.openFile('test-data/temp/overwrite.bin', Overwrite, (e, file) -> { + if(noException(e)) { + var data = bytes([99, 88, 77]); + file.write(0, data, 0, data.length, (e, r) -> { + if(noException(e)) { + equals(data.length, r); + file.close((e, _) -> { + if(noException(e)) + FileSystem.readBytes('test-data/temp/overwrite.bin', (_, r) -> { + var expected = bytesBinContent(); + expected.blit(0, data, 0, data.length); + same(expected, r); + }); + }); + } + }); + } + }); + }), + //non-existent file + FileSystem.openFile('test-data/temp/non-existent', Overwrite, (e, file) -> { + if(noException(e)) { + var data = bytes([66, 55, 44]); + file.write(10, data, 0, data.length, (e, r) -> { + if(noException(e)) { + equals(data.length, r); + file.close((e, _) -> { + if(noException(e)) + FileSystem.readBytes('test-data/temp/non-existent', (_, r) -> { + var expected = Bytes.alloc(10 + data.length); + expected.blit(10, data, 0, data.length); + same(expected, r); + }); + }); + } + }); + } + }), + //exceptions + FileSystem.openFile('test-data/temp/non/existent', Overwrite, (e, _) -> { + assertType(e, FsException, e -> { + equalPaths('test-data/temp/non/existent', e.path); + }); + }) + ); + } + + function testOpenOverwriteRead(async:Async) { + asyncAll(async, + //existing file + FileSystem.copyFile('test-data/bytes.bin', 'test-data/temp/overwrite-read.bin', (_, _) -> { + FileSystem.openFile('test-data/temp/overwrite-read.bin', OverwriteRead, (e, file) -> { + if(noException(e)) { + var readBuf = Bytes.alloc(10); + var content = bytesBinContent(); + file.read(0, readBuf, 0, readBuf.length, (e, r) -> { + if(noException(e)) { + equals(readBuf.length, r); + same(content.sub(0, readBuf.length), readBuf); + var writeBuf = bytes([5, 7, 8, 9]); + file.write(readBuf.length, writeBuf, 0, writeBuf.length, (e, r) -> { + if(noException(e)) { + equals(writeBuf.length, r); + file.read(readBuf.length, readBuf, 0, writeBuf.length, (e, r) -> { + if(noException(e)) { + equals(writeBuf.length, r); + same(writeBuf, readBuf.sub(0, writeBuf.length)); + file.close((e, _) -> { + if(noException(e)) + FileSystem.readBytes('test-data/temp/overwrite-read.bin', (e, r) -> { + content.blit(readBuf.length, writeBuf, 0, writeBuf.length); + same(content, r); + }); + }); + } + }); + } + }); + } + }); + } + }); + }), + //non-existent file + FileSystem.openFile('test-data/temp/non-existent', OverwriteRead, (e, file) -> { + if(noException(e)) { + var data = bytes([12, 34, 56, 78]); + file.write(0, data, 0, data.length, (e, r) -> { + if(noException(e)) { + equals(data.length, r); + var buf = Bytes.alloc(data.length); + file.read(0, buf, 0, data.length, (e, r) -> { + if(noException(e)) { + equals(data.length, r); + same(data, buf); + file.close((e, _) -> { + if(noException(e)) + FileSystem.readBytes('test-data/temp/non-existent', (_, r) -> { + same(data, r); + }); + }); + } + }); + } + }); + } + }), + //exceptions + FileSystem.openFile('test-data/temp/non/existent', OverwriteRead, (e, _) -> { + assertType(e, FsException, e -> { + equalPaths('test-data/temp/non/existent', e.path); + }); + }) + ); + } + + //TODO create a test which actually tests `flush` behavior + @:depends(testOpenWrite) + function testFlush(async:Async) { + asyncAll(async, + FileSystem.openFile('test-data/temp/flush', Write, (e, file) -> { + var data = bytes([123, 234, 56]); + file.write(0, data, 0, data.length, (_, _) -> { + file.flush((e, _) -> { + if(noException(e)) + file.close((_, _) -> {}); + }); + }); + }) + ); + } + + @:depends(testOpenRead) + function testInfo(async:Async) { + asyncAll(async, + FileSystem.openFile('test-data/sub/hello.world', Read, (_, file) -> { + file.info((e, r) -> { + if(noException(e)) { + equals(13, r.size); + isTrue(r.mode.isFile()); + isFalse(r.mode.isDirectory()); + isFalse(r.mode.isLink()); + } + file.close((_, _) -> {}); + }); + }) + ); + } + +#if !neko + + @:depends(testOpenWrite, testInfo) + function testPermissions(async:Async) { + if(isWindows) { + pass(); + async.done(); + return; + } + + asyncAll(async, + FileSystem.openFile('test-data/temp/set-perm', Write, (_, file) -> { + var permissions:FilePermissions = [0, 7, 6, 5]; + file.setPermissions(permissions, (e, r) -> { + if(noException(e)) { + #if cs //TODO + pass(); + file.close((_, _) -> {}); + #else + file.info((_, r) -> { + isTrue(r.mode.has(permissions)); + file.close((_, _) -> {}); + }); + #end + } + }); + }) + ); + } + + @:depends(testInfo, testOpenWrite) + function testSetOwner(async:Async) { + if(isWindows #if cs || true #end) { + pass(); + async.done(); + return; + } + + asyncAll(async, + FileSystem.openFile('test-data/temp/set-owner', Write, (_, file) -> { + file.info((_, r) -> { + file.setOwner(r.user, r.group, (e, _) -> { + noException(e); + file.close((_, _) -> {}); + }); + }); + }) + ); + } + + @:depends(testOpenReadWrite) + function testResize(async:Async) { + asyncAll(async, + FileSystem.writeString('test-data/temp/resize1', 'hello', (_, _) -> { + FileSystem.openFile('test-data/temp/resize1', ReadWrite, (_, file) -> { + file.resize(2, (e, r) -> { + if(noException(e)) { + var buf = Bytes.alloc(100); + file.read(0, buf, 0, buf.length, (e, r) -> { + if(noException(e)) { + equals(2, r); + same(bytes(['h'.code, 'e'.code]), buf.sub(0, 2)); + } + file.close((_, _) -> {}); + }); + } + }); + }); + }), + FileSystem.writeString('test-data/temp/resize2', 'hi', (_, _) -> { + FileSystem.openFile('test-data/temp/resize2', ReadWrite, (_, file) -> { + file.resize(10, (e, r) -> { + if(noException(e)) { + var buf = Bytes.alloc(100); + file.read(0, buf, 0, buf.length, (e, r) -> { + if(noException(e)) { + equals(10, r); + var expected = Bytes.alloc(10); + expected.set(0, 'h'.code); + expected.set(1, 'i'.code); + same(expected, buf.sub(0, 10)); + } + file.close((_, _) -> {}); + }); + } + }); + }); + }) + ); + } + + @:depends(testOpenWrite, testInfo) + function testSetTimes(async:Async) { + var modificationTime = Std.int(Date.fromString('2020-01-01 00:01:02').getTime() / 1000); + var accessTime = Std.int(Date.fromString('2020-02-03 04:05:06').getTime() / 1000); + asyncAll(async, + FileSystem.openFile('test-data/temp/set-times', Write, (_, file) -> { + file.setTimes(accessTime, modificationTime, (e, r) -> { + if(noException(e)) + file.info((_, r) -> { + #if eval + // TODO: + // The time is always set to a slightly (by 10-60 sec) different value. + // Find out why. Perhaps it's a bug in OCaml luv library. + isTrue(Math.abs(modificationTime - r.modificationTime) < 100); + isTrue(Math.abs(modificationTime - r.modificationTime) < 100); + #else + equals(modificationTime, r.modificationTime); + equals(accessTime, r.accessTime); + #end + file.close((_, _) -> {}); + }); + }); + }) + //TODO: add a test for `file.setTimes(...)` failure + ); + } +#end + // TODO: see the doc block for `asys.native.filesystem.File.lock` + // @:depends(testOpenWrite) + // function testLock(async:Async) { + // //TODO: proper test for File.lock + // if(Sys.systemName() != 'Linux') { + // pass(); + // return; + // } + + // asyncAll(async, + // FileSystem.openFile('test-data/temp/file.lock', Write, (_, file) -> { + // file.lock(Exclusive, false, (e, r) -> { + // if(noException(e) && isTrue(r)) { + // var lockedExternally = 0 == Sys.command('flock', ['-n', 'test-data/temp/file.lock', '-c', 'echo']); + // isFalse(lockedExternally); + // file.lock(Unlock, (e, r) -> { + // if(noException(e) && isTrue(r)) { + // var lockedExternally = 0 == Sys.command('flock', ['-n', 'test-data/temp/file.lock', '-c', 'echo']); + // isTrue(lockedExternally); + // } + // file.close((_, _) -> {}); + // }); + // } + // }); + // }) + // ); + // } + + @:depends(testOpenRead) + function testClose_multipleClose(async:Async) { + asyncAll(async, + FileSystem.openFile('test-data/bytes.bin', Read, (e, file) -> { + file.close((e, _) -> { + if(noException(e)) + file.close((e, _) -> noException(e)); + }); + }) + ); + } + + @:depends(testOpenWriteRead) + function testFileSystem_tempFile(async:Async) { + asyncAll(async, + FileSystem.tempFile((e, file) -> { + if(noException(e)) { + var path = file.path; + FileSystem.check(path, Exists, (e, r) -> { + if(noException(e) && isTrue(r)) { + var writeBuf = bytes([0, 1, 2, 3]); + file.write(0, writeBuf, 0, writeBuf.length, (_, r) -> { + if(equals(writeBuf.length, r)) { + var readBuf = Bytes.alloc(writeBuf.length); + file.read(0, readBuf, 0, readBuf.length, (_, r) -> { + same(writeBuf, readBuf); + equals(readBuf.length, r); + file.close((_, _) -> { + FileSystem.check(path, Exists, (_, r) -> isFalse(r)); + }); + }); + } + }); + } + }); + } + }) + ); + } +} \ No newline at end of file diff --git a/tests/asys/src/cases/asys/native/filesystem/TestFilePath.hx b/tests/asys/src/cases/asys/native/filesystem/TestFilePath.hx new file mode 100644 index 00000000000..8beb2c69e26 --- /dev/null +++ b/tests/asys/src/cases/asys/native/filesystem/TestFilePath.hx @@ -0,0 +1,198 @@ +package cases.asys.native.filesystem; + +import haxe.PosInfos; +import asys.native.filesystem.FsException; +import haxe.io.Bytes; +import asys.native.filesystem.FilePath; +import haxe.io.Path; +import haxe.exceptions.ArgumentException; +import haxe.exceptions.PosException; + +/** + * INFO + * Paths are checked for equality using `equalPaths`, which automatically translates + * (back)slashes and ignores trailing slashes if needed. + */ +class TestFilePath extends FsTest { + function expect(value:FilePath, ?pos:PosInfos) { + return {value:value, pos:pos}; + } + + inline function cases(m:Map<{value:FilePath,pos:PosInfos},Null>) { + return m; + } + + function check(cases:Map<{value:FilePath,pos:PosInfos},Null>, subject:(Null)->Null) { + for(path => expected in cases) { + var actual = try { + subject(path.value); + } catch(e) { + throw new haxe.exceptions.PosException(e.message, e, path.pos); + } + equalPaths(expected, actual, path.pos); + } + } + + function testCreatePath() { + var cases = cases([ + expect(FilePath.createPath('path', 'to', 'file')) => 'path/to/file', + expect(FilePath.createPath('path/', 'to', 'file')) => 'path/to/file', + expect(FilePath.createPath('path', '/to', 'file')) => '/to/file', + expect(FilePath.createPath('path', '', 'file')) => 'path/file', + expect(FilePath.createPath(['path', 'to', 'file'])) => 'path/to/file', + expect(FilePath.createPath(['path/', 'to', 'file'])) => 'path/to/file', + expect(FilePath.createPath(['path', '', 'file'])) => 'path/file', + expect(FilePath.createPath(['path', '/to', 'file'])) => '/to/file', + ]); + if(isWindows) { + cases[expect(FilePath.createPath('C:/', 'file'))] = 'C:/file'; + raises(() -> FilePath.createPath([]), ArgumentException); + raises(() -> FilePath.createPath('D:/path', 'C:file'), ArgumentException); + //TODO: I'm not sure about these + // cases[expect('C:file')] = FilePath.createPath('C:', 'file');//??? + // cases[expect('C:path/file')] = FilePath.createPath('path', 'C:file'); //??? + } + check(cases, p -> p); + } +#if target.unicode + function testOfString_unicode() { + var s = '𠜎/aa😂/éé'; + var p = FilePath.ofString(s); + equalPaths(s, p); + } +#end + function testOfArray() { + #if target.unicode + var p:FilePath = ['𠜎', '😂']; + equalPaths('𠜎/😂', p); + #else + var p:FilePath = ['hello', 'world']; + equalPaths('hello/world', p); + #end + } + + function testEqual() { + var p1 = FilePath.ofString('qwe'); + var p2 = FilePath.ofString('qwe'); + isTrue(p1 == p2); + } + + function testIsAbsolute() { + isTrue((Sys.getCwd():FilePath).isAbsolute()); + isTrue(('/something/something':FilePath).isAbsolute()); + isFalse(('':FilePath).isAbsolute()); + isFalse(('./':FilePath).isAbsolute()); + isFalse(('..':FilePath).isAbsolute()); + if(isWindows) { + isTrue(('C:\\something':FilePath).isAbsolute()); + isTrue(('\\':FilePath).isAbsolute()); + isFalse(('C:something':FilePath).isAbsolute()); + } else { + isFalse(('\\':FilePath).isAbsolute()); + } + } + + function testNormalize() { + var cases = cases([ + expect('some/path') => 'some/path', + expect('') => '', + expect('.') => '', + expect('./') => '', + expect('path/to/./../../non-existent/./file') => 'non-existent/file', + expect('check///slashes/') => 'check/slashes', + expect('./all/redundant/../..') => '', + expect('leaves/../non-redundant/../double-dots/../../..') => '../..', + expect('...') => '...', + expect('/absolute/path') => '/absolute/path', + ]); + if(isWindows) { + cases[expect('C:/absolute/../path')] = 'C:/path'; + cases[expect('C:/back/to/root/../../..')] = 'C:/'; + cases[expect('C:/absolute/excessive/dots/../../../..')] = 'C:/'; + cases[expect('C:relative/.././')] = 'C:'; + cases[expect('C:relative/../excessive/dots/../../../..')] = 'C:../..'; + } + check(cases, p -> p.normalize()); + } + + function testAbsolute() { + var cwd = Path.addTrailingSlash(Sys.getCwd()); + var cases = cases([ + expect('some/path') => cwd + 'some/path', + expect('') => cwd, + expect('.') => cwd + '.', + expect('./') => cwd + '.', + expect('non-existent/file') => cwd + 'non-existent/file', + ]); + if(isWindows) { + var currentDrive = cwd.substr(0, 1); + cases[expect('/absolute/path')] = '/absolute/path'; + cases[expect('C:/absolute/path')] = 'C:/absolute/path'; + cases[expect(cwd + 'relative/path')] = cwd + 'relative/path'; + } else { + cases[expect('/absolute/path')] = '/absolute/path'; + } + check(cases, p -> p.absolute()); + } + + function testParent() { + var cases = cases([ + expect('file') => null, + expect('/file') => '/', + expect('./file') => '.', + expect('path/to/file') => 'path/to', + expect('path/to/dir/') => 'path/to', + expect('path/to///dir/') => 'path/to', + expect('path/to/../file') => 'path/to/..', + expect('path/to/..') => 'path/to', + expect('path/to/.') => 'path/to', + expect('.hidden') => null, + expect('.') => null, + expect('') => null, + expect('/') => null, + expect('\\') => null, + ]); + if(isWindows) { + cases[expect('C:\\')] = null; + cases[expect('C:')] = null; + cases[expect('C:\\dir')] = 'C:\\'; + cases[expect('C:dir')] = 'C:'; + cases[expect('C:.\\dir')] = 'C:.'; + } + check(cases, p -> p.parent()); + } + + function testName() { + var cases = cases([ + expect('file.ext') => 'file.ext', + expect('path/to/file.ext') => 'file.ext', + expect('./file.ext') => 'file.ext', + expect('path/to/dir/') => 'dir', + expect('path/to/.') => '.', + expect('') => '', + expect('/') => '', + ]); + if(isWindows) { + cases[expect('C:\\')] = 'C:\\'; // TODO: Is this what we want? Or ''? Or 'C:'? + cases[expect('C:')] = 'C:'; + cases[expect('C:\\file.ext')] = 'file.ext'; + cases[expect('C:\\dir\\')] = 'dir'; + cases[expect('C:dir')] = 'dir'; + } + check(cases, p -> p.name()); + } + + function testAdd() { + var dir = FilePath.ofString('dir'); + var cases = cases([ + expect(dir.add('file')) => 'dir/file', + expect(dir.add('/file')) => '/file', + expect(dir.add('')) => 'dir', + expect(FilePath.ofString('').add(dir)) => 'dir', + ]); + if(isWindows) { + cases[expect(FilePath.ofString('C:/').add(dir))] = 'C:/dir'; + } + check(cases, p -> p); + } +} diff --git a/tests/asys/src/cases/asys/native/filesystem/TestFilePermissions.hx b/tests/asys/src/cases/asys/native/filesystem/TestFilePermissions.hx new file mode 100644 index 00000000000..e5eaa7e805c --- /dev/null +++ b/tests/asys/src/cases/asys/native/filesystem/TestFilePermissions.hx @@ -0,0 +1,23 @@ +package cases.asys.native.filesystem; + +import haxe.PosInfos; +import asys.native.filesystem.FilePermissions; +import asys.native.filesystem.FilePermissions.octal; + +class TestFilePermissions extends FsTest { + inline function check(expected:FilePermissions, actual:FilePermissions, ?pos:PosInfos) { + isTrue(expected == actual, 'Expected $expected but got $actual', pos); + } + + function test() { + check(668, octal(1, 2, 3, 4)); + check(438, octal(0, 6, 6, 6)); + check(493, octal(0, 7, 5, 5)); + check(493, octal(0, 7, 5, 5)); + + check(668, [1, 2, 3, 4]); + check(438, [0, 6, 6, 6]); + check(493, [0, 7, 5, 5]); + check(493, [0, 7, 5, 5]); + } +} \ No newline at end of file diff --git a/tests/asys/src/cases/asys/native/filesystem/TestFileSystem.hx b/tests/asys/src/cases/asys/native/filesystem/TestFileSystem.hx new file mode 100644 index 00000000000..bed769d55fa --- /dev/null +++ b/tests/asys/src/cases/asys/native/filesystem/TestFileSystem.hx @@ -0,0 +1,737 @@ +package cases.asys.native.filesystem; + +import haxe.PosInfos; +import haxe.io.Path; +import asys.native.filesystem.FilePermissions; +import haxe.NoData; +import asys.native.filesystem.Callback; +import asys.native.filesystem.FileOpenFlag; +import haxe.io.Bytes; +import asys.native.filesystem.FilePath; +import asys.native.filesystem.FsException; +import asys.native.filesystem.FileSystem; + +@:depends( + cases.asys#if (java && !jvm) ._native #else .native #end.filesystem.TestFilePath, + cases.asys#if (java && !jvm) ._native #else .native #end.filesystem.TestFilePermissions +) +class TestFileSystem extends FsTest { + + function testReadBytes(async:Async) { + asyncAll(async, + FileSystem.readBytes('test-data/bytes.bin', (e, r) -> { + if(noException(e)) + same(bytesBinContent(), r); + }), + FileSystem.readBytes('test-data/', (e, r) -> { + assertType(e, FsException, e -> equalPaths('test-data', e.path)); + }), + FileSystem.readBytes('non-existent', (e, r) -> { + assertType(e, FsException, e -> equalPaths('non-existent', e.path)); + }) + ); + } + + function testReadString(async:Async) { + asyncAll(async, + FileSystem.readString('test-data/sub/hello.world', (e, r) -> { + if(noException(e)) + equals('Hello, world!', r); + }), + FileSystem.readString('test-data/', (e, r) -> { + assertType(e, FsException, e -> equalPaths('test-data', e.path)); + }), + FileSystem.readString('non-existent', (e, r) -> { + assertType(e, FsException, e -> equalPaths('non-existent', e.path)); + }) + ); + } + + @:depends(testReadString) + function testWriteString(async:Async) { + function writeAndCheck(textToWrite:String, expectedContent:String, flag:FileOpenFlag, callback:(ok:Bool)->Void, ?pos:PosInfos) { + FileSystem.writeString('test-data/temp/test.txt', textToWrite, flag, (e, _) -> { + if(noException(e, pos)) + FileSystem.readString('test-data/temp/test.txt', (e, r) -> { + if(noException(e, pos)) + callback(equals(expectedContent, r, pos)) + else + callback(false); + }) + else + callback(false); + }); + } + + asyncAll(async, + writeAndCheck('Hello, ', 'Hello, ', Write, ok -> { + if(ok) writeAndCheck('world!', 'Hello, world!', Append, ok -> { + if(ok) writeAndCheck('Goodbye', 'Goodbye', Write, ok -> { + if(ok) writeAndCheck('Bye-', 'Bye-bye', Overwrite, ok -> {}); + }); + }); + }), + { + var path = 'test-data/temp/non-existent-dir/test.txt'; + FileSystem.writeString(path, '', (e, r) -> { + assertType(e, FsException, e -> equalPaths(path, e.path)); + }); + } + ); + } + + @:depends(testReadBytes) + function testWriteBytes(async:Async) { + function writeAndCheck(bytesToWrite:Bytes, expectedContent:Bytes, flag:FileOpenFlag, callback:(ok:Bool)->Void, ?pos:PosInfos) { + FileSystem.writeBytes('test-data/temp/test.bin', bytesToWrite, flag, (e, _) -> { + if(noException(e, pos)) + FileSystem.readBytes('test-data/temp/test.bin', (e, r) -> { + if(noException(e, pos)) + callback(same(expectedContent, r, pos)) + else + callback(true); + }) + else + callback(false); + }); + } + + asyncAll(async, + writeAndCheck(bytes([0, 1, 2]), bytes([0, 1, 2]), Write, ok -> { + if(ok) writeAndCheck(bytes([3, 4, 5]), bytes([0, 1, 2, 3, 4, 5]), Append, ok -> { + if(ok) writeAndCheck(bytes([6, 7, 8, 9]), bytes([6, 7, 8, 9]), Write, ok -> { + if(ok) writeAndCheck(bytes([10, 11]), bytes([10, 11, 8, 9]), Overwrite, ok -> {}); + }); + }); + }), + { + var path = 'test-data/temp/non-existent-dir/test.bin'; + FileSystem.writeBytes(path, Bytes.alloc(1), (e, r) -> { + assertType(e, FsException, e -> equalPaths(path, e.path)); + }); + } + ); + } + + @:depends(testLink,testIsLink) + function testCheck(async:Async) { + asyncAll(async, + FileSystem.check('test-data/sub', Exists, (e, r) -> { + if(noException(e)) + isTrue(r); + }), + FileSystem.check('test-data/sub/hello.world', Readable, (e, r) -> { + if(noException(e)) + isTrue(r); + }), + // #if !cs //TODO: implement these checks depending on .NET/mono versions + FileSystem.check('test-data/temp', Writable, (e, r) -> { + if(noException(e)) + isTrue(r); + }), + FileSystem.check('test-data/temp', Writable | Readable, (e, r) -> { + if(noException(e)) + isTrue(r); + }), + // #end + #if !cs + //too much hassle making windows links to work across different machines (CI, local PC, netowrk share etc) + if(!isWindows) + FileSystem.link('non-existent', 'test-data/temp/faulty-link', (_, _) -> { + FileSystem.isLink('test-data/temp/faulty-link', (e, r) -> { + if(noException(e) && isTrue(r)) + FileSystem.check('test-data/temp/faulty-link', Exists, (e, r) -> { + if(noException(e)) + isFalse(r); + }); + }); + }), + #end + FileSystem.check('non-existent', Exists, (e, r) -> { + if(noException(e)) + isFalse(r); + }) + ); + if(!isWindows) { + asyncAll(async, + FileSystem.check('/bin', Readable, (e, r) -> { + if(noException(e)) + isTrue(r); + }), + #if !cs //TODO: implement these checks depending on .NET/mono versions + FileSystem.check('/bin', Writable, (e, r) -> { + if(noException(e)) + isFalse(r); + }), + FileSystem.check('/bin', Readable | Writable, (e, r) -> { + if(noException(e)) + isFalse(r); + }), + #end + FileSystem.check('/bin', Exists, (e, r) -> { + if(noException(e)) + isTrue(r); + }) + ); + } + } + + function testIsDirectory(async:Async) { + asyncAll(async, + FileSystem.isDirectory('test-data/sub', (e, r) -> { + if(noException(e)) + isTrue(r); + }), + FileSystem.isDirectory('test-data/sub/hello.world', (e, r) -> { + if(noException(e)) + isFalse(r); + }), + FileSystem.isDirectory('test-data/symlink-dir', (e, r) -> { + if(noException(e)) + isTrue(r); + }), + FileSystem.isDirectory('non-existent', (e, r) -> { + if(noException(e)) + isFalse(r); + }) + ); + } + + function testIsFile(async:Async) { + asyncAll(async, + FileSystem.isFile('test-data/sub/hello.world', (e, r) -> { + if(noException(e)) + isTrue(r); + }), + FileSystem.isFile('test-data/sub', (e, r) -> { + if(noException(e)) + isFalse(r); + }), + FileSystem.isFile('test-data/symlink', (e, r) -> { + if(noException(e)) + isTrue(r); + }), + FileSystem.isFile('non-existent', (e, r) -> { + if(noException(e)) + isFalse(r); + }) + ); + } + + @:depends(testIsDirectory) + function testCreateDirectory(async:Async) { + asyncAll(async, + FileSystem.createDirectory('test-data/temp/dir', (e, r) -> { + if(noException(e)) + FileSystem.isDirectory('test-data/temp/dir', (e, r) -> isTrue(r)); + }), + FileSystem.createDirectory('test-data/temp/non/existent', (e, r) -> { + assertType(e, FsException, e -> equalPaths('test-data/temp/non/existent', e.path)); + }), + FileSystem.createDirectory('test-data/temp/non-existent1/non-existent2/non-existent3', true, (e, r) -> { + if(noException(e)) + FileSystem.isDirectory('test-data/temp/non-existent1/non-existent2/non-existent3', (e, r) -> isTrue(r)); + }) + ); + } + + @:depends(testCreateDirectory, testWriteString, testReadString, testCheck) + function testMove(async:Async) { + function createData(path:String, fileContent:String, callback:()->Void) { + FileSystem.createDirectory(path, (e, r) -> { + FileSystem.writeString('$path/file', fileContent, (e, r) -> { + callback(); + }); + }); + } + + asyncAll(async, + //move directory + createData('test-data/temp/dir1', 'hello', () -> { + FileSystem.move('test-data/temp/dir1', 'test-data/temp/moved', (e, r) -> { + if(noException(e)) + asyncAll(async, + FileSystem.check('test-data/temp/dir1', Exists, (e, r) -> isFalse(r)), + FileSystem.readString('test-data/temp/moved/file', (e, r) -> { + equals('hello', r); + asyncAll(async, + //overwrite + createData('test-data/temp/dir2', 'world', () -> { + FileSystem.move('test-data/temp/dir2/file', 'test-data/temp/moved/file', true, (e, r) -> { + if(noException(e)) + FileSystem.readString('test-data/temp/moved/file', (e, r) -> equals('world', r)); + }); + }), + //disable overwrite + createData('test-data/temp/dir3', 'unexpected', () -> { + FileSystem.move('test-data/temp/dir3/file', 'test-data/temp/moved/file', false, (e, r) -> { + isOfType(e, FsException); + }); + }), + //non-empty directory + createData('test-data/temp/dir4', 'hello', () -> { + FileSystem.move('test-data/temp/dir4', 'test-data/temp/moved', true, (e, r) -> { + isOfType(e, FsException); + }); + }) + ); + }) + ); + }); + }), + //move file + createData('test-data/temp/dir4', 'hello', () -> { + FileSystem.move('test-data/temp/dir4/file', 'test-data/temp/dir4/file2', (e, r) -> { + if(noException(e)) + FileSystem.readString('test-data/temp/dir4/file2', (e, r) -> equals('hello', r)); + }); + }), + //check exceptions + FileSystem.move('test-data/temp/non/existent', 'test-data/temp/non-existent', (e, r) -> { + assertType(e, FsException, e -> equalPaths('test-data/temp/non/existent', e.path)); + }) + ); + } + + @:depends(testWriteString, testCheck) + function testDeleteFile(async:Async) { + asyncAll(async, + FileSystem.writeString('test-data/temp/test.txt', '', (e, r) -> { + FileSystem.deleteFile('test-data/temp/test.txt', (e, r) -> { + if(noException(e)) + FileSystem.check('test-data/temp/test.txt', Exists, (e, r) -> isFalse(r)); + }); + }), + FileSystem.deleteFile('non-existent', (e, r) -> { + assertType(e, FsException, e -> equalPaths('non-existent', e.path)); + }), + FileSystem.deleteFile('test-data/temp', (e, r) -> { + assertType(e, FsException, e -> equalPaths('test-data/temp', e.path)); + }) + ); + } + + @:depends(testCreateDirectory, testWriteString, testCheck) + function testDeleteDirectory(async:Async) { + asyncAll(async, + FileSystem.createDirectory('test-data/temp/del-dir', (e, r) -> { + FileSystem.deleteDirectory('test-data/temp/del-dir', (e, r) -> { + if(noException(e)) + FileSystem.check('test-data/temp/del-dir', Exists, (e, r) -> isFalse(r)); + }); + }), + FileSystem.deleteDirectory('non-existent', (e, r) -> { + assertType(e, FsException, e -> equalPaths('non-existent', e.path)); + }), + FileSystem.writeString('test-data/temp/file', '', (e, r) -> { + FileSystem.deleteDirectory('test-data/temp/file', (e, r) -> { + assertType(e, FsException, e -> equalPaths('test-data/temp/file', e.path)); + }); + }), + FileSystem.createDirectory('test-data/temp/non-empty', (e, r) -> { + FileSystem.writeString('test-data/temp/non-empty/file', '', (e, r) -> { + FileSystem.deleteDirectory('test-data/temp/non-empty', (e, r) -> { + assertType(e, FsException, e -> equalPaths('test-data/temp/non-empty', e.path)); + }); + }); + }) + ); + } + + function testInfo(async:Async) { + asyncAll(async, + FileSystem.info('test-data/sub/hello.world', (e, r) -> { + if(noException(e)) { + equals(13, r.size); + isTrue(r.mode.isFile()); + isFalse(r.mode.isDirectory()); + #if !cs + isFalse(r.mode.isLink()); + #end + } + }), + #if !cs + FileSystem.info('test-data/symlink', (e, r) -> { + if(noException(e)) { + equals(13, r.size); + isTrue(r.mode.isFile()); + isFalse(r.mode.isDirectory()); + isFalse(r.mode.isLink()); + } + }), + #end + FileSystem.info('test-data/sub', (e, r) -> { + if(noException(e)) { + isFalse(r.mode.isFile()); + isTrue(r.mode.isDirectory()); + #if !cs + isFalse(r.mode.isLink()); + #end + } + }), + FileSystem.info('non-existent', (e, r) -> { + assertType(e, FsException, e -> equalPaths('non-existent', e.path)); + }) + ); + } + + @:depends(testWriteString, testInfo) + function testSetPermissions(async:Async) { + if(isWindows #if cs || true #end) { + pass(); + async.done(); + return; + } + + asyncAll(async, + FileSystem.writeString('test-data/temp/perm', '', (_, _) -> { + var permissions:FilePermissions = [0, 7, 6, 5]; + FileSystem.setPermissions('test-data/temp/perm', permissions, (e, r) -> { + if(noException(e)) + FileSystem.info('test-data/temp/perm', (_, r) -> { + isTrue(r.mode.has(permissions)); + }); + }); + }), + FileSystem.setPermissions('non-existent', [0, 7, 7, 7], (e, r) -> { + assertType(e, FsException, e -> equalPaths('non-existent', e.path)); + }) + ); + } + + @:depends(testWriteString,testInfo) + function testSetOwner(async:Async) { + if(isWindows #if cs || true #end) { + pass(); + async.done(); + return; + } + + asyncAll(async, + FileSystem.writeString('test-data/temp/set-owner', '', (e, r) -> { + FileSystem.info('test-data/temp/set-owner', (e, r) -> { + FileSystem.setOwner('test-data/temp/set-owner', r.user, r.group, (e, _) -> { + noException(e); + FileSystem.setOwner('test-data/temp/non-existent', r.user, r.group, (e, r) -> { + assertType(e, FsException, e -> equalPaths('test-data/temp/non-existent', e.path)); + }); + }); + }); + }) + ); + } + + + @:depends(testWriteString,testReadString,testReadBytes) + function testResize(async:Async) { + asyncAll(async, + FileSystem.writeString('test-data/temp/resize1', 'hello', (e, r) -> { + FileSystem.resize('test-data/temp/resize1', 2, (e, r) -> { + if(noException(e)) + FileSystem.readString('test-data/temp/resize1', (e, r) -> equals('he', r)); + }); + }), + FileSystem.writeString('test-data/temp/resize2', 'hi', (e, r) -> { + FileSystem.resize('test-data/temp/resize2', 10, (e, r) -> { + if(noException(e)) { + var expected = Bytes.alloc(10); + expected.set(0, 'h'.code); + expected.set(1, 'i'.code); + FileSystem.readBytes('test-data/temp/resize2', (e, r) -> same(expected, r)); + } + }); + }), + FileSystem.resize('test-data/temp/non-existent-file', 10, (e, r) -> { + if(noException(e)) { + var expected = Bytes.alloc(10); + FileSystem.readBytes('test-data/temp/non-existent-file', (e, r) -> same(expected, r)); + } + }), + FileSystem.resize('test-data/temp/non-existent-dir/file', 5, (e, r) -> { + assertType(e, FsException, e -> equalPaths('test-data/temp/non-existent-dir/file', e.path)); + }) + ); + } + + @:depends(testInfo) + function testSetTimes(async:Async) { + var modificationTime = 1577826063; // 2019-12-31 21:01:03 + var accessTime = 1580691906; // 2020-02-03 01:05:06 + asyncAll(async, + FileSystem.setTimes('test-data/sub/hello.world', accessTime, modificationTime, (e, r) -> { + if(noException(e)) + FileSystem.info('test-data/sub/hello.world', (e, r) -> { + #if eval + // TODO: + // The time is always set to a slightly (by 10-60 sec) different value. + // Find out why. Perhaps it's a bug in OCaml luv library. + isTrue(Math.abs(modificationTime - r.modificationTime) < 100); + isTrue(Math.abs(modificationTime - r.modificationTime) < 100); + #else + equals(modificationTime, r.modificationTime); + equals(accessTime, r.accessTime); + #end + }); + }), + FileSystem.setTimes('test-data/temp/set-times-non-existent', accessTime, modificationTime, (e, r) -> { + assertType(e, FsException, e -> equalPaths('test-data/temp/set-times-non-existent', e.path)); + }), + FileSystem.setTimes('test-data/temp/non/existent/set-times', accessTime, modificationTime, (e, r) -> { + assertType(e, FsException, e -> equalPaths('test-data/temp/non/existent/set-times', e.path)); + }) + ); + } + + function testIsLink(async:Async) { + #if cs + pass(); + async.done(); + return; + #end + + asyncAll(async, + //too much hassle making windows links to work across different machines (CI, local PC, netowrk share etc) + if(!isWindows) + FileSystem.isLink('test-data/symlink', (e, r) -> { + if(noException(e)) + isTrue(r); + }), + FileSystem.isLink('test-data/sub/hello.world', (e, r) -> { + if(noException(e)) + isFalse(r); + }), + FileSystem.isLink('test-data', (e, r) -> { + if(noException(e)) + isFalse(r); + }), + FileSystem.isLink('non-existent', (e, r) -> { + if(noException(e)) + isFalse(r); + }) + ); + } + + function testReadLink(async:Async) { + #if cs + pass(); + async.done(); + return; + #end + + asyncAll(async, + //too much hassle making windows links to work across different machines (CI, local PC, netowrk share etc) + if(!isWindows) + FileSystem.readLink('test-data/symlink', (e, r) -> { + if(noException(e)) + equals('sub' + FilePath.SEPARATOR + 'hello.world', r.toString()); + }), + FileSystem.readLink('test-data/sub/hello.world', (e, r) -> { + assertType(e, FsException, e -> equalPaths('test-data/sub/hello.world', e.path)); + }), + FileSystem.readLink('non-existent', (e, r) -> { + assertType(e, FsException, e -> equalPaths('non-existent', e.path)); + }) + ); + } + + function testLinkInfo(async:Async) { + #if cs + pass(); + async.done(); + return; + #end + + asyncAll(async, + //too much hassle making windows links to work across different machines (CI, local PC, netowrk share etc) + if(!isWindows) + FileSystem.linkInfo('test-data/symlink', (e, r) -> { + if(noException(e)) { + isFalse(r.mode.isFile()); + isFalse(r.mode.isDirectory()); + isTrue(r.mode.isLink()); + } + }), + FileSystem.linkInfo('non-existent', (e, r) -> { + assertType(e, FsException, e -> equalPaths('non-existent', e.path)); + }) + ); + } + + @:depends(testReadLink, testIsLink, testReadString) + function testLink(async:Async) { + #if cs + pass(); + async.done(); + return; + #end + + asyncAll(async, + //too much hassle making windows links to work across different machines (CI, local PC, netowrk share etc) + if(!isWindows) + FileSystem.link('../sub/hello.world', 'test-data/temp/symlink', SymLink, (e, r) -> { + if(noException(e)) + FileSystem.readLink('test-data/temp/symlink', (e, r) -> { + if(noException(e)) + equals('../sub/hello.world', r.toString()); + }); + }), + //windows requires elevated mode or UAC disabled to create hard links + if(!isWindows) + FileSystem.link('test-data/sub/hello.world', 'test-data/temp/hardlink', HardLink, (e, r) -> { + if(noException(e)) + FileSystem.isLink('test-data/temp/hardlink', (e, r) -> { + if(noException(e)) + if(isFalse(r)) + FileSystem.readString('test-data/temp/hardlink', (e, r) -> { + if(noException(e)) + equals('Hello, world!', r); + }); + }); + }), + FileSystem.link('../sub/hello.world', 'test-data/temp/non-existent/link', (e, r) -> { + assertType(e, FsException, e -> equalPaths('test-data/temp/non-existent/link', e.path)); + }) + ); + } + + @:depends(testLink,testInfo) + function testSetLinkOwner(async:Async) { + #if cs + pass(); + async.done(); + return; + #end + if(isWindows) { + pass(); + async.done(); + return; + } + + asyncAll(async, + FileSystem.link('../sub/hello.world', 'test-data/temp/set-link-owner', (e, r) -> { + FileSystem.info('test-data/temp/set-link-owner', (e, r) -> { + FileSystem.setLinkOwner('test-data/temp/set-link-owner', r.user, r.group, (e, _) -> { + noException(e); + FileSystem.setLinkOwner('test-data/temp/non-existent-link', r.user, r.group, (e, r) -> { + assertType(e, FsException, e -> equalPaths('test-data/temp/non-existent-link', e.path)); + }); + }); + }); + }) + ); + } + + @:depends(testReadBytes, testReadString) + function testCopyFile(async:Async) { + asyncAll(async, + FileSystem.copyFile('test-data/bytes.bin', 'test-data/temp/copy', (e, r) -> { + if(noException(e)) + FileSystem.readBytes('test-data/temp/copy', (e, r) -> { + if(noException(e) && same(bytesBinContent(), r)) { + asyncAll(async, + //disable overwrite + FileSystem.copyFile('test-data/sub/hello.world', 'test-data/temp/copy', false, (e, r) -> { + assertType(e, FsException, e -> { + var path = e.path.toString(); + isTrue(path == 'test-data/sub/hello.world' || path == 'test-data/temp/copy'); + }); + }), + //overwrite + FileSystem.copyFile('test-data/sub/hello.world', 'test-data/temp/copy', (e, r) -> { + if(noException(e)) + FileSystem.readString('test-data/temp/copy', (e, r) -> { + if(noException(e)) + equals('Hello, world!', r); + }); + }) + ); + } + }); + }), + FileSystem.copyFile('non-existent', 'test-data/temp/copy', (e, r) -> { + assertType(e, FsException, e -> equalPaths('non-existent', e.path)); + }), + FileSystem.copyFile('test-data/sub/hello.world', 'test-data/non-existent/copy', (e, r) -> { + assertType(e, FsException, e -> { + var path = e.path.toString(); + isTrue(path == 'test-data/sub/hello.world' || path == 'test-data/non-existent/copy'); + }); + }) + ); + } + + function testListDirectory(async:Async) { + asyncAll(async, + FileSystem.listDirectory('test-data', (e, r) -> { + if(noException(e)) { + var stringified = r.map(p -> p.toString()); + var expected = ['sub', 'symlink-dir', 'temp', 'bytes.bin', 'symlink']; + expected.sort(Reflect.compare); + stringified.sort(Reflect.compare); + same(expected, stringified); + } + }), + FileSystem.listDirectory('test-data/temp/non-existent', (e, r) -> { + assertType(e, FsException, e -> { + equalPaths('test-data/temp/non-existent', e.path); + }); + }) + ); + } + + @:depends(testInfo) + function testUniqueDirectory(async:Async) { + var permissions:FilePermissions = [0, 7, 6, 5]; + asyncAll(async, + FileSystem.uniqueDirectory('test-data/temp/non-existent/dir1', permissions, true, (e, path) -> { + if(noException(e)) + FileSystem.info(path, (e, r) -> { + if(noException(e)) { + isTrue(r.mode.isDirectory()); + #if !cs + isTrue(r.mode.has(permissions)); + #end + } + }); + }), + FileSystem.uniqueDirectory('test-data/temp/non-existent-2/dir2', false, (e, path) -> { + assertType(e, FsException, e -> { + equalPaths('test-data/temp/non-existent-2/dir2', e.path); + }); + }) + ); + } + + function testRealPath(async:Async) { + var expected = Path.join([Sys.getCwd(), 'test-data', 'sub', 'hello.world']); + + asyncAll(async, { + var p:FilePath = 'test-data/sub/.././../test-data////sub/hello.world'; + FileSystem.realPath(p, (e, p) -> { + if(noException(e)) { + equalPaths(expected, p.toString()); + } + }); + }, + #if !cs //C# does not have API to resolve symlinks + { + var p:FilePath = 'test-data/symlink'; + //too much hassle making windows links to work across different machines (CI, local PC, netowrk share etc) + if(!isWindows) + FileSystem.realPath(p, (e, p) -> { + if(noException(e)) { + equalPaths(expected, p.toString()); + } + }); + }, + #end + { + var p:FilePath = 'non-existent'; + FileSystem.realPath(p, (e, r) -> { + assertType(e, FsException, e -> { + isTrue(p == e.path); + }); + }); + }); + } +} diff --git a/tests/asys/src/cases/asys/native/net/TestDns.hx b/tests/asys/src/cases/asys/native/net/TestDns.hx new file mode 100644 index 00000000000..4311c5b93e6 --- /dev/null +++ b/tests/asys/src/cases/asys/native/net/TestDns.hx @@ -0,0 +1,10 @@ +package cases.asys.native.net; + +import utest.Assert; +import asys.native.net.Dns; + +class TestDns extends Test { + function test() { + Assert.pass(); + } +} \ No newline at end of file diff --git a/tests/asys/src/cases/asys/native/net/TestIp.hx b/tests/asys/src/cases/asys/native/net/TestIp.hx new file mode 100644 index 00000000000..531181c028c --- /dev/null +++ b/tests/asys/src/cases/asys/native/net/TestIp.hx @@ -0,0 +1,10 @@ +package cases.asys.native.net; + +import utest.Assert; +import asys.native.net.Ip; + +class TestIp extends Test { + function test() { + Assert.pass(); + } +} \ No newline at end of file diff --git a/tests/asys/src/cases/asys/native/net/TestSecureServer.hx b/tests/asys/src/cases/asys/native/net/TestSecureServer.hx new file mode 100644 index 00000000000..338d5be8958 --- /dev/null +++ b/tests/asys/src/cases/asys/native/net/TestSecureServer.hx @@ -0,0 +1,10 @@ +package cases.asys.native.net; + +import utest.Assert; +import asys.native.net.SecureServer; + +class TestSecureServer extends Test { + function test() { + Assert.pass(); + } +} \ No newline at end of file diff --git a/tests/asys/src/cases/asys/native/net/TestSecureSocket.hx b/tests/asys/src/cases/asys/native/net/TestSecureSocket.hx new file mode 100644 index 00000000000..ec13d2cc5b8 --- /dev/null +++ b/tests/asys/src/cases/asys/native/net/TestSecureSocket.hx @@ -0,0 +1,10 @@ +package cases.asys.native.net; + +import utest.Assert; +import asys.native.net.SecureSocket; + +class TestSecureSocket extends Test { + function test() { + Assert.pass(); + } +} \ No newline at end of file diff --git a/tests/asys/src/cases/asys/native/net/TestServer.hx b/tests/asys/src/cases/asys/native/net/TestServer.hx new file mode 100644 index 00000000000..a6568e850f2 --- /dev/null +++ b/tests/asys/src/cases/asys/native/net/TestServer.hx @@ -0,0 +1,10 @@ +package cases.asys.native.net; + +import utest.Assert; +import asys.native.net.Server; + +class TestServer extends Test { + function test() { + Assert.pass(); + } +} \ No newline at end of file diff --git a/tests/asys/src/cases/asys/native/net/TestSocket.hx b/tests/asys/src/cases/asys/native/net/TestSocket.hx new file mode 100644 index 00000000000..07256fe95d7 --- /dev/null +++ b/tests/asys/src/cases/asys/native/net/TestSocket.hx @@ -0,0 +1,10 @@ +package cases.asys.native.net; + +import utest.Assert; +import asys.native.net.Socket; + +class TestSocket extends Test { + function test() { + Assert.pass(); + } +} \ No newline at end of file diff --git a/tests/asys/src/cases/asys/native/net/TestUdpSocket.hx b/tests/asys/src/cases/asys/native/net/TestUdpSocket.hx new file mode 100644 index 00000000000..efdd9bfa735 --- /dev/null +++ b/tests/asys/src/cases/asys/native/net/TestUdpSocket.hx @@ -0,0 +1,10 @@ +package cases.asys.native.net; + +import utest.Assert; +import asys.native.net.UdpSocket; + +class TestUdpSocket extends Test { + function test() { + Assert.pass(); + } +} \ No newline at end of file diff --git a/tests/asys/src/cases/asys/native/system/TestChildProcess.hx b/tests/asys/src/cases/asys/native/system/TestChildProcess.hx new file mode 100644 index 00000000000..9448bfbcaa3 --- /dev/null +++ b/tests/asys/src/cases/asys/native/system/TestChildProcess.hx @@ -0,0 +1,10 @@ +package cases.asys.native.system; + +import utest.Assert; +import asys.native.system.ChildProcess; + +class TestChildProcess extends Test { + function test() { + Assert.pass(); + } +} \ No newline at end of file diff --git a/tests/asys/src/cases/asys/native/system/TestCurrentProcess.hx b/tests/asys/src/cases/asys/native/system/TestCurrentProcess.hx new file mode 100644 index 00000000000..895783ad494 --- /dev/null +++ b/tests/asys/src/cases/asys/native/system/TestCurrentProcess.hx @@ -0,0 +1,10 @@ +package cases.asys.native.system; + +import utest.Assert; +import asys.native.system.CurrentProcess; + +class TestCurrentProcess extends Test { + function test() { + Assert.pass(); + } +} \ No newline at end of file diff --git a/tests/asys/src/cases/asys/native/system/TestProcess.hx b/tests/asys/src/cases/asys/native/system/TestProcess.hx new file mode 100644 index 00000000000..73527df8dc6 --- /dev/null +++ b/tests/asys/src/cases/asys/native/system/TestProcess.hx @@ -0,0 +1,10 @@ +package cases.asys.native.system; + +import utest.Assert; +import asys.native.system.Process; + +class TestProcess extends Test { + function test() { + Assert.pass(); + } +} \ No newline at end of file diff --git a/tests/asys/src/import.hx b/tests/asys/src/import.hx new file mode 100644 index 00000000000..9c5715f17a2 --- /dev/null +++ b/tests/asys/src/import.hx @@ -0,0 +1,2 @@ +import utest.Assert.*; +import utest.Async; \ No newline at end of file diff --git a/tests/asys/test-data/bytes.bin b/tests/asys/test-data/bytes.bin new file mode 100644 index 00000000000..c86626638e0 Binary files /dev/null and b/tests/asys/test-data/bytes.bin differ diff --git a/tests/asys/test-data/sub/hello.world b/tests/asys/test-data/sub/hello.world new file mode 100644 index 00000000000..5dd01c177f5 --- /dev/null +++ b/tests/asys/test-data/sub/hello.world @@ -0,0 +1 @@ +Hello, world! \ No newline at end of file diff --git a/tests/asys/test-data/symlink b/tests/asys/test-data/symlink new file mode 120000 index 00000000000..4dfb60e198d --- /dev/null +++ b/tests/asys/test-data/symlink @@ -0,0 +1 @@ +sub/hello.world \ No newline at end of file diff --git a/tests/asys/test-data/symlink-dir b/tests/asys/test-data/symlink-dir new file mode 120000 index 00000000000..3de0f365ba5 --- /dev/null +++ b/tests/asys/test-data/symlink-dir @@ -0,0 +1 @@ +sub \ No newline at end of file diff --git a/tests/runci/Config.hx b/tests/runci/Config.hx index 19faea0060a..ee800b03304 100644 --- a/tests/runci/Config.hx +++ b/tests/runci/Config.hx @@ -1,7 +1,7 @@ package runci; -import sys.FileSystem; import haxe.io.Path; +import sys.FileSystem; enum Ci { GithubActions; @@ -13,6 +13,7 @@ class Config { static public final repoDir = FileSystem.fullPath("..") + "/"; static public final unitDir = Path.join([cwd, "unit"]); static public final sysDir = Path.join([cwd, "sys"]); + static public final asysDir = Path.join([cwd, "asys"]); static public final optDir = cwd + "optimization/"; static public final displayDir = Path.join([cwd, "display"]); static public final serverDir = Path.join([cwd, "server"]); @@ -25,20 +26,16 @@ class Config { static public function getMiscSubDir(...subDir:String) return Path.join([cwd, "misc"].concat(subDir.toArray())); - static public final ci:Null = - if (Sys.getEnv("GITHUB_ACTIONS") == "true") - GithubActions; - else - null; + static public final ci:Null = if (Sys.getEnv("GITHUB_ACTIONS") == "true") GithubActions; else null; static public macro function isCi() { return macro $v{ci != null}; } static public final colorSupported = switch [ci, systemName] { - case [GithubActions, _]: true; - case [_, "Linux" | "Mac"]: true; - case [_, "Windows"]: false; - case _: false; - } + case [GithubActions, _]: true; + case [_, "Linux" | "Mac"]: true; + case [_, "Windows"]: false; + case _: false; + } } diff --git a/tests/runci/targets/Cs.hx b/tests/runci/targets/Cs.hx index a2879cf6a06..45696cb48e8 100644 --- a/tests/runci/targets/Cs.hx +++ b/tests/runci/targets/Cs.hx @@ -67,6 +67,10 @@ class Cs { runSysTest("mono", [exe]); } + changeDirectory(asysDir); + runCommand("haxe", ["compile-cs.hxml",'-D'].concat(args)); + runCs("bin/cs/bin/Main-Debug.exe", []); + changeDirectory(threadsDir); runCommand("haxe", ["build.hxml", "-cs", "export/cs"]); runCs("export/cs/bin/Main.exe"); diff --git a/tests/runci/targets/Hl.hx b/tests/runci/targets/Hl.hx index eba72b2a951..fcf0ded8ba3 100644 --- a/tests/runci/targets/Hl.hx +++ b/tests/runci/targets/Hl.hx @@ -91,6 +91,10 @@ class Hl { runCommand("haxe", ["compile-hl.hxml"].concat(args)); runSysTest(hlBinary, ["bin/hl/sys.hl"]); + changeDirectory(asysDir); + runCommand("haxe", ["compile-hl.hxml"].concat(args)); + runCommand(hlBinary, ["bin/asys.hl"]); + changeDirectory(miscHlDir); runCommand("haxe", ["run.hxml"]); } diff --git a/tests/runci/targets/Java.hx b/tests/runci/targets/Java.hx index 1248b41776f..2be8bd3020f 100644 --- a/tests/runci/targets/Java.hx +++ b/tests/runci/targets/Java.hx @@ -31,6 +31,10 @@ class Java { runCommand("haxe", ["compile-java.hxml"].concat(args)); runSysTest("java", ["-jar", "bin/java/Main-Debug.jar"]); + changeDirectory(asysDir); + runCommand("haxe", ["compile-java.hxml"].concat(args)); + runCommand("java", ["-jar", "bin/java/Main-Debug.jar"]); + changeDirectory(threadsDir); runCommand("haxe", ["build.hxml", "-java", "export/java"].concat(args)); runCommand("java", ["-jar", "export/java/Main.jar"]); diff --git a/tests/runci/targets/Jvm.hx b/tests/runci/targets/Jvm.hx index 1aa4927eb06..31747646c9d 100644 --- a/tests/runci/targets/Jvm.hx +++ b/tests/runci/targets/Jvm.hx @@ -21,6 +21,10 @@ class Jvm { runCommand("haxe", ["compile-jvm.hxml"].concat(args)); runSysTest("java", ["-jar", "bin/jvm/sys.jar"]); + changeDirectory(asysDir); + runCommand("haxe", ["compile-jvm.hxml"].concat(args)); + runCommand("java", ["-jar", "bin/jvm/asys.jar"]); + changeDirectory(threadsDir); runCommand("haxe", ["build.hxml", "--jvm", "export/threads.jar"].concat(args)); runCommand("java", ["-jar", "export/threads.jar"]); diff --git a/tests/runci/targets/Lua.hx b/tests/runci/targets/Lua.hx index f907834acd0..49365b8502c 100644 --- a/tests/runci/targets/Lua.hx +++ b/tests/runci/targets/Lua.hx @@ -1,43 +1,45 @@ package runci.targets; -import runci.System.*; -import runci.Config.*; import haxe.io.*; +import runci.Config.*; +import runci.System.*; + using StringTools; class Lua { static final miscLuaDir = getMiscSubDir('lua'); - static public function getLuaDependencies(){ - switch (systemName){ + static public function getLuaDependencies() { + switch (systemName) { case "Linux": Linux.requireAptPackages(["libpcre3-dev", "libssl-dev", "libreadline-dev"]); runCommand("pip", ["install", "--user", "hererocks"]); final pyUserBase = commandResult("python", ["-m", "site", "--user-base"]).stdout.trim(); addToPATH(Path.join([pyUserBase, "bin"])); - case "Mac": { - if (commandSucceed("python3", ["-V"])) - infoMsg('python3 has already been installed.'); - else - runNetworkCommand("brew", ["install", "python3"]); - - attemptCommand("brew", ["install", "pcre"]); - runCommand("pip3", ["install", "hererocks"]); - runCommand("brew", ["install", "openssl"]); - } + case "Mac": + { + if (commandSucceed("python3", ["-V"])) + infoMsg('python3 has already been installed.'); + else + runNetworkCommand("brew", ["install", "python3"]); + + attemptCommand("brew", ["install", "pcre"]); + runCommand("pip3", ["install", "hererocks"]); + runCommand("brew", ["install", "openssl"]); + } } } - static function installLib(lib : String, version : String, ?server :String){ + static function installLib(lib:String, version:String, ?server:String) { if (!commandSucceed("luarocks", ["show", lib, version])) { - final args = ["install", lib, version]; + final args = ["install", lib, version]; if (systemName == "Mac") { args.push('OPENSSL_DIR=/usr/local/opt/openssl@3'); } - if (server != null){ - final server_arg = '--server=$server'; - args.push(server_arg); - } + if (server != null) { + final server_arg = '--server=$server'; + args.push(server_arg); + } runCommand("luarocks", args); } else { infoMsg('Lua dependency $lib is already installed at version $version'); @@ -45,21 +47,20 @@ class Lua { } static public function run(args:Array) { - getLuaDependencies(); for (lv in ["-l5.1", "-l5.2", "-l5.3"].concat(systemName == 'Linux' && Linux.arch == Arm64 ? [] : ["-j2.0", "-j2.1"])) { final envpath = getInstallPath() + '/lua_env/lua$lv'; addToPATH(envpath + '/bin'); - if (systemName == "Mac" && lv.startsWith("-j")) continue; + if (systemName == "Mac" && lv.startsWith("-j")) + continue; Sys.println('--------------------'); Sys.println('Lua Version: $lv'); runCommand("hererocks", [envpath, lv, "-rlatest", "-i"]); trace('path: ' + Sys.getEnv("PATH")); - - runCommand("lua",["-v"]); + runCommand("lua", ["-v"]); runCommand("luarocks", ["config", "--lua-incdir"]); runCommand("luarocks", ["config", "--lua-libdir"]); @@ -81,6 +82,10 @@ class Lua { runCommand("haxe", ["compile-lua.hxml"].concat(args)); runSysTest("lua", ["bin/lua/sys.lua"]); + changeDirectory(asysDir); + runCommand("haxe", ["compile-lua.hxml"].concat(args)); + runCommand("lua", ["bin/asys.lua"]); + changeDirectory(getMiscSubDir("luaDeadCode", "stringReflection")); runCommand("haxe", ["compile.hxml"]); diff --git a/tests/runci/targets/Macro.hx b/tests/runci/targets/Macro.hx index c0eab03ec8c..c945b29f794 100644 --- a/tests/runci/targets/Macro.hx +++ b/tests/runci/targets/Macro.hx @@ -29,6 +29,9 @@ class Macro { changeDirectory(sysDir); runSysTest("haxe", ["compile-macro.hxml"].concat(args)); + changeDirectory(asysDir); + runCommand("haxe", ["compile-macro.hxml"].concat(args)); + switch Sys.systemName() { case 'Linux': changeDirectory(getMiscSubDir('compiler_loops')); diff --git a/tests/runci/targets/Neko.hx b/tests/runci/targets/Neko.hx index f37b61eea18..1054fb9b150 100644 --- a/tests/runci/targets/Neko.hx +++ b/tests/runci/targets/Neko.hx @@ -12,6 +12,10 @@ class Neko { runCommand("haxe", ["compile-neko.hxml"].concat(args)); runSysTest("neko", ["bin/neko/sys.n"]); + changeDirectory(sysDir); + runCommand("haxe", ["compile-neko.hxml"].concat(args)); + runCommand("neko", ["bin/asys.n"]); + changeDirectory(threadsDir); runCommand("haxe", ["build.hxml", "--neko", "export/threads.n"]); runCommand("neko", ["export/threads.n"]); diff --git a/tests/runci/targets/Php.hx b/tests/runci/targets/Php.hx index e09a0a4a6a8..d6abd9816cc 100644 --- a/tests/runci/targets/Php.hx +++ b/tests/runci/targets/Php.hx @@ -1,7 +1,7 @@ package runci.targets; -import runci.System.*; import runci.Config.*; +import runci.System.*; using haxe.io.Path; @@ -35,10 +35,7 @@ class Php { static public function getPhpDependencies() { final phpCmd = commandResult("php", ["-v"]); final phpVerReg = ~/PHP ([0-9]+\.[0-9]+)/i; - final phpVer = if (phpVerReg.match(phpCmd.stdout)) - Std.parseFloat(phpVerReg.matched(1)); - else - null; + final phpVer = if (phpVerReg.match(phpCmd.stdout)) Std.parseFloat(phpVerReg.matched(1)); else null; if (phpCmd.exitCode == 0 && phpVer != null && phpVer >= 7.0) { switch systemName { @@ -74,25 +71,32 @@ class Php { final binDir = "bin/php"; final prefixes = [[]]; - if(isCi()) { + if (isCi()) { prefixes.push(['-D', 'php-prefix=haxe']); prefixes.push(['-D', 'php-prefix=my.pack']); } - for(prefix in prefixes) { + for (prefix in prefixes) { changeDirectory(unitDir); - if(isCi()) + if (isCi()) deleteDirectoryRecursively(binDir); runCommand("haxe", ["compile-php.hxml"].concat(prefix).concat(args)); runCommand("php", generateArgs(binDir + "/index.php")); changeDirectory(sysDir); - if(isCi()) + if (isCi()) deleteDirectoryRecursively(binDir); runCommand("haxe", ["compile-php.hxml"].concat(prefix).concat(args)); runSysTest("php", generateArgs(binDir + "/Main/index.php")); + + changeDirectory(asysDir); + if (isCi()) { + deleteDirectoryRecursively(binDir); + } + runCommand("haxe", ["compile-php.hxml"].concat(prefix).concat(args)); + runCommand("php", generateArgs(binDir + "/index.php")); } } } diff --git a/tests/runci/targets/Python.hx b/tests/runci/targets/Python.hx index 5d603490279..a61053b6255 100644 --- a/tests/runci/targets/Python.hx +++ b/tests/runci/targets/Python.hx @@ -85,5 +85,11 @@ class Python { for (py in pys) { runCommand(py, ["export/threads.py"]); } + + changeDirectory(asysDir); + runCommand("haxe", ["compile-python.hxml"].concat(args)); + for (py in pys) { + runCommand(py, ["bin/test.py"]); + } } } diff --git a/tests/threads/src/misc/TestThreadPoolBase.hx b/tests/threads/src/misc/TestThreadPoolBase.hx index d787a10f138..48350f6d10c 100644 --- a/tests/threads/src/misc/TestThreadPoolBase.hx +++ b/tests/threads/src/misc/TestThreadPoolBase.hx @@ -4,6 +4,7 @@ import sys.thread.IThreadPool; import sys.thread.ThreadPoolException; import sys.thread.Deque; import sys.thread.Mutex; +import haxe.Exception; import haxe.Timer; abstract class TestThreadPoolBase extends utest.Test { @@ -101,4 +102,39 @@ abstract class TestThreadPoolBase extends utest.Test { pool.shutdown(); pass(); } + + function testRunFor(async:Async) { + var mainThread = Thread.current(); + var pool = createThreadPool(1); + //result + async.branch(async -> { + pool.runFor( + () -> { + isFalse(mainThread == Thread.current()); + return 123; + }, + (e, r) -> { + if(e != null) + fail(e.message); + isTrue(mainThread == Thread.current()); + equals(123, r); + async.done(); + } + ); + }); + //exception + async.branch(async -> { + pool.runFor( + () -> { + isFalse(mainThread == Thread.current()); + throw new Exception(''); + }, + (e, r) -> { + isOfType(e, Exception); + isTrue(mainThread == Thread.current()); + async.done(); + } + ); + }); + } } \ No newline at end of file