-
Notifications
You must be signed in to change notification settings - Fork 4.7k
/
Environment.GetFolderPathCore.Unix.cs
254 lines (226 loc) · 11.8 KB
/
Environment.GetFolderPathCore.Unix.cs
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
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
using System.Collections;
using System.Collections.Generic;
using System.Diagnostics;
using System.IO;
using System.Reflection;
using System.Runtime.InteropServices;
using System.Text;
using System.Threading;
#if TARGET_OSX
using NSSearchPathDirectory = Interop.Sys.NSSearchPathDirectory;
#endif
namespace System
{
public static partial class Environment
{
private static string GetFolderPathCore(SpecialFolder folder, SpecialFolderOption option)
{
// Get the path for the SpecialFolder
string path = GetFolderPathCoreWithoutValidation(folder) ?? string.Empty;
Debug.Assert(path != null);
// If we didn't get one, or if we got one but we're not supposed to verify it,
// or if we're supposed to verify it and it passes verification, return the path.
if (path.Length == 0 ||
option == SpecialFolderOption.DoNotVerify ||
Interop.Sys.Access(path, Interop.Sys.AccessMode.R_OK) == 0)
{
return path;
}
// Failed verification. If None, then we're supposed to return an empty string.
// If Create, we're supposed to create it and then return the path.
if (option == SpecialFolderOption.None)
{
return string.Empty;
}
Debug.Assert(option == SpecialFolderOption.Create);
Directory.CreateDirectory(path);
return path;
}
private static string? GetFolderPathCoreWithoutValidation(SpecialFolder folder)
{
// First handle any paths that involve only static paths, avoiding the overheads of getting user-local paths.
// https://www.freedesktop.org/software/systemd/man/file-hierarchy.html
switch (folder)
{
case SpecialFolder.CommonApplicationData: return "/usr/share";
case SpecialFolder.CommonTemplates: return "/usr/share/templates";
#if TARGET_OSX
case SpecialFolder.ProgramFiles: return "/Applications";
case SpecialFolder.System: return "/System";
#endif
}
// All other paths are based on the XDG Base Directory Specification:
// https://specifications.freedesktop.org/basedir-spec/latest/
string? home = null;
try
{
home = PersistedFiles.GetHomeDirectory();
}
catch (Exception exc)
{
Debug.Fail($"Unable to get home directory: {exc}");
}
// Fall back to '/' when we can't determine the home directory.
// This location isn't writable by non-root users which provides some safeguard
// that the application doesn't write data which is meant to be private.
if (string.IsNullOrEmpty(home))
{
home = "/";
}
// TODO: Consider caching (or precomputing and caching) all subsequent results.
// This would significantly improve performance for repeated access, at the expense
// of not being responsive to changes in the underlying environment variables,
// configuration files, etc.
switch (folder)
{
case SpecialFolder.UserProfile:
return home;
case SpecialFolder.Templates:
return ReadXdgDirectory(home, "XDG_TEMPLATES_DIR", "Templates");
// TODO: Consider merging the OSX path with the rest of the Apple systems here:
// https://github.com/dotnet/runtime/blob/main/src/libraries/System.Private.CoreLib/src/System/Environment.iOS.cs
#if TARGET_OSX
case SpecialFolder.Desktop:
case SpecialFolder.DesktopDirectory:
return Interop.Sys.SearchPath(NSSearchPathDirectory.NSDesktopDirectory);
case SpecialFolder.ApplicationData:
case SpecialFolder.LocalApplicationData:
return Interop.Sys.SearchPath(NSSearchPathDirectory.NSApplicationSupportDirectory);
case SpecialFolder.MyDocuments: // same value as Personal
return Interop.Sys.SearchPath(NSSearchPathDirectory.NSDocumentDirectory);
case SpecialFolder.MyMusic:
return Interop.Sys.SearchPath(NSSearchPathDirectory.NSMusicDirectory);
case SpecialFolder.MyVideos:
return Interop.Sys.SearchPath(NSSearchPathDirectory.NSMoviesDirectory);
case SpecialFolder.MyPictures:
return Interop.Sys.SearchPath(NSSearchPathDirectory.NSPicturesDirectory);
case SpecialFolder.Fonts:
return Path.Combine(home, "Library", "Fonts");
case SpecialFolder.Favorites:
return Path.Combine(home, "Library", "Favorites");
case SpecialFolder.InternetCache:
return Interop.Sys.SearchPath(NSSearchPathDirectory.NSCachesDirectory);
#else
case SpecialFolder.Desktop:
case SpecialFolder.DesktopDirectory:
return ReadXdgDirectory(home, "XDG_DESKTOP_DIR", "Desktop");
case SpecialFolder.ApplicationData:
return GetXdgConfig(home);
case SpecialFolder.LocalApplicationData:
// "$XDG_DATA_HOME defines the base directory relative to which user specific data files should be stored."
// "If $XDG_DATA_HOME is either not set or empty, a default equal to $HOME/.local/share should be used."
string? data = GetEnvironmentVariable("XDG_DATA_HOME");
if (data is null || !data.StartsWith('/'))
{
data = Path.Combine(home, ".local", "share");
}
return data;
case SpecialFolder.MyDocuments: // same value as Personal
return ReadXdgDirectory(home, "XDG_DOCUMENTS_DIR", "Documents");
case SpecialFolder.MyMusic:
return ReadXdgDirectory(home, "XDG_MUSIC_DIR", "Music");
case SpecialFolder.MyVideos:
return ReadXdgDirectory(home, "XDG_VIDEOS_DIR", "Videos");
case SpecialFolder.MyPictures:
return ReadXdgDirectory(home, "XDG_PICTURES_DIR", "Pictures");
case SpecialFolder.Fonts:
return Path.Combine(home, ".fonts");
#endif
}
// No known path for the SpecialFolder
return string.Empty;
}
private static string GetXdgConfig(string home)
{
// "$XDG_CONFIG_HOME defines the base directory relative to which user specific configuration files should be stored."
// "If $XDG_CONFIG_HOME is either not set or empty, a default equal to $HOME/.config should be used."
string? config = GetEnvironmentVariable("XDG_CONFIG_HOME");
if (config is null || !config.StartsWith('/'))
{
config = Path.Combine(home, ".config");
}
return config;
}
private static string ReadXdgDirectory(string homeDir, string key, string fallback)
{
Debug.Assert(!string.IsNullOrEmpty(homeDir), $"Expected non-empty homeDir");
Debug.Assert(!string.IsNullOrEmpty(key), $"Expected non-empty key");
Debug.Assert(!string.IsNullOrEmpty(fallback), $"Expected non-empty fallback");
string? envPath = GetEnvironmentVariable(key);
if (envPath is not null && envPath.StartsWith('/'))
{
return envPath;
}
// Use the user-dirs.dirs file to look up the right config.
// Note that the docs also highlight a list of directories in which to look for this file:
// "$XDG_CONFIG_DIRS defines the preference-ordered set of base directories to search for configuration files in addition
// to the $XDG_CONFIG_HOME base directory. The directories in $XDG_CONFIG_DIRS should be separated with a colon ':'. If
// $XDG_CONFIG_DIRS is either not set or empty, a value equal to / etc / xdg should be used."
// For simplicity, we don't currently do that. We can add it if/when necessary.
string userDirsPath = Path.Combine(GetXdgConfig(homeDir), "user-dirs.dirs");
if (Interop.Sys.Access(userDirsPath, Interop.Sys.AccessMode.R_OK) == 0)
{
try
{
using (var reader = new StreamReader(userDirsPath))
{
string? line;
while ((line = reader.ReadLine()) != null)
{
// Example lines:
// XDG_DESKTOP_DIR="$HOME/Desktop"
// XDG_PICTURES_DIR = "/absolute/path"
// Skip past whitespace at beginning of line
int pos = 0;
SkipWhitespace(line, ref pos);
if (pos >= line.Length) continue;
// Skip past requested key name
if (string.CompareOrdinal(line, pos, key, 0, key.Length) != 0) continue;
pos += key.Length;
// Skip past whitespace and past '='
SkipWhitespace(line, ref pos);
if (pos >= line.Length - 4 || line[pos] != '=') continue; // 4 for ="" and at least one char between quotes
pos++; // skip past '='
// Skip past whitespace and past first quote
SkipWhitespace(line, ref pos);
if (pos >= line.Length - 3 || line[pos] != '"') continue; // 3 for "" and at least one char between quotes
pos++; // skip past opening '"'
// Skip past relative prefix if one exists
bool relativeToHome = false;
const string RelativeToHomePrefix = "$HOME/";
if (string.CompareOrdinal(line, pos, RelativeToHomePrefix, 0, RelativeToHomePrefix.Length) == 0)
{
relativeToHome = true;
pos += RelativeToHomePrefix.Length;
}
else if (line[pos] != '/') // if not relative to home, must be absolute path
{
continue;
}
// Find end of path
int endPos = line.IndexOf('"', pos);
if (endPos <= pos) continue;
// Got we need. Now extract it.
string path = line.Substring(pos, endPos - pos);
return relativeToHome ?
Path.Combine(homeDir, path) :
path;
}
}
}
catch (Exception exc)
{
// assembly not found, file not found, errors reading file, etc. Just eat everything.
Debug.Fail($"Failed reading {userDirsPath}: {exc}");
}
}
return Path.Combine(homeDir, fallback);
}
private static void SkipWhitespace(string line, ref int pos)
{
while (pos < line.Length && char.IsWhiteSpace(line[pos])) pos++;
}
}
}