// Written in the D programming language.

/**
Standard I/O functions that extend $(B std.c.stdio).  $(B std.c.stdio)
is $(D_PARAM public)ally imported when importing $(B std.stdio).

Source: $(PHOBOSSRC std/_stdio.d)
Macros:
WIKI=Phobos/StdStdio

Copyright: Copyright Digital Mars 2007-.
License:   $(WEB boost.org/LICENSE_1_0.txt, Boost License 1.0).
Authors:   $(WEB digitalmars.com, Walter Bright),
           $(WEB erdani.org, Andrei Alexandrescu),
           Alex Rønne Petersen
 */
module std.stdio;

public import core.stdc.stdio, std.string : KeepTerminator;
static import std.c.stdio;
import std.stdiobase;
import core.stdc.errno, core.stdc.stddef, core.stdc.stdlib, core.memory,
    core.stdc.string, core.stdc.wchar_;
import std.algorithm, std.array, std.conv, std.exception, std.format,
    std.range, std.string, std.traits, std.typecons,
    std.typetuple, std.utf;
version(unittest) import std.file;

version (DigitalMars)
{
    version (Win32)
    {
        // Specific to the way Digital Mars C does stdio
        version = DIGITAL_MARS_STDIO;
        import std.c.stdio : __fhnd_info, FHND_WCHAR, FHND_TEXT;
    }
    else version (Win64)
    {
        version = MICROSOFT_STDIO;
    }
}

version (Posix)
{
    import core.sys.posix.stdio;
    alias core.sys.posix.stdio.fileno fileno;
}

version (linux)
{
    // Specific to the way Gnu C does stdio
    version = GCC_IO;
}

version (OSX)
{
    version = GENERIC_IO;
}

version (FreeBSD)
{
    version = GENERIC_IO;
}

version(Windows)
{
    // core.stdc.stdio.fopen expects file names to be
    // encoded in CP_ACP on Windows instead of UTF-8.
    /+ Waiting for druntime pull 299
    +/
    extern (C) nothrow FILE* _wfopen(in wchar* filename, in wchar* mode);
}

version (DIGITAL_MARS_STDIO)
{
    extern (C)
    {
        /* **
         * Digital Mars under-the-hood C I/O functions.
         * Use _iobuf* for the unshared version of FILE*,
         * usable when the FILE is locked.
         */
        int _fputc_nlock(int, _iobuf*);
        int _fputwc_nlock(int, _iobuf*);
        int _fgetc_nlock(_iobuf*);
        int _fgetwc_nlock(_iobuf*);
        int __fp_lock(FILE*);
        void __fp_unlock(FILE*);

        int setmode(int, int);
    }
    alias _fputc_nlock FPUTC;
    alias _fputwc_nlock FPUTWC;
    alias _fgetc_nlock FGETC;
    alias _fgetwc_nlock FGETWC;

    alias __fp_lock FLOCK;
    alias __fp_unlock FUNLOCK;

    alias setmode _setmode;
    enum _O_BINARY = 0x8000;
    int _fileno(FILE* f) { return f._file; }
    alias _fileno fileno;
}
else version (MICROSOFT_STDIO)
{
    extern (C)
    {
        /* **
         * Microsoft under-the-hood C I/O functions
         */
        int _fputc_nolock(int, _iobuf*);
        int _fputwc_nolock(int, _iobuf*);
        int _fgetc_nolock(_iobuf*);
        int _fgetwc_nolock(_iobuf*);
        void _lock_file(FILE*);
        void _unlock_file(FILE*);
    }
    alias _fputc_nolock FPUTC;
    alias _fputwc_nolock FPUTWC;
    alias _fgetc_nolock FGETC;
    alias _fgetwc_nolock FGETWC;

    alias _lock_file FLOCK;
    alias _unlock_file FUNLOCK;
}
else version (GCC_IO)
{
    /* **
     * Gnu under-the-hood C I/O functions; see
     * http://gnu.org/software/libc/manual/html_node/I_002fO-on-Streams.html
     */
    extern (C)
    {
        int fputc_unlocked(int, _iobuf*);
        int fputwc_unlocked(wchar_t, _iobuf*);
        int fgetc_unlocked(_iobuf*);
        int fgetwc_unlocked(_iobuf*);
        void flockfile(FILE*);
        void funlockfile(FILE*);
        ptrdiff_t getline(char**, size_t*, FILE*);
        ptrdiff_t getdelim (char**, size_t*, int, FILE*);

        private size_t fwrite_unlocked(const(void)* ptr,
                size_t size, size_t n, _iobuf *stream);
    }

    alias fputc_unlocked FPUTC;
    alias fputwc_unlocked FPUTWC;
    alias fgetc_unlocked FGETC;
    alias fgetwc_unlocked FGETWC;

    alias flockfile FLOCK;
    alias funlockfile FUNLOCK;
}
else version (GENERIC_IO)
{
    extern (C)
    {
        void flockfile(FILE*);
        void funlockfile(FILE*);
    }

    int fputc_unlocked(int c, _iobuf* fp) { return fputc(c, cast(shared) fp); }
    int fputwc_unlocked(wchar_t c, _iobuf* fp)
    {
        return fputwc(c, cast(shared) fp);
    }
    int fgetc_unlocked(_iobuf* fp) { return fgetc(cast(shared) fp); }
    int fgetwc_unlocked(_iobuf* fp) { return fgetwc(cast(shared) fp); }

    alias fputc_unlocked FPUTC;
    alias fputwc_unlocked FPUTWC;
    alias fgetc_unlocked FGETC;
    alias fgetwc_unlocked FGETWC;

    alias flockfile FLOCK;
    alias funlockfile FUNLOCK;
}
else
{
    static assert(0, "unsupported C I/O system");
}

//------------------------------------------------------------------------------
struct ByRecord(Fields...)
{
private:
    File file;
    char[] line;
    Tuple!(Fields) current;
    string format;

public:
    this(File f, string format)
    {
        assert(f.isOpen);
        file = f;
        this.format = format;
        popFront(); // prime the range
    }

    /// Range primitive implementations.
    @property bool empty()
    {
        return !file.isOpen;
    }

    /// Ditto
    @property ref Tuple!(Fields) front()
    {
        return current;
    }

    /// Ditto
    void popFront()
    {
        enforce(file.isOpen);
        file.readln(line);
        if (!line.length)
        {
            file.detach();
        }
        else
        {
            line = chomp(line);
            formattedRead(line, format, &current);
            enforce(line.empty, text("Leftover characters in record: `",
                            line, "'"));
        }
    }
}

template byRecord(Fields...)
{
    ByRecord!(Fields) byRecord(File f, string format)
    {
        return typeof(return)(f, format);
    }
}

/**
Encapsulates a $(D FILE*). Generally D does not attempt to provide
thin wrappers over equivalent functions in the C standard library, but
manipulating $(D FILE*) values directly is unsafe and error-prone in
many ways. The $(D File) type ensures safe manipulation, automatic
file closing, and a lot of convenience.

The underlying $(D FILE*) handle is maintained in a reference-counted
manner, such that as soon as the last $(D File) variable bound to a
given $(D FILE*) goes out of scope, the underlying $(D FILE*) is
automatically closed.

Example:
----
// test.d
void main(string args[])
{
    auto f = File("test.txt", "w"); // open for writing
    f.write("Hello");
    if (args.length > 1)
    {
        auto g = f; // now g and f write to the same file
                    // internal reference count is 2
        g.write(", ", args[1]);
        // g exits scope, reference count decreases to 1
    }
    f.writeln("!");
    // f exits scope, reference count falls to zero,
    // underlying $(D FILE*) is closed.
}
----
<pre class=console>
% rdmd test.d Jimmy
% cat test.txt
Hello, Jimmy!
% __
</pre>
 */
struct File
{
    private struct Impl
    {
        FILE * handle = null; // Is null iff this Impl is closed by another File
        uint refs = uint.max / 2;
        bool isPipe;
    }
    private Impl* _p;
    private string _name;

    private this(FILE* handle, string name, uint refs = 1, bool isPipe = false)
    {
        assert(!_p);
        _p = cast(Impl*) enforce(malloc(Impl.sizeof), "Out of memory");
        _p.handle = handle;
        _p.refs = refs;
        _p.isPipe = isPipe;
        _name = name;
    }

/**
Constructor taking the name of the file to open and the open mode
(with the same semantics as in the C standard library $(WEB
cplusplus.com/reference/clibrary/cstdio/fopen.html, fopen)
function). Throws an exception if the file could not be opened.

Copying one $(D File) object to another results in the two $(D File)
objects referring to the same underlying file.

The destructor automatically closes the file as soon as no $(D File)
object refers to it anymore.
 */
    this(string name, in char[] stdioOpenmode = "rb")
    {
        this(errnoEnforce(.fopen(name, stdioOpenmode),
                        text("Cannot open file `", name, "' in mode `",
                                stdioOpenmode, "'")),
                name);
    }

    ~this()
    {
        detach();
    }

    this(this)
    {
        if (!_p) return;
        assert(_p.refs);
        ++_p.refs;
    }

/**
Assigns a file to another. The target of the assignment gets detached
from whatever file it was attached to, and attaches itself to the new
file.
 */
    void opAssign(File rhs)
    {
        swap(this, rhs);
    }

/**
First calls $(D detach) (throwing on failure), and then attempts to
_open file $(D name) with mode $(D stdioOpenmode). The mode has the
same semantics as in the C standard library $(WEB
cplusplus.com/reference/clibrary/cstdio/fopen.html, fopen) function.
Throws exception in case of error.
 */
    void open(string name, in char[] stdioOpenmode = "rb")
    {
        detach();
        this = File(name, stdioOpenmode);
    }

/**
First calls $(D detach) (throwing on failure), and then runs a command
by calling the C standard library function $(WEB
opengroup.org/onlinepubs/007908799/xsh/_popen.html, _popen).
 */
    version(Posix) void popen(string command, in char[] stdioOpenmode = "r")
    {
        detach();
        this = File(errnoEnforce(.popen(command, stdioOpenmode),
                        "Cannot run command `"~command~"'"),
                command, 1, true);
    }

/** Returns $(D true) if the file is opened. */
    @property bool isOpen() const pure nothrow
    {
        return _p !is null && _p.handle;
    }

/**
Returns $(D true) if the file is at end (see $(WEB
cplusplus.com/reference/clibrary/cstdio/feof.html, feof)). The file
must be opened, otherwise an exception is thrown.
 */
    @property bool eof() const pure
    {
        enforce(_p && _p.handle, "Calling eof() against an unopened file.");
        return .feof(cast(FILE*) _p.handle) != 0;
    }

/** Returns the name of the last opened file, if any.
If a $(D File) was created with $(LREF tmpfile) and $(LREF wrapFile)
it has no name.*/
    @property string name() const pure nothrow
    {
        return _name;
    }

/**
If the file is not opened, returns $(D false). Otherwise, returns
$(WEB cplusplus.com/reference/clibrary/cstdio/ferror.html, ferror) for
the file handle.
 */
    @property bool error() const pure nothrow
    {
        return !_p.handle || .ferror(cast(FILE*) _p.handle);
    }

/**
Detaches from the underlying file. If the sole owner, calls $(D close)
and throws if that fails.
  */
    void detach()
    {
        if (!_p) return;
        if (_p.refs == 1)
            close();
        else
        {
            assert(_p.refs);
            --_p.refs;
            _p = null;
        }
    }

    unittest
    {
        auto deleteme = testFilename();
        scope(exit) std.file.remove(deleteme);
        auto f = File(deleteme, "w");
        {
            auto f2 = f;
            f2.detach();
        }
        assert(f._p.refs == 1);
        f.close();
    }

/**
If the file was unopened, succeeds vacuously. Otherwise closes the
file (by calling $(WEB
cplusplus.com/reference/clibrary/cstdio/fclose.html, fclose)),
throwing on error. Even if an exception is thrown, afterwards the $(D
File) object is empty. This is different from $(D detach) in that it
always closes the file; consequently, all other $(D File) objects
referring to the same handle will see a closed file henceforth.
 */
    void close()
    {
        if (!_p) return; // succeed vacuously
        scope(exit)
        {
            assert(_p.refs);
            if(!--_p.refs)
                free(_p);
            _p = null; // start a new life
        }
        if (!_p.handle) return; // Impl is closed by another File

        scope(exit) _p.handle = null; // nullify the handle anyway
        version (Posix)
        {
            if (_p.isPipe)
            {
                // Ignore the result of the command
                errnoEnforce(.pclose(_p.handle) != -1,
                        "Could not close pipe `"~_name~"'");
                return;
            }
        }
        //fprintf(std.c.stdio.stderr, ("Closing file `"~name~"`.\n\0").ptr);
        errnoEnforce(.fclose(_p.handle) == 0,
                "Could not close file `"~_name~"'");
    }

/**
If the file is not opened, succeeds vacuously. Otherwise, returns
$(WEB cplusplus.com/reference/clibrary/cstdio/_clearerr.html,
_clearerr) for the file handle.
 */
    void clearerr() pure nothrow
    {
        _p is null || _p.handle is null ||
        .clearerr(_p.handle);
    }

/**
If the file is not opened, throws an exception. Otherwise, calls $(WEB
cplusplus.com/reference/clibrary/cstdio/_fflush.html, _fflush) for the
file handle and throws on error.
 */
    void flush()
    {
        errnoEnforce
        (.fflush(enforce(_p.handle, "Calling fflush() on an unopened file"))
                == 0);
    }

/**
If the file is not opened, throws an exception. Otherwise, calls $(WEB
cplusplus.com/reference/clibrary/cstdio/fread.html, fread) for the
file handle and throws on error. The number of items to read and the size of
each item is inferred from the size and type of the input array, respectively.

Returns: The slice of $(D buffer) containing the data that was actually read.
This will be shorter than $(D buffer) if EOF was reached before the buffer
could be filled.

$(D rawRead) always reads in binary mode on Windows.
 */
    T[] rawRead(T)(T[] buffer)
    {
        enforce(buffer.length, "rawRead must take a non-empty buffer");
        version(Win32)
        {
            immutable fd = ._fileno(_p.handle);
            immutable mode = ._setmode(fd, _O_BINARY);
            scope(exit) ._setmode(fd, mode);
            version(DIGITAL_MARS_STDIO)
            {
                // @@@BUG@@@ 4243
                immutable info = __fhnd_info[fd];
                __fhnd_info[fd] &= ~FHND_TEXT;
                scope(exit) __fhnd_info[fd] = info;
            }
        }
        immutable result =
            .fread(buffer.ptr, T.sizeof, buffer.length, _p.handle);
        errnoEnforce(!error);
        return result ? buffer[0 .. result] : null;
    }

    unittest
    {
        auto deleteme = testFilename();
        std.file.write(deleteme, "\r\n\n\r\n");
        scope(exit) std.file.remove(deleteme);
        auto f = File(deleteme, "r");
        auto buf = f.rawRead(new char[5]);
        f.close();
        assert(buf == "\r\n\n\r\n");
        /+
        buf = stdin.rawRead(new char[5]);
        assert(buf == "\r\n\n\r\n");
        +/
    }

/**
If the file is not opened, throws an exception. Otherwise, calls $(WEB
cplusplus.com/reference/clibrary/cstdio/fwrite.html, fwrite) for the file
handle and throws on error. The number of items to write and the size of each
item is inferred from the size and type of the input array, respectively. An
error is thrown if the buffer could not be written in its entirety.

$(D rawWrite) always writes in binary mode on Windows.
 */
    void rawWrite(T)(in T[] buffer)
    {
        version(Windows)
        {
            flush(); // before changing translation mode
            immutable fd = ._fileno(_p.handle);
            immutable mode = ._setmode(fd, _O_BINARY);
            scope(exit) ._setmode(fd, mode);
            version(DIGITAL_MARS_STDIO)
            {
                // @@@BUG@@@ 4243
                immutable info = __fhnd_info[fd];
                __fhnd_info[fd] &= ~FHND_TEXT;
                scope(exit) __fhnd_info[fd] = info;
            }
            scope(exit) flush(); // before restoring translation mode
        }
        auto result =
            .fwrite(buffer.ptr, T.sizeof, buffer.length, _p.handle);
        if (result == result.max) result = 0;
        errnoEnforce(result == buffer.length,
                text("Wrote ", result, " instead of ", buffer.length,
                        " objects of type ", T.stringof, " to file `",
                        _name, "'"));
    }

    version(Win64) {} else
    unittest
    {
        auto deleteme = testFilename();
        auto f = File(deleteme, "w");
        scope(exit) std.file.remove(deleteme);
        f.rawWrite("\r\n\n\r\n");
        f.close();
        assert(std.file.read(deleteme) == "\r\n\n\r\n");
    }

/**
If the file is not opened, throws an exception. Otherwise, calls $(WEB
cplusplus.com/reference/clibrary/cstdio/fseek.html, fseek) for the
file handle. Throws on error.
 */
    void seek(long offset, int origin = SEEK_SET)
    {
        enforce(isOpen, "Attempting to seek() in an unopened file");
        version (Windows)
        {
            errnoEnforce(fseek(_p.handle, to!int(offset), origin) == 0,
                    "Could not seek in file `"~_name~"'");
        }
        else
        {
            //static assert(off_t.sizeof == 8);
            errnoEnforce(fseeko(_p.handle, offset, origin) == 0,
                    "Could not seek in file `"~_name~"'");
        }
    }

    version(Win64) {} else
    unittest
    {
        auto deleteme = testFilename();
        auto f = File(deleteme, "w+");
        scope(exit) { f.close(); std.file.remove(deleteme); }
        f.rawWrite("abcdefghijklmnopqrstuvwxyz");
        f.seek(7);
        assert(f.readln() == "hijklmnopqrstuvwxyz");
        version (Windows)
        {
            // No test for large files yet
        }
        else
        {
            auto bigOffset = cast(ulong) int.max + 100;
            f.seek(bigOffset);
            assert(f.tell == bigOffset, text(f.tell));
            // Uncomment the tests below only if you want to wait for
            // a long time
            // f.rawWrite("abcdefghijklmnopqrstuvwxyz");
            // f.seek(-3, SEEK_END);
            // assert(f.readln() == "xyz");
        }
    }

/**
If the file is not opened, throws an exception. Otherwise, calls $(WEB
cplusplus.com/reference/clibrary/cstdio/ftell.html, ftell) for the
managed file handle. Throws on error.
 */
    @property ulong tell() const
    {
        enforce(isOpen, "Attempting to tell() in an unopened file");
        version (Windows)
        {
            immutable result = ftell(cast(FILE*) _p.handle);
        }
        else
        {
            immutable result = ftello(cast(FILE*) _p.handle);
        }
        errnoEnforce(result != -1,
                "Query ftell() failed for file `"~_name~"'");
        return result;
    }

    unittest
    {
        auto deleteme = testFilename();
        std.file.write(deleteme, "abcdefghijklmnopqrstuvwqxyz");
        scope(exit) { std.file.remove(deleteme); }
        auto f = File(deleteme);
        auto a = new ubyte[4];
        f.rawRead(a);
        assert(f.tell == 4, text(f.tell));
    }

/**
If the file is not opened, throws an exception. Otherwise, calls $(WEB
cplusplus.com/reference/clibrary/cstdio/_rewind.html, _rewind) for the
file handle. Throws on error.
 */
    void rewind()
    {
        enforce(isOpen, "Attempting to rewind() an unopened file");
        .rewind(_p.handle);
    }

/**
If the file is not opened, throws an exception. Otherwise, calls $(WEB
cplusplus.com/reference/clibrary/cstdio/_setvbuf.html, _setvbuf) for
the file handle.
 */
    void setvbuf(size_t size, int mode = _IOFBF)
    {
        enforce(isOpen, "Attempting to call setvbuf() on an unopened file");
        errnoEnforce(.setvbuf(_p.handle, null, mode, size) == 0,
                "Could not set buffering for file `"~_name~"'");
    }

/**
If the file is not opened, throws an exception. Otherwise, calls
$(WEB cplusplus.com/reference/clibrary/cstdio/_setvbuf.html,
_setvbuf) for the file handle. */
    void setvbuf(void[] buf, int mode = _IOFBF)
    {
        enforce(isOpen, "Attempting to call setvbuf() on an unopened file");
        errnoEnforce(.setvbuf(_p.handle,
                        cast(char*) buf.ptr, mode, buf.length) == 0,
                "Could not set buffering for file `"~_name~"'");
    }

/**
If the file is not opened, throws an exception. Otherwise, writes its
arguments in text format to the file. */
    void write(S...)(S args)
    {
        auto w = lockingTextWriter;
        foreach (arg; args)
        {
            alias typeof(arg) A;
            static if (isAggregateType!A || is(A == enum))
            {
                std.format.formattedWrite(w, "%s", arg);
            }
            else static if (isSomeString!A)
            {
                put(w, arg);
            }
            else static if (isIntegral!A)
            {
                toTextRange(arg, w);
            }
            else static if (isBoolean!A)
            {
                put(w, arg ? "true" : "false");
            }
            else static if (isSomeChar!A)
            {
                put(w, arg);
            }
            else
            {
                // Most general case
                std.format.formattedWrite(w, "%s", arg);
            }
        }
    }

/**
If the file is not opened, throws an exception. Otherwise, writes its
arguments in text format to the file, followed by a newline. */
    void writeln(S...)(S args)
    {
        write(args, '\n');
    }

/**
If the file is not opened, throws an exception. Otherwise, writes its
arguments in text format to the file, according to the format in the
first argument. */
    void writef(Char, A...)(in Char[] fmt, A args)
    {
        std.format.formattedWrite(lockingTextWriter, fmt, args);
    }

/**
Same as writef, plus adds a newline. */
    void writefln(Char, A...)(in Char[] fmt, A args)
    {
        auto w = lockingTextWriter;
        std.format.formattedWrite(w, fmt, args);
        w.put('\n');
    }

/**********************************
Read line from stream $(D fp) and write it to $(D buf[]), including
terminating character.

This is often faster than $(D File.readln(dchar)) because the buffer
is reused each call. Note that reusing the buffer means that the
previous contents of it has to be copied if needed.

Params:
fp = input stream
buf = buffer used to store the resulting line data. buf is
resized as necessary.

Returns:
0 for end of file, otherwise number of characters read

Throws: $(D StdioException) on I/O error, or $(D UnicodeException) on Unicode
conversion error.

Example:
---
// Reads $(D stdin) and writes it to $(D stdout).
import std.stdio;

int main()
{
    char[] buf;
    while (stdin.readln(buf))
        write(buf);
    return 0;
}
---

This method is more efficient than the one in the previous example
because $(D stdin.readln(buf)) reuses (if possible) memory allocated
by $(D buf), whereas $(D buf = stdin.readln()) makes a new memory allocation
with every line.  */
    S readln(S = string)(dchar terminator = '\n')
    {
        Unqual!(ElementEncodingType!S)[] buf;
        readln(buf, terminator);
        return assumeUnique(buf);
    }

    unittest
    {
        auto deleteme = testFilename();
        std.file.write(deleteme, "hello\nworld\n");
        scope(exit) std.file.remove(deleteme);
        foreach (C; Tuple!(char, wchar, dchar).Types)
        {
            auto witness = [ "hello\n", "world\n" ];
            auto f = File(deleteme);
            uint i = 0;
            immutable(C)[] buf;
            while ((buf = f.readln!(typeof(buf))()).length)
            {
                assert(i < witness.length);
                assert(equal(buf, witness[i++]));
            }
            assert(i == witness.length);
        }
    }

/** ditto */
    size_t readln(C)(ref C[] buf, dchar terminator = '\n') if (isSomeChar!C && !is(C == enum))
    {
        static if (is(C == char))
        {
            enforce(_p && _p.handle, "Attempt to read from an unopened file.");
            return readlnImpl(_p.handle, buf, terminator);
        }
        else
        {
            // TODO: optimize this
            string s = readln(terminator);
            if (!s.length) return 0;
            buf.length = 0;
            foreach (wchar c; s)
            {
                buf ~= c;
            }
            return buf.length;
        }
    }

/** ditto */
    size_t readln(C, R)(ref C[] buf, R terminator)
        if (isBidirectionalRange!R && is(typeof(terminator.front == buf[0])))
    {
        auto last = terminator.back;
        C[] buf2;
        swap(buf, buf2);
        for (;;) {
            if (!readln(buf2, last) || endsWith(buf2, terminator)) {
                if (buf.empty) {
                    buf = buf2;
                } else {
                    buf ~= buf2;
                }
                break;
            }
            buf ~= buf2;
        }
        return buf.length;
    }

    unittest
    {
        auto deleteme = testFilename();
        std.file.write(deleteme, "hello\n\rworld\nhow\n\rare ya");
        auto witness = [ "hello\n\r", "world\nhow\n\r", "are ya" ];
        scope(exit) std.file.remove(deleteme);
        auto f = File(deleteme);
        uint i = 0;
        char[] buf;
        while (f.readln(buf, "\n\r"))
        {
            assert(i < witness.length);
            assert(buf == witness[i++]);
        }
    }

    /**
     * Read data from the file according to the specified
     * $(LINK2 std_format.html#format-string, format specifier) using
     * $(XREF format,formattedRead).
     */
    uint readf(Data...)(in char[] format, Data data)
    {
        assert(isOpen);
        auto input = LockingTextReader(this);
        return formattedRead(input, format, data);
    }

    unittest
    {
        auto deleteme = testFilename();
        std.file.write(deleteme, "hello\nworld\n");
        scope(exit) std.file.remove(deleteme);
        string s;
        auto f = File(deleteme);
        f.readf("%s\n", &s);
        assert(s == "hello", "["~s~"]");
    }

/**
 Returns a temporary file by calling $(WEB
 cplusplus.com/reference/clibrary/cstdio/_tmpfile.html, _tmpfile).
 Note that the created file has no $(LREF name).*/
    static File tmpfile()
    {
        return File(errnoEnforce(core.stdc.stdio.tmpfile(),
                "Could not create temporary file with tmpfile()"),
            null);
    }

/**
Unsafe function that wraps an existing $(D FILE*). The resulting $(D
File) never takes the initiative in closing the file.
Note that the created file has no $(LREF name)*/
    /*private*/ static File wrapFile(FILE* f)
    {
        return File(enforce(f, "Could not wrap null FILE*"),
            null, /*uint.max / 2*/ 9999);
    }

/**
Returns the $(D FILE*) corresponding to this object.
 */
    FILE* getFP() pure
    {
        enforce(_p && _p.handle,
                "Attempting to call getFP() on an unopened file");
        return _p.handle;
    }

    unittest
    {
        assert(stdout.getFP() == std.c.stdio.stdout);
    }

/**
Returns the file number corresponding to this object.
 */
    /*version(Posix) */int fileno() const
    {
        enforce(isOpen, "Attempting to call fileno() on an unopened file");
        return .fileno(cast(FILE*) _p.handle);
    }

/**
Range that reads one line at a time. */
    /// ditto
    struct ByLine(Char, Terminator)
    {
        File file;
        Char[] line;
        Terminator terminator;
        KeepTerminator keepTerminator;
        bool first_call = true;

        this(File f, KeepTerminator kt = KeepTerminator.no,
                Terminator terminator = '\n')
        {
            file = f;
            this.terminator = terminator;
            keepTerminator = kt;
        }

        /// Range primitive implementations.
        @property bool empty() const
        {
            if (line !is null) return false;
            if (!file.isOpen) return true;

            // First read ever, must make sure stream is not empty. We
            // do so by reading a character and putting it back. Doing
            // so is guaranteed to work on all files opened in all
            // buffering modes. Although we internally mutate the
            // state of the file, we restore everything, which
            // justifies the cast.
            auto mutableFP = (cast(File*) &file).getFP();
            auto c = fgetc(mutableFP);
            if (c == -1)
            {
                return true;
            }
            ungetc(c, mutableFP) == c
                || assert(false, "Bug in cstdlib implementation");
            return false;
        }

        /// Ditto
        @property Char[] front()
        {
            if (first_call)
            {
                popFront();
                first_call = false;
            }
            return line;
        }

        /// Ditto
        void popFront()
        {
            assert(file.isOpen);
            assumeSafeAppend(line);
            file.readln(line, terminator);
            if (line.empty)
            {
                file.detach();
                line = null;
            }
            else if (keepTerminator == KeepTerminator.no
                    && std.algorithm.endsWith(line, terminator))
            {
                line = line.ptr[0 .. line.length - 1];
            }
        }
    }

/**
Convenience function that returns the $(D LinesReader) corresponding
to this file. */
    ByLine!(Char, Terminator) byLine(Terminator = char, Char = char)
    (KeepTerminator keepTerminator = KeepTerminator.no,
            Terminator terminator = '\n')
    {
        return typeof(return)(this, keepTerminator, terminator);
    }

    unittest
    {
        //printf("Entering test at line %d\n", __LINE__);
        scope(failure) printf("Failed test at line %d\n", __LINE__);
        auto deleteme = testFilename();
        std.file.write(deleteme, "");
        scope(success) std.file.remove(deleteme);

        // Test empty file
        auto f = File(deleteme);
        foreach (line; f.byLine())
        {
            assert(false);
        }
        f.close();

        void test(string txt, string[] witness,
                KeepTerminator kt = KeepTerminator.no,
                bool popFirstLine = false)
        {
            uint i;
            std.file.write(deleteme, txt);
            auto f = File(deleteme);
            scope(exit)
            {
                f.close();
                assert(!f.isOpen);
            }
            auto lines = f.byLine(kt);
            if (popFirstLine)
            {
                lines.popFront();
                i = 1;
            }
            foreach (line; lines)
            {
                assert(line == witness[i++]);
            }
            assert(i == witness.length, text(i, " != ", witness.length));
        }

        test("", null);
        test("\n", [ "" ]);
        test("asd\ndef\nasdf", [ "asd", "def", "asdf" ]);
        test("asd\ndef\nasdf", [ "asd", "def", "asdf" ], KeepTerminator.no, true);
        test("asd\ndef\nasdf\n", [ "asd", "def", "asdf" ]);

        test("", null, KeepTerminator.yes);
        test("\n", [ "\n" ], KeepTerminator.yes);
        test("asd\ndef\nasdf", [ "asd\n", "def\n", "asdf" ], KeepTerminator.yes);
        test("asd\ndef\nasdf\n", [ "asd\n", "def\n", "asdf\n" ], KeepTerminator.yes);
        test("asd\ndef\nasdf\n", [ "asd\n", "def\n", "asdf\n" ], KeepTerminator.yes, true);
    }

    template byRecord(Fields...)
    {
        ByRecord!(Fields) byRecord(string format)
        {
            return typeof(return)(this, format);
        }
    }

    unittest
    {
        // auto deleteme = testFilename();
        // rndGen.popFront();
        // scope(failure) printf("Failed test at line %d\n", __LINE__);
        // std.file.write(deleteme, "1 2\n4 1\n5 100");
        // scope(exit) std.file.remove(deleteme);
        // File f = File(deleteme);
        // scope(exit) f.close();
        // auto t = [ tuple(1, 2), tuple(4, 1), tuple(5, 100) ];
        // uint i;
        // foreach (e; f.byRecord!(int, int)("%s %s"))
        // {
        //     //.writeln(e);
        //     assert(e == t[i++]);
        // }
    }


    /**
     * Range that reads a chunk at a time.
     */
    struct ByChunk
    {
      private:
        File    file_;
        ubyte[] chunk_;


      public:
        this(File file, size_t size)
        in
        {
            assert(size, "size must be larger than 0");
        }
        body
        {
            file_  = file;
            chunk_ = new ubyte[](size);

            popFront();
        }


        /// Range primitive operations.
        @property
        bool empty() const
        {
            return !file_.isOpen;
        }


        /// Ditto
        @property
        nothrow ubyte[] front()
        {
            return chunk_;
        }


        /// Ditto
        void popFront()
        {
            enforce(!empty, "Cannot call popFront on empty range");

            chunk_ = file_.rawRead(chunk_);
            if (chunk_.length == 0)
                file_.detach();
        }
    }

/**
Iterates through a file a chunk at a time by using $(D foreach).

Example:

---------
void main()
{
  foreach (ubyte[] buffer; stdin.byChunk(4096))
  {
    ... use buffer ...
  }
}
---------

The content of $(D buffer) is reused across calls. In the example
above, $(D buffer.length) is 4096 for all iterations, except for the
last one, in which case $(D buffer.length) may be less than 4096 (but
always greater than zero).

In case of an I/O error, an $(D StdioException) is thrown.
 */
    ByChunk byChunk(size_t chunkSize)
    {
        return ByChunk(this, chunkSize);
    }

    unittest
    {
        scope(failure) printf("Failed test at line %d\n", __LINE__);

        auto deleteme = testFilename();
        std.file.write(deleteme, "asd\ndef\nasdf");

        auto witness = ["asd\n", "def\n", "asdf" ];
        auto f = File(deleteme);
        scope(exit)
        {
            f.close();
            assert(!f.isOpen);
            std.file.remove(deleteme);
        }

        uint i;
        foreach (chunk; f.byChunk(4))
            assert(chunk == cast(ubyte[])witness[i++]);

        assert(i == witness.length);
    }

/**
$(D Range) that locks the file and allows fast writing to it.
 */
    struct LockingTextWriter
    {
        FILE* fps;          // the shared file handle
        _iobuf* handle;     // the unshared version of fps
        int orientation;

        this(ref File f)
        {
            enforce(f._p && f._p.handle);
            fps = f._p.handle;
            orientation = fwide(fps, 0);
            FLOCK(fps);
            handle = cast(_iobuf*)fps;
        }

        ~this()
        {
            FUNLOCK(fps);
            fps = null;
            handle = null;
        }

        this(this)
        {
            enforce(fps);
            FLOCK(fps);
        }

        /// Range primitive implementations.
        void put(A)(A writeme) if (is(ElementType!A : const(dchar)))
        {
            alias ElementEncodingType!A C;
            static assert(!is(C == void));
            if (writeme[0].sizeof == 1 && orientation <= 0)
            {
                //file.write(writeme); causes infinite recursion!!!
                //file.rawWrite(writeme);
                auto result =
                    .fwrite(writeme.ptr, C.sizeof, writeme.length, fps);
                if (result != writeme.length) errnoEnforce(0);
            }
            else
            {
                // put each character in turn
                foreach (dchar c; writeme)
                {
                    put(c);
                }
            }
        }

        // @@@BUG@@@ 2340
        //void front(C)(C c) if (is(C : dchar)) {
        /// ditto
        void put(C)(C c) if (is(C : const(dchar)))
        {
            static if (c.sizeof == 1)
            {
                // simple char
                if (orientation <= 0) FPUTC(c, handle);
                else FPUTWC(c, handle);
            }
            else static if (c.sizeof == 2)
            {
                if (orientation <= 0)
                {
                    if (c <= 0x7F)
                    {
                        FPUTC(c, handle);
                    }
                    else
                    {
                        char[4] buf;
                        auto b = std.utf.toUTF8(buf, c);
                        foreach (i ; 0 .. b.length)
                            FPUTC(b[i], handle);
                    }
                }
                else
                {
                    FPUTWC(c, handle);
                }
            }
            else // 32-bit characters
            {
                if (orientation <= 0)
                {
                    if (c <= 0x7F)
                    {
                        FPUTC(c, handle);
                    }
                    else
                    {
                        char[4] buf = void;
                        auto b = std.utf.toUTF8(buf, c);
                        foreach (i ; 0 .. b.length)
                            FPUTC(b[i], handle);
                    }
                }
                else
                {
                    version (Windows)
                    {
                        assert(isValidDchar(c));
                        if (c <= 0xFFFF)
                        {
                            FPUTWC(c, handle);
                        }
                        else
                        {
                            FPUTWC(cast(wchar)
                                    ((((c - 0x10000) >> 10) & 0x3FF)
                                            + 0xD800), handle);
                            FPUTWC(cast(wchar)
                                    (((c - 0x10000) & 0x3FF) + 0xDC00),
                                    handle);
                        }
                    }
                    else version (Posix)
                    {
                        FPUTWC(c, handle);
                    }
                    else
                    {
                        static assert(0);
                    }
                }
            }
        }
    }

/// Convenience function.
    @property LockingTextWriter lockingTextWriter()
    {
        return LockingTextWriter(this);
    }

/// Get the size of the file, ulong.max if file is not searchable, but still throws if an actual error occurs.
    @property ulong size()
    {
        ulong pos = void;
        if (collectException(pos = tell)) return ulong.max;
        scope(exit) seek(pos);
        seek(0, SEEK_END);
        return tell;
    }
}

unittest
{
    auto deleteme = testFilename();
    scope(exit) collectException(std.file.remove(deleteme));
    std.file.write(deleteme, "1 2 3");
    auto f = File(deleteme);
    assert(f.size == 5);
    assert(f.tell == 0);
}

struct LockingTextReader
{
    private File _f;
    private dchar _crt;

    this(File f)
    {
        enforce(f.isOpen);
        _f = f;
        FLOCK(_f._p.handle);
    }

    this(this)
    {
        FLOCK(_f._p.handle);
    }

    ~this()
    {
        // File locking has its own reference count
        if (_f.isOpen) FUNLOCK(_f._p.handle);
    }

    void opAssign(LockingTextReader r)
    {
        swap(this, r);
    }

    @property bool empty()
    {
        if (!_f.isOpen || _f.eof) return true;
        if (_crt == _crt.init)
        {
            _crt = FGETC(cast(_iobuf*) _f._p.handle);
            if (_crt == -1)
            {
                .destroy(_f);
                return true;
            }
            else
            {
                enforce(ungetc(_crt, cast(FILE*) _f._p.handle) == _crt);
            }
        }
        return false;
    }

    @property dchar front()
    {
        enforce(!empty);
        return _crt;
    }

    void popFront()
    {
        enforce(!empty);
        if (FGETC(cast(_iobuf*) _f._p.handle) == -1)
        {
            enforce(_f.eof);
        }
        _crt = _crt.init;
    }

    // void unget(dchar c)
    // {
    //     ungetc(c, cast(FILE*) _f._p.handle);
    // }
}

unittest
{
    static assert(isInputRange!LockingTextReader);
    auto deleteme = testFilename();
    std.file.write(deleteme, "1 2 3");
    scope(exit) std.file.remove(deleteme);
    int x, y;
    auto f = File(deleteme);
    f.readf("%s ", &x);
    assert(x == 1);
    f.readf("%d ", &x);
    assert(x == 2);
    f.readf("%d ", &x);
    assert(x == 3);
    //pragma(msg, "--- todo: readf ---");
}

private
void writefx(FILE* fps, TypeInfo[] arguments, void* argptr, int newline=false)
{
    int orientation = fwide(fps, 0);    // move this inside the lock?

    /* Do the file stream locking at the outermost level
     * rather than character by character.
     */
    FLOCK(fps);
    scope(exit) FUNLOCK(fps);

    auto fp = cast(_iobuf*)fps;     // fp is locked version

    if (orientation <= 0)                // byte orientation or no orientation
    {
        void putc(dchar c)
        {
            if (c <= 0x7F)
            {
                FPUTC(c, fp);
            }
            else
            {
                char[4] buf = void;
                foreach (i; 0 .. std.utf.toUTF8(buf, c).length)
                    FPUTC(buf[i], fp);
            }
        }

        std.format.doFormat(&putc, arguments, argptr);
        if (newline)
            FPUTC('\n', fp);
    }
    else if (orientation > 0)                // wide orientation
    {
        version (Windows)
        {
            void putcw(dchar c)
            {
                assert(isValidDchar(c));
                if (c <= 0xFFFF)
                {
                    FPUTWC(c, fp);
                }
                else
                {
                    FPUTWC(cast(wchar) ((((c - 0x10000) >> 10) & 0x3FF) +
                                    0xD800), fp);
                    FPUTWC(cast(wchar) (((c - 0x10000) & 0x3FF) + 0xDC00), fp);
                }
            }
        }
        else version (Posix)
        {
            void putcw(dchar c)
            {
                FPUTWC(c, fp);
            }
        }
        else
        {
            static assert(0);
        }

        std.format.doFormat(&putcw, arguments, argptr);
        if (newline)
            FPUTWC('\n', fp);
    }
}

/**
 * Indicates whether $(D T) is a file handle of some kind.
 */
template isFileHandle(T)
{
    enum isFileHandle = is(T : FILE*) ||
        is(T : File);
}

unittest
{
    static assert(isFileHandle!(FILE*));
    static assert(isFileHandle!(File));
}

/**
 * $(RED Scheduled for deprecation in January 2013.
 *       Please use $(D isFileHandle) instead.)
 */
alias isFileHandle isStreamingDevice;

/***********************************
For each argument $(D arg) in $(D args), format the argument (as per
$(LINK2 std_conv.html, to!(string)(arg))) and write the resulting
string to $(D args[0]). A call without any arguments will fail to
compile.

Throws: In case of an I/O error, throws an $(D StdioException).
 */
void write(T...)(T args) if (!is(T[0] : File))
{
    stdout.write(args);
}

unittest
{
    //printf("Entering test at line %d\n", __LINE__);
    scope(failure) printf("Failed test at line %d\n", __LINE__);
    void[] buf;
    if (false) write(buf);
    // test write
    auto deleteme = testFilename();
    auto f = File(deleteme, "w");
    f.write("Hello, ",  "world number ", 42, "!");
    f.close();
    scope(exit) { std.file.remove(deleteme); }
    assert(cast(char[]) std.file.read(deleteme) == "Hello, world number 42!");
    // // test write on stdout
    //auto saveStdout = stdout;
    //scope(exit) stdout = saveStdout;
    //stdout.open(file, "w");
    Object obj;
    //write("Hello, ",  "world number ", 42, "! ", obj);
    //stdout.close();
    // auto result = cast(char[]) std.file.read(file);
    // assert(result == "Hello, world number 42! null", result);
}

/***********************************
 * Equivalent to $(D write(args, '\n')).  Calling $(D writeln) without
 * arguments is valid and just prints a newline to the standard
 * output.
 */
void writeln(T...)(T args)
{
    static if (T.length == 0)
    {
        enforce(fputc('\n', .stdout._p.handle) == '\n');
    }
    else static if (T.length == 1 &&
                    is(typeof(args[0]) : const(char)[]) &&
                    !is(typeof(args[0]) == enum) && !is(typeof(args[0]) == typeof(null)) &&
                    !isAggregateType!(typeof(args[0])))
    {
        // Specialization for strings - a very frequent case
        enforce(fprintf(.stdout._p.handle, "%.*s\n",
                        cast(int) args[0].length, args[0].ptr) >= 0);
    }
    else
    {
        // Most general instance
        stdout.write(args, '\n');
    }
}

unittest
{
    // Just make sure the call compiles
    if (false) writeln();

    if (false) writeln("wyda");

    // bug 8040
    if (false) writeln(null);
    if (false) writeln(">", null, "<");
}

unittest
{
    //printf("Entering test at line %d\n", __LINE__);
    scope(failure) printf("Failed test at line %d\n", __LINE__);

    // test writeln
    auto deleteme = testFilename();
    auto f = File(deleteme, "w");
    scope(exit) { std.file.remove(deleteme); }
    f.writeln("Hello, ",  "world number ", 42, "!");
    f.close();
    version (Windows)
        assert(cast(char[]) std.file.read(deleteme) ==
                "Hello, world number 42!\r\n");
    else
        assert(cast(char[]) std.file.read(deleteme) ==
                "Hello, world number 42!\n");

    // test writeln on stdout
    auto saveStdout = stdout;
    scope(exit) stdout = saveStdout;
    stdout.open(deleteme, "w");
    writeln("Hello, ",  "world number ", 42, "!");
    stdout.close();
    version (Windows)
        assert(cast(char[]) std.file.read(deleteme) ==
                "Hello, world number 42!\r\n");
    else
        assert(cast(char[]) std.file.read(deleteme) ==
                "Hello, world number 42!\n");

    stdout.open(deleteme, "w");
    writeln("Hello!"c);
    writeln("Hello!"w);    // bug 8386
    writeln("Hello!"d);    // bug 8386
    stdout.close();
    version (Windows)
        assert(cast(char[]) std.file.read(deleteme) ==
            "Hello!\r\nHello!\r\nHello!\r\n");
    else
        assert(cast(char[]) std.file.read(deleteme) ==
            "Hello!\nHello!\nHello!\n");
}

unittest
{
    auto deleteme = testFilename();
    auto f = File(deleteme, "w");
    scope(exit) { std.file.remove(deleteme); }

    enum EI : int    { A, B }
    enum ED : double { A, B }
    enum EC : char   { A, B }
    enum ES : string { A = "aaa", B = "bbb" }

    f.writeln(EI.A);  // false, but A on 2.058
    f.writeln(EI.B);  // true, but B on 2.058

    f.writeln(ED.A);  // A
    f.writeln(ED.B);  // B

    f.writeln(EC.A);  // A
    f.writeln(EC.B);  // B

    f.writeln(ES.A);  // A
    f.writeln(ES.B);  // B

    f.close();
    version (Windows)
        assert(cast(char[]) std.file.read(deleteme) ==
                "A\r\nB\r\nA\r\nB\r\nA\r\nB\r\nA\r\nB\r\n");
    else
        assert(cast(char[]) std.file.read(deleteme) ==
                "A\nB\nA\nB\nA\nB\nA\nB\n");
}

/***********************************
 * If the first argument $(D args[0]) is a $(D FILE*), use
 * $(LINK2 std_format.html#format-string, the format specifier) in
 * $(D args[1]) to control the formatting of $(D
 * args[2..$]), and write the resulting string to $(D args[0]).
 * If $(D arg[0]) is not a $(D FILE*), the call is
 * equivalent to $(D writef(stdout, args)).
 *

IMPORTANT:

New behavior starting with D 2.006: unlike previous versions,
$(D writef) (and also $(D writefln)) only scans its first
string argument for format specifiers, but not subsequent string
arguments. This decision was made because the old behavior made it
unduly hard to simply print string variables that occasionally
embedded percent signs.

Also new starting with 2.006 is support for positional
parameters with
$(LINK2 http://opengroup.org/onlinepubs/009695399/functions/printf.html,
POSIX) syntax.

Example:

-------------------------
writef("Date: %2$s %1$s", "October", 5); // "Date: 5 October"
------------------------

The positional and non-positional styles can be mixed in the same
format string. (POSIX leaves this behavior undefined.) The internal
counter for non-positional parameters tracks the popFront parameter after
the largest positional parameter already used.

New starting with 2.008: raw format specifiers. Using the "%r"
specifier makes $(D writef) simply write the binary
representation of the argument. Use "%-r" to write numbers in little
endian format, "%+r" to write numbers in big endian format, and "%r"
to write numbers in platform-native format.

*/

void writef(T...)(T args)
{
    stdout.writef(args);
}

unittest
{
    //printf("Entering test at line %d\n", __LINE__);
    scope(failure) printf("Failed test at line %d\n", __LINE__);
    // test writef
    auto deleteme = testFilename();
    auto f = File(deleteme, "w");
    scope(exit) { std.file.remove(deleteme); }
    f.writef("Hello, %s world number %s!", "nice", 42);
    f.close();
    assert(cast(char[]) std.file.read(deleteme) ==  "Hello, nice world number 42!");
    // test write on stdout
    auto saveStdout = stdout;
    scope(exit) stdout = saveStdout;
    stdout.open(deleteme, "w");
    writef("Hello, %s world number %s!", "nice", 42);
    stdout.close();
    assert(cast(char[]) std.file.read(deleteme) == "Hello, nice world number 42!");
}

/***********************************
 * Equivalent to $(D writef(args, '\n')).
 */
void writefln(T...)(T args)
{
    stdout.writefln(args);
}

unittest
{
        //printf("Entering test at line %d\n", __LINE__);
    scope(failure) printf("Failed test at line %d\n", __LINE__);
    // test writefln
    auto deleteme = testFilename();
    auto f = File(deleteme, "w");
    scope(exit) { std.file.remove(deleteme); }
    f.writefln("Hello, %s world number %s!", "nice", 42);
    f.close();
    version (Windows)
        assert(cast(char[]) std.file.read(deleteme) ==
                "Hello, nice world number 42!\r\n");
    else
        assert(cast(char[]) std.file.read(deleteme) ==
                "Hello, nice world number 42!\n",
                cast(char[]) std.file.read(deleteme));
    // test write on stdout
    // auto saveStdout = stdout;
    // scope(exit) stdout = saveStdout;
    // stdout.open(file, "w");
    // assert(stdout.isOpen);
    // writefln("Hello, %s world number %s!", "nice", 42);
    // foreach (F ; TypeTuple!(ifloat, idouble, ireal))
    // {
    //     F a = 5i;
    //     F b = a % 2;
    //     writeln(b);
    // }
    // stdout.close();
    // auto read = cast(char[]) std.file.read(file);
    // version (Windows)
    //     assert(read == "Hello, nice world number 42!\r\n1\r\n1\r\n1\r\n", read);
    // else
    //     assert(read == "Hello, nice world number 42!\n1\n1\n1\n", "["~read~"]");
}

/**
 * Read data from $(D stdin) according to the specified
 * $(LINK2 std_format.html#format-string, format specifier) using
 * $(XREF format,formattedRead).
 */
uint readf(A...)(in char[] format, A args)
{
    return stdin.readf(format, args);
}

unittest
{
    float f;
    if (false) uint x = readf("%s", &f);

    char a;
    wchar b;
    dchar c;
    if (false) readf("%s %s %s", &a,&b,&c);
}

/**********************************
 * Read line from stream $(D fp).
 * Returns:
 *        $(D null) for end of file,
 *        $(D char[]) for line read from $(D fp), including terminating character
 * Params:
 *        $(D fp) = input stream
 *        $(D terminator) = line terminator, '\n' by default
 * Throws:
 *        $(D StdioException) on error
 * Example:
 *        Reads $(D stdin) and writes it to $(D stdout).
---
import std.stdio;

int main()
{
    char[] buf;
    while ((buf = readln()) != null)
        write(buf);
    return 0;
}
---
*/
string readln(dchar terminator = '\n')
{
    return stdin.readln(terminator);
}

/** ditto */
size_t readln(ref char[] buf, dchar terminator = '\n')
{
    return stdin.readln(buf, terminator);
}

/*
 * Convenience function that forwards to $(D core.stdc.stdio.fopen)
 * (to $(D _wfopen) on Windows)
 * with appropriately-constructed C-style strings.
 */
private FILE* fopen(in char[] name, in char[] mode = "r")
{
    version(Windows)
        return _wfopen(toUTF16z(name), toUTF16z(mode));
    else version(Posix)
    {
        /*
         * The new opengroup large file support API is transparently
         * included in the normal C bindings. http://opengroup.org/platform/lfs.html#1.0
         * if _FILE_OFFSET_BITS in druntime is 64, off_t is 64 bit and
         * the normal functions work fine. If not, then large file support
         * probably isn't available. Do not use the old transitional API
         * (the native extern(C) fopen64, http://www.unix.org/version2/whatsnew/lfs20mar.html#3.0)
         */
        return core.sys.posix.stdio.fopen(toStringz(name), toStringz(mode));
    }
    else
    {
        return core.stdc.stdio.fopen(toStringz(name), toStringz(mode));
    }
}

version (Posix)
{
    /***********************************
     * Convenience function that forwards to $(D std.c.stdio.popen)
     * with appropriately-constructed C-style strings.
     */
    FILE* popen(in char[] name, in char[] mode = "r")
    {
        return core.sys.posix.stdio.popen(toStringz(name), toStringz(mode));
    }
}

/*
 * Convenience function that forwards to $(D std.c.stdio.fwrite)
 * and throws an exception upon error
 */
private void binaryWrite(T)(FILE* f, T obj)
{
    immutable result = fwrite(obj.ptr, obj[0].sizeof, obj.length, f);
    if (result != obj.length) StdioException();
}

/**
 * Iterates through the lines of a file by using $(D foreach).
 *
 * Example:
 *
---------
void main()
{
  foreach (string line; lines(stdin))
  {
    ... use line ...
  }
}
---------
 The line terminator ('\n' by default) is part of the string read (it
could be missing in the last line of the file). Several types are
supported for $(D line), and the behavior of $(D lines)
changes accordingly:

$(OL $(LI If $(D line) has type $(D string), $(D
wstring), or $(D dstring), a new string of the respective type
is allocated every read.) $(LI If $(D line) has type $(D
char[]), $(D wchar[]), $(D dchar[]), the line's content
will be reused (overwritten) across reads.) $(LI If $(D line)
has type $(D immutable(ubyte)[]), the behavior is similar to
case (1), except that no UTF checking is attempted upon input.) $(LI
If $(D line) has type $(D ubyte[]), the behavior is
similar to case (2), except that no UTF checking is attempted upon
input.))

In all cases, a two-symbols versions is also accepted, in which case
the first symbol (of integral type, e.g. $(D ulong) or $(D
uint)) tracks the zero-based number of the current line.

Example:
----
  foreach (ulong i, string line; lines(stdin))
  {
    ... use line ...
  }
----

 In case of an I/O error, an $(D StdioException) is thrown.
 */

struct lines
{
    private File f;
    private dchar terminator = '\n';
    // private string fileName;  // Curretly, no use

    this(File f, dchar terminator = '\n')
    {
        this.f = f;
        this.terminator = terminator;
    }

    // Keep these commented lines for later, when Walter fixes the
    // exception model.

//     static lines opCall(string fName, dchar terminator = '\n')
//     {
//         auto f = enforce(fopen(fName),
//             new StdioException("Cannot open file `"~fName~"' for reading"));
//         auto result = lines(f, terminator);
//         result.fileName = fName;
//         return result;
//     }

    int opApply(D)(scope D dg)
    {
//         scope(exit) {
//             if (fileName.length && fclose(f))
//                 StdioException("Could not close file `"~fileName~"'");
//         }
        alias ParameterTypeTuple!(dg) Parms;
        static if (isSomeString!(Parms[$ - 1]))
        {
            enum bool duplicate = is(Parms[$ - 1] == string)
                || is(Parms[$ - 1] == wstring) || is(Parms[$ - 1] == dstring);
            int result = 0;
            static if (is(Parms[$ - 1] : const(char)[]))
                alias char C;
            else static if (is(Parms[$ - 1] : const(wchar)[]))
                alias wchar C;
            else static if (is(Parms[$ - 1] : const(dchar)[]))
                alias dchar C;
            C[] line;
            static if (Parms.length == 2)
                Parms[0] i = 0;
            for (;;)
            {
                if (!f.readln(line, terminator)) break;
                auto copy = to!(Parms[$ - 1])(line);
                static if (Parms.length == 2)
                {
                    result = dg(i, copy);
                    ++i;
                }
                else
                {
                    result = dg(copy);
                }
                if (result != 0) break;
            }
            return result;
        }
        else
        {
            // raw read
            return opApplyRaw(dg);
        }
    }
    // no UTF checking
    int opApplyRaw(D)(scope D dg)
    {
        alias ParameterTypeTuple!(dg) Parms;
        enum duplicate = is(Parms[$ - 1] : immutable(ubyte)[]);
        int result = 1;
        int c = void;
        FLOCK(f._p.handle);
        scope(exit) FUNLOCK(f._p.handle);
        ubyte[] buffer;
        static if (Parms.length == 2)
            Parms[0] line = 0;
        while ((c = FGETC(cast(_iobuf*)f._p.handle)) != -1)
        {
            buffer ~= to!(ubyte)(c);
            if (c == terminator)
            {
                static if (duplicate)
                    auto arg = assumeUnique(buffer);
                else
                    alias buffer arg;
                // unlock the file while calling the delegate
                FUNLOCK(f._p.handle);
                scope(exit) FLOCK(f._p.handle);
                static if (Parms.length == 1)
                {
                    result = dg(arg);
                }
                else
                {
                    result = dg(line, arg);
                    ++line;
                }
                if (result) break;
                static if (!duplicate)
                    buffer.length = 0;
            }
        }
        // can only reach when FGETC returned -1
        if (!f.eof) throw new StdioException("Error in reading file"); // error occured
        return result;
    }
}

unittest
{
        //printf("Entering test at line %d\n", __LINE__);
    scope(failure) printf("Failed test at line %d\n", __LINE__);
    auto deleteme = testFilename();
    scope(exit) { std.file.remove(deleteme); }
    alias TypeTuple!(string, wstring, dstring,
                     char[], wchar[], dchar[])
        TestedWith;
    foreach (T; TestedWith) {
        // test looping with an empty file
        std.file.write(deleteme, "");
        auto f = File(deleteme, "r");
        foreach (T line; lines(f))
        {
            assert(false);
        }
        f.close();

        // test looping with a file with three lines
        std.file.write(deleteme, "Line one\nline two\nline three\n");
        f.open(deleteme, "r");
        uint i = 0;
        foreach (T line; lines(f))
        {
            if (i == 0) assert(line == "Line one\n");
            else if (i == 1) assert(line == "line two\n");
            else if (i == 2) assert(line == "line three\n");
            else assert(false);
            ++i;
        }
        f.close();

        // test looping with a file with three lines, last without a newline
        std.file.write(deleteme, "Line one\nline two\nline three");
        f.open(deleteme, "r");
        i = 0;
        foreach (T line; lines(f))
        {
            if (i == 0) assert(line == "Line one\n");
            else if (i == 1) assert(line == "line two\n");
            else if (i == 2) assert(line == "line three");
            else assert(false);
            ++i;
        }
        f.close();
    }

    // test with ubyte[] inputs
    //@@@BUG 2612@@@
    //alias TypeTuple!(immutable(ubyte)[], ubyte[]) TestedWith2;
    alias TypeTuple!(immutable(ubyte)[], ubyte[]) TestedWith2;
    foreach (T; TestedWith2) {
        // test looping with an empty file
        std.file.write(deleteme, "");
        auto f = File(deleteme, "r");
        foreach (T line; lines(f))
        {
            assert(false);
        }
        f.close();

        // test looping with a file with three lines
        std.file.write(deleteme, "Line one\nline two\nline three\n");
        f.open(deleteme, "r");
        uint i = 0;
        foreach (T line; lines(f))
        {
            if (i == 0) assert(cast(char[]) line == "Line one\n");
            else if (i == 1) assert(cast(char[]) line == "line two\n",
                T.stringof ~ " " ~ cast(char[]) line);
            else if (i == 2) assert(cast(char[]) line == "line three\n");
            else assert(false);
            ++i;
        }
        f.close();

        // test looping with a file with three lines, last without a newline
        std.file.write(deleteme, "Line one\nline two\nline three");
        f.open(deleteme, "r");
        i = 0;
        foreach (T line; lines(f))
        {
            if (i == 0) assert(cast(char[]) line == "Line one\n");
            else if (i == 1) assert(cast(char[]) line == "line two\n");
            else if (i == 2) assert(cast(char[]) line == "line three");
            else assert(false);
            ++i;
        }
        f.close();

    }

    foreach (T; TypeTuple!(ubyte[]))
    {
        // test looping with a file with three lines, last without a newline
        // using a counter too this time
        std.file.write(deleteme, "Line one\nline two\nline three");
        auto f = File(deleteme, "r");
        uint i = 0;
        foreach (ulong j, T line; lines(f))
        {
            if (i == 0) assert(cast(char[]) line == "Line one\n");
            else if (i == 1) assert(cast(char[]) line == "line two\n");
            else if (i == 2) assert(cast(char[]) line == "line three");
            else assert(false);
            ++i;
        }
        f.close();
    }
}

/**
Iterates through a file a chunk at a time by using $(D
foreach).

Example:

---------
void main()
{
  foreach (ubyte[] buffer; chunks(stdin, 4096))
  {
    ... use buffer ...
  }
}
---------

The content of $(D buffer) is reused across calls. In the
 example above, $(D buffer.length) is 4096 for all iterations,
 except for the last one, in which case $(D buffer.length) may
 be less than 4096 (but always greater than zero).

 In case of an I/O error, an $(D StdioException) is thrown.
*/

struct chunks
{
    private File f;
    private size_t size;
    // private string fileName; // Currently, no use

    this(File f, size_t size)
    in
    {
        assert(size, "size must be larger than 0");
    }
    body
    {
        this.f = f;
        this.size = size;
    }

//     static chunks opCall(string fName, size_t size)
//     {
//         auto f = enforce(fopen(fName),
//             new StdioException("Cannot open file `"~fName~"' for reading"));
//         auto result = chunks(f, size);
//         result.fileName  = fName;
//         return result;
//     }

    int opApply(D)(scope D dg)
    {
        const maxStackSize = 1024 * 16;
        ubyte[] buffer = void;
        if (size < maxStackSize)
            buffer = (cast(ubyte*) alloca(size))[0 .. size];
        else
            buffer = new ubyte[size];
        size_t r = void;
        int result = 1;
        uint tally = 0;
        while ((r = core.stdc.stdio.fread(buffer.ptr,
                                buffer[0].sizeof, size, f._p.handle)) > 0)
        {
            assert(r <= size);
            if (r != size)
            {
                // error occured
                if (!f.eof) throw new StdioException(null);
                buffer.length = r;
            }
            static if (is(typeof(dg(tally, buffer)))) {
                if ((result = dg(tally, buffer)) != 0) break;
            } else {
                if ((result = dg(buffer)) != 0) break;
            }
            ++tally;
        }
        return result;
    }
}

unittest
{
        //printf("Entering test at line %d\n", __LINE__);
    scope(failure) printf("Failed test at line %d\n", __LINE__);
    auto deleteme = testFilename();
    scope(exit) { std.file.remove(deleteme); }
    // test looping with an empty file
    std.file.write(deleteme, "");
    auto f = File(deleteme, "r");
    foreach (ubyte[] line; chunks(f, 4))
    {
        assert(false);
    }
    f.close();

    // test looping with a file with three lines
    std.file.write(deleteme, "Line one\nline two\nline three\n");
    f = File(deleteme, "r");
    uint i = 0;
    foreach (ubyte[] line; chunks(f, 3))
    {
        if (i == 0) assert(cast(char[]) line == "Lin");
        else if (i == 1) assert(cast(char[]) line == "e o");
        else if (i == 2) assert(cast(char[]) line == "ne\n");
        else break;
        ++i;
    }
    f.close();
}

/*********************
 * Thrown if I/O errors happen.
 */
class StdioException : Exception
{
    /// Operating system error code.
    uint errno;

/**
Initialize with a message and an error code. */
    this(string message, uint e = .errno)
    {
        errno = e;
        version (Posix)
        {
            char[256] buf = void;
            version (linux)
            {
                auto s = std.c.string.strerror_r(errno, buf.ptr, buf.length);
            }
            else
            {
                std.c.string.strerror_r(errno, buf.ptr, buf.length);
                auto s = buf.ptr;
            }
        }
        else
        {
            auto s = std.c.string.strerror(errno);
        }
        auto sysmsg = to!string(s);
        super(message ? message ~ "(" ~ sysmsg ~ ")" : sysmsg);
    }

/** Convenience functions that throw an $(D StdioException). */
    static void opCall(string msg)
    {
        throw new StdioException(msg);
    }

/// ditto
    static void opCall()
    {
        throw new StdioException(null, .errno);
    }
}

extern(C) void std_stdio_static_this()
{
    //Bind stdin, stdout, stderr
    __gshared File.Impl stdinImpl;
    stdinImpl.handle = core.stdc.stdio.stdin;
    .stdin._p = &stdinImpl;
    // stdout
    __gshared File.Impl stdoutImpl;
    stdoutImpl.handle = core.stdc.stdio.stdout;
    .stdout._p = &stdoutImpl;
    // stderr
    __gshared File.Impl stderrImpl;
    stderrImpl.handle = core.stdc.stdio.stderr;
    .stderr._p = &stderrImpl;
}

//---------
__gshared
{
    File stdin; /// The standard input stream.
    File stdout; /// The standard output stream.
    File stderr; /// The standard error stream.
}

unittest
{
    scope(failure) printf("Failed test at line %d\n", __LINE__);
    auto deleteme = testFilename();
    std.file.write(deleteme, "1 2\n4 1\n5 100");
    scope(exit) std.file.remove(deleteme);
    {
        File f = File(deleteme);
        scope(exit) f.close();
        auto t = [ tuple(1, 2), tuple(4, 1), tuple(5, 100) ];
        uint i;
        foreach (e; f.byRecord!(int, int)("%s %s"))
        {
            //writeln(e);
            assert(e == t[i++]);
        }
        assert(i == 3);
    }
}

// Private implementation of readln
version (DIGITAL_MARS_STDIO)
private size_t readlnImpl(FILE* fps, ref char[] buf, dchar terminator = '\n')
{
    FLOCK(fps);
    scope(exit) FUNLOCK(fps);

    /* Since fps is now locked, we can create an "unshared" version
     * of fp.
     */
    auto fp = cast(_iobuf*)fps;

    if (__fhnd_info[fp._file] & FHND_WCHAR)
    {   /* Stream is in wide characters.
         * Read them and convert to chars.
         */
        static assert(wchar_t.sizeof == 2);
        auto app = appender(buf);
        app.clear();
        for (int c = void; (c = FGETWC(fp)) != -1; )
        {
            if ((c & ~0x7F) == 0)
            {   app.put(cast(char) c);
                if (c == terminator)
                    break;
            }
            else
            {
                if (c >= 0xD800 && c <= 0xDBFF)
                {
                    int c2 = void;
                    if ((c2 = FGETWC(fp)) != -1 ||
                            c2 < 0xDC00 && c2 > 0xDFFF)
                    {
                        StdioException("unpaired UTF-16 surrogate");
                    }
                    c = ((c - 0xD7C0) << 10) + (c2 - 0xDC00);
                }
                //std.utf.encode(buf, c);
                app.put(cast(dchar)c);
            }
        }
        if (ferror(fps))
            StdioException();
        buf = app.data;
        return buf.length;
    }

    auto sz = GC.sizeOf(buf.ptr);
    //auto sz = buf.length;
    buf = buf.ptr[0 .. sz];
    if (fp._flag & _IONBF)
    {
        /* Use this for unbuffered I/O, when running
         * across buffer boundaries, or for any but the common
         * cases.
         */
      L1:
        auto app = appender(buf);
        app.clear();
        if(app.capacity == 0)
            app.reserve(128); // get at least 128 bytes available

        int c;
        while((c = FGETC(fp)) != -1) {
            app.put(cast(char) c);
            if(c == terminator) {
                buf = app.data;
                return buf.length;
            }

        }

        if (ferror(fps))
            StdioException();
        buf = app.data;
        return buf.length;
    }
    else
    {
        int u = fp._cnt;
        char* p = fp._ptr;
        int i;
        if (fp._flag & _IOTRAN)
        {   /* Translated mode ignores \r and treats ^Z as end-of-file
             */
            char c;
            while (1)
            {
                if (i == u)                // if end of buffer
                    goto L1;        // give up
                c = p[i];
                i++;
                if (c != '\r')
                {
                    if (c == terminator)
                        break;
                    if (c != 0x1A)
                        continue;
                    goto L1;
                }
                else
                {   if (i != u && p[i] == terminator)
                        break;
                    goto L1;
                }
            }
            if (i > sz)
            {
                buf = uninitializedArray!(char[])(i);
            }
            if (i - 1)
                memcpy(buf.ptr, p, i - 1);
            buf[i - 1] = cast(char)terminator;
            buf = buf[0 .. i];
            if (terminator == '\n' && c == '\r')
                i++;
        }
        else
        {
            while (1)
            {
                if (i == u)                // if end of buffer
                    goto L1;        // give up
                auto c = p[i];
                i++;
                if (c == terminator)
                    break;
            }
            if (i > sz)
            {
                buf = uninitializedArray!(char[])(i);
            }
            memcpy(buf.ptr, p, i);
            buf = buf[0 .. i];
        }
        fp._cnt -= i;
        fp._ptr += i;
        return i;
    }
}

version (MICROSOFT_STDIO)
private size_t readlnImpl(FILE* fps, ref char[] buf, dchar terminator = '\n')
{
    FLOCK(fps);
    scope(exit) FUNLOCK(fps);

    /* Since fps is now locked, we can create an "unshared" version
     * of fp.
     */
    auto fp = cast(_iobuf*)fps;

    auto sz = GC.sizeOf(buf.ptr);
    //auto sz = buf.length;
    buf = buf.ptr[0 .. sz];

    auto app = appender(buf);
    app.clear();
    if(app.capacity == 0)
        app.reserve(128); // get at least 128 bytes available

    int c;
    while((c = FGETC(fp)) != -1) {
        app.put(cast(char) c);
        if(c == terminator) {
            buf = app.data;
            return buf.length;
        }

    }

    if (ferror(fps))
        StdioException();
    buf = app.data;
    return buf.length;
}

version (GCC_IO)
private size_t readlnImpl(FILE* fps, ref char[] buf, dchar terminator = '\n')
{
    if (fwide(fps, 0) > 0)
    {   /* Stream is in wide characters.
         * Read them and convert to chars.
         */
        FLOCK(fps);
        scope(exit) FUNLOCK(fps);
        auto fp = cast(_iobuf*)fps;
        version (Windows)
        {
            buf.length = 0;
            for (int c = void; (c = FGETWC(fp)) != -1; )
            {
                if ((c & ~0x7F) == 0)
                {   buf ~= c;
                    if (c == terminator)
                        break;
                }
                else
                {
                    if (c >= 0xD800 && c <= 0xDBFF)
                    {
                        int c2 = void;
                        if ((c2 = FGETWC(fp)) != -1 ||
                                c2 < 0xDC00 && c2 > 0xDFFF)
                        {
                            StdioException("unpaired UTF-16 surrogate");
                        }
                        c = ((c - 0xD7C0) << 10) + (c2 - 0xDC00);
                    }
                    std.utf.encode(buf, c);
                }
            }
            if (ferror(fp))
                StdioException();
            return buf.length;
        }
        else version (Posix)
        {
            buf.length = 0;
            for (int c; (c = FGETWC(fp)) != -1; )
            {
                if ((c & ~0x7F) == 0)
                    buf ~= cast(char)c;
                else
                    std.utf.encode(buf, cast(dchar)c);
                if (c == terminator)
                    break;
            }
            if (ferror(fps))
                StdioException();
            return buf.length;
        }
        else
        {
            static assert(0);
        }
    }

    char *lineptr = null;
    size_t n = 0;
    auto s = getdelim(&lineptr, &n, terminator, fps);
    scope(exit) free(lineptr);
    if (s < 0)
    {
        if (ferror(fps))
            StdioException();
        buf.length = 0;                // end of file
        return 0;
    }
    buf = buf.ptr[0 .. GC.sizeOf(buf.ptr)];
    if (s <= buf.length)
    {
        buf.length = s;
        buf[] = lineptr[0 .. s];
    }
    else
    {
        buf = lineptr[0 .. s].dup;
    }
    return s;
}

version (GENERIC_IO)
private size_t readlnImpl(FILE* fps, ref char[] buf, dchar terminator = '\n')
{
    FLOCK(fps);
    scope(exit) FUNLOCK(fps);
    auto fp = cast(_iobuf*)fps;
    if (fwide(fps, 0) > 0)
    {   /* Stream is in wide characters.
         * Read them and convert to chars.
         */
        version (Windows)
        {
            buf.length = 0;
            for (int c; (c = FGETWC(fp)) != -1; )
            {
                if ((c & ~0x7F) == 0)
                {   buf ~= c;
                    if (c == terminator)
                        break;
                }
                else
                {
                    if (c >= 0xD800 && c <= 0xDBFF)
                    {
                        int c2 = void;
                        if ((c2 = FGETWC(fp)) != -1 ||
                                c2 < 0xDC00 && c2 > 0xDFFF)
                        {
                            StdioException("unpaired UTF-16 surrogate");
                        }
                        c = ((c - 0xD7C0) << 10) + (c2 - 0xDC00);
                    }
                    std.utf.encode(buf, c);
                }
            }
            if (ferror(fp))
                StdioException();
            return buf.length;
        }
        else version (Posix)
        {
            buf.length = 0;
            for (int c; (c = FGETWC(fp)) != -1; )
            {
                if ((c & ~0x7F) == 0)
                    buf ~= cast(char)c;
                else
                    std.utf.encode(buf, cast(dchar)c);
                if (c == terminator)
                    break;
            }
            if (ferror(fps))
                StdioException();
            return buf.length;
        }
        else
        {
            static assert(0);
        }
    }

    // Narrow stream
    // First, fill the existing buffer
    for (size_t bufPos = 0; bufPos < buf.length; )
    {
        immutable c = FGETC(fp);
        if (c == -1)
        {
            buf.length = bufPos;
            goto endGame;
        }
        buf.ptr[bufPos++] = cast(char) c;
        if (c == terminator)
        {
            // No need to test for errors in file
            buf.length = bufPos;
            return bufPos;
        }
    }
    // Then, append to it
    for (int c; (c = FGETC(fp)) != -1; )
    {
        buf ~= cast(char)c;
        if (c == terminator)
        {
            // No need to test for errors in file
            return buf.length;
        }
    }

  endGame:
    if (ferror(fps))
        StdioException();
    return buf.length;
}


/** Experimental network access via the File interface

        Opens a TCP connection to the given host and port, then returns
        a File struct with read and write access through the same interface
        as any other file (meaning writef and the byLine ranges work!).

        Authors:
                Adam D. Ruppe

        Bugs:
                Only works on Linux
*/
version(linux) {
    static import linux = std.c.linux.linux;
    static import sock = std.c.linux.socket;

    File openNetwork(string host, ushort port) {
        auto h = enforce( sock.gethostbyname(std.string.toStringz(host)),
            new StdioException("gethostbyname"));

        int s = sock.socket(sock.AF_INET, sock.SOCK_STREAM, 0);
        enforce(s != -1, new StdioException("socket"));

        scope(failure) {
            linux.close(s); // want to make sure it doesn't dangle if
                            // something throws. Upon normal exit, the
                            // File struct's reference counting takes
                            // care of closing, so we don't need to
                            // worry about success
        }

        sock.sockaddr_in addr;

        addr.sin_family = sock.AF_INET;
        addr.sin_port = sock.htons(port);
        std.c.string.memcpy(&addr.sin_addr.s_addr, h.h_addr, h.h_length);

        enforce(sock.connect(s, cast(sock.sockaddr*) &addr, addr.sizeof) != -1,
            new StdioException("Connect failed"));

        return File(enforce(fdopen(s, "w+".ptr)),
            host ~ ":" ~ to!string(port));
    }
}

version(unittest) string testFilename(string file = __FILE__, size_t line = __LINE__)
{
    import std.path;

    // Non-ASCII characters can't be used because of snn.lib @@@BUG8643@@@
    version(DIGITAL_MARS_STDIO)
        return text("deleteme-.", baseName(file), ".", line);
    else

        // filename intentionally contains non-ASCII (Russian) characters
        return text("deleteme-детка.", baseName(file), ".", line);
}
