@@ -19,78 +19,88 @@ public enum Key {
19
19
20
20
}
21
21
22
- private const uint MUI_LANGUAGE_NAME = 8 ;
22
+ public static readonly IReadOnlyList < string > LOCALE_NAMES = ( ( List < string > ) [ CultureInfo . CurrentCulture . Name , CultureInfo . CurrentUICulture . Name ] ) . Concat ( getCurrentSystemLocaleNames ( ) ) . Distinct ( )
23
+ . ToList ( ) ;
23
24
24
- public static string userLocaleName { get ; } = CultureInfo . CurrentUICulture . Name ;
25
- public static string systemLocaleName { get ; } = getCurrentSystemLocaleName ( ) ;
26
- private static CultureInfo systemCulture { get ; } = CultureInfo . GetCultureInfo ( systemLocaleName ) ;
27
-
28
- private static readonly FrozenDictionary < Key , IList < string > > STRINGS ;
29
- private static readonly StringComparer STRING_COMPARER = StringComparer . CurrentCulture ;
25
+ private static readonly FrozenDictionary < Key , IList < string > > STRINGS ;
26
+ private static readonly StringComparer STRING_COMPARER = StringComparer . CurrentCulture ;
27
+ private static readonly IDictionary < string , PortableExecutableImage ? > DLL_CACHE = new Dictionary < string , PortableExecutableImage ? > ( ) ;
28
+ private static readonly IDictionary < ( string , int ) , StringTable ? > STRING_TABLE_CACHE = new Dictionary < ( string , int ) , StringTable ? > ( ) ;
30
29
31
30
static I18N ( ) {
32
31
StringTableResource . Register ( ) ;
33
-
34
32
string systemRoot = Environment . GetEnvironmentVariable ( "SystemRoot" ) ?? "C:\\ Windows" ;
35
- // #2: CredentialUIBroker.exe runs as the current user
36
- IList < string ? > fidoCredProvStrings = getPeFileStrings ( Path . Combine ( systemRoot , "System32" , userLocaleName , "fidocredprov.dll.mui" ) , [
37
- ( 15 , 230 ) , // Security key
38
- ( 15 , 231 ) , // Smartphone; also appears in webauthn.dll.mui string table 4 entries 50 and 56
39
- ( 15 , 232 ) // Windows
40
- ] ) ;
41
-
42
- // #2: CryptSvc runs as NETWORK SERVICE
43
- IList < string ? > webauthnStrings = getPeFileStrings ( Path . Combine ( systemRoot , "System32" , systemLocaleName , "webauthn.dll.mui" ) , [
44
- ( 4 , 53 ) // Sign In With Your Passkey title; entry 63 has the same value, not sure which one is used
45
- ] ) ;
46
33
47
34
STRINGS = new Dictionary < Key , IList < string > > {
48
- [ Key . SECURITY_KEY ] = getUniqueNonNullStrings ( Strings . securityKey , fidoCredProvStrings [ 0 ] ) ,
49
- [ Key . SMARTPHONE ] = getUniqueNonNullStrings ( Strings . smartphone , fidoCredProvStrings [ 1 ] ) ,
50
- [ Key . WINDOWS ] = getUniqueNonNullStrings ( Strings . windows , fidoCredProvStrings [ 2 ] ) ,
51
- [ Key . SIGN_IN_WITH_YOUR_PASSKEY ] = getUniqueNonNullStrings ( Strings . ResourceManager . GetString ( nameof ( Strings . signInWithYourPasskey ) , systemCulture ) ,
52
- webauthnStrings [ 0 ] ) ,
35
+ [ Key . SECURITY_KEY ] = getStrings ( nameof ( Strings . securityKey ) , fidoCredProvMuiPath , 15 , 230 ) , // Security key
36
+ [ Key . SMARTPHONE ] = getStrings ( nameof ( Strings . smartphone ) , fidoCredProvMuiPath , 15 , 231 ) , // Smartphone; also appears in webauthn.dll.mui string table 4 entries 50 and 56
37
+ [ Key . WINDOWS ] = getStrings ( nameof ( Strings . windows ) , fidoCredProvMuiPath , 15 , 232 ) , // Windows
38
+ [ Key . SIGN_IN_WITH_YOUR_PASSKEY ] = getStrings ( nameof ( Strings . signInWithYourPasskey ) , webAuthnMuiPath , 4 , 53 ) // Sign In With Your Passkey title; entry 63 has the same value
53
39
} . ToFrozenDictionary ( ) ;
54
40
55
- static IList < string > getUniqueNonNullStrings ( params string ? [ ] strings ) => strings . Compact ( ) . Distinct ( STRING_COMPARER ) . ToList ( ) ;
41
+ foreach ( PortableExecutableImage ? dllFile in DLL_CACHE . Values ) {
42
+ dllFile ? . Dispose ( ) ;
43
+ }
44
+
45
+ STRING_TABLE_CACHE . Clear ( ) ;
46
+ DLL_CACHE . Clear ( ) ;
47
+
48
+ string fidoCredProvMuiPath ( string locale ) => Path . Combine ( systemRoot , "System32" , locale , "fidocredprov.dll.mui" ) ;
49
+ string webAuthnMuiPath ( string locale ) => Path . Combine ( systemRoot , "System32" , locale , "webauthn.dll.mui" ) ;
56
50
}
57
51
58
52
public static IEnumerable < string > getStrings ( Key key ) => STRINGS [ key ] ;
59
53
60
- private static IList < string ? > getPeFileStrings ( string peFile , IList < ( int stringTableId , int stringTableEntryId ) > queries ) {
61
- try {
62
- using PortableExecutableImage file = PortableExecutableImage . FromFile ( peFile ) ;
54
+ // #18: The most-preferred language pack can be missing MUI files if it was installed after Windows, so always fall back to all other preferred languages
55
+ private static IList < string > getStrings ( string compiledResourceName , Func < string , string > libraryPath , int stringTableId , int stringTableEntryId ) =>
56
+ LOCALE_NAMES . SelectMany ( locale => ( List < string ? > ) [
57
+ Strings . ResourceManager . GetString ( compiledResourceName , CultureInfo . GetCultureInfo ( locale ) ) ,
58
+ getPeFileString ( libraryPath ( locale ) , stringTableId , stringTableEntryId )
59
+ ] ) . Compact ( ) . Distinct ( STRING_COMPARER ) . ToList ( ) ;
63
60
64
- IDictionary < int , StringTable ? > stringTableCache = new Dictionary < int , StringTable ? > ( queries . Count ) ;
65
- ResourceType ? stringTables = ResourceCollection . Get ( file ) . FirstOrDefault ( type => type . Id == ResourceType . String ) ;
66
- IList < string ? > results = new List < string ? > ( queries . Count ) ;
61
+ private static string ? getPeFileString ( string peFile , int stringTableId , int stringTableEntryId ) {
62
+ try {
63
+ if ( ! STRING_TABLE_CACHE . TryGetValue ( ( peFile , stringTableId ) , out StringTable ? stringTable ) ) {
64
+ if ( ! DLL_CACHE . TryGetValue ( peFile , out PortableExecutableImage ? file ) ) {
65
+ try {
66
+ file = PortableExecutableImage . FromFile ( peFile ) ;
67
+ } catch ( FileNotFoundException ) { } catch ( DirectoryNotFoundException ) { }
68
+ DLL_CACHE . Add ( peFile , file ) ;
69
+ }
67
70
68
- foreach ( ( int stringTableId , int stringTableEntryId ) in queries ) {
69
- if ( ! stringTableCache . TryGetValue ( stringTableId , out StringTable ? stringTable ) ) {
71
+ if ( file != null ) {
72
+ ResourceType ? stringTables = ResourceCollection . Get ( file ) . FirstOrDefault ( type => type . Id == ResourceType . String ) ;
70
73
StringTableResource ? stringTableResource = stringTables ? . FirstOrDefault ( resource => resource . Id == stringTableId ) as StringTableResource ;
71
74
stringTable = stringTableResource ? . GetTable ( stringTableResource . Languages [ 0 ] ) ; // #2: use the table's language, not always English
72
-
73
- stringTableCache [ stringTableId ] = stringTable ;
74
75
}
75
76
76
- results . Add ( stringTable ? . FirstOrDefault ( entry => entry . Id == stringTableEntryId ) ? . Value ) ;
77
+ STRING_TABLE_CACHE . Add ( ( peFile , stringTableId ) , stringTable ) ;
77
78
}
78
79
79
- return results ;
80
- } catch ( FileNotFoundException ) { } catch ( DirectoryNotFoundException ) { } catch ( PortableExecutableImageException ) { }
81
-
82
- return Enumerable . Repeat < string ? > ( null , queries . Count ) . ToList ( ) ;
80
+ return stringTable ? . FirstOrDefault ( entry => entry . Id == stringTableEntryId ) ? . Value ;
81
+ } catch ( PortableExecutableImageException ) {
82
+ return null ;
83
+ }
83
84
}
84
85
85
- private static unsafe string getCurrentSystemLocaleName ( ) {
86
- int bufferSize = 0 ;
86
+ private static unsafe string [ ] getCurrentSystemLocaleNames ( ) {
87
+ const uint MUI_LANGUAGE_NAME = 8 ;
88
+ int bufferSize = 0 ;
87
89
getSystemPreferredUILanguages ( MUI_LANGUAGE_NAME , out _ , null , ref bufferSize ) ;
88
90
char [ ] buffer = new char [ bufferSize ] ;
91
+ uint languageCount ;
89
92
fixed ( char * bufferStart = & buffer [ 0 ] ) {
90
- getSystemPreferredUILanguages ( MUI_LANGUAGE_NAME , out _ , bufferStart , ref bufferSize ) ;
93
+ getSystemPreferredUILanguages ( MUI_LANGUAGE_NAME , out languageCount , bufferStart , ref bufferSize ) ;
94
+ }
95
+ var resultsBuffer = new ReadOnlySpan < char > ( buffer , 0 , bufferSize ) ;
96
+ // #18: Get all preferred languages, not just the first one, in case the most-preferred language pack is missing MUI files
97
+ var resultsSplit = new Range [ languageCount ] ;
98
+ resultsBuffer . Trim ( '\0 ' ) . Split ( resultsSplit , '\0 ' ) ; // ReadOnlySpan.Split will leave delimiters intact if the destination span length is 1, which sucks, so trim early
99
+ string [ ] results = new string [ languageCount ] ;
100
+ for ( int i = 0 ; i < languageCount ; i ++ ) {
101
+ results [ i ] = resultsBuffer [ resultsSplit [ i ] ] . ToString ( ) ;
91
102
}
92
- var results = new ReadOnlySpan < char > ( buffer , 0 , bufferSize ) ;
93
- return new string ( results [ ..results . IndexOf ( '\0 ' ) ] ) ; // only return the first language name, even if buffer contains more than one (null-delimited)
103
+ return results ;
94
104
}
95
105
96
106
/// <summary>
0 commit comments