Skip to content

Commit 372bf42

Browse files
authored
[mono] Implement Environment.GetFolderPath on iOS (#34022)
* Implement Environment.GetFolderPath on iOS * Address feedback * Move GetFolderPathCore to Environment.Unix.GetFolderPathCore.cs * Fix build issue * Address feedback * cache all special directories * Fix build issue * remove a whitespace * Fix UserProfile issue * undo changes in GetEnvironmentVariableCore * Update Environment.Unix.Mono.cs * Extract to InternalGetEnvironmentVariable * Fix build issue * Return emtpy string if underlying native function returns null * Add nullability
1 parent be469ad commit 372bf42

File tree

10 files changed

+422
-241
lines changed

10 files changed

+422
-241
lines changed
Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
// Licensed to the .NET Foundation under one or more agreements.
2+
// The .NET Foundation licenses this file to you under the MIT license.
3+
// See the LICENSE file in the project root for more information.
4+
5+
#nullable enable
6+
using System;
7+
using System.Runtime.InteropServices;
8+
9+
internal static partial class Interop
10+
{
11+
internal static partial class Sys
12+
{
13+
[DllImport(Libraries.SystemNative, EntryPoint = "SystemNative_SearchPath")]
14+
internal static extern string? SearchPath(NSSearchPathDirectory folderId);
15+
16+
internal enum NSSearchPathDirectory
17+
{
18+
NSApplicationDirectory = 1,
19+
NSLibraryDirectory = 5,
20+
NSUserDirectory = 7,
21+
NSDocumentDirectory = 9,
22+
NSDesktopDirectory = 12,
23+
NSCachesDirectory = 13,
24+
NSMoviesDirectory = 17,
25+
NSMusicDirectory = 18,
26+
NSPicturesDirectory = 19
27+
}
28+
}
29+
}

src/libraries/Native/Unix/System.Native/CMakeLists.txt

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,9 @@ set(NATIVE_SOURCES
2323
)
2424

2525
if (CLR_CMAKE_TARGET_IOS)
26-
set(NATIVE_SOURCES ${NATIVE_SOURCES} pal_log.m)
26+
set(NATIVE_SOURCES ${NATIVE_SOURCES}
27+
pal_log.m
28+
pal_searchpath.m)
2729
else ()
2830
set(NATIVE_SOURCES ${NATIVE_SOURCES} pal_console.c)
2931
endif ()
Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
// Licensed to the .NET Foundation under one or more agreements.
2+
// The .NET Foundation licenses this file to you under the MIT license.
3+
// See the LICENSE file in the project root for more information.
4+
5+
#pragma once
6+
7+
#include "pal_compiler.h"
8+
#include "pal_types.h"
9+
10+
PALEXPORT const char* SystemNative_SearchPath(int32_t folderId);
Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
// Licensed to the .NET Foundation under one or more agreements.
2+
// The .NET Foundation licenses this file to you under the MIT license.
3+
// See the LICENSE file in the project root for more information.
4+
5+
#include "pal_searchpath.h"
6+
#import <Foundation/Foundation.h>
7+
8+
const char* SystemNative_SearchPath(int32_t folderId)
9+
{
10+
NSSearchPathDirectory spd = (NSSearchPathDirectory) folderId;
11+
NSURL* url = [[[NSFileManager defaultManager] URLsForDirectory:spd inDomains:NSUserDomainMask] lastObject];
12+
const char* path = [[url path] UTF8String];
13+
return path == NULL ? NULL : strdup (path);
14+
}

src/libraries/System.Private.CoreLib/src/System.Private.CoreLib.Shared.projitems

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1723,6 +1723,7 @@
17231723
<Compile Include="$(MSBuildThisFileDirectory)System\Diagnostics\Tracing\RuntimeEventSourceHelper.Unix.cs" Condition="'$(FeaturePerfTracing)' == 'true'" />
17241724
<Compile Include="$(MSBuildThisFileDirectory)System\Environment.NoRegistry.cs" />
17251725
<Compile Include="$(MSBuildThisFileDirectory)System\Environment.Unix.cs" />
1726+
<Compile Include="$(MSBuildThisFileDirectory)System\Environment.Unix.GetFolderPathCore.cs" Condition="'$(TargetsiOS)' != 'true'" />
17261727
<Compile Include="$(MSBuildThisFileDirectory)System\Globalization\CalendarData.Unix.cs" />
17271728
<Compile Include="$(MSBuildThisFileDirectory)System\Globalization\CompareInfo.Unix.cs" />
17281729
<Compile Include="$(MSBuildThisFileDirectory)System\Globalization\CultureData.Unix.cs" />
Lines changed: 250 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,250 @@
1+
// Licensed to the .NET Foundation under one or more agreements.
2+
// The .NET Foundation licenses this file to you under the MIT license.
3+
// See the LICENSE file in the project root for more information.
4+
5+
using System.Collections;
6+
using System.Collections.Generic;
7+
using System.Diagnostics;
8+
using System.IO;
9+
using System.Reflection;
10+
using System.Runtime.InteropServices;
11+
using System.Text;
12+
using System.Threading;
13+
14+
namespace System
15+
{
16+
public static partial class Environment
17+
{
18+
private static Func<string, object>? s_directoryCreateDirectory;
19+
20+
private static string GetFolderPathCore(SpecialFolder folder, SpecialFolderOption option)
21+
{
22+
// Get the path for the SpecialFolder
23+
string path = GetFolderPathCoreWithoutValidation(folder);
24+
Debug.Assert(path != null);
25+
26+
// If we didn't get one, or if we got one but we're not supposed to verify it,
27+
// or if we're supposed to verify it and it passes verification, return the path.
28+
if (path.Length == 0 ||
29+
option == SpecialFolderOption.DoNotVerify ||
30+
Interop.Sys.Access(path, Interop.Sys.AccessMode.R_OK) == 0)
31+
{
32+
return path;
33+
}
34+
35+
// Failed verification. If None, then we're supposed to return an empty string.
36+
// If Create, we're supposed to create it and then return the path.
37+
if (option == SpecialFolderOption.None)
38+
{
39+
return string.Empty;
40+
}
41+
else
42+
{
43+
Debug.Assert(option == SpecialFolderOption.Create);
44+
45+
Func<string, object> createDirectory = LazyInitializer.EnsureInitialized(ref s_directoryCreateDirectory, () =>
46+
{
47+
Type dirType = Type.GetType("System.IO.Directory, System.IO.FileSystem, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a", throwOnError: true)!;
48+
MethodInfo mi = dirType.GetTypeInfo().GetDeclaredMethod("CreateDirectory")!;
49+
return (Func<string, object>)mi.CreateDelegate(typeof(Func<string, object>));
50+
});
51+
createDirectory(path);
52+
53+
return path;
54+
}
55+
}
56+
57+
private static string GetFolderPathCoreWithoutValidation(SpecialFolder folder)
58+
{
59+
// First handle any paths that involve only static paths, avoiding the overheads of getting user-local paths.
60+
// https://www.freedesktop.org/software/systemd/man/file-hierarchy.html
61+
switch (folder)
62+
{
63+
case SpecialFolder.CommonApplicationData: return "/usr/share";
64+
case SpecialFolder.CommonTemplates: return "/usr/share/templates";
65+
#if TARGET_OSX
66+
case SpecialFolder.ProgramFiles: return "/Applications";
67+
case SpecialFolder.System: return "/System";
68+
#endif
69+
}
70+
71+
// All other paths are based on the XDG Base Directory Specification:
72+
// https://specifications.freedesktop.org/basedir-spec/latest/
73+
string? home = null;
74+
try
75+
{
76+
home = PersistedFiles.GetHomeDirectory();
77+
}
78+
catch (Exception exc)
79+
{
80+
Debug.Fail($"Unable to get home directory: {exc}");
81+
}
82+
83+
// Fall back to '/' when we can't determine the home directory.
84+
// This location isn't writable by non-root users which provides some safeguard
85+
// that the application doesn't write data which is meant to be private.
86+
if (string.IsNullOrEmpty(home))
87+
{
88+
home = "/";
89+
}
90+
91+
// TODO: Consider caching (or precomputing and caching) all subsequent results.
92+
// This would significantly improve performance for repeated access, at the expense
93+
// of not being responsive to changes in the underlying environment variables,
94+
// configuration files, etc.
95+
96+
switch (folder)
97+
{
98+
case SpecialFolder.UserProfile:
99+
case SpecialFolder.MyDocuments: // same value as Personal
100+
return home;
101+
case SpecialFolder.ApplicationData:
102+
return GetXdgConfig(home);
103+
case SpecialFolder.LocalApplicationData:
104+
// "$XDG_DATA_HOME defines the base directory relative to which user specific data files should be stored."
105+
// "If $XDG_DATA_HOME is either not set or empty, a default equal to $HOME/.local/share should be used."
106+
string? data = GetEnvironmentVariable("XDG_DATA_HOME");
107+
if (string.IsNullOrEmpty(data) || data[0] != '/')
108+
{
109+
data = Path.Combine(home, ".local", "share");
110+
}
111+
return data;
112+
113+
case SpecialFolder.Desktop:
114+
case SpecialFolder.DesktopDirectory:
115+
return ReadXdgDirectory(home, "XDG_DESKTOP_DIR", "Desktop");
116+
case SpecialFolder.Templates:
117+
return ReadXdgDirectory(home, "XDG_TEMPLATES_DIR", "Templates");
118+
case SpecialFolder.MyVideos:
119+
return ReadXdgDirectory(home, "XDG_VIDEOS_DIR", "Videos");
120+
121+
#if TARGET_OSX
122+
case SpecialFolder.MyMusic:
123+
return Path.Combine(home, "Music");
124+
case SpecialFolder.MyPictures:
125+
return Path.Combine(home, "Pictures");
126+
case SpecialFolder.Fonts:
127+
return Path.Combine(home, "Library", "Fonts");
128+
case SpecialFolder.Favorites:
129+
return Path.Combine(home, "Library", "Favorites");
130+
case SpecialFolder.InternetCache:
131+
return Path.Combine(home, "Library", "Caches");
132+
#else
133+
case SpecialFolder.MyMusic:
134+
return ReadXdgDirectory(home, "XDG_MUSIC_DIR", "Music");
135+
case SpecialFolder.MyPictures:
136+
return ReadXdgDirectory(home, "XDG_PICTURES_DIR", "Pictures");
137+
case SpecialFolder.Fonts:
138+
return Path.Combine(home, ".fonts");
139+
#endif
140+
}
141+
142+
// No known path for the SpecialFolder
143+
return string.Empty;
144+
}
145+
146+
private static string GetXdgConfig(string home)
147+
{
148+
// "$XDG_CONFIG_HOME defines the base directory relative to which user specific configuration files should be stored."
149+
// "If $XDG_CONFIG_HOME is either not set or empty, a default equal to $HOME/.config should be used."
150+
string? config = GetEnvironmentVariable("XDG_CONFIG_HOME");
151+
if (string.IsNullOrEmpty(config) || config[0] != '/')
152+
{
153+
config = Path.Combine(home, ".config");
154+
}
155+
return config;
156+
}
157+
158+
private static string ReadXdgDirectory(string homeDir, string key, string fallback)
159+
{
160+
Debug.Assert(!string.IsNullOrEmpty(homeDir), $"Expected non-empty homeDir");
161+
Debug.Assert(!string.IsNullOrEmpty(key), $"Expected non-empty key");
162+
Debug.Assert(!string.IsNullOrEmpty(fallback), $"Expected non-empty fallback");
163+
164+
string? envPath = GetEnvironmentVariable(key);
165+
if (!string.IsNullOrEmpty(envPath) && envPath[0] == '/')
166+
{
167+
return envPath;
168+
}
169+
170+
// Use the user-dirs.dirs file to look up the right config.
171+
// Note that the docs also highlight a list of directories in which to look for this file:
172+
// "$XDG_CONFIG_DIRS defines the preference-ordered set of base directories to search for configuration files in addition
173+
// to the $XDG_CONFIG_HOME base directory. The directories in $XDG_CONFIG_DIRS should be separated with a colon ':'. If
174+
// $XDG_CONFIG_DIRS is either not set or empty, a value equal to / etc / xdg should be used."
175+
// For simplicity, we don't currently do that. We can add it if/when necessary.
176+
177+
string userDirsPath = Path.Combine(GetXdgConfig(homeDir), "user-dirs.dirs");
178+
if (Interop.Sys.Access(userDirsPath, Interop.Sys.AccessMode.R_OK) == 0)
179+
{
180+
try
181+
{
182+
using (var reader = new StreamReader(userDirsPath))
183+
{
184+
string? line;
185+
while ((line = reader.ReadLine()) != null)
186+
{
187+
// Example lines:
188+
// XDG_DESKTOP_DIR="$HOME/Desktop"
189+
// XDG_PICTURES_DIR = "/absolute/path"
190+
191+
// Skip past whitespace at beginning of line
192+
int pos = 0;
193+
SkipWhitespace(line, ref pos);
194+
if (pos >= line.Length) continue;
195+
196+
// Skip past requested key name
197+
if (string.CompareOrdinal(line, pos, key, 0, key.Length) != 0) continue;
198+
pos += key.Length;
199+
200+
// Skip past whitespace and past '='
201+
SkipWhitespace(line, ref pos);
202+
if (pos >= line.Length - 4 || line[pos] != '=') continue; // 4 for ="" and at least one char between quotes
203+
pos++; // skip past '='
204+
205+
// Skip past whitespace and past first quote
206+
SkipWhitespace(line, ref pos);
207+
if (pos >= line.Length - 3 || line[pos] != '"') continue; // 3 for "" and at least one char between quotes
208+
pos++; // skip past opening '"'
209+
210+
// Skip past relative prefix if one exists
211+
bool relativeToHome = false;
212+
const string RelativeToHomePrefix = "$HOME/";
213+
if (string.CompareOrdinal(line, pos, RelativeToHomePrefix, 0, RelativeToHomePrefix.Length) == 0)
214+
{
215+
relativeToHome = true;
216+
pos += RelativeToHomePrefix.Length;
217+
}
218+
else if (line[pos] != '/') // if not relative to home, must be absolute path
219+
{
220+
continue;
221+
}
222+
223+
// Find end of path
224+
int endPos = line.IndexOf('"', pos);
225+
if (endPos <= pos) continue;
226+
227+
// Got we need. Now extract it.
228+
string path = line.Substring(pos, endPos - pos);
229+
return relativeToHome ?
230+
Path.Combine(homeDir, path) :
231+
path;
232+
}
233+
}
234+
}
235+
catch (Exception exc)
236+
{
237+
// assembly not found, file not found, errors reading file, etc. Just eat everything.
238+
Debug.Fail($"Failed reading {userDirsPath}: {exc}");
239+
}
240+
}
241+
242+
return Path.Combine(homeDir, fallback);
243+
}
244+
245+
private static void SkipWhitespace(string line, ref int pos)
246+
{
247+
while (pos < line.Length && char.IsWhiteSpace(line[pos])) pos++;
248+
}
249+
}
250+
}

0 commit comments

Comments
 (0)