
// Written in the D programming language.

/* Written by Walter Bright and Andrei Alexandrescu
 * www.digitalmars.com
 * Placed in the Public Domain.
 */

/********************************
 * 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).
 * Macros:
 *	WIKI=Phobos/StdStdio
 */

module std.stdio;

public import std.c.stdio;

import std.format;
import std.utf;
import std.string;
import std.gc;
import std.c.stdlib;
import std.c.string;
import std.c.stddef;
import std.conv;
import std.traits;
import std.contracts;
import std.file, std.typetuple; // for testing only

version (DigitalMars)
{
    version (Windows)
    {
	// Specific to the way Digital Mars C does stdio
	version = DIGITAL_MARS_STDIO;
    }
}

version (DIGITAL_MARS_STDIO)
{
}
else
{
    // Specific to the way Gnu C does stdio
    version = GCC_IO;
    import std.c.linux.linux;
}

version (DIGITAL_MARS_STDIO)
{
    extern (C)
    {
	/* **
	 * Digital Mars under-the-hood C I/O functions
	 */
	int _fputc_nlock(int, FILE*);
	int _fputwc_nlock(int, FILE*);
	int _fgetc_nlock(FILE*);
	int _fgetwc_nlock(FILE*);
	int __fp_lock(FILE*);
	void __fp_unlock(FILE*);
    }
    alias _fputc_nlock FPUTC;
    alias _fputwc_nlock FPUTWC;
    alias _fgetc_nlock FGETC;
    alias _fgetwc_nlock FGETWC;

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

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

         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");
}


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

    this(string msg)
    {
	super(msg);
    }

    this(uint errno)
    {	const(char*) s = std.string.strerror(errno);
	super(std.string.toString(s).idup);
    }

    static void opCall(string msg)
    {
	throw new StdioException(msg);
    }

    static void opCall()
    {
	throw new StdioException(getErrno());
    }
}

private
void writefx(FILE* fp, TypeInfo[] arguments, void* argptr, int newline=false)
{
    int orientation = fwide(fp, 0);

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

    if (orientation <= 0)		// byte orientation or no orientation
    {
	void putc(dchar c)
	{
	    if (c <= 0x7F)
	    {
		FPUTC(c, fp);
	    }
	    else
	    {   char[4] buf;
		auto b = std.utf.toUTF8(buf, c);
		for (size_t i = 0; i < b.length; i++)
		    FPUTC(b[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
		{   wchar[2] buf;

		    buf[0] = cast(wchar) ((((c - 0x10000) >> 10) & 0x3FF) + 0xD800);
		    buf[1] = cast(wchar) (((c - 0x10000) & 0x3FF) + 0xDC00);
		    FPUTWC(buf[0], fp);
		    FPUTWC(buf[1], fp);
		}
	    }
	}
	else version (linux)
             {
                 void putcw(dchar c)
                 {
                     FPUTWC(c, fp);
                 }
             }
	else
	{
	    static assert(0);
	}

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

/***********************************
 * If the first argument $(D_PARAM args[0]) is a $(D_PARAM FILE*), for
 * each argument $(D_PARAM arg) in $(D_PARAM args[1..$]), format the
 * argument (as per $(LINK2 std_conv.html, to!(string)(arg))) and
 * write the resulting string to $(D_PARAM args[0]). If $(D_PARAM
 * args[0]) is not a $(D_PARAM FILE*), the call is equivalent to
 * $(D_PARAM write(stdout, args)).
 *
 * A call without any arguments will fail to compile. In the
 * exceedingly rare case you'd want to print a $(D_PARAM FILE*) to
 * $(D_PARAM stdout) as a hex pointer, $(D_PARAM write("", myFilePtr))
 * will do the trick.
 *
 * In case of an I/O error, throws an StdioException.
 */
void write(T...)(T args)
{
    static if (is(typeof(args[0]) : FILE*))
    {
        alias args[0] target;
        static const first = 1;
    }
    else
    {
        alias stdout target;
        static const first = 0;
    }
    writef(target, "", args[first .. $]);
}

unittest
{
    void[] buf;
    write(buf);
    // test write
    string file = "dmd-build-test.deleteme.txt";
    FILE* f = fopen(file, "w");
    assert(f);
    write(f, "Hello, ",  "world number ", 42, "!");
    fclose(f) == 0 || assert(false);
    assert(cast(char[]) std.file.read(file) == "Hello, world number 42!");
    // test write on stdout
    auto saveStdout = stdout;
    scope(exit) stdout = saveStdout;
    stdout = fopen(file, "w");
    assert(stdout);
    Object obj;
    write("Hello, ",  "world number ", 42, "! ", obj);
    fclose(stdout) == 0 || assert(false);
    auto result = cast(char[]) std.file.read(file);
    assert(result == "Hello, world number 42! null", result);
}

/***********************************
 * Equivalent to $(D_PARAM write(args, '\n')).  Calling $(D_PARAM
 * writeln) without arguments is valid and just prints a newline to
 * the standard output.
 */
void writeln(T...)(T args)
{
    write(args, '\n');
}

unittest
{
    // test writeln
    string file = "dmd-build-test.deleteme.txt";
    FILE* f = fopen(file, "w");
    assert(f);
    writeln(f, "Hello, ",  "world number ", 42, "!");
    fclose(f) == 0 || assert(false);
    assert(cast(char[]) std.file.read(file) == "Hello, world number 42!\n");
    // test writeln on stdout
    auto saveStdout = stdout;
    scope(exit) stdout = saveStdout;
    stdout = fopen(file, "w");
    assert(stdout);
    writeln("Hello, ",  "world number ", 42, "!");
    fclose(stdout) == 0 || assert(false);
    assert(cast(char[]) std.file.read(file) == "Hello, world number 42!\n");
}

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

 Warning:

 New behavior starting with D 2.006: unlike previous versions,
 $(D_PARAM writef) (and also $(D_PARAM writefln)) only scans its first
 string argument for format specifiers, but not subsequent string
 arguments. Also new in 2.006 is support for positional parameters
 with $(LINK2 http://www.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 next parameter after
the largest positional parameter already used. */

void writef(T...)(T args)
{
    FileWriter!(char) w;
    static const errorMessage =
        "You must pass a formatting string as the first"
        " argument to writef. If no formatting is needed,"
        " you may want to use write.";
    static if (is(typeof(args[0]) : FILE*))
    {
        alias args[0] target;
        static const first = 1;
    }
    else
    {
        alias stdout target;
        static const first = 0;
    }
    w.backend = target;
    FLOCK(w.backend);
    scope(exit) FUNLOCK(w.backend);
    static if (!isSomeString!(T[first]))
    {
        // compatibility hack
        std.format.formattedWrite(w, "", args[first .. $]);
    }
    else
    {
        std.format.formattedWrite(w, args[first .. $]);
    }
}

unittest
{
    // test writef
    string file = "dmd-build-test.deleteme.txt";
    auto f = fopen(file, "w");
    assert(f);
    writef(f, "Hello, %s world number %s!", "nice", 42);
    fclose(f) == 0 || assert(false);
    assert(cast(char[]) std.file.read(file) ==  "Hello, nice world number 42!");
    // test write on stdout
    auto saveStdout = stdout;
    scope(exit) stdout = saveStdout;
    stdout = fopen(file, "w");
    assert(stdout);
    writef("Hello, %s world number %s!", "nice", 42);
    fclose(stdout) == 0 || assert(false);
    assert(cast(char[]) std.file.read(file) == "Hello, nice world number 42!");
}

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

unittest
{
    // test writefln
    string file = "dmd-build-test.deleteme.txt";
    FILE* f = fopen(file, "w");
    assert(f);
    writefln(f, "Hello, %s world number %s!", "nice", 42);
    fclose(f) == 0 || assert(false);
    assert(cast(char[]) std.file.read(file) == "Hello, nice world number 42!\n");
    // test write on stdout
    auto saveStdout = stdout;
    scope(exit) stdout = saveStdout;
    stdout = fopen(file, "w");
    assert(stdout);
    writefln("Hello, %s world number %s!", "nice", 42);
    foreach (F ; TypeTuple!(ifloat, idouble, ireal))
    {
        F a = 5i;
        F b = a % 2;
        writefln(b);
    }
    fclose(stdout) == 0 || assert(false);
    auto read = cast(char[]) std.file.read(file);
    assert(read == "Hello, nice world number 42!\n1\n1\n1\n", read);
}

/***********************************
 * Kept for backward compatibility. Use $(D_PARAM writef) instead.
 */
void fwritef(FILE* fp, ...)
{
    writefx(fp, _arguments, _argptr, 0);
}

/***********************************
 * Kept for backward compatibility. Use $(D_PARAM writefln) instead.
 */
void fwritefln(FILE* fp, ...)
{
    writefx(fp, _arguments, _argptr, 1);    
}

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

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

/**********************************
 * Read line from stream $(D_PARAM fp) and write it to $(D_PARAM
 * buf[]), including terminating character.
 *
 * This is often faster than $(D_PARAM readln(FILE*)) because the buffer
 * is reused each call. Note that reusing the buffer means that
 * the previous contents of it need to be copied if needed.
 * Params:
 *	$(D_PARAM fp) = input stream
 *	$(D_PARAM 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_PARAM StdioException) on error
 * Example:
 *	Reads $(D_PARAM stdin) and writes it to $(D_PARAM stdout).
---
import std.stdio;

int main()
{
    char[] buf;
    while (readln(stdin, buf))
	write(buf);
    return 0;
}
---
This method is more efficient than the one in the previous example
because $(D_PARAM readln(stdin, buf)) reuses (if possible) memory
allocated by $(D_PARAM buf), whereas $(D_PARAM buf = readln()) makes a
new memory allocation with every line.
*/
size_t readln(FILE* fp, inout char[] buf, dchar terminator = '\n')
{
    version (DIGITAL_MARS_STDIO)
    {
	FLOCK(fp);
	scope(exit) FUNLOCK(fp);

	if (__fhnd_info[fp._file] & FHND_WCHAR)
	{   /* Stream is in wide characters.
	     * Read them and convert to chars.
	     */
	    static assert(wchar_t.sizeof == 2);
	    buf.length = 0;
	    int c2;
	    for (int c; (c = FGETWC(fp)) != -1; )
	    {
		if ((c & ~0x7F) == 0)
		{   buf ~= c;
		    if (c == terminator)
			break;
		}
		else
		{
		    if (c >= 0xD800 && c <= 0xDBFF)
		    {
			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;
	}

	auto sz = std.gc.capacity(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:
	    char *p;

	    if (sz)
	    {
		p = buf.ptr;
	    }
	    else
	    {
		sz = 64;
		p = cast(char*) std.gc.malloc(sz);
		std.gc.hasNoPointers(p);
		buf = p[0 .. sz];
	    }
	    size_t i = 0;
	    for (int c; (c = FGETC(fp)) != -1; )
	    {
		if ((p[i] = c) != terminator)
		{
		    i++;
		    if (i < sz)
			continue;
		    buf = p[0 .. i] ~ readln(fp);
		    return buf.length;
		}
		else
		{
		    buf = p[0 .. i + 1];
		    return i + 1;
		}
	    }
	    if (ferror(fp))
		StdioException();
	    buf = p[0 .. i];
	    return i;
	}
	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 = cast(char[])std.gc.malloc(i);
		    std.gc.hasNoPointers(buf.ptr);
		}
		if (i - 1)
		    memcpy(buf.ptr, p, i - 1);
		buf[i - 1] = terminator;
		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 = cast(char[])std.gc.malloc(i);
		    std.gc.hasNoPointers(buf.ptr);
		}
		memcpy(buf.ptr, p, i);
	    }
	    fp._cnt -= i;
	    fp._ptr += i;
	    buf = buf[0 .. i];
	    return i;
	}
    }
    else version (GCC_IO)
         {
             if (fwide(fp, 0) > 0)
             {   /* Stream is in wide characters.
                  * Read them and convert to chars.
                  */
                 FLOCK(fp);
                 scope(exit) FUNLOCK(fp);
                 version (Windows)
                 {
                     buf.length = 0;
                     int c2;
                     for (int c; (c = FGETWC(fp)) != -1; )
                     {
                         if ((c & ~0x7F) == 0)
                         {   buf ~= c;
                             if (c == terminator)
                                 break;
                         }
                         else
                         {
                             if (c >= 0xD800 && c <= 0xDBFF)
                             {
                                 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 (linux)
                      {
                          buf.length = 0;
                          for (int c; (c = FGETWC(fp)) != -1; )
                          {
                              if ((c & ~0x7F) == 0)
                                  buf ~= c;
                              else
                                  std.utf.encode(buf, cast(dchar)c);
                              if (c == terminator)
                                  break;
                          }
                          if (ferror(fp))
                              StdioException();
                          return buf.length;
                      }
                 else
                 {
                     static assert(0);
                 }
             }

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

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

/** ditto */
// TODO: optimize this
size_t readln(FILE* f, inout wchar[] buf, dchar terminator = '\n')
{
    string s = readln(f, terminator);
    if (!s.length) return 0;
    buf.length = 0;
    foreach (wchar c; s)
    {
        buf ~= c;
    }
    return buf.length;
}

/** ditto */
// TODO: fold this together with wchar
size_t readln(FILE* f, inout dchar[] buf, dchar terminator = '\n')
{
    string s = readln(f, terminator);
    if (!s.length) return 0;
    buf.length = 0;
    foreach (dchar c; s)
    {
        buf ~= c;
    }
    return buf.length;
}

/***********************************
 * Convenience function that forwards to $(D_PARAM std.c.stdio.fopen)
 * with appropriately-constructed C-style strings.
 */
FILE* fopen(string name, string mode = "r")
{
    return std.c.stdio.fopen(toStringz(name), toStringz(mode));
}

extern(C) FILE* popen(const char*, const char*);

/***********************************
 * Convenience function that forwards to $(D_PARAM std.c.stdio.popen)
 * with appropriately-constructed C-style strings.
 */
FILE* popen(string name, string mode)
{
    return popen(toStringz(name), toStringz(mode));
}

/*
 * Implements the static Writer interface for a FILE*. Instantiate it
 * with the character type, e.g. FileWriter!(char),
 * FileWriter!(wchar), or FileWriter!(dchar). Regardless of
 * instantiation, FileWriter supports all character widths; it only is
 * the most efficient at accepting the character type it was
 * instantiated with.
 *
 * */
private struct FileWriter(Char)
{
    alias Char NativeChar;
    FILE* backend;
    int orientation;
    void write(C)(in C[] s)
    {
        if (!orientation) orientation = fwide(backend, 0);
        if (orientation <= 0 && C.sizeof == 1)
        {
            // lucky case: narrow chars on narrow stream
            if (std.c.stdio.fwrite(s.ptr, C.sizeof, s.length, backend)
                != s.length)
            {
                StdioException();
            }
        }
        else
        {
            // put each character in turn
            foreach (C c; s)
            {
                putchar(c);
            }
        }
    }
    void putchar(C)(in C c)
    {
        if (!orientation) orientation = fwide(backend, 0);
        static if (c.sizeof == 1)
        {
            // simple char
            if (orientation <= 0) FPUTC(c, backend);
            else FPUTWC(c, backend);
        }
        else static if (c.sizeof == 2)
        {
            if (orientation <= 0)
            {
                if (c <= 0x7F)
                {
                    FPUTC(c, backend);
                }
                else
                {
                    char[4] buf;
                    auto b = std.utf.toUTF8(buf, c);
                    foreach (i ; 0 .. b.length)
                        FPUTC(b[i], backend);
                }
            }
            else
            {
                FPUTWC(c, backend);
            }
        }
        else // 32-bit characters
        {
            if (orientation <= 0)
            {
                if (c <= 0x7F)
                {
                    FPUTC(c, backend);
                }
                else
                {
                    char[4] buf;
                    auto b = std.utf.toUTF8(buf, c);
                    foreach (i ; 0 .. b.length)
                        FPUTC(b[i], backend);
                }
            }
            else
            {
                version (Windows)
                {
                    assert(isValidDchar(c));
                    if (c <= 0xFFFF)
                    {
                        FPUTWC(c, backend);
                    }
                    else
                    {
                        FPUTWC(cast(wchar) ((((c - 0x10000) >> 10) & 0x3FF)
                                            + 0xD800), backend);
                        FPUTWC(cast(wchar) (((c - 0x10000) & 0x3FF) + 0xDC00),
                               backend);
                    }
                }
                else version (linux)
                {
                    FPUTWC(c, backend);
                }
                else
                {
                    static assert(0);
                }
            }
        }
    }
}

/**
 * Iterates through the lines of a file by using $(D_PARAM 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_PARAM line), and the behavior of $(D_PARAM lines)
changes accordingly:

$(OL $(LI If $(D_PARAM line) has type $(D_PARAM string), $(D_PARAM
wstring), or $(D_PARAM dstring), a new string of the respective type
is allocated every read.) $(LI If $(D_PARAM line) has type $(D_PARAM
char[]), $(D_PARAM wchar[]), $(D_PARAM dchar[]), the line's content
will be reused (overwritten) across reads.) $(LI If $(D_PARAM line)
has type $(D_PARAM invariant(ubyte)[]), the behavior is similar to
case (1), except that no UTF checking is attempted upon input.) $(LI
If $(D_PARAM line) has type $(D_PARAM 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_PARAM ulong) or $(D_PARAM
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_PARAM StdioException) is thrown. 
 */

struct lines
{
    FILE * f;
    dchar terminator = '\n';
    int opApply(D)(D dg)
    {
        alias ParameterTypeTuple!(dg) Parms;
        static if (isSomeString!(Parms[$ - 1]))
        {
            static const 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 (!readln(f, 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)(D dg)
    {
        alias ParameterTypeTuple!(dg) Parms;
        static const duplicate = is(typeof(Parms[$ - 1]) : invariant(ubyte)[]);
        int result = 1;
        int c = void;
        FLOCK(f);
	scope(exit) FUNLOCK(f);
        ubyte[] buffer;
        static if (Parms.length == 2)
            Parms[0] line = 0;
        while ((c = FGETC(f)) != -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);
                scope(exit) FLOCK(f);
                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 (!feof(f)) throw new StdioException(ferror(f)); // error occured
        return result;
    }
}

unittest
{
    string file = "dmd-build-test.deleteme.txt";
    alias TypeTuple!(string, wstring, dstring,
                     char[], wchar[], dchar[])
        TestedWith;
    foreach (T; TestedWith) {
        // test looping with an empty file
        std.file.write(file, "");
        auto f = fopen(file, "r");
        foreach (T line; lines(f))
        {
            assert(false);
        }
        fclose(f) == 0 || assert(false);

        // test looping with a file with three lines
        std.file.write(file, "Line one\nline two\nline three\n");
        f = fopen(file, "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;
        }
        fclose(f) == 0 || assert(false);
    
        // test looping with a file with three lines, last without a newline
        std.file.write(file, "Line one\nline two\nline three");
        f = fopen(file, "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;
        }
        fclose(f) == 0 || assert(false);
    }

    // test with ubyte[] inputs
    alias TypeTuple!(invariant(ubyte)[], ubyte[])
        TestedWith2;
    foreach (T; TestedWith2) {
        // test looping with an empty file
        std.file.write(file, "");
        auto f = fopen(file, "r");
        foreach (T line; lines(f))
        {
            assert(false);
        }
        fclose(f) == 0 || assert(false);

        // test looping with a file with three lines
        std.file.write(file, "Line one\nline two\nline three\n");
        f = fopen(file, "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;
        }
        fclose(f) == 0 || assert(false);
    
        // test looping with a file with three lines, last without a newline
        std.file.write(file, "Line one\nline two\nline three");
        f = fopen(file, "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;
        }
        fclose(f) == 0 || assert(false);

    }

    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(file, "Line one\nline two\nline three");
        auto f = fopen(file, "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;
        }
        fclose(f) == 0 || assert(false);
    }
}

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

Example:

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

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

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

struct chunks
{
    private FILE* f;
    private size_t size;
    int opApply(int delegate(ref ubyte[]) dg)
    {
        const maxStackSize = 500 * 1024;
        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;
        while ((r = std.c.stdio.fread(buffer.ptr,
                                      buffer[0].sizeof, size, f)) > 0)
        {
            assert(r <= size);
            if (r != size)
            {
                // error occured
                if (!feof(f)) throw new StdioException(ferror(f));
                if (!r) break; // done reading
            }
            if ((result = dg(buffer)) != 0) break;
        }
        return result;
    }
}

unittest
{
    string file = "dmd-build-test.deleteme.txt";
    // test looping with an empty file
    std.file.write(file, "");
    auto f = fopen(file, "r");
    foreach (ubyte[] line; chunks(f, 4))
    {
        assert(false);
    }
    fclose(f) == 0 || assert(false);
    
    // test looping with a file with three lines
    std.file.write(file, "Line one\nline two\nline three\n");
    f = fopen(file, "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;
    }
    fclose(f) == 0 || assert(false);
}
