Parcourir la source

Merge pull request #2369 from nextcloud/enh/windows-msi

Windows: MSI support & Win32 migration tools
Michael Schuster il y a 5 ans
Parent
commit
b72bfb5c65
42 fichiers modifiés avec 5614 ajouts et 15 suppressions
  1. 19 11
      NEXTCLOUD.cmake
  2. 7 2
      admin/CMakeLists.txt
  3. 8 0
      admin/win/CMakeLists.txt
  4. 25 0
      admin/win/msi/CMakeLists.txt
  5. 207 0
      admin/win/msi/Nextcloud.wxs
  6. 54 0
      admin/win/msi/OEM.wxi.in
  7. 38 0
      admin/win/msi/Platform.wxi
  8. 44 0
      admin/win/msi/collect-transform.xsl.in
  9. BIN
      admin/win/msi/gui/banner.bmp
  10. 67 0
      admin/win/msi/gui/banner.svg
  11. BIN
      admin/win/msi/gui/dialog.bmp
  12. 26 0
      admin/win/msi/make-msi.bat.in
  13. 63 0
      admin/win/tools/CMakeLists.txt
  14. 41 0
      admin/win/tools/NCMsiHelper/CMakeLists.txt
  15. 118 0
      admin/win/tools/NCMsiHelper/CustomAction.cpp
  16. 3 0
      admin/win/tools/NCMsiHelper/CustomAction.def
  17. 134 0
      admin/win/tools/NCMsiHelper/LogResult.cpp
  18. 48 0
      admin/win/tools/NCMsiHelper/LogResult.h
  19. 84 0
      admin/win/tools/NCMsiHelper/NCMsiHelper.cpp
  20. 99 0
      admin/win/tools/NCMsiHelper/NCMsiHelper.h
  21. 43 0
      admin/win/tools/NCMsiHelper/NCMsiHelper.wxs
  22. 14 0
      admin/win/tools/NCNavRemove/CMakeLists.txt
  23. 77 0
      admin/win/tools/NCNavRemove/ConfigIni.cpp
  24. 30 0
      admin/win/tools/NCNavRemove/ConfigIni.h
  25. 2 0
      admin/win/tools/NCNavRemove/NavRemove.ini.in
  26. 9 0
      admin/win/tools/NCNavRemove/NavRemoveConstants.h.in
  27. 25 0
      admin/win/tools/NCNavRemove/dll/CMakeLists.txt
  28. 40 0
      admin/win/tools/NCNavRemove/dll/NavRemove.cpp
  29. 32 0
      admin/win/tools/NCNavRemove/dll/NavRemove.h
  30. 42 0
      admin/win/tools/NCNavRemove/dll/dllmain.cpp
  31. 2 0
      admin/win/tools/NCNavRemove/dll/exports.def
  32. 19 0
      admin/win/tools/NCNavRemove/exe/CMakeLists.txt
  33. 53 0
      admin/win/tools/NCNavRemove/exe/main.cpp
  34. 40 0
      admin/win/tools/NCNavRemove/version.rc.in
  35. 3474 0
      admin/win/tools/NCToolsShared/3rdparty/SimpleIni.h
  36. 4 0
      admin/win/tools/NCToolsShared/CMakeLists.txt
  37. 48 0
      admin/win/tools/NCToolsShared/SimpleNamedMutex.cpp
  38. 31 0
      admin/win/tools/NCToolsShared/SimpleNamedMutex.h
  39. 58 0
      admin/win/tools/NCToolsShared/utility.h
  40. 477 0
      admin/win/tools/NCToolsShared/utility_win.cpp
  41. 8 1
      shell_integration/windows/CMakeLists.txt
  42. 1 1
      shell_integration/windows/WinShellExt.wxs.in

+ 19 - 11
NEXTCLOUD.cmake

@@ -37,15 +37,23 @@ option( APPLICATION_WIZARD_USE_CUSTOM_LOGO "Use the logo from ':/client/theme/co
 
 
 #
-## Windows Shell Extensions - IMPORTANT: Generate new GUIDs for custom builds with "guidgen" or "uuidgen"
+## Windows Shell Extensions & MSI - IMPORTANT: Generate new GUIDs for custom builds with "guidgen" or "uuidgen"
 #
-
-# Context Menu
-set( WIN_SHELLEXT_CONTEXT_MENU_GUID      "{BC6988AB-ACE2-4B81-84DC-DC34F9B24401}" )
-
-# Overlays
-set( WIN_SHELLEXT_OVERLAY_GUID_ERROR     "{E0342B74-7593-4C70-9D61-22F294AAFE05}" )
-set( WIN_SHELLEXT_OVERLAY_GUID_OK        "{E1094E94-BE93-4EA2-9639-8475C68F3886}" )
-set( WIN_SHELLEXT_OVERLAY_GUID_OK_SHARED "{E243AD85-F71B-496B-B17E-B8091CBE93D2}" )
-set( WIN_SHELLEXT_OVERLAY_GUID_SYNC      "{E3D6DB20-1D83-4829-B5C9-941B31C0C35A}" )
-set( WIN_SHELLEXT_OVERLAY_GUID_WARNING   "{E4977F33-F93A-4A0A-9D3C-83DEA0EE8483}" )
+if(WIN32)
+    # Context Menu
+    set( WIN_SHELLEXT_CONTEXT_MENU_GUID      "{BC6988AB-ACE2-4B81-84DC-DC34F9B24401}" )
+
+    # Overlays
+    set( WIN_SHELLEXT_OVERLAY_GUID_ERROR     "{E0342B74-7593-4C70-9D61-22F294AAFE05}" )
+    set( WIN_SHELLEXT_OVERLAY_GUID_OK        "{E1094E94-BE93-4EA2-9639-8475C68F3886}" )
+    set( WIN_SHELLEXT_OVERLAY_GUID_OK_SHARED "{E243AD85-F71B-496B-B17E-B8091CBE93D2}" )
+    set( WIN_SHELLEXT_OVERLAY_GUID_SYNC      "{E3D6DB20-1D83-4829-B5C9-941B31C0C35A}" )
+    set( WIN_SHELLEXT_OVERLAY_GUID_WARNING   "{E4977F33-F93A-4A0A-9D3C-83DEA0EE8483}" )
+
+    # MSI Upgrade Code (without brackets)
+    set( WIN_MSI_UPGRADE_CODE                "FD2FCCA9-BB8F-4485-8F70-A0621B84A7F4" )
+
+    # Windows build options
+    option( BUILD_WIN_MSI "Build MSI scripts and helper DLL" OFF )
+    option( BUILD_WIN_TOOLS "Build Win32 migration tools" OFF )
+endif()

+ 7 - 2
admin/CMakeLists.txt

@@ -1,2 +1,7 @@
-# traverse into osx subdirectory to install and patch the create-pack script
-add_subdirectory(osx)
+if(APPLE)
+    # traverse into osx subdirectory to install and patch the create-pack script
+    add_subdirectory(osx)
+elseif(WIN32)
+    # MSI package scripts, helper DLL and migration tools
+    add_subdirectory(win)
+endif()

+ 8 - 0
admin/win/CMakeLists.txt

@@ -0,0 +1,8 @@
+# MSI package scripts, helper DLL and migration tools
+if(BUILD_WIN_MSI)
+    add_subdirectory(msi)
+endif()
+
+if(BUILD_WIN_MSI OR BUILD_WIN_TOOLS)
+    add_subdirectory(tools)
+endif()

+ 25 - 0
admin/win/msi/CMakeLists.txt

@@ -0,0 +1,25 @@
+if(CMAKE_SIZEOF_VOID_P MATCHES 4)
+    set(MSI_BUILD_ARCH x86)
+else()
+    set(MSI_BUILD_ARCH x64)
+endif()
+
+string(SUBSTRING ${GIT_SHA1} 0 7 GIT_REVISION)
+
+set(VERSION "${MIRALL_VERSION_MAJOR}.${MIRALL_VERSION_MINOR}.${MIRALL_VERSION_PATCH}.${MIRALL_VERSION_BUILD}")
+
+set(MSI_INSTALLER_FILENAME "${APPLICATION_SHORTNAME}-${VERSION}-${MSI_BUILD_ARCH}.msi")
+
+configure_file(OEM.wxi.in ${CMAKE_CURRENT_BINARY_DIR}/OEM.wxi)
+configure_file(collect-transform.xsl.in ${CMAKE_CURRENT_BINARY_DIR}/collect-transform.xsl)
+configure_file(make-msi.bat.in ${CMAKE_CURRENT_BINARY_DIR}/make-msi.bat)
+
+install(FILES
+        ${CMAKE_CURRENT_BINARY_DIR}/OEM.wxi
+        ${CMAKE_CURRENT_BINARY_DIR}/collect-transform.xsl
+        ${CMAKE_CURRENT_BINARY_DIR}/make-msi.bat
+        Platform.wxi
+        Nextcloud.wxs
+        gui/banner.bmp
+        gui/dialog.bmp
+    DESTINATION msi/)

+ 207 - 0
admin/win/msi/Nextcloud.wxs

@@ -0,0 +1,207 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ *
+ * Copyright (C) by Michael Schuster <michael.schuster@nextcloud.com>
+ *
+ * This program 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.
+ *
+ * This program 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.
+ *
+-->
+<?include $(sys.CURRENTDIR)OEM.wxi?>
+<?include $(sys.CURRENTDIR)Platform.wxi?>
+
+<Wix xmlns="http://schemas.microsoft.com/wix/2006/wi" xmlns:util="http://schemas.microsoft.com/wix/UtilExtension">
+
+    <!--
+         When to change the Product GUID:
+         https://www.firegiant.com/wix/tutorial/upgrades-and-modularization/
+         https://www.firegiant.com/wix/tutorial/upgrades-and-modularization/checking-for-oldies/
+
+         We change the Product Id for every release, to let up-/downgrading always work.
+         But we then should never change the UpgradeCode.
+     -->
+    <Product Name="$(var.AppName)" Manufacturer="$(var.AppVendor)"
+        Id="*" 
+        UpgradeCode="$(var.UpgradeCode)"
+        Language="1033" Codepage="$(var.codepage)" Version="$(var.VerFull)">
+    <Package Id="*" Keywords="Installer" Description="$(var.AppName) $(var.VerDesc)" Manufacturer="$(var.AppVendor)"
+        InstallerVersion="300" Platform="$(var.Platform)" Languages="1033" Compressed="yes" SummaryCodepage="$(var.codepage)" InstallScope="perMachine" />
+
+    <!--
+        Upgrading: Since we always want to allow up-/downgrade, we don't specify a maximum version, thus
+                   leading the WiX linker (light.exe) to trigger the following warning:
+                        warning LGHT1076 : ICE61: This product should remove only older versions of itself. No Maximum version was detected for the current product. (WIX_UPGRADE_DETECTED)
+                   We suppress the warning: light.exe -sw1076
+        
+        If at some point we want to change this behaviour, read the docs:
+        https://www.firegiant.com/wix/tutorial/upgrades-and-modularization/replacing-ourselves/
+        https://www.joyofsetup.com/2010/01/16/major-upgrades-now-easier-than-ever/
+    -->
+    <MajorUpgrade Schedule="afterInstallInitialize" AllowDowngrades="yes" />
+
+    <Media Id="1" Cabinet="$(var.AppShortName).cab" EmbedCab="yes" />
+
+    <!-- If already installed: Use previously chosen path (use 32-bit registry like NSIS does) -->
+    <Property Id="INSTALLDIR">
+        <RegistrySearch Id="RegistryInstallDir" Type="raw" Root="HKLM" Key="Software\$(var.AppVendor)\$(var.AppName)" Win64="no" />
+    </Property>
+
+    <!-- Detect legacy NSIS installation -->
+    <Property Id="NSIS_UNINSTALLEXE">
+        <DirectorySearch Id="LegacyUninstallVersion" Path="[INSTALLDIR]">
+            <FileSearch Name="Uninstall.exe" />
+        </DirectorySearch>
+    </Property>
+
+    <!-- Quit / restart application -->
+    <util:RestartResource ProcessName="$(var.AppExe)" />
+
+    <!-- Helper DLL Custom Actions -->
+    <SetProperty Id="ExecNsisUninstaller" Value="&quot;$(var.AppShortName)&quot; &quot;[NSIS_UNINSTALLEXE]&quot;" Before="ExecNsisUninstaller" Sequence="execute" />
+    <SetProperty Id="RemoveNavigationPaneEntries" Value="&quot;$(var.AppName)&quot;" Before="RemoveNavigationPaneEntries" Sequence="execute" />
+
+    <InstallExecuteSequence>
+        <!-- Install: Remove previous NSIS installation, if detected -->
+        <Custom Action="ExecNsisUninstaller" Before="ProcessComponents">NSIS_UNINSTALLEXE AND NOT Installed</Custom>
+
+        <!-- Uninstall: Remove sync folders from Explorer's Navigation Pane, only effective for the current user (home users) -->
+        <Custom Action="RemoveNavigationPaneEntries" After="RemoveFiles">(NOT UPGRADINGPRODUCTCODE) AND (REMOVE="ALL")</Custom>
+
+        <!-- Schedule Reboot for the Shell Extensions -->
+        <ScheduleReboot After="InstallFinalize">NOT (DO_NOT_SCHEDULE_REBOOT=1)</ScheduleReboot>
+    </InstallExecuteSequence>
+
+    <!-- "Add or Remove" Programs Entries -->
+    <Property Id="ARPPRODUCTICON">$(var.AppIcon)</Property>
+    <Property Id="ARPHELPLINK">$(var.AppHelpLink)</Property>
+    <Property Id="ARPURLINFOABOUT">$(var.AppInfoLink)</Property>
+
+    <!-- https://www.firegiant.com/wix/tutorial/com-expression-syntax-miscellanea/add-or-remove-programs-entries/ -->
+    <!--
+    <Property Id="ARPNOMODIFY">1</Property>
+    <Property Id="ARPNOREPAIR">1</Property>
+    -->
+
+    <!-- App icon -->
+    <Icon Id="$(var.AppIcon)" SourceFile="$(var.HarvestAppDir)\$(var.AppIcon)" />
+
+    <!-- Custom bitmaps -->
+    <WixVariable Id="WixUIBannerBmp" Value="$(var.UIBannerBmp)" />
+    <WixVariable Id="WixUIDialogBmp" Value="$(var.UIDialogBmp)" />
+
+    <!-- Custom icons -->
+    <!-- https://wixtoolset.org/documentation/manual/v3/wixui/wixui_customizations.html -->
+    <!--
+    <WixVariable Id="WixUIExclamationIco" Value="ui\Exclam.ico" />
+    <WixVariable Id="WixUIInfoIco" Value="ui\Info.ico" />
+    <WixVariable Id="WixUINewIco" Value="ui\New.ico" />
+    <WixVariable Id="WixUIUpIco" Value="ui\Up.ico" />
+    -->
+
+    <!-- Custom license -->
+    <!--
+    <WixVariable Id="WixUILicenseRtf" Value="$(var.AppLicenseRtf)" />
+    -->
+
+    <UI>
+        <UIRef Id="WixUI_FeatureTree" />
+        <UIRef Id="WixUI_ErrorProgressText" />
+
+        <!-- Skip the license page -->
+        <Publish Dialog="WelcomeDlg" Control="Next" Event="NewDialog" Value="CustomizeDlg" Order="3">1</Publish>
+        <!-- Skip the page on the way back too -->
+        <Publish Dialog="CustomizeDlg" Control="Back" Event="NewDialog" Value="WelcomeDlg" Order="3">1</Publish>
+
+        <!-- https://wixtoolset.org/documentation/manual/v3/howtos/ui_and_localization/run_program_after_install.html -->        
+        <Publish Dialog="ExitDialog" 
+            Control="Finish" 
+            Event="DoAction" 
+            Value="LaunchApplication">WIXUI_EXITDIALOGOPTIONALCHECKBOX = 1 and NOT Installed</Publish>
+
+        <ProgressText Action="ExecNsisUninstaller">Removing previous installation</ProgressText>
+        <ProgressText Action="KillProcess">Trying to terminate application process of previous installation</ProgressText>
+        <ProgressText Action="RemoveNavigationPaneEntries">Removing sync folders from Explorer's Navigation Pane</ProgressText>
+    </UI>
+
+    <!-- "Launch" checkbox -->
+    <Property Id="WixShellExecTarget" Value="[#MainExecutable]" />
+    <CustomAction Id="LaunchApplication" BinaryKey="WixCA" DllEntry="WixShellExec" Impersonate="yes" />
+    <Property Id="WIXUI_EXITDIALOGOPTIONALCHECKBOXTEXT" Value="Launch $(var.AppName)" />
+    <SetProperty Id="WIXUI_EXITDIALOGOPTIONALCHECKBOX" Value="1" Before="CostInitialize">NOT (LAUNCH=0)</SetProperty>
+
+    <!-- Components -->
+    <Directory Id="TARGETDIR" Name="SourceDir">
+        <Directory Id="$(var.PlatformProgramFilesFolder)" Name="PFiles">
+            <Directory Id="INSTALLDIR" Name="$(var.AppName)">
+                <!-- Shell Extensions -->
+                <Directory Id="ShellExtDir" Name="shellext" />
+            </Directory>
+        </Directory>
+
+        <Directory Id="ProgramMenuFolder" Name="Programs">
+            <!-- Start Menu Shortcut -->
+            <Component Id="StartMenuIcon" Guid="*" Win64="$(var.PlatformWin64)">
+                <Shortcut Id="StartMenu" Name="$(var.AppName)" Target="[INSTALLDIR]$(var.AppExe)" WorkingDirectory="INSTALLDIR" Icon="$(var.AppIcon)" IconIndex="0" Advertise="no" />
+                <RegistryValue Root="HKCU" Key="Software\$(var.AppVendor)\$(var.AppName)" Name="installedStartMenuShortcut" Type="integer" Value="1" KeyPath="yes"/>
+            </Component>
+        </Directory>
+
+        <Directory Id="DesktopFolder" Name="Desktop">
+            <!-- Desktop Shortcut -->
+            <Component Id="DesktopIcon" Guid="*" Win64="$(var.PlatformWin64)">
+                <Shortcut Id="Desktop" Name="$(var.AppName)" Target="[INSTALLDIR]$(var.AppExe)" WorkingDirectory="INSTALLDIR" Icon="$(var.AppIcon)" IconIndex="0" Advertise="no" />
+                <RegistryValue Root="HKCU" Key="Software\$(var.AppVendor)\$(var.AppName)" Name="installedDesktopShortcut" Type="integer" Value="1" KeyPath="yes"/>
+            </Component>
+        </Directory>
+    </Directory>
+
+    <DirectoryRef Id="TARGETDIR">
+        <Component Id="RegistryEntries" Guid="*" Win64="no">
+            <!-- Version numbers used to detect existing installation (use 32-bit registry like NSIS does) -->
+            <RegistryKey Root="HKLM" Key="Software\$(var.AppVendor)\$(var.AppName)" ForceCreateOnInstall="yes" ForceDeleteOnUninstall="yes">
+                <RegistryValue Type="string" Value="[INSTALLDIR]" />
+                <RegistryValue Type="integer" Name="VersionMajor" Value="$(var.VerMajor)" />
+                <RegistryValue Type="integer" Name="VersionMinor" Value="$(var.VerMinor)" />
+                <RegistryValue Type="integer" Name="VersionRevision" Value="$(var.VerRevision)" />
+                <RegistryValue Type="integer" Name="VersionBuild" Value="$(var.VerBuild)" />
+
+                <!-- Save MSI ProductCode to allow being uninstalled by custom tools -->
+                <RegistryValue Type="string" Name="InstallerProductCode" Value="[ProductCode]" />
+            </RegistryKey>
+        </Component>
+    </DirectoryRef>
+
+    <!-- Features -->
+    <Feature Id="Client" Title="$(var.AppName) $(var.PlatformBitness)" Display="collapse" Absent="disallow" ConfigurableDirectory="INSTALLDIR"
+        Description="$(var.AppName) $(var.VerDesc)">
+        <ComponentGroupRef Id="ClientFiles" />
+
+        <ComponentRef Id="RegistryEntries" />
+
+        <Feature Id="ShellExtensions" Title="Integration for Windows Explorer"
+            Description="This feature requires a reboot." >
+            <ComponentGroupRef Id="ShellExtensions" />
+
+            <Condition Level="0">(NO_SHELL_EXTENSIONS=1)</Condition>
+        </Feature>
+
+        <Feature Id="StartMenuShortcut" Title="Start Menu Shortcut">
+            <ComponentRef Id="StartMenuIcon" />
+            <Condition Level="0">(NO_START_MENU_SHORTCUTS=1)</Condition>
+        </Feature>
+
+        <Feature Id="DesktopShortcut" Title="Desktop Shortcut">
+            <ComponentRef Id="DesktopIcon" />
+            <Condition Level="0">(NO_DESKTOP_SHORTCUT=1)</Condition>
+        </Feature>
+    </Feature>
+
+    </Product>
+</Wix>

+ 54 - 0
admin/win/msi/OEM.wxi.in

@@ -0,0 +1,54 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ *
+ * Copyright (C) by Michael Schuster <michael.schuster@nextcloud.com>
+ *
+ * This program 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.
+ *
+ * This program 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.
+ *
+-->
+<Include>
+
+    <!-- Changing the Vendor breaks registry (also NSIS) product detection -->
+    <?define AppVendor = "@APPLICATION_VENDOR@" ?>
+
+    <!-- App Defines -->
+    <?define AppName = "@APPLICATION_NAME@" ?>
+    <?define AppShortName = "@APPLICATION_EXECUTABLE@" ?>
+
+    <?define AppIcon = "@APPLICATION_ICON_NAME@.ico" ?>
+    <?define AppExe = "@APPLICATION_EXECUTABLE@.exe" ?>
+
+    <?define AppHelpLink = "https://@APPLICATION_DOMAIN@/" ?>
+    <?define AppInfoLink = "$(var.AppHelpLink)" ?>
+
+    <!-- Custom license: To use it, also remove the "Skip the license page" stuff in the <UI> section
+                         and uncomment <WixVariable Id="WixUILicenseRtf"...
+    <?define AppLicenseRtf = "path\License.rtf" ?>
+    -->
+
+    <!-- App Version -->
+    <?define VerMajor = "@MIRALL_VERSION_MAJOR@" ?>
+    <?define VerMinor = "@MIRALL_VERSION_MINOR@" ?>
+    <?define VerRevision = "@MIRALL_VERSION_PATCH@" ?>
+    <?define VerBuild = "@MIRALL_VERSION_BUILD@" ?>
+    <?define VerStd = "$(var.VerMajor).$(var.VerMinor).$(var.VerRevision)" ?>
+    <?define VerFull = "$(var.VerStd).$(var.VerBuild)" ?>
+
+    <?define VerDesc = "@MIRALL_VERSION_STRING@ (Git revision @GIT_REVISION@)" ?>
+
+    <!-- MSI upgrade support -->
+    <?define UpgradeCode = "@WIN_MSI_UPGRADE_CODE@" ?>
+
+    <!-- UI resources -->
+    <?define UIBannerBmp = "banner.bmp" ?>
+    <?define UIDialogBmp = "dialog.bmp" ?>
+
+</Include>

+ 38 - 0
admin/win/msi/Platform.wxi

@@ -0,0 +1,38 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ *
+ * Copyright (C) by Michael Schuster <michael.schuster@nextcloud.com>
+ *
+ * This program 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.
+ *
+ * This program 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.
+ *
+-->
+<Include>
+
+    <!--
+        MSI packages are built either for x86 or x64, we use defines to maintain a single WiX script.
+
+        Some hints:
+        https://www.joyofsetup.com/2010/05/14/working-hard-or-hardly-working/
+        https://stackoverflow.com/questions/18628790/build-wix-3-6-project-targeting-x64
+        https://www.howtobuildsoftware.com/index.php/how-do/1oQ/wix-detect-if-32-or-64-bit-windows-and-define-var
+    -->
+
+    <?if $(var.Platform) = x64 ?>
+    <?define PlatformBitness = "(64-bit)" ?>
+    <?define PlatformWin64 = "yes" ?>
+    <?define PlatformProgramFilesFolder = "ProgramFiles64Folder" ?>
+    <?else ?>
+    <?define PlatformBitness = "(32-bit)" ?>
+    <?define PlatformWin64 = "no" ?>
+    <?define PlatformProgramFilesFolder = "ProgramFilesFolder" ?>
+    <?endif ?>
+
+</Include>

+ 44 - 0
admin/win/msi/collect-transform.xsl.in

@@ -0,0 +1,44 @@
+<?xml version="1.0" encoding="utf-8"?>
+<xsl:stylesheet version="1.0"
+    xmlns:xsl="http://www.w3.org/1999/XSL/Transform"
+    xmlns:wix="http://schemas.microsoft.com/wix/2006/wi">
+
+  <xsl:output method="xml" indent="yes" />
+
+  <!-- Copy all attributes and elements to the output. -->
+  <xsl:template match="@*|*">
+    <xsl:copy>
+      <xsl:apply-templates select="@*" />
+      <xsl:apply-templates select="*" />
+    </xsl:copy>
+  </xsl:template>
+
+  <!-- Identify MainExecutable -->
+  <xsl:key name="exe-search" match="wix:File[contains(@Source, '@APPLICATION_EXECUTABLE@.exe')]" use="@Id" />
+  <xsl:template match="wix:File[key('exe-search', @Id)]">
+    <xsl:copy>
+      <xsl:apply-templates select="@*" />
+      <xsl:attribute name="Id">
+        <xsl:text>MainExecutable</xsl:text>
+      </xsl:attribute>
+      <xsl:apply-templates/>
+    </xsl:copy>
+  </xsl:template>
+
+  <!-- Exclude Shell Extensions -->
+  <xsl:key name="shellext-search" match="wix:Component[contains(wix:File/@Source, 'shellext')]" use="@Id" />
+  <xsl:template match="wix:Component[key('shellext-search', @Id)]" />
+  <xsl:template match="wix:ComponentRef[key('shellext-search', @Id)]" />
+
+  <xsl:key name="shellext-search" match="wix:Directory[contains(@Name, 'shellext')]" use="@Id" />
+  <xsl:template match="wix:Directory[key('shellext-search', @Id)]" />
+
+  <!-- Exclude VC Redist -->
+  <xsl:key name="vc-redist-32-search" match="wix:Component[contains(wix:File/@Source, 'vc_redist.x86.exe')]" use="@Id" />
+  <xsl:template match="wix:Component[key('vc-redist-32-search', @Id)]" />
+  <xsl:template match="wix:ComponentRef[key('vc-redist-32-search', @Id)]" />
+
+  <xsl:key name="vc-redist-64-search" match="wix:Component[contains(wix:File/@Source, 'vc_redist.x64.exe')]" use="@Id" />
+  <xsl:template match="wix:Component[key('vc-redist-64-search', @Id)]" />
+  <xsl:template match="wix:ComponentRef[key('vc-redist-64-search', @Id)]" />
+</xsl:stylesheet>

BIN
admin/win/msi/gui/banner.bmp


Fichier diff supprimé car celui-ci est trop grand
+ 67 - 0
admin/win/msi/gui/banner.svg


BIN
admin/win/msi/gui/dialog.bmp


+ 26 - 0
admin/win/msi/make-msi.bat.in

@@ -0,0 +1,26 @@
+@echo off
+set HarvestAppDir=%~1
+set BuildArch=@MSI_BUILD_ARCH@
+
+if "%HarvestAppDir%" == "" (
+    echo "Missing parameter: Please specify file collection source path (HarvestAppDir)."
+    exit 1
+)
+
+if "%WIX%" == "" (
+    echo "WiX Toolset path not set (environment variable 'WIX'). Please install the WiX Toolset."
+    exit 1
+)
+
+Rem Generate collect.wxs
+"%WIX%\bin\heat.exe" dir "%HarvestAppDir%" -dr INSTALLDIR -sreg -srd -sfrag -ag -cg ClientFiles -var var.HarvestAppDir -platform='%BuildArch%' -t collect-transform.xsl -out collect.wxs
+if %ERRORLEVEL% neq 0 exit %ERRORLEVEL%
+
+Rem Compile en-US (https://www.firegiant.com/wix/tutorial/transforms/morphing-installers/)
+"%WIX%\bin\candle.exe" -dcodepage=1252 -dPlatform=%BuildArch% -arch %BuildArch% -dHarvestAppDir="%HarvestAppDir%" -ext WixUtilExtension NCMsiHelper.wxs WinShellExt.wxs collect.wxs Nextcloud.wxs
+if %ERRORLEVEL% neq 0 exit %ERRORLEVEL%
+
+Rem Link MSI package
+"%WIX%\bin\light.exe" -sw1076 -ext WixUIExtension -ext WixUtilExtension -cultures:en-us NCMsiHelper.wixobj WinShellExt.wixobj collect.wixobj Nextcloud.wixobj -out "@MSI_INSTALLER_FILENAME@"
+
+exit %ERRORLEVEL%

+ 63 - 0
admin/win/tools/CMakeLists.txt

@@ -0,0 +1,63 @@
+cmake_minimum_required(VERSION 3.2)
+set(CMAKE_CXX_STANDARD 17)
+
+if(CMAKE_SIZEOF_VOID_P MATCHES 4)
+    set(BITNESS 32)
+else()
+    set(BITNESS 64)
+endif()
+
+include_directories(
+    ${CMAKE_CURRENT_SOURCE_DIR}
+    ${CMAKE_CURRENT_BINARY_DIR}
+    NCToolsShared
+)
+
+add_definitions(-DUNICODE)
+add_definitions(-D_UNICODE)
+add_definitions(-DNDEBUG)
+add_definitions(-D_WINDOWS)
+
+# Get APIs from from Vista onwards.
+add_definitions(-D_WIN32_WINNT=0x0601)
+add_definitions(-DWINVER=0x0601)
+
+# Use automatic overload for suitable CRT safe-functions
+# See https://docs.microsoft.com/de-de/cpp/c-runtime-library/security-features-in-the-crt?view=vs-2019
+add_definitions(-D_CRT_SECURE_CPP_OVERLOAD_STANDARD_NAMES=1)
+# Also: Disable compiler warnings because we don't use Windows CRT safe-functions explicitly and don't intend to
+# as this is a pure cross-platform source the only alternative would be a ton of ifdefs with calls to the _s version
+add_definitions(-D_CRT_SECURE_NO_WARNINGS)
+
+# Optimize for size
+set(COMPILER_FLAGS "/GL /O1 /sdl /Zc:inline /Oi /EHsc /nologo")
+set(LINKER_FLAGS "/LTCG /OPT:REF /SUBSYSTEM:WINDOWS /NOLOGO")
+
+# Enable DEP, ASLR and CFG
+set(LINKER_FLAGS "${LINKER_FLAGS} /nxcompat /dynamicbase /guard:cf")
+
+# x86 only: Enable SafeSEH
+if(CMAKE_SIZEOF_VOID_P MATCHES 4)
+    set(LINKER_FLAGS "${LINKER_FLAGS} /safeseh")
+endif()
+
+set(CMAKE_C_FLAGS "${CMAKE_C_FLAGS} ${COMPILER_FLAGS}")
+set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} ${COMPILER_FLAGS}")
+
+set(CMAKE_EXE_LINKER_FLAGS "${CMAKE_EXE_LINKER_FLAGS} ${LINKER_FLAGS}")
+set(CMAKE_SHARED_LINKER_FLAGS "${CMAKE_SHARED_LINKER_FLAGS} ${LINKER_FLAGS}")
+
+# Use static runtime for all subdirectories
+foreach(buildType "" "_DEBUG" "_MINSIZEREL" "_RELEASE" "_RELWITHDEBINFO")
+    string(REPLACE "/MD" "/MT" "CMAKE_CXX_FLAGS${buildType}" "${CMAKE_CXX_FLAGS${buildType}}")
+endforeach()
+
+add_subdirectory(NCToolsShared)
+
+if(BUILD_WIN_MSI)
+    add_subdirectory(NCMsiHelper)
+endif()
+
+if(BUILD_WIN_TOOLS)
+    add_subdirectory(NCNavRemove)
+endif()

+ 41 - 0
admin/win/tools/NCMsiHelper/CMakeLists.txt

@@ -0,0 +1,41 @@
+# Find WiX Toolset
+if(NOT DEFINED ENV{WIX})
+    # Example: WIX=C:\Program Files (x86)\WiX Toolset v3.11\
+    message(FATAL_ERROR "WiX Toolset path not set (environment variable 'WIX'). Please install the WiX Toolset.")
+else()
+    set(WIX_SDK_PATH $ENV{WIX}/SDK/VS2017)
+    message(STATUS "WiX Toolset SDK path: ${WIX_SDK_PATH}")
+endif()
+
+include_directories(${WIX_SDK_PATH}/inc)
+
+if(CMAKE_SIZEOF_VOID_P MATCHES 4)
+    link_directories(${WIX_SDK_PATH}/lib/x86)
+else()
+    link_directories(${WIX_SDK_PATH}/lib/x64)
+endif()
+
+add_definitions(-D_NCMSIHELPER_EXPORTS)
+add_definitions(-D_USRDLL)
+add_definitions(-D_WINDLL)
+
+set(TARGET_NAME NCMsiHelper${BITNESS})
+
+add_library(${TARGET_NAME} MODULE
+    CustomAction.cpp
+    CustomAction.def
+    LogResult.cpp
+    NCMsiHelper.cpp
+)
+
+target_link_libraries(${TARGET_NAME}
+    NCToolsShared
+)
+
+install(TARGETS ${TARGET_NAME}
+    DESTINATION msi/
+)
+install(FILES
+    NCMsiHelper.wxs
+    DESTINATION msi/
+)

+ 118 - 0
admin/win/tools/NCMsiHelper/CustomAction.cpp

@@ -0,0 +1,118 @@
+/*
+ * Copyright (C) by Michael Schuster <michael.schuster@nextcloud.com>
+ *
+ * This program 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.
+ *
+ * This program 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.
+ * 
+ * Parts of this file are based on:
+ * https://www.codeproject.com/articles/570751/devmsi-an-example-cplusplus-msi-wix-deferred-custo
+ * 
+ * Licensed under the The Code Project Open License (CPOL):
+ * https://www.codeproject.com/info/cpol10.aspx
+ * 
+ */
+
+#include "NCMsiHelper.h"
+
+/**
+ *  Sets up logging for MSIs and then calls the appropriate custom action with argc/argv parameters.
+ * 
+ *  MSI deferred custom action dlls have to handle parameters (properties) a little differently,
+ *  since the deferred action may not have an active session when it begins.  Since the easiest
+ *  way to pass parameter(s) is to put them all into a CustomActionData property and then retrieve it,
+ *  the easiest thing to do on this ( C/C++ ) end is to pull the parameter and then split it into
+ *  a list of parameter(s) that we need.
+ * 
+ *  For this implementation, it made sense to treat the single string provided in CustomActionData
+ *  as if it were a command line, and then parse it out just as if it were a command line.  Obviously,
+ *  the "program name" isn't going to be the first argument unless the MSI writer is pedantic, but
+ *  otherwise it seems to be a good way to do it.
+ * 
+ *  Since all entry points need to do this same work, it was easiest to have a single function that
+ *  would do the setup, pull the CustomActionData parameter, split it into an argc/argv style of
+ *  argument list, and then pass that argument list into a function that actually does something
+ *  interesting.
+ *
+ *  @param hInstall The hInstall parameter provided by MSI/WiX.
+ *  @param func The function to be called with argc/argv parameters.
+ *  @param actionName The text description of the function.  It will be put in the log.
+ *  @return Returns ERROR_SUCCESS or ERROR_INSTALL_FAILURE.
+ */
+UINT CustomActionArgcArgv(MSIHANDLE hInstall, CUSTOM_ACTION_ARGC_ARGV func, LPCSTR actionName)
+{
+    LPWSTR pszCustomActionData = nullptr, *argv = nullptr;
+
+    HRESULT hr = WcaInitialize(hInstall, actionName);
+    ExitOnFailure(hr, "Failed to initialize");
+
+    WcaLog(LOGMSG_STANDARD, "Initialized.");
+    
+    // Retrieve our custom action property. This is one of
+    // only three properties we can request on a Deferred
+    // Custom Action.  So, we assume the caller puts all
+    // parameters in this one property.
+    hr = WcaGetProperty(L"CustomActionData", &pszCustomActionData);
+    ExitOnFailure(hr, "Failed to get Custom Action Data.");
+    WcaLog(LOGMSG_STANDARD, "Custom Action Data = '%ls'.", pszCustomActionData);
+
+    // Convert the string retrieved into a standard argc/arg layout
+    // (ignoring the fact that the first parameter is whatever was
+    // passed, not necessarily the application name/path).
+    int argc = 0;
+    argv = CommandLineToArgvW(pszCustomActionData, &argc);
+    if (argv) {
+        hr = HRESULT_FROM_WIN32(GetLastError());
+        ExitOnFailure(hr, "Failed to convert Custom Action Data to argc/argv.");
+    }
+
+    hr = (func)(argc, argv);
+    ExitOnFailure(hr, "Custom action failed");
+
+LExit:
+    // Resource freeing here!
+    ReleaseStr(pszCustomActionData);
+    if (argv)
+        LocalFree(argv);
+
+    return WcaFinalize(SUCCEEDED(hr) ? ERROR_SUCCESS : ERROR_INSTALL_FAILURE);
+}
+
+UINT __stdcall ExecNsisUninstaller(MSIHANDLE hInstall)
+{
+    return CustomActionArgcArgv(hInstall, DoExecNsisUninstaller, "ExecNsisUninstaller");
+}
+
+UINT __stdcall RemoveNavigationPaneEntries(MSIHANDLE hInstall)
+{
+    return CustomActionArgcArgv(hInstall, DoRemoveNavigationPaneEntries, "RemoveNavigationPaneEntries");
+}
+
+/**
+ * DllMain - Initialize and cleanup WiX custom action utils.
+ */
+extern "C" BOOL WINAPI DllMain(
+    __in HINSTANCE hInst,
+    __in ULONG ulReason,
+    __in LPVOID
+    )
+{
+    switch(ulReason)
+    {
+    case DLL_PROCESS_ATTACH:
+        WcaGlobalInitialize(hInst);
+        break;
+
+    case DLL_PROCESS_DETACH:
+        WcaGlobalFinalize();
+        break;
+    }
+
+    return TRUE;
+}

+ 3 - 0
admin/win/tools/NCMsiHelper/CustomAction.def

@@ -0,0 +1,3 @@
+EXPORTS
+ExecNsisUninstaller
+RemoveNavigationPaneEntries

+ 134 - 0
admin/win/tools/NCMsiHelper/LogResult.cpp

@@ -0,0 +1,134 @@
+/*
+ * Copyright (C) by Michael Schuster <michael.schuster@nextcloud.com>
+ *
+ * This program 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.
+ *
+ * This program 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.
+ * 
+ * Parts of this file are based on:
+ * https://www.codeproject.com/articles/570751/devmsi-an-example-cplusplus-msi-wix-deferred-custo
+ * 
+ * Licensed under the The Code Project Open License (CPOL):
+ * https://www.codeproject.com/info/cpol10.aspx
+ * 
+ */
+
+#include "NCMsiHelper.h"
+
+//
+// This code modified from MSDN article 256348
+// "How to obtain error message descriptions using the FormatMessage API"
+// Currently found at http://support.microsoft.com/kb/256348/en-us
+
+#define ERRMSGBUFFERSIZE 256
+
+/**
+ * Use FormatMessage() to look an error code and log the error text.
+ *
+ * @param dwErrorMsgId The error code to be investigated.
+ */
+void LogError(DWORD dwErrorMsgId)
+{
+    HLOCAL pBuffer = nullptr;   // Buffer to hold the textual error description.
+    DWORD ret = 0;              // Temp space to hold a return value.
+    HINSTANCE hInst = nullptr;  // Instance handle for DLL.
+    bool doLookup = true;
+    DWORD dwMessageId = dwErrorMsgId;
+    LPCSTR pMessage = "Error %d";
+    DWORD flags = FORMAT_MESSAGE_ALLOCATE_BUFFER | FORMAT_MESSAGE_IGNORE_INSERTS;
+
+    if (HRESULT_FACILITY(dwErrorMsgId) == FACILITY_MSMQ) {
+        hInst = LoadLibrary(TEXT("MQUTIL.DLL"));
+        flags |= FORMAT_MESSAGE_FROM_HMODULE;
+        doLookup = (nullptr != hInst);
+    } else if (dwErrorMsgId >= NERR_BASE && dwErrorMsgId <= MAX_NERR) {
+        hInst = LoadLibrary(TEXT("NETMSG.DLL"));
+        flags |= FORMAT_MESSAGE_FROM_HMODULE;
+        doLookup = (nullptr != hInst);
+    } else if (HRESULT_FACILITY(dwErrorMsgId) == FACILITY_WIN32) {
+        // A "GetLastError" error, drop the HRESULT_FACILITY
+        dwMessageId &= 0x0000FFFF;
+        flags |= FORMAT_MESSAGE_FROM_SYSTEM;
+    }
+
+    if (doLookup) {
+        ret = FormatMessageA( 
+            flags,
+            hInst, // Handle to the DLL.
+            dwMessageId, // Message identifier.
+            MAKELANGID(LANG_NEUTRAL, SUBLANG_DEFAULT), // Default language.
+            reinterpret_cast<LPSTR>(&pBuffer), // Buffer that will hold the text string.
+            ERRMSGBUFFERSIZE, // Allocate at least this many chars for pBuffer.
+            nullptr // No insert values.
+        );
+    }
+
+    if (0 < ret && nullptr != pBuffer) {
+        pMessage = static_cast<LPSTR>(pBuffer);
+    }
+
+    // Display the string.
+    if (WcaIsInitialized()) {
+        WcaLogError(dwErrorMsgId, pMessage, dwMessageId);
+    } else { 
+        // Log to stdout/stderr
+        fprintf_s(stderr, pMessage, dwMessageId);
+        if ('\n' != pMessage[strlen(pMessage) - 1]) {
+            fprintf_s(stderr, "\n");
+        }
+    }
+
+    // Free the buffer.
+    LocalFree(pBuffer);
+}
+
+void LogResult(
+    __in HRESULT hr,
+    __in_z __format_string PCSTR fmt, ...
+    )
+{
+    // This code taken from MSDN vsprintf example found currently at
+    // http://msdn.microsoft.com/en-us/library/28d5ce15(v=vs.71).aspx
+    // ...and then modified... because it doesn't seem to work!
+    va_list args;
+
+    va_start(args, fmt);
+#pragma warning(push)
+#pragma warning(disable : 4996)
+    const auto len = _vsnprintf(nullptr, 0, fmt, args) + 1;
+#pragma warning(pop)
+    auto buffer = static_cast<char*>(malloc(len * sizeof(char)));
+
+#ifdef _DEBUG
+    ::ZeroMemory(buffer, len);
+#endif // _DEBUG
+    _vsnprintf_s(buffer, len, len - 1, fmt, args);
+
+    // (MSDN code complete)
+
+    // Now that the buffer holds the formatted string, send it to
+    // the appropriate output.
+    if (WcaIsInitialized()) {
+        if (FAILED(hr)) {
+            WcaLogError(hr, buffer);
+            LogError(hr);
+        } else {
+            WcaLog(LOGMSG_STANDARD, buffer);
+        }
+    } else { // Log to stdout/stderr
+        if (FAILED(hr)) {
+            fprintf_s(stderr, "%s\n", buffer);
+            LogError(hr);
+        } else {
+            fprintf_s(stdout, "%s\n", buffer);
+        }
+    }
+
+    free(buffer);
+}

+ 48 - 0
admin/win/tools/NCMsiHelper/LogResult.h

@@ -0,0 +1,48 @@
+/*
+ * Copyright (C) by Michael Schuster <michael.schuster@nextcloud.com>
+ *
+ * This program 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.
+ *
+ * This program 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.
+ * 
+ * Parts of this file are based on:
+ * https://www.codeproject.com/articles/570751/devmsi-an-example-cplusplus-msi-wix-deferred-custo
+ * 
+ * Licensed under the The Code Project Open License (CPOL):
+ * https://www.codeproject.com/info/cpol10.aspx
+ * 
+ */
+
+/**
+ *  Function prototype for LogResult()
+ */
+#pragma once
+
+/**
+ *  Log a message.
+ *
+ *  If the DLL is being used in a WiX MSI environment, LogResult() will
+ *  route any log messages to the MSI log file via WcaLog() or WcaLogError().
+ *
+ *  If the DLL is NOT being used in a WiX MSI environment, LogResult() will
+ *  route any log messages to stdout or stderr.
+ *
+ *  If the result is an error code, LogResult will attempt to gather a 
+ *  text version of the error code and place it in the log.  For example,
+ *  if the error code means ERROR_FILE_NOT_FOUND, it will look up the appropriate
+ *  message ( via FormatMessage() ) and add "The system cannot find the file specified."
+ *  to the log.
+ *
+ * @param hr The HRESULT to be interrogated for success or failure.
+ * @param fmt The string format for a user-specified error message.
+ */
+void LogResult(
+    __in HRESULT hr,
+    __in_z __format_string PCSTR fmt, ...
+);

+ 84 - 0
admin/win/tools/NCMsiHelper/NCMsiHelper.cpp

@@ -0,0 +1,84 @@
+/*
+ * Copyright (C) by Michael Schuster <michael.schuster@nextcloud.com>
+ *
+ * This program 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.
+ *
+ * This program 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.
+ */
+
+#include "NCMsiHelper.h"
+#include "utility.h"
+#include "LogResult.h"
+
+using namespace NCTools;
+
+HRESULT NCMSIHELPER_API DoExecNsisUninstaller(int argc, LPWSTR *argv)
+{
+    if (argc != 2) {
+        return HRESULT_FROM_WIN32(ERROR_INVALID_PARAMETER);
+    }
+
+    const auto appShortName = std::wstring(argv[0]);
+    const auto uninstallExePath = std::wstring(argv[1]);
+
+    if (appShortName.empty()
+         || uninstallExePath.empty()) {
+        return HRESULT_FROM_WIN32(ERROR_INVALID_PARAMETER);
+    }
+
+    auto appInstallDir = uninstallExePath;
+    const auto posLastSlash = appInstallDir.find_last_of(PathSeparator);
+    if (posLastSlash != std::wstring::npos) {
+        appInstallDir.erase(posLastSlash);
+    } else {
+        return HRESULT_FROM_WIN32(ERROR_INVALID_PARAMETER);
+    }
+
+    // Run uninstaller
+    const std::wstring cmd = L'\"' + uninstallExePath + L"\" /S _?=" + appInstallDir;
+    LogResult(S_OK, "Running '%ls'.", cmd.data());
+    Utility::execCmd(cmd);
+
+    LogResult(S_OK, "Waiting for NSIS uninstaller.");
+
+    // Can't wait for the process because Uninstall.exe (opposed to Setup.exe) immediately returns, so we'll sleep a bit.
+    Utility::waitForNsisUninstaller(appShortName);
+
+    LogResult(S_OK, "Removing the NSIS uninstaller.");
+
+    // Sleep a bit and clean up the NSIS mess
+    Sleep(1500);
+    DeleteFile(uninstallExePath.data());
+    RemoveDirectory(appInstallDir.data());
+
+    LogResult(S_OK, "Finished.");
+
+    return S_OK;
+}
+
+HRESULT NCMSIHELPER_API DoRemoveNavigationPaneEntries(int argc, LPWSTR *argv)
+{
+    if (argc != 1) {
+        return HRESULT_FROM_WIN32(ERROR_INVALID_PARAMETER);
+    }
+
+    const auto appName = std::wstring(argv[0]);
+
+    if (appName.empty()) {
+        return HRESULT_FROM_WIN32(ERROR_INVALID_PARAMETER);
+    }
+
+    LogResult(S_OK, "Removing '%ls' sync folders from Explorer's Navigation Pane for the current user.", appName.data());
+
+    Utility::removeNavigationPaneEntries(appName);
+
+    LogResult(S_OK, "Finished.");
+
+    return S_OK;
+}

+ 99 - 0
admin/win/tools/NCMsiHelper/NCMsiHelper.h

@@ -0,0 +1,99 @@
+/*
+ * Copyright (C) by Michael Schuster <michael.schuster@nextcloud.com>
+ *
+ * This program 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.
+ *
+ * This program 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.
+ * 
+ * Parts of this file are based on:
+ * https://www.codeproject.com/articles/570751/devmsi-an-example-cplusplus-msi-wix-deferred-custo
+ * 
+ * Licensed under the The Code Project Open License (CPOL):
+ * https://www.codeproject.com/info/cpol10.aspx
+ * 
+ */
+
+/**
+ * Function prototypes for external "C" interfaces into the DLL.
+ *
+ * This project builds a "hybrid" DLL that will work either from
+ * a MSI Custom Action environment or from an external C program.
+ * The former routes through "C" interface functions defined in 
+ * CustomAction.def.  The latter uses the interfaces defined here.
+ *
+ * This header is suitable for inclusion by a project wanting to
+ * call these methods.  Note that _NCMSIHELPER_EXPORTS should not be
+ * defined for the accessing application source code.
+ */
+#pragma once
+
+#include <windows.h>
+
+#ifdef _NCMSIHELPER_EXPORTS
+#  pragma comment (lib, "newdev")
+#  pragma comment (lib, "setupapi")
+#  pragma comment (lib, "msi")
+#  pragma comment (lib, "dutil")
+#  pragma comment (lib, "wcautil")
+#  pragma comment (lib, "Version")
+
+#  include <cstdlib>
+#  include <string>
+#  include <tchar.h>
+#  include <msiquery.h>
+#  include <lmerr.h>
+
+// WiX Header Files:
+#  include <wcautil.h>
+#  include <strutil.h>
+
+#  define NCMSIHELPER_API __declspec(dllexport)
+#else
+#  define NCMSIHELPER_API __declspec(dllimport)
+#endif
+
+/**
+ * Runs the NSIS uninstaller and waits for its completion.
+ *
+ * argc MUST be 2.
+ *
+ * argv[0] is APPLICATION_EXECUTABLE, e.g. "nextcloud"
+ * argv[1] is the full path to "Uninstall.exe"
+ *
+ * @param argc  The count of valid arguments in argv.
+ * @param argv  An array of string arguments for the function.
+ * @return Returns an HRESULT indicating success or failure.
+ */
+HRESULT NCMSIHELPER_API DoExecNsisUninstaller(int argc, LPWSTR *argv);
+
+
+/**
+ * Removes the Explorer's Navigation Pane entries.
+ *
+ * argc MUST be 1.
+ *
+ * argv[0] is APPLICATION_NAME, e.g. "Nextcloud"
+ *
+ * @param argc  The count of valid arguments in argv.
+ * @param argv  An array of string arguments for the function.
+ * @return Returns an HRESULT indicating success or failure.
+ */
+HRESULT NCMSIHELPER_API DoRemoveNavigationPaneEntries(int argc, LPWSTR *argv);
+
+/**
+ *  Standardized function prototype for NCMsiHelper.
+ *
+ *  Functions in NCMsiHelper can be called through the MSI Custom
+ *  Action DLL or through an external C program.  Both
+ *  methods expect to wrap things into this function prototype.
+ *
+ *  As a result, all functions defined in this header should
+ *  conform to this function prototype.
+ */
+using CUSTOM_ACTION_ARGC_ARGV = NCMSIHELPER_API HRESULT(*)(int argc, LPWSTR *argv);

+ 43 - 0
admin/win/tools/NCMsiHelper/NCMsiHelper.wxs

@@ -0,0 +1,43 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ *
+ * Copyright (C) by Michael Schuster <michael.schuster@nextcloud.com>
+ *
+ * This program 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.
+ *
+ * This program 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.
+ *
+-->
+<Wix xmlns="http://schemas.microsoft.com/wix/2006/wi">
+    <Fragment>
+
+        <?if $(var.Platform) = x64 ?>
+        <?define bitness = "64" ?>
+        <?else ?>
+        <?define bitness = "32" ?>
+        <?endif ?>
+
+        <Binary Id="NCMsiHelper" SourceFile="NCMsiHelper$(var.bitness).dll" />
+
+        <CustomAction Id="ExecNsisUninstaller"
+                    Return="ignore"
+                    BinaryKey="NCMsiHelper"
+                    DllEntry="ExecNsisUninstaller"
+                    Execute="deferred"
+                    Impersonate="no" />
+
+        <CustomAction Id="RemoveNavigationPaneEntries"
+                    Return="ignore"
+                    BinaryKey="NCMsiHelper"
+                    DllEntry="RemoveNavigationPaneEntries"
+                    Execute="deferred"
+                    Impersonate="yes" />
+
+    </Fragment>
+</Wix>

+ 14 - 0
admin/win/tools/NCNavRemove/CMakeLists.txt

@@ -0,0 +1,14 @@
+project(NCNavRemove)
+
+set(MUTEX_NAME "NCNavRemove")
+
+configure_file(NavRemoveConstants.h.in ${CMAKE_CURRENT_BINARY_DIR}/NavRemoveConstants.h)
+configure_file(NavRemove.ini.in ${CMAKE_CURRENT_BINARY_DIR}/NavRemove.ini)
+configure_file(version.rc.in ${CMAKE_CURRENT_BINARY_DIR}/version.rc)
+
+add_subdirectory(dll)
+add_subdirectory(exe)
+
+install(FILES ${CMAKE_CURRENT_BINARY_DIR}/NavRemove.ini
+    DESTINATION tools/NCNavRemove/
+)

+ 77 - 0
admin/win/tools/NCNavRemove/ConfigIni.cpp

@@ -0,0 +1,77 @@
+/*
+ * Copyright (C) by Michael Schuster <michael.schuster@nextcloud.com>
+ *
+ * This program 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.
+ *
+ * This program 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.
+ */
+
+#include <windows.h>
+#include "3rdparty/SimpleIni.h"
+#include "NavRemoveConstants.h"
+#include "ConfigIni.h"
+
+ConfigIni::ConfigIni()
+{
+}
+
+bool ConfigIni::load()
+{
+    const DWORD bufferLen = GetCurrentDirectory(0, nullptr);
+    TCHAR *pszBuffer = nullptr;
+
+    if (bufferLen == 0) {
+        return false;
+    }
+
+    pszBuffer = new TCHAR[bufferLen];
+    if (!pszBuffer) {
+        return false;
+    }
+
+    std::wstring filename;
+    if (GetCurrentDirectory(bufferLen, pszBuffer) != 0) {
+        filename = pszBuffer;
+    }
+    delete[] pszBuffer;
+
+    if (filename.empty()) {
+        return false;
+    }
+
+    filename.append(L"\\");
+    filename.append(INI_NAME);
+
+    CSimpleIni ini;
+    const wchar_t iniSection[] = CFG_KEY;
+    const wchar_t iniKey[] = CFG_VAR_APPNAME;
+
+    const auto rc = ini.LoadFile(filename.data());
+
+    if (rc != SI_OK) {
+        return false;
+    }
+
+    const auto pv = ini.GetValue(iniSection, iniKey);
+    bool success = false;
+
+    if (pv) {
+        _appName = pv;
+        success = !_appName.empty();
+    }
+ 
+    ini.Reset();
+
+    return success;
+}
+
+std::wstring ConfigIni::getAppName() const
+{
+    return _appName;
+}

+ 30 - 0
admin/win/tools/NCNavRemove/ConfigIni.h

@@ -0,0 +1,30 @@
+/*
+ * Copyright (C) by Michael Schuster <michael.schuster@nextcloud.com>
+ *
+ * This program 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.
+ *
+ * This program 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.
+ */
+
+#pragma once
+
+#include <string>
+
+class ConfigIni
+{
+public:
+    ConfigIni();
+
+    bool load();
+
+    std::wstring getAppName() const;
+
+private:
+    std::wstring _appName;
+};

+ 2 - 0
admin/win/tools/NCNavRemove/NavRemove.ini.in

@@ -0,0 +1,2 @@
+[NavRemove]
+ApplicationName=@APPLICATION_NAME@

+ 9 - 0
admin/win/tools/NCNavRemove/NavRemoveConstants.h.in

@@ -0,0 +1,9 @@
+#pragma once
+
+#define MUTEX_NAME          L"@MUTEX_NAME@"
+#define TOOL_NAME           L"NCNavRemove (@BITNESS@-bit)"
+#define TOOL_DESCRIPTION    L"Removes all Explorer Navigation Pane entries for a given ApplicationName."
+
+#define INI_NAME            L"NavRemove.ini"
+#define CFG_KEY             L"NavRemove"
+#define CFG_VAR_APPNAME     L"ApplicationName"

+ 25 - 0
admin/win/tools/NCNavRemove/dll/CMakeLists.txt

@@ -0,0 +1,25 @@
+add_definitions(-D_NAVREMOVE_EXPORTS)
+add_definitions(-D_USRDLL)
+add_definitions(-D_WINDLL)
+
+include_directories(
+    ${CMAKE_CURRENT_BINARY_DIR}/../
+)
+
+set(TARGET_NAME libNavRemove${BITNESS})
+
+add_library(${TARGET_NAME} MODULE
+    dllmain.cpp
+    NavRemove.cpp
+    exports.def
+    ../ConfigIni.cpp
+    ${CMAKE_CURRENT_BINARY_DIR}/../version.rc
+)
+
+target_link_libraries(${TARGET_NAME}
+    NCToolsShared
+)
+
+install(TARGETS ${TARGET_NAME}
+    DESTINATION tools/NCNavRemove/dll/
+)

+ 40 - 0
admin/win/tools/NCNavRemove/dll/NavRemove.cpp

@@ -0,0 +1,40 @@
+/*
+ * Copyright (C) by Michael Schuster <michael.schuster@nextcloud.com>
+ *
+ * This program 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.
+ *
+ * This program 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.
+ */
+
+#include <windows.h>
+#include "utility.h"
+#include "NavRemove.h"
+#include "../ConfigIni.h"
+
+using namespace NCTools;
+
+extern bool g_alreadyRunning;
+
+HRESULT NAVREMOVE_API RemoveNavigationPaneEntries()
+{
+    if (g_alreadyRunning) {
+        return S_OK;
+    }
+
+    // Config
+    ConfigIni ini;
+
+    if (!ini.load()) {
+        return HRESULT_FROM_WIN32(ERROR_INVALID_PARAMETER);
+    }
+
+    Utility::removeNavigationPaneEntries(ini.getAppName());
+
+    return S_OK;
+}

+ 32 - 0
admin/win/tools/NCNavRemove/dll/NavRemove.h

@@ -0,0 +1,32 @@
+/*
+ * Copyright (C) by Michael Schuster <michael.schuster@nextcloud.com>
+ *
+ * This program 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.
+ *
+ * This program 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.
+ */
+
+#pragma once
+
+#include <windows.h>
+
+// The following ifdef block is the standard way of creating macros which make exporting
+// from a DLL simpler. All files within this DLL are compiled with the _NAVREMOVE_EXPORTS
+// symbol defined on the command line. This symbol should not be defined on any project
+// that uses this DLL. This way any other project whose source files include this file see
+// NAVREMOVE_API functions as being imported from a DLL, whereas this DLL sees symbols
+// defined with this macro as being exported.
+
+#ifdef _NAVREMOVE_EXPORTS
+#define NAVREMOVE_API __declspec(dllexport)
+#else
+#define NAVREMOVE_API __declspec(dllimport)
+#endif
+
+NAVREMOVE_API HRESULT RemoveNavigationPaneEntries();

+ 42 - 0
admin/win/tools/NCNavRemove/dll/dllmain.cpp

@@ -0,0 +1,42 @@
+/*
+ * Copyright (C) by Michael Schuster <michael.schuster@nextcloud.com>
+ *
+ * This program 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.
+ *
+ * This program 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.
+ */
+
+#include <windows.h>
+#include "SimpleNamedMutex.h"
+#include "NavRemoveConstants.h"
+
+SimpleNamedMutex g_mutex(std::wstring(MUTEX_NAME));
+bool g_alreadyRunning = false;
+
+extern "C" BOOL WINAPI DllMain(
+    __in HINSTANCE hInst,
+    __in ULONG ulReason,
+    __in LPVOID
+    )
+{
+    switch(ulReason)
+    {
+    case DLL_PROCESS_ATTACH:
+        // Mutex
+        g_alreadyRunning = !g_mutex.lock();
+        break;
+
+    case DLL_PROCESS_DETACH:
+        // Release mutex
+        g_mutex.unlock();
+        break;
+    }
+
+    return TRUE;
+}

+ 2 - 0
admin/win/tools/NCNavRemove/dll/exports.def

@@ -0,0 +1,2 @@
+EXPORTS
+RemoveNavigationPaneEntries

+ 19 - 0
admin/win/tools/NCNavRemove/exe/CMakeLists.txt

@@ -0,0 +1,19 @@
+set(TARGET_NAME NavRemove${BITNESS})
+
+include_directories(
+    ${CMAKE_CURRENT_BINARY_DIR}/../
+)
+
+add_executable(${TARGET_NAME} WIN32
+    main.cpp
+    ../ConfigIni.cpp
+    ${CMAKE_CURRENT_BINARY_DIR}/../version.rc
+)
+
+target_link_libraries(${TARGET_NAME}
+    NCToolsShared
+)
+
+install(TARGETS ${TARGET_NAME}
+    DESTINATION tools/NCNavRemove/exe/
+)

+ 53 - 0
admin/win/tools/NCNavRemove/exe/main.cpp

@@ -0,0 +1,53 @@
+/*
+ * Copyright (C) by Michael Schuster <michael.schuster@nextcloud.com>
+ *
+ * This program 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.
+ *
+ * This program 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.
+ */
+
+#include <windows.h>
+#include "utility.h"
+#include "SimpleNamedMutex.h"
+#include "NavRemoveConstants.h"
+#include "../ConfigIni.h"
+
+using namespace NCTools;
+
+int APIENTRY wWinMain(_In_ HINSTANCE hInstance,
+    _In_opt_ HINSTANCE hPrevInstance,
+    _In_ LPWSTR    lpCmdLine,
+    _In_ int       nCmdShow)
+{
+    UNREFERENCED_PARAMETER(hInstance);
+    UNREFERENCED_PARAMETER(hPrevInstance);
+    UNREFERENCED_PARAMETER(lpCmdLine);
+    UNREFERENCED_PARAMETER(nCmdShow);
+
+    // Mutex
+    SimpleNamedMutex mutex(std::wstring(MUTEX_NAME));
+
+    if (!mutex.lock()) {
+        return 0;
+    }
+
+    // Config
+    ConfigIni ini;
+
+    if (!ini.load()) {
+        return 1;
+    }
+
+    Utility::removeNavigationPaneEntries(ini.getAppName());
+
+    // Release mutex
+    mutex.unlock();
+
+    return 0;
+}

+ 40 - 0
admin/win/tools/NCNavRemove/version.rc.in

@@ -0,0 +1,40 @@
+#include "winresrc.h"
+#include "NavRemoveConstants.h"
+
+#define VER_FILEVERSION             @MIRALL_VERSION_MAJOR@,@MIRALL_VERSION_MINOR@,@MIRALL_VERSION_PATCH@,@MIRALL_VERSION_BUILD@
+#define VER_FILEVERSION_STR         "@MIRALL_VERSION_MAJOR@.@MIRALL_VERSION_MINOR@.@MIRALL_VERSION_PATCH@.@MIRALL_VERSION_BUILD@\0"
+
+#define VER_PRODUCTVERSION          @MIRALL_VERSION_MAJOR@,@MIRALL_VERSION_MINOR@,@MIRALL_VERSION_PATCH@,@MIRALL_VERSION_BUILD@
+#define VER_PRODUCTVERSION_STR      "@MIRALL_VERSION_MAJOR@.@MIRALL_VERSION_MINOR@.@MIRALL_VERSION_PATCH@.@MIRALL_VERSION_BUILD@\0"
+
+#define VER_PRODUCTNAME_STR         TOOL_NAME
+#define VER_COMPANYNAME_STR         "@APPLICATION_VENDOR@"
+#define VER_COPYRIGHT_STR           "(c) @MIRALL_VERSION_YEAR@"
+
+#define VER_PRODUCTDESC_STR         TOOL_DESCRIPTION
+
+VS_VERSION_INFO VERSIONINFO
+FILEVERSION    VER_FILEVERSION
+PRODUCTVERSION VER_PRODUCTVERSION
+FILEOS         VOS__WINDOWS32
+FILETYPE       VFT_APP
+BEGIN
+    BLOCK "StringFileInfo"
+    BEGIN
+        BLOCK "080904b0"
+        BEGIN
+            VALUE "CompanyName",     VER_COMPANYNAME_STR
+            VALUE "LegalCopyright",  VER_COPYRIGHT_STR
+            VALUE "FileVersion",     VER_FILEVERSION_STR
+            VALUE "Comment",         VER_PRODUCTNAME_STR
+            VALUE "FileDescription", VER_PRODUCTDESC_STR
+            VALUE "ProductName",     VER_PRODUCTNAME_STR
+            VALUE "ProductVersion",  VER_PRODUCTVERSION_STR
+
+        END
+    END
+    BLOCK "VarFileInfo"
+    BEGIN
+        VALUE "Translation", 0x809, 1200
+    END
+END

+ 3474 - 0
admin/win/tools/NCToolsShared/3rdparty/SimpleIni.h

@@ -0,0 +1,3474 @@
+/** @mainpage
+
+    <table>
+        <tr><th>Library     <td>SimpleIni
+        <tr><th>File        <td>SimpleIni.h
+        <tr><th>Author      <td>Brodie Thiesfield [brofield at gmail dot com]
+        <tr><th>Source      <td>https://github.com/brofield/simpleini
+        <tr><th>Version     <td>4.17
+    </table>
+
+    Jump to the @link CSimpleIniTempl CSimpleIni @endlink interface documentation.
+
+    @section intro INTRODUCTION
+
+    This component allows an INI-style configuration file to be used on both
+    Windows and Linux/Unix. It is fast, simple and source code using this
+    component will compile unchanged on either OS.
+
+
+    @section features FEATURES
+
+    - MIT Licence allows free use in all software (including GPL and commercial)
+    - multi-platform (Windows CE/9x/NT..10/etc, Linux, MacOSX, Unix)
+    - loading and saving of INI-style configuration files
+    - configuration files can have any newline format on all platforms
+    - liberal acceptance of file format
+        - key/values with no section
+        - removal of whitespace around sections, keys and values
+    - support for multi-line values (values with embedded newline characters)
+    - optional support for multiple keys with the same name
+    - optional case-insensitive sections and keys (for ASCII characters only)
+    - saves files with sections and keys in the same order as they were loaded
+    - preserves comments on the file, section and keys where possible.
+    - supports both char or wchar_t programming interfaces
+    - supports both MBCS (system locale) and UTF-8 file encodings
+    - system locale does not need to be UTF-8 on Linux/Unix to load UTF-8 file
+    - support for non-ASCII characters in section, keys, values and comments
+    - support for non-standard character types or file encodings
+      via user-written converter classes
+    - support for adding/modifying values programmatically
+    - compiles cleanly in the following compilers:
+        - Windows/VC6 (warning level 3)
+        - Windows/VC.NET 2003 (warning level 4)
+        - Windows/VC 2005 (warning level 4)
+        - Linux/gcc (-Wall)
+
+
+    @section usage USAGE SUMMARY
+
+    -#  Define the appropriate symbol for the converter you wish to use and
+        include the SimpleIni.h header file. If no specific converter is defined
+        then the default converter is used. The default conversion mode uses
+        SI_CONVERT_WIN32 on Windows and SI_CONVERT_GENERIC on all other
+        platforms. If you are using ICU then SI_CONVERT_ICU is supported on all
+        platforms.
+    -#  Declare an instance the appropriate class. Note that the following
+        definitions are just shortcuts for commonly used types. Other types
+        (PRUnichar, unsigned short, unsigned char) are also possible.
+        <table>
+            <tr><th>Interface   <th>Case-sensitive  <th>Load UTF-8  <th>Load MBCS   <th>Typedef
+        <tr><th>SI_CONVERT_GENERIC
+            <tr><td>char        <td>No              <td>Yes         <td>Yes #1      <td>CSimpleIniA
+            <tr><td>char        <td>Yes             <td>Yes         <td>Yes         <td>CSimpleIniCaseA
+            <tr><td>wchar_t     <td>No              <td>Yes         <td>Yes         <td>CSimpleIniW
+            <tr><td>wchar_t     <td>Yes             <td>Yes         <td>Yes         <td>CSimpleIniCaseW
+        <tr><th>SI_CONVERT_WIN32
+            <tr><td>char        <td>No              <td>No #2       <td>Yes         <td>CSimpleIniA
+            <tr><td>char        <td>Yes             <td>Yes         <td>Yes         <td>CSimpleIniCaseA
+            <tr><td>wchar_t     <td>No              <td>Yes         <td>Yes         <td>CSimpleIniW
+            <tr><td>wchar_t     <td>Yes             <td>Yes         <td>Yes         <td>CSimpleIniCaseW
+        <tr><th>SI_CONVERT_ICU
+            <tr><td>char        <td>No              <td>Yes         <td>Yes         <td>CSimpleIniA
+            <tr><td>char        <td>Yes             <td>Yes         <td>Yes         <td>CSimpleIniCaseA
+            <tr><td>UChar       <td>No              <td>Yes         <td>Yes         <td>CSimpleIniW
+            <tr><td>UChar       <td>Yes             <td>Yes         <td>Yes         <td>CSimpleIniCaseW
+        </table>
+        #1  On Windows you are better to use CSimpleIniA with SI_CONVERT_WIN32.<br>
+        #2  Only affects Windows. On Windows this uses MBCS functions and
+            so may fold case incorrectly leading to uncertain results.
+    -# Call LoadData() or LoadFile() to load and parse the INI configuration file
+    -# Access and modify the data of the file using the following functions
+        <table>
+            <tr><td>GetAllSections  <td>Return all section names
+            <tr><td>GetAllKeys      <td>Return all key names within a section
+            <tr><td>GetAllValues    <td>Return all values within a section & key
+            <tr><td>GetSection      <td>Return all key names and values in a section
+            <tr><td>GetSectionSize  <td>Return the number of keys in a section
+            <tr><td>GetValue        <td>Return a value for a section & key
+            <tr><td>SetValue        <td>Add or update a value for a section & key
+            <tr><td>Delete          <td>Remove a section, or a key from a section
+        </table>
+    -# Call Save() or SaveFile() to save the INI configuration data
+
+    @section iostreams IO STREAMS
+
+    SimpleIni supports reading from and writing to STL IO streams. Enable this
+    by defining SI_SUPPORT_IOSTREAMS before including the SimpleIni.h header
+    file. Ensure that if the streams are backed by a file (e.g. ifstream or
+    ofstream) then the flag ios_base::binary has been used when the file was
+    opened.
+
+    @section multiline MULTI-LINE VALUES
+
+    Values that span multiple lines are created using the following format.
+
+        <pre>
+        key = <<<ENDTAG
+        .... multiline value ....
+        ENDTAG
+        </pre>
+
+    Note the following:
+    - The text used for ENDTAG can be anything and is used to find
+      where the multi-line text ends.
+    - The newline after ENDTAG in the start tag, and the newline
+      before ENDTAG in the end tag is not included in the data value.
+    - The ending tag must be on it's own line with no whitespace before
+      or after it.
+    - The multi-line value is modified at load so that each line in the value
+      is delimited by a single '\\n' character on all platforms. At save time
+      it will be converted into the newline format used by the current
+      platform.
+
+    @section comments COMMENTS
+
+    Comments are preserved in the file within the following restrictions:
+    - Every file may have a single "file comment". It must start with the
+      first character in the file, and will end with the first non-comment
+      line in the file.
+    - Every section may have a single "section comment". It will start
+      with the first comment line following the file comment, or the last
+      data entry. It ends at the beginning of the section.
+    - Every key may have a single "key comment". This comment will start
+      with the first comment line following the section start, or the file
+      comment if there is no section name.
+    - Comments are set at the time that the file, section or key is first
+      created. The only way to modify a comment on a section or a key is to
+      delete that entry and recreate it with the new comment. There is no
+      way to change the file comment.
+
+    @section save SAVE ORDER
+
+    The sections and keys are written out in the same order as they were
+    read in from the file. Sections and keys added to the data after the
+    file has been loaded will be added to the end of the file when it is
+    written. There is no way to specify the location of a section or key
+    other than in first-created, first-saved order.
+
+    @section notes NOTES
+
+    - To load UTF-8 data on Windows 95, you need to use Microsoft Layer for
+      Unicode, or SI_CONVERT_GENERIC, or SI_CONVERT_ICU.
+    - When using SI_CONVERT_GENERIC, ConvertUTF.c must be compiled and linked.
+    - When using SI_CONVERT_ICU, ICU header files must be on the include
+      path and icuuc.lib must be linked in.
+    - To load a UTF-8 file on Windows AND expose it with SI_CHAR == char,
+      you should use SI_CONVERT_GENERIC.
+    - The collation (sorting) order used for sections and keys returned from
+      iterators is NOT DEFINED. If collation order of the text is important
+      then it should be done yourself by either supplying a replacement
+      SI_STRLESS class, or by sorting the strings external to this library.
+    - Usage of the <mbstring.h> header on Windows can be disabled by defining
+      SI_NO_MBCS. This is defined automatically on Windows CE platforms.
+    - Not thread-safe so manage your own locking
+
+    @section contrib CONTRIBUTIONS
+    
+    - 2010/05/03: Tobias Gehrig: added GetDoubleValue()
+
+    @section licence MIT LICENCE
+
+    The licence text below is the boilerplate "MIT Licence" used from:
+    http://www.opensource.org/licenses/mit-license.php
+
+    Copyright (c) 2006-2012, Brodie Thiesfield
+
+    Permission is hereby granted, free of charge, to any person obtaining a copy
+    of this software and associated documentation files (the "Software"), to deal
+    in the Software without restriction, including without limitation the rights
+    to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+    copies of the Software, and to permit persons to whom the Software is furnished
+    to do so, subject to the following conditions:
+
+    The above copyright notice and this permission notice shall be included in
+    all copies or substantial portions of the Software.
+
+    THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+    IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
+    FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
+    COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
+    IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
+    CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
+*/
+
+#ifndef INCLUDED_SimpleIni_h
+#define INCLUDED_SimpleIni_h
+
+#if defined(_MSC_VER) && (_MSC_VER >= 1020)
+# pragma once
+#endif
+
+// Disable these warnings in MSVC:
+//  4127 "conditional expression is constant" as the conversion classes trigger
+//  it with the statement if (sizeof(SI_CHAR) == sizeof(char)). This test will
+//  be optimized away in a release build.
+//  4503 'insert' : decorated name length exceeded, name was truncated
+//  4702 "unreachable code" as the MS STL header causes it in release mode.
+//  Again, the code causing the warning will be cleaned up by the compiler.
+//  4786 "identifier truncated to 256 characters" as this is thrown hundreds
+//  of times VC6 as soon as STL is used.
+#ifdef _MSC_VER
+# pragma warning (push)
+# pragma warning (disable: 4127 4503 4702 4786)
+#endif
+
+#include <cstring>
+#include <cstdlib>
+#include <string>
+#include <map>
+#include <list>
+#include <algorithm>
+#include <stdio.h>
+
+#ifdef SI_SUPPORT_IOSTREAMS
+# include <iostream>
+#endif // SI_SUPPORT_IOSTREAMS
+
+#ifdef _DEBUG
+# ifndef assert
+#  include <cassert>
+# endif
+# define SI_ASSERT(x)   assert(x)
+#else
+# define SI_ASSERT(x)
+#endif
+
+enum SI_Error {
+    SI_OK       =  0,   //!< No error
+    SI_UPDATED  =  1,   //!< An existing value was updated
+    SI_INSERTED =  2,   //!< A new value was inserted
+
+    // note: test for any error with (retval < 0)
+    SI_FAIL     = -1,   //!< Generic failure
+    SI_NOMEM    = -2,   //!< Out of memory error
+    SI_FILE     = -3    //!< File error (see errno for detail error)
+};
+
+#define SI_UTF8_SIGNATURE     "\xEF\xBB\xBF"
+
+#ifdef _WIN32
+# define SI_NEWLINE_A   "\r\n"
+# define SI_NEWLINE_W   L"\r\n"
+#else // !_WIN32
+# define SI_NEWLINE_A   "\n"
+# define SI_NEWLINE_W   L"\n"
+#endif // _WIN32
+
+#if defined(SI_CONVERT_ICU)
+# include <unicode/ustring.h>
+#endif
+
+#if defined(_WIN32)
+# define SI_HAS_WIDE_FILE
+# define SI_WCHAR_T     wchar_t
+#elif defined(SI_CONVERT_ICU)
+# define SI_HAS_WIDE_FILE
+# define SI_WCHAR_T     UChar
+#endif
+
+
+// ---------------------------------------------------------------------------
+//                              MAIN TEMPLATE CLASS
+// ---------------------------------------------------------------------------
+
+/** Simple INI file reader.
+
+    This can be instantiated with the choice of unicode or native characterset,
+    and case sensitive or insensitive comparisons of section and key names.
+    The supported combinations are pre-defined with the following typedefs:
+
+    <table>
+        <tr><th>Interface   <th>Case-sensitive  <th>Typedef
+        <tr><td>char        <td>No              <td>CSimpleIniA
+        <tr><td>char        <td>Yes             <td>CSimpleIniCaseA
+        <tr><td>wchar_t     <td>No              <td>CSimpleIniW
+        <tr><td>wchar_t     <td>Yes             <td>CSimpleIniCaseW
+    </table>
+
+    Note that using other types for the SI_CHAR is supported. For instance,
+    unsigned char, unsigned short, etc. Note that where the alternative type
+    is a different size to char/wchar_t you may need to supply new helper
+    classes for SI_STRLESS and SI_CONVERTER.
+ */
+template<class SI_CHAR, class SI_STRLESS, class SI_CONVERTER>
+class CSimpleIniTempl
+{
+public:
+    typedef SI_CHAR SI_CHAR_T;
+
+    /** key entry */
+    struct Entry {
+        const SI_CHAR * pItem;
+        const SI_CHAR * pComment;
+        int             nOrder;
+
+        Entry(const SI_CHAR * a_pszItem = nullptr, int a_nOrder = 0)
+            : pItem(a_pszItem)
+            , pComment(nullptr)
+            , nOrder(a_nOrder)
+        { }
+        Entry(const SI_CHAR * a_pszItem, const SI_CHAR * a_pszComment, int a_nOrder)
+            : pItem(a_pszItem)
+            , pComment(a_pszComment)
+            , nOrder(a_nOrder)
+        { }
+        Entry(const Entry & rhs) { operator=(rhs); }
+        Entry & operator=(const Entry & rhs) {
+            pItem    = rhs.pItem;
+            pComment = rhs.pComment;
+            nOrder   = rhs.nOrder;
+            return *this;
+        }
+
+#if defined(_MSC_VER) && _MSC_VER <= 1200
+        /** STL of VC6 doesn't allow me to specify my own comparator for list::sort() */
+        bool operator<(const Entry & rhs) const { return LoadOrder()(*this, rhs); }
+        bool operator>(const Entry & rhs) const { return LoadOrder()(rhs, *this); }
+#endif
+
+        /** Strict less ordering by name of key only */
+        struct KeyOrder {
+            bool operator()(const Entry & lhs, const Entry & rhs) const {
+                const static SI_STRLESS isLess = SI_STRLESS();
+                return isLess(lhs.pItem, rhs.pItem);
+            }
+        };
+
+        /** Strict less ordering by order, and then name of key */
+        struct LoadOrder {
+            bool operator()(const Entry & lhs, const Entry & rhs) const {
+                if (lhs.nOrder != rhs.nOrder) {
+                    return lhs.nOrder < rhs.nOrder;
+                }
+                return KeyOrder()(lhs.pItem, rhs.pItem);
+            }
+        };
+    };
+
+    /** map keys to values */
+    typedef std::multimap<Entry,const SI_CHAR *,typename Entry::KeyOrder> TKeyVal;
+
+    /** map sections to key/value map */
+    typedef std::map<Entry,TKeyVal,typename Entry::KeyOrder> TSection;
+
+    /** set of dependent string pointers. Note that these pointers are
+        dependent on memory owned by CSimpleIni.
+    */
+    typedef std::list<Entry> TNamesDepend;
+
+    /** interface definition for the OutputWriter object to pass to Save()
+        in order to output the INI file data.
+    */
+    class OutputWriter {
+    public:
+        OutputWriter() { }
+        virtual ~OutputWriter() { }
+        virtual void Write(const char * a_pBuf) = 0;
+    private:
+        OutputWriter(const OutputWriter &);             // disable
+        OutputWriter & operator=(const OutputWriter &); // disable
+    };
+
+    /** OutputWriter class to write the INI data to a file */
+    class FileWriter : public OutputWriter {
+        FILE * m_file;
+    public:
+        FileWriter(FILE * a_file) : m_file(a_file) { }
+        void Write(const char * a_pBuf) {
+            fputs(a_pBuf, m_file);
+        }
+    private:
+        FileWriter(const FileWriter &);             // disable
+        FileWriter & operator=(const FileWriter &); // disable
+    };
+
+    /** OutputWriter class to write the INI data to a string */
+    class StringWriter : public OutputWriter {
+        std::string & m_string;
+    public:
+        StringWriter(std::string & a_string) : m_string(a_string) { }
+        void Write(const char * a_pBuf) {
+            m_string.append(a_pBuf);
+        }
+    private:
+        StringWriter(const StringWriter &);             // disable
+        StringWriter & operator=(const StringWriter &); // disable
+    };
+
+#ifdef SI_SUPPORT_IOSTREAMS
+    /** OutputWriter class to write the INI data to an ostream */
+    class StreamWriter : public OutputWriter {
+        std::ostream & m_ostream;
+    public:
+        StreamWriter(std::ostream & a_ostream) : m_ostream(a_ostream) { }
+        void Write(const char * a_pBuf) {
+            m_ostream << a_pBuf;
+        }
+    private:
+        StreamWriter(const StreamWriter &);             // disable
+        StreamWriter & operator=(const StreamWriter &); // disable
+    };
+#endif // SI_SUPPORT_IOSTREAMS
+
+    /** Characterset conversion utility class to convert strings to the
+        same format as is used for the storage.
+    */
+    class Converter : private SI_CONVERTER {
+    public:
+        Converter(bool a_bStoreIsUtf8) : SI_CONVERTER(a_bStoreIsUtf8) {
+            m_scratch.resize(1024);
+        }
+        Converter(const Converter & rhs) { operator=(rhs); }
+        Converter & operator=(const Converter & rhs) {
+            m_scratch = rhs.m_scratch;
+            return *this;
+        }
+        bool ConvertToStore(const SI_CHAR * a_pszString) {
+            size_t uLen = SI_CONVERTER::SizeToStore(a_pszString);
+            if (uLen == (size_t)(-1)) {
+                return false;
+            }
+            while (uLen > m_scratch.size()) {
+                m_scratch.resize(m_scratch.size() * 2);
+            }
+            return SI_CONVERTER::ConvertToStore(
+                a_pszString,
+                const_cast<char*>(m_scratch.data()),
+                m_scratch.size());
+        }
+        const char * Data() { return m_scratch.data(); }
+    private:
+        std::string m_scratch;
+    };
+
+public:
+    /*-----------------------------------------------------------------------*/
+
+    /** Default constructor.
+
+        @param a_bIsUtf8     See the method SetUnicode() for details.
+        @param a_bMultiKey   See the method SetMultiKey() for details.
+        @param a_bMultiLine  See the method SetMultiLine() for details.
+     */
+    CSimpleIniTempl(
+        bool a_bIsUtf8    = false,
+        bool a_bMultiKey  = false,
+        bool a_bMultiLine = false
+        );
+
+    /** Destructor */
+    ~CSimpleIniTempl();
+
+    /** Deallocate all memory stored by this object */
+    void Reset();
+
+    /** Has any data been loaded */
+    bool IsEmpty() const { return m_data.empty(); }
+
+    /*-----------------------------------------------------------------------*/
+    /** @{ @name Settings */
+
+    /** Set the storage format of the INI data. This affects both the loading
+        and saving of the INI data using all of the Load/Save API functions.
+        This value cannot be changed after any INI data has been loaded.
+
+        If the file is not set to Unicode (UTF-8), then the data encoding is
+        assumed to be the OS native encoding. This encoding is the system
+        locale on Linux/Unix and the legacy MBCS encoding on Windows NT/2K/XP.
+        If the storage format is set to Unicode then the file will be loaded
+        as UTF-8 encoded data regardless of the native file encoding. If
+        SI_CHAR == char then all of the char* parameters take and return UTF-8
+        encoded data regardless of the system locale.
+
+        \param a_bIsUtf8     Assume UTF-8 encoding for the source?
+     */
+    void SetUnicode(bool a_bIsUtf8 = true) {
+        if (!m_pData) m_bStoreIsUtf8 = a_bIsUtf8;
+    }
+
+    /** Get the storage format of the INI data. */
+    bool IsUnicode() const { return m_bStoreIsUtf8; }
+
+    /** Should multiple identical keys be permitted in the file. If set to false
+        then the last value encountered will be used as the value of the key.
+        If set to true, then all values will be available to be queried. For
+        example, with the following input:
+
+        <pre>
+        [section]
+        test=value1
+        test=value2
+        </pre>
+
+        Then with SetMultiKey(true), both of the values "value1" and "value2"
+        will be returned for the key test. If SetMultiKey(false) is used, then
+        the value for "test" will only be "value2". This value may be changed
+        at any time.
+
+        \param a_bAllowMultiKey  Allow multi-keys in the source?
+     */
+    void SetMultiKey(bool a_bAllowMultiKey = true) {
+        m_bAllowMultiKey = a_bAllowMultiKey;
+    }
+
+    /** Get the storage format of the INI data. */
+    bool IsMultiKey() const { return m_bAllowMultiKey; }
+
+    /** Should data values be permitted to span multiple lines in the file. If
+        set to false then the multi-line construct <<<TAG as a value will be
+        returned as is instead of loading the data. This value may be changed
+        at any time.
+
+        \param a_bAllowMultiLine     Allow multi-line values in the source?
+     */
+    void SetMultiLine(bool a_bAllowMultiLine = true) {
+        m_bAllowMultiLine = a_bAllowMultiLine;
+    }
+
+    /** Query the status of multi-line data */
+    bool IsMultiLine() const { return m_bAllowMultiLine; }
+
+    /** Should spaces be added around the equals sign when writing key/value
+        pairs out. When true, the result will be "key = value". When false, 
+        the result will be "key=value". This value may be changed at any time.
+
+        \param a_bSpaces     Add spaces around the equals sign?
+     */
+    void SetSpaces(bool a_bSpaces = true) {
+        m_bSpaces = a_bSpaces;
+    }
+
+    /** Query the status of spaces output */
+    bool UsingSpaces() const { return m_bSpaces; }
+    
+    /*-----------------------------------------------------------------------*/
+    /** @}
+        @{ @name Loading INI Data */
+
+    /** Load an INI file from disk into memory
+
+        @param a_pszFile    Path of the file to be loaded. This will be passed
+                            to fopen() and so must be a valid path for the
+                            current platform.
+
+        @return SI_Error    See error definitions
+     */
+    SI_Error LoadFile(
+        const char * a_pszFile
+        );
+
+#ifdef SI_HAS_WIDE_FILE
+    /** Load an INI file from disk into memory
+
+        @param a_pwszFile   Path of the file to be loaded in UTF-16.
+
+        @return SI_Error    See error definitions
+     */
+    SI_Error LoadFile(
+        const SI_WCHAR_T * a_pwszFile
+        );
+#endif // SI_HAS_WIDE_FILE
+
+    /** Load the file from a file pointer.
+
+        @param a_fpFile     Valid file pointer to read the file data from. The
+                            file will be read until end of file.
+
+        @return SI_Error    See error definitions
+    */
+    SI_Error LoadFile(
+        FILE * a_fpFile
+        );
+
+#ifdef SI_SUPPORT_IOSTREAMS
+    /** Load INI file data from an istream.
+
+        @param a_istream    Stream to read from
+
+        @return SI_Error    See error definitions
+     */
+    SI_Error LoadData(
+        std::istream & a_istream
+        );
+#endif // SI_SUPPORT_IOSTREAMS
+
+    /** Load INI file data direct from a std::string
+
+        @param a_strData    Data to be loaded
+
+        @return SI_Error    See error definitions
+     */
+    SI_Error LoadData(const std::string & a_strData) {
+        return LoadData(a_strData.c_str(), a_strData.size());
+    }
+
+    /** Load INI file data direct from memory
+
+        @param a_pData      Data to be loaded
+        @param a_uDataLen   Length of the data in bytes
+
+        @return SI_Error    See error definitions
+     */
+    SI_Error LoadData(
+        const char *    a_pData,
+        size_t          a_uDataLen
+        );
+
+    /*-----------------------------------------------------------------------*/
+    /** @}
+        @{ @name Saving INI Data */
+
+    /** Save an INI file from memory to disk
+
+        @param a_pszFile    Path of the file to be saved. This will be passed
+                            to fopen() and so must be a valid path for the
+                            current platform.
+
+        @param a_bAddSignature  Prepend the UTF-8 BOM if the output data is
+                            in UTF-8 format. If it is not UTF-8 then
+                            this parameter is ignored.
+
+        @return SI_Error    See error definitions
+     */
+    SI_Error SaveFile(
+        const char *    a_pszFile,
+        bool            a_bAddSignature = true
+        ) const;
+
+#ifdef SI_HAS_WIDE_FILE
+    /** Save an INI file from memory to disk
+
+        @param a_pwszFile   Path of the file to be saved in UTF-16.
+
+        @param a_bAddSignature  Prepend the UTF-8 BOM if the output data is
+                            in UTF-8 format. If it is not UTF-8 then
+                            this parameter is ignored.
+
+        @return SI_Error    See error definitions
+     */
+    SI_Error SaveFile(
+        const SI_WCHAR_T *  a_pwszFile,
+        bool                a_bAddSignature = true
+        ) const;
+#endif // _WIN32
+
+    /** Save the INI data to a file. See Save() for details.
+
+        @param a_pFile      Handle to a file. File should be opened for
+                            binary output.
+
+        @param a_bAddSignature  Prepend the UTF-8 BOM if the output data is in
+                            UTF-8 format. If it is not UTF-8 then this value is
+                            ignored. Do not set this to true if anything has
+                            already been written to the file.
+
+        @return SI_Error    See error definitions
+     */
+    SI_Error SaveFile(
+        FILE *  a_pFile,
+        bool    a_bAddSignature = false
+        ) const;
+
+    /** Save the INI data. The data will be written to the output device
+        in a format appropriate to the current data, selected by:
+
+        <table>
+            <tr><th>SI_CHAR     <th>FORMAT
+            <tr><td>char        <td>same format as when loaded (MBCS or UTF-8)
+            <tr><td>wchar_t     <td>UTF-8
+            <tr><td>other       <td>UTF-8
+        </table>
+
+        Note that comments from the original data is preserved as per the
+        documentation on comments. The order of the sections and values
+        from the original file will be preserved.
+
+        Any data prepended or appended to the output device must use the the
+        same format (MBCS or UTF-8). You may use the GetConverter() method to
+        convert text to the correct format regardless of the output format
+        being used by SimpleIni.
+
+        To add a BOM to UTF-8 data, write it out manually at the very beginning
+        like is done in SaveFile when a_bUseBOM is true.
+
+        @param a_oOutput    Output writer to write the data to.
+
+        @param a_bAddSignature  Prepend the UTF-8 BOM if the output data is in
+                            UTF-8 format. If it is not UTF-8 then this value is
+                            ignored. Do not set this to true if anything has
+                            already been written to the OutputWriter.
+
+        @return SI_Error    See error definitions
+     */
+    SI_Error Save(
+        OutputWriter &  a_oOutput,
+        bool            a_bAddSignature = false
+        ) const;
+
+#ifdef SI_SUPPORT_IOSTREAMS
+    /** Save the INI data to an ostream. See Save() for details.
+
+        @param a_ostream    String to have the INI data appended to.
+
+        @param a_bAddSignature  Prepend the UTF-8 BOM if the output data is in
+                            UTF-8 format. If it is not UTF-8 then this value is
+                            ignored. Do not set this to true if anything has
+                            already been written to the stream.
+
+        @return SI_Error    See error definitions
+     */
+    SI_Error Save(
+        std::ostream &  a_ostream,
+        bool            a_bAddSignature = false
+        ) const
+    {
+        StreamWriter writer(a_ostream);
+        return Save(writer, a_bAddSignature);
+    }
+#endif // SI_SUPPORT_IOSTREAMS
+
+    /** Append the INI data to a string. See Save() for details.
+
+        @param a_sBuffer    String to have the INI data appended to.
+
+        @param a_bAddSignature  Prepend the UTF-8 BOM if the output data is in
+                            UTF-8 format. If it is not UTF-8 then this value is
+                            ignored. Do not set this to true if anything has
+                            already been written to the string.
+
+        @return SI_Error    See error definitions
+     */
+    SI_Error Save(
+        std::string &   a_sBuffer,
+        bool            a_bAddSignature = false
+        ) const
+    {
+        StringWriter writer(a_sBuffer);
+        return Save(writer, a_bAddSignature);
+    }
+
+    /*-----------------------------------------------------------------------*/
+    /** @}
+        @{ @name Accessing INI Data */
+
+    /** Retrieve all section names. The list is returned as an STL vector of
+        names and can be iterated or searched as necessary. Note that the
+        sort order of the returned strings is NOT DEFINED. You can sort
+        the names into the load order if desired. Search this file for ".sort"
+        for an example.
+
+        NOTE! This structure contains only pointers to strings. The actual
+        string data is stored in memory owned by CSimpleIni. Ensure that the
+        CSimpleIni object is not destroyed or Reset() while these pointers
+        are in use!
+
+        @param a_names          Vector that will receive all of the section
+                                 names. See note above!
+     */
+    void GetAllSections(
+        TNamesDepend & a_names
+        ) const;
+
+    /** Retrieve all unique key names in a section. The sort order of the
+        returned strings is NOT DEFINED. You can sort the names into the load 
+        order if desired. Search this file for ".sort" for an example. Only 
+        unique key names are returned.
+
+        NOTE! This structure contains only pointers to strings. The actual
+        string data is stored in memory owned by CSimpleIni. Ensure that the
+        CSimpleIni object is not destroyed or Reset() while these strings
+        are in use!
+
+        @param a_pSection       Section to request data for
+        @param a_names          List that will receive all of the key
+                                 names. See note above!
+
+        @return true            Section was found.
+        @return false           Matching section was not found.
+     */
+    bool GetAllKeys(
+        const SI_CHAR * a_pSection,
+        TNamesDepend &  a_names
+        ) const;
+
+    /** Retrieve all values for a specific key. This method can be used when
+        multiple keys are both enabled and disabled. Note that the sort order 
+        of the returned strings is NOT DEFINED. You can sort the names into 
+        the load order if desired. Search this file for ".sort" for an example.
+
+        NOTE! The returned values are pointers to string data stored in memory
+        owned by CSimpleIni. Ensure that the CSimpleIni object is not destroyed
+        or Reset while you are using this pointer!
+
+        @param a_pSection       Section to search
+        @param a_pKey           Key to search for
+        @param a_values         List to return if the key is not found
+
+        @return true            Key was found.
+        @return false           Matching section/key was not found.
+     */
+    bool GetAllValues(
+        const SI_CHAR * a_pSection,
+        const SI_CHAR * a_pKey,
+        TNamesDepend &  a_values
+        ) const;
+
+    /** Query the number of keys in a specific section. Note that if multiple
+        keys are enabled, then this value may be different to the number of
+        keys returned by GetAllKeys.
+
+        @param a_pSection       Section to request data for
+
+        @return -1              Section does not exist in the file
+        @return >=0             Number of keys in the section
+     */
+    int GetSectionSize(
+        const SI_CHAR * a_pSection
+        ) const;
+
+    /** Retrieve all key and value pairs for a section. The data is returned
+        as a pointer to an STL map and can be iterated or searched as
+        desired. Note that multiple entries for the same key may exist when
+        multiple keys have been enabled.
+
+        NOTE! This structure contains only pointers to strings. The actual
+        string data is stored in memory owned by CSimpleIni. Ensure that the
+        CSimpleIni object is not destroyed or Reset() while these strings
+        are in use!
+
+        @param a_pSection       Name of the section to return
+        @return boolean         Was a section matching the supplied
+                                name found.
+     */
+    const TKeyVal * GetSection(
+        const SI_CHAR * a_pSection
+        ) const;
+
+    /** Retrieve the value for a specific key. If multiple keys are enabled
+        (see SetMultiKey) then only the first value associated with that key
+        will be returned, see GetAllValues for getting all values with multikey.
+
+        NOTE! The returned value is a pointer to string data stored in memory
+        owned by CSimpleIni. Ensure that the CSimpleIni object is not destroyed
+        or Reset while you are using this pointer!
+
+        @param a_pSection       Section to search
+        @param a_pKey           Key to search for
+        @param a_pDefault       Value to return if the key is not found
+        @param a_pHasMultiple   Optionally receive notification of if there are
+                                multiple entries for this key.
+
+        @return a_pDefault      Key was not found in the section
+        @return other           Value of the key
+     */
+    const SI_CHAR * GetValue(
+        const SI_CHAR * a_pSection,
+        const SI_CHAR * a_pKey,
+        const SI_CHAR * a_pDefault     = nullptr,
+        bool *          a_pHasMultiple = nullptr
+        ) const;
+
+    /** Retrieve a numeric value for a specific key. If multiple keys are enabled
+        (see SetMultiKey) then only the first value associated with that key
+        will be returned, see GetAllValues for getting all values with multikey.
+
+        @param a_pSection       Section to search
+        @param a_pKey           Key to search for
+        @param a_nDefault       Value to return if the key is not found
+        @param a_pHasMultiple   Optionally receive notification of if there are
+                                multiple entries for this key.
+
+        @return a_nDefault      Key was not found in the section
+        @return other           Value of the key
+     */
+    long GetLongValue(
+        const SI_CHAR * a_pSection,
+        const SI_CHAR * a_pKey,
+        long            a_nDefault     = 0,
+        bool *          a_pHasMultiple = nullptr
+        ) const;
+
+    /** Retrieve a numeric value for a specific key. If multiple keys are enabled
+        (see SetMultiKey) then only the first value associated with that key
+        will be returned, see GetAllValues for getting all values with multikey.
+
+        @param a_pSection       Section to search
+        @param a_pKey           Key to search for
+        @param a_nDefault       Value to return if the key is not found
+        @param a_pHasMultiple   Optionally receive notification of if there are
+                                multiple entries for this key.
+
+        @return a_nDefault      Key was not found in the section
+        @return other           Value of the key
+     */
+    double GetDoubleValue(
+        const SI_CHAR * a_pSection,
+        const SI_CHAR * a_pKey,
+        double          a_nDefault     = 0,
+        bool *          a_pHasMultiple = nullptr
+        ) const;
+
+    /** Retrieve a boolean value for a specific key. If multiple keys are enabled
+        (see SetMultiKey) then only the first value associated with that key
+        will be returned, see GetAllValues for getting all values with multikey.
+
+        Strings starting with "t", "y", "on" or "1" are returned as logically true.
+        Strings starting with "f", "n", "of" or "0" are returned as logically false.
+        For all other values the default is returned. Character comparisons are 
+        case-insensitive.
+
+        @param a_pSection       Section to search
+        @param a_pKey           Key to search for
+        @param a_bDefault       Value to return if the key is not found
+        @param a_pHasMultiple   Optionally receive notification of if there are
+                                multiple entries for this key.
+
+        @return a_nDefault      Key was not found in the section
+        @return other           Value of the key
+     */
+    bool GetBoolValue(
+        const SI_CHAR * a_pSection,
+        const SI_CHAR * a_pKey,
+        bool            a_bDefault     = false,
+        bool *          a_pHasMultiple = nullptr
+        ) const;
+
+    /** Add or update a section or value. This will always insert
+        when multiple keys are enabled.
+
+        @param a_pSection   Section to add or update
+        @param a_pKey       Key to add or update. Set to nullptr to
+                            create an empty section.
+        @param a_pValue     Value to set. Set to nullptr to create an
+                            empty section.
+        @param a_pComment   Comment to be associated with the section or the
+                            key. If a_pKey is nullptr then it will be associated
+                            with the section, otherwise the key. Note that a
+                            comment may be set ONLY when the section or key is
+                            first created (i.e. when this function returns the
+                            value SI_INSERTED). If you wish to create a section
+                            with a comment then you need to create the section
+                            separately to the key. The comment string must be
+                            in full comment form already (have a comment
+                            character starting every line).
+        @param a_bForceReplace  Should all existing values in a multi-key INI
+                            file be replaced with this entry. This option has
+                            no effect if not using multi-key files. The 
+                            difference between Delete/SetValue and SetValue
+                            with a_bForceReplace = true, is that the load 
+                            order and comment will be preserved this way.
+
+        @return SI_Error    See error definitions
+        @return SI_UPDATED  Value was updated
+        @return SI_INSERTED Value was inserted
+     */
+    SI_Error SetValue(
+        const SI_CHAR * a_pSection,
+        const SI_CHAR * a_pKey,
+        const SI_CHAR * a_pValue,
+        const SI_CHAR * a_pComment      = nullptr,
+        bool            a_bForceReplace = false
+        )
+    {
+        return AddEntry(a_pSection, a_pKey, a_pValue, a_pComment, a_bForceReplace, true);
+    }
+
+    /** Add or update a numeric value. This will always insert
+        when multiple keys are enabled.
+
+        @param a_pSection   Section to add or update
+        @param a_pKey       Key to add or update. 
+        @param a_nValue     Value to set. 
+        @param a_pComment   Comment to be associated with the key. See the 
+                            notes on SetValue() for comments.
+        @param a_bUseHex    By default the value will be written to the file 
+                            in decimal format. Set this to true to write it 
+                            as hexadecimal.
+        @param a_bForceReplace  Should all existing values in a multi-key INI
+                            file be replaced with this entry. This option has
+                            no effect if not using multi-key files. The 
+                            difference between Delete/SetLongValue and 
+                            SetLongValue with a_bForceReplace = true, is that 
+                            the load order and comment will be preserved this 
+                            way.
+
+        @return SI_Error    See error definitions
+        @return SI_UPDATED  Value was updated
+        @return SI_INSERTED Value was inserted
+     */
+    SI_Error SetLongValue(
+        const SI_CHAR * a_pSection,
+        const SI_CHAR * a_pKey,
+        long            a_nValue,
+        const SI_CHAR * a_pComment      = nullptr,
+        bool            a_bUseHex       = false,
+        bool            a_bForceReplace = false
+        );
+
+    /** Add or update a double value. This will always insert
+        when multiple keys are enabled.
+
+        @param a_pSection   Section to add or update
+        @param a_pKey       Key to add or update. 
+        @param a_nValue     Value to set. 
+        @param a_pComment   Comment to be associated with the key. See the 
+                            notes on SetValue() for comments.
+        @param a_bForceReplace  Should all existing values in a multi-key INI
+                            file be replaced with this entry. This option has
+                            no effect if not using multi-key files. The 
+                            difference between Delete/SetDoubleValue and 
+                            SetDoubleValue with a_bForceReplace = true, is that 
+                            the load order and comment will be preserved this 
+                            way.
+
+        @return SI_Error    See error definitions
+        @return SI_UPDATED  Value was updated
+        @return SI_INSERTED Value was inserted
+     */
+    SI_Error SetDoubleValue(
+        const SI_CHAR * a_pSection,
+        const SI_CHAR * a_pKey,
+        double          a_nValue,
+        const SI_CHAR * a_pComment      = nullptr,
+        bool            a_bForceReplace = false
+        );
+
+    /** Add or update a boolean value. This will always insert
+        when multiple keys are enabled.
+
+        @param a_pSection   Section to add or update
+        @param a_pKey       Key to add or update. 
+        @param a_bValue     Value to set. 
+        @param a_pComment   Comment to be associated with the key. See the 
+                            notes on SetValue() for comments.
+        @param a_bForceReplace  Should all existing values in a multi-key INI
+                            file be replaced with this entry. This option has
+                            no effect if not using multi-key files. The 
+                            difference between Delete/SetBoolValue and 
+                            SetBoolValue with a_bForceReplace = true, is that 
+                            the load order and comment will be preserved this 
+                            way.
+
+        @return SI_Error    See error definitions
+        @return SI_UPDATED  Value was updated
+        @return SI_INSERTED Value was inserted
+     */
+    SI_Error SetBoolValue(
+        const SI_CHAR * a_pSection,
+        const SI_CHAR * a_pKey,
+        bool            a_bValue,
+        const SI_CHAR * a_pComment      = nullptr,
+        bool            a_bForceReplace = false
+        );
+
+    /** Delete an entire section, or a key from a section. Note that the
+        data returned by GetSection is invalid and must not be used after
+        anything has been deleted from that section using this method.
+        Note when multiple keys is enabled, this will delete all keys with
+        that name; to selectively delete individual key/values, use
+        DeleteValue.
+
+        @param a_pSection       Section to delete key from, or if
+                                a_pKey is nullptr, the section to remove.
+        @param a_pKey           Key to remove from the section. Set to
+                                nullptr to remove the entire section.
+        @param a_bRemoveEmpty   If the section is empty after this key has
+                                been deleted, should the empty section be
+                                removed?
+
+        @return true            Key or section was deleted.
+        @return false           Key or section was not found.
+     */
+    bool Delete(
+        const SI_CHAR * a_pSection,
+        const SI_CHAR * a_pKey,
+        bool            a_bRemoveEmpty = false
+        );
+
+    /** Delete an entire section, or a key from a section. If value is
+        provided, only remove keys with the value. Note that the data
+        returned by GetSection is invalid and must not be used after
+        anything has been deleted from that section using this method.
+        Note when multiple keys is enabled, all keys with the value will
+        be deleted.
+
+        @param a_pSection       Section to delete key from, or if
+                                a_pKey is nullptr, the section to remove.
+        @param a_pKey           Key to remove from the section. Set to
+                                nullptr to remove the entire section.
+        @param a_pValue         Value of key to remove from the section.
+                                Set to nullptr to remove all keys.
+        @param a_bRemoveEmpty   If the section is empty after this key has
+                                been deleted, should the empty section be
+                                removed?
+
+        @return true            Key/value or section was deleted.
+        @return false           Key/value or section was not found.
+     */
+    bool DeleteValue(
+        const SI_CHAR * a_pSection,
+        const SI_CHAR * a_pKey,
+        const SI_CHAR * a_pValue,
+        bool            a_bRemoveEmpty = false
+        );
+
+    /*-----------------------------------------------------------------------*/
+    /** @}
+        @{ @name Converter */
+
+    /** Return a conversion object to convert text to the same encoding
+        as is used by the Save(), SaveFile() and SaveString() functions.
+        Use this to prepare the strings that you wish to append or prepend
+        to the output INI data.
+     */
+    Converter GetConverter() const {
+        return Converter(m_bStoreIsUtf8);
+    }
+
+    /*-----------------------------------------------------------------------*/
+    /** @} */
+
+private:
+    // copying is not permitted
+    CSimpleIniTempl(const CSimpleIniTempl &); // disabled
+    CSimpleIniTempl & operator=(const CSimpleIniTempl &); // disabled
+
+    /** Parse the data looking for a file comment and store it if found.
+    */
+    SI_Error FindFileComment(
+        SI_CHAR *&      a_pData,
+        bool            a_bCopyStrings
+        );
+
+    /** Parse the data looking for the next valid entry. The memory pointed to
+        by a_pData is modified by inserting NULL characters. The pointer is
+        updated to the current location in the block of text.
+    */
+    bool FindEntry(
+        SI_CHAR *&  a_pData,
+        const SI_CHAR *&  a_pSection,
+        const SI_CHAR *&  a_pKey,
+        const SI_CHAR *&  a_pVal,
+        const SI_CHAR *&  a_pComment
+        ) const;
+
+    /** Add the section/key/value to our data.
+
+        @param a_pSection   Section name. Sections will be created if they
+                            don't already exist.
+        @param a_pKey       Key name. May be nullptr to create an empty section.
+                            Existing entries will be updated. New entries will
+                            be created.
+        @param a_pValue     Value for the key.
+        @param a_pComment   Comment to be associated with the section or the
+                            key. If a_pKey is nullptr then it will be associated
+                            with the section, otherwise the key. This must be
+                            a string in full comment form already (have a
+                            comment character starting every line).
+        @param a_bForceReplace  Should all existing values in a multi-key INI
+                            file be replaced with this entry. This option has
+                            no effect if not using multi-key files. The 
+                            difference between Delete/AddEntry and AddEntry
+                            with a_bForceReplace = true, is that the load 
+                            order and comment will be preserved this way.
+        @param a_bCopyStrings   Should copies of the strings be made or not.
+                            If false then the pointers will be used as is.
+    */
+    SI_Error AddEntry(
+        const SI_CHAR * a_pSection,
+        const SI_CHAR * a_pKey,
+        const SI_CHAR * a_pValue,
+        const SI_CHAR * a_pComment,
+        bool            a_bForceReplace,
+        bool            a_bCopyStrings
+        );
+
+    /** Is the supplied character a whitespace character? */
+    inline bool IsSpace(SI_CHAR ch) const {
+        return (ch == ' ' || ch == '\t' || ch == '\r' || ch == '\n');
+    }
+
+    /** Does the supplied character start a comment line? */
+    inline bool IsComment(SI_CHAR ch) const {
+        return (ch == ';' || ch == '#');
+    }
+
+
+    /** Skip over a newline character (or characters) for either DOS or UNIX */
+    inline void SkipNewLine(SI_CHAR *& a_pData) const {
+        a_pData += (*a_pData == '\r' && *(a_pData+1) == '\n') ? 2 : 1;
+    }
+
+    /** Make a copy of the supplied string, replacing the original pointer */
+    SI_Error CopyString(const SI_CHAR *& a_pString);
+
+    /** Delete a string from the copied strings buffer if necessary */
+    void DeleteString(const SI_CHAR * a_pString);
+
+    /** Internal use of our string comparison function */
+    bool IsLess(const SI_CHAR * a_pLeft, const SI_CHAR * a_pRight) const {
+        const static SI_STRLESS isLess = SI_STRLESS();
+        return isLess(a_pLeft, a_pRight);
+    }
+
+    bool IsMultiLineTag(const SI_CHAR * a_pData) const;
+    bool IsMultiLineData(const SI_CHAR * a_pData) const;
+    bool LoadMultiLineText(
+        SI_CHAR *&          a_pData,
+        const SI_CHAR *&    a_pVal,
+        const SI_CHAR *     a_pTagName,
+        bool                a_bAllowBlankLinesInComment = false
+        ) const;
+    bool IsNewLineChar(SI_CHAR a_c) const;
+
+    bool OutputMultiLineText(
+        OutputWriter &  a_oOutput,
+        Converter &     a_oConverter,
+        const SI_CHAR * a_pText
+        ) const;
+
+private:
+    /** Copy of the INI file data in our character format. This will be
+        modified when parsed to have NULL characters added after all
+        interesting string entries. All of the string pointers to sections,
+        keys and values point into this block of memory.
+     */
+    SI_CHAR * m_pData;
+
+    /** Length of the data that we have stored. Used when deleting strings
+        to determine if the string is stored here or in the allocated string
+        buffer.
+     */
+    size_t m_uDataLen;
+
+    /** File comment for this data, if one exists. */
+    const SI_CHAR * m_pFileComment;
+
+    /** Parsed INI data. Section -> (Key -> Value). */
+    TSection m_data;
+
+    /** This vector stores allocated memory for copies of strings that have
+        been supplied after the file load. It will be empty unless SetValue()
+        has been called.
+     */
+    TNamesDepend m_strings;
+
+    /** Is the format of our datafile UTF-8 or MBCS? */
+    bool m_bStoreIsUtf8;
+
+    /** Are multiple values permitted for the same key? */
+    bool m_bAllowMultiKey;
+
+    /** Are data values permitted to span multiple lines? */
+    bool m_bAllowMultiLine;
+
+    /** Should spaces be written out surrounding the equals sign? */
+    bool m_bSpaces;
+    
+    /** Next order value, used to ensure sections and keys are output in the
+        same order that they are loaded/added.
+     */
+    int m_nOrder;
+};
+
+// ---------------------------------------------------------------------------
+//                                  IMPLEMENTATION
+// ---------------------------------------------------------------------------
+
+template<class SI_CHAR, class SI_STRLESS, class SI_CONVERTER>
+CSimpleIniTempl<SI_CHAR,SI_STRLESS,SI_CONVERTER>::CSimpleIniTempl(
+    bool a_bIsUtf8,
+    bool a_bAllowMultiKey,
+    bool a_bAllowMultiLine
+    )
+  : m_pData(0)
+  , m_uDataLen(0)
+  , m_pFileComment(nullptr)
+  , m_bStoreIsUtf8(a_bIsUtf8)
+  , m_bAllowMultiKey(a_bAllowMultiKey)
+  , m_bAllowMultiLine(a_bAllowMultiLine)
+  , m_bSpaces(true)
+  , m_nOrder(0)
+{ }
+
+template<class SI_CHAR, class SI_STRLESS, class SI_CONVERTER>
+CSimpleIniTempl<SI_CHAR,SI_STRLESS,SI_CONVERTER>::~CSimpleIniTempl()
+{
+    Reset();
+}
+
+template<class SI_CHAR, class SI_STRLESS, class SI_CONVERTER>
+void
+CSimpleIniTempl<SI_CHAR,SI_STRLESS,SI_CONVERTER>::Reset()
+{
+    // remove all data
+    delete[] m_pData;
+    m_pData = nullptr;
+    m_uDataLen = 0;
+    m_pFileComment = nullptr;
+    if (!m_data.empty()) {
+        m_data.erase(m_data.begin(), m_data.end());
+    }
+
+    // remove all strings
+    if (!m_strings.empty()) {
+        typename TNamesDepend::iterator i = m_strings.begin();
+        for (; i != m_strings.end(); ++i) {
+            delete[] const_cast<SI_CHAR*>(i->pItem);
+        }
+        m_strings.erase(m_strings.begin(), m_strings.end());
+    }
+}
+
+template<class SI_CHAR, class SI_STRLESS, class SI_CONVERTER>
+SI_Error
+CSimpleIniTempl<SI_CHAR,SI_STRLESS,SI_CONVERTER>::LoadFile(
+    const char * a_pszFile
+    )
+{
+    FILE * fp = nullptr;
+#if __STDC_WANT_SECURE_LIB__ && !_WIN32_WCE
+    fopen_s(&fp, a_pszFile, "rb");
+#else // !__STDC_WANT_SECURE_LIB__
+    fp = fopen(a_pszFile, "rb");
+#endif // __STDC_WANT_SECURE_LIB__
+    if (!fp) {
+        return SI_FILE;
+    }
+    SI_Error rc = LoadFile(fp);
+    fclose(fp);
+    return rc;
+}
+
+#ifdef SI_HAS_WIDE_FILE
+template<class SI_CHAR, class SI_STRLESS, class SI_CONVERTER>
+SI_Error
+CSimpleIniTempl<SI_CHAR,SI_STRLESS,SI_CONVERTER>::LoadFile(
+    const SI_WCHAR_T * a_pwszFile
+    )
+{
+#ifdef _WIN32
+    FILE * fp = nullptr;
+#if __STDC_WANT_SECURE_LIB__ && !_WIN32_WCE
+    _wfopen_s(&fp, a_pwszFile, L"rb");
+#else // !__STDC_WANT_SECURE_LIB__
+    fp = _wfopen(a_pwszFile, L"rb");
+#endif // __STDC_WANT_SECURE_LIB__
+    if (!fp) return SI_FILE;
+    SI_Error rc = LoadFile(fp);
+    fclose(fp);
+    return rc;
+#else // !_WIN32 (therefore SI_CONVERT_ICU)
+    char szFile[256];
+    u_austrncpy(szFile, a_pwszFile, sizeof(szFile));
+    return LoadFile(szFile);
+#endif // _WIN32
+}
+#endif // SI_HAS_WIDE_FILE
+
+template<class SI_CHAR, class SI_STRLESS, class SI_CONVERTER>
+SI_Error
+CSimpleIniTempl<SI_CHAR,SI_STRLESS,SI_CONVERTER>::LoadFile(
+    FILE * a_fpFile
+    )
+{
+    // load the raw file data
+    int retval = fseek(a_fpFile, 0, SEEK_END);
+    if (retval != 0) {
+        return SI_FILE;
+    }
+    long lSize = ftell(a_fpFile);
+    if (lSize < 0) {
+        return SI_FILE;
+    }
+    if (lSize == 0) {
+        return SI_OK;
+    }
+    
+    // allocate and ensure NULL terminated
+    char * pData = new(std::nothrow) char[lSize+1];
+    if (!pData) {
+        return SI_NOMEM;
+    }
+    pData[lSize] = 0;
+    
+    // load data into buffer
+    fseek(a_fpFile, 0, SEEK_SET);
+    size_t uRead = fread(pData, sizeof(char), lSize, a_fpFile);
+    if (uRead != (size_t) lSize) {
+        delete[] pData;
+        return SI_FILE;
+    }
+
+    // convert the raw data to unicode
+    SI_Error rc = LoadData(pData, uRead);
+    delete[] pData;
+    return rc;
+}
+
+template<class SI_CHAR, class SI_STRLESS, class SI_CONVERTER>
+SI_Error
+CSimpleIniTempl<SI_CHAR,SI_STRLESS,SI_CONVERTER>::LoadData(
+    const char *    a_pData,
+    size_t          a_uDataLen
+    )
+{
+    if (!a_pData) {
+        return SI_OK;
+    }
+    
+    // if the UTF-8 BOM exists, consume it and set mode to unicode, if we have
+    // already loaded data and try to change mode half-way through then this will
+    // be ignored and we will assert in debug versions
+    if (a_uDataLen >= 3 && memcmp(a_pData, SI_UTF8_SIGNATURE, 3) == 0) {
+        a_pData    += 3;
+        a_uDataLen -= 3;
+        SI_ASSERT(m_bStoreIsUtf8 || !m_pData); // we don't expect mixed mode data
+        SetUnicode();
+    }
+
+    if (a_uDataLen == 0) {
+        return SI_OK;
+    }
+
+    // determine the length of the converted data
+    SI_CONVERTER converter(m_bStoreIsUtf8);
+    size_t uLen = converter.SizeFromStore(a_pData, a_uDataLen);
+    if (uLen == (size_t)(-1)) {
+        return SI_FAIL;
+    }
+
+    // allocate memory for the data, ensure that there is a NULL
+    // terminator wherever the converted data ends
+    SI_CHAR * pData = new(std::nothrow) SI_CHAR[uLen+1];
+    if (!pData) {
+        return SI_NOMEM;
+    }
+    memset(pData, 0, sizeof(SI_CHAR)*(uLen+1));
+
+    // convert the data
+    if (!converter.ConvertFromStore(a_pData, a_uDataLen, pData, uLen)) {
+        delete[] pData;
+        return SI_FAIL;
+    }
+
+    // parse it
+    const static SI_CHAR empty = 0;
+    SI_CHAR * pWork = pData;
+    const SI_CHAR * pSection = &empty;
+    const SI_CHAR * pItem = nullptr;
+    const SI_CHAR * pVal = nullptr;
+    const SI_CHAR * pComment = nullptr;
+
+    // We copy the strings if we are loading data into this class when we
+    // already have stored some.
+    bool bCopyStrings = (m_pData != nullptr);
+
+    // find a file comment if it exists, this is a comment that starts at the
+    // beginning of the file and continues until the first blank line.
+    SI_Error rc = FindFileComment(pWork, bCopyStrings);
+    if (rc < 0) return rc;
+
+    // add every entry in the file to the data table
+    while (FindEntry(pWork, pSection, pItem, pVal, pComment)) {
+        rc = AddEntry(pSection, pItem, pVal, pComment, false, bCopyStrings);
+        if (rc < 0) return rc;
+    }
+
+    // store these strings if we didn't copy them
+    if (bCopyStrings) {
+        delete[] pData;
+    }
+    else {
+        m_pData = pData;
+        m_uDataLen = uLen+1;
+    }
+
+    return SI_OK;
+}
+
+#ifdef SI_SUPPORT_IOSTREAMS
+template<class SI_CHAR, class SI_STRLESS, class SI_CONVERTER>
+SI_Error
+CSimpleIniTempl<SI_CHAR,SI_STRLESS,SI_CONVERTER>::LoadData(
+    std::istream & a_istream
+    )
+{
+    std::string strData;
+    char szBuf[512];
+    do {
+        a_istream.get(szBuf, sizeof(szBuf), '\0');
+        strData.append(szBuf);
+    }
+    while (a_istream.good());
+    return LoadData(strData);
+}
+#endif // SI_SUPPORT_IOSTREAMS
+
+template<class SI_CHAR, class SI_STRLESS, class SI_CONVERTER>
+SI_Error
+CSimpleIniTempl<SI_CHAR,SI_STRLESS,SI_CONVERTER>::FindFileComment(
+    SI_CHAR *&      a_pData,
+    bool            a_bCopyStrings
+    )
+{
+    // there can only be a single file comment
+    if (m_pFileComment) {
+        return SI_OK;
+    }
+
+    // Load the file comment as multi-line text, this will modify all of
+    // the newline characters to be single \n chars
+    if (!LoadMultiLineText(a_pData, m_pFileComment, nullptr, false)) {
+        return SI_OK;
+    }
+
+    // copy the string if necessary
+    if (a_bCopyStrings) {
+        SI_Error rc = CopyString(m_pFileComment);
+        if (rc < 0) return rc;
+    }
+
+    return SI_OK;
+}
+
+template<class SI_CHAR, class SI_STRLESS, class SI_CONVERTER>
+bool
+CSimpleIniTempl<SI_CHAR,SI_STRLESS,SI_CONVERTER>::FindEntry(
+    SI_CHAR *&        a_pData,
+    const SI_CHAR *&  a_pSection,
+    const SI_CHAR *&  a_pKey,
+    const SI_CHAR *&  a_pVal,
+    const SI_CHAR *&  a_pComment
+    ) const
+{
+    a_pComment = nullptr;
+
+    SI_CHAR * pTrail = nullptr;
+    while (*a_pData) {
+        // skip spaces and empty lines
+        while (*a_pData && IsSpace(*a_pData)) {
+            ++a_pData;
+        }
+        if (!*a_pData) {
+            break;
+        }
+
+        // skip processing of comment lines but keep a pointer to
+        // the start of the comment.
+        if (IsComment(*a_pData)) {
+            LoadMultiLineText(a_pData, a_pComment, nullptr, true);
+            continue;
+        }
+
+        // process section names
+        if (*a_pData == '[') {
+            // skip leading spaces
+            ++a_pData;
+            while (*a_pData && IsSpace(*a_pData)) {
+                ++a_pData;
+            }
+
+            // find the end of the section name (it may contain spaces)
+            // and convert it to lowercase as necessary
+            a_pSection = a_pData;
+            while (*a_pData && *a_pData != ']' && !IsNewLineChar(*a_pData)) {
+                ++a_pData;
+            }
+
+            // if it's an invalid line, just skip it
+            if (*a_pData != ']') {
+                continue;
+            }
+
+            // remove trailing spaces from the section
+            pTrail = a_pData - 1;
+            while (pTrail >= a_pSection && IsSpace(*pTrail)) {
+                --pTrail;
+            }
+            ++pTrail;
+            *pTrail = 0;
+
+            // skip to the end of the line
+            ++a_pData;  // safe as checked that it == ']' above
+            while (*a_pData && !IsNewLineChar(*a_pData)) {
+                ++a_pData;
+            }
+
+            a_pKey = nullptr;
+            a_pVal = nullptr;
+            return true;
+        }
+
+        // find the end of the key name (it may contain spaces)
+        // and convert it to lowercase as necessary
+        a_pKey = a_pData;
+        while (*a_pData && *a_pData != '=' && !IsNewLineChar(*a_pData)) {
+            ++a_pData;
+        }
+
+        // if it's an invalid line, just skip it
+        if (*a_pData != '=') {
+            continue;
+        }
+
+        // empty keys are invalid
+        if (a_pKey == a_pData) {
+            while (*a_pData && !IsNewLineChar(*a_pData)) {
+                ++a_pData;
+            }
+            continue;
+        }
+
+        // remove trailing spaces from the key
+        pTrail = a_pData - 1;
+        while (pTrail >= a_pKey && IsSpace(*pTrail)) {
+            --pTrail;
+        }
+        ++pTrail;
+        *pTrail = 0;
+
+        // skip leading whitespace on the value
+        ++a_pData;  // safe as checked that it == '=' above
+        while (*a_pData && !IsNewLineChar(*a_pData) && IsSpace(*a_pData)) {
+            ++a_pData;
+        }
+
+        // find the end of the value which is the end of this line
+        a_pVal = a_pData;
+        while (*a_pData && !IsNewLineChar(*a_pData)) {
+            ++a_pData;
+        }
+
+        // remove trailing spaces from the value
+        pTrail = a_pData - 1;
+        if (*a_pData) { // prepare for the next round
+            SkipNewLine(a_pData);
+        }
+        while (pTrail >= a_pVal && IsSpace(*pTrail)) {
+            --pTrail;
+        }
+        ++pTrail;
+        *pTrail = 0;
+
+        // check for multi-line entries
+        if (m_bAllowMultiLine && IsMultiLineTag(a_pVal)) {
+            // skip the "<<<" to get the tag that will end the multiline
+            const SI_CHAR * pTagName = a_pVal + 3;
+            return LoadMultiLineText(a_pData, a_pVal, pTagName);
+        }
+
+        // return the standard entry
+        return true;
+    }
+
+    return false;
+}
+
+template<class SI_CHAR, class SI_STRLESS, class SI_CONVERTER>
+bool
+CSimpleIniTempl<SI_CHAR,SI_STRLESS,SI_CONVERTER>::IsMultiLineTag(
+    const SI_CHAR * a_pVal
+    ) const
+{
+    // check for the "<<<" prefix for a multi-line entry
+    if (*a_pVal++ != '<') return false;
+    if (*a_pVal++ != '<') return false;
+    if (*a_pVal++ != '<') return false;
+    return true;
+}
+
+template<class SI_CHAR, class SI_STRLESS, class SI_CONVERTER>
+bool
+CSimpleIniTempl<SI_CHAR,SI_STRLESS,SI_CONVERTER>::IsMultiLineData(
+    const SI_CHAR * a_pData
+    ) const
+{
+    // data is multi-line if it has any of the following features:
+    //  * whitespace prefix
+    //  * embedded newlines
+    //  * whitespace suffix
+
+    // empty string
+    if (!*a_pData) {
+        return false;
+    }
+
+    // check for prefix
+    if (IsSpace(*a_pData)) {
+        return true;
+    }
+
+    // embedded newlines
+    while (*a_pData) {
+        if (IsNewLineChar(*a_pData)) {
+            return true;
+        }
+        ++a_pData;
+    }
+
+    // check for suffix
+    if (IsSpace(*--a_pData)) {
+        return true;
+    }
+
+    return false;
+}
+
+template<class SI_CHAR, class SI_STRLESS, class SI_CONVERTER>
+bool
+CSimpleIniTempl<SI_CHAR,SI_STRLESS,SI_CONVERTER>::IsNewLineChar(
+    SI_CHAR a_c
+    ) const
+{
+    return (a_c == '\n' || a_c == '\r');
+}
+
+template<class SI_CHAR, class SI_STRLESS, class SI_CONVERTER>
+bool
+CSimpleIniTempl<SI_CHAR,SI_STRLESS,SI_CONVERTER>::LoadMultiLineText(
+    SI_CHAR *&          a_pData,
+    const SI_CHAR *&    a_pVal,
+    const SI_CHAR *     a_pTagName,
+    bool                a_bAllowBlankLinesInComment
+    ) const
+{
+    // we modify this data to strip all newlines down to a single '\n'
+    // character. This means that on Windows we need to strip out some
+    // characters which will make the data shorter.
+    // i.e.  LINE1-LINE1\r\nLINE2-LINE2\0 will become
+    //       LINE1-LINE1\nLINE2-LINE2\0
+    // The pDataLine entry is the pointer to the location in memory that
+    // the current line needs to start to run following the existing one.
+    // This may be the same as pCurrLine in which case no move is needed.
+    SI_CHAR * pDataLine = a_pData;
+    SI_CHAR * pCurrLine;
+
+    // value starts at the current line
+    a_pVal = a_pData;
+
+    // find the end tag. This tag must start in column 1 and be
+    // followed by a newline. We ignore any whitespace after the end
+    // tag but not whitespace before it.
+    SI_CHAR cEndOfLineChar = *a_pData;
+    for(;;) {
+        // if we are loading comments then we need a comment character as
+        // the first character on every line
+        if (!a_pTagName && !IsComment(*a_pData)) {
+            // if we aren't allowing blank lines then we're done
+            if (!a_bAllowBlankLinesInComment) {
+                break;
+            }
+
+            // if we are allowing blank lines then we only include them
+            // in this comment if another comment follows, so read ahead
+            // to find out.
+            SI_CHAR * pCurr = a_pData;
+            int nNewLines = 0;
+            while (IsSpace(*pCurr)) {
+                if (IsNewLineChar(*pCurr)) {
+                    ++nNewLines;
+                    SkipNewLine(pCurr);
+                }
+                else {
+                    ++pCurr;
+                }
+            }
+
+            // we have a comment, add the blank lines to the output
+            // and continue processing from here
+            if (IsComment(*pCurr)) {
+                for (; nNewLines > 0; --nNewLines) *pDataLine++ = '\n';
+                a_pData = pCurr;
+                continue;
+            }
+
+            // the comment ends here
+            break;
+        }
+
+        // find the end of this line
+        pCurrLine = a_pData;
+        while (*a_pData && !IsNewLineChar(*a_pData)) ++a_pData;
+
+        // move this line down to the location that it should be if necessary
+        if (pDataLine < pCurrLine) {
+            size_t nLen = (size_t) (a_pData - pCurrLine);
+            memmove(pDataLine, pCurrLine, nLen * sizeof(SI_CHAR));
+            pDataLine[nLen] = '\0';
+        }
+
+        // end the line with a NULL
+        cEndOfLineChar = *a_pData;
+        *a_pData = 0;
+
+        // if are looking for a tag then do the check now. This is done before
+        // checking for end of the data, so that if we have the tag at the end
+        // of the data then the tag is removed correctly.
+        if (a_pTagName) {
+            // strip whitespace from the end of this tag
+            SI_CHAR* pc = a_pData - 1;
+            while (pc > pDataLine && IsSpace(*pc)) --pc;
+            SI_CHAR ch = *++pc;
+            *pc = 0;
+
+            if (!IsLess(pDataLine, a_pTagName) && !IsLess(a_pTagName, pDataLine)) {
+                break;
+            }
+
+            *pc = ch;
+        }
+
+        // if we are at the end of the data then we just automatically end
+        // this entry and return the current data.
+        if (!cEndOfLineChar) {
+            return true;
+        }
+
+        // otherwise we need to process this newline to ensure that it consists
+        // of just a single \n character.
+        pDataLine += (a_pData - pCurrLine);
+        *a_pData = cEndOfLineChar;
+        SkipNewLine(a_pData);
+        *pDataLine++ = '\n';
+    }
+
+    // if we didn't find a comment at all then return false
+    if (a_pVal == a_pData) {
+        a_pVal = nullptr;
+        return false;
+    }
+
+    // the data (which ends at the end of the last line) needs to be
+    // null-terminated BEFORE before the newline character(s). If the
+    // user wants a new line in the multi-line data then they need to
+    // add an empty line before the tag.
+    *--pDataLine = '\0';
+
+    // if looking for a tag and if we aren't at the end of the data,
+    // then move a_pData to the start of the next line.
+    if (a_pTagName && cEndOfLineChar) {
+        SI_ASSERT(IsNewLineChar(cEndOfLineChar));
+        *a_pData = cEndOfLineChar;
+        SkipNewLine(a_pData);
+    }
+
+    return true;
+}
+
+template<class SI_CHAR, class SI_STRLESS, class SI_CONVERTER>
+SI_Error
+CSimpleIniTempl<SI_CHAR,SI_STRLESS,SI_CONVERTER>::CopyString(
+    const SI_CHAR *& a_pString
+    )
+{
+    size_t uLen = 0;
+    if (sizeof(SI_CHAR) == sizeof(char)) {
+        uLen = strlen((const char *)a_pString);
+    }
+    else if (sizeof(SI_CHAR) == sizeof(wchar_t)) {
+        uLen = wcslen((const wchar_t *)a_pString);
+    }
+    else {
+        for ( ; a_pString[uLen]; ++uLen) /*loop*/ ;
+    }
+    ++uLen; // NULL character
+    SI_CHAR * pCopy = new(std::nothrow) SI_CHAR[uLen];
+    if (!pCopy) {
+        return SI_NOMEM;
+    }
+    memcpy(pCopy, a_pString, sizeof(SI_CHAR)*uLen);
+    m_strings.push_back(pCopy);
+    a_pString = pCopy;
+    return SI_OK;
+}
+
+template<class SI_CHAR, class SI_STRLESS, class SI_CONVERTER>
+SI_Error
+CSimpleIniTempl<SI_CHAR,SI_STRLESS,SI_CONVERTER>::AddEntry(
+    const SI_CHAR * a_pSection,
+    const SI_CHAR * a_pKey,
+    const SI_CHAR * a_pValue,
+    const SI_CHAR * a_pComment,
+    bool            a_bForceReplace,
+    bool            a_bCopyStrings
+    )
+{
+    SI_Error rc;
+    bool bInserted = false;
+
+    SI_ASSERT(!a_pComment || IsComment(*a_pComment));
+
+    // if we are copying strings then make a copy of the comment now
+    // because we will need it when we add the entry.
+    if (a_bCopyStrings && a_pComment) {
+        rc = CopyString(a_pComment);
+        if (rc < 0) return rc;
+    }
+
+    // create the section entry if necessary
+    typename TSection::iterator iSection = m_data.find(a_pSection);
+    if (iSection == m_data.end()) {
+        // if the section doesn't exist then we need a copy as the
+        // string needs to last beyond the end of this function
+        if (a_bCopyStrings) {
+            rc = CopyString(a_pSection);
+            if (rc < 0) return rc;
+        }
+
+        // only set the comment if this is a section only entry
+        Entry oSection(a_pSection, ++m_nOrder);
+        if (a_pComment && (!a_pKey || !a_pValue)) {
+            oSection.pComment = a_pComment;
+        }
+
+        typename TSection::value_type oEntry(oSection, TKeyVal());
+        typedef typename TSection::iterator SectionIterator;
+        std::pair<SectionIterator,bool> i = m_data.insert(oEntry);
+        iSection = i.first;
+        bInserted = true;
+    }
+    if (!a_pKey || !a_pValue) {
+        // section only entries are specified with pItem and pVal as nullptr
+        return bInserted ? SI_INSERTED : SI_UPDATED;
+    }
+
+    // check for existence of the key
+    TKeyVal & keyval = iSection->second;
+    typename TKeyVal::iterator iKey = keyval.find(a_pKey);
+    bInserted = iKey == keyval.end();
+
+    // remove all existing entries but save the load order and
+    // comment of the first entry
+    int nLoadOrder = ++m_nOrder;
+    if (iKey != keyval.end() && m_bAllowMultiKey && a_bForceReplace) {
+        const SI_CHAR * pComment = nullptr;
+        while (iKey != keyval.end() && !IsLess(a_pKey, iKey->first.pItem)) {
+            if (iKey->first.nOrder < nLoadOrder) {
+                nLoadOrder = iKey->first.nOrder;
+                pComment   = iKey->first.pComment;
+            }
+            ++iKey;
+        }
+        if (pComment) {
+            DeleteString(a_pComment);
+            a_pComment = pComment;
+            CopyString(a_pComment);
+        }
+        Delete(a_pSection, a_pKey);
+        iKey = keyval.end();
+    }
+
+    // make string copies if necessary
+    bool bForceCreateNewKey = m_bAllowMultiKey && !a_bForceReplace;
+    if (a_bCopyStrings) {
+        if (bForceCreateNewKey || iKey == keyval.end()) {
+            // if the key doesn't exist then we need a copy as the
+            // string needs to last beyond the end of this function
+            // because we will be inserting the key next
+            rc = CopyString(a_pKey);
+            if (rc < 0) return rc;
+        }
+
+        // we always need a copy of the value
+        rc = CopyString(a_pValue);
+        if (rc < 0) return rc;
+    }
+
+    // create the key entry
+    if (iKey == keyval.end() || bForceCreateNewKey) {
+        Entry oKey(a_pKey, nLoadOrder);
+        if (a_pComment) {
+            oKey.pComment = a_pComment;
+        }
+        typename TKeyVal::value_type oEntry(oKey, static_cast<const SI_CHAR *>(nullptr));
+        iKey = keyval.insert(oEntry);
+    }
+
+    iKey->second = a_pValue;
+    return bInserted ? SI_INSERTED : SI_UPDATED;
+}
+
+template<class SI_CHAR, class SI_STRLESS, class SI_CONVERTER>
+const SI_CHAR *
+CSimpleIniTempl<SI_CHAR,SI_STRLESS,SI_CONVERTER>::GetValue(
+    const SI_CHAR * a_pSection,
+    const SI_CHAR * a_pKey,
+    const SI_CHAR * a_pDefault,
+    bool *          a_pHasMultiple
+    ) const
+{
+    if (a_pHasMultiple) {
+        *a_pHasMultiple = false;
+    }
+    if (!a_pSection || !a_pKey) {
+        return a_pDefault;
+    }
+    typename TSection::const_iterator iSection = m_data.find(a_pSection);
+    if (iSection == m_data.end()) {
+        return a_pDefault;
+    }
+    typename TKeyVal::const_iterator iKeyVal = iSection->second.find(a_pKey);
+    if (iKeyVal == iSection->second.end()) {
+        return a_pDefault;
+    }
+
+    // check for multiple entries with the same key
+    if (m_bAllowMultiKey && a_pHasMultiple) {
+        typename TKeyVal::const_iterator iTemp = iKeyVal;
+        if (++iTemp != iSection->second.end()) {
+            if (!IsLess(a_pKey, iTemp->first.pItem)) {
+                *a_pHasMultiple = true;
+            }
+        }
+    }
+
+    return iKeyVal->second;
+}
+
+template<class SI_CHAR, class SI_STRLESS, class SI_CONVERTER>
+long
+CSimpleIniTempl<SI_CHAR,SI_STRLESS,SI_CONVERTER>::GetLongValue(
+    const SI_CHAR * a_pSection,
+    const SI_CHAR * a_pKey,
+    long            a_nDefault,
+    bool *          a_pHasMultiple
+    ) const
+{
+    // return the default if we don't have a value
+    const SI_CHAR * pszValue = GetValue(a_pSection, a_pKey, nullptr, a_pHasMultiple);
+    if (!pszValue || !*pszValue) return a_nDefault;
+
+    // convert to UTF-8/MBCS which for a numeric value will be the same as ASCII
+    char szValue[64] = { 0 };
+    SI_CONVERTER c(m_bStoreIsUtf8);
+    if (!c.ConvertToStore(pszValue, szValue, sizeof(szValue))) {
+        return a_nDefault;
+    }
+
+    // handle the value as hex if prefaced with "0x"
+    long nValue = a_nDefault;
+    char * pszSuffix = szValue;
+    if (szValue[0] == '0' && (szValue[1] == 'x' || szValue[1] == 'X')) {
+    	if (!szValue[2]) return a_nDefault;
+        nValue = strtol(&szValue[2], &pszSuffix, 16);
+    }
+    else {
+        nValue = strtol(szValue, &pszSuffix, 10);
+    }
+
+    // any invalid strings will return the default value
+    if (*pszSuffix) { 
+        return a_nDefault; 
+    }
+
+    return nValue;
+}
+
+template<class SI_CHAR, class SI_STRLESS, class SI_CONVERTER>
+SI_Error 
+CSimpleIniTempl<SI_CHAR,SI_STRLESS,SI_CONVERTER>::SetLongValue(
+    const SI_CHAR * a_pSection,
+    const SI_CHAR * a_pKey,
+    long            a_nValue,
+    const SI_CHAR * a_pComment,
+    bool            a_bUseHex,
+    bool            a_bForceReplace
+    )
+{
+    // use SetValue to create sections
+    if (!a_pSection || !a_pKey) return SI_FAIL;
+
+    // convert to an ASCII string
+    char szInput[64];
+#if __STDC_WANT_SECURE_LIB__ && !_WIN32_WCE
+    sprintf_s(szInput, a_bUseHex ? "0x%lx" : "%ld", a_nValue);
+#else // !__STDC_WANT_SECURE_LIB__
+    sprintf(szInput, a_bUseHex ? "0x%lx" : "%ld", a_nValue);
+#endif // __STDC_WANT_SECURE_LIB__
+
+    // convert to output text
+    SI_CHAR szOutput[64];
+    SI_CONVERTER c(m_bStoreIsUtf8);
+    c.ConvertFromStore(szInput, strlen(szInput) + 1, 
+        szOutput, sizeof(szOutput) / sizeof(SI_CHAR));
+
+    // actually add it
+    return AddEntry(a_pSection, a_pKey, szOutput, a_pComment, a_bForceReplace, true);
+}
+
+template<class SI_CHAR, class SI_STRLESS, class SI_CONVERTER>
+double
+CSimpleIniTempl<SI_CHAR,SI_STRLESS,SI_CONVERTER>::GetDoubleValue(
+    const SI_CHAR * a_pSection,
+    const SI_CHAR * a_pKey,
+    double          a_nDefault,
+    bool *          a_pHasMultiple
+    ) const
+{
+    // return the default if we don't have a value
+    const SI_CHAR * pszValue = GetValue(a_pSection, a_pKey, nullptr, a_pHasMultiple);
+    if (!pszValue || !*pszValue) return a_nDefault;
+
+    // convert to UTF-8/MBCS which for a numeric value will be the same as ASCII
+    char szValue[64] = { 0 };
+    SI_CONVERTER c(m_bStoreIsUtf8);
+    if (!c.ConvertToStore(pszValue, szValue, sizeof(szValue))) {
+        return a_nDefault;
+    }
+
+    char * pszSuffix = nullptr;
+    double nValue = strtod(szValue, &pszSuffix);
+
+    // any invalid strings will return the default value
+    if (!pszSuffix || *pszSuffix) { 
+        return a_nDefault; 
+    }
+
+    return nValue;
+}
+
+template<class SI_CHAR, class SI_STRLESS, class SI_CONVERTER>
+SI_Error 
+CSimpleIniTempl<SI_CHAR,SI_STRLESS,SI_CONVERTER>::SetDoubleValue(
+	const SI_CHAR * a_pSection,
+	const SI_CHAR * a_pKey,
+	double          a_nValue,
+	const SI_CHAR * a_pComment,
+	bool            a_bForceReplace
+	)
+{
+	// use SetValue to create sections
+	if (!a_pSection || !a_pKey) return SI_FAIL;
+
+	// convert to an ASCII string
+	char szInput[64];
+#if __STDC_WANT_SECURE_LIB__ && !_WIN32_WCE
+	sprintf_s(szInput, "%f", a_nValue);
+#else // !__STDC_WANT_SECURE_LIB__
+	sprintf(szInput, "%f", a_nValue);
+#endif // __STDC_WANT_SECURE_LIB__
+
+	// convert to output text
+	SI_CHAR szOutput[64];
+	SI_CONVERTER c(m_bStoreIsUtf8);
+	c.ConvertFromStore(szInput, strlen(szInput) + 1, 
+		szOutput, sizeof(szOutput) / sizeof(SI_CHAR));
+
+	// actually add it
+	return AddEntry(a_pSection, a_pKey, szOutput, a_pComment, a_bForceReplace, true);
+}
+
+template<class SI_CHAR, class SI_STRLESS, class SI_CONVERTER>
+bool
+CSimpleIniTempl<SI_CHAR,SI_STRLESS,SI_CONVERTER>::GetBoolValue(
+    const SI_CHAR * a_pSection,
+    const SI_CHAR * a_pKey,
+    bool            a_bDefault,
+    bool *          a_pHasMultiple
+    ) const
+{
+    // return the default if we don't have a value
+    const SI_CHAR * pszValue = GetValue(a_pSection, a_pKey, nullptr, a_pHasMultiple);
+    if (!pszValue || !*pszValue) return a_bDefault;
+
+    // we only look at the minimum number of characters
+    switch (pszValue[0]) {
+    case 't': case 'T': // true
+    case 'y': case 'Y': // yes
+    case '1':           // 1 (one)
+        return true;
+
+    case 'f': case 'F': // false
+    case 'n': case 'N': // no
+    case '0':           // 0 (zero)
+        return false;
+
+    case 'o': case 'O':
+        if (pszValue[1] == 'n' || pszValue[1] == 'N') return true;  // on
+        if (pszValue[1] == 'f' || pszValue[1] == 'F') return false; // off
+        break;
+    }
+
+    // no recognized value, return the default
+    return a_bDefault;
+}
+
+template<class SI_CHAR, class SI_STRLESS, class SI_CONVERTER>
+SI_Error 
+CSimpleIniTempl<SI_CHAR,SI_STRLESS,SI_CONVERTER>::SetBoolValue(
+    const SI_CHAR * a_pSection,
+    const SI_CHAR * a_pKey,
+    bool            a_bValue,
+    const SI_CHAR * a_pComment,
+    bool            a_bForceReplace
+    )
+{
+    // use SetValue to create sections
+    if (!a_pSection || !a_pKey) return SI_FAIL;
+
+    // convert to an ASCII string
+    const char * pszInput = a_bValue ? "true" : "false";
+
+    // convert to output text
+    SI_CHAR szOutput[64];
+    SI_CONVERTER c(m_bStoreIsUtf8);
+    c.ConvertFromStore(pszInput, strlen(pszInput) + 1, 
+        szOutput, sizeof(szOutput) / sizeof(SI_CHAR));
+
+    // actually add it
+    return AddEntry(a_pSection, a_pKey, szOutput, a_pComment, a_bForceReplace, true);
+}
+    
+template<class SI_CHAR, class SI_STRLESS, class SI_CONVERTER>
+bool
+CSimpleIniTempl<SI_CHAR,SI_STRLESS,SI_CONVERTER>::GetAllValues(
+    const SI_CHAR * a_pSection,
+    const SI_CHAR * a_pKey,
+    TNamesDepend &  a_values
+    ) const
+{
+    a_values.clear();
+
+    if (!a_pSection || !a_pKey) {
+        return false;
+    }
+    typename TSection::const_iterator iSection = m_data.find(a_pSection);
+    if (iSection == m_data.end()) {
+        return false;
+    }
+    typename TKeyVal::const_iterator iKeyVal = iSection->second.find(a_pKey);
+    if (iKeyVal == iSection->second.end()) {
+        return false;
+    }
+
+    // insert all values for this key
+    a_values.push_back(Entry(iKeyVal->second, iKeyVal->first.pComment, iKeyVal->first.nOrder));
+    if (m_bAllowMultiKey) {
+        ++iKeyVal;
+        while (iKeyVal != iSection->second.end() && !IsLess(a_pKey, iKeyVal->first.pItem)) {
+            a_values.push_back(Entry(iKeyVal->second, iKeyVal->first.pComment, iKeyVal->first.nOrder));
+            ++iKeyVal;
+        }
+    }
+
+    return true;
+}
+
+template<class SI_CHAR, class SI_STRLESS, class SI_CONVERTER>
+int
+CSimpleIniTempl<SI_CHAR,SI_STRLESS,SI_CONVERTER>::GetSectionSize(
+    const SI_CHAR * a_pSection
+    ) const
+{
+    if (!a_pSection) {
+        return -1;
+    }
+
+    typename TSection::const_iterator iSection = m_data.find(a_pSection);
+    if (iSection == m_data.end()) {
+        return -1;
+    }
+    const TKeyVal & section = iSection->second;
+
+    // if multi-key isn't permitted then the section size is
+    // the number of keys that we have.
+    if (!m_bAllowMultiKey || section.empty()) {
+        return (int) section.size();
+    }
+
+    // otherwise we need to count them
+    int nCount = 0;
+    const SI_CHAR * pLastKey = nullptr;
+    typename TKeyVal::const_iterator iKeyVal = section.begin();
+    for (int n = 0; iKeyVal != section.end(); ++iKeyVal, ++n) {
+        if (!pLastKey || IsLess(pLastKey, iKeyVal->first.pItem)) {
+            ++nCount;
+            pLastKey = iKeyVal->first.pItem;
+        }
+    }
+    return nCount;
+}
+
+template<class SI_CHAR, class SI_STRLESS, class SI_CONVERTER>
+const typename CSimpleIniTempl<SI_CHAR,SI_STRLESS,SI_CONVERTER>::TKeyVal *
+CSimpleIniTempl<SI_CHAR,SI_STRLESS,SI_CONVERTER>::GetSection(
+    const SI_CHAR * a_pSection
+    ) const
+{
+    if (a_pSection) {
+        typename TSection::const_iterator i = m_data.find(a_pSection);
+        if (i != m_data.end()) {
+            return &(i->second);
+        }
+    }
+    return 0;
+}
+
+template<class SI_CHAR, class SI_STRLESS, class SI_CONVERTER>
+void
+CSimpleIniTempl<SI_CHAR,SI_STRLESS,SI_CONVERTER>::GetAllSections(
+    TNamesDepend & a_names
+    ) const
+{
+    a_names.clear();
+    typename TSection::const_iterator i = m_data.begin();
+    for (int n = 0; i != m_data.end(); ++i, ++n ) {
+        a_names.push_back(i->first);
+    }
+}
+
+template<class SI_CHAR, class SI_STRLESS, class SI_CONVERTER>
+bool
+CSimpleIniTempl<SI_CHAR,SI_STRLESS,SI_CONVERTER>::GetAllKeys(
+    const SI_CHAR * a_pSection,
+    TNamesDepend &  a_names
+    ) const
+{
+    a_names.clear();
+
+    if (!a_pSection) {
+        return false;
+    }
+
+    typename TSection::const_iterator iSection = m_data.find(a_pSection);
+    if (iSection == m_data.end()) {
+        return false;
+    }
+
+    const TKeyVal & section = iSection->second;
+    const SI_CHAR * pLastKey = nullptr;
+    typename TKeyVal::const_iterator iKeyVal = section.begin();
+    for (int n = 0; iKeyVal != section.end(); ++iKeyVal, ++n ) {
+        if (!pLastKey || IsLess(pLastKey, iKeyVal->first.pItem)) {
+            a_names.push_back(iKeyVal->first);
+            pLastKey = iKeyVal->first.pItem;
+        }
+    }
+
+    return true;
+}
+
+template<class SI_CHAR, class SI_STRLESS, class SI_CONVERTER>
+SI_Error
+CSimpleIniTempl<SI_CHAR,SI_STRLESS,SI_CONVERTER>::SaveFile(
+    const char *    a_pszFile,
+    bool            a_bAddSignature
+    ) const
+{
+    FILE * fp = nullptr;
+#if __STDC_WANT_SECURE_LIB__ && !_WIN32_WCE
+    fopen_s(&fp, a_pszFile, "wb");
+#else // !__STDC_WANT_SECURE_LIB__
+    fp = fopen(a_pszFile, "wb");
+#endif // __STDC_WANT_SECURE_LIB__
+    if (!fp) return SI_FILE;
+    SI_Error rc = SaveFile(fp, a_bAddSignature);
+    fclose(fp);
+    return rc;
+}
+
+#ifdef SI_HAS_WIDE_FILE
+template<class SI_CHAR, class SI_STRLESS, class SI_CONVERTER>
+SI_Error
+CSimpleIniTempl<SI_CHAR,SI_STRLESS,SI_CONVERTER>::SaveFile(
+    const SI_WCHAR_T *  a_pwszFile,
+    bool                a_bAddSignature
+    ) const
+{
+#ifdef _WIN32
+    FILE * fp = nullptr;
+#if __STDC_WANT_SECURE_LIB__ && !_WIN32_WCE
+    _wfopen_s(&fp, a_pwszFile, L"wb");
+#else // !__STDC_WANT_SECURE_LIB__
+    fp = _wfopen(a_pwszFile, L"wb");
+#endif // __STDC_WANT_SECURE_LIB__
+    if (!fp) return SI_FILE;
+    SI_Error rc = SaveFile(fp, a_bAddSignature);
+    fclose(fp);
+    return rc;
+#else // !_WIN32 (therefore SI_CONVERT_ICU)
+    char szFile[256];
+    u_austrncpy(szFile, a_pwszFile, sizeof(szFile));
+    return SaveFile(szFile, a_bAddSignature);
+#endif // _WIN32
+}
+#endif // SI_HAS_WIDE_FILE
+
+template<class SI_CHAR, class SI_STRLESS, class SI_CONVERTER>
+SI_Error
+CSimpleIniTempl<SI_CHAR,SI_STRLESS,SI_CONVERTER>::SaveFile(
+    FILE *  a_pFile,
+    bool    a_bAddSignature
+    ) const
+{
+    FileWriter writer(a_pFile);
+    return Save(writer, a_bAddSignature);
+}
+
+template<class SI_CHAR, class SI_STRLESS, class SI_CONVERTER>
+SI_Error
+CSimpleIniTempl<SI_CHAR,SI_STRLESS,SI_CONVERTER>::Save(
+    OutputWriter &  a_oOutput,
+    bool            a_bAddSignature
+    ) const
+{
+    Converter convert(m_bStoreIsUtf8);
+
+    // add the UTF-8 signature if it is desired
+    if (m_bStoreIsUtf8 && a_bAddSignature) {
+        a_oOutput.Write(SI_UTF8_SIGNATURE);
+    }
+
+    // get all of the sections sorted in load order
+    TNamesDepend oSections;
+    GetAllSections(oSections);
+#if defined(_MSC_VER) && _MSC_VER <= 1200
+    oSections.sort();
+#elif defined(__BORLANDC__)
+    oSections.sort(Entry::LoadOrder());
+#else
+    oSections.sort(typename Entry::LoadOrder());
+#endif
+
+    // if there is an empty section name, then it must be written out first
+    // regardless of the load order
+    typename TNamesDepend::iterator is = oSections.begin();
+    for (; is != oSections.end(); ++is) {
+        if (!*is->pItem) {
+            // move the empty section name to the front of the section list
+            if (is != oSections.begin()) {
+                oSections.splice(oSections.begin(), oSections, is, std::next(is));
+            }
+            break;
+        }
+    }
+
+    // write the file comment if we have one
+    bool bNeedNewLine = false;
+    if (m_pFileComment) {
+        if (!OutputMultiLineText(a_oOutput, convert, m_pFileComment)) {
+            return SI_FAIL;
+        }
+        bNeedNewLine = true;
+    }
+
+    // iterate through our sections and output the data
+    typename TNamesDepend::const_iterator iSection = oSections.begin();
+    for ( ; iSection != oSections.end(); ++iSection ) {
+        // write out the comment if there is one
+        if (iSection->pComment) {
+            if (bNeedNewLine) {
+                a_oOutput.Write(SI_NEWLINE_A);
+                a_oOutput.Write(SI_NEWLINE_A);
+            }
+            if (!OutputMultiLineText(a_oOutput, convert, iSection->pComment)) {
+                return SI_FAIL;
+            }
+            bNeedNewLine = false;
+        }
+
+        if (bNeedNewLine) {
+            a_oOutput.Write(SI_NEWLINE_A);
+            a_oOutput.Write(SI_NEWLINE_A);
+            bNeedNewLine = false;
+        }
+
+        // write the section (unless there is no section name)
+        if (*iSection->pItem) {
+            if (!convert.ConvertToStore(iSection->pItem)) {
+                return SI_FAIL;
+            }
+            a_oOutput.Write("[");
+            a_oOutput.Write(convert.Data());
+            a_oOutput.Write("]");
+            a_oOutput.Write(SI_NEWLINE_A);
+        }
+
+        // get all of the keys sorted in load order
+        TNamesDepend oKeys;
+        GetAllKeys(iSection->pItem, oKeys);
+#if defined(_MSC_VER) && _MSC_VER <= 1200
+        oKeys.sort();
+#elif defined(__BORLANDC__)
+        oKeys.sort(Entry::LoadOrder());
+#else
+        oKeys.sort(typename Entry::LoadOrder());
+#endif
+
+        // write all keys and values
+        typename TNamesDepend::const_iterator iKey = oKeys.begin();
+        for ( ; iKey != oKeys.end(); ++iKey) {
+            // get all values for this key
+            TNamesDepend oValues;
+            GetAllValues(iSection->pItem, iKey->pItem, oValues);
+
+            typename TNamesDepend::const_iterator iValue = oValues.begin();
+            for ( ; iValue != oValues.end(); ++iValue) {
+                // write out the comment if there is one
+                if (iValue->pComment) {
+                    a_oOutput.Write(SI_NEWLINE_A);
+                    if (!OutputMultiLineText(a_oOutput, convert, iValue->pComment)) {
+                        return SI_FAIL;
+                    }
+                }
+
+                // write the key
+                if (!convert.ConvertToStore(iKey->pItem)) {
+                    return SI_FAIL;
+                }
+                a_oOutput.Write(convert.Data());
+
+                // write the value
+                if (!convert.ConvertToStore(iValue->pItem)) {
+                    return SI_FAIL;
+                }
+                a_oOutput.Write(m_bSpaces ? " = " : "=");
+                if (m_bAllowMultiLine && IsMultiLineData(iValue->pItem)) {
+                    // multi-line data needs to be processed specially to ensure
+                    // that we use the correct newline format for the current system
+                    a_oOutput.Write("<<<END_OF_TEXT" SI_NEWLINE_A);
+                    if (!OutputMultiLineText(a_oOutput, convert, iValue->pItem)) {
+                        return SI_FAIL;
+                    }
+                    a_oOutput.Write("END_OF_TEXT");
+                }
+                else {
+                    a_oOutput.Write(convert.Data());
+                }
+                a_oOutput.Write(SI_NEWLINE_A);
+            }
+        }
+
+        bNeedNewLine = true;
+    }
+
+    return SI_OK;
+}
+
+template<class SI_CHAR, class SI_STRLESS, class SI_CONVERTER>
+bool
+CSimpleIniTempl<SI_CHAR,SI_STRLESS,SI_CONVERTER>::OutputMultiLineText(
+    OutputWriter &  a_oOutput,
+    Converter &     a_oConverter,
+    const SI_CHAR * a_pText
+    ) const
+{
+    const SI_CHAR * pEndOfLine;
+    SI_CHAR cEndOfLineChar = *a_pText;
+    while (cEndOfLineChar) {
+        // find the end of this line
+        pEndOfLine = a_pText;
+        for (; *pEndOfLine && *pEndOfLine != '\n'; ++pEndOfLine) /*loop*/ ;
+        cEndOfLineChar = *pEndOfLine;
+
+        // temporarily null terminate, convert and output the line
+        *const_cast<SI_CHAR*>(pEndOfLine) = 0;
+        if (!a_oConverter.ConvertToStore(a_pText)) {
+            return false;
+        }
+        *const_cast<SI_CHAR*>(pEndOfLine) = cEndOfLineChar;
+        a_pText += (pEndOfLine - a_pText) + 1;
+        a_oOutput.Write(a_oConverter.Data());
+        a_oOutput.Write(SI_NEWLINE_A);
+    }
+    return true;
+}
+
+template<class SI_CHAR, class SI_STRLESS, class SI_CONVERTER>
+bool
+CSimpleIniTempl<SI_CHAR,SI_STRLESS,SI_CONVERTER>::Delete(
+    const SI_CHAR * a_pSection,
+    const SI_CHAR * a_pKey,
+    bool            a_bRemoveEmpty
+    )
+{
+    return DeleteValue(a_pSection, a_pKey, nullptr, a_bRemoveEmpty);
+}
+
+template<class SI_CHAR, class SI_STRLESS, class SI_CONVERTER>
+bool
+CSimpleIniTempl<SI_CHAR,SI_STRLESS,SI_CONVERTER>::DeleteValue(
+    const SI_CHAR * a_pSection,
+    const SI_CHAR * a_pKey,
+    const SI_CHAR * a_pValue,
+    bool            a_bRemoveEmpty
+    )
+{
+    if (!a_pSection) {
+        return false;
+    }
+
+    typename TSection::iterator iSection = m_data.find(a_pSection);
+    if (iSection == m_data.end()) {
+        return false;
+    }
+
+    // remove a single key if we have a keyname
+    if (a_pKey) {
+        typename TKeyVal::iterator iKeyVal = iSection->second.find(a_pKey);
+        if (iKeyVal == iSection->second.end()) {
+            return false;
+        }
+
+        const static SI_STRLESS isLess = SI_STRLESS();
+
+        // remove any copied strings and then the key
+        typename TKeyVal::iterator iDelete;
+        bool bDeleted = false;
+        do {
+            iDelete = iKeyVal++;
+
+            if(a_pValue == nullptr ||
+            (isLess(a_pValue, iDelete->second) == false &&
+            isLess(iDelete->second, a_pValue) == false)) {
+                DeleteString(iDelete->first.pItem);
+                DeleteString(iDelete->second);
+                iSection->second.erase(iDelete);
+                bDeleted = true;
+            }
+        }
+        while (iKeyVal != iSection->second.end()
+            && !IsLess(a_pKey, iKeyVal->first.pItem));
+
+        if(!bDeleted) {
+            return false;
+        }
+
+        // done now if the section is not empty or we are not pruning away
+        // the empty sections. Otherwise let it fall through into the section
+        // deletion code
+        if (!a_bRemoveEmpty || !iSection->second.empty()) {
+            return true;
+        }
+    }
+    else {
+        // delete all copied strings from this section. The actual
+        // entries will be removed when the section is removed.
+        typename TKeyVal::iterator iKeyVal = iSection->second.begin();
+        for ( ; iKeyVal != iSection->second.end(); ++iKeyVal) {
+            DeleteString(iKeyVal->first.pItem);
+            DeleteString(iKeyVal->second);
+        }
+    }
+
+    // delete the section itself
+    DeleteString(iSection->first.pItem);
+    m_data.erase(iSection);
+
+    return true;
+}
+
+template<class SI_CHAR, class SI_STRLESS, class SI_CONVERTER>
+void
+CSimpleIniTempl<SI_CHAR,SI_STRLESS,SI_CONVERTER>::DeleteString(
+    const SI_CHAR * a_pString
+    )
+{
+    // strings may exist either inside the data block, or they will be
+    // individually allocated and stored in m_strings. We only physically
+    // delete those stored in m_strings.
+    if (a_pString < m_pData || a_pString >= m_pData + m_uDataLen) {
+        typename TNamesDepend::iterator i = m_strings.begin();
+        for (;i != m_strings.end(); ++i) {
+            if (a_pString == i->pItem) {
+                delete[] const_cast<SI_CHAR*>(i->pItem);
+                m_strings.erase(i);
+                break;
+            }
+        }
+    }
+}
+
+// ---------------------------------------------------------------------------
+//                              CONVERSION FUNCTIONS
+// ---------------------------------------------------------------------------
+
+// Defines the conversion classes for different libraries. Before including
+// SimpleIni.h, set the converter that you wish you use by defining one of the
+// following symbols.
+//
+//  SI_NO_CONVERSION        Do not make the "W" wide character version of the 
+//                          library available. Only CSimpleIniA etc is defined.
+//  SI_CONVERT_GENERIC      Use the Unicode reference conversion library in
+//                          the accompanying files ConvertUTF.h/c
+//  SI_CONVERT_ICU          Use the IBM ICU conversion library. Requires
+//                          ICU headers on include path and icuuc.lib
+//  SI_CONVERT_WIN32        Use the Win32 API functions for conversion.
+
+#if !defined(SI_NO_CONVERSION) && !defined(SI_CONVERT_GENERIC) && !defined(SI_CONVERT_WIN32) && !defined(SI_CONVERT_ICU)
+# ifdef _WIN32
+#  define SI_CONVERT_WIN32
+# else
+#  define SI_CONVERT_GENERIC
+# endif
+#endif
+
+/**
+ * Generic case-sensitive less than comparison. This class returns numerically
+ * ordered ASCII case-sensitive text for all possible sizes and types of
+ * SI_CHAR.
+ */
+template<class SI_CHAR>
+struct SI_GenericCase {
+    bool operator()(const SI_CHAR * pLeft, const SI_CHAR * pRight) const {
+        long cmp;
+        for ( ;*pLeft && *pRight; ++pLeft, ++pRight) {
+            cmp = (long) *pLeft - (long) *pRight;
+            if (cmp != 0) {
+                return cmp < 0;
+            }
+        }
+        return *pRight != 0;
+    }
+};
+
+/**
+ * Generic ASCII case-insensitive less than comparison. This class returns
+ * numerically ordered ASCII case-insensitive text for all possible sizes
+ * and types of SI_CHAR. It is not safe for MBCS text comparison where
+ * ASCII A-Z characters are used in the encoding of multi-byte characters.
+ */
+template<class SI_CHAR>
+struct SI_GenericNoCase {
+    inline SI_CHAR locase(SI_CHAR ch) const {
+        return (ch < 'A' || ch > 'Z') ? ch : (ch - 'A' + 'a');
+    }
+    bool operator()(const SI_CHAR * pLeft, const SI_CHAR * pRight) const {
+        long cmp;
+        for ( ;*pLeft && *pRight; ++pLeft, ++pRight) {
+            cmp = (long) locase(*pLeft) - (long) locase(*pRight);
+            if (cmp != 0) {
+                return cmp < 0;
+            }
+        }
+        return *pRight != 0;
+    }
+};
+
+/**
+ * Null conversion class for MBCS/UTF-8 to char (or equivalent).
+ */
+template<class SI_CHAR>
+class SI_ConvertA {
+    bool m_bStoreIsUtf8;
+protected:
+    SI_ConvertA() { }
+public:
+    SI_ConvertA(bool a_bStoreIsUtf8) : m_bStoreIsUtf8(a_bStoreIsUtf8) { }
+
+    /* copy and assignment */
+    SI_ConvertA(const SI_ConvertA & rhs) { operator=(rhs); }
+    SI_ConvertA & operator=(const SI_ConvertA & rhs) {
+        m_bStoreIsUtf8 = rhs.m_bStoreIsUtf8;
+        return *this;
+    }
+
+    /** Calculate the number of SI_CHAR required for converting the input
+     * from the storage format. The storage format is always UTF-8 or MBCS.
+     *
+     * @param a_pInputData  Data in storage format to be converted to SI_CHAR.
+     * @param a_uInputDataLen Length of storage format data in bytes. This
+     *                      must be the actual length of the data, including
+     *                      NULL byte if NULL terminated string is required.
+     * @return              Number of SI_CHAR required by the string when
+     *                      converted. If there are embedded NULL bytes in the
+     *                      input data, only the string up and not including
+     *                      the NULL byte will be converted.
+     * @return              -1 cast to size_t on a conversion error.
+     */
+    size_t SizeFromStore(
+        const char *    a_pInputData,
+        size_t          a_uInputDataLen)
+    {
+        (void)a_pInputData;
+        SI_ASSERT(a_uInputDataLen != (size_t) -1);
+
+        // ASCII/MBCS/UTF-8 needs no conversion
+        return a_uInputDataLen;
+    }
+
+    /** Convert the input string from the storage format to SI_CHAR.
+     * The storage format is always UTF-8 or MBCS.
+     *
+     * @param a_pInputData  Data in storage format to be converted to SI_CHAR.
+     * @param a_uInputDataLen Length of storage format data in bytes. This
+     *                      must be the actual length of the data, including
+     *                      NULL byte if NULL terminated string is required.
+     * @param a_pOutputData Pointer to the output buffer to received the
+     *                      converted data.
+     * @param a_uOutputDataSize Size of the output buffer in SI_CHAR.
+     * @return              true if all of the input data was successfully
+     *                      converted.
+     */
+    bool ConvertFromStore(
+        const char *    a_pInputData,
+        size_t          a_uInputDataLen,
+        SI_CHAR *       a_pOutputData,
+        size_t          a_uOutputDataSize)
+    {
+        // ASCII/MBCS/UTF-8 needs no conversion
+        if (a_uInputDataLen > a_uOutputDataSize) {
+            return false;
+        }
+        memcpy(a_pOutputData, a_pInputData, a_uInputDataLen);
+        return true;
+    }
+
+    /** Calculate the number of char required by the storage format of this
+     * data. The storage format is always UTF-8 or MBCS.
+     *
+     * @param a_pInputData  NULL terminated string to calculate the number of
+     *                      bytes required to be converted to storage format.
+     * @return              Number of bytes required by the string when
+     *                      converted to storage format. This size always
+     *                      includes space for the terminating NULL character.
+     * @return              -1 cast to size_t on a conversion error.
+     */
+    size_t SizeToStore(
+        const SI_CHAR * a_pInputData)
+    {
+        // ASCII/MBCS/UTF-8 needs no conversion
+        return strlen((const char *)a_pInputData) + 1;
+    }
+
+    /** Convert the input string to the storage format of this data.
+     * The storage format is always UTF-8 or MBCS.
+     *
+     * @param a_pInputData  NULL terminated source string to convert. All of
+     *                      the data will be converted including the
+     *                      terminating NULL character.
+     * @param a_pOutputData Pointer to the buffer to receive the converted
+     *                      string.
+     * @param a_uOutputDataSize Size of the output buffer in char.
+     * @return              true if all of the input data, including the
+     *                      terminating NULL character was successfully
+     *                      converted.
+     */
+    bool ConvertToStore(
+        const SI_CHAR * a_pInputData,
+        char *          a_pOutputData,
+        size_t          a_uOutputDataSize)
+    {
+        // calc input string length (SI_CHAR type and size independent)
+        size_t uInputLen = strlen((const char *)a_pInputData) + 1;
+        if (uInputLen > a_uOutputDataSize) {
+            return false;
+        }
+
+        // ascii/UTF-8 needs no conversion
+        memcpy(a_pOutputData, a_pInputData, uInputLen);
+        return true;
+    }
+};
+
+
+// ---------------------------------------------------------------------------
+//                              SI_CONVERT_GENERIC
+// ---------------------------------------------------------------------------
+#ifdef SI_CONVERT_GENERIC
+
+#define SI_Case     SI_GenericCase
+#define SI_NoCase   SI_GenericNoCase
+
+#include <wchar.h>
+#include "ConvertUTF.h"
+
+/**
+ * Converts UTF-8 to a wchar_t (or equivalent) using the Unicode reference
+ * library functions. This can be used on all platforms.
+ */
+template<class SI_CHAR>
+class SI_ConvertW {
+    bool m_bStoreIsUtf8;
+protected:
+    SI_ConvertW() { }
+public:
+    SI_ConvertW(bool a_bStoreIsUtf8) : m_bStoreIsUtf8(a_bStoreIsUtf8) { }
+
+    /* copy and assignment */
+    SI_ConvertW(const SI_ConvertW & rhs) { operator=(rhs); }
+    SI_ConvertW & operator=(const SI_ConvertW & rhs) {
+        m_bStoreIsUtf8 = rhs.m_bStoreIsUtf8;
+        return *this;
+    }
+
+    /** Calculate the number of SI_CHAR required for converting the input
+     * from the storage format. The storage format is always UTF-8 or MBCS.
+     *
+     * @param a_pInputData  Data in storage format to be converted to SI_CHAR.
+     * @param a_uInputDataLen Length of storage format data in bytes. This
+     *                      must be the actual length of the data, including
+     *                      NULL byte if NULL terminated string is required.
+     * @return              Number of SI_CHAR required by the string when
+     *                      converted. If there are embedded NULL bytes in the
+     *                      input data, only the string up and not including
+     *                      the NULL byte will be converted.
+     * @return              -1 cast to size_t on a conversion error.
+     */
+    size_t SizeFromStore(
+        const char *    a_pInputData,
+        size_t          a_uInputDataLen)
+    {
+        SI_ASSERT(a_uInputDataLen != (size_t) -1);
+
+        if (m_bStoreIsUtf8) {
+            // worst case scenario for UTF-8 to wchar_t is 1 char -> 1 wchar_t
+            // so we just return the same number of characters required as for
+            // the source text.
+            return a_uInputDataLen;
+        }
+
+#if defined(SI_NO_MBSTOWCS_NULL) || (!defined(_MSC_VER) && !defined(_linux))
+        // fall back processing for platforms that don't support a NULL dest to mbstowcs
+        // worst case scenario is 1:1, this will be a sufficient buffer size
+        (void)a_pInputData;
+        return a_uInputDataLen;
+#else
+        // get the actual required buffer size
+        return mbstowcs(nullptr, a_pInputData, a_uInputDataLen);
+#endif
+    }
+
+    /** Convert the input string from the storage format to SI_CHAR.
+     * The storage format is always UTF-8 or MBCS.
+     *
+     * @param a_pInputData  Data in storage format to be converted to SI_CHAR.
+     * @param a_uInputDataLen Length of storage format data in bytes. This
+     *                       must be the actual length of the data, including
+     *                       NULL byte if NULL terminated string is required.
+     * @param a_pOutputData Pointer to the output buffer to received the
+     *                       converted data.
+     * @param a_uOutputDataSize Size of the output buffer in SI_CHAR.
+     * @return              true if all of the input data was successfully
+     *                       converted.
+     */
+    bool ConvertFromStore(
+        const char *    a_pInputData,
+        size_t          a_uInputDataLen,
+        SI_CHAR *       a_pOutputData,
+        size_t          a_uOutputDataSize)
+    {
+        if (m_bStoreIsUtf8) {
+            // This uses the Unicode reference implementation to do the
+            // conversion from UTF-8 to wchar_t. The required files are
+            // ConvertUTF.h and ConvertUTF.c which should be included in
+            // the distribution but are publically available from unicode.org
+            // at http://www.unicode.org/Public/PROGRAMS/CVTUTF/
+            ConversionResult retval;
+            const UTF8 * pUtf8 = (const UTF8 *) a_pInputData;
+            if (sizeof(wchar_t) == sizeof(UTF32)) {
+                UTF32 * pUtf32 = (UTF32 *) a_pOutputData;
+                retval = ConvertUTF8toUTF32(
+                    &pUtf8, pUtf8 + a_uInputDataLen,
+                    &pUtf32, pUtf32 + a_uOutputDataSize,
+                    lenientConversion);
+            }
+            else if (sizeof(wchar_t) == sizeof(UTF16)) {
+                UTF16 * pUtf16 = (UTF16 *) a_pOutputData;
+                retval = ConvertUTF8toUTF16(
+                    &pUtf8, pUtf8 + a_uInputDataLen,
+                    &pUtf16, pUtf16 + a_uOutputDataSize,
+                    lenientConversion);
+            }
+            return retval == conversionOK;
+        }
+
+        // convert to wchar_t
+        size_t retval = mbstowcs(a_pOutputData,
+            a_pInputData, a_uOutputDataSize);
+        return retval != (size_t)(-1);
+    }
+
+    /** Calculate the number of char required by the storage format of this
+     * data. The storage format is always UTF-8 or MBCS.
+     *
+     * @param a_pInputData  NULL terminated string to calculate the number of
+     *                       bytes required to be converted to storage format.
+     * @return              Number of bytes required by the string when
+     *                       converted to storage format. This size always
+     *                       includes space for the terminating NULL character.
+     * @return              -1 cast to size_t on a conversion error.
+     */
+    size_t SizeToStore(
+        const SI_CHAR * a_pInputData)
+    {
+        if (m_bStoreIsUtf8) {
+            // worst case scenario for wchar_t to UTF-8 is 1 wchar_t -> 6 char
+            size_t uLen = 0;
+            while (a_pInputData[uLen]) {
+                ++uLen;
+            }
+            return (6 * uLen) + 1;
+        }
+        else {
+            size_t uLen = wcstombs(nullptr, a_pInputData, 0);
+            if (uLen == (size_t)(-1)) {
+                return uLen;
+            }
+            return uLen + 1; // include NULL terminator
+        }
+    }
+
+    /** Convert the input string to the storage format of this data.
+     * The storage format is always UTF-8 or MBCS.
+     *
+     * @param a_pInputData  NULL terminated source string to convert. All of
+     *                       the data will be converted including the
+     *                       terminating NULL character.
+     * @param a_pOutputData Pointer to the buffer to receive the converted
+     *                       string.
+     * @param a_uOutputDataSize Size of the output buffer in char.
+     * @return              true if all of the input data, including the
+     *                       terminating NULL character was successfully
+     *                       converted.
+     */
+    bool ConvertToStore(
+        const SI_CHAR * a_pInputData,
+        char *          a_pOutputData,
+        size_t          a_uOutputDataSize
+        )
+    {
+        if (m_bStoreIsUtf8) {
+            // calc input string length (SI_CHAR type and size independent)
+            size_t uInputLen = 0;
+            while (a_pInputData[uInputLen]) {
+                ++uInputLen;
+            }
+            ++uInputLen; // include the NULL char
+
+            // This uses the Unicode reference implementation to do the
+            // conversion from wchar_t to UTF-8. The required files are
+            // ConvertUTF.h and ConvertUTF.c which should be included in
+            // the distribution but are publically available from unicode.org
+            // at http://www.unicode.org/Public/PROGRAMS/CVTUTF/
+            ConversionResult retval;
+            UTF8 * pUtf8 = (UTF8 *) a_pOutputData;
+            if (sizeof(wchar_t) == sizeof(UTF32)) {
+                const UTF32 * pUtf32 = (const UTF32 *) a_pInputData;
+                retval = ConvertUTF32toUTF8(
+                    &pUtf32, pUtf32 + uInputLen,
+                    &pUtf8, pUtf8 + a_uOutputDataSize,
+                    lenientConversion);
+            }
+            else if (sizeof(wchar_t) == sizeof(UTF16)) {
+                const UTF16 * pUtf16 = (const UTF16 *) a_pInputData;
+                retval = ConvertUTF16toUTF8(
+                    &pUtf16, pUtf16 + uInputLen,
+                    &pUtf8, pUtf8 + a_uOutputDataSize,
+                    lenientConversion);
+            }
+            return retval == conversionOK;
+        }
+        else {
+            size_t retval = wcstombs(a_pOutputData,
+                a_pInputData, a_uOutputDataSize);
+            return retval != (size_t) -1;
+        }
+    }
+};
+
+#endif // SI_CONVERT_GENERIC
+
+
+// ---------------------------------------------------------------------------
+//                              SI_CONVERT_ICU
+// ---------------------------------------------------------------------------
+#ifdef SI_CONVERT_ICU
+
+#define SI_Case     SI_GenericCase
+#define SI_NoCase   SI_GenericNoCase
+
+#include <unicode/ucnv.h>
+
+/**
+ * Converts MBCS/UTF-8 to UChar using ICU. This can be used on all platforms.
+ */
+template<class SI_CHAR>
+class SI_ConvertW {
+    const char * m_pEncoding;
+    UConverter * m_pConverter;
+protected:
+    SI_ConvertW() : m_pEncoding(nullptr), m_pConverter(nullptr) { }
+public:
+    SI_ConvertW(bool a_bStoreIsUtf8) : m_pConverter(nullptr) {
+        m_pEncoding = a_bStoreIsUtf8 ? "UTF-8" : nullptr;
+    }
+
+    /* copy and assignment */
+    SI_ConvertW(const SI_ConvertW & rhs) { operator=(rhs); }
+    SI_ConvertW & operator=(const SI_ConvertW & rhs) {
+        m_pEncoding = rhs.m_pEncoding;
+        m_pConverter = nullptr;
+        return *this;
+    }
+    ~SI_ConvertW() { if (m_pConverter) ucnv_close(m_pConverter); }
+
+    /** Calculate the number of UChar required for converting the input
+     * from the storage format. The storage format is always UTF-8 or MBCS.
+     *
+     * @param a_pInputData  Data in storage format to be converted to UChar.
+     * @param a_uInputDataLen Length of storage format data in bytes. This
+     *                      must be the actual length of the data, including
+     *                      NULL byte if NULL terminated string is required.
+     * @return              Number of UChar required by the string when
+     *                      converted. If there are embedded NULL bytes in the
+     *                      input data, only the string up and not including
+     *                      the NULL byte will be converted.
+     * @return              -1 cast to size_t on a conversion error.
+     */
+    size_t SizeFromStore(
+        const char *    a_pInputData,
+        size_t          a_uInputDataLen)
+    {
+        SI_ASSERT(a_uInputDataLen != (size_t) -1);
+
+        UErrorCode nError;
+
+        if (!m_pConverter) {
+            nError = U_ZERO_ERROR;
+            m_pConverter = ucnv_open(m_pEncoding, &nError);
+            if (U_FAILURE(nError)) {
+                return (size_t) -1;
+            }
+        }
+
+        nError = U_ZERO_ERROR;
+        int32_t nLen = ucnv_toUChars(m_pConverter, nullptr, 0,
+            a_pInputData, (int32_t) a_uInputDataLen, &nError);
+        if (U_FAILURE(nError) && nError != U_BUFFER_OVERFLOW_ERROR) {
+            return (size_t) -1;
+        }
+
+        return (size_t) nLen;
+    }
+
+    /** Convert the input string from the storage format to UChar.
+     * The storage format is always UTF-8 or MBCS.
+     *
+     * @param a_pInputData  Data in storage format to be converted to UChar.
+     * @param a_uInputDataLen Length of storage format data in bytes. This
+     *                      must be the actual length of the data, including
+     *                      NULL byte if NULL terminated string is required.
+     * @param a_pOutputData Pointer to the output buffer to received the
+     *                      converted data.
+     * @param a_uOutputDataSize Size of the output buffer in UChar.
+     * @return              true if all of the input data was successfully
+     *                      converted.
+     */
+    bool ConvertFromStore(
+        const char *    a_pInputData,
+        size_t          a_uInputDataLen,
+        UChar *         a_pOutputData,
+        size_t          a_uOutputDataSize)
+    {
+        UErrorCode nError;
+
+        if (!m_pConverter) {
+            nError = U_ZERO_ERROR;
+            m_pConverter = ucnv_open(m_pEncoding, &nError);
+            if (U_FAILURE(nError)) {
+                return false;
+            }
+        }
+
+        nError = U_ZERO_ERROR;
+        ucnv_toUChars(m_pConverter,
+            a_pOutputData, (int32_t) a_uOutputDataSize,
+            a_pInputData, (int32_t) a_uInputDataLen, &nError);
+        if (U_FAILURE(nError)) {
+            return false;
+        }
+
+        return true;
+    }
+
+    /** Calculate the number of char required by the storage format of this
+     * data. The storage format is always UTF-8 or MBCS.
+     *
+     * @param a_pInputData  NULL terminated string to calculate the number of
+     *                      bytes required to be converted to storage format.
+     * @return              Number of bytes required by the string when
+     *                      converted to storage format. This size always
+     *                      includes space for the terminating NULL character.
+     * @return              -1 cast to size_t on a conversion error.
+     */
+    size_t SizeToStore(
+        const UChar * a_pInputData)
+    {
+        UErrorCode nError;
+
+        if (!m_pConverter) {
+            nError = U_ZERO_ERROR;
+            m_pConverter = ucnv_open(m_pEncoding, &nError);
+            if (U_FAILURE(nError)) {
+                return (size_t) -1;
+            }
+        }
+
+        nError = U_ZERO_ERROR;
+        int32_t nLen = ucnv_fromUChars(m_pConverter, nullptr, 0,
+            a_pInputData, -1, &nError);
+        if (U_FAILURE(nError) && nError != U_BUFFER_OVERFLOW_ERROR) {
+            return (size_t) -1;
+        }
+
+        return (size_t) nLen + 1;
+    }
+
+    /** Convert the input string to the storage format of this data.
+     * The storage format is always UTF-8 or MBCS.
+     *
+     * @param a_pInputData  NULL terminated source string to convert. All of
+     *                      the data will be converted including the
+     *                      terminating NULL character.
+     * @param a_pOutputData Pointer to the buffer to receive the converted
+     *                      string.
+     * @param a_pOutputDataSize Size of the output buffer in char.
+     * @return              true if all of the input data, including the
+     *                      terminating NULL character was successfully
+     *                      converted.
+     */
+    bool ConvertToStore(
+        const UChar *   a_pInputData,
+        char *          a_pOutputData,
+        size_t          a_uOutputDataSize)
+    {
+        UErrorCode nError;
+
+        if (!m_pConverter) {
+            nError = U_ZERO_ERROR;
+            m_pConverter = ucnv_open(m_pEncoding, &nError);
+            if (U_FAILURE(nError)) {
+                return false;
+            }
+        }
+
+        nError = U_ZERO_ERROR;
+        ucnv_fromUChars(m_pConverter,
+            a_pOutputData, (int32_t) a_uOutputDataSize,
+            a_pInputData, -1, &nError);
+        if (U_FAILURE(nError)) {
+            return false;
+        }
+
+        return true;
+    }
+};
+
+#endif // SI_CONVERT_ICU
+
+
+// ---------------------------------------------------------------------------
+//                              SI_CONVERT_WIN32
+// ---------------------------------------------------------------------------
+#ifdef SI_CONVERT_WIN32
+
+#define SI_Case     SI_GenericCase
+
+// Windows CE doesn't have errno or MBCS libraries
+#ifdef _WIN32_WCE
+# ifndef SI_NO_MBCS
+#  define SI_NO_MBCS
+# endif
+#endif
+
+#include <windows.h>
+#ifdef SI_NO_MBCS
+# define SI_NoCase   SI_GenericNoCase
+#else // !SI_NO_MBCS
+/**
+ * Case-insensitive comparison class using Win32 MBCS functions. This class
+ * returns a case-insensitive semi-collation order for MBCS text. It may not
+ * be safe for UTF-8 text returned in char format as we don't know what
+ * characters will be folded by the function! Therefore, if you are using
+ * SI_CHAR == char and SetUnicode(true), then you need to use the generic
+ * SI_NoCase class instead.
+ */
+#include <mbstring.h>
+template<class SI_CHAR>
+struct SI_NoCase {
+    bool operator()(const SI_CHAR * pLeft, const SI_CHAR * pRight) const {
+        if (sizeof(SI_CHAR) == sizeof(char)) {
+            return _mbsicmp((const unsigned char *)pLeft,
+                (const unsigned char *)pRight) < 0;
+        }
+        if (sizeof(SI_CHAR) == sizeof(wchar_t)) {
+            return _wcsicmp((const wchar_t *)pLeft,
+                (const wchar_t *)pRight) < 0;
+        }
+        return SI_GenericNoCase<SI_CHAR>()(pLeft, pRight);
+    }
+};
+#endif // SI_NO_MBCS
+
+/**
+ * Converts MBCS and UTF-8 to a wchar_t (or equivalent) on Windows. This uses
+ * only the Win32 functions and doesn't require the external Unicode UTF-8
+ * conversion library. It will not work on Windows 95 without using Microsoft
+ * Layer for Unicode in your application.
+ */
+template<class SI_CHAR>
+class SI_ConvertW {
+    UINT m_uCodePage;
+protected:
+    SI_ConvertW() { }
+public:
+    SI_ConvertW(bool a_bStoreIsUtf8) {
+        m_uCodePage = a_bStoreIsUtf8 ? CP_UTF8 : CP_ACP;
+    }
+
+    /* copy and assignment */
+    SI_ConvertW(const SI_ConvertW & rhs) { operator=(rhs); }
+    SI_ConvertW & operator=(const SI_ConvertW & rhs) {
+        m_uCodePage = rhs.m_uCodePage;
+        return *this;
+    }
+
+    /** Calculate the number of SI_CHAR required for converting the input
+     * from the storage format. The storage format is always UTF-8 or MBCS.
+     *
+     * @param a_pInputData  Data in storage format to be converted to SI_CHAR.
+     * @param a_uInputDataLen Length of storage format data in bytes. This
+     *                      must be the actual length of the data, including
+     *                      NULL byte if NULL terminated string is required.
+     * @return              Number of SI_CHAR required by the string when
+     *                      converted. If there are embedded NULL bytes in the
+     *                      input data, only the string up and not including
+     *                      the NULL byte will be converted.
+     * @return              -1 cast to size_t on a conversion error.
+     */
+    size_t SizeFromStore(
+        const char *    a_pInputData,
+        size_t          a_uInputDataLen)
+    {
+        SI_ASSERT(a_uInputDataLen != (size_t) -1);
+
+        int retval = MultiByteToWideChar(
+            m_uCodePage, 0,
+            a_pInputData, (int) a_uInputDataLen,
+            0, 0);
+        return (size_t)(retval > 0 ? retval : -1);
+    }
+
+    /** Convert the input string from the storage format to SI_CHAR.
+     * The storage format is always UTF-8 or MBCS.
+     *
+     * @param a_pInputData  Data in storage format to be converted to SI_CHAR.
+     * @param a_uInputDataLen Length of storage format data in bytes. This
+     *                      must be the actual length of the data, including
+     *                      NULL byte if NULL terminated string is required.
+     * @param a_pOutputData Pointer to the output buffer to received the
+     *                      converted data.
+     * @param a_uOutputDataSize Size of the output buffer in SI_CHAR.
+     * @return              true if all of the input data was successfully
+     *                      converted.
+     */
+    bool ConvertFromStore(
+        const char *    a_pInputData,
+        size_t          a_uInputDataLen,
+        SI_CHAR *       a_pOutputData,
+        size_t          a_uOutputDataSize)
+    {
+        int nSize = MultiByteToWideChar(
+            m_uCodePage, 0,
+            a_pInputData, (int) a_uInputDataLen,
+            (wchar_t *) a_pOutputData, (int) a_uOutputDataSize);
+        return (nSize > 0);
+    }
+
+    /** Calculate the number of char required by the storage format of this
+     * data. The storage format is always UTF-8.
+     *
+     * @param a_pInputData  NULL terminated string to calculate the number of
+     *                      bytes required to be converted to storage format.
+     * @return              Number of bytes required by the string when
+     *                      converted to storage format. This size always
+     *                      includes space for the terminating NULL character.
+     * @return              -1 cast to size_t on a conversion error.
+     */
+    size_t SizeToStore(
+        const SI_CHAR * a_pInputData)
+    {
+        int retval = WideCharToMultiByte(
+            m_uCodePage, 0,
+            (const wchar_t *) a_pInputData, -1,
+            0, 0, 0, 0);
+        return (size_t) (retval > 0 ? retval : -1);
+    }
+
+    /** Convert the input string to the storage format of this data.
+     * The storage format is always UTF-8 or MBCS.
+     *
+     * @param a_pInputData  NULL terminated source string to convert. All of
+     *                      the data will be converted including the
+     *                      terminating NULL character.
+     * @param a_pOutputData Pointer to the buffer to receive the converted
+     *                      string.
+     * @param a_pOutputDataSize Size of the output buffer in char.
+     * @return              true if all of the input data, including the
+     *                      terminating NULL character was successfully
+     *                      converted.
+     */
+    bool ConvertToStore(
+        const SI_CHAR * a_pInputData,
+        char *          a_pOutputData,
+        size_t          a_uOutputDataSize)
+    {
+        int retval = WideCharToMultiByte(
+            m_uCodePage, 0,
+            (const wchar_t *) a_pInputData, -1,
+            a_pOutputData, (int) a_uOutputDataSize, 0, 0);
+        return retval > 0;
+    }
+};
+
+#endif // SI_CONVERT_WIN32
+
+
+// ---------------------------------------------------------------------------
+//                                  TYPE DEFINITIONS
+// ---------------------------------------------------------------------------
+
+typedef CSimpleIniTempl<char,
+    SI_NoCase<char>,SI_ConvertA<char> >                 CSimpleIniA;
+typedef CSimpleIniTempl<char,
+    SI_Case<char>,SI_ConvertA<char> >                   CSimpleIniCaseA;
+
+#if defined(SI_NO_CONVERSION)
+// if there is no wide char conversion then we don't need to define the 
+// widechar "W" versions of CSimpleIni
+# define CSimpleIni      CSimpleIniA
+# define CSimpleIniCase  CSimpleIniCaseA
+# define SI_NEWLINE      SI_NEWLINE_A
+#else
+# if defined(SI_CONVERT_ICU)
+typedef CSimpleIniTempl<UChar,
+    SI_NoCase<UChar>,SI_ConvertW<UChar> >               CSimpleIniW;
+typedef CSimpleIniTempl<UChar,
+    SI_Case<UChar>,SI_ConvertW<UChar> >                 CSimpleIniCaseW;
+# else
+typedef CSimpleIniTempl<wchar_t,
+    SI_NoCase<wchar_t>,SI_ConvertW<wchar_t> >           CSimpleIniW;
+typedef CSimpleIniTempl<wchar_t,
+    SI_Case<wchar_t>,SI_ConvertW<wchar_t> >             CSimpleIniCaseW;
+# endif
+
+# ifdef _UNICODE
+#  define CSimpleIni      CSimpleIniW
+#  define CSimpleIniCase  CSimpleIniCaseW
+#  define SI_NEWLINE      SI_NEWLINE_W
+# else // !_UNICODE 
+#  define CSimpleIni      CSimpleIniA
+#  define CSimpleIniCase  CSimpleIniCaseA
+#  define SI_NEWLINE      SI_NEWLINE_A
+# endif // _UNICODE
+#endif
+
+#ifdef _MSC_VER
+# pragma warning (pop)
+#endif
+
+#endif // INCLUDED_SimpleIni_h
+

+ 4 - 0
admin/win/tools/NCToolsShared/CMakeLists.txt

@@ -0,0 +1,4 @@
+add_library(NCToolsShared STATIC
+    utility_win.cpp
+    SimpleNamedMutex.cpp
+)

+ 48 - 0
admin/win/tools/NCToolsShared/SimpleNamedMutex.cpp

@@ -0,0 +1,48 @@
+/*
+ * Copyright (C) by Michael Schuster <michael.schuster@nextcloud.com>
+ *
+ * This program 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.
+ *
+ * This program 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.
+ */
+
+#include "SimpleNamedMutex.h"
+
+SimpleNamedMutex::SimpleNamedMutex(const std::wstring &name)
+{
+    _name = name;
+}
+
+bool SimpleNamedMutex::lock()
+{
+    if (_name.empty() || _hMutex) {
+        return false;
+    }
+
+    // Mutex
+    _hMutex = CreateMutex(nullptr, TRUE, _name.data());
+
+    if (GetLastError() == ERROR_ALREADY_EXISTS) {
+        CloseHandle(_hMutex);
+        _hMutex = nullptr;
+        return false;
+    }
+
+    return true;
+}
+
+void SimpleNamedMutex::unlock()
+{
+    // Release mutex
+    if (_hMutex) {
+        ReleaseMutex(_hMutex);
+        CloseHandle(_hMutex);
+        _hMutex = nullptr;
+    }
+}

+ 31 - 0
admin/win/tools/NCToolsShared/SimpleNamedMutex.h

@@ -0,0 +1,31 @@
+/*
+ * Copyright (C) by Michael Schuster <michael.schuster@nextcloud.com>
+ *
+ * This program 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.
+ *
+ * This program 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.
+ */
+
+#pragma once
+
+#include <windows.h>
+#include <string>
+
+class SimpleNamedMutex
+{
+public:
+    SimpleNamedMutex(const std::wstring &name);
+
+    bool lock();
+    void unlock();
+
+private:
+    std::wstring _name;
+    HANDLE _hMutex = nullptr;
+};

+ 58 - 0
admin/win/tools/NCToolsShared/utility.h

@@ -0,0 +1,58 @@
+/*
+ * Copyright (C) by Klaas Freitag <freitag@owncloud.com>
+ * Copyright (C) by Daniel Molkentin <danimo@owncloud.com>
+ * Copyright (C) by Michael Schuster <michael.schuster@nextcloud.com>
+ *
+ * This library is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 2.1 of the License, or (at your option) any later version.
+ *
+ * This library 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
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with this library; if not, write to the Free Software
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
+ */
+
+#pragma once
+
+#include <windows.h>
+#include <string>
+#include <vector>
+#include <variant>
+#include <functional>
+
+namespace NCTools {
+
+typedef std::variant<int, std::wstring, std::vector<unsigned char>> registryVariant;
+
+static const std::wstring PathSeparator = L"\\";
+
+namespace Utility {
+    // Ported from libsync
+    registryVariant registryGetKeyValue(HKEY hRootKey, const std::wstring& subKey, const std::wstring& valueName);
+    bool registrySetKeyValue(HKEY hRootKey, const std::wstring &subKey, const std::wstring &valueName, DWORD type, const registryVariant &value);
+    bool registryDeleteKeyTree(HKEY hRootKey, const std::wstring &subKey);
+    bool registryDeleteKeyValue(HKEY hRootKey, const std::wstring &subKey, const std::wstring &valueName);
+    bool registryWalkSubKeys(HKEY hRootKey, const std::wstring &subKey, const std::function<void(HKEY, const std::wstring&)> &callback);
+
+    // Ported from gui, modified to optionally rename matching files
+    typedef std::function<void(const std::wstring&, std::wstring&)> copy_dir_recursive_callback;
+    bool copy_dir_recursive(std::wstring from_dir, std::wstring to_dir, copy_dir_recursive_callback* callbackFileNameMatchReplace = nullptr);
+
+    // Created for native Win32
+    DWORD execCmd(std::wstring cmd, bool wait = true);
+    bool killProcess(const std::wstring &exePath);
+    bool isValidDirectory(const std::wstring &path);
+    std::wstring getAppRegistryString(const std::wstring &appVendor, const std::wstring &appName, const std::wstring &valueName);
+    std::wstring getAppPath(const std::wstring &appVendor, const std::wstring &appName);
+    std::wstring getConfigPath(const std::wstring &appName);
+    void waitForNsisUninstaller(const std::wstring& appShortName);
+    void removeNavigationPaneEntries(const std::wstring &appName);
+}
+
+} // namespace NCTools

+ 477 - 0
admin/win/tools/NCToolsShared/utility_win.cpp

@@ -0,0 +1,477 @@
+/*
+ * Copyright (C) by Daniel Molkentin <danimo@owncloud.com>
+ * Copyright (C) by Michael Schuster <michael.schuster@nextcloud.com>
+ *
+ * This library is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 2.1 of the License, or (at your option) any later version.
+ *
+ * This library 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
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with this library; if not, write to the Free Software
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
+ */
+
+#include "utility.h"
+#include <cassert>
+#include <algorithm>
+#include <Shlobj.h>
+#include <psapi.h>
+
+#define ASSERT assert
+#define Q_ASSERT assert
+
+namespace NCTools {
+
+// Ported from libsync
+
+registryVariant Utility::registryGetKeyValue(HKEY hRootKey, const std::wstring &subKey, const std::wstring &valueName)
+{
+    registryVariant value;
+
+    HKEY hKey;
+
+    REGSAM sam = KEY_READ | KEY_WOW64_64KEY;
+    LONG result = RegOpenKeyEx(hRootKey, reinterpret_cast<LPCWSTR>(subKey.data()), 0, sam, &hKey);
+    ASSERT(result == ERROR_SUCCESS || result == ERROR_FILE_NOT_FOUND);
+    if (result != ERROR_SUCCESS)
+        return value;
+
+    DWORD type = 0, sizeInBytes = 0;
+    result = RegQueryValueEx(hKey, reinterpret_cast<LPCWSTR>(valueName.data()), 0, &type, nullptr, &sizeInBytes);
+    ASSERT(result == ERROR_SUCCESS || result == ERROR_FILE_NOT_FOUND);
+    if (result == ERROR_SUCCESS) {
+        switch (type) {
+        case REG_DWORD:
+            DWORD dword;
+            Q_ASSERT(sizeInBytes == sizeof(dword));
+            if (RegQueryValueEx(hKey, reinterpret_cast<LPCWSTR>(valueName.data()), 0, &type, reinterpret_cast<LPBYTE>(&dword), &sizeInBytes) == ERROR_SUCCESS) {
+                value = int(dword);
+            }
+            break;
+        case REG_EXPAND_SZ:
+        case REG_SZ: {
+            std::wstring string;
+            string.resize(sizeInBytes / sizeof(wchar_t));
+            result = RegQueryValueEx(hKey, reinterpret_cast<LPCWSTR>(valueName.data()), 0, &type, reinterpret_cast<LPBYTE>(string.data()), &sizeInBytes);
+
+            if (result == ERROR_SUCCESS) {
+                int newCharSize = sizeInBytes / sizeof(wchar_t);
+                // From the doc:
+                // If the data has the REG_SZ, REG_MULTI_SZ or REG_EXPAND_SZ type, the string may not have been stored with
+                // the proper terminating null characters. Therefore, even if the function returns ERROR_SUCCESS,
+                // the application should ensure that the string is properly terminated before using it; otherwise, it may overwrite a buffer.
+                if (string.at(newCharSize - 1) == wchar_t('\0'))
+                    string.resize(newCharSize - 1);
+                value = string;
+            }
+            break;
+        }
+        case REG_BINARY: {
+            std::vector<unsigned char> buffer;
+            buffer.resize(sizeInBytes);
+            result = RegQueryValueEx(hKey, reinterpret_cast<LPCWSTR>(valueName.data()), 0, &type, reinterpret_cast<LPBYTE>(buffer.data()), &sizeInBytes);
+            if (result == ERROR_SUCCESS) {
+                value = buffer.at(12);
+            }
+            break;
+        }
+        default:
+            break;// Q_UNREACHABLE();
+        }
+    }
+    ASSERT(result == ERROR_SUCCESS || result == ERROR_FILE_NOT_FOUND);
+
+    RegCloseKey(hKey);
+    return value;
+}
+
+bool Utility::registrySetKeyValue(HKEY hRootKey, const std::wstring &subKey, const std::wstring &valueName, DWORD type, const registryVariant &value)
+{
+    HKEY hKey;
+    // KEY_WOW64_64KEY is necessary because CLSIDs are "Redirected and reflected only for CLSIDs that do not specify InprocServer32 or InprocHandler32."
+    // https://msdn.microsoft.com/en-us/library/windows/desktop/aa384253%28v=vs.85%29.aspx#redirected__shared__and_reflected_keys_under_wow64
+    // This shouldn't be an issue in our case since we use shell32.dll as InprocServer32, so we could write those registry keys for both 32 and 64bit.
+    // FIXME: Not doing so at the moment means that explorer will show the cloud provider, but 32bit processes' open dialogs (like the ownCloud client itself) won't show it.
+    REGSAM sam = KEY_WRITE | KEY_WOW64_64KEY;
+    LONG result = RegCreateKeyEx(hRootKey, reinterpret_cast<LPCWSTR>(subKey.data()), 0, nullptr, 0, sam, nullptr, &hKey, nullptr);
+    ASSERT(result == ERROR_SUCCESS);
+    if (result != ERROR_SUCCESS)
+        return false;
+
+    result = -1;
+    switch (type) {
+    case REG_DWORD: {
+        try {
+            DWORD dword = std::get<int>(value);
+            result = RegSetValueEx(hKey, reinterpret_cast<LPCWSTR>(valueName.data()), 0, type, reinterpret_cast<const BYTE *>(&dword), sizeof(dword));
+        }
+        catch (const std::bad_variant_access&) {}
+        break;
+    }
+    case REG_EXPAND_SZ:
+    case REG_SZ: {
+        try {
+            std::wstring string = std::get<std::wstring>(value);
+            result = RegSetValueEx(hKey, reinterpret_cast<LPCWSTR>(valueName.data()), 0, type, reinterpret_cast<const BYTE *>(string.data()), static_cast<DWORD>((string.size() + 1) * sizeof(wchar_t)));
+        }
+        catch (const std::bad_variant_access&) {}
+        break;
+    }
+    default:
+        break;// Q_UNREACHABLE();
+    }
+    ASSERT(result == ERROR_SUCCESS);
+
+    RegCloseKey(hKey);
+    return result == ERROR_SUCCESS;
+}
+
+bool Utility::registryDeleteKeyTree(HKEY hRootKey, const std::wstring &subKey)
+{
+    HKEY hKey;
+    REGSAM sam = DELETE | KEY_ENUMERATE_SUB_KEYS | KEY_QUERY_VALUE | KEY_SET_VALUE | KEY_WOW64_64KEY;
+    LONG result = RegOpenKeyEx(hRootKey, reinterpret_cast<LPCWSTR>(subKey.data()), 0, sam, &hKey);
+    ASSERT(result == ERROR_SUCCESS);
+    if (result != ERROR_SUCCESS)
+        return false;
+
+    result = RegDeleteTree(hKey, nullptr);
+    RegCloseKey(hKey);
+    ASSERT(result == ERROR_SUCCESS);
+
+    result |= RegDeleteKeyEx(hRootKey, reinterpret_cast<LPCWSTR>(subKey.data()), sam, 0);
+    ASSERT(result == ERROR_SUCCESS);
+
+    return result == ERROR_SUCCESS;
+}
+
+bool Utility::registryDeleteKeyValue(HKEY hRootKey, const std::wstring &subKey, const std::wstring &valueName)
+{
+    HKEY hKey;
+    REGSAM sam = KEY_WRITE | KEY_WOW64_64KEY;
+    LONG result = RegOpenKeyEx(hRootKey, reinterpret_cast<LPCWSTR>(subKey.data()), 0, sam, &hKey);
+    ASSERT(result == ERROR_SUCCESS);
+    if (result != ERROR_SUCCESS)
+        return false;
+
+    result = RegDeleteValue(hKey, reinterpret_cast<LPCWSTR>(valueName.data()));
+    ASSERT(result == ERROR_SUCCESS);
+
+    RegCloseKey(hKey);
+    return result == ERROR_SUCCESS;
+}
+
+bool Utility::registryWalkSubKeys(HKEY hRootKey, const std::wstring &subKey, const std::function<void(HKEY, const std::wstring&)> &callback)
+{
+    HKEY hKey;
+    REGSAM sam = KEY_READ | KEY_WOW64_64KEY;
+    LONG result = RegOpenKeyEx(hRootKey, reinterpret_cast<LPCWSTR>(subKey.data()), 0, sam, &hKey);
+    ASSERT(result == ERROR_SUCCESS);
+    if (result != ERROR_SUCCESS)
+        return false;
+
+    DWORD maxSubKeyNameSize;
+    // Get the largest keyname size once instead of relying each call on ERROR_MORE_DATA.
+    result = RegQueryInfoKey(hKey, nullptr, nullptr, nullptr, nullptr, &maxSubKeyNameSize, nullptr, nullptr, nullptr, nullptr, nullptr, nullptr);
+    ASSERT(result == ERROR_SUCCESS);
+    if (result != ERROR_SUCCESS) {
+        RegCloseKey(hKey);
+        return false;
+    }
+
+    std::wstring subKeyName;
+    subKeyName.reserve(maxSubKeyNameSize + 1);
+
+    DWORD retCode = ERROR_SUCCESS;
+    for (DWORD i = 0; retCode == ERROR_SUCCESS; ++i) {
+        Q_ASSERT(unsigned(subKeyName.capacity()) > maxSubKeyNameSize);
+        // Make the previously reserved capacity official again.
+        subKeyName.resize(subKeyName.capacity());
+        DWORD subKeyNameSize = static_cast<DWORD>(subKeyName.size());
+        retCode = RegEnumKeyEx(hKey, i, reinterpret_cast<LPWSTR>(subKeyName.data()), &subKeyNameSize, nullptr, nullptr, nullptr, nullptr);
+
+        ASSERT(result == ERROR_SUCCESS || retCode == ERROR_NO_MORE_ITEMS);
+        if (retCode == ERROR_SUCCESS) {
+            // subKeyNameSize excludes the trailing \0
+            subKeyName.resize(subKeyNameSize);
+            // Pass only the sub keyname, not the full path.
+            callback(hKey, subKeyName);
+        }
+    }
+
+    RegCloseKey(hKey);
+    return retCode != ERROR_NO_MORE_ITEMS;
+}
+
+// Created for Win32
+
+DWORD Utility::execCmd(std::wstring cmd, bool wait)
+{
+    // https://docs.microsoft.com/en-us/windows/win32/procthread/creating-processes
+    STARTUPINFO si;
+    PROCESS_INFORMATION pi;
+
+    ZeroMemory(&si, sizeof(si));
+    si.cb = sizeof(si);
+    ZeroMemory(&pi, sizeof(pi));
+
+    // Start the child process. 
+    if (!CreateProcess(nullptr,  // No module name (use command line)
+        cmd.data(),              // Command line
+        nullptr,        // Process handle not inheritable
+        nullptr,        // Thread handle not inheritable
+        FALSE,          // Set handle inheritance to FALSE
+        0,              // No creation flags
+        nullptr,        // Use parent's environment block
+        nullptr,        // Use parent's starting directory 
+        &si,            // Pointer to STARTUPINFO structure
+        &pi)            // Pointer to PROCESS_INFORMATION structure
+        )
+    {
+        return ERROR_INVALID_FUNCTION;
+    }
+
+    DWORD exitCode = 0;
+
+    if (wait) {
+        // Wait until child process exits.
+        WaitForSingleObject(pi.hProcess, INFINITE);
+
+        GetExitCodeProcess(pi.hProcess, &exitCode);
+    }
+
+    // Close process and thread handles. 
+    CloseHandle(pi.hProcess);
+    CloseHandle(pi.hThread);
+
+    return exitCode;
+}
+
+bool Utility::killProcess(const std::wstring &exePath)
+{
+    // https://docs.microsoft.com/en-us/windows/win32/psapi/enumerating-all-processes
+    // Get the list of process identifiers.
+    DWORD aProcesses[1024], cbNeeded, cProcesses, i;
+
+    if (!EnumProcesses(aProcesses, sizeof(aProcesses), &cbNeeded)) {
+        return false;
+    }
+
+    // Calculate how many process identifiers were returned.
+    cProcesses = cbNeeded / sizeof(DWORD);
+
+    std::wstring tmpMatch = exePath;
+    std::transform(tmpMatch.begin(), tmpMatch.end(), tmpMatch.begin(), std::tolower);
+
+    for (i = 0; i < cProcesses; i++) {
+        if (aProcesses[i] != 0) {
+            // Get a handle to the process.
+            HANDLE hProcess = OpenProcess(PROCESS_QUERY_INFORMATION | PROCESS_TERMINATE, FALSE, aProcesses[i]);
+
+            // Get the process name.
+            if (hProcess) {
+                TCHAR szProcessName[MAX_PATH] = {0};
+                DWORD cbSize = sizeof(szProcessName) / sizeof(TCHAR);
+
+                if (QueryFullProcessImageName(hProcess, 0, szProcessName, &cbSize) == TRUE && cbSize > 0) {
+                    std::wstring procName = szProcessName;
+                    std::transform(procName.begin(), procName.end(), procName.begin(), std::tolower);
+
+                    if (procName == tmpMatch) {
+                        if (TerminateProcess(hProcess, 0) == TRUE) {
+                            WaitForSingleObject(hProcess, INFINITE);
+                            CloseHandle(hProcess);
+                            return true;
+                        }
+                    }
+                }
+
+                CloseHandle(hProcess);
+            }
+        }
+    }
+
+    return false;
+}
+
+bool Utility::isValidDirectory(const std::wstring &path)
+{
+    auto attrib = GetFileAttributes(path.data());
+
+    if (attrib == INVALID_FILE_ATTRIBUTES || GetLastError() == ERROR_FILE_NOT_FOUND) {
+        return false;
+    }
+
+    return (attrib & FILE_ATTRIBUTE_DIRECTORY);
+}
+
+std::wstring Utility::getAppRegistryString(const std::wstring &appVendor, const std::wstring &appName, const std::wstring &valueName)
+{
+    std::wstring appKey = std::wstring(LR"(SOFTWARE\)") + appVendor + L'\\' + appName;
+    std::wstring appKeyWow64 = std::wstring(LR"(SOFTWARE\WOW6432Node\)") + appVendor + L'\\' + appName;
+
+    std::vector<std::wstring> appKeys = { appKey, appKeyWow64 };
+
+    for (auto &key : appKeys) {
+        try {
+            return std::get<std::wstring>(Utility::registryGetKeyValue(HKEY_LOCAL_MACHINE,
+                key,
+                valueName));
+        }
+        catch (const std::bad_variant_access&) {}
+    }
+
+    return {};
+}
+
+std::wstring Utility::getAppPath(const std::wstring &appVendor, const std::wstring &appName)
+{
+    return getAppRegistryString(appVendor, appName, L""); // intentionally left empty to get the key's "(default)" value
+}
+
+std::wstring Utility::getConfigPath(const std::wstring &appName)
+{
+    // On Windows, use AppDataLocation, that's where the roaming data is and where we should store the config file
+    PWSTR pszPath = nullptr;
+    if (!SUCCEEDED(SHGetKnownFolderPath(FOLDERID_RoamingAppData, 0, nullptr, &pszPath)) || !pszPath) {
+        return {};
+    }
+    std::wstring path = pszPath + PathSeparator + appName + PathSeparator;
+    CoTaskMemFree(pszPath);
+             
+    auto newLocation = path;
+
+    return newLocation;
+}
+
+void Utility::waitForNsisUninstaller(const std::wstring &appShortName)
+{
+    // Can't WaitForSingleObject because NSIS Uninstall.exe copies itself to a TEMP directory and creates a new process,
+    // so we do sort of a hack and wait for its mutex (see nextcloud.nsi).
+    HANDLE hMutex;
+    DWORD lastError = ERROR_SUCCESS;
+    std::wstring name = appShortName + std::wstring(L"Uninstaller");
+
+    // Give the process enough time to start, to wait for the NSIS mutex.
+    Sleep(1500);
+
+    do {
+        hMutex = OpenMutex(MUTEX_ALL_ACCESS, FALSE, name.data());
+        lastError = GetLastError();
+        if (hMutex) {
+            CloseHandle(hMutex);
+        }
+
+        // This is sort of a hack because WaitForSingleObject immediately returns for the NSIS mutex.
+        Sleep(500);
+    } while (lastError != ERROR_FILE_NOT_FOUND);
+}
+
+void Utility::removeNavigationPaneEntries(const std::wstring &appName)
+{
+    if (appName.empty()) {
+        return;
+    }
+
+    // Start by looking at every registered namespace extension for the sidebar, and look for an "ApplicationName" value
+    // that matches ours when we saved.
+    std::vector<std::wstring> entriesToRemove;
+    Utility::registryWalkSubKeys(
+        HKEY_CURRENT_USER,
+        LR"(Software\Microsoft\Windows\CurrentVersion\Explorer\Desktop\NameSpace)",
+        [&entriesToRemove, &appName](HKEY key, const std::wstring &subKey) {
+            try {
+                auto curAppName = std::get<std::wstring>(Utility::registryGetKeyValue(key, subKey, L"ApplicationName"));
+
+                if (curAppName == appName) {
+                    entriesToRemove.push_back(subKey);
+                }
+            }
+            catch (const std::bad_variant_access&) {}
+        });
+
+    for (auto &clsid : entriesToRemove) {
+        std::wstring clsidStr = clsid;
+        std::wstring clsidPath = std::wstring(LR"(Software\Classes\CLSID\)") + clsidStr;
+        std::wstring clsidPathWow64 = std::wstring(LR"(Software\Classes\Wow6432Node\CLSID\)") + clsidStr;
+        std::wstring namespacePath = std::wstring(LR"(Software\Microsoft\Windows\CurrentVersion\Explorer\Desktop\NameSpace\)") + clsidStr;
+
+        Utility::registryDeleteKeyTree(HKEY_CURRENT_USER, clsidPath);
+        Utility::registryDeleteKeyTree(HKEY_CURRENT_USER, clsidPathWow64);
+        Utility::registryDeleteKeyTree(HKEY_CURRENT_USER, namespacePath);
+        Utility::registryDeleteKeyValue(HKEY_CURRENT_USER, LR"(Software\Microsoft\Windows\CurrentVersion\Explorer\HideDesktopIcons\NewStartPanel)", clsidStr);
+    }
+}
+
+// Ported from gui, modified to optionally rename matching files
+bool Utility::copy_dir_recursive(std::wstring from_dir, std::wstring to_dir, copy_dir_recursive_callback *callbackFileNameMatchReplace)
+{
+    WIN32_FIND_DATA fileData;
+
+    if (from_dir.empty() || to_dir.empty()) {
+        return false;
+    }
+
+    if (from_dir.back() != PathSeparator.front())
+        from_dir.append(PathSeparator);
+    if (to_dir.back() != PathSeparator.front())
+        to_dir.append(PathSeparator);
+
+    std::wstring startDir = from_dir;
+    startDir.append(L"*.*");
+
+    auto hFind = FindFirstFile(startDir.data(), &fileData);
+
+    if (hFind == INVALID_HANDLE_VALUE) {
+        return false;
+    }
+
+    bool success = true;
+
+    do {
+        if (fileData.dwFileAttributes & FILE_ATTRIBUTE_DIRECTORY) {
+            if (std::wstring(fileData.cFileName) == L"." || std::wstring(fileData.cFileName) == L"..") {
+                continue;
+            }
+
+            std::wstring from = from_dir + fileData.cFileName;
+            std::wstring to = to_dir + fileData.cFileName;
+
+            if (CreateDirectoryEx(from.data(), to.data(), nullptr) == FALSE) {
+                success = false;
+                break;
+            }
+
+            if (copy_dir_recursive(from, to, callbackFileNameMatchReplace) == false) {
+                success = false;
+                break;
+            }
+        } else {
+            std::wstring newFilename = fileData.cFileName;
+
+            if (callbackFileNameMatchReplace) {
+                (*callbackFileNameMatchReplace)(std::wstring(fileData.cFileName), newFilename);
+            }
+
+            std::wstring from = from_dir + fileData.cFileName;
+            std::wstring to = to_dir + newFilename;
+
+            if (CopyFile(from.data(), to.data(), TRUE) == FALSE) {
+                success = false;
+                break;
+            }
+        }
+    } while (FindNextFile(hFind, &fileData));
+
+    FindClose(hFind);
+
+    return success;
+}
+
+} // namespace NCTools

+ 8 - 1
shell_integration/windows/CMakeLists.txt

@@ -7,9 +7,16 @@ include_directories(
     ${CMAKE_CURRENT_BINARY_DIR}
 )
 configure_file(WinShellExtConstants.h.in ${CMAKE_CURRENT_BINARY_DIR}/WinShellExtConstants.h)
-configure_file(WinShellExt.wxs.in ${CMAKE_CURRENT_BINARY_DIR}/WinShellExt.wxs)
 
 add_subdirectory(NCContextMenu)
 add_subdirectory(NCOverlays)
 add_subdirectory(NCUtil)
 
+if(BUILD_WIN_MSI)
+    configure_file(WinShellExt.wxs.in ${CMAKE_CURRENT_BINARY_DIR}/WinShellExt.wxs)
+
+    install(FILES
+        ${CMAKE_CURRENT_BINARY_DIR}/WinShellExt.wxs
+        DESTINATION msi/
+    )
+endif()

+ 1 - 1
shell_integration/windows/WinShellExt.wxs.in

@@ -14,7 +14,7 @@
  * for more details.
  *
 -->
-<?include $(var.ProjectPath)Platform.wxi?>
+<?include $(sys.CURRENTDIR)Platform.wxi?>
 
 <Wix xmlns="http://schemas.microsoft.com/wix/2006/wi">
     <Fragment>

Certains fichiers n'ont pas été affichés car il y a eu trop de fichiers modifiés dans ce diff