summaryrefslogtreecommitdiffhomepage
path: root/windows/nsis-plugins/src/tray
diff options
context:
space:
mode:
Diffstat (limited to 'windows/nsis-plugins/src/tray')
-rw-r--r--windows/nsis-plugins/src/tray/dllmain.cpp11
-rw-r--r--windows/nsis-plugins/src/tray/iconstreams.h68
-rw-r--r--windows/nsis-plugins/src/tray/mullvad_tray_record.binbin0 -> 1640 bytes
-rw-r--r--windows/nsis-plugins/src/tray/resource.h1
-rw-r--r--windows/nsis-plugins/src/tray/stdafx.cpp8
-rw-r--r--windows/nsis-plugins/src/tray/stdafx.h16
-rw-r--r--windows/nsis-plugins/src/tray/targetver.h12
-rw-r--r--windows/nsis-plugins/src/tray/tray.cpp188
-rw-r--r--windows/nsis-plugins/src/tray/tray.def5
-rw-r--r--windows/nsis-plugins/src/tray/tray.rc3
-rw-r--r--windows/nsis-plugins/src/tray/tray.vcxproj131
-rw-r--r--windows/nsis-plugins/src/tray/tray.vcxproj.filters24
-rw-r--r--windows/nsis-plugins/src/tray/trayjuggler.cpp220
-rw-r--r--windows/nsis-plugins/src/tray/trayjuggler.h58
-rw-r--r--windows/nsis-plugins/src/tray/trayparser.cpp73
-rw-r--r--windows/nsis-plugins/src/tray/trayparser.h22
16 files changed, 840 insertions, 0 deletions
diff --git a/windows/nsis-plugins/src/tray/dllmain.cpp b/windows/nsis-plugins/src/tray/dllmain.cpp
new file mode 100644
index 0000000000..a5a44613dd
--- /dev/null
+++ b/windows/nsis-plugins/src/tray/dllmain.cpp
@@ -0,0 +1,11 @@
+#include "stdafx.h"
+#include <windows.h>
+
+BOOL APIENTRY DllMain(HMODULE, DWORD reason, LPVOID)
+{
+ //
+ // Avoid doing work in DllMain since the loader lock is held
+ //
+
+ return TRUE;
+}
diff --git a/windows/nsis-plugins/src/tray/iconstreams.h b/windows/nsis-plugins/src/tray/iconstreams.h
new file mode 100644
index 0000000000..b096915a4a
--- /dev/null
+++ b/windows/nsis-plugins/src/tray/iconstreams.h
@@ -0,0 +1,68 @@
+#pragma once
+
+#include <cstdint>
+#include <windows.h>
+
+#pragma pack(push, 1)
+typedef struct ICON_STREAMS_HEADER_
+{
+ uint32_t HeaderSize;
+ uint32_t u1; // 7
+ uint16_t u2; // 1
+ uint16_t u3; // 1
+ uint32_t NumberRecords;
+ uint32_t OffsetFirstRecord;
+}
+ICON_STREAMS_HEADER;
+#pragma pack(pop)
+
+enum ICON_STREAMS_VISIBILITY
+{
+ NOTIFICATIONS_ONLY = 0,
+ HIDE_ICON_AND_NOTIFICATIONS = 1,
+ SHOW_ICON_AND_NOTIFICATIONS = 2,
+};
+
+#pragma pack(push, 1)
+typedef struct ICON_STREAMS_RECORD_
+{
+ uint16_t ApplicationPath[MAX_PATH];
+ uint32_t u1; // id that the owning application uses to identify the icon?
+ // this value is constant except when used for the networking icon
+ // where it cycles through a set of ids.
+
+ uint32_t u2; // 0
+ uint32_t Visibility;
+ uint16_t YearCreated;
+ uint16_t MonthCreated;
+ uint16_t LastTooltip[MAX_PATH];
+ uint32_t u6; // 0
+ uint32_t u7; // 0 or 1 but why?
+ uint32_t ImagelistId; // id of cached icon, or -1
+ GUID Guid; //
+ uint32_t u8; // 0
+ uint32_t u9; // 0
+ uint32_t u10; // 0
+ FILETIME Time1; // discrete event 1 UTC
+ FILETIME Time2; // discrete event 2 UTC, this can be 0
+ uint32_t u11; // 0
+
+ union
+ {
+ struct
+ {
+ uint16_t ApplicationName[256 + 1];
+ uint8_t Padding[6];
+ } details;
+ struct
+ {
+ uint32_t u12; // 200d0000
+ uint16_t u13; // b0fe
+ uint16_t ApplicationName[256 + 1];
+ } extended_details;
+ } DUMMYUNIONNAME;
+
+ uint32_t Ordinal; // determines ordering within group.
+}
+ICON_STREAMS_RECORD;
+#pragma pack(pop)
diff --git a/windows/nsis-plugins/src/tray/mullvad_tray_record.bin b/windows/nsis-plugins/src/tray/mullvad_tray_record.bin
new file mode 100644
index 0000000000..347af479bc
--- /dev/null
+++ b/windows/nsis-plugins/src/tray/mullvad_tray_record.bin
Binary files differ
diff --git a/windows/nsis-plugins/src/tray/resource.h b/windows/nsis-plugins/src/tray/resource.h
new file mode 100644
index 0000000000..df7adc2724
--- /dev/null
+++ b/windows/nsis-plugins/src/tray/resource.h
@@ -0,0 +1 @@
+#define MULLVAD_TRAY_RECORD 9001
diff --git a/windows/nsis-plugins/src/tray/stdafx.cpp b/windows/nsis-plugins/src/tray/stdafx.cpp
new file mode 100644
index 0000000000..3b6341d106
--- /dev/null
+++ b/windows/nsis-plugins/src/tray/stdafx.cpp
@@ -0,0 +1,8 @@
+// stdafx.cpp : source file that includes just the standard includes
+// driverlogic.pch will be the pre-compiled header
+// stdafx.obj will contain the pre-compiled type information
+
+#include "stdafx.h"
+
+// TODO: reference any additional headers you need in STDAFX.H
+// and not in this file
diff --git a/windows/nsis-plugins/src/tray/stdafx.h b/windows/nsis-plugins/src/tray/stdafx.h
new file mode 100644
index 0000000000..f3a07375c7
--- /dev/null
+++ b/windows/nsis-plugins/src/tray/stdafx.h
@@ -0,0 +1,16 @@
+// stdafx.h : include file for standard system include files,
+// or project specific include files that are used frequently, but
+// are changed infrequently
+//
+
+#pragma once
+
+#include "targetver.h"
+
+#define WIN32_LEAN_AND_MEAN // Exclude rarely-used stuff from Windows headers
+// Windows Header Files:
+#include <windows.h>
+
+
+
+// TODO: reference additional headers your program requires here
diff --git a/windows/nsis-plugins/src/tray/targetver.h b/windows/nsis-plugins/src/tray/targetver.h
new file mode 100644
index 0000000000..ae4a5c032c
--- /dev/null
+++ b/windows/nsis-plugins/src/tray/targetver.h
@@ -0,0 +1,12 @@
+#pragma once
+
+// Including SDKDDKVer.h defines the highest available Windows platform.
+
+// If you wish to build your application for a previous Windows platform, include WinSDKVer.h and
+// set the _WIN32_WINNT macro to the platform you wish to support before including SDKDDKVer.h.
+
+#include <WinSDKVer.h>
+
+#define _WIN32_WINNT _WIN32_WINNT_WIN7
+
+#include <SDKDDKVer.h>
diff --git a/windows/nsis-plugins/src/tray/tray.cpp b/windows/nsis-plugins/src/tray/tray.cpp
new file mode 100644
index 0000000000..f270c47261
--- /dev/null
+++ b/windows/nsis-plugins/src/tray/tray.cpp
@@ -0,0 +1,188 @@
+#include "stdafx.h"
+#include "trayparser.h"
+#include "trayjuggler.h"
+#include "resource.h"
+#include <windows.h>
+#include <log/log.h>
+#include <libcommon/string.h>
+#include <libcommon/registry/registry.h>
+#include <libcommon/resourcedata.h>
+#include <libcommon/filesystem.h>
+#include <libcommon/process.h>
+#include <libcommon/security.h>
+#include <nsis/pluginapi.h>
+#include <stdexcept>
+#include <experimental/filesystem>
+
+namespace
+{
+
+EXTERN_C IMAGE_DOS_HEADER __ImageBase;
+
+void InjectMullvadRecord(TrayJuggler &juggler)
+{
+ //
+ // There's no existing mullvad tray record in the system.
+ // Load a template mullvad record, which is then fixed up and injected.
+ //
+
+ auto moduleHandle = reinterpret_cast<HMODULE>(&__ImageBase);
+
+ auto resource = common::resourcedata::LoadBinaryResource(moduleHandle, MULLVAD_TRAY_RECORD);
+
+ if (resource.size != sizeof(ICON_STREAMS_RECORD))
+ {
+ throw std::runtime_error("Invalid tray template, size mismatch");
+ }
+
+ ICON_STREAMS_RECORD newRecord(*reinterpret_cast<const ICON_STREAMS_RECORD *>(resource.data));
+
+ juggler.injectRecord(newRecord);
+}
+
+void UpdateRegistry(common::registry::RegistryKey &regkey, const std::wstring &valueName, const TrayJuggler &juggler)
+{
+ //
+ // Construct path to 'explorer.exe'
+ //
+
+ const auto windir = common::fs::GetKnownFolderPath(FOLDERID_Windows, 0, nullptr);
+ const auto explorer = std::experimental::filesystem::path(windir).append(L"explorer.exe");
+
+ //
+ // Determine process id of active instance(s).
+ // There's a setting in 'explorer' that will open all folder views as separate processes.
+ //
+
+ const auto pids = common::process::GetAllProcessIdsFromName(explorer);
+
+ if (pids.empty())
+ {
+ throw std::runtime_error("Could not determine PID of explorer.exe");
+ }
+
+ //
+ // Make a copy of the process security context before we start terminating processes.
+ // Should we make an effort to choose the process that has been alive the longest?
+ //
+ auto context = common::security::DuplicateSecurityContext(*pids.begin());
+
+ size_t terminated = 0;
+
+ for (auto pid : pids)
+ {
+ auto handle = OpenProcess(PROCESS_TERMINATE, FALSE, pid);
+
+ if (nullptr != handle)
+ {
+ //
+ // 'winlogon' is monitoring 'explorer' and immediately
+ // restarts it if the exit code is 0.
+ //
+ if (FALSE != TerminateProcess(handle, 1))
+ {
+ WaitForSingleObject(handle, INFINITE);
+ ++terminated;
+ }
+
+ CloseHandle(handle);
+ }
+ }
+
+ if (0 == terminated)
+ {
+ throw std::runtime_error("Could not terminate explorer.exe");
+ }
+
+ //
+ // We've terminated one/more instances of explorer.exe so we have to follow through.
+ //
+
+ if (pids.size() != terminated)
+ {
+ PluginLog(L"Could not terminate all instances of explorer.exe");
+ }
+
+ regkey.writeValue(valueName, juggler.pack());
+
+ common::process::RunInContext(*context, explorer);
+}
+
+} // anonymous namespace
+
+//
+// PromoteTrayIcon
+//
+// Ensure the GUI's tray icon is placed in the visible part of the notification area.
+// This is accomplished by updating a binary blob in the registry.
+//
+enum class PromoteTrayIconStatus
+{
+ GENERAL_ERROR = 0,
+ SUCCESS
+};
+
+void __declspec(dllexport) NSISCALL PromoteTrayIcon
+(
+ HWND hwndParent,
+ int string_size,
+ LPTSTR variables,
+ stack_t **stacktop,
+ extra_parameters *extra,
+ ...
+)
+{
+ EXDLL_INIT();
+
+ try
+ {
+ static const wchar_t keyName[] = L"Software\\Classes\\Local Settings\\Software\\Microsoft\\Windows\\CurrentVersion\\TrayNotify";
+ static const wchar_t valueName[] = L"IconStreams";
+
+ auto regkey = common::registry::Registry::OpenKey(HKEY_CURRENT_USER, keyName, true);
+
+ TrayParser parser(regkey->readBinaryBlob(valueName));
+
+ TrayJuggler juggler(parser);
+
+ bool updateRegistry = true;
+
+ if (auto mullvadRecord = juggler.findRecord(L"Mullvad VPN"))
+ {
+ if (ICON_STREAMS_VISIBILITY::SHOW_ICON_AND_NOTIFICATIONS == mullvadRecord->Visibility)
+ {
+ updateRegistry = false;
+ }
+ else
+ {
+ juggler.promoteRecord(mullvadRecord);
+ }
+ }
+ else
+ {
+ InjectMullvadRecord(juggler);
+ }
+
+ //
+ // Only update the registry if the record/record set was updated.
+ //
+
+ if (updateRegistry)
+ {
+ UpdateRegistry(*regkey, valueName, juggler);
+ }
+
+ pushstring(L"");
+ pushint(PromoteTrayIconStatus::SUCCESS);
+ }
+ catch (std::exception &err)
+ {
+ pushstring(common::string::ToWide(err.what()).c_str());
+ pushint(PromoteTrayIconStatus::GENERAL_ERROR);
+ }
+ catch (...)
+ {
+ pushstring(L"Unspecified error");
+ pushint(PromoteTrayIconStatus::GENERAL_ERROR);
+ }
+}
diff --git a/windows/nsis-plugins/src/tray/tray.def b/windows/nsis-plugins/src/tray/tray.def
new file mode 100644
index 0000000000..c32b9d9698
--- /dev/null
+++ b/windows/nsis-plugins/src/tray/tray.def
@@ -0,0 +1,5 @@
+LIBRARY tray
+
+EXPORTS
+
+PromoteTrayIcon
diff --git a/windows/nsis-plugins/src/tray/tray.rc b/windows/nsis-plugins/src/tray/tray.rc
new file mode 100644
index 0000000000..761bd7e71f
--- /dev/null
+++ b/windows/nsis-plugins/src/tray/tray.rc
@@ -0,0 +1,3 @@
+#include "resource.h"
+
+MULLVAD_TRAY_RECORD RCDATA "mullvad_tray_record.bin"
diff --git a/windows/nsis-plugins/src/tray/tray.vcxproj b/windows/nsis-plugins/src/tray/tray.vcxproj
new file mode 100644
index 0000000000..a8db9a7b63
--- /dev/null
+++ b/windows/nsis-plugins/src/tray/tray.vcxproj
@@ -0,0 +1,131 @@
+<?xml version="1.0" encoding="utf-8"?>
+<Project DefaultTargets="Build" ToolsVersion="15.0" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
+ <ItemGroup Label="ProjectConfigurations">
+ <ProjectConfiguration Include="Debug|Win32">
+ <Configuration>Debug</Configuration>
+ <Platform>Win32</Platform>
+ </ProjectConfiguration>
+ <ProjectConfiguration Include="Release|Win32">
+ <Configuration>Release</Configuration>
+ <Platform>Win32</Platform>
+ </ProjectConfiguration>
+ </ItemGroup>
+ <PropertyGroup Label="Globals">
+ <VCProjectVersion>15.0</VCProjectVersion>
+ <ProjectGuid>{E3E05654-D5D6-4D39-A9B4-BB14B012C5FF}</ProjectGuid>
+ <Keyword>Win32Proj</Keyword>
+ <RootNamespace>tray</RootNamespace>
+ <WindowsTargetPlatformVersion>10.0.16299.0</WindowsTargetPlatformVersion>
+ </PropertyGroup>
+ <Import Project="$(VCTargetsPath)\Microsoft.Cpp.Default.props" />
+ <PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Debug|Win32'" Label="Configuration">
+ <ConfigurationType>DynamicLibrary</ConfigurationType>
+ <UseDebugLibraries>true</UseDebugLibraries>
+ <PlatformToolset>v141</PlatformToolset>
+ <CharacterSet>Unicode</CharacterSet>
+ </PropertyGroup>
+ <PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Release|Win32'" Label="Configuration">
+ <ConfigurationType>DynamicLibrary</ConfigurationType>
+ <UseDebugLibraries>false</UseDebugLibraries>
+ <PlatformToolset>v141</PlatformToolset>
+ <WholeProgramOptimization>true</WholeProgramOptimization>
+ <CharacterSet>Unicode</CharacterSet>
+ </PropertyGroup>
+ <Import Project="$(VCTargetsPath)\Microsoft.Cpp.props" />
+ <ImportGroup Label="ExtensionSettings">
+ </ImportGroup>
+ <ImportGroup Label="Shared">
+ </ImportGroup>
+ <ImportGroup Label="PropertySheets" Condition="'$(Configuration)|$(Platform)'=='Debug|Win32'">
+ <Import Project="$(UserRootDir)\Microsoft.Cpp.$(Platform).user.props" Condition="exists('$(UserRootDir)\Microsoft.Cpp.$(Platform).user.props')" Label="LocalAppDataPlatform" />
+ </ImportGroup>
+ <ImportGroup Label="PropertySheets" Condition="'$(Configuration)|$(Platform)'=='Release|Win32'">
+ <Import Project="$(UserRootDir)\Microsoft.Cpp.$(Platform).user.props" Condition="exists('$(UserRootDir)\Microsoft.Cpp.$(Platform).user.props')" Label="LocalAppDataPlatform" />
+ </ImportGroup>
+ <PropertyGroup Label="UserMacros" />
+ <PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Debug|Win32'">
+ <LinkIncremental>true</LinkIncremental>
+ <OutDir>$(SolutionDir)bin\$(Platform)-$(Configuration)\</OutDir>
+ <IntDir>$(SolutionDir)bin\temp\$(Platform)-$(Configuration)\$(ProjectName)\</IntDir>
+ </PropertyGroup>
+ <PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Release|Win32'">
+ <LinkIncremental>false</LinkIncremental>
+ <OutDir>$(SolutionDir)bin\$(Platform)-$(Configuration)\</OutDir>
+ <IntDir>$(SolutionDir)bin\temp\$(Platform)-$(Configuration)\$(ProjectName)\</IntDir>
+ </PropertyGroup>
+ <ItemDefinitionGroup Condition="'$(Configuration)|$(Platform)'=='Debug|Win32'">
+ <ClCompile>
+ <PrecompiledHeader>Use</PrecompiledHeader>
+ <WarningLevel>Level3</WarningLevel>
+ <Optimization>Disabled</Optimization>
+ <SDLCheck>true</SDLCheck>
+ <PreprocessorDefinitions>WIN32;_DEBUG;TRAY_EXPORTS;_WINDOWS;_USRDLL;%(PreprocessorDefinitions)</PreprocessorDefinitions>
+ <ConformanceMode>true</ConformanceMode>
+ <AdditionalIncludeDirectories>$(ProjectDir)../../../../dist-assets/binaries/windows/;$(ProjectDir)../../../windows-libraries/src/;$(ProjectDir)../</AdditionalIncludeDirectories>
+ <LanguageStandard>stdcpplatest</LanguageStandard>
+ <RuntimeLibrary>MultiThreadedDebug</RuntimeLibrary>
+ </ClCompile>
+ <Link>
+ <SubSystem>Windows</SubSystem>
+ <GenerateDebugInformation>true</GenerateDebugInformation>
+ <ImageHasSafeExceptionHandlers>false</ImageHasSafeExceptionHandlers>
+ <AdditionalLibraryDirectories>$(ProjectDir)../../../../dist-assets/binaries/windows/nsis/;$(SolutionDir)bin\$(Platform)-$(Configuration)\</AdditionalLibraryDirectories>
+ <AdditionalDependencies>log.lib;libcommon.lib;pluginapi-x86-unicode.lib;kernel32.lib;user32.lib;gdi32.lib;winspool.lib;comdlg32.lib;advapi32.lib;shell32.lib;ole32.lib;oleaut32.lib;uuid.lib;odbc32.lib;odbccp32.lib;%(AdditionalDependencies)</AdditionalDependencies>
+ <IgnoreSpecificDefaultLibraries>libc.lib</IgnoreSpecificDefaultLibraries>
+ <ModuleDefinitionFile>tray.def</ModuleDefinitionFile>
+ </Link>
+ </ItemDefinitionGroup>
+ <ItemDefinitionGroup Condition="'$(Configuration)|$(Platform)'=='Release|Win32'">
+ <ClCompile>
+ <PrecompiledHeader>Use</PrecompiledHeader>
+ <WarningLevel>Level3</WarningLevel>
+ <Optimization>MaxSpeed</Optimization>
+ <FunctionLevelLinking>true</FunctionLevelLinking>
+ <IntrinsicFunctions>true</IntrinsicFunctions>
+ <SDLCheck>true</SDLCheck>
+ <PreprocessorDefinitions>WIN32;NDEBUG;TRAY_EXPORTS;_WINDOWS;_USRDLL;%(PreprocessorDefinitions)</PreprocessorDefinitions>
+ <ConformanceMode>true</ConformanceMode>
+ <AdditionalIncludeDirectories>$(ProjectDir)../../../../dist-assets/binaries/windows/;$(ProjectDir)../../../windows-libraries/src/;$(ProjectDir)../</AdditionalIncludeDirectories>
+ <RuntimeLibrary>MultiThreaded</RuntimeLibrary>
+ <LanguageStandard>stdcpplatest</LanguageStandard>
+ </ClCompile>
+ <Link>
+ <SubSystem>Windows</SubSystem>
+ <EnableCOMDATFolding>true</EnableCOMDATFolding>
+ <OptimizeReferences>true</OptimizeReferences>
+ <GenerateDebugInformation>true</GenerateDebugInformation>
+ <ImageHasSafeExceptionHandlers>false</ImageHasSafeExceptionHandlers>
+ <AdditionalLibraryDirectories>$(ProjectDir)../../../../dist-assets/binaries/windows/nsis/;$(SolutionDir)bin\$(Platform)-$(Configuration)\</AdditionalLibraryDirectories>
+ <AdditionalDependencies>log.lib;libcommon.lib;pluginapi-x86-unicode.lib;kernel32.lib;user32.lib;gdi32.lib;winspool.lib;comdlg32.lib;advapi32.lib;shell32.lib;ole32.lib;oleaut32.lib;uuid.lib;odbc32.lib;odbccp32.lib;%(AdditionalDependencies)</AdditionalDependencies>
+ <IgnoreSpecificDefaultLibraries>libc.lib</IgnoreSpecificDefaultLibraries>
+ <ModuleDefinitionFile>tray.def</ModuleDefinitionFile>
+ </Link>
+ </ItemDefinitionGroup>
+ <ItemGroup>
+ <ClInclude Include="iconstreams.h" />
+ <ClInclude Include="resource.h" />
+ <ClInclude Include="stdafx.h" />
+ <ClInclude Include="targetver.h" />
+ <ClInclude Include="trayjuggler.h" />
+ <ClInclude Include="trayparser.h" />
+ </ItemGroup>
+ <ItemGroup>
+ <ClCompile Include="dllmain.cpp" />
+ <ClCompile Include="tray.cpp" />
+ <ClCompile Include="stdafx.cpp">
+ <PrecompiledHeader Condition="'$(Configuration)|$(Platform)'=='Debug|Win32'">Create</PrecompiledHeader>
+ <PrecompiledHeader Condition="'$(Configuration)|$(Platform)'=='Release|Win32'">Create</PrecompiledHeader>
+ </ClCompile>
+ <ClCompile Include="trayjuggler.cpp" />
+ <ClCompile Include="trayparser.cpp" />
+ </ItemGroup>
+ <ItemGroup>
+ <None Include="tray.def" />
+ </ItemGroup>
+ <ItemGroup>
+ <ResourceCompile Include="tray.rc" />
+ </ItemGroup>
+ <Import Project="$(VCTargetsPath)\Microsoft.Cpp.targets" />
+ <ImportGroup Label="ExtensionTargets">
+ </ImportGroup>
+</Project> \ No newline at end of file
diff --git a/windows/nsis-plugins/src/tray/tray.vcxproj.filters b/windows/nsis-plugins/src/tray/tray.vcxproj.filters
new file mode 100644
index 0000000000..5f89a1538e
--- /dev/null
+++ b/windows/nsis-plugins/src/tray/tray.vcxproj.filters
@@ -0,0 +1,24 @@
+<?xml version="1.0" encoding="utf-8"?>
+<Project ToolsVersion="4.0" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
+ <ItemGroup>
+ <ClInclude Include="stdafx.h" />
+ <ClInclude Include="targetver.h" />
+ <ClInclude Include="resource.h" />
+ <ClInclude Include="trayjuggler.h" />
+ <ClInclude Include="trayparser.h" />
+ <ClInclude Include="iconstreams.h" />
+ </ItemGroup>
+ <ItemGroup>
+ <ClCompile Include="dllmain.cpp" />
+ <ClCompile Include="stdafx.cpp" />
+ <ClCompile Include="tray.cpp" />
+ <ClCompile Include="trayjuggler.cpp" />
+ <ClCompile Include="trayparser.cpp" />
+ </ItemGroup>
+ <ItemGroup>
+ <None Include="tray.def" />
+ </ItemGroup>
+ <ItemGroup>
+ <ResourceCompile Include="tray.rc" />
+ </ItemGroup>
+</Project> \ No newline at end of file
diff --git a/windows/nsis-plugins/src/tray/trayjuggler.cpp b/windows/nsis-plugins/src/tray/trayjuggler.cpp
new file mode 100644
index 0000000000..21c65ee3d2
--- /dev/null
+++ b/windows/nsis-plugins/src/tray/trayjuggler.cpp
@@ -0,0 +1,220 @@
+#include "stdafx.h"
+#include "trayjuggler.h"
+#include <algorithm>
+
+namespace
+{
+
+template<typename T>
+void AppendBuffer(std::vector<uint8_t> &buffer, const T &t)
+{
+ const auto prevSize = buffer.size();
+
+ buffer.resize(prevSize + sizeof(T));
+
+ memcpy(&buffer[prevSize], &t, sizeof(T));
+}
+
+inline wchar_t rot13(wchar_t c)
+{
+ if (c >= L'A' && c <= L'Z')
+ {
+ auto temp = (wchar_t)(c + 13);
+ return (temp <= L'Z' ? temp : (wchar_t)(L'A' + (temp - L'Z' - 1)));
+ }
+ else if (c >= L'a' && c <= L'z')
+ {
+ auto temp = (wchar_t)(c + 13);
+ return (temp <= L'z' ? temp : (wchar_t)(L'a' + (temp - L'z' - 1)));
+ }
+
+ return c;
+}
+
+} // anonymous namespace
+
+TrayJuggler::TrayJuggler(const TrayParser &parser)
+{
+ m_header = parser.getHeader();
+
+ for (const auto &record : parser.getRecords())
+ {
+ m_records.push_back(std::make_shared<ICON_STREAMS_RECORD>(record));
+ }
+}
+
+std::shared_ptr<ICON_STREAMS_RECORD> TrayJuggler::findRecord(const std::wstring &path) const
+{
+ for (const auto &record : m_records)
+ {
+ const auto appPath = DecodeString(record->ApplicationPath, sizeof(record->ApplicationPath));
+
+ if (nullptr != wcsstr(appPath.c_str(), path.c_str()))
+ {
+ return record;
+ }
+ }
+
+ return std::shared_ptr<ICON_STREAMS_RECORD>();
+}
+
+uint32_t TrayJuggler::getNextFreeOrdinal(TraySearchGroup searchGroup) const
+{
+ uint32_t highestOrdinal = 0;
+
+ bool noMatchingRecord = true;
+
+ enumerateRecords([&](std::shared_ptr<ICON_STREAMS_RECORD> record)
+ {
+ if ((TraySearchGroup::Visible == searchGroup && ICON_STREAMS_VISIBILITY::SHOW_ICON_AND_NOTIFICATIONS == record->Visibility)
+ || (TraySearchGroup::Hidden == searchGroup) && ICON_STREAMS_VISIBILITY::SHOW_ICON_AND_NOTIFICATIONS != record->Visibility)
+ {
+ noMatchingRecord = false;
+
+ if (record->Ordinal > highestOrdinal)
+ {
+ highestOrdinal = record->Ordinal;
+ }
+ }
+
+ return true;
+ });
+
+ return (noMatchingRecord ? 0 : highestOrdinal + 1);
+}
+
+
+void TrayJuggler::injectRecord(const ICON_STREAMS_RECORD &record)
+{
+ SYSTEMTIME time;
+
+ GetSystemTime(&time);
+
+ auto newRecord = std::make_shared<ICON_STREAMS_RECORD>(record);
+
+ {
+ newRecord->Visibility = ICON_STREAMS_VISIBILITY::SHOW_ICON_AND_NOTIFICATIONS;
+
+ newRecord->YearCreated = time.wYear;
+ newRecord->MonthCreated = time.wMonth;
+
+ // TODO: Meaning of this bool?
+ newRecord->u7 = 0;
+
+ newRecord->ImagelistId = 0xFFFFFFFF;
+
+ FILETIME fileTime;
+
+ SystemTimeToFileTime(&time, &fileTime);
+
+ newRecord->Time1 = fileTime;
+
+ newRecord->Time2.dwHighDateTime = 0;
+ newRecord->Time2.dwLowDateTime = 0;
+
+ newRecord->Ordinal = getNextFreeOrdinal(TraySearchGroup::Visible);
+ }
+
+ //
+ // By default, the first few records will always be Microsoft icons such as
+ // battery, networking, sounds.
+ //
+ // These have a visibility of SHOW_ICON_AND_NOTIFICATIONS.
+ //
+ // There can be other icons with visibility SHOW_ICON_AND_NOTIFICATIONS towards
+ // the end of the array. But these icons typically don't have a valid ImagelistId.
+ //
+ // In the end, the ordering of records doesn't seem to matter.
+ //
+ // Insert new record at the end.
+ //
+
+ m_records.push_back(newRecord);
+}
+
+void TrayJuggler::promoteRecord(std::shared_ptr<ICON_STREAMS_RECORD> record)
+{
+ if (ICON_STREAMS_VISIBILITY::SHOW_ICON_AND_NOTIFICATIONS == record->Visibility)
+ {
+ // Abort if the icon is already visible.
+ return;
+ }
+
+ record->Visibility = ICON_STREAMS_VISIBILITY::SHOW_ICON_AND_NOTIFICATIONS;
+ record->Ordinal = getNextFreeOrdinal(TraySearchGroup::Visible);
+
+ SYSTEMTIME time;
+
+ GetSystemTime(&time);
+
+ FILETIME fileTime;
+
+ SystemTimeToFileTime(&time, &fileTime);
+
+ record->Time1 = fileTime;
+}
+
+bool TrayJuggler::enumerateRecords(std::function<bool(std::shared_ptr<ICON_STREAMS_RECORD> record)> callback) const
+{
+ for (auto record : m_records)
+ {
+ if (false == callback(record))
+ {
+ return false;
+ }
+ }
+
+ return true;
+}
+
+std::vector<uint8_t> TrayJuggler::pack() const
+{
+ std::vector<uint8_t> blob;
+
+ const auto requiredSize = (m_records.size() * sizeof(ICON_STREAMS_RECORD))
+ + sizeof(ICON_STREAMS_HEADER);
+
+ blob.reserve(requiredSize);
+
+ // Use original header as a template.
+ ICON_STREAMS_HEADER header = m_header;
+
+ header.HeaderSize = sizeof(header);
+ header.NumberRecords = static_cast<uint32_t>(m_records.size());
+ header.OffsetFirstRecord = sizeof(header);
+
+ AppendBuffer(blob, header);
+
+ for (const auto record : m_records)
+ {
+ AppendBuffer(blob, *record);
+ }
+
+ return blob;
+}
+
+//static
+std::wstring TrayJuggler::DecodeString(const uint16_t *encoded, size_t bufferSize)
+{
+ const auto numCharacters = bufferSize / sizeof(uint16_t);
+
+ std::wstring decoded;
+
+ decoded.reserve(numCharacters);
+
+ const auto end = encoded + numCharacters;
+
+ for (; encoded != end; ++encoded)
+ {
+ auto source = (wchar_t)*encoded;
+
+ if (L'\0' == source)
+ {
+ break;
+ }
+
+ decoded.push_back(rot13(source));
+ }
+
+ return decoded;
+}
diff --git a/windows/nsis-plugins/src/tray/trayjuggler.h b/windows/nsis-plugins/src/tray/trayjuggler.h
new file mode 100644
index 0000000000..9fcfe35f67
--- /dev/null
+++ b/windows/nsis-plugins/src/tray/trayjuggler.h
@@ -0,0 +1,58 @@
+#pragma once
+
+#include "iconstreams.h"
+#include "trayparser.h"
+#include <cstdint>
+#include <vector>
+#include <memory>
+#include <string>
+#include <functional>
+#include <list>
+
+class TrayJuggler
+{
+public:
+
+ TrayJuggler(const TrayParser &parser);
+
+ // Find record based on substring present in record's application path.
+ std::shared_ptr<ICON_STREAMS_RECORD> findRecord(const std::wstring &path) const;
+
+ enum class TraySearchGroup
+ {
+ Visible,
+ Hidden
+ };
+
+ uint32_t getNextFreeOrdinal(TraySearchGroup searchGroup) const;
+
+ //
+ // Fix up and inject record.
+ // The icon will be displayed in the rightmost position.
+ //
+ void injectRecord(const ICON_STREAMS_RECORD &record);
+
+ //
+ // Update and promote existing record.
+ // The icon will be displayed in the rightmost position.
+ //
+ void promoteRecord(std::shared_ptr<ICON_STREAMS_RECORD> record);
+
+ bool enumerateRecords(std::function<bool(std::shared_ptr<ICON_STREAMS_RECORD> record)> callback) const;
+
+ // Generate a valid stream including header.
+ std::vector<uint8_t> pack() const;
+
+ static std::wstring DecodeString(const uint16_t *encoded, size_t bufferSize);
+
+private:
+
+ //
+ // This is the original header.
+ // We keep it around to be able to preserve the values for
+ // the unknown fields.
+ //
+ ICON_STREAMS_HEADER m_header;
+
+ std::list<std::shared_ptr<ICON_STREAMS_RECORD> > m_records;
+};
diff --git a/windows/nsis-plugins/src/tray/trayparser.cpp b/windows/nsis-plugins/src/tray/trayparser.cpp
new file mode 100644
index 0000000000..4ff3573c81
--- /dev/null
+++ b/windows/nsis-plugins/src/tray/trayparser.cpp
@@ -0,0 +1,73 @@
+#include "stdafx.h"
+#include "trayparser.h"
+#include <stdexcept>
+#include <algorithm>
+
+TrayParser::TrayParser(const std::vector<uint8_t> &blob)
+{
+ if (blob.size() < sizeof(ICON_STREAMS_HEADER))
+ {
+ throw std::runtime_error("Invalid icon streams header - truncated");
+ }
+
+ auto header = reinterpret_cast<const ICON_STREAMS_HEADER *>(&blob[0]);
+
+ if (header->HeaderSize != sizeof(ICON_STREAMS_HEADER))
+ {
+ throw std::runtime_error("Invalid icon streams header - size mismatch");
+ }
+
+ memcpy(&m_header, header, sizeof(ICON_STREAMS_HEADER));
+
+ if (0 == header->NumberRecords)
+ {
+ return;
+ }
+
+ //
+ // At least one record.
+ //
+
+ if (blob.size() < sizeof(ICON_STREAMS_HEADER) + sizeof(ICON_STREAMS_RECORD))
+ {
+ throw std::runtime_error("Invalid icon streams - truncated");
+ }
+
+ const auto lastValidRecordOffset = blob.size() - sizeof(ICON_STREAMS_RECORD);
+
+ if (header->OffsetFirstRecord < header->HeaderSize
+ || header->OffsetFirstRecord > lastValidRecordOffset)
+ {
+ throw std::runtime_error("Invalid icon streams header - record offset");
+ }
+
+ const auto estimatedSize = header->HeaderSize
+ + (header->OffsetFirstRecord - header->HeaderSize)
+ + (header->NumberRecords * sizeof(ICON_STREAMS_RECORD));
+
+ if (blob.size() != estimatedSize)
+ {
+ throw std::runtime_error("Invalid icon streams - size mismatch");
+ }
+
+ //
+ // Size checks out.
+ //
+
+ m_records.reserve(header->NumberRecords);
+
+ auto begin = reinterpret_cast<const ICON_STREAMS_RECORD *>(&blob[0] + header->OffsetFirstRecord);
+ auto end = reinterpret_cast<const ICON_STREAMS_RECORD *>(&blob[0] + blob.size());
+
+ std::copy(begin, end, std::back_inserter(m_records));
+}
+
+const ICON_STREAMS_HEADER &TrayParser::getHeader() const
+{
+ return m_header;
+}
+
+const std::vector<ICON_STREAMS_RECORD> &TrayParser::getRecords() const
+{
+ return m_records;
+}
diff --git a/windows/nsis-plugins/src/tray/trayparser.h b/windows/nsis-plugins/src/tray/trayparser.h
new file mode 100644
index 0000000000..6aa3186713
--- /dev/null
+++ b/windows/nsis-plugins/src/tray/trayparser.h
@@ -0,0 +1,22 @@
+#pragma once
+
+#include "iconstreams.h"
+#include <cstdint>
+#include <vector>
+#include <memory>
+
+class TrayParser
+{
+public:
+
+ TrayParser(const std::vector<uint8_t> &blob);
+
+ const ICON_STREAMS_HEADER &getHeader() const;
+
+ const std::vector<ICON_STREAMS_RECORD> &getRecords() const;
+
+private:
+
+ ICON_STREAMS_HEADER m_header;
+ std::vector<ICON_STREAMS_RECORD> m_records;
+};