forked from microsoft/winget-cli
-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathFonts.cpp
More file actions
1118 lines (987 loc) · 48.7 KB
/
Copy pathFonts.cpp
File metadata and controls
1118 lines (987 loc) · 48.7 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT License.
#include "pch.h"
#include <AppInstallerErrors.h>
#include <AppInstallerLogging.h>
#include <AppInstallerRuntime.h>
#include <AppInstallerStrings.h>
#include <winget/Filesystem.h>
#include <winget/Fonts.h>
#include <winget/Locale.h>
#include <winget/Manifest.h>
#include <winget/ManifestCommon.h>
#include <winget/Registry.h>
#include <ShObjIdl_core.h>
#include <propkey.h>
#include <wingdi.h>
using namespace AppInstaller::Utility;
namespace AppInstaller::Fonts
{
namespace
{
constexpr std::wstring_view s_FontsPathSubkey = L"SOFTWARE\\Microsoft\\Windows NT\\CurrentVersion\\Fonts";
constexpr std::wstring_view s_FontsUserInstallFolder = L"Microsoft\\Windows\\Fonts";
constexpr std::wstring_view s_TrueType = L" (TrueType)";
constexpr std::wstring_view s_IdentifierPrefix = L"FONT";
constexpr std::wstring_view s_Separator = L"\\";
constexpr std::wstring_view s_FontsWinGetPrefix = L"Microsoft.DesktopAppInstaller_8wekyb3d8bbwe";
const int s_RemoveFontResourceMaxTries = 1000;
std::wstring GetFontFileTitle(const std::filesystem::path& fontFilePath)
{
// This code can fail in a number of ways, including if the file isn't actually
// a font file or does not have a title.
wil::com_ptr<IPropertyStore> pPropertyStore;
if (FAILED(SHGetPropertyStoreFromParsingName(fontFilePath.c_str(), nullptr, GPS_DEFAULT, IID_PPV_ARGS(&pPropertyStore))))
{
// Intentionally not throwing here as this is an expected failure case for non-font files or files without titles.
return {};
}
PROPVARIANT prop;
PropVariantInit(&prop);
THROW_IF_FAILED(pPropertyStore->GetValue(PKEY_Title, &prop));
std::wstring title;
if (prop.pwszVal)
{
title = prop.pwszVal;
}
THROW_IF_FAILED(PropVariantClear(&prop));
return title;
}
void AssertPackageInformation(const FontContext& context)
{
if (!context.PackageId.empty() && !context.PackageVersion.empty())
{
return;
}
// This is a programming error if we reach this point where the package identifer cannot be created or derived.
THROW_HR_MSG(E_UNEXPECTED, "Package Id and Version must be provided and non-empty.");
}
std::wstring GetFontRegistryPath(const FontContext& context)
{
// Registry path for a font is well-defined based on installer source and package information.
auto path = std::wstringstream();
path << s_FontsPathSubkey;
switch (context.InstallerSource)
{
case InstallerSource::Unknown:
// Unknown installer could be anywhere, assume it is in the default fonts location.
break;
case InstallerSource::WinGet:
// WinGet path adds the WinGet prefix + package id + version.
path << s_Separator << s_FontsWinGetPrefix << s_Separator << context.PackageId <<
s_Separator << context.PackageVersion;
break;
default:
THROW_HR_MSG(E_UNEXPECTED, "Undefined InstallerSource.");
}
return path.str();
}
std::filesystem::path GetRootFontPath(Manifest::ScopeEnum scope)
{
if (scope == Manifest::ScopeEnum::Machine)
{
return Runtime::GetPathTo(Runtime::PathName::FontsMachineInstallLocation);
}
else
{
return Runtime::GetPathTo(Runtime::PathName::FontsUserInstallLocation);
}
}
std::filesystem::path GetFontFileInstallPath(const FontContext& context)
{
auto path = GetRootFontPath(context.Scope);
switch (context.InstallerSource)
{
case InstallerSource::Unknown:
// Unknown installer is assumed to be installed in the default location for machine.
// For a user installed font it could be anywhere.
break;
case InstallerSource::WinGet:
// WinGet installed packages have two formats depending on the scope.
// For Per-user we use subfolders for better robustness and cleaner format.
// For Per-machine we must use a single folder due to a system requirement,
// so the root path is the machine install path.
if (context.Scope == Manifest::ScopeEnum::User)
{
AssertPackageInformation(context);
path /= s_FontsWinGetPrefix;
path /= context.PackageId;
path /= context.PackageVersion;
}
break;
default:
THROW_HR_MSG(E_UNEXPECTED, "Undefined InstallerSource.");
}
return path;
}
// For machine-installed fonts the files all reside in the same folder. We need a unique name that is reasonably human
// readable. The way system fonts normally do this is by appending a number to the end of the file stem. We will follow
// the same pattern by using the original file and adding a number until we arrive at a unique filename.
std::filesystem::path GetUniquePathForDestination(const std::filesystem::path& source, const std::filesystem::path& destination)
{
const auto stem = source.stem();
const auto ext = source.extension();
auto uniqueName = source.filename();
auto candidatePath = destination / uniqueName;
int index = 0;
while (std::filesystem::exists(candidatePath))
{
std::filesystem::path appendId = { std::to_string(index) };
uniqueName = stem;
uniqueName += appendId;
uniqueName += ext;
candidatePath = destination / uniqueName;
index++;
}
return candidatePath;
}
std::vector<std::filesystem::path> GetFontFilePaths(const wil::com_ptr<IDWriteFontFace>& fontFace)
{
UINT32 fileCount = 0;
THROW_IF_FAILED(fontFace->GetFiles(&fileCount, nullptr));
static_assert(sizeof(wil::com_ptr<IDWriteFontFile>) == sizeof(IDWriteFontFile*));
std::vector<wil::com_ptr<IDWriteFontFile>> fontFiles;
fontFiles.resize(fileCount);
THROW_IF_FAILED(fontFace->GetFiles(&fileCount, fontFiles[0].addressof()));
std::vector<std::filesystem::path> filePaths;
for (UINT32 i = 0; i < fileCount; ++i) {
wil::com_ptr<IDWriteFontFileLoader> loader;
THROW_IF_FAILED(fontFiles[i]->GetLoader(loader.addressof()));
const void* fontFileReferenceKey;
UINT32 fontFileReferenceKeySize;
THROW_IF_FAILED(fontFiles[i]->GetReferenceKey(&fontFileReferenceKey, &fontFileReferenceKeySize));
if (const auto localLoader = loader.try_query<IDWriteLocalFontFileLoader>()) {
UINT32 pathLength;
THROW_IF_FAILED(localLoader->GetFilePathLengthFromKey(fontFileReferenceKey, fontFileReferenceKeySize, &pathLength));
pathLength += 1; // Account for the trailing null terminator during allocation.
std::wstring path;
path.resize(pathLength);
THROW_IF_FAILED(localLoader->GetFilePathFromKey(fontFileReferenceKey, fontFileReferenceKeySize, &path[0], pathLength));
path.resize(pathLength - 1); // Remove the null char.
filePaths.emplace_back(std::move(path));
}
}
return filePaths;
}
void RemoveAllFontResources(const std::filesystem::path& filePath)
{
// The recommended uninstall method of a font is to call RemoveFontResource until it fails,
// This is not guaranteed to remove the file from use, but it is the best we have.
// https://learn.microsoft.com/en-us/windows/win32/api/wingdi/nf-wingdi-removefontresourcea
int i = 0;
while (::RemoveFontResource(filePath.c_str()))
{
// Let us not loop endlessly.
if (++i >= s_RemoveFontResourceMaxTries)
{
break;
}
}
}
void NotifyFontChange()
{
// Send the WM_FONTCHANGE message so the system and apps know that there has been a font change.
// Sometimes this does not have an error when it returns non-zero. To avoid random assert failures
// we will not try to log failures for this call. If it fails it does not affect install state.
auto sendResult = ::SendNotifyMessage(HWND_BROADCAST, WM_FONTCHANGE, 0, 0);
AICLI_LOG(Core, Info, << "Notified system of font change: " << sendResult);
}
// Checks the registry path for the file specified, and returns the key value if it finds one.
std::optional<std::wstring> CheckRegistryForFontFileReference(const std::wstring& registryPath, const std::filesystem::path& filePath, Manifest::ScopeEnum scope)
{
try
{
const auto& hive = scope == Manifest::ScopeEnum::Machine ? HKEY_LOCAL_MACHINE : HKEY_CURRENT_USER;
const auto& key = Registry::Key::OpenIfExists(hive, registryPath, 0UL, KEY_READ);
if (key)
{
// Check for the file being present in this key.
for (const auto& valueRef : key.Values())
{
const auto& value = valueRef.Value();
if (!value.has_value() || (value.value().GetType() != Registry::Value::Type::String))
{
continue;
}
const auto& valueName = ConvertToUTF16(valueRef.Name());
const std::filesystem::path& valuePath = { value->GetValue<Registry::Value::Type::String>() };
if (valuePath.is_relative())
{
// Relative value is OK, just compare the filenames.
// We will assume this is in the standard font paths.
if (valuePath.filename() == filePath.filename())
{
return valueName;
}
}
else
{
if (valuePath == filePath)
{
return valueName;
}
}
}
}
}
CATCH_LOG();
return std::nullopt;
}
// The package registry for machine installed fonts stores the package filename as a registry value.
// This is so we can map the original package filename to the installed filename since they may be different.
std::optional<std::filesystem::path> GetRegisteredMachineFontInstallPath(const std::wstring& registryPath, const std::filesystem::path& filePath)
{
try
{
const auto& key = Registry::Key::OpenIfExists(HKEY_LOCAL_MACHINE, registryPath, 0UL, KEY_READ);
if (key)
{
for (const auto& valueRef : key.Values())
{
const auto& value = valueRef.Value();
if (!value.has_value() || (value.value().GetType() != Registry::Value::Type::String))
{
continue;
}
const auto& valueName = ConvertToUTF16(valueRef.Name());
if (valueName == filePath.filename())
{
const std::filesystem::path& valuePath = { value->GetValue<Registry::Value::Type::String>() };
return valuePath;
}
}
}
}
CATCH_LOG();
return std::nullopt;
}
}
FontResult FontOperationResult::Result() const
{
if (FAILED(HResult))
{
return FontResult::Error;
}
else
{
return FontResult::Success;
}
}
void FontContext::AddPackageFile(const std::filesystem::path& filePath)
{
PackageFiles.push_back(filePath);
}
Manifest::AppsAndFeaturesEntry FontContext::GetAppsAndFeaturesEntry() const
{
Manifest::AppsAndFeaturesEntry entry;
entry.DisplayName = ConvertToUTF8(PackageId);
entry.DisplayVersion = ConvertToUTF8(PackageVersion);
entry.InstallerType = Manifest::InstallerTypeEnum::Font;
// Product Code is the PackageId
entry.ProductCode = ConvertToUTF8(PackageId);
return entry;
}
std::wstring FontContext::GetPackageIdentifier() const
{
auto stream = std::wstringstream();
stream << s_IdentifierPrefix << s_Separator << ConvertToUTF16(Manifest::ScopeToString(Scope)) << s_Separator << PackageId;
return stream.str();
}
// Creates font file info used for install, validation, and enumeration.
FontFileInfo CreateFontFileInfo(const FontContext& context, const std::filesystem::path& filePath, const std::wstring& title)
{
auto fileInfo = FontFileInfo();
fileInfo.FilePath = filePath;
fileInfo.PackageIdentifier = context.GetPackageIdentifier();
fileInfo.PackageId = context.PackageId;
fileInfo.PackageVersion = context.PackageVersion;
fileInfo.InstallerSource = context.InstallerSource;
fileInfo.Scope = context.Scope;
fileInfo.Title = title;
// For font file paths in the registry, we may have a relative filename (because the path is assumed).
if (fileInfo.FilePath.is_relative())
{
// Relative path is assumed to be relative to the default location for the scope.
// We are assuming that a relative file path input refers to an already-installed file.
auto fullPath = GetRootFontPath(fileInfo.Scope);
fullPath /= fileInfo.FilePath;
fileInfo.FilePath = std::move(fullPath);
}
// Get information about the font file itself.
auto fontCatalog = FontCatalog();
fileInfo.WinGetSupported = fontCatalog.IsFontFileSupported(fileInfo.FilePath, fileInfo.FileType);
if (fileInfo.WinGetSupported && fileInfo.Title.empty())
{
try
{
fileInfo.Title = GetFontFileTitle(fileInfo.FilePath);
}
CATCH_LOG();
}
if (fileInfo.Title.empty())
{
// If the title is still empty (such as failure to set it from the File), use the Filename
fileInfo.Title = fileInfo.FilePath.filename().wstring();
}
fileInfo.RegistryPackagePath = GetFontRegistryPath(context);
if (fileInfo.Scope == Manifest::ScopeEnum::Machine && fileInfo.InstallerSource == InstallerSource::WinGet)
{
// Machine fonts from WinGet still install into the Font folder but we track package source separately
// due to limited information in the Font registry and all font files being in the same key and folder.
fileInfo.RegistryInstallPath = s_FontsPathSubkey;
}
else
{
// User-installed and non-Winget fonts have same registry package path and install path.
fileInfo.RegistryInstallPath = fileInfo.RegistryPackagePath;
}
// Check if the file is already in the install path, in which case this is a query.
auto const& installPath = GetFontFileInstallPath(context);
if (installPath == fileInfo.FilePath.parent_path())
{
fileInfo.InstallPath = fileInfo.FilePath;
}
else
{
if (fileInfo.InstallerSource == InstallerSource::WinGet && fileInfo.Scope == Manifest::ScopeEnum::Machine)
{
// Machine installed WinGet fonts must have a non-conflicting filename in the root font folder.
// To ensure a unique font name and allow different versions to exist side by side, the install file
// name may be a different name from the original package install. To account for this, the Package
// registry location uses the original filename as the registry value name and the value as the
// resulting path. If we are installing a machine font that already exists, we must check to see
// if this file already has a mapping defined.
const auto& installedFilePath = GetRegisteredMachineFontInstallPath(fileInfo.RegistryPackagePath, fileInfo.FilePath.filename());
if (installedFilePath.has_value())
{
fileInfo.InstallPath = installedFilePath.value();
}
else
{
// File not already defined, we need to create a non-conflicting filename.
// Machine-installed WinGet must provide a non-conflicting filename to the root install location.
fileInfo.InstallPath = GetUniquePathForDestination(fileInfo.FilePath, installPath);
}
}
else
{
fileInfo.InstallPath = installPath / fileInfo.FilePath.filename();
}
}
// If our font file is registered it will exist in the install path and have a registry value.
const auto& registryTitle = CheckRegistryForFontFileReference(fileInfo.RegistryInstallPath, fileInfo.InstallPath, fileInfo.Scope);
if (fileInfo.Title.empty())
{
// Use the registry title or filename if we still dont' know.
if (registryTitle.has_value())
{
fileInfo.Title = registryTitle.value();
}
else
{
fileInfo.Title = fileInfo.FilePath.filename().wstring();
}
}
fileInfo.IsFontFileInstalled = std::filesystem::exists(fileInfo.InstallPath) ? true : false;
fileInfo.IsFontFileRegistered = registryTitle.has_value() ? true : false;
if (fileInfo.IsFontFileInstalled && fileInfo.IsFontFileRegistered)
{
fileInfo.Status = FontStatus::OK;
}
else if (fileInfo.IsFontFileInstalled || fileInfo.IsFontFileRegistered)
{
fileInfo.Status = FontStatus::Corrupt;
}
else
{
fileInfo.Status = FontStatus::Absent;
}
return fileInfo;
}
FontValidationResult ValidateFontPackage(FontContext& context)
{
AssertPackageInformation(context);
auto result = FontValidationResult();
AICLI_LOG(Core, Info, << "Validating font package: " << context.PackageId.c_str() << " " << context.PackageVersion.c_str());
try
{
// Create font file info for each package file
for (const auto& file : context.PackageFiles)
{
result.FontFileInfos.push_back(CreateFontFileInfo(context, file));
}
// We create an overall status for the validation with the following determination:
// If ALL of the files are OK, the package is OK.
// If ALL of the files are Absent, the package is Absent.
// If there is any combination, the package is Corrupt.
// We must check each file info to see if the package is supported by WinGet.
// If any file is unsupported then there are unsupported fonts in the package.
for (const auto& fontFileInfo : result.FontFileInfos)
{
if (!fontFileInfo.WinGetSupported)
{
result.HasUnsupportedFonts = true;
}
if (fontFileInfo.Status != result.Status)
{
if (result.Status == FontStatus::Unknown)
{
// This is the first status we've seen, make it the default.
result.Status = fontFileInfo.Status;
}
else
{
// Previous status now differs from current status, package is corrupt.
result.Status = FontStatus::Corrupt;
}
}
}
result.Result = FontResult::Success;
}
catch (...)
{
LOG_CAUGHT_EXCEPTION();
result.HResult = APPINSTALLER_CLI_ERROR_FONT_VALIDATION_FAILED;
result.Result = FontResult::Error;
}
return result;
}
FontOperationResult InstallFontPackage(FontContext& context)
{
AssertPackageInformation(context);
FontOperationResult result;
if (context.InstallerSource != InstallerSource::WinGet)
{
throw std::logic_error("Only WinGet format of font install is supported.");
}
if (context.PackageFiles.size() == 0)
{
result.HResult = winrt::hresult(APPINSTALLER_CLI_ERROR_FONT_FILE_NOT_FOUND);
AICLI_LOG(Core, Error, << "Font package has no files: " << context.PackageId.c_str() << " - " << AppInstaller::Logging::SetHRFormat << result.HResult);
return result;
}
// Validate the package data was all processed successfully.
const auto& validationResult = ValidateFontPackage(context);
if (validationResult.Result != FontResult::Success)
{
result.HResult = winrt::hresult(APPINSTALLER_CLI_ERROR_FONT_FILE_NOT_SUPPORTED);
AICLI_LOG(Core, Error, << "Font package validation failed: " << context.PackageId.c_str() << " - " << AppInstaller::Logging::SetHRFormat << validationResult.HResult);
return result;
}
// Check for unsupported font files.
if (validationResult.HasUnsupportedFonts)
{
result.HResult = winrt::hresult(APPINSTALLER_CLI_ERROR_FONT_FILE_NOT_SUPPORTED);
AICLI_LOG(Core, Error, << "Font package has unsupported fonts: " << context.PackageId.c_str() << " - " << result.HResult);
return result;
}
// Check for package already installed and correct unless this is a force install.
if (validationResult.Status == FontStatus::OK && !context.Force)
{
result.HResult = winrt::hresult(APPINSTALLER_CLI_ERROR_FONT_ALREADY_INSTALLED);
AICLI_LOG(Core, Info, << "Font package is already installed and in a good state.: " << context.PackageId.c_str() << " - " << AppInstaller::Logging::SetHRFormat << result.HResult);
return result;
}
// We will attempt a cleanup / force install if either this is a forced install or the package was not in a good state.
if (validationResult.Status == FontStatus::Corrupt || context.Force)
{
// Force install of a font can have problems because if the font is already there then it
// may be in use. The scenarios for using force is if a prior install attempt failed, so
// we will try to clean up the existing font registration so it may be successful this time.
AICLI_LOG(Core, Info, << "Package is corrupt or forced install, attempting to remove any existing registration.");
auto uninstallResult = UninstallFontPackage(context);
if (uninstallResult.Result() != FontResult::Success)
{
result.HResult = uninstallResult.HResult;
AICLI_LOG(Core, Error, << "Font cleanup uninstall failed: " << context.PackageId.c_str() << " - " << AppInstaller::Logging::SetHRFormat << uninstallResult.HResult);
return result;
}
}
AICLI_LOG(Core, Info, << "Starting install of " << context.PackageId.c_str());
try
{
// Install each file from the file info.
for (const auto& fontFileInfo : validationResult.FontFileInfos)
{
// Font install step 1: Copy file to winget package identifiable location.
AICLI_LOG(Core, Info, << "Moving " << fontFileInfo.FilePath << " to " << fontFileInfo.InstallPath);
if (!std::filesystem::exists(fontFileInfo.InstallPath.parent_path()))
{
std::filesystem::create_directories(fontFileInfo.InstallPath.parent_path());
}
// The file should not be present, but if it is, do another attempt to remove it.
if (std::filesystem::exists(fontFileInfo.InstallPath))
{
// Try to remove font resource to avoid file-in-use.
RemoveAllFontResources(fontFileInfo.InstallPath);
if (!std::filesystem::remove(fontFileInfo.InstallPath))
{
AICLI_LOG(Core, Error, << "Font file already exists and was unable to be removed.");
THROW_HR(APPINSTALLER_CLI_ERROR_FONT_INSTALL_FAILED);
}
}
if (context.Scope == Manifest::ScopeEnum::Machine)
{
// Using std::filesystem::rename does not work when installing into the system Fonts
// folder; the system does not recognize the font files. std::filesystem::copy_file does work.
std::filesystem::copy_file(fontFileInfo.FilePath, fontFileInfo.InstallPath);
std::filesystem::remove(fontFileInfo.FilePath);
}
else
{
// We prefer to use rename where it works.
AppInstaller::Filesystem::RenameFile(fontFileInfo.FilePath, fontFileInfo.InstallPath);
}
AICLI_LOG(Core, Info, << "Adding " << fontFileInfo.Title.c_str() << " to " << fontFileInfo.InstallPath);
if (context.Scope == Manifest::ScopeEnum::User)
{
auto key = Registry::Key::Create(HKEY_CURRENT_USER, fontFileInfo.RegistryInstallPath);
key.SetValue(fontFileInfo.Title, fontFileInfo.InstallPath, REG_SZ);
}
else
{
// Machine install we set two keys, one for source information, and one for the actual install.
// The value name is the source filename so we can map this later for validation.
auto sourceKey = Registry::Key::Create(HKEY_LOCAL_MACHINE, fontFileInfo.RegistryPackagePath, 0UL, KEY_ALL_ACCESS);
sourceKey.SetValue(fontFileInfo.FilePath.filename(), fontFileInfo.InstallPath, REG_SZ);
// Machine font install uses relative filename. Use filename as value to avoid collisions in the key.
auto installKey = Registry::Key::OpenIfExists(HKEY_LOCAL_MACHINE, fontFileInfo.RegistryInstallPath, 0UL, KEY_ALL_ACCESS);
installKey.SetValue(fontFileInfo.InstallPath.filename(), fontFileInfo.InstallPath.filename(), REG_SZ);
}
// Add Font Resource to the session.
const auto& fontsAdded = ::AddFontResource(fontFileInfo.InstallPath.c_str());
if (fontsAdded == 0)
{
// AddFontResource does not add additional information for us, so we dont know why they were not added,
// only that they were not added. At this point the "install" is successful, but we were not able to
// add the font to the system, which means subsequent adds on session start may also fail, but is not
// guaranteed to fail. We will note it in the log and carry on.
AICLI_LOG(Core, Warning, << "Failed to add font resource: " << fontFileInfo.InstallPath);
}
else
{
AICLI_LOG(Core, Info, << "Added " << fontsAdded << " fonts to the session.");
}
}
result.HResult = S_OK;
}
catch (...)
{
LOG_CAUGHT_EXCEPTION();
AICLI_LOG(Core, Error, << "Install failed for " << context.PackageId.c_str() << ", attempting rollback.");
try
{
// Rollback. In this case, rollback is uninstall, which should remove any partial installation.
// This is best-effort.
auto rollbackResult = UninstallFontPackage(context);
if (rollbackResult.Result() == FontResult::Success)
{
AICLI_LOG(Core, Info, << "Rollback for " << context.PackageId.c_str() << " successful.");
}
else
{
AICLI_LOG(Core, Error, << "Rollback for " << context.PackageId.c_str() << " failed: " << AppInstaller::Logging::SetHRFormat << rollbackResult.HResult);
}
}
CATCH_LOG();
result.HResult = APPINSTALLER_CLI_ERROR_FONT_INSTALL_FAILED;
}
// Regardless of the result there's likely some changes that occurred.
NotifyFontChange();
return result;
}
FontOperationResult UninstallFontPackage(FontContext& context)
{
FontOperationResult result;
if (context.InstallerSource != InstallerSource::WinGet)
{
throw std::logic_error("Only WinGet format of font package uninstall is supported.");
}
AssertPackageInformation(context);
const auto& installFolderPath = GetFontFileInstallPath(context);
const auto& installPackagePath = GetFontRegistryPath(context);
std::vector<std::filesystem::path> filesToRemove;
try
{
auto hive = context.Scope == Manifest::ScopeEnum::Machine ? HKEY_LOCAL_MACHINE : HKEY_CURRENT_USER;
auto key = Registry::Key::OpenIfExists(hive, installPackagePath, 0ul, KEY_ALL_ACCESS);
if (key)
{
// Assume all values in this key are fonts related to this package.
for (const auto& fontEntry : key.Values())
{
auto value = fontEntry.Value();
if (!value.has_value() || (value.value().GetType() != Registry::Value::Type::String))
{
// Not a valid entry, nothing to do here.
continue;
}
std::filesystem::path filePath = { value->GetValue<Registry::Value::Type::String>() };
if (!std::filesystem::exists(filePath))
{
// File doesn't exist, nothing to do here.
continue;
}
// The font may be in use by the system or other apps, it needs to be removed from
// from use or at least attempted to be removed from use.
RemoveAllFontResources(filePath);
filesToRemove.push_back(filePath);
}
// Delete the key
if (!Registry::Key::DeleteTree(hive, installPackagePath))
{
AICLI_LOG(Core, Warning, << "Failed removing registry tree " << installPackagePath.c_str());
}
if (context.Scope == Manifest::ScopeEnum::Machine)
{
// Machine installed fonts also have an entry in the System font registry.
// We have to find the values that have the filename as data.
auto machineFontRoot = Registry::Key::OpenIfExists(HKEY_LOCAL_MACHINE, s_FontsPathSubkey.data(), 0ul, KEY_ALL_ACCESS);
for (const auto& file : filesToRemove)
{
machineFontRoot.DeleteValue(file.filename());
}
}
}
}
catch (...)
{
LOG_CAUGHT_EXCEPTION();
// For uninstall we will log the error and continue trying to remove it, since we have partial remove and are in an unknown state.
AICLI_LOG(Core, Error, << "Failed removing fonts in the registry.");
}
// Remove any font files.
for (const auto& file : filesToRemove)
{
try
{
if (std::filesystem::exists(file))
{
// TODO: Add robustness for files-in-use scenarios.
std::filesystem::remove(file);
}
}
catch (...)
{
LOG_CAUGHT_EXCEPTION();
AICLI_LOG(Core, Error, << "Failed removing font files.");
result.HResult = winrt::hresult(APPINSTALLER_CLI_ERROR_FONT_UNINSTALL_FAILED);
}
}
if (context.Scope == Manifest::ScopeEnum::User)
{
// User installed fonts also have an install folder, we will clean that up as well.
if (std::filesystem::exists(installFolderPath))
{
try
{
const auto& parent = installFolderPath.parent_path();
std::filesystem::remove_all(installFolderPath);
// This may have been the only version installed, so remove parent if empty.
if (std::filesystem::is_empty(parent))
{
std::filesystem::remove(parent);
}
}
catch (...)
{
// Font files have already been removed at this point, so failure to delete
// this folder is not a failure of removing the fonts.
LOG_CAUGHT_EXCEPTION();
AICLI_LOG(Core, Error, << "Failed removing font folders.");
}
}
}
// Notify system of font changes.
NotifyFontChange();
return result;
}
std::wstring GetFontRegistryRoot()
{
return s_FontsPathSubkey.data();
}
FontCatalog::FontCatalog()
{
m_preferredLocales = AppInstaller::Locale::GetUserPreferredLanguagesUTF16();
THROW_IF_FAILED(DWriteCreateFactory(DWRITE_FACTORY_TYPE_SHARED, __uuidof(m_factory), m_factory.put_unknown()));
}
std::vector<FontFamily> FontCatalog::GetInstalledFontFamilies(std::optional<std::wstring> familyName)
{
wil::com_ptr<IDWriteFontCollection> collection;
THROW_IF_FAILED(m_factory->GetSystemFontCollection(collection.addressof(), FALSE));
std::vector<FontFamily> installedFontFamilies;
if (familyName.has_value())
{
UINT32 index;
BOOL exists;
THROW_IF_FAILED(collection->FindFamilyName(familyName.value().c_str(), &index, &exists));
if (exists)
{
installedFontFamilies.emplace_back(GetFontFamilyByIndex(collection, index));
}
}
else
{
UINT32 familyCount = collection->GetFontFamilyCount();
for (UINT32 index = 0; index < familyCount; index++)
{
installedFontFamilies.emplace_back(GetFontFamilyByIndex(collection, index));
}
}
return installedFontFamilies;
}
bool FontCatalog::IsFontFileSupported(const std::filesystem::path& filePath, DWRITE_FONT_FILE_TYPE& fileType)
{
if (!std::filesystem::exists(filePath))
{
fileType = DWRITE_FONT_FILE_TYPE_UNKNOWN;
return false;
}
wil::com_ptr<IDWriteFontFile> fontFile;
THROW_IF_FAILED(m_factory->CreateFontFileReference(filePath.c_str(), NULL, &fontFile));
BOOL isSupported;
DWRITE_FONT_FACE_TYPE faceType;
UINT32 numOfFaces;
THROW_IF_FAILED(fontFile->Analyze(&isSupported, &fileType, &faceType, &numOfFaces));
return isSupported;
}
std::wstring FontCatalog::GetLocalizedStringFromFont(const wil::com_ptr<IDWriteLocalizedStrings>& localizedStringCollection)
{
UINT32 index = 0;
BOOL exists = false;
for (const auto& locale : m_preferredLocales)
{
if (SUCCEEDED_LOG(localizedStringCollection->FindLocaleName(locale.c_str(), &index, &exists)) && exists)
{
break;
}
}
// If the locale does not exist, resort to the default value at the 0 index.
if (!exists)
{
index = 0;
}
UINT32 length = 0;
THROW_IF_FAILED(localizedStringCollection->GetStringLength(index, &length));
length += 1; // Account for the trailing null terminator during allocation.
std::wstring localizedString;
localizedString.resize(length);
THROW_IF_FAILED(localizedStringCollection->GetString(index, &localizedString[0], length));
localizedString.resize(length - 1); // Remove the null char.
return localizedString;
}
std::wstring FontCatalog::GetFontFaceName(const wil::com_ptr<IDWriteFont>& font)
{
wil::com_ptr<IDWriteLocalizedStrings> faceNames;
THROW_IF_FAILED(font->GetFaceNames(faceNames.addressof()));
return GetLocalizedStringFromFont(faceNames);
}
std::wstring FontCatalog::GetFontFamilyName(const wil::com_ptr<IDWriteFontFamily>& fontFamily)
{
wil::com_ptr<IDWriteLocalizedStrings> familyNames;
THROW_IF_FAILED(fontFamily->GetFamilyNames(familyNames.addressof()));
return GetLocalizedStringFromFont(familyNames);
}
Utility::OpenTypeFontVersion FontCatalog::GetFontFaceVersion(const wil::com_ptr<IDWriteFont>& font)
{
wil::com_ptr<IDWriteLocalizedStrings> fontVersion;
BOOL exists;
THROW_IF_FAILED(font->GetInformationalStrings(DWRITE_INFORMATIONAL_STRING_VERSION_STRINGS, fontVersion.addressof(), &exists));
if (!exists)
{
return {};
}
std::string value = ConvertToUTF8(GetLocalizedStringFromFont(fontVersion));
Utility::OpenTypeFontVersion openTypeFontVersion{ value };
return openTypeFontVersion;
}
FontFamily FontCatalog::GetFontFamilyByIndex(const wil::com_ptr<IDWriteFontCollection>& collection, UINT32 index)
{
wil::com_ptr<IDWriteFontFamily> family;
THROW_IF_FAILED(collection->GetFontFamily(index, family.addressof()));
std::wstring familyName = GetFontFamilyName(family);
std::vector<FontFace> fontFaces;
UINT32 fontCount = family->GetFontCount();
for (UINT32 fontIndex = 0; fontIndex < fontCount; fontIndex++)
{
wil::com_ptr<IDWriteFont> font;
THROW_IF_FAILED(family->GetFont(fontIndex, font.addressof()));
wil::com_ptr<IDWriteFontFace> fontFace;
THROW_IF_FAILED(font->CreateFontFace(fontFace.addressof()));
FontFace fontFaceEntry;
fontFaceEntry.Name = GetFontFaceName(font);
fontFaceEntry.Version = GetFontFaceVersion(font);
fontFaceEntry.FilePaths = GetFontFilePaths(fontFace);
fontFaces.emplace_back(std::move(fontFaceEntry));
}
FontFamily fontFamily;
fontFamily.Name = std::move(familyName);
fontFamily.Faces = std::move(fontFaces);
return fontFamily;
}
// This will create an inventory of all known permanently installed fonts to the user.
// A font is "permanently installed" if it is present in the Font registry for the machine or the user.
// This will not include fonts that are temporarily installed for the session.
std::vector<FontFileInfo> GetInstalledFontFiles()
{
auto fontFiles = std::vector<FontFileInfo>();
try
{
auto wingetMachineFonts = std::unordered_map<std::filesystem::path, bool>();
// Iterate through scopes for machine and user
for (const auto& scope : { Manifest::ScopeEnum::Machine, Manifest::ScopeEnum::User })
{
auto hive = scope == Manifest::ScopeEnum::Machine ? HKEY_LOCAL_MACHINE : HKEY_CURRENT_USER;
auto root = Registry::Key::OpenIfExists(hive, std::wstring{ s_FontsPathSubkey });
// There are two supported scenarios for tracking sub-keys.
// 1) We created the subkey, therefore it is a winget installed font.
// 2) A package created the subkey for a package-deployed font
// We assume that all sub-keys not the WinGet key are packaged keys. It is not guaranteed that
// a package created the font, but we will assume that it is to be safe in how we handle them.
for (const auto& subkey : root)
{
auto subkeyName = ConvertToUTF16(subkey.Name());
auto subkeyKey = subkey.Open();
if (subkeyName == s_FontsWinGetPrefix)
{
// Assume all sub-keys are WinGet Packages
for (const auto& packageSubKey : subkeyKey)
{
// All sub-keys should be versions of the package.
auto wingetPackageSubKey = packageSubKey.Open();
for (const auto& versionSubKey : wingetPackageSubKey)
{
auto packageId = ConvertToUTF16(packageSubKey.Name());
auto version = ConvertToUTF16(versionSubKey.Name());
auto packageVersionSubKey = versionSubKey.Open();
for (const auto& versionValue : packageVersionSubKey.Values())
{
// Values are the files.
auto value = versionValue.Value();
if (!value.has_value() || (value.value().GetType() != Registry::Value::Type::String))
{
continue;
}
std::filesystem::path filePath = { value->GetValue<Registry::Value::Type::String>() };
if (scope == Manifest::ScopeEnum::Machine)
{
// Capture the filename for winget machine fonts so we can avoid duplicates.
wingetMachineFonts[filePath.filename()] = true;
}
auto context = FontContext();
context.Scope = scope;
context.InstallerSource = InstallerSource::WinGet;
context.PackageId = packageId;
context.PackageVersion = version;
auto fontFile = CreateFontFileInfo(context, filePath, ConvertToUTF16(versionValue.Name()));
fontFiles.push_back(std::move(fontFile));
}
}
}