core: standardize Windows path handling
[nit.git] / lib / core / file.nit
index 0454f1d..09b20d9 100644 (file)
@@ -28,8 +28,16 @@ in "C Header" `{
        #include <sys/stat.h>
        #include <unistd.h>
        #include <stdio.h>
-       #include <poll.h>
        #include <errno.h>
+#ifndef _WIN32
+       #include <poll.h>
+#endif
+`}
+
+in "C" `{
+#ifdef _WIN32
+       #include <windows.h>
+#endif
 `}
 
 # `Stream` used to interact with a File or FileDescriptor
@@ -49,23 +57,24 @@ abstract class FileStream
        # Return null in case of error
        fun file_stat: nullable FileStat
        do
-               var stat = _file.file_stat
+               var stat = _file.as(not null).file_stat
                if stat.address_is_null then return null
                return new FileStat(stat)
        end
 
        # File descriptor of this file
-       fun fd: Int do return _file.fileno
+       fun fd: Int do return _file.as(not null).fileno
 
        redef fun close
        do
-               if _file == null then return
-               if _file.address_is_null then
+               var file = _file
+               if file == null then return
+               if file.address_is_null then
                        if last_error != null then return
                        last_error = new IOError("Cannot close unopened file")
                        return
                end
-               var i = _file.io_close
+               var i = file.io_close
                if i != 0 then
                        last_error = new IOError("Close failed due to error {sys.errno.strerror}")
                end
@@ -83,7 +92,7 @@ abstract class FileStream
        # * `buffer_mode_none`
        fun set_buffering_mode(buf_size, mode: Int) do
                if buf_size <= 0 then buf_size = 512
-               if _file.set_buffering_type(buf_size, mode) != 0 then
+               if _file.as(not null).set_buffering_type(buf_size, mode) != 0 then
                        last_error = new IOError("Error while changing buffering type for FileStream, returned error {sys.errno.strerror}")
                end
        end
@@ -105,10 +114,10 @@ class FileReader
        #     assert l == f.read_line
        fun reopen
        do
-               if not eof and not _file.address_is_null then close
+               if not eof and not _file.as(not null).address_is_null then close
                last_error = null
-               _file = new NativeFile.io_open_read(path.to_cstring)
-               if _file.address_is_null then
+               _file = new NativeFile.io_open_read(path.as(not null).to_cstring)
+               if _file.as(not null).address_is_null then
                        last_error = new IOError("Cannot open `{path.as(not null)}`: {sys.errno.strerror}")
                        end_reached = true
                        return
@@ -126,8 +135,8 @@ class FileReader
 
        redef fun fill_buffer
        do
-               var nb = _file.io_read(_buffer, _buffer_capacity)
-               if last_error == null and _file.ferror then
+               var nb = _file.as(not null).io_read(_buffer, _buffer_capacity)
+               if last_error == null and _file.as(not null).ferror then
                        last_error = new IOError("Cannot read `{path.as(not null)}`: {sys.errno.strerror}")
                        end_reached = true
                end
@@ -158,7 +167,7 @@ class FileReader
                self.path = path
                prepare_buffer(100)
                _file = new NativeFile.io_open_read(path.to_cstring)
-               if _file.address_is_null then
+               if _file.as(not null).address_is_null then
                        last_error = new IOError("Cannot open `{path}`: {sys.errno.strerror}")
                        end_reached = true
                end
@@ -171,7 +180,7 @@ class FileReader
                self.path = ""
                prepare_buffer(1)
                _file = fd.fd_to_stream(read_only)
-               if _file.address_is_null then
+               if _file.as(not null).address_is_null then
                        last_error = new IOError("Error: Converting fd {fd} to stream failed with '{sys.errno.strerror}'")
                        end_reached = true
                end
@@ -187,8 +196,12 @@ class FileReader
        end
 
        private fun native_poll_in(fd: Int): Int `{
+#ifndef _WIN32
                struct pollfd fds = {(int)fd, POLLIN, 0};
                return poll(&fds, 1, 0);
+#else
+               return 0;
+#endif
        `}
 end
 
@@ -223,13 +236,13 @@ class FileWriter
                        last_error = new IOError("Cannot write to non-writable stream")
                        return
                end
-               if _file.address_is_null then
+               if _file.as(not null).address_is_null then
                        last_error = new IOError("Writing on a null stream")
                        _is_writable = false
                        return
                end
 
-               var err = _file.write_byte(value)
+               var err = _file.as(not null).write_byte(value)
                if err != 1 then
                        # Big problem
                        last_error = new IOError("Problem writing a byte: {err}")
@@ -244,19 +257,19 @@ class FileWriter
        redef var is_writable = false
 
        # Write `len` bytes from `native`.
-       private fun write_native(native: NativeString, from, len: Int)
+       private fun write_native(native: CString, from, len: Int)
        do
                if last_error != null then return
                if not _is_writable then
                        last_error = new IOError("Cannot write to non-writable stream")
                        return
                end
-               if _file.address_is_null then
+               if _file.as(not null).address_is_null then
                        last_error = new IOError("Writing on a null stream")
                        _is_writable = false
                        return
                end
-               var err = _file.io_write(native, from, len)
+               var err = _file.as(not null).io_write(native, from, len)
                if err != len then
                        # Big problem
                        last_error = new IOError("Problem in writing : {err} {len} \n")
@@ -269,7 +282,7 @@ class FileWriter
                _file = new NativeFile.io_open_write(path.to_cstring)
                self.path = path
                _is_writable = true
-               if _file.address_is_null then
+               if _file.as(not null).address_is_null then
                        last_error = new IOError("Cannot open `{path}`: {sys.errno.strerror}")
                        is_writable = false
                end
@@ -280,7 +293,7 @@ class FileWriter
                self.path = ""
                _file = fd.fd_to_stream(wipe_write)
                _is_writable = true
-                if _file.address_is_null then
+                if _file.as(not null).address_is_null then
                         last_error = new IOError("Error: Opening stream from file descriptor {fd} failed with '{sys.errno.strerror}'")
                        _is_writable = false
                end
@@ -291,20 +304,20 @@ redef class Int
        # Creates a file stream from a file descriptor `fd` using the file access `mode`.
        #
        # NOTE: The `mode` specified must be compatible with the one used in the file descriptor.
-       private fun fd_to_stream(mode: NativeString): NativeFile `{
+       private fun fd_to_stream(mode: CString): NativeFile `{
                return fdopen((int)self, mode);
        `}
 end
 
 # Constant for read-only file streams
-private fun read_only: NativeString do return once "r".to_cstring
+private fun read_only: CString do return once "r".to_cstring
 
 # Constant for write-only file streams
 #
 # If a stream is opened on a file with this method,
 # it will wipe the previous file if any.
 # Else, it will create the file.
-private fun wipe_write: NativeString do return once "w".to_cstring
+private fun wipe_write: CString do return once "w".to_cstring
 
 ###############################################################################
 
@@ -498,8 +511,8 @@ class Path
                var output = dest.open_wo
 
                while not input.eof do
-                       var buffer = input.read(1024)
-                       output.write buffer
+                       var buffer = input.read_bytes(1024)
+                       output.write_bytes buffer
                end
 
                input.close
@@ -625,6 +638,22 @@ class Path
                return res
        end
 
+       # Correctly join `self` with `subpath` using the directory separator.
+       #
+       # Using a standard "{self}/{path}" does not work in the following cases:
+       #
+       # * `self` is empty.
+       # * `path` starts with `'/'`.
+       #
+       # This method ensures that the join is valid.
+       #
+       #     var hello = "hello".to_path
+       #     assert (hello/"world").to_s   == "hello/world"
+       #     assert ("hel/lo".to_path / "wor/ld").to_s == "hel/lo/wor/ld"
+       #     assert ("".to_path / "world").to_s == "world"
+       #     assert (hello / "/world").to_s  == "/world"
+       #     assert ("hello/".to_path / "world").to_s  == "hello/world"
+       fun /(subpath: String): Path do return new Path(path / subpath)
 
        # Lists the files contained within the directory at `path`.
        #
@@ -654,35 +683,54 @@ class Path
                                # readdir cannot fail, so null means end of list
                                break
                        end
-                       var name = de.to_s_with_copy
+                       var name = de.to_s
                        if name == "." or name == ".." then continue
-                       res.add new Path(path / name)
+                       res.add self / name
                end
                d.closedir
 
                return res
        end
 
-       # Delete a directory and all of its content
+       # Is `self` the path to an existing directory ?
+       #
+       # ~~~nit
+       # assert ".".to_path.is_dir
+       # assert not "/etc/issue".to_path.is_dir
+       # assert not "/should/not/exist".to_path.is_dir
+       # ~~~
+       fun is_dir: Bool do
+               var st = stat
+               if st == null then return false
+               return st.is_dir
+       end
+
+       # Recursively delete a directory and all of its content
        #
        # Does not go through symbolic links and may get stuck in a cycle if there
        # is a cycle in the file system.
        #
-       # `last_error` is updated to contains the error information on error, and null on success.
-       # The method does not stop on the first error and try to remove most file and directories.
+       # `last_error` is updated with the first encountered error, or null on success.
+       # The method does not stop on the first error and tries to remove the most files and directories.
        #
        # ~~~
        # var path = "/does/not/exists/".to_path
        # path.rmdir
        # assert path.last_error != null
+       #
+       # path = "/tmp/path/to/create".to_path
+       # path.to_s.mkdir
+       # assert path.exists
+       # path.rmdir
+       # assert path.last_error == null
        # ~~~
        fun rmdir
        do
-               last_error = null
+               var first_error = null
                for file in self.files do
                        var stat = file.link_stat
                        if stat == null then
-                               last_error = file.last_error
+                               if first_error == null then first_error = file.last_error
                                continue
                        end
                        if stat.is_dir then
@@ -691,15 +739,16 @@ class Path
                        else
                                file.delete
                        end
-                       if last_error == null then last_error = file.last_error
+                       if first_error == null then first_error = file.last_error
                end
 
                # Delete the directory itself if things are fine
-               if last_error == null then
-                       if path.to_cstring.rmdir then
-                               last_error = new IOError("Cannot remove `{self}`: {sys.errno.strerror}")
+               if first_error == null then
+                       if not path.to_cstring.rmdir then
+                               first_error = new IOError("Cannot remove `{self}`: {sys.errno.strerror}")
                        end
                end
+               self.last_error = first_error
        end
 
        redef fun ==(other) do return other isa Path and simplified.path == other.simplified.path
@@ -844,14 +893,14 @@ redef class Text
 
        private fun write_native_to(s: FileWriter)
        do
-               for i in substrings do s.write_native(i.to_cstring, 0, i.bytelen)
+               for i in substrings do s.write_native(i.to_cstring, 0, i.byte_length)
        end
-end
 
-redef class String
        # return true if a file with this names exists
        fun file_exists: Bool do return to_cstring.file_exists
+end
 
+redef class String
        # The status of a file. see POSIX stat(2).
        fun file_stat: nullable FileStat
        do
@@ -914,17 +963,26 @@ redef class String
        #     assert "path/to".basename(".ext")                 == "to"
        #     assert "path/to/".basename(".ext")                == "to"
        #     assert "path/to".basename                         == "to"
-       #     assert "path".basename("")                        == "path"
-       #     assert "/path".basename("")                       == "path"
-       #     assert "/".basename("")                           == "/"
-       #     assert "".basename("")                            == ""
+       #     assert "path".basename                            == "path"
+       #     assert "/path".basename                           == "path"
+       #     assert "/".basename                               == "/"
+       #     assert "".basename                                == ""
+       #
+       # On Windows, '\' are replaced by '/':
+       #
+       # ~~~nitish
+       # assert "C:\\path\\to\\a_file.ext".basename(".ext")    == "a_file"
+       # assert "C:\\".basename                                == "C:"
+       # ~~~
        fun basename(extension: nullable String): String
        do
+               var n = self
+               if is_windows then n = n.replace("\\", "/")
+
                var l = length - 1 # Index of the last char
                while l > 0 and self.chars[l] == '/' do l -= 1 # remove all trailing `/`
                if l == 0 then return "/"
                var pos = chars.last_index_of_from('/', l)
-               var n = self
                if pos >= 0 then
                        n = substring(pos+1, l-pos)
                end
@@ -944,13 +1002,23 @@ redef class String
        #     assert "/path".dirname                       == "/"
        #     assert "/".dirname                           == "/"
        #     assert "".dirname                            == "."
+       #
+       # On Windows, '\' are replaced by '/':
+       #
+       # ~~~nitish
+       # assert "C:\\path\\to\\a_file.ext".dirname        == "C:/path/to"
+       # assert "C:\\file".dirname                        == "C:"
+       # ~~~
        fun dirname: String
        do
+               var s = self
+               if is_windows then s = s.replace("\\", "/")
+
                var l = length - 1 # Index of the last char
-               while l > 0 and self.chars[l] == '/' do l -= 1 # remove all trailing `/`
-               var pos = chars.last_index_of_from('/', l)
+               while l > 0 and s.chars[l] == '/' do l -= 1 # remove all trailing `/`
+               var pos = s.chars.last_index_of_from('/', l)
                if pos > 0 then
-                       return substring(0, pos)
+                       return s.substring(0, pos)
                else if pos == 0 then
                        return "/"
                else
@@ -964,7 +1032,7 @@ redef class String
        fun realpath: String do
                var cs = to_cstring.file_realpath
                assert file_exists
-               var res = cs.to_s_with_copy
+               var res = cs.to_s
                cs.free
                return res
        end
@@ -995,9 +1063,18 @@ redef class String
        # assert "./../dir".simplify_path                  == "../dir"
        # assert "./dir".simplify_path                     == "dir"
        # ~~~
+       #
+       # On Windows, '\' are replaced by '/':
+       #
+       # ~~~nitish
+       # assert "C:\\some\\.\\complex\\../../path/to/a_file.ext".simplify_path == "C:/path/to/a_file.ext"
+       # assert "C:\\".simplify_path              == "C:"
+       # ~~~
        fun simplify_path: String
        do
-               var a = self.split_with("/")
+               var s = self
+               if is_windows then s = s.replace("\\", "/")
+               var a = s.split_with("/")
                var a2 = new Array[String]
                for x in a do
                        if x == "." and not a2.is_empty then continue # skip `././`
@@ -1122,6 +1199,7 @@ redef class String
        #     assert "/" + "/".relpath(".") == getcwd
        fun relpath(dest: String): String
        do
+               # TODO windows support
                var cwd = getcwd
                var from = (cwd/self).simplify_path.split("/")
                if from.last.is_empty then from.pop # case for the root directory
@@ -1154,8 +1232,10 @@ redef class String
        fun mkdir(mode: nullable Int): nullable Error
        do
                mode = mode or else 0o777
+               var s = self
+               if is_windows then s = s.replace("\\", "/")
 
-               var dirs = self.split_with("/")
+               var dirs = s.split_with("/")
                var path = new FlatBuffer
                if dirs.is_empty then return null
                if dirs[0].is_empty then
@@ -1163,15 +1243,21 @@ redef class String
                        path.add('/')
                end
                var error: nullable Error = null
-               for d in dirs do
+               for i in [0 .. dirs.length - 1[ do
+                       var d = dirs[i]
                        if d.is_empty then continue
                        path.append(d)
                        path.add('/')
-                       var res = path.to_s.to_cstring.file_mkdir(mode)
+                       if path.file_exists then continue
+                       var res = path.to_cstring.file_mkdir(mode)
                        if not res and error == null then
                                error = new IOError("Cannot create directory `{path}`: {sys.errno.strerror}")
                        end
                end
+               var res = s.to_cstring.file_mkdir(mode)
+               if not res and error == null then
+                       error = new IOError("Cannot create directory `{path}`: {sys.errno.strerror}")
+               end
                return error
        end
 
@@ -1258,7 +1344,7 @@ redef class String
                loop
                        var de = d.readdir
                        if de.address_is_null then break
-                       var name = de.to_s_with_copy
+                       var name = de.to_s
                        if name == "." or name == ".." then continue
                        res.add name
                end
@@ -1271,7 +1357,7 @@ end
 redef class FlatString
        redef fun write_native_to(s)
        do
-               s.write_native(items, first_byte, bytelen)
+               s.write_native(items, first_byte, byte_length)
        end
 
        redef fun file_extension do
@@ -1289,27 +1375,36 @@ redef class FlatString
        end
 
        redef fun basename(extension) do
-               var l = last_byte
-               var its = _items
-               var min = _first_byte
+               var s = self
+               if is_windows then s = s.replace("\\", "/").as(FlatString)
+
+               var bname
+               var l = s.last_byte
+               var its = s._items
+               var min = s._first_byte
                var sl = '/'.ascii
                while l > min and its[l] == sl do l -= 1
                if l == min then return "/"
                var ns = l
                while ns >= min and its[ns] != sl do ns -= 1
-               var bname = new FlatString.with_infos(its, l - ns, ns + 1)
+               bname = new FlatString.with_infos(its, l - ns, ns + 1)
 
                return if extension != null then bname.strip_extension(extension) else bname
        end
 end
 
-redef class NativeString
+redef class CString
        private fun file_exists: Bool `{
+#ifdef _WIN32
+               DWORD attribs = GetFileAttributesA(self);
+               return attribs != INVALID_FILE_ATTRIBUTES;
+#else
                FILE *hdl = fopen(self,"r");
                if(hdl != NULL){
                        fclose(hdl);
                }
                return hdl != NULL;
+#endif
        `}
 
        private fun file_stat: NativeFileStat `{
@@ -1323,15 +1418,26 @@ redef class NativeString
        `}
 
        private fun file_lstat: NativeFileStat `{
+#ifdef _WIN32
+               // FIXME use a higher level abstraction to support WIN32
+               return NULL;
+#else
                struct stat* stat_element;
                int res;
                stat_element = malloc(sizeof(struct stat));
                res = lstat(self, stat_element);
                if (res == -1) return NULL;
                return stat_element;
+#endif
        `}
 
-       private fun file_mkdir(mode: Int): Bool `{ return !mkdir(self, mode); `}
+       private fun file_mkdir(mode: Int): Bool `{
+#ifdef _WIN32
+               return !mkdir(self);
+#else
+               return !mkdir(self, mode);
+#endif
+       `}
 
        private fun rmdir: Bool `{ return !rmdir(self); `}
 
@@ -1341,7 +1447,16 @@ redef class NativeString
 
        private fun file_chdir: Bool `{ return !chdir(self); `}
 
-       private fun file_realpath: NativeString `{ return realpath(self, NULL); `}
+       private fun file_realpath: CString `{
+#ifdef _WIN32
+               DWORD len = GetFullPathName(self, 0, NULL, NULL);
+               char *buf = malloc(len+1); // FIXME don't leak memory
+               len = GetFullPathName(self, len+1, buf, NULL);
+               return buf;
+#else
+               return realpath(self, NULL);
+#endif
+       `}
 end
 
 # This class is system dependent ... must reify the vfs
@@ -1378,20 +1493,37 @@ private extern class NativeFileStat `{ struct stat * `}
        fun is_fifo: Bool `{ return S_ISFIFO(self->st_mode); `}
 
        # Returns true if the type is a link
-       fun is_lnk: Bool `{ return S_ISLNK(self->st_mode); `}
+       fun is_lnk: Bool `{
+#ifdef _WIN32
+       return 0;
+#else
+       return S_ISLNK(self->st_mode);
+#endif
+       `}
 
        # Returns true if the type is a socket
-       fun is_sock: Bool `{ return S_ISSOCK(self->st_mode); `}
+       fun is_sock: Bool `{
+#ifdef _WIN32
+       return 0;
+#else
+       return S_ISSOCK(self->st_mode);
+#endif
+       `}
 end
 
 # Instance of this class are standard FILE * pointers
 private extern class NativeFile `{ FILE* `}
-       fun io_read(buf: NativeString, len: Int): Int `{
+       fun io_read(buf: CString, len: Int): Int `{
                return fread(buf, 1, len, self);
        `}
 
-       fun io_write(buf: NativeString, from, len: Int): Int `{
-               return fwrite(buf+from, 1, len, self);
+       fun io_write(buf: CString, from, len: Int): Int `{
+               size_t res = fwrite(buf+from, 1, len, self);
+#ifdef _WIN32
+               // Force flushing buffer because end of line does not trigger a flush
+               fflush(self);
+#endif
+               return (long)res;
        `}
 
        fun write_byte(value: Byte): Int `{
@@ -1423,9 +1555,9 @@ private extern class NativeFile `{ FILE* `}
                return setvbuf(self, NULL, (int)mode, buf_length);
        `}
 
-       new io_open_read(path: NativeString) `{ return fopen(path, "r"); `}
+       new io_open_read(path: CString) `{ return fopen(path, "r"); `}
 
-       new io_open_write(path: NativeString) `{ return fopen(path, "w"); `}
+       new io_open_write(path: CString) `{ return fopen(path, "w"); `}
 
        new native_stdin `{ return stdin; `}
 
@@ -1438,13 +1570,13 @@ end
 private extern class NativeDir `{ DIR* `}
 
        # Open a directory
-       new opendir(path: NativeString) `{ return opendir(path); `}
+       new opendir(path: CString) `{ return opendir(path); `}
 
        # Close a directory
        fun closedir `{ closedir(self); `}
 
        # Read the next directory entry
-       fun readdir: NativeString `{
+       fun readdir: CString `{
                struct dirent *de;
                de = readdir(self);
                if (!de) return NULL;
@@ -1498,6 +1630,9 @@ redef class Sys
 
        private fun intern_poll(in_fds: Array[Int], out_fds: Array[Int]): nullable Int
        import Array[Int].length, Array[Int].[], Int.as(nullable Int) `{
+#ifndef _WIN32
+               // FIXME use a higher level abstraction to support WIN32
+
                int in_len, out_len, total_len;
                struct pollfd *c_fds;
                int i;
@@ -1542,6 +1677,7 @@ redef class Sys
                }
                else if ( result < 0 )
                        fprintf( stderr, "Error in Stream:poll: %s\n", strerror( errno ) );
+#endif
 
                return null_Int();
        `}
@@ -1585,4 +1721,4 @@ end
 # Return the working (current) directory
 fun getcwd: String do return native_getcwd.to_s
 
-private fun native_getcwd: NativeString `{ return getcwd(NULL, 0); `}
+private fun native_getcwd: CString `{ return getcwd(NULL, 0); `}