Index: /trunk/src/kDuplicates/Makefile.kmk
===================================================================
--- /trunk/src/kDuplicates/Makefile.kmk	(revision 3010)
+++ /trunk/src/kDuplicates/Makefile.kmk	(revision 3010)
@@ -0,0 +1,35 @@
+# $Id$
+## @file
+# Sub-makefile for kDuplicates.
+#
+
+#
+# Copyright (c) 2016 knut st. osmundsen <bird-kBuild-spamx@anduin.net>
+#
+# This file is part of kBuild.
+#
+# kBuild is free software; you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation; either version 3 of the License, or
+# (at your option) any later version.
+#
+# kBuild is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with kBuild.  If not, see <http://www.gnu.org/licenses/>
+#
+#
+
+SUB_DEPTH = ../..
+include $(KBUILD_PATH)/subheader.kmk
+
+PROGRAMS += kDuplicates
+kDuplicates_TEMPLATE        = BIN
+kDuplicates_LIBS            = $(LIB_KUTIL)
+kDuplicates_SOURCES         = kDuplicates.c
+
+include $(FILE_KBUILD_SUB_FOOTER)
+
Index: /trunk/src/kDuplicates/kDuplicates.c
===================================================================
--- /trunk/src/kDuplicates/kDuplicates.c	(revision 3010)
+++ /trunk/src/kDuplicates/kDuplicates.c	(revision 3010)
@@ -0,0 +1,838 @@
+/* $Id$ */
+/** @file
+ * kDuplicate - Utility that finds duplicate files.
+ */
+
+/*
+ * Copyright (c) 2016 knut st. osmundsen <bird-kBuild-spamx@anduin.net>
+ *
+ * This file is part of kBuild.
+ *
+ * kBuild is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * kBuild is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with kBuild; if not, write to the Free Software
+ * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
+ *
+ */
+
+/*******************************************************************************
+*   Header Files                                                               *
+*******************************************************************************/
+#include <k/kTypes.h>
+//#include <stdlib.h>
+#include <wchar.h>
+#include <string.h>
+#include <stdio.h>
+
+#include "md5.h"
+//#include "sha2.h"
+
+#include "nt/ntstuff.h"
+#include "nt/ntstat.h"
+#include "nt/fts-nt.h"
+#include "nt/nthlp.h"
+#include "nt/ntunlink.h"
+
+
+/*********************************************************************************************************************************
+*   Structures and Typedefs                                                                                                      *
+*********************************************************************************************************************************/
+/**
+ * The key is made up of two cryptographic hashes, collisions are
+ * highly unlikely (once SHA2 is implemented).
+ */
+typedef struct KDUPFILENODEKEY
+{
+    /** The MD5 digest of the file. */
+    KU8             abMd5[16];
+    /** The 256-bit SHA-2 digest of the file. */
+    KU8             abSha2[32];
+} KDUPFILENODEKEY;
+/** Pointer to a file node.*/
+typedef struct KDUPFILENODE *PKDUPFILENODE;
+/**
+ * Hash tree node.
+ */
+typedef struct KDUPFILENODE
+{
+    /** The is made up of two hashes. */
+    KDUPFILENODEKEY mKey;
+    /** Left branch. */
+    PKDUPFILENODE   mpLeft;
+    /** Right branch. */
+    PKDUPFILENODE   mpRight;
+    /** Tree height (hmm). */
+    KU8             mHeight;
+
+    /** The inode number. */
+    KU64            uInode;
+    /** The device number. */
+    KU64            uDev;
+
+    /** Pointer to next hard linked node (same inode and udev values). */
+    PKDUPFILENODE   pNextHardLink;
+    /** Pointer to next duplicate node. */
+    PKDUPFILENODE   pNextDup;
+    /** Pointer to next duplicate node on the global list. */
+    PKDUPFILENODE   pNextGlobalDup;
+
+    /** The path to this file (variable size). */
+    wchar_t         wszPath[1];
+} KDUPFILENODE;
+
+/*#define KAVL_EQUAL_ALLOWED*/
+#define KAVL_CHECK_FOR_EQUAL_INSERT
+#define KAVL_MAX_STACK          32
+/*#define KAVL_RANGE */
+/*#define KAVL_OFFSET */
+/*#define KAVL_STD_KEY_COMP*/
+#define KAVLKEY                 KDUPFILENODEKEY
+#define KAVLNODE                KDUPFILENODE
+#define KAVL_FN(name)           kDupFileTree_ ## name
+#define KAVL_TYPE(prefix,name)  prefix ## KDUPFILENODE ## name
+#define KAVL_INT(name)          KDUPFILENODEINT ## name
+#define KAVL_DECL(rettype)      static rettype
+#define KAVL_G(key1, key2)      ( memcmp(&(key1), &(key2), sizeof(KDUPFILENODEKEY)) >  0 )
+#define KAVL_E(key1, key2)      ( memcmp(&(key1), &(key2), sizeof(KDUPFILENODEKEY)) == 0 )
+#define KAVL_NE(key1, key2)     ( memcmp(&(key1), &(key2), sizeof(KDUPFILENODEKEY)) != 0 )
+
+#define register
+#include <k/kAvlTmpl/kAvlBase.h>
+#include <k/kAvlTmpl/kAvlDoWithAll.h>
+//#include <k/kAvlTmpl/kAvlEnum.h> - busted
+#include <k/kAvlTmpl/kAvlGet.h>
+#include <k/kAvlTmpl/kAvlGetBestFit.h>
+#include <k/kAvlTmpl/kAvlGetWithParent.h>
+#include <k/kAvlTmpl/kAvlRemove2.h>
+#include <k/kAvlTmpl/kAvlRemoveBestFit.h>
+#include <k/kAvlTmpl/kAvlUndef.h>
+#undef register
+
+
+/** Pointer to a size tree node. */
+typedef struct KDUPSIZENODE *PKDUPSIZENODE;
+/**
+ * Size tree node.
+ */
+typedef struct KDUPSIZENODE
+{
+    /** The file size. */
+    KU64            mKey;
+    /** Left branch. */
+    PKDUPSIZENODE   mpLeft;
+    /** Right branch. */
+    PKDUPSIZENODE   mpRight;
+    /** Tree height (hmm). */
+    KU8             mHeight;
+    /** Number of files. */
+    KU32            cFiles;
+    /** Tree with same sized files.
+     * When cFiles is 1 the root node does not have hashes calculated yet.  */
+    KDUPFILENODEROOT FileRoot;
+} KDUPSIZENODE;
+
+/*#define KAVL_EQUAL_ALLOWED*/
+#define KAVL_CHECK_FOR_EQUAL_INSERT
+#define KAVL_MAX_STACK          32
+/*#define KAVL_RANGE */
+/*#define KAVL_OFFSET */
+#define KAVL_STD_KEY_COMP
+#define KAVLKEY                 KU64
+#define KAVLNODE                KDUPSIZENODE
+#define KAVL_FN(name)           kDupSizeTree_ ## name
+#define KAVL_TYPE(prefix,name)  prefix ## KDUPSIZENODE ## name
+#define KAVL_INT(name)          KDUPSIZENODEINT ## name
+#define KAVL_DECL(rettype)      static rettype
+
+#include <k/kAvlTmpl/kAvlBase.h>
+#include <k/kAvlTmpl/kAvlDoWithAll.h>
+//#include <k/kAvlTmpl/kAvlEnum.h> - busted
+#include <k/kAvlTmpl/kAvlGet.h>
+#include <k/kAvlTmpl/kAvlGetBestFit.h>
+#include <k/kAvlTmpl/kAvlGetWithParent.h>
+#include <k/kAvlTmpl/kAvlRemove2.h>
+#include <k/kAvlTmpl/kAvlRemoveBestFit.h>
+#include <k/kAvlTmpl/kAvlUndef.h>
+
+
+/*********************************************************************************************************************************
+*   Global Variables                                                                                                             *
+*********************************************************************************************************************************/
+/** The verbosity level. */
+static unsigned g_cVerbosity                    = 2;
+
+/** Whether to recurse into subdirectories. */
+static KBOOL    g_fRecursive                    = K_FALSE;
+/** Whether to recurse into symlinked subdirectories. */
+static KBOOL    g_fRecursiveViaSymlinks         = K_FALSE;
+/** Whether to follow symbolicly linked files. */
+static KBOOL    g_fFollowSymlinkedFiles         = K_TRUE;
+
+/** Minimum file size to care about.   */
+static KU64     g_cbMinFileSize                 = 1;
+/** Maximum file size to care about.   */
+static KU64     g_cbMaxFileSize                 = KU64_MAX;
+
+/** The root of the size tree.   */
+static KDUPSIZENODEROOT g_SizeRoot;
+
+/** Global list of duplicate file with duplicates.
+ * @remarks This only contains the files in the hash tree, not the ones on
+ *          the KDUPFILENODE::pNextDup list. */
+static PKDUPFILENODE    g_pDuplicateHead        = NULL;
+/** Where to insert the next file with duplicates. */
+static PKDUPFILENODE   *g_ppNextDuplicate       = &g_pDuplicateHead;
+
+/** Number of files we're tracking. */
+static KU64             g_cFiles                = 0;
+/** Number of hardlinked files or files entered more than once. */
+static KU64             g_cHardlinked           = 0;
+/** Number of duplicates files (not hardlinked). */
+static KU64             g_cDuplicates           = 0;
+/** Number of duplicates files that can be hardlinked. */
+static KU64             g_cDuplicatesSaved      = 0;
+/** Size that could be saved if the duplicates were hardlinked. */
+static KU64             g_cbDuplicatesSaved     = 0;
+
+
+
+/**
+ * Wrapper around malloc() that complains when out of memory.
+ *
+ * @returns Pointer to allocated memory
+ * @param   cb      The size of the memory to allocate.
+ */
+static void *kDupAlloc(KSIZE cb)
+{
+    void *pvRet = malloc(cb);
+    if (pvRet)
+        return pvRet;
+    fprintf(stderr, "kDuplicate: error: out of memory! (cb=%#z)\n", cb);
+    return NULL;
+}
+
+/** Wrapper around free() for symmetry. */
+#define kDupFree(ptr) free(ptr)
+
+
+static void kDupHashFile(PKDUPFILENODE pFileNode, FTSENT *pFtsEnt)
+{
+    KSIZE           i;
+    PKDUPFILENODE  *ppHash;
+
+    /*
+     * Open the file.
+     */
+    HANDLE hFile;
+    if (pFtsEnt && pFtsEnt->fts_parent && pFtsEnt->fts_parent->fts_dirfd != INVALID_HANDLE_VALUE)
+        hFile = birdOpenFileExW(pFtsEnt->fts_parent->fts_dirfd, pFtsEnt->fts_wcsname,
+                                FILE_READ_DATA | SYNCHRONIZE,
+                                FILE_ATTRIBUTE_NORMAL,
+                                FILE_SHARE_READ | FILE_SHARE_WRITE | FILE_SHARE_DELETE,
+                                FILE_OPEN,
+                                FILE_NON_DIRECTORY_FILE | FILE_OPEN_FOR_BACKUP_INTENT | FILE_SYNCHRONOUS_IO_NONALERT,
+                                OBJ_CASE_INSENSITIVE);
+    else
+        hFile = birdOpenFileExW(NULL, pFileNode->wszPath,
+                                FILE_READ_DATA | SYNCHRONIZE,
+                                FILE_ATTRIBUTE_NORMAL,
+                                FILE_SHARE_READ | FILE_SHARE_WRITE | FILE_SHARE_DELETE,
+                                FILE_OPEN,
+                                FILE_NON_DIRECTORY_FILE | FILE_OPEN_FOR_BACKUP_INTENT | FILE_SYNCHRONOUS_IO_NONALERT,
+                                OBJ_CASE_INSENSITIVE);
+    if (hFile != INVALID_HANDLE_VALUE)
+    {
+        /*
+         * Init the hash calculation contexts.
+         */
+        struct MD5Context Md5Ctx;
+        //SHA256CONTEXT Sha256Ctx;
+        MD5Init(&Md5Ctx);
+        //Sha256Init(&Sha256Ctx);
+
+        /*
+         * Process the file chunk by chunk.
+         *
+         * We could complicate this by memory mapping medium sized files, but
+         * those kind of complications can wait.
+         */
+        for (;;)
+        {
+            static KU8          s_abBuffer[2*1024*1024];
+            MY_NTSTATUS         rcNt;
+            MY_IO_STATUS_BLOCK  Ios;
+            Ios.Information = -1;
+            Ios.u.Status    = -1;
+            rcNt = g_pfnNtReadFile(hFile, NULL /*hEvent*/, NULL /*pfnApc*/, NULL /*pvApcCtx*/,
+                                   &Ios, s_abBuffer, sizeof(s_abBuffer), NULL /*poffFile*/, NULL /*puKey*/);
+            if (MY_NT_SUCCESS(rcNt))
+            {
+                MD5Update(&Md5Ctx, s_abBuffer, (unsigned)Ios.Information);
+                //SHA256Update(&Sha256Ctx, s_abBuffer, Ios.Information);
+            }
+            else if (rcNt != STATUS_END_OF_FILE)
+            {
+                fprintf(stderr, "kDuplicate: warning: Error reading '%ls': %#x\n", pFileNode->wszPath, rcNt);
+                break;
+            }
+
+            /* Check for end of file. */
+            if (   rcNt == STATUS_END_OF_FILE
+                || Ios.Information < sizeof(s_abBuffer))
+            {
+                MD5Final(pFileNode->mKey.abMd5, &Md5Ctx);
+                //Sha256Final(pFileNode->mKey.abSha2, &Sha256Ctx);
+
+                birdCloseFile(hFile);
+                return;
+            }
+        }
+
+        birdCloseFile(hFile);
+    }
+    else
+        fprintf(stderr, "kDuplicate: warning: Failed to open '%ls': %s (%d)\n", pFileNode->wszPath, strerror(errno), errno);
+
+    /*
+     * Hashing failed.  We fake the digests by repeating the node pointer value
+     * again and again, holding a collision with both SHA2 and MD5 with similar
+     * digest pattern for highly unlikely.
+     */
+    ppHash = (PKDUPFILENODE *)&pFileNode->mKey;
+    i = sizeof(pFileNode->mKey) / sizeof(*ppHash);
+    while (i-- > 0)
+        *ppHash++ = pFileNode;
+}
+
+
+/**
+ * Deal with one file, adding it to the tree if it matches the criteria.
+ *
+ * @returns 0 on success, non-zero on failure.
+ * @param   pFtsEnt         The FTS entry for the file.
+ */
+static int kDupDoFile(FTSENT *pFtsEnt)
+{
+    KU64 cbFile;
+
+    if (g_cVerbosity >= 2)
+        printf("debug: kDupDoFile(%ls)\n", pFtsEnt->fts_wcsaccpath);
+
+    /*
+     * Check that it's within the size range.
+     */
+    cbFile = pFtsEnt->fts_stat.st_size;
+    if (   cbFile >= g_cbMinFileSize
+        && cbFile <= g_cbMaxFileSize)
+    {
+        /*
+         * Start out treating this like a unique file with a unique size, i.e.
+         * allocate all the structures we might possibly need.
+         */
+        size_t        cbAccessPath = (wcslen(pFtsEnt->fts_wcsaccpath) + 1) * sizeof(wchar_t);
+        PKDUPFILENODE pFileNode = (PKDUPFILENODE)kDupAlloc(sizeof(*pFileNode) + cbAccessPath);
+        PKDUPSIZENODE pSizeNode = (PKDUPSIZENODE)kDupAlloc(sizeof(*pSizeNode));
+        if (!pFileNode || !pSizeNode)
+            return 3;
+        g_cFiles++;
+
+        memset(&pFileNode->mKey, 0, sizeof(pFileNode->mKey));
+        pFileNode->pNextHardLink    = NULL;
+        pFileNode->pNextDup         = NULL;
+        pFileNode->pNextGlobalDup   = NULL;
+        pFileNode->uDev             = pFtsEnt->fts_stat.st_dev;
+        pFileNode->uInode           = pFtsEnt->fts_stat.st_ino;
+        memcpy(pFileNode->wszPath, pFtsEnt->fts_wcsaccpath, cbAccessPath);
+
+        pSizeNode->mKey = cbFile;
+        pSizeNode->cFiles = 1;
+        kDupFileTree_Init(&pSizeNode->FileRoot);
+        kDupFileTree_Insert(&pSizeNode->FileRoot, pFileNode);
+
+        /*
+         * Try insert it.
+         */
+        if (kDupSizeTree_Insert(&g_SizeRoot, pSizeNode))
+        { /* unique size, nothing more to do for now. */ }
+        else
+        {
+            /*
+             * More than one file with this size.  We may need to hash the
+             * hash the file we encountered with this size, if this is the
+             * second one.  In that case we should check for hardlinked or
+             * double entering of the file first as well.
+             */
+            kDupFree(pSizeNode);
+            pSizeNode = kDupSizeTree_Get(&g_SizeRoot, cbFile);
+            if (pSizeNode->cFiles == 1)
+            {
+                PKDUPFILENODE pFirstFileNode = pSizeNode->FileRoot.mpRoot;
+                if (   pFirstFileNode->uInode == pFileNode->uInode
+                    && pFileNode->uInode != 0
+                    && pFirstFileNode->uDev   == pFileNode->uDev)
+                {
+                    pFileNode->pNextHardLink      = pFirstFileNode->pNextHardLink;
+                    pFirstFileNode->pNextHardLink = pFileNode;
+                    if (g_cVerbosity >= 1)
+                        printf("Found hardlinked: '%ls' -> '%ls' (ino:%#" KX64_PRI " dev:%#" KX64_PRI ")\n",
+                               pFileNode->wszPath, pFirstFileNode->wszPath, pFileNode->uInode, pFileNode->uDev);
+                    g_cHardlinked += 1;
+                    return 0;
+                }
+
+                kDupHashFile(pFirstFileNode, NULL);
+            }
+            kDupHashFile(pFileNode, pFtsEnt);
+
+            if (kDupFileTree_Insert(&pSizeNode->FileRoot, pFileNode))
+            {  /* great, unique content */ }
+            else
+            {
+                /*
+                 * Duplicate content.  Could be hardlinked or a duplicate entry.
+                 */
+                PKDUPFILENODE pDupFileNode = kDupFileTree_Get(&pSizeNode->FileRoot, pFileNode->mKey);
+                if (   pDupFileNode->uInode == pFileNode->uInode
+                    && pFileNode->uInode != 0
+                    && pDupFileNode->uDev   == pFileNode->uDev)
+                {
+                    pFileNode->pNextHardLink = pDupFileNode->pNextHardLink;
+                    pDupFileNode->pNextHardLink = pFileNode;
+                    if (g_cVerbosity >= 1)
+                        printf("Found hardlinked: '%ls' -> '%ls' (ino:%#" KX64_PRI " dev:%#" KX64_PRI ")\n",
+                               pFileNode->wszPath, pDupFileNode->wszPath, pFileNode->uInode, pFileNode->uDev);
+                    g_cHardlinked += 1;
+                }
+                else
+                {
+                    KBOOL fDifferentDev;
+
+                    /* Genuinly duplicate (or inode numbers are busted). */
+                    if (!pDupFileNode->pNextDup)
+                    {
+                        *g_ppNextDuplicate = pDupFileNode;
+                        g_ppNextDuplicate = &pDupFileNode->pNextGlobalDup;
+                    }
+
+                    /* The list is sorted by device to better facility hardlinking later. */
+                    while (   (fDifferentDev = pDupFileNode->uDev != pFileNode->uDev)
+                           && pDupFileNode->pNextDup)
+                        pDupFileNode = pDupFileNode->pNextDup;
+
+                    pFileNode->pNextDup = pDupFileNode->pNextDup;
+                    pDupFileNode->pNextDup = pFileNode;
+
+                    g_cDuplicates += 1;
+                    if (!fDifferentDev)
+                    {
+                        g_cDuplicatesSaved += 1;
+                        g_cbDuplicatesSaved += pFtsEnt->fts_stat.st_blocks * BIRD_STAT_BLOCK_SIZE;
+                        if (g_cVerbosity >= 1)
+                            printf("Found duplicate: '%ls' <-> '%ls'\n", pFileNode->wszPath, pDupFileNode->wszPath);
+                    }
+                    else if (g_cVerbosity >= 1)
+                        printf("Found duplicate: '%ls' <-> '%ls' (devices differ).\n", pFileNode->wszPath, pDupFileNode->wszPath);
+                }
+            }
+        }
+    }
+    else if (g_cVerbosity >= 1)
+        printf("Skipping '%ls' because %" KU64_PRI " bytes is outside the size range.\n",
+               pFtsEnt->fts_wcsaccpath, cbFile);
+    return 0;
+}
+
+
+/**
+ * Process the non-option arguments, creating the file tree.
+ *
+ * @returns 0 on success, non-zero on failure.
+ * @param   papwszFtsArgs   The input in argv style.
+ * @param   fFtsOptions     The FTS options.
+ */
+static int kDupReadAll(wchar_t **papwszFtsArgs, unsigned fFtsOptions)
+{
+    int  rcExit = 0;
+    FTS *pFts = nt_fts_openw(papwszFtsArgs, fFtsOptions, NULL /*pfnCompare*/);
+    if (pFts != NULL)
+    {
+        for (;;)
+        {
+            FTSENT *pFtsEnt = nt_fts_read(pFts);
+            if (pFtsEnt)
+            {
+                switch (pFtsEnt->fts_info)
+                {
+                    case FTS_F:
+                        rcExit = kDupDoFile(pFtsEnt);
+                        if (rcExit == 0)
+                            continue;
+                        break;
+
+                    case FTS_D:
+                        if (   g_fRecursive
+                            || pFtsEnt->fts_level == FTS_ROOTLEVEL) /* enumerate dirs on the command line */
+                            continue;
+                        rcExit = nt_fts_set(pFts, pFtsEnt, FTS_SKIP);
+                        if (rcExit == 0)
+                            continue;
+                        fprintf(stderr, "kDuplicate: internal error: nt_fts_set failed!\n");
+                        rcExit = 1;
+                        break;
+
+                    case FTS_DP:
+                        /* nothing to do here. */
+                        break;
+
+                    case FTS_SL:
+                        /* The nice thing on windows is that we already know whether it's a
+                           directory or file when encountering the symbolic link. */
+                        if (   (pFtsEnt->fts_stat.st_isdirsymlink ? g_fRecursiveViaSymlinks : g_fFollowSymlinkedFiles)
+                            &&  pFtsEnt->fts_number == 0)
+                        {
+                            pFtsEnt->fts_number++;
+                            rcExit = nt_fts_set(pFts, pFtsEnt, FTS_FOLLOW);
+                            if (rcExit == 0)
+                                continue;
+                            fprintf(stderr, "kDuplicate: internal error: nt_fts_set failed!\n");
+                            rcExit = 1;
+                        }
+                        break;
+
+                    case FTS_DC:
+                        fprintf(stderr, "kDuplicate: warning: Ignoring cycle '%ls'!\n", pFtsEnt->fts_wcsaccpath);
+                        continue;
+
+                    case FTS_NS:
+                        fprintf(stderr, "kDuplicate: warning: Failed to stat '%ls': %s (%d)\n",
+                                pFtsEnt->fts_wcsaccpath, strerror(pFtsEnt->fts_errno), pFtsEnt->fts_errno);
+                        continue;
+
+                    case FTS_DNR:
+                        fprintf(stderr, "kDuplicate: error: Error reading directory '%ls': %s (%d)\n",
+                                pFtsEnt->fts_wcsaccpath, strerror(pFtsEnt->fts_errno), pFtsEnt->fts_errno);
+                        rcExit = 1;
+                        break;
+
+                    case FTS_ERR:
+                        fprintf(stderr, "kDuplicate: error: Error on '%ls': %s (%d)\n",
+                                pFtsEnt->fts_wcsaccpath, strerror(pFtsEnt->fts_errno), pFtsEnt->fts_errno);
+                        rcExit = 1;
+                        break;
+
+
+                    /* ignore */
+                    case FTS_SLNONE:
+                    case FTS_DEFAULT:
+                        break;
+
+                    /* Not supposed to get here. */
+                    default:
+                        fprintf(stderr, "kDuplicate: internal error: fts_info=%d - '%ls'\n",
+                                pFtsEnt->fts_info, pFtsEnt->fts_wcsaccpath);
+                        rcExit = 1;
+                        break;
+                }
+            }
+            else if (errno == 0)
+                break;
+            else
+            {
+                fprintf(stderr, "kDuplicate: error: nt_fts_read failed: %s (%d)\n", strerror(errno), errno);
+                rcExit = 1;
+                break;
+            }
+        }
+
+        if (nt_fts_close(pFts) != 0)
+        {
+            fprintf(stderr, "kDuplicate: error: nt_fts_close failed: %s (%d)\n", strerror(errno), errno);
+            rcExit = 1;
+        }
+    }
+    else
+    {
+        fprintf(stderr, "kDuplicate: error: nt_fts_openw failed: %s (%d)\n", strerror(errno), errno);
+        rcExit = 1;
+    }
+
+    return rcExit;
+}
+
+
+/**
+ * Hardlink duplicates.
+ */
+static int kDupHardlinkDuplicates(void)
+{
+    int           rcExit = 0;
+    PKDUPFILENODE pFileNode;
+    for (pFileNode = g_pDuplicateHead; pFileNode != NULL; pFileNode = pFileNode->pNextGlobalDup)
+    {
+        PKDUPFILENODE pTargetFile = pFileNode;
+        PKDUPFILENODE pDupFile;
+        for (pDupFile = pFileNode->pNextDup; pDupFile != NULL; pDupFile = pDupFile->pNextDup)
+        {
+            /*
+             * Can only hard link if the files are on the same device.
+             */
+            if (pDupFile->uDev == pTargetFile->uDev)
+            {
+                /** @todo compare the files?   */
+                if (1)
+                {
+                    /*
+                     * Start by renaming the orinal file before we try create the hard link.
+                     */
+                    static const wchar_t s_wszBackupSuffix[] = L".kDepBackup";
+                    wchar_t wszBackup[0x4000];
+                    size_t  cwcPath = wcslen(pDupFile->wszPath);
+                    if (cwcPath + sizeof(s_wszBackupSuffix) / sizeof(wchar_t) < K_ELEMENTS(wszBackup))
+                    {
+                        memcpy(wszBackup, pDupFile->wszPath, cwcPath * sizeof(wchar_t));
+                        memcpy(&wszBackup[cwcPath], s_wszBackupSuffix, sizeof(s_wszBackupSuffix));
+                        if (MoveFileW(pDupFile->wszPath, wszBackup))
+                        {
+                            if (CreateHardLinkW(pDupFile->wszPath, pTargetFile->wszPath, NULL))
+                            {
+                                if (birdUnlinkForcedW(wszBackup) == 0)
+                                {
+                                    if (g_cVerbosity >= 1)
+                                        printf("Hardlined '%ls' to '%ls'.\n", pDupFile->wszPath, pTargetFile->wszPath);
+                                }
+                                else
+                                {
+                                    fprintf(stderr, "kDuplicate: fatal: failed to delete '%ls' after hardlinking: %s (%d)\n",
+                                            wszBackup, strerror(errno), errno);
+                                    return 8;
+                                }
+                            }
+                            else
+                            {
+                                fprintf(stderr, "kDuplicate: error: failed to hard link '%ls' to '%ls': %u\n",
+                                        pDupFile->wszPath, wszBackup, GetLastError());
+                                if (!MoveFileW(wszBackup, pDupFile->wszPath))
+                                {
+                                    fprintf(stderr, "kDuplicate: fatal: Restore back '%ls' to '%ls' after hardlinking faild: %u\n",
+                                            wszBackup, pDupFile->wszPath, GetLastError());
+                                    return 8;
+                                }
+                                rcExit = 1;
+                            }
+                        }
+                        else
+                        {
+                            fprintf(stderr, "kDuplicate: error: failed to rename '%ls' to '%ls': %u\n",
+                                    pDupFile->wszPath, wszBackup, GetLastError());
+                            rcExit = 1;
+                        }
+                    }
+                    else
+                    {
+                        fprintf(stderr, "kDuplicate: error: too long backup path: '%ls'\n", pDupFile->wszPath);
+                        rcExit = 1;
+                    }
+                }
+            }
+            /*
+             * Since the list is sorted by uDev, we now change the target file.
+             */
+            else
+                pTargetFile = pDupFile;
+        }
+    }
+    return rcExit;
+}
+
+
+static int usage(const char *pszName, FILE *pOut)
+{
+    fprintf(pOut,
+            "usage: %s [options] <path1> [path2 [..]]\n"
+            "usage: %s <-V|--version>\n"
+            "usage: %s <-h|--help>\n"
+            , pszName, pszName, pszName);
+    fprintf(pOut,
+            "\n"
+            "Options:\n"
+            "  -H, --dereference-command-line, --no-dereference-command-line\n"
+            "    Follow symbolic links on the command line.\n"
+            "  -L, --dereference\n"
+            "    Follow symbolic links while scanning directories.\n"
+            "  -P, --no-dereference\n"
+            "    Do not follow symbolic links while scanning directories.\n"
+            "  -r, --recursive\n"
+            "    Recurse into subdirectories, but do not follow links to them.\n"
+            "  -R, --recursive-dereference\n"
+            "    Same as -r, but also follow into symlinked subdirectories.\n"
+            "  -x, --one-file-system\n"
+            "    Do not consider other file system (volumes), either down thru a\n"
+            "    mount point or via a symbolic link to a directory.\n"
+            "  --no-one-file-system, --cross-file-systems\n"
+            "    Reverses the effect of --one-file-system.\n"
+            );
+    return 0;
+}
+
+
+int wmain(int argc, wchar_t **argv)
+{
+    int             rcExit;
+
+    /*
+     * Process parameters.  Position.
+     */
+    wchar_t   **papwszFtsArgs = (wchar_t **)calloc(argc + 1, sizeof(wchar_t *));
+    unsigned    cFtsArgs      = 0;
+    unsigned    fFtsOptions   = FTS_NOCHDIR | FTS_NO_ANSI;
+    KBOOL       fEndOfOptions = K_FALSE;
+    KBOOL       fHardlinkDups = K_FALSE;
+    int         i;
+    for (i = 1; i < argc; i++)
+    {
+        wchar_t *pwszArg = argv[i];
+        if (   *pwszArg == '-'
+            && !fEndOfOptions)
+        {
+            wchar_t wcOpt = *++pwszArg;
+            pwszArg++;
+            if (wcOpt == '-')
+            {
+                /* Translate long options. */
+                if (wcscmp(pwszArg, L"help") == 0)
+                    wcOpt = 'h';
+                else if (wcscmp(pwszArg, L"version") == 0)
+                    wcOpt = 'V';
+                else if (wcscmp(pwszArg, L"recursive") == 0)
+                    wcOpt = 'r';
+                else if (wcscmp(pwszArg, L"dereference-recursive") == 0)
+                    wcOpt = 'R';
+                else if (wcscmp(pwszArg, L"dereference") == 0)
+                    wcOpt = 'L';
+                else if (wcscmp(pwszArg, L"dereference-command-line") == 0)
+                    wcOpt = 'H';
+                else if (wcscmp(pwszArg, L"one-file-system") == 0)
+                    wcOpt = 'x';
+                /* Process long options. */
+                else if (*pwszArg == '\0')
+                {
+                    fEndOfOptions = K_TRUE;
+                    continue;
+                }
+                else if (wcscmp(pwszArg, L"no-recursive") == 0)
+                {
+                    g_fRecursive = g_fRecursiveViaSymlinks = K_FALSE;
+                    continue;
+                }
+                else if (wcscmp(pwszArg, L"no-dereference-command-line") == 0)
+                {
+                    fFtsOptions &= ~FTS_COMFOLLOW;
+                    continue;
+                }
+                else if (   wcscmp(pwszArg, L"no-one-file-system") == 0
+                         || wcscmp(pwszArg, L"cross-file-systems") == 0)
+                {
+                    fFtsOptions &= ~FTS_XDEV;
+                    continue;
+                }
+                else if (wcscmp(pwszArg, L"hardlink-duplicates") == 0)
+                {
+                    fHardlinkDups = K_TRUE;
+                    continue;
+                }
+                else
+                {
+                    fprintf(stderr, "kDuplicate: syntax error: Unknown option '--%ls'\n", pwszArg);
+                    return 2;
+                }
+            }
+
+            /* Process one or more short options. */
+            do
+            {
+                switch (wcOpt)
+                {
+                    case 'r': /* --recursive */
+                        g_fRecursive = K_TRUE;
+                        break;
+
+                    case 'R': /* --dereference-recursive */
+                        g_fRecursive = g_fRecursiveViaSymlinks = K_TRUE;
+                        break;
+
+                    case 'H': /* --dereference-command-line */
+                        fFtsOptions |= FTS_COMFOLLOW;
+                        break;
+
+                    case 'L': /* --dereference*/
+                        g_fFollowSymlinkedFiles = K_TRUE;
+                        break;
+
+                    case 'x': /* --one-file-system*/
+                        fFtsOptions |= FTS_XDEV;
+                        break;
+
+                    case 'q':
+                        g_cVerbosity = 0;
+                        break;
+
+                    case 'v':
+                        g_cVerbosity++;
+                        break;
+
+
+                    case 'h':
+                    case '?':
+                        return usage("kDuplicate", stdout);
+
+                    case 'V':
+                        printf("0.0.1\n");
+                        return 0;
+
+                    default:
+                        fprintf(stderr, "kDuplicate: syntax error: Unknown option '-%lc'\n", wcOpt);
+                        return 2;
+                }
+
+                wcOpt = *pwszArg++;
+            } while (wcOpt != '\0');
+        }
+        else
+        {
+            /*
+             * Append non-option arguments to the FTS argument vector.
+             */
+            papwszFtsArgs[cFtsArgs] = pwszArg;
+            cFtsArgs++;
+        }
+    }
+
+    /*
+     * Do the FTS processing.
+     */
+    kDupSizeTree_Init(&g_SizeRoot);
+    rcExit = kDupReadAll(papwszFtsArgs, fFtsOptions);
+    if (rcExit == 0)
+    {
+        /*
+         * Display the result.
+         */
+        printf("Found %" KU64_PRI " duplicate files, out which %" KU64_PRI " can be hardlinked saving %" KU64_PRI " bytes\n",
+               g_cDuplicates, g_cDuplicatesSaved, g_cbDuplicatesSaved);
+
+        if (fHardlinkDups)
+            rcExit = kDupHardlinkDuplicates();
+    }
+
+    return rcExit;
+}
+
